Merge branch 'develop' into push-rules-settings
# Conflicts: # src/component-index.js # src/components/views/rooms/RoomSettings.jspull/21833/head
						commit
						33edeccb43
					
				| 
						 | 
				
			
			@ -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",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,22 +158,55 @@ 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();        
 | 
			
		||||
    },    
 | 
			
		||||
 | 
			
		||||
    onChatClick: function() {
 | 
			
		||||
        // check if there are any existing rooms with just us and them (1:1)
 | 
			
		||||
        // If so, just view that room. If not, create a private room with them.
 | 
			
		||||
| 
						 | 
				
			
			@ -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'
 | 
			
		||||
            });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
        );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 New Issue