mirror of
https://github.com/element-hq/element-web
synced 2024-11-28 12:28:50 +03:00
Merge branch 'develop' into push-rules-settings
# Conflicts: # src/component-index.js # src/components/views/rooms/RoomSettings.js
This commit is contained in:
commit
33edeccb43
40 changed files with 2772 additions and 711 deletions
|
@ -34,7 +34,8 @@
|
|||
"react-dom": "^0.14.2",
|
||||
"react-gemini-scrollbar": "^2.0.1",
|
||||
"sanitize-html": "^1.11.1",
|
||||
"velocity-animate": "^1.2.3"
|
||||
"velocity-animate": "^1.2.3",
|
||||
"velocity-ui-pack": "^1.2.2"
|
||||
},
|
||||
"//deps": "The loader packages are here because webpack in a project that depends on us needs them in this package's node_modules folder",
|
||||
"//depsbuglink": "https://github.com/webpack/webpack/issues/1472",
|
||||
|
|
104
src/PasswordReset.js
Normal file
104
src/PasswordReset.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
|
||||
/**
|
||||
* Allows a user to reset their password on a homeserver.
|
||||
*
|
||||
* This involves getting an email token from the identity server to "prove" that
|
||||
* the client owns the given email address, which is then passed to the password
|
||||
* API on the homeserver in question with the new password.
|
||||
*/
|
||||
class PasswordReset {
|
||||
|
||||
/**
|
||||
* Configure the endpoints for password resetting.
|
||||
* @param {string} homeserverUrl The URL to the HS which has the account to reset.
|
||||
* @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping.
|
||||
*/
|
||||
constructor(homeserverUrl, identityUrl) {
|
||||
this.client = Matrix.createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
idBaseUrl: identityUrl
|
||||
});
|
||||
this.clientSecret = generateClientSecret();
|
||||
this.identityServerDomain = identityUrl.split("://")[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to reset the user's password. This will trigger a side-effect of
|
||||
* sending an email to the provided email address.
|
||||
* @param {string} emailAddress The email address
|
||||
* @param {string} newPassword The new password for the account.
|
||||
* @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
|
||||
*/
|
||||
resetPassword(emailAddress, newPassword) {
|
||||
this.password = newPassword;
|
||||
return this.client.requestEmailToken(emailAddress, this.clientSecret, 1).then((res) => {
|
||||
this.sessionId = res.sid;
|
||||
return res;
|
||||
}, function(err) {
|
||||
if (err.httpStatus) {
|
||||
err.message = err.message + ` (Status ${err.httpStatus})`;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the email link has been clicked by attempting to change the password
|
||||
* for the mxid linked to the email.
|
||||
* @return {Promise} Resolves if the password was reset. Rejects with an object
|
||||
* with a "message" property which contains a human-readable message detailing why
|
||||
* the reset failed, e.g. "There is no mapped matrix user ID for the given email address".
|
||||
*/
|
||||
checkEmailLinkClicked() {
|
||||
return this.client.setPassword({
|
||||
type: "m.login.email.identity",
|
||||
threepid_creds: {
|
||||
sid: this.sessionId,
|
||||
client_secret: this.clientSecret,
|
||||
id_server: this.identityServerDomain
|
||||
}
|
||||
}, this.password).catch(function(err) {
|
||||
if (err.httpStatus === 401) {
|
||||
err.message = "Failed to verify email address: make sure you clicked the link in the email";
|
||||
}
|
||||
else if (err.httpStatus === 404) {
|
||||
err.message = "Your email address does not appear to be associated with a Matrix ID on this Homeserver.";
|
||||
}
|
||||
else if (err.httpStatus) {
|
||||
err.message += ` (Status ${err.httpStatus})`;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// from Angular SDK
|
||||
function generateClientSecret() {
|
||||
var ret = "";
|
||||
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (var i = 0; i < 32; i++) {
|
||||
ret += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
module.exports = PasswordReset;
|
|
@ -152,6 +152,8 @@ class Register extends Signup {
|
|||
} else {
|
||||
if (error.errcode === 'M_USER_IN_USE') {
|
||||
throw new Error("Username in use");
|
||||
} else if (error.errcode == 'M_INVALID_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) {
|
||||
|
|
|
@ -20,6 +20,31 @@ var dis = require("./dispatcher");
|
|||
var encryption = require("./encryption");
|
||||
var Tinter = require("./Tinter");
|
||||
|
||||
|
||||
class Command {
|
||||
constructor(name, paramArgs, runFn) {
|
||||
this.name = name;
|
||||
this.paramArgs = paramArgs;
|
||||
this.runFn = runFn;
|
||||
}
|
||||
|
||||
getCommand() {
|
||||
return "/" + this.name;
|
||||
}
|
||||
|
||||
getCommandWithArgs() {
|
||||
return this.getCommand() + " " + this.paramArgs;
|
||||
}
|
||||
|
||||
run(roomId, args) {
|
||||
return this.runFn.bind(this)(roomId, args);
|
||||
}
|
||||
|
||||
getUsage() {
|
||||
return "Usage: " + this.getCommandWithArgs()
|
||||
}
|
||||
}
|
||||
|
||||
var reject = function(msg) {
|
||||
return {
|
||||
error: msg
|
||||
|
@ -34,22 +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());
|
||||
}),
|
||||
|
||||
// Takes an #rrggbb colourcode and retints the UI (just for debugging)
|
||||
tint: function(room_id, args) {
|
||||
Tinter.tint(args);
|
||||
return success();
|
||||
},
|
||||
// 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: function(room_id, args) {
|
||||
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;
|
||||
|
@ -65,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) {
|
||||
|
@ -88,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) {
|
||||
|
@ -101,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
|
||||
|
@ -135,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
|
||||
|
@ -182,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) {
|
||||
|
@ -194,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) {
|
||||
|
@ -207,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) {
|
||||
|
@ -221,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
|
||||
|
@ -250,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) {
|
||||
|
@ -273,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 = {
|
||||
/**
|
||||
|
@ -298,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) {
|
||||
|
|
|
@ -127,6 +127,11 @@ module.exports = {
|
|||
cached = true;
|
||||
}
|
||||
|
||||
if (!primaryColor) {
|
||||
primaryColor = "#76CFA6"; // Vector green
|
||||
secondaryColor = "#EAF5F0"; // Vector light green
|
||||
}
|
||||
|
||||
if (!secondaryColor) {
|
||||
var x = 0.16; // average weighting factor calculated from vector green & light green
|
||||
var rgb = hexToRgb(primaryColor);
|
||||
|
@ -146,6 +151,13 @@ module.exports = {
|
|||
tertiaryColor = rgbToHex(rgb1);
|
||||
}
|
||||
|
||||
if (colors[0] === primaryColor &&
|
||||
colors[1] === secondaryColor &&
|
||||
colors[2] === tertiaryColor)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
colors = [primaryColor, secondaryColor, tertiaryColor];
|
||||
|
||||
// go through manually fixing up the stylesheets.
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
@ -41,6 +46,7 @@ module.exports.components['views.dialogs.LogoutPrompt'] = require('./components/
|
|||
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');
|
||||
|
@ -64,8 +70,10 @@ module.exports.components['views.rooms.MemberInfo'] = require('./components/view
|
|||
module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList');
|
||||
module.exports.components['views.rooms.MemberTile'] = require('./components/views/rooms/MemberTile');
|
||||
module.exports.components['views.rooms.MessageComposer'] = require('./components/views/rooms/MessageComposer');
|
||||
module.exports.components['views.rooms.PresenceLabel'] = require('./components/views/rooms/PresenceLabel');
|
||||
module.exports.components['views.rooms.RoomHeader'] = require('./components/views/rooms/RoomHeader');
|
||||
module.exports.components['views.rooms.RoomList'] = require('./components/views/rooms/RoomList');
|
||||
module.exports.components['views.rooms.RoomPreviewBar'] = require('./components/views/rooms/RoomPreviewBar');
|
||||
module.exports.components['views.rooms.RoomSettings'] = require('./components/views/rooms/RoomSettings');
|
||||
module.exports.components['views.rooms.RoomTile'] = require('./components/views/rooms/RoomTile');
|
||||
module.exports.components['views.rooms.SearchResultTile'] = require('./components/views/rooms/SearchResultTile');
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -30,6 +30,7 @@ var Registration = require("./login/Registration");
|
|||
var PostRegistration = require("./login/PostRegistration");
|
||||
|
||||
var Modal = require("../../Modal");
|
||||
var Tinter = require("../../Tinter");
|
||||
var sdk = require('../../index');
|
||||
var MatrixTools = require('../../MatrixTools');
|
||||
var linkifyMatrix = require("../../linkify-matrix");
|
||||
|
@ -63,7 +64,7 @@ module.exports = React.createClass({
|
|||
collapse_lhs: false,
|
||||
collapse_rhs: false,
|
||||
ready: false,
|
||||
width: 10000
|
||||
width: 10000,
|
||||
};
|
||||
if (s.logged_in) {
|
||||
if (MatrixClientPeg.get().getRooms().length) {
|
||||
|
@ -233,6 +234,13 @@ module.exports = React.createClass({
|
|||
});
|
||||
this.notifyNewScreen('register');
|
||||
break;
|
||||
case 'start_password_recovery':
|
||||
if (this.state.logged_in) return;
|
||||
this.replaceState({
|
||||
screen: 'forgot_password'
|
||||
});
|
||||
this.notifyNewScreen('forgot_password');
|
||||
break;
|
||||
case 'token_login':
|
||||
if (this.state.logged_in) return;
|
||||
|
||||
|
@ -296,7 +304,7 @@ module.exports = React.createClass({
|
|||
});
|
||||
break;
|
||||
case 'view_room':
|
||||
this._viewRoom(payload.room_id);
|
||||
this._viewRoom(payload.room_id, payload.show_settings);
|
||||
break;
|
||||
case 'view_prev_room':
|
||||
roomIndexDelta = -1;
|
||||
|
@ -349,8 +357,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);
|
||||
|
@ -391,7 +420,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();
|
||||
|
||||
|
@ -411,7 +440,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;
|
||||
}
|
||||
|
@ -420,6 +458,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
|
||||
|
@ -505,7 +546,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,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -559,6 +602,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',
|
||||
|
@ -614,6 +662,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, {
|
||||
|
@ -621,6 +671,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) {
|
||||
|
@ -668,6 +726,10 @@ module.exports = React.createClass({
|
|||
this.showScreen("login");
|
||||
},
|
||||
|
||||
onForgotPasswordClick: function() {
|
||||
this.showScreen("forgot_password");
|
||||
},
|
||||
|
||||
onRegistered: function(credentials) {
|
||||
this.onLoggedIn(credentials);
|
||||
// do post-registration stuff
|
||||
|
@ -706,6 +768,7 @@ module.exports = React.createClass({
|
|||
var CreateRoom = sdk.getComponent('structures.CreateRoom');
|
||||
var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
||||
var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||
var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
|
||||
|
||||
// needs to be before normal PageTypes as you are logged in technically
|
||||
if (this.state.screen == 'post_registration') {
|
||||
|
@ -801,13 +864,21 @@ module.exports = React.createClass({
|
|||
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;
|
||||
|
||||
|
@ -74,6 +78,9 @@ module.exports = React.createClass({
|
|||
syncState: MatrixClientPeg.get().getSyncState(),
|
||||
hasUnsentMessages: this._hasUnsentMessages(room),
|
||||
callState: null,
|
||||
guestsCanJoin: false,
|
||||
readMarkerEventId: room ? room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId) : null,
|
||||
readMarkerGhostEventId: undefined
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -81,6 +88,7 @@ module.exports = React.createClass({
|
|||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
||||
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
|
||||
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
||||
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
||||
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
|
||||
|
@ -88,8 +96,6 @@ module.exports = React.createClass({
|
|||
// xchat-style tab complete, add a colon if tab
|
||||
// completing at the start of the text
|
||||
this.tabComplete = new TabComplete({
|
||||
startingWordSuffix: ": ",
|
||||
wordSuffix: " ",
|
||||
allowLooping: false,
|
||||
autoEnterTabComplete: true,
|
||||
onClickCompletes: true,
|
||||
|
@ -106,15 +112,27 @@ module.exports = React.createClass({
|
|||
// succeeds then great, show the preview (but we still may be able to /join!).
|
||||
if (!this.state.room) {
|
||||
console.log("Attempting to peek into room %s", this.props.roomId);
|
||||
MatrixClientPeg.get().peekInRoom(this.props.roomId).done(function() {
|
||||
MatrixClientPeg.get().peekInRoom(this.props.roomId).done(() => {
|
||||
// we don't need to do anything - JS SDK will emit Room events
|
||||
// which will update the UI.
|
||||
// which will update the UI. We *do* however need to know if we
|
||||
// can join the room so we can fiddle with the UI appropriately.
|
||||
var peekedRoom = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
if (!peekedRoom) {
|
||||
return;
|
||||
}
|
||||
var guestAccessEvent = peekedRoom.currentState.getStateEvents("m.room.guest_access", "");
|
||||
if (!guestAccessEvent) {
|
||||
return;
|
||||
}
|
||||
if (guestAccessEvent.getContent().guest_access === "can_join") {
|
||||
this.setState({
|
||||
guestsCanJoin: true
|
||||
});
|
||||
}
|
||||
}, function(err) {
|
||||
console.error("Failed to peek into room: %s", err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -124,21 +142,22 @@ module.exports = React.createClass({
|
|||
// (We could use isMounted, but facebook have deprecated that.)
|
||||
this.unmounted = true;
|
||||
|
||||
if (this.refs.messagePanel) {
|
||||
// disconnect the D&D event listeners from the message panel. This
|
||||
// is really just for hygiene - the messagePanel is going to be
|
||||
if (this.refs.roomView) {
|
||||
// disconnect the D&D event listeners from the room view. This
|
||||
// is really just for hygiene - we're going to be
|
||||
// deleted anyway, so it doesn't matter if the event listeners
|
||||
// don't get cleaned up.
|
||||
var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel);
|
||||
messagePanel.removeEventListener('drop', this.onDrop);
|
||||
messagePanel.removeEventListener('dragover', this.onDragOver);
|
||||
messagePanel.removeEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||
messagePanel.removeEventListener('dragend', this.onDragLeaveOrEnd);
|
||||
var roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
||||
roomView.removeEventListener('drop', this.onDrop);
|
||||
roomView.removeEventListener('dragover', this.onDragOver);
|
||||
roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||
roomView.removeEventListener('dragend', this.onDragLeaveOrEnd);
|
||||
}
|
||||
dis.unregister(this.dispatcherRef);
|
||||
if (MatrixClientPeg.get()) {
|
||||
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);
|
||||
|
@ -146,6 +165,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
window.removeEventListener('resize', this.onResize);
|
||||
|
||||
Tinter.tint(); // reset colourscheme
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
|
@ -199,6 +220,12 @@ module.exports = React.createClass({
|
|||
|
||||
break;
|
||||
case 'user_activity':
|
||||
case 'user_activity_end':
|
||||
// we could treat user_activity_end differently and not
|
||||
// send receipts for messages that have arrived between
|
||||
// the actual user activity and the time they stopped
|
||||
// being active, but let's see if this is actually
|
||||
// necessary.
|
||||
this.sendReadReceipt();
|
||||
break;
|
||||
}
|
||||
|
@ -259,9 +286,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,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -338,6 +414,14 @@ module.exports = React.createClass({
|
|||
window.addEventListener('resize', this.onResize);
|
||||
this.onResize();
|
||||
|
||||
if (this.refs.roomView) {
|
||||
var roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
||||
roomView.addEventListener('drop', this.onDrop);
|
||||
roomView.addEventListener('dragover', this.onDragOver);
|
||||
roomView.addEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||
roomView.addEventListener('dragend', this.onDragLeaveOrEnd);
|
||||
}
|
||||
|
||||
this._updateTabCompleteList(this.state.room);
|
||||
},
|
||||
|
||||
|
@ -346,7 +430,9 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
this.tabComplete.setCompletionList(
|
||||
MemberEntry.fromMemberList(room.getJoinedMembers())
|
||||
MemberEntry.fromMemberList(room.getJoinedMembers()).concat(
|
||||
CommandEntry.fromCommands(SlashCommands.getCommandList())
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -354,13 +440,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() {
|
||||
|
@ -682,10 +765,10 @@ module.exports = React.createClass({
|
|||
|
||||
var EventTile = sdk.getComponent('rooms.EventTile');
|
||||
|
||||
|
||||
var prevEvent = null; // the last event we showed
|
||||
var readReceiptEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId);
|
||||
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];
|
||||
|
||||
|
@ -699,6 +782,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) {
|
||||
|
@ -735,13 +837,29 @@ module.exports = React.createClass({
|
|||
</li>
|
||||
);
|
||||
|
||||
if (eventId == readReceiptEventId) {
|
||||
ret.push(<hr className="mx_RoomView_myReadMarker" />);
|
||||
// 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;
|
||||
},
|
||||
|
||||
|
@ -769,9 +887,27 @@ 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 != newVals.name && newVals.name != undefined && newVals.name) {
|
||||
if (old_name != newVals.name && newVals.name != undefined) {
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().setRoomName(this.state.room.roomId, newVals.name)
|
||||
);
|
||||
|
@ -819,26 +955,128 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().setGuestAccess(this.state.room.roomId, {
|
||||
allowRead: newVals.guest_read,
|
||||
allowJoin: newVals.guest_join
|
||||
})
|
||||
);
|
||||
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 (newVals.color_scheme) {
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().setRoomAccountData(
|
||||
this.state.room.roomId, "org.matrix.room.color_scheme", newVals.color_scheme
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (old_guest_read != newVals.guest_read ||
|
||||
old_guest_join != newVals.guest_join)
|
||||
{
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().setGuestAccess(this.state.room.roomId, {
|
||||
allowRead: newVals.guest_read,
|
||||
allowJoin: newVals.guest_join
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (deferreds.length) {
|
||||
var self = this;
|
||||
q.all(deferreds).fail(function(err) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to set state",
|
||||
description: err.toString()
|
||||
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: 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,
|
||||
});
|
||||
});
|
||||
}).finally(function() {
|
||||
self.setState({
|
||||
uploadingRoomSettings: false,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
editingRoomSettings: false,
|
||||
|
@ -906,23 +1144,27 @@ module.exports = React.createClass({
|
|||
|
||||
onSaveClick: function() {
|
||||
this.setState({
|
||||
editingRoomSettings: false,
|
||||
uploadingRoomSettings: true,
|
||||
});
|
||||
|
||||
this.uploadNewState({
|
||||
name: this.refs.header.getRoomName(),
|
||||
topic: this.refs.room_settings.getTopic(),
|
||||
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()
|
||||
guest_read: this.refs.room_settings.canGuestsRead(),
|
||||
color_scheme: this.refs.room_settings.getColorScheme(),
|
||||
});
|
||||
},
|
||||
|
||||
onCancelClick: function() {
|
||||
this.updateTint();
|
||||
this.setState({editingRoomSettings: false});
|
||||
},
|
||||
|
||||
|
@ -1070,26 +1312,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;
|
||||
|
||||
// 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
|
||||
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();
|
||||
|
||||
// header + footer + status + give us at least 100px of scrollback at all times.
|
||||
auxPanelMaxHeight = window.innerHeight -
|
||||
(83 + 72 +
|
||||
sdk.getComponent('rooms.MessageComposer').MAX_HEIGHT +
|
||||
100);
|
||||
|
||||
// XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
|
||||
// but it's better than the video going missing entirely
|
||||
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
|
||||
|
||||
video.style.maxHeight = auxPanelMaxHeight + "px";
|
||||
|
||||
// the above might have made the video panel resize itself, so now
|
||||
// we need to tell the gemini panel to adapt.
|
||||
this.onChildResize();
|
||||
}
|
||||
|
||||
// we need to do this for general auxPanels too
|
||||
if (this.refs.auxPanel) {
|
||||
this.refs.auxPanel.style.maxHeight = auxPanelMaxHeight + "px";
|
||||
}
|
||||
},
|
||||
|
||||
onFullscreenClick: function() {
|
||||
|
@ -1132,6 +1380,13 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
showSettings: function(show) {
|
||||
// XXX: this is a bit naughty; we should be doing this via props
|
||||
if (show) {
|
||||
this.setState({editingRoomSettings: true});
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
||||
var MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
||||
|
@ -1140,6 +1395,7 @@ module.exports = React.createClass({
|
|||
var SearchBar = sdk.getComponent("rooms.SearchBar");
|
||||
var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
var RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar");
|
||||
|
||||
if (!this.state.room) {
|
||||
if (this.props.roomId) {
|
||||
|
@ -1281,7 +1537,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");
|
||||
|
@ -1290,6 +1546,12 @@ 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} />
|
||||
);
|
||||
}
|
||||
|
||||
var conferenceCallNotification = null;
|
||||
if (this.state.displayConfCallNotification) {
|
||||
|
@ -1309,7 +1571,7 @@ module.exports = React.createClass({
|
|||
fileDropTarget = <div className="mx_RoomView_fileDropTarget">
|
||||
<div className="mx_RoomView_fileDropTargetLabel" title="Drop File Here">
|
||||
<TintableSvg src="img/upload-big.svg" width="45" height="59"/><br/>
|
||||
Drop File Here
|
||||
Drop file here to upload
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
@ -1410,7 +1672,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}
|
||||
|
@ -1423,8 +1685,8 @@ module.exports = React.createClass({
|
|||
onLeaveClick={
|
||||
(myMember && myMember.membership === "join") ? this.onLeaveClick : null
|
||||
} />
|
||||
{ fileDropTarget }
|
||||
<div className="mx_RoomView_auxPanel">
|
||||
<div className="mx_RoomView_auxPanel" ref="auxPanel">
|
||||
{ fileDropTarget }
|
||||
<CallView ref="callView" room={this.state.room} ConferenceHandler={this.props.ConferenceHandler}
|
||||
onResize={this.onChildResize} />
|
||||
{ conferenceCallNotification }
|
||||
|
|
|
@ -57,7 +57,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
|||
}
|
||||
}
|
||||
if (!upload) {
|
||||
upload = uploads[0];
|
||||
return <div />
|
||||
}
|
||||
|
||||
var innerProgressStyle = {
|
||||
|
|
|
@ -21,6 +21,7 @@ var dis = require("../../dispatcher");
|
|||
var q = require('q');
|
||||
var version = require('../../../package.json').version;
|
||||
var UserSettingsStore = require('../../UserSettingsStore');
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'UserSettings',
|
||||
|
@ -83,6 +84,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;
|
||||
|
@ -172,7 +179,7 @@ module.exports = React.createClass({
|
|||
if (MatrixClientPeg.get().isGuest()) {
|
||||
accountJsx = (
|
||||
<div className="mx_UserSettings_button" onClick={this.onUpgradeClicked}>
|
||||
Upgrade (It's free!)
|
||||
Create an account
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -193,6 +200,8 @@ module.exports = React.createClass({
|
|||
<div className="mx_UserSettings">
|
||||
<RoomHeader simpleHeader="Settings" />
|
||||
|
||||
<GeminiScrollbar className="mx_UserSettings_body" autoshow={true}>
|
||||
|
||||
<h2>Profile</h2>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
|
@ -222,13 +231,15 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
|
||||
<div className="mx_UserSettings_avatarPicker">
|
||||
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
|
||||
showUploadSection={false} className="mx_UserSettings_avatarPicker_img"/>
|
||||
<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>
|
||||
|
@ -238,13 +249,12 @@ module.exports = React.createClass({
|
|||
<h2>Account</h2>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
{accountJsx}
|
||||
</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>
|
||||
|
@ -263,6 +273,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 (
|
||||
|
|
|
@ -159,6 +159,15 @@ module.exports = React.createClass({
|
|||
case "RegistrationForm.ERR_PASSWORD_LENGTH":
|
||||
errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`;
|
||||
break;
|
||||
case "RegistrationForm.ERR_EMAIL_INVALID":
|
||||
errMsg = "This doesn't look like a valid email address";
|
||||
break;
|
||||
case "RegistrationForm.ERR_USERNAME_INVALID":
|
||||
errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores.";
|
||||
break;
|
||||
case "RegistrationForm.ERR_USERNAME_BLANK":
|
||||
errMsg = "You need to enter a user name";
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown error code: %s", errCode);
|
||||
errMsg = "An unknown error occurred.";
|
||||
|
|
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();
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
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();
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
this.setState({
|
||||
urls: this.getImageUrls(newProps)
|
||||
})
|
||||
},
|
||||
|
||||
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();
|
||||
this.setState({
|
||||
imageUrl: this._nextUrl()
|
||||
});
|
||||
}
|
||||
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();
|
||||
|
||||
return (
|
||||
<span>
|
||||
<span className="mx_RoomAvatar_initial" aria-hidden="true"
|
||||
style={{ fontSize: (this.props.width * 0.65) + "px",
|
||||
width: this.props.width + "px",
|
||||
lineHeight: this.props.height + "px" }}>{ initial }</span>
|
||||
<img className="mx_RoomAvatar" src={this.state.imageUrl}
|
||||
onError={this.onError} style={style} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return <img className="mx_RoomAvatar" src={this.state.imageUrl}
|
||||
onError={this.onError} style={style} />
|
||||
}
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
return (
|
||||
<BaseAvatar {...this.props} name={this.props.room.name}
|
||||
idName={this.props.room.roomId} urls={this.state.urls} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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() {
|
||||
this.cancelEdit();
|
||||
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>;
|
||||
} 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>
|
||||
);
|
||||
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 {
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -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,8 +17,15 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var Velocity = require('velocity-animate');
|
||||
require('velocity-ui-pack');
|
||||
var sdk = require('../../../index');
|
||||
|
||||
var FIELD_EMAIL = 'field_email';
|
||||
var FIELD_USERNAME = 'field_username';
|
||||
var FIELD_PASSWORD = 'field_password';
|
||||
var FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a registration form.
|
||||
*/
|
||||
|
@ -50,52 +57,151 @@ 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()
|
||||
// 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);
|
||||
|
||||
var errCode;
|
||||
if (!pwd1 || !pwd2) {
|
||||
errCode = "RegistrationForm.ERR_PASSWORD_MISSING";
|
||||
}
|
||||
else if (pwd1 !== pwd2) {
|
||||
errCode = "RegistrationForm.ERR_PASSWORD_MISMATCH";
|
||||
}
|
||||
else if (pwd1.length < this.props.minPasswordLength) {
|
||||
errCode = "RegistrationForm.ERR_PASSWORD_LENGTH";
|
||||
}
|
||||
if (errCode) {
|
||||
this.props.onError(errCode);
|
||||
return;
|
||||
}
|
||||
|
||||
var promise = this.props.onRegisterClick({
|
||||
username: this.refs.username.value.trim(),
|
||||
password: pwd1,
|
||||
email: this.refs.email.value.trim()
|
||||
});
|
||||
|
||||
if (promise) {
|
||||
ev.target.disabled = true;
|
||||
promise.finally(function() {
|
||||
ev.target.disabled = false;
|
||||
if (this.allFieldsValid()) {
|
||||
var promise = this.props.onRegisterClick({
|
||||
username: this.refs.username.value.trim(),
|
||||
password: this.refs.password.value.trim(),
|
||||
email: this.refs.email.value.trim()
|
||||
});
|
||||
|
||||
if (promise) {
|
||||
ev.target.disabled = true;
|
||||
promise.finally(function() {
|
||||
ev.target.disabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if all fields were valid last time
|
||||
* they were validated.
|
||||
*/
|
||||
allFieldsValid: function() {
|
||||
var keys = Object.keys(this.state.fieldValid);
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
if (this.state.fieldValid[keys[i]] == false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
validateField: function(field_id) {
|
||||
var pwd1 = this.refs.password.value.trim();
|
||||
var pwd2 = this.refs.passwordConfirm.value.trim()
|
||||
|
||||
switch (field_id) {
|
||||
case FIELD_EMAIL:
|
||||
this.markFieldValid(
|
||||
field_id,
|
||||
this.refs.email.value == '' || !!this.refs.email.value.match(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i),
|
||||
"RegistrationForm.ERR_EMAIL_INVALID"
|
||||
);
|
||||
break;
|
||||
case FIELD_USERNAME:
|
||||
// XXX: SPEC-1
|
||||
if (encodeURIComponent(this.refs.username.value) != this.refs.username.value) {
|
||||
this.markFieldValid(
|
||||
field_id,
|
||||
false,
|
||||
"RegistrationForm.ERR_USERNAME_INVALID"
|
||||
);
|
||||
} else if (this.refs.username.value == '') {
|
||||
this.markFieldValid(
|
||||
field_id,
|
||||
false,
|
||||
"RegistrationForm.ERR_USERNAME_BLANK"
|
||||
);
|
||||
} else {
|
||||
this.markFieldValid(field_id, true);
|
||||
}
|
||||
break;
|
||||
case FIELD_PASSWORD:
|
||||
if (pwd1 == '') {
|
||||
this.markFieldValid(
|
||||
field_id,
|
||||
false,
|
||||
"RegistrationForm.ERR_PASSWORD_MISSING"
|
||||
);
|
||||
} else if (pwd1.length < this.props.minPasswordLength) {
|
||||
this.markFieldValid(
|
||||
field_id,
|
||||
false,
|
||||
"RegistrationForm.ERR_PASSWORD_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) {
|
||||
|
@ -111,13 +217,19 @@ module.exports = React.createClass({
|
|||
<br />
|
||||
<input className="mx_Login_field" type="text" ref="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}
|
||||
|
|
|
@ -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));
|
||||
},
|
||||
|
|
|
@ -58,15 +58,16 @@ module.exports = React.createClass({
|
|||
var roomId = this.props.member.roomId;
|
||||
var target = this.props.member.userId;
|
||||
MatrixClientPeg.get().kick(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("Kick success");
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Kick error",
|
||||
description: err.message
|
||||
});
|
||||
});
|
||||
// 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("Kick success");
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Kick error",
|
||||
description: err.message
|
||||
});
|
||||
}
|
||||
);
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
|
@ -74,16 +75,18 @@ 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() {
|
||||
// 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");
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Ban error",
|
||||
description: err.message
|
||||
});
|
||||
});
|
||||
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");
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Ban error",
|
||||
description: err.message
|
||||
});
|
||||
}
|
||||
);
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
|
@ -118,16 +121,17 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
MatrixClientPeg.get().setPowerLevel(roomId, target, level, 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("Mute toggle success");
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Mute error",
|
||||
description: err.message
|
||||
});
|
||||
});
|
||||
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("Mute toggle success");
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Mute error",
|
||||
description: err.message
|
||||
});
|
||||
}
|
||||
);
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
|
@ -154,19 +158,52 @@ 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(
|
||||
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("Mod toggle success");
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Mod error",
|
||||
description: err.message
|
||||
});
|
||||
});
|
||||
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("Mod toggle success");
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
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,20 +246,22 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().createRoom({
|
||||
invite: [this.props.member.userId],
|
||||
preset: "private_chat"
|
||||
}).done(function(res) {
|
||||
self.setState({ creatingRoom: false });
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: res.room_id
|
||||
});
|
||||
self.props.onFinished();
|
||||
}, function(err) {
|
||||
self.setState({ creatingRoom: false });
|
||||
console.error(
|
||||
"Failed to create room: %s", JSON.stringify(err)
|
||||
);
|
||||
self.props.onFinished();
|
||||
});
|
||||
}).done(
|
||||
function(res) {
|
||||
self.setState({ creatingRoom: false });
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: res.room_id
|
||||
});
|
||||
self.props.onFinished();
|
||||
}, function(err) {
|
||||
self.setState({ creatingRoom: false });
|
||||
console.error(
|
||||
"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,35 +390,56 @@ 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_profileField">
|
||||
{ this.props.member.userId }
|
||||
</div>
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
power: { this.props.member.powerLevelNorm }%
|
||||
</div>
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
{interactButton}
|
||||
{muteButton}
|
||||
{kickButton}
|
||||
{banButton}
|
||||
{giveModButton}
|
||||
{spinner}
|
||||
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
{ this.props.member.userId }
|
||||
</div>
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
Level: <b><PowerSelector value={ parseInt(this.props.member.powerLevel) } disabled={ !this.state.can.modifyLevel } onChange={ this.onPowerChange }/></b>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ startChat }
|
||||
|
||||
{ adminTools }
|
||||
|
||||
{ spinner }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,12 +15,19 @@ limitations under the License.
|
|||
*/
|
||||
var React = require('react');
|
||||
var classNames = require('classnames');
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
var q = require('q');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var Modal = require("../../../Modal");
|
||||
var sdk = require('../../../index');
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
|
||||
var INITIAL_LOAD_NUM_MEMBERS = 50;
|
||||
var SHARE_HISTORY_WARNING = "Newly invited users will see the history of this room. "+
|
||||
"If you'd prefer invited users not to see messages that were sent before they joined, "+
|
||||
"turn off, 'Share message history with new users' in the settings for this room.";
|
||||
|
||||
var shown_invite_warning_this_session = false;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberList',
|
||||
|
@ -131,12 +138,41 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
var promise;
|
||||
var invite_defer = q.defer();
|
||||
|
||||
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
var history_visibility = room.currentState.getStateEvents('m.room.history_visibility', '');
|
||||
if (history_visibility) history_visibility = history_visibility.getContent().history_visibility;
|
||||
|
||||
if (history_visibility == 'shared' && !shown_invite_warning_this_session) {
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Warning",
|
||||
description: SHARE_HISTORY_WARNING,
|
||||
button: "Invite",
|
||||
onFinished: function(should_invite) {
|
||||
if (should_invite) {
|
||||
shown_invite_warning_this_session = true;
|
||||
invite_defer.resolve();
|
||||
} else {
|
||||
invite_defer.reject(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
invite_defer.resolve();
|
||||
}
|
||||
|
||||
var promise = invite_defer.promise;;
|
||||
if (isEmailAddress) {
|
||||
promise = MatrixClientPeg.get().inviteByEmail(this.props.roomId, inputText);
|
||||
promise = promise.then(function() {
|
||||
MatrixClientPeg.get().inviteByEmail(self.props.roomId, inputText);
|
||||
});
|
||||
}
|
||||
else {
|
||||
promise = MatrixClientPeg.get().invite(this.props.roomId, inputText);
|
||||
promise = promise.then(function() {
|
||||
MatrixClientPeg.get().invite(self.props.roomId, inputText);
|
||||
});
|
||||
}
|
||||
|
||||
self.setState({
|
||||
|
@ -151,11 +187,13 @@ module.exports = React.createClass({
|
|||
inviting: false
|
||||
});
|
||||
}, function(err) {
|
||||
console.error("Failed to invite: %s", JSON.stringify(err));
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Server error whilst inviting",
|
||||
description: err.message
|
||||
});
|
||||
if (err !== null) {
|
||||
console.error("Failed to invite: %s", JSON.stringify(err));
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Server error whilst inviting",
|
||||
description: err.message
|
||||
});
|
||||
}
|
||||
self.setState({
|
||||
inviting: false
|
||||
});
|
||||
|
@ -229,7 +267,8 @@ module.exports = React.createClass({
|
|||
var MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||
|
||||
var self = this;
|
||||
return self.state.members.filter(function(userId) {
|
||||
|
||||
var memberList = self.state.members.filter(function(userId) {
|
||||
var m = self.memberDict[userId];
|
||||
return m.membership == membership;
|
||||
}).map(function(userId) {
|
||||
|
@ -238,6 +277,31 @@ module.exports = React.createClass({
|
|||
<MemberTile key={userId} member={m} ref={userId} />
|
||||
);
|
||||
});
|
||||
|
||||
if (membership === "invite") {
|
||||
// include 3pid invites (m.room.third_party_invite) state events.
|
||||
// The HS may have already converted these into m.room.member invites so
|
||||
// we shouldn't add them if the 3pid invite state key (token) is in the
|
||||
// member invite (content.third_party_invite.signed.token)
|
||||
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
if (room) {
|
||||
room.currentState.getStateEvents("m.room.third_party_invite").forEach(
|
||||
function(e) {
|
||||
// discard all invites which have a m.room.member event since we've
|
||||
// already added them.
|
||||
var memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey());
|
||||
if (memberEvent) {
|
||||
return;
|
||||
}
|
||||
memberList.push(
|
||||
<MemberTile key={e.getStateKey()} ref={e.getStateKey()}
|
||||
customDisplayName={e.getContent().display_name} />
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return memberList;
|
||||
},
|
||||
|
||||
onPopulateInvite: function(e) {
|
||||
|
@ -254,7 +318,7 @@ module.exports = React.createClass({
|
|||
} else {
|
||||
return (
|
||||
<form onSubmit={this.onPopulateInvite}>
|
||||
<input className="mx_MemberList_invite" ref="invite" placeholder="Invite user (email)"/>
|
||||
<input className="mx_MemberList_invite" ref="invite" id="mx_MemberList_invite" placeholder="Invite user (email)"/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,20 +26,19 @@ var Modal = require("../../../Modal");
|
|||
module.exports = React.createClass({
|
||||
displayName: 'MemberTile',
|
||||
|
||||
propTypes: {
|
||||
member: React.PropTypes.any, // RoomMember
|
||||
onFinished: React.PropTypes.func,
|
||||
customDisplayName: React.PropTypes.string // for 3pid invites
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {};
|
||||
},
|
||||
|
||||
onLeaveClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'leave_room',
|
||||
room_id: this.props.member.roomId,
|
||||
});
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
if (this.state.hover !== nextState.hover) return true;
|
||||
if (!this.props.member) { return false; } // e.g. 3pid members
|
||||
if (
|
||||
this.member_last_modified_time === undefined ||
|
||||
this.member_last_modified_time < nextProps.member.getLastModifiedTime()
|
||||
|
@ -65,117 +64,121 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onClick: function(e) {
|
||||
if (!this.props.member) { return; } // e.g. 3pid members
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_user',
|
||||
member: this.props.member,
|
||||
});
|
||||
},
|
||||
|
||||
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";
|
||||
_getDisplayName: function() {
|
||||
if (this.props.customDisplayName) {
|
||||
return this.props.customDisplayName;
|
||||
}
|
||||
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";
|
||||
return this.props.member.name;
|
||||
},
|
||||
|
||||
getPowerLabel: function() {
|
||||
var label = this.props.member.userId;
|
||||
if (this.state.isTargetMod) {
|
||||
label += " - Mod (" + this.props.member.powerLevelNorm + "%)";
|
||||
if (!this.props.member) {
|
||||
return this._getDisplayName();
|
||||
}
|
||||
var label = this.props.member.userId + " (power " + this.props.member.powerLevel + ")";
|
||||
return label;
|
||||
},
|
||||
|
||||
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 member = this.props.member;
|
||||
var isMyUser = false;
|
||||
var name = this._getDisplayName();
|
||||
var active = -1;
|
||||
var presenceClass = "mx_MemberTile_offline";
|
||||
var mainClassName = "mx_MemberTile ";
|
||||
if (this.props.member.user) {
|
||||
if (this.props.member.user.presence === "online") {
|
||||
presenceClass = "mx_MemberTile_online";
|
||||
|
||||
if (member) {
|
||||
if (member.user) {
|
||||
this.user_last_modified_time = member.user.getLastModifiedTime();
|
||||
|
||||
// FIXME: make presence data update whenever User.presence changes...
|
||||
active = (
|
||||
(Date.now() - (member.user.lastPresenceTs - member.user.lastActiveAgo)) || -1
|
||||
);
|
||||
|
||||
if (member.user.presence === "online") {
|
||||
presenceClass = "mx_MemberTile_online";
|
||||
}
|
||||
else if (member.user.presence === "unavailable") {
|
||||
presenceClass = "mx_MemberTile_unavailable";
|
||||
}
|
||||
}
|
||||
else if (this.props.member.user.presence === "unavailable") {
|
||||
presenceClass = "mx_MemberTile_unavailable";
|
||||
this.member_last_modified_time = member.getLastModifiedTime();
|
||||
isMyUser = MatrixClientPeg.get().credentials.userId == member.userId;
|
||||
|
||||
// if (this.props.member && this.props.member.powerLevelNorm > 0) {
|
||||
// var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png";
|
||||
// power = <img src={ img } className="mx_MemberTile_power" width="44" height="44" alt=""/>;
|
||||
// }
|
||||
|
||||
var power;
|
||||
if (this.props.member) {
|
||||
var powerLevel = this.props.member.powerLevel;
|
||||
if (powerLevel >= 50 && powerLevel < 99) {
|
||||
power = <img src="img/mod.svg" className="mx_MemberTile_power" width="16" height="17" alt="Mod"/>;
|
||||
}
|
||||
if (powerLevel >= 99) {
|
||||
power = <img src="img/admin.svg" className="mx_MemberTile_power" width="16" height="17" alt="Admin"/>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mainClassName = "mx_MemberTile ";
|
||||
mainClassName += presenceClass;
|
||||
if (this.state.hover) {
|
||||
mainClassName += " mx_MemberTile_hover";
|
||||
}
|
||||
|
||||
var name = this.props.member.name;
|
||||
// if (isMyUser) name += " (me)"; // this does nothing other than introduce line wrapping and pain
|
||||
//var leave = isMyUser ? <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 =
|
||||
if (this.state.hover && this.props.member) {
|
||||
var presenceState = (member && member.user) ? member.user.presence : null;
|
||||
var PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
|
||||
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 }
|
||||
<PresenceLabel activeAgo={active}
|
||||
presenceState={presenceState} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
nameEl =
|
||||
nameEl = (
|
||||
<div className="mx_MemberTile_name">
|
||||
{ name }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
|
||||
var av;
|
||||
if (member) {
|
||||
av = (
|
||||
<MemberAvatar member={this.props.member} width={36} height={36} />
|
||||
);
|
||||
}
|
||||
else {
|
||||
av = (
|
||||
<BaseAvatar name={name} width={36} height={36} />
|
||||
);
|
||||
}
|
||||
|
||||
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 }
|
||||
{ av }
|
||||
{ power }
|
||||
</div>
|
||||
{ nameEl }
|
||||
</div>
|
||||
|
|
|
@ -75,7 +75,7 @@ module.exports = React.createClass({
|
|||
|
||||
// a callback which is called when the height of the composer is
|
||||
// changed due to a change in content.
|
||||
onResize: React.PropTypes.function,
|
||||
onResize: React.PropTypes.func,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -209,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();
|
||||
this.resizeInput();
|
||||
}
|
||||
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) {
|
||||
|
@ -341,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'
|
||||
});
|
||||
|
|
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,26 +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">
|
||||
|
@ -87,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 !== '?') {
|
||||
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.
|
||||
|
@ -123,14 +226,48 @@ module.exports = React.createClass({
|
|||
<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) {
|
||||
roomAvatar = (
|
||||
<RoomAvatar room={this.props.room} width="48" height="48" />
|
||||
);
|
||||
if (can_set_room_avatar) {
|
||||
roomAvatar = (
|
||||
<div className="mx_RoomHeader_avatarPicker">
|
||||
<div onClick={ this.onAvatarPickerClick }>
|
||||
<ChangeAvatar ref="changeAvatar" room={this.props.room} showUploadSection={false} width={48} height={48} />
|
||||
</div>
|
||||
<div className="mx_RoomHeader_avatarPicker_edit">
|
||||
<label htmlFor="avatarInput" ref="file_label">
|
||||
<img src="img/camera.svg"
|
||||
alt="Upload avatar" title="Upload avatar"
|
||||
width="17" height="15" />
|
||||
</label>
|
||||
<input id="avatarInput" type="file" onChange={ this.onAvatarSelected }/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
roomAvatar = (
|
||||
<div onClick={this.props.onSettingsClick}>
|
||||
<RoomAvatar room={this.props.room} width={48} height={48}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var leave_button;
|
||||
|
@ -149,6 +286,18 @@ module.exports = React.createClass({
|
|||
</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>;
|
||||
}
|
||||
|
||||
header =
|
||||
<div className="mx_RoomHeader_wrapper">
|
||||
<div className="mx_RoomHeader_leftRow">
|
||||
|
@ -160,20 +309,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" onClick={this.props.onSearchClick} title="Search">
|
||||
<TintableSvg src="img/search.svg" width="21" height="19"/>
|
||||
</div>
|
||||
</div>
|
||||
{cancel_button}
|
||||
{right_row}
|
||||
</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomHeader">
|
||||
<div className={ "mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "") }>
|
||||
{ header }
|
||||
</div>
|
||||
);
|
||||
|
|
56
src/components/views/rooms/RoomPreviewBar.js
Normal file
56
src/components/views/rooms/RoomPreviewBar.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');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomPreviewBar',
|
||||
|
||||
propTypes: {
|
||||
onJoinClick: React.PropTypes.func,
|
||||
canJoin: React.PropTypes.bool
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onJoinClick: function() {},
|
||||
canJoin: false
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var joinBlock;
|
||||
|
||||
if (this.props.canJoin) {
|
||||
joinBlock = (
|
||||
<div className="mx_RoomPreviewBar_join_text">
|
||||
Would you like to <a onClick={this.props.onJoinClick}>join</a> this room?
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomPreviewBar">
|
||||
<div className="mx_RoomPreviewBar_preview_text">
|
||||
This is a preview of this room. Room interactions have been disabled.
|
||||
</div>
|
||||
{joinBlock}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -16,21 +16,98 @@ 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,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
// XXX: dirty hack to gutwrench to focus on the invite box
|
||||
if (this.props.room.getJoinedMembers().length == 1) {
|
||||
var inviteBox = document.getElementById("mx_MemberList_invite");
|
||||
if (inviteBox) setTimeout(function() { inviteBox.focus(); }, 0);
|
||||
}
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
|
@ -40,7 +117,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getTopic: function() {
|
||||
return this.refs.topic.value;
|
||||
return this.refs.topic ? this.refs.topic.value : "";
|
||||
},
|
||||
|
||||
getJoinRules: function() {
|
||||
|
@ -62,13 +139,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,
|
||||
};
|
||||
|
@ -76,17 +153,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 alias format",
|
||||
description: "'" + alias + "' is not a valid format for an alias",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
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;
|
||||
|
@ -108,7 +399,10 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
var events_levels = power_levels.events || {};
|
||||
var events_levels = (power_levels ? power_levels.events : {}) || {};
|
||||
|
||||
var user_id = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
|
||||
if (power_levels) {
|
||||
power_levels = power_levels.getContent();
|
||||
|
@ -127,8 +421,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;
|
||||
|
||||
|
@ -157,117 +449,308 @@ 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 change_avatar;
|
||||
if (can_set_room_avatar) {
|
||||
change_avatar = <div>
|
||||
<h3>Room Icon</h3>
|
||||
<ChangeAvatar room={this.props.room} />
|
||||
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">
|
||||
This room can be found elsewhere as:
|
||||
</div>
|
||||
<div className="mx_RoomSettings_aliasesTable">
|
||||
{ remote_domains.map(function(state_key, i) {
|
||||
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 className="mx_RoomSettings_deleteAlias">
|
||||
</div>
|
||||
</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 set</option>
|
||||
</select>
|
||||
}
|
||||
else {
|
||||
canonical_alias_section = <b>{ canonical_alias || "not set" }</b>;
|
||||
}
|
||||
|
||||
var aliases_section =
|
||||
<div>
|
||||
<h3>Directory</h3>
|
||||
<div className="mx_RoomSettings_aliasLabel">
|
||||
{ this.state.aliases[domain].length
|
||||
? "This room can be found on " + domain + " as:"
|
||||
: "This room is not findable on " + domain }
|
||||
</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 alias (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 alias (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 }
|
||||
|
||||
<div className="mx_RoomSettings_aliasLabel">The official way to refer to this room is: { canonical_alias_section }</div>
|
||||
</div>;
|
||||
|
||||
var room_colors_section =
|
||||
<div>
|
||||
<h3>Room Colour</h3>
|
||||
<div className="mx_RoomSettings_roomColors">
|
||||
{room_colors.map(function(room_color, i) {
|
||||
var selected;
|
||||
if (i === self.state.color_scheme_index) {
|
||||
selected =
|
||||
<div className="mx_RoomSettings_roomColor_selected">
|
||||
<img src="img/tick.svg" width="17" height="14" alt="./"/>
|
||||
</div>
|
||||
}
|
||||
var boundClick = self.onColorSchemeChanged.bind(self, i)
|
||||
return (
|
||||
<div className="mx_RoomSettings_roomColor"
|
||||
key={ "room_color_" + i }
|
||||
style={{ backgroundColor: room_color[1] }}
|
||||
onClick={ boundClick }>
|
||||
{ selected }
|
||||
<div className="mx_RoomSettings_roomColorPrimary" style={{ backgroundColor: room_color[0] }}></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
var user_levels_section;
|
||||
if (user_levels.length) {
|
||||
user_levels_section =
|
||||
<div>
|
||||
<div>
|
||||
Users with specific roles are:
|
||||
</div>
|
||||
<div>
|
||||
{Object.keys(user_levels).map(function(user, i) {
|
||||
return (
|
||||
<div className="mx_RoomSettings_userLevel" key={user}>
|
||||
{ user } is a
|
||||
<PowerSelector value={ user_levels[user] } disabled={true}/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
else {
|
||||
user_levels_section = <div>No users have specific privileges in this room.</div>
|
||||
}
|
||||
|
||||
var banned = this.props.room.getMembersWithMembership("ban");
|
||||
var banned_users_section;
|
||||
if (banned.length) {
|
||||
banned_users_section =
|
||||
<div>
|
||||
<h3>Banned users</h3>
|
||||
<div className="mx_RoomSettings_banned">
|
||||
{banned.map(function(member, i) {
|
||||
return (
|
||||
<div key={i}>
|
||||
{member.userId}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</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">
|
||||
This room is 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>
|
||||
|
||||
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>
|
||||
<input type="checkbox" ref="guests_read" defaultChecked={history_visibility === "world_readable"}/>
|
||||
Allow guests to read messages in this room
|
||||
</label> <br/>
|
||||
<label>
|
||||
<input type="checkbox" ref="guests_join" defaultChecked={guest_access === "can_join"}/>
|
||||
Allow guests to join this room
|
||||
</label> <br/>
|
||||
<label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label> <br/>
|
||||
<label><input type="checkbox" ref="guests_read" defaultChecked={history_visibility === "world_readable"}/> Allow guests to read messages in this room</label> <br/>
|
||||
<label><input type="checkbox" ref="guests_join" defaultChecked={guest_access === "can_join"}/> Allow guests to join this room</label> <br/>
|
||||
<label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label>
|
||||
|
||||
{ tags_section }
|
||||
|
||||
{ room_colors_section }
|
||||
|
||||
{ aliases_section }
|
||||
|
||||
<h3>Notifications</h3>
|
||||
<div className="mx_RoomSettings_settings">
|
||||
<label><input type="checkbox" ref="are_notifications_muted" defaultChecked={are_notifications_muted}/> Mute notifications for this room</label>
|
||||
</div>
|
||||
|
||||
<h3>Power levels</h3>
|
||||
<div className="mx_RoomSettings_power_levels mx_RoomSettings_settings">
|
||||
<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}/>
|
||||
<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>
|
||||
<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 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>
|
||||
<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 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>
|
||||
<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 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>
|
||||
<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 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>
|
||||
<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 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>
|
||||
<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 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>
|
||||
</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 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>
|
||||
|
||||
<h3>Banned users</h3>
|
||||
<div className="mx_RoomSettings_banned">
|
||||
{banned.map(function(member, i) {
|
||||
return (
|
||||
<div key={i}>
|
||||
{member.userId}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<h3>Users</h3>
|
||||
<div className="mx_RoomSettings_userLevels mx_RoomSettings_settings">
|
||||
<div>
|
||||
Your role in this room is currently <b><PowerSelector room={ this.props.room } value={current_user_level} disabled={true}/></b>.
|
||||
</div>
|
||||
|
||||
{ user_levels_section }
|
||||
</div>
|
||||
{change_avatar}
|
||||
|
||||
{ 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 }
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
|
||||
var React = require('react');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var CommandEntry = require("../../../TabCompleteEntries").CommandEntry;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'TabCompleteBar',
|
||||
|
@ -31,8 +32,9 @@ module.exports = React.createClass({
|
|||
<div className="mx_TabCompleteBar">
|
||||
{this.props.entries.map(function(entry, i) {
|
||||
return (
|
||||
<div key={entry.getKey() || i + ""} className="mx_TabCompleteBar_item"
|
||||
onClick={entry.onClick.bind(entry)} >
|
||||
<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">
|
||||
{entry.getText()}
|
||||
|
|
|
@ -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,15 @@ 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} />;
|
||||
// FIXME: surely we should be using MemberAvatar or UserAvatar or something here...
|
||||
avatarImg = <img className="mx_RoomAvatar" 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} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
// a callback which is called when the video within the callview
|
||||
// due to a change in video metadata
|
||||
onResize: React.PropTypes.function,
|
||||
onResize: React.PropTypes.func,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
|
|
@ -24,7 +24,7 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
// a callback which is called when the video element is resized
|
||||
// due to a change in video metadata
|
||||
onResize: React.PropTypes.function,
|
||||
onResize: React.PropTypes.func,
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
|
|
Loading…
Reference in a new issue