Merge remote-tracking branch 'origin/experimental' into travis/fix-memberlist-order

This commit is contained in:
Travis Ralston 2019-01-03 20:01:28 -07:00
commit cc8fa7911b
46 changed files with 788 additions and 193 deletions

View File

@ -30,7 +30,7 @@ popd
if [ "$TRAVIS_BRANCH" = "develop" ]
then
# run end to end tests
git clone https://github.com/matrix-org/matrix-react-end-to-end-tests.git --branch master
scripts/fetchdep.sh matrix-org matrix-react-end-to-end-tests master
pushd matrix-react-end-to-end-tests
ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web
# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh

View File

@ -124,8 +124,9 @@
"eslint-plugin-flowtype": "^2.30.0",
"eslint-plugin-react": "^7.7.0",
"estree-walker": "^0.5.0",
"expect": "^1.16.0",
"expect": "^23.6.0",
"flow-parser": "^0.57.3",
"jest-mock": "^23.2.0",
"karma": "^3.0.0",
"karma-chrome-launcher": "^0.2.3",
"karma-cli": "^1.0.1",

View File

@ -25,8 +25,10 @@
@import "./structures/_ViewSource.scss";
@import "./structures/login/_Login.scss";
@import "./views/avatars/_BaseAvatar.scss";
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
@import "./views/context_menus/_MessageContextMenu.scss";
@import "./views/context_menus/_RoomTileContextMenu.scss";
@import "./views/context_menus/_StatusMessageContextMenu.scss";
@import "./views/context_menus/_TagTileContextMenu.scss";
@import "./views/context_menus/_TopLeftMenu.scss";
@import "./views/dialogs/_BugReportDialog.scss";
@ -35,7 +37,6 @@
@import "./views/dialogs/_ChatInviteDialog.scss";
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
@import "./views/dialogs/_CreateGroupDialog.scss";
@import "./views/dialogs/_CreateKeyBackupDialog.scss";
@import "./views/dialogs/_CreateRoomDialog.scss";
@import "./views/dialogs/_DeactivateAccountDialog.scss";
@import "./views/dialogs/_DevtoolsDialog.scss";

View File

@ -14,12 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_CreateKeyBackupDialog {
padding-right: 40px;
}
.mx_CreateKeyBackupDialog_recoveryKey {
padding: 20px;
color: $info-plinth-fg-color;
background-color: $info-plinth-bg-color;
.mx_MemberStatusMessageAvatar_hasStatus {
border: 2px solid $accent-color;
border-radius: 40px;
padding-right: 0 !important; /* Override AccessibleButton styling */
}

View File

@ -0,0 +1,55 @@
/*
Copyright 2018 New Vector 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.
*/
.mx_StatusMessageContextMenu_message {
display: inline-block;
border-radius: 3px 0 0 3px;
border: 1px solid $input-border-color;
font-size: 13px;
padding: 7px 7px 7px 9px;
width: 135px;
background-color: $primary-bg-color !important;
}
.mx_StatusMessageContextMenu_submit {
display: inline-block;
}
.mx_StatusMessageContextMenu_submitFaded {
opacity: 0.5;
}
.mx_StatusMessageContextMenu_submit img {
vertical-align: middle;
margin-left: 8px;
}
.mx_StatusMessageContextMenu hr {
border: 0.5px solid $menu-border-color;
}
.mx_StatusMessageContextMenu_clearIcon {
margin: 5px 15px 5px 5px;
vertical-align: middle;
}
.mx_StatusMessageContextMenu_clear {
padding: 2px;
}
.mx_StatusMessageContextMenu_hasStatus .mx_StatusMessageContextMenu_clear {
color: $warning-color;
}

View File

@ -13,7 +13,11 @@ 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.
*/
.mx_CreateKeyBackupDialog {
padding-right: 40px;
}
.mx_CreateKeyBackupDialog_primaryContainer {
/*FIXME: plinth colour in new theme(s). background-color: $accent-color;*/
padding: 20px
@ -25,9 +29,13 @@ limitations under the License.
display: block;
}
.mx_CreateKeyBackupDialog_passPhraseContainer {
display: flex;
align-items: start;
}
.mx_CreateKeyBackupDialog_passPhraseHelp {
float: right;
width: 230px;
flex: 1;
height: 85px;
margin-left: 20px;
font-size: 80%;
@ -38,20 +46,36 @@ limitations under the License.
}
.mx_CreateKeyBackupDialog_passPhraseInput {
flex: none;
width: 250px;
border: 1px solid $accent-color;
border-radius: 5px;
padding: 10px;
margin-bottom: 1em;
}
.mx_CreateKeyBackupDialog_passPhraseMatch {
float: right;
margin-left: 20px;
}
.mx_CreateKeyBackupDialog_recoveryKeyButtons {
float: right;
.mx_CreateKeyBackupDialog_recoveryKeyHeader {
margin-bottom: 1em;
}
.mx_CreateKeyBackupDialog_recoveryKeyContainer {
display: flex;
}
.mx_CreateKeyBackupDialog_recoveryKey {
width: 300px;
width: 262px;
padding: 20px;
color: $info-plinth-fg-color;
background-color: $info-plinth-bg-color;
margin-right: 12px;
}
.mx_CreateKeyBackupDialog_recoveryKeyButtons {
flex: 1;
display: flex;
align-items: center;
}

View File

@ -107,3 +107,10 @@ limitations under the License.
}
*/
.mx_EntityTile_subtext {
font-size: 11px;
opacity: 0.5;
overflow: hidden;
white-space: nowrap;
text-overflow: clip;
}

View File

@ -132,6 +132,13 @@ limitations under the License.
margin-left: 8px;
}
.mx_MemberInfo_statusMessage {
font-size: 11px;
opacity: 0.5;
overflow: hidden;
white-space: nowrap;
text-overflow: clip;
}
.mx_MemberInfo .mx_MemberInfo_scrollContainer {
flex: 1;
}

View File

@ -48,15 +48,48 @@ limitations under the License.
left: -12px;
}
.mx_RoomTile_avatar {
flex: 0;
padding: 4px;
.mx_RoomTile_nameContainer {
display: flex;
align-items: center;
flex: 1;
vertical-align: middle;
}
.mx_RoomTile_labelContainer {
display: flex;
flex-direction: column;
flex: 1;
}
.mx_RoomTile_subtext {
display: inline-block;
font-size: 11px;
padding: 0 0 0 7px;
margin: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: clip;
position: relative;
bottom: 4px;
}
.mx_RoomTile_avatar_container {
position: relative;
}
.mx_RoomTile_avatar {
flex: 0;
padding: 4px;
width: 24px;
vertical-align: middle;
}
.mx_RoomTile_hasSubtext .mx_RoomTile_avatar {
padding-top: 0;
vertical-align: super;
}
.mx_RoomTile_dm {
display: block;
position: absolute;
@ -69,7 +102,7 @@ limitations under the License.
flex: 1 5 auto;
font-size: 14px;
font-weight: 600;
padding: 6px;
padding: 0 6px;
color: $roomtile-name-color;
white-space: nowrap;
overflow-x: hidden;

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.5 (67469) - http://www.bohemiancoding.com/sketch -->
<title>Tick</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Custom-Status-Copy" transform="translate(-529.000000, -917.000000)" fill-rule="nonzero">
<g id="Tick" transform="translate(530.000000, 918.000000)">
<circle id="Oval" stroke="#6AAC8C" fill="#75CFA6" cx="9" cy="9" r="9"></circle>
<g id="Glyph" transform="translate(8.949747, 7.949747) rotate(-45.000000) translate(-8.949747, -7.949747) translate(4.449747, 5.449747)" fill="#FFFFFF">
<rect id="Rectangle" x="0" y="0" width="2" height="5"></rect>
<rect id="Rectangle" x="0" y="3" width="9" height="2"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -2,6 +2,9 @@
org="$1"
repo="$2"
defbranch="$3"
[ -z "$defbranch" ] && defbranch="develop"
rm -r "$repo" || true
@ -20,5 +23,5 @@ clone $TRAVIS_PULL_REQUEST_BRANCH
clone $TRAVIS_BRANCH
# Try the current branch from Jenkins.
clone `"echo $GIT_BRANCH" | sed -e 's/^origin\///'`
# Use develop as the last resort.
clone develop
# Use the default branch as the last resort.
clone $defbranch

View File

@ -204,6 +204,19 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
const data = await client.login(loginType, loginParams);
const wellknown = data.well_known;
if (wellknown) {
if (wellknown["m.homeserver"] && wellknown["m.homeserver"]["base_url"]) {
hsUrl = wellknown["m.homeserver"]["base_url"];
console.log(`Overrode homeserver setting with ${hsUrl} from login response`);
}
if (wellknown["m.identity_server"] && wellknown["m.identity_server"]["base_url"]) {
// TODO: should we prompt here?
isUrl = wellknown["m.identity_server"]["base_url"];
console.log(`Overrode IS setting with ${isUrl} from login response`);
}
}
return {
homeserverUrl: hsUrl,
identityServerUrl: isUrl,

View File

@ -289,11 +289,6 @@ const Notifier = {
const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
if (actions && actions.notify) {
dis.dispatch({
action: "event_notification",
event: ev,
room: room,
});
if (this.isEnabled()) {
this._displayPopupNotification(ev, room);
}

View File

@ -392,7 +392,7 @@ class Tinter {
// XXX: we could just move this all into TintableSvg, but as it's so similar
// to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
// keeping it here for now.
calcSvgFixups(svgs, forceColors) {
calcSvgFixups(svgs) {
// go through manually fixing up SVG colours.
// we could do this by stylesheets, but keeping the stylesheets
// updated would be a PITA, so just brute-force search for the
@ -420,21 +420,13 @@ class Tinter {
const tag = tags[j];
for (let k = 0; k < this.svgAttrs.length; k++) {
const attr = this.svgAttrs[k];
for (let m = 0; m < this.keyHex.length; m++) { // dev note: don't use L please.
// We use a different attribute from the one we're setting
// because we may also be using forceColors. If we were to
// check the keyHex against a forceColors value, it may not
// match and therefore not change when we need it to.
const valAttrName = "mx-val-" + attr;
let attribute = tag.getAttribute(valAttrName);
if (!attribute) attribute = tag.getAttribute(attr); // fall back to the original
if (attribute && (attribute.toUpperCase() === this.keyHex[m] || attribute.toLowerCase() === this.keyRgb[m])) {
for (let l = 0; l < this.keyHex.length; l++) {
if (tag.getAttribute(attr) &&
tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) {
fixups.push({
node: tag,
attr: attr,
refAttr: valAttrName,
index: m,
forceColors: forceColors,
index: l,
});
}
}
@ -450,9 +442,7 @@ class Tinter {
if (DEBUG) console.log("applySvgFixups start for " + fixups);
for (let i = 0; i < fixups.length; i++) {
const svgFixup = fixups[i];
const forcedColor = svgFixup.forceColors ? svgFixup.forceColors[svgFixup.index] : null;
svgFixup.node.setAttribute(svgFixup.attr, forcedColor ? forcedColor : this.colors[svgFixup.index]);
svgFixup.node.setAttribute(svgFixup.refAttr, this.colors[svgFixup.index]);
svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]);
}
if (DEBUG) console.log("applySvgFixups end");
}

View File

@ -239,17 +239,19 @@ export default React.createClass({
<p>{_t("You'll need it if you log out or lose access to this device.")}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseHelp">
{strengthMeter}
{helpText}
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
<input type="password"
onChange={this._onPassPhraseChange}
onKeyPress={this._onPassPhraseKeyPress}
value={this.state.passPhrase}
className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Enter a passphrase...")}
/>
<div className="mx_CreateKeyBackupDialog_passPhraseHelp">
{strengthMeter}
{helpText}
</div>
</div>
<input type="password"
onChange={this._onPassPhraseChange}
onKeyPress={this._onPassPhraseKeyPress}
value={this.state.passPhrase}
className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Enter a passphrase...")}
/>
</div>
<DialogButtons primaryButton={_t('Next')}
@ -317,16 +319,18 @@ export default React.createClass({
"somewhere safe.",
)}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
{passPhraseMatch}
<div>
<input type="password"
onChange={this._onPassPhraseConfirmChange}
onKeyPress={this._onPassPhraseConfirmKeyPress}
value={this.state.passPhraseConfirm}
className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Repeat your passphrase...")}
autoFocus={true}
/>
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
<div>
<input type="password"
onChange={this._onPassPhraseConfirmChange}
onKeyPress={this._onPassPhraseConfirmKeyPress}
value={this.state.passPhraseConfirm}
className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Repeat your passphrase...")}
autoFocus={true}
/>
</div>
{passPhraseMatch}
</div>
</div>
<DialogButtons primaryButton={_t('Next')}
@ -351,21 +355,21 @@ export default React.createClass({
<p>{_t("Make a copy of this Recovery Key and keep it safe.")}</p>
<p>{bodyText}</p>
<p className="mx_CreateKeyBackupDialog_primaryContainer">
<div>{_t("Your Recovery Key")}</div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
<button onClick={this._onCopyClick}>
{_t("Copy to clipboard")}
</button>
{
// FIXME REDESIGN: buttons should be adjacent but insufficient room in current design
}
<br /><br />
<button onClick={this._onDownloadClick}>
{_t("Download")}
</button>
<div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
{_t("Your Recovery Key")}
</div>
<div className="mx_CreateKeyBackupDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._keyBackupInfo.recovery_key}</code>
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._keyBackupInfo.recovery_key}</code>
</div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
<button className="mx_Dialog_primary" onClick={this._onCopyClick}>
{_t("Copy to clipboard")}
</button>
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
{_t("Download")}
</button>
</div>
</div>
</p>
<br />

View File

@ -91,11 +91,15 @@ class HomePage extends React.Component {
this._unmounted = true;
}
onLoginClick() {
onLoginClick(ev) {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({ action: 'start_login' });
}
onRegisterClick() {
onRegisterClick(ev) {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({ action: 'start_registration' });
}

View File

@ -927,6 +927,10 @@ export default React.createClass({
},
_viewHome: function() {
// The home page requires the "logged in" view, so we'll set that.
this.setStateForNewView({
view: VIEWS.LOGGED_IN,
});
this._setPage(PageTypes.HomePage);
this.notifyNewScreen('home');
},
@ -1183,10 +1187,7 @@ export default React.createClass({
* @param {string} teamToken
*/
_onLoggedIn: async function(teamToken) {
this.setState({
view: VIEWS.LOGGED_IN,
});
this.setStateForNewView({view: VIEWS.LOGGED_IN});
if (teamToken) {
// A team member has logged in, not a guest
this._teamToken = teamToken;

View File

@ -163,6 +163,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
MatrixClientPeg.get().on("accountData", this.onAccountData);
MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus);
this._fetchMediaConfig();
// Start listening for RoomViewStore updates
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
@ -451,6 +452,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
MatrixClientPeg.get().removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus);
}
window.removeEventListener('beforeunload', this.onPageUnload);
@ -620,6 +622,11 @@ module.exports = React.createClass({
false,
);
}
},
onKeyBackupStatus() {
// Key backup status changes affect whether the in-room recovery
// reminder is displayed.
this.forceUpdate();
},

View File

@ -162,6 +162,18 @@ module.exports = React.createClass({
this.setState(newState);
},
onLoginClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
this.props.onLoginClick();
},
onRegisterClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
this.props.onRegisterClick();
},
showErrorDialog: function(body, title) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, {
@ -253,10 +265,10 @@ module.exports = React.createClass({
</form>
{ serverConfigSection }
{ errorText }
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
<a className="mx_Login_create" onClick={this.onLoginClick} href="#">
{ _t('Return to login screen') }
</a>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
<a className="mx_Login_create" onClick={this.onRegisterClick} href="#">
{ _t('Create an account') }
</a>
<LanguageSelector />

View File

@ -214,7 +214,10 @@ module.exports = React.createClass({
}).done();
},
_onLoginAsGuestClick: function() {
_onLoginAsGuestClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
const self = this;
self.setState({
busy: true,
@ -297,6 +300,12 @@ module.exports = React.createClass({
});
},
onRegisterClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
this.props.onRegisterClick();
},
_tryWellKnownDiscovery: async function(serverName) {
if (!serverName.trim()) {
// Nothing to discover
@ -567,7 +576,7 @@ module.exports = React.createClass({
{ errorTextSection }
{ this.componentForStep(this.state.currentFlow) }
{ serverConfig }
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
<a className="mx_Login_create" onClick={this.onRegisterClick} href="#">
{ _t('Create an account') }
</a>
{ loginAsGuestJsx }

View File

@ -363,6 +363,12 @@ module.exports = React.createClass({
}
},
onLoginClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
this.props.onLoginClick();
},
_makeRegisterRequest: function(auth) {
// Only send the bind params if we're sending username / pw params
// (Since we need to send no params at all to use the ones saved in the
@ -468,7 +474,7 @@ module.exports = React.createClass({
let signIn;
if (!this.state.doingUIAuth) {
signIn = (
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
<a className="mx_Login_create" onClick={this.onLoginClick} href="#">
{ theme === 'status' ? _t('Sign in') : _t('I already have an account') }
</a>
);

View File

@ -0,0 +1,120 @@
/*
Copyright 2018 New Vector 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.
*/
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import AccessibleButton from '../elements/AccessibleButton';
import MemberAvatar from '../avatars/MemberAvatar';
import classNames from 'classnames';
import * as ContextualMenu from "../../structures/ContextualMenu";
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
import SettingsStore from "../../../settings/SettingsStore";
export default class MemberStatusMessageAvatar extends React.Component {
static propTypes = {
member: PropTypes.object.isRequired,
width: PropTypes.number,
height: PropTypes.number,
resizeMethod: PropTypes.string,
};
static defaultProps = {
width: 40,
height: 40,
resizeMethod: 'crop',
};
constructor(props, context) {
super(props, context);
}
componentWillMount() {
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
}
}
componentDidMount() {
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
if (this.props.member.user) {
this.setState({message: this.props.member.user._unstable_statusMessage});
} else {
this.setState({message: ""});
}
}
componentWillUnmount() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
}
}
_onRoomStateEvents = (ev, state) => {
if (ev.getStateKey() !== MatrixClientPeg.get().getUserId()) return;
if (ev.getType() !== "im.vector.user_status") return;
// TODO: We should be relying on `this.props.member.user._unstable_statusMessage`
// We don't currently because the js-sdk doesn't emit a specific event for this
// change, and we don't want to race it. This should be improved when we rip out
// the im.vector.user_status stuff and replace it with a complete solution.
this.setState({message: ev.getContent()["status"]});
};
_onClick = (e) => {
e.stopPropagation();
const elementRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const x = (elementRect.left + window.pageXOffset) - (elementRect.width / 2) + 3;
const chevronOffset = 12;
let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron
ContextualMenu.createMenu(StatusMessageContextMenu, {
chevronOffset: chevronOffset,
chevronFace: 'bottom',
left: x,
top: y,
menuWidth: 190,
user: this.props.member.user,
});
};
render() {
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
return <MemberAvatar member={this.props.member}
width={this.props.width}
height={this.props.height}
resizeMethod={this.props.resizeMethod} />;
}
const hasStatus = this.props.member.user ? !!this.props.member.user._unstable_statusMessage : false;
const classes = classNames({
"mx_MemberStatusMessageAvatar": true,
"mx_MemberStatusMessageAvatar_hasStatus": hasStatus,
});
return <AccessibleButton onClick={this._onClick} className={classes} element="div">
<MemberAvatar member={this.props.member}
width={this.props.width}
height={this.props.height}
resizeMethod={this.props.resizeMethod} />
</AccessibleButton>;
}
}

View File

@ -0,0 +1,86 @@
/*
Copyright 2018 New Vector 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.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import AccessibleButton from '../elements/AccessibleButton';
import classNames from 'classnames';
export default class StatusMessageContextMenu extends React.Component {
static propTypes = {
// js-sdk User object. Not required because it might not exist.
user: PropTypes.object,
};
constructor(props, context) {
super(props, context);
this.state = {
message: props.user ? props.user._unstable_statusMessage : "",
};
}
_onClearClick = async(e) => {
await MatrixClientPeg.get()._unstable_setStatusMessage("");
this.setState({message: ""});
};
_onSubmit = (e) => {
e.preventDefault();
MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message);
};
_onStatusChange = (e) => {
this.setState({message: e.target.value});
};
render() {
const formSubmitClasses = classNames({
"mx_StatusMessageContextMenu_submit": true,
"mx_StatusMessageContextMenu_submitFaded": !this.state.message, // no message == faded
});
const form = <form className="mx_StatusMessageContextMenu_form" onSubmit={this._onSubmit} autoComplete="off">
<input type="text" key="message" placeholder={_t("Set a new status...")} autoFocus={true}
className="mx_StatusMessageContextMenu_message"
value={this.state.message} onChange={this._onStatusChange} maxLength="60" />
<AccessibleButton onClick={this._onSubmit} element="div" className={formSubmitClasses}>
<img src="img/icons-checkmark.svg" width="22" height="22" />
</AccessibleButton>
</form>;
const clearIcon = this.state.message ? "img/cancel-red.svg" : "img/cancel.svg";
const clearButton = <AccessibleButton onClick={this._onClearClick} disabled={!this.state.message}
className="mx_StatusMessageContextMenu_clear">
<img src={clearIcon} alt={_t('Clear status')} width="12" height="12"
className="mx_filterFlipColor mx_StatusMessageContextMenu_clearIcon" />
<span>{_t("Clear status")}</span>
</AccessibleButton>;
const menuClasses = classNames({
"mx_StatusMessageContextMenu": true,
"mx_StatusMessageContextMenu_hasStatus": this.state.message,
});
return <div className={menuClasses}>
{ form }
<hr />
{ clearButton }
</div>;
}
}

View File

@ -36,8 +36,12 @@ export default class ChangelogDialog extends React.Component {
for (let i=0; i<REPOS.length; i++) {
const oldVersion = version2[2*i];
const newVersion = version[2*i];
request(`https://api.github.com/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`, (a, b, body) => {
if (body == null) return;
const url = `https://api.github.com/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`;
request(url, (err, response, body) => {
if (response.statusCode < 200 || response.statusCode >= 300) {
this.setState({ [REPOS[i]]: response.statusText });
return;
}
this.setState({[REPOS[i]]: JSON.parse(body).commits});
});
}
@ -58,13 +62,20 @@ export default class ChangelogDialog extends React.Component {
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
const logs = REPOS.map(repo => {
if (this.state[repo] == null) return <Spinner key={repo} />;
let content;
if (this.state[repo] == null) {
content = <Spinner key={repo} />;
} else if (typeof this.state[repo] === "string") {
content = _t("Unable to load commit detail: %(msg)s", {
msg: this.state[repo],
});
} else {
content = this.state[repo].map(this._elementsForCommit);
}
return (
<div key={repo}>
<h2>{repo}</h2>
<ul>
{this.state[repo].map(this._elementsForCommit)}
</ul>
<ul>{content}</ul>
</div>
);
});

View File

@ -29,7 +29,6 @@ var TintableSvg = React.createClass({
width: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
className: PropTypes.string,
forceColors: PropTypes.arrayOf(PropTypes.string),
},
statics: {
@ -51,12 +50,6 @@ var TintableSvg = React.createClass({
delete TintableSvg.mounts[this.id];
},
componentDidUpdate: function(prevProps, prevState) {
if (prevProps.forceColors !== this.props.forceColors) {
this.calcAndApplyFixups(this.refs.svgContainer);
}
},
tint: function() {
// TODO: only bother running this if the global tint settings have changed
// since we loaded!
@ -64,13 +57,8 @@ var TintableSvg = React.createClass({
},
onLoad: function(event) {
this.calcAndApplyFixups(event.target);
},
calcAndApplyFixups: function(target) {
if (!target) return;
// console.log("TintableSvg.calcAndApplyFixups for " + this.props.src);
this.fixups = Tinter.calcSvgFixups([target], this.props.forceColors);
// console.log("TintableSvg.onLoad for " + this.props.src);
this.fixups = Tinter.calcSvgFixups([event.target]);
Tinter.applySvgFixups(this.fixups);
},
@ -83,7 +71,6 @@ var TintableSvg = React.createClass({
height={this.props.height}
onLoad={this.onLoad}
tabIndex="-1"
ref="svgContainer"
/>
);
},

View File

@ -85,8 +85,8 @@ export default React.createClass({
_getDisplayedGroups(userGroups, relatedGroups) {
let displayedGroups = userGroups || [];
if (relatedGroups && relatedGroups.length > 0) {
displayedGroups = displayedGroups.filter((groupId) => {
return relatedGroups.includes(groupId);
displayedGroups = relatedGroups.filter((groupId) => {
return displayedGroups.includes(groupId);
});
} else {
displayedGroups = [];

View File

@ -70,6 +70,7 @@ const EntityTile = React.createClass({
onClick: PropTypes.func,
suppressOnHover: PropTypes.bool,
showPresence: PropTypes.bool,
subtextLabel: PropTypes.string,
},
getDefaultProps: function() {
@ -129,6 +130,9 @@ const EntityTile = React.createClass({
presenceState={this.props.presenceState} />;
nameClasses += ' mx_EntityTile_name_hover';
}
if (this.props.subtextLabel) {
presenceLabel = <span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>;
}
nameEl = (
<div className="mx_EntityTile_details">
<EmojiText element="div" className={nameClasses} dir="auto">
@ -137,6 +141,15 @@ const EntityTile = React.createClass({
{presenceLabel}
</div>
);
} else if (this.props.subtextLabel) {
nameEl = (
<div className="mx_EntityTile_details">
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">
{name}
</EmojiText>
<span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>
</div>
);
} else {
nameEl = (
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">{ name }</EmojiText>

View File

@ -42,6 +42,7 @@ import AccessibleButton from '../elements/AccessibleButton';
import RoomViewStore from '../../../stores/RoomViewStore';
import SdkConfig from '../../../SdkConfig';
import MultiInviter from "../../../utils/MultiInviter";
import SettingsStore from "../../../settings/SettingsStore";
module.exports = withMatrixClient(React.createClass({
displayName: 'MemberInfo',
@ -889,11 +890,16 @@ module.exports = withMatrixClient(React.createClass({
let presenceState;
let presenceLastActiveAgo;
let presenceCurrentlyActive;
let statusMessage;
if (this.props.member.user) {
presenceState = this.props.member.user.presence;
presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
presenceCurrentlyActive = this.props.member.user.currentlyActive;
if (SettingsStore.isFeatureEnabled("feature_custom_status")) {
statusMessage = this.props.member.user._unstable_statusMessage;
}
}
const room = this.props.matrixClient.getRoom(this.props.member.roomId);
@ -915,6 +921,11 @@ module.exports = withMatrixClient(React.createClass({
presenceState={presenceState} />;
}
let statusLabel = null;
if (statusMessage) {
statusLabel = <span className="mx_MemberInfo_statusMessage">{ statusMessage }</span>;
}
let roomMemberDetails = null;
if (this.props.member.roomId) { // is in room
const PowerSelector = sdk.getComponent('elements.PowerSelector');
@ -931,6 +942,7 @@ module.exports = withMatrixClient(React.createClass({
</div>
<div className="mx_MemberInfo_profileField">
{presenceLabel}
{statusLabel}
</div>
</div>;
}

View File

@ -16,6 +16,8 @@ limitations under the License.
'use strict';
import SettingsStore from "../../../settings/SettingsStore";
const React = require('react');
import PropTypes from 'prop-types';
@ -85,6 +87,11 @@ module.exports = React.createClass({
const active = -1;
const presenceState = member.user ? member.user.presence : null;
let statusMessage = null;
if (member.user && SettingsStore.isFeatureEnabled("feature_custom_status")) {
statusMessage = member.user._unstable_statusMessage;
}
const av = (
<MemberAvatar member={member} width={36} height={36} />
);
@ -106,7 +113,9 @@ module.exports = React.createClass({
presenceLastTs={member.user ? member.user.lastPresenceTs : 0}
presenceCurrentlyActive={member.user ? member.user.currentlyActive : false}
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
name={name} powerStatus={powerStatus} showPresence={this.props.showPresence} />
name={name} powerStatus={powerStatus} showPresence={this.props.showPresence}
subtextLabel={statusMessage}
/>
);
},
});

View File

@ -291,7 +291,7 @@ export default class MessageComposer extends React.Component {
render() {
const uploadInputStyle = {display: 'none'};
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
@ -300,7 +300,7 @@ export default class MessageComposer extends React.Component {
if (this.state.me) {
controls.push(
<div key="controls_avatar" className="mx_MessageComposer_avatar">
<MemberAvatar member={this.state.me} width={24} height={24} />
<MemberStatusMessageAvatar member={this.state.me} width={24} height={24} />
</div>,
);
}
@ -349,6 +349,34 @@ export default class MessageComposer extends React.Component {
const canSendMessages = !this.state.tombstone &&
this.props.room.maySendMessage();
// TODO: Remove temporary logging for riot-web#7838
// Note: we rip apart the power level event ourselves because we don't want to
// log too much data about it - just the bits we care about. Many of the variables
// logged here are to help figure out where in the stack the 'cannot post in room'
// warning is coming from. This means logging various numbers from the PL event to
// verify RoomState._maySendEventOfType is doing the right thing.
const room = this.props.room;
const plEvent = room.currentState.getStateEvents('m.room.power_levels', '');
let plEventString = "<no power level event>";
if (plEvent) {
const content = plEvent.getContent();
if (!content) {
plEventString = "<no event content>";
} else {
const stringifyFalsey = (v) => v === null ? '<null>' : (v === undefined ? '<undefined>' : v);
const actualUserPl = stringifyFalsey(content.users ? content.users[room.myUserId] : "<no users in content>");
const usersPl = stringifyFalsey(content.users_default);
const actualEventPl = stringifyFalsey(content.events ? content.events['m.room.message'] : "<no events in content>");
const eventPl = stringifyFalsey(content.events_default);
plEventString = `actualUserPl=${actualUserPl} defaultUserPl=${usersPl} actualEventPl=${actualEventPl} defaultEventPl=${eventPl}`;
}
}
console.log(
`[riot-web#7838] renderComposer() hasTombstone=${!!this.state.tombstone} maySendMessage=${room.maySendMessage()}` +
` myMembership=${room.getMyMembership()} maySendEvent=${room.currentState.maySendEvent('m.room.message', room.myUserId)}` +
` myUserId=${room.myUserId} roomId=${room.roomId} hasPlEvent=${!!plEvent} powerLevels='${plEventString}'`
);
if (canSendMessages) {
// This also currently includes the call buttons. Really we should
// check separately for whether we can call, but this is slightly
@ -425,6 +453,8 @@ export default class MessageComposer extends React.Component {
</div>
</div>);
} else {
// TODO: Remove temporary logging for riot-web#7838
console.log("[riot-web#7838] Falling back to showing cannot post in room error");
controls.push(
<div key="controls_error" className="mx_MessageComposer_noperm_error">
{ _t('You do not have permission to post to this room') }

View File

@ -86,6 +86,7 @@ module.exports = React.createClass({
incomingCallTag: null,
incomingCall: null,
selectedTags: [],
hover: false,
};
},
@ -294,6 +295,17 @@ module.exports = React.createClass({
this.forceUpdate();
},
onMouseEnter: function(ev) {
this.setState({hover: true});
},
onMouseLeave: function(ev) {
this.setState({hover: false});
// Refresh the room list just in case the user missed something.
this._delayedRefreshRoomList();
},
_delayedRefreshRoomList: new rate_limited_func(function() {
this.refreshRoomList();
}, 500),
@ -346,6 +358,11 @@ module.exports = React.createClass({
},
refreshRoomList: function() {
if (this.state.hover) {
// Don't re-sort the list if we're hovering over the list
return;
}
// TODO: ideally we'd calculate this once at start, and then maintain
// any changes to it incrementally, updating the appropriate sublists
// as needed.
@ -693,9 +710,10 @@ module.exports = React.createClass({
const subListComponents = this._mapSubListProps(subLists);
return (
<div ref={this._collectResizeContainer} className="mx_RoomList">
<div ref={this._collectResizeContainer} className="mx_RoomList"
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
{ subListComponents }
</div>
);
},
});
});

View File

@ -19,13 +19,76 @@ import PropTypes from "prop-types";
import sdk from "../../../index";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import MatrixClientPeg from "../../../MatrixClientPeg";
export default class RoomRecoveryReminder extends React.PureComponent {
static propTypes = {
onFinished: PropTypes.func.isRequired,
}
showKeyBackupDialog = () => {
constructor(props) {
super(props);
this.state = {
loading: true,
error: null,
unverifiedDevice: null,
};
}
componentWillMount() {
this._loadBackupStatus();
}
async _loadBackupStatus() {
let backupSigStatus;
try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
} catch (e) {
console.log("Unable to fetch key backup status", e);
this.setState({
loading: false,
error: e,
});
return;
}
let unverifiedDevice;
for (const sig of backupSigStatus.sigs) {
if (!sig.device.isVerified()) {
unverifiedDevice = sig.device;
break;
}
}
this.setState({
loading: false,
unverifiedDevice,
});
}
showSetupDialog = () => {
if (this.state.unverifiedDevice) {
// A key backup exists for this account, but the creating device is not
// verified, so we'll show the device verify dialog.
// TODO: Should change to a restore key backup flow that checks the recovery
// passphrase while at the same time also cross-signing the device as well in
// a single flow (for cases where a key backup exists but the backup creating
// device is unverified). Since we don't have that yet, we'll look for an
// unverified device and verify it. Note that this means we won't restore
// keys yet; instead we'll only trust the backup for sending our own new keys
// to it.
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
userId: MatrixClientPeg.get().credentials.userId,
device: this.state.unverifiedDevice,
onFinished: this.props.onFinished,
});
return;
}
// The default case assumes that a key backup doesn't exist for this account, so
// we'll show the create key backup flow.
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
{
@ -46,29 +109,51 @@ export default class RoomRecoveryReminder extends React.PureComponent {
this.props.onFinished(false);
},
onSetup: () => {
this.showKeyBackupDialog();
this.showSetupDialog();
},
},
);
}
onSetupClick = () => {
this.showKeyBackupDialog();
this.showSetupDialog();
}
render() {
if (this.state.loading) {
return null;
}
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
let body;
if (this.state.error) {
body = <div className="error">
{_t("Unable to load key backup status")}
</div>;
} else if (this.state.unverifiedDevice) {
// A key backup exists for this account, but the creating device is not
// verified.
body = _t(
"To view your secure message history and ensure you can view new " +
"messages on future devices, set up Secure Message Recovery.",
);
} else {
// The default case assumes that a key backup doesn't exist for this account.
// (This component doesn't currently check that itself.)
body = _t(
"If you log out or use another device, you'll lose your " +
"secure message history. To prevent this, set up Secure " +
"Message Recovery.",
);
}
return (
<div className="mx_RoomRecoveryReminder">
<div className="mx_RoomRecoveryReminder_header">{_t(
"Secure Message Recovery",
)}</div>
<div className="mx_RoomRecoveryReminder_body">{_t(
"If you log out or use another device, you'll lose your " +
"secure message history. To prevent this, set up Secure " +
"Message Recovery.",
)}</div>
<div className="mx_RoomRecoveryReminder_body">{body}</div>
<div className="mx_RoomRecoveryReminder_buttons">
<AccessibleButton className="mx_RoomRecoveryReminder_button mx_RoomRecoveryReminder_secondary"
onClick={this.onDontAskAgainClick}>

View File

@ -30,6 +30,7 @@ import * as FormattingUtils from '../../../utils/FormattingUtils';
import AccessibleButton from '../elements/AccessibleButton';
import ActiveRoomObserver from '../../../ActiveRoomObserver';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore from "../../../settings/SettingsStore";
module.exports = React.createClass({
displayName: 'RoomTile',
@ -251,6 +252,17 @@ module.exports = React.createClass({
const mentionBadges = this.props.highlight && this._shouldShowMentionBadge();
const badges = notifBadges || mentionBadges;
const isJoined = this.props.room.getMyMembership() === "join";
const looksLikeDm = this.props.room.getInvitedAndJoinedMemberCount() === 2;
let subtext = null;
if (!isInvite && isJoined && looksLikeDm && SettingsStore.isFeatureEnabled("feature_custom_status")) {
const selfId = MatrixClientPeg.get().getUserId();
const otherMember = this.props.room.currentState.getMembersExcept([selfId])[0];
if (otherMember && otherMember.user && otherMember.user._unstable_statusMessage) {
subtext = otherMember.user._unstable_statusMessage;
}
}
const classes = classNames({
'mx_RoomTile': true,
'mx_RoomTile_selected': this.state.selected,
@ -261,6 +273,7 @@ module.exports = React.createClass({
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
'mx_RoomTile_noBadges': !badges,
'mx_RoomTile_transparent': this.props.transparent,
'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
});
const avatarClasses = classNames({
@ -286,6 +299,7 @@ module.exports = React.createClass({
const EmojiText = sdk.getComponent('elements.EmojiText');
let label;
let subtextLabel;
let tooltip;
if (!this.props.collapsed) {
const nameClasses = classNames({
@ -294,6 +308,8 @@ module.exports = React.createClass({
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
});
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
if (this.state.selected) {
const nameSelected = <EmojiText>{ name }</EmojiText>;
@ -337,9 +353,14 @@ module.exports = React.createClass({
{ dmIndicator }
</div>
</div>
{ label }
{ contextMenuButton }
{ badge }
<div className="mx_RoomTile_nameContainer">
<div className="mx_RoomTile_labelContainer">
{ label }
{ subtextLabel }
</div>
{ contextMenuButton }
{ badge }
</div>
{ /* { incomingCallBox } */ }
{ tooltip }
</AccessibleButton>;

View File

@ -250,11 +250,14 @@
"A word by itself is easy to guess": "A word by itself is easy to guess",
"Names and surnames by themselves are easy to guess": "Names and surnames by themselves are easy to guess",
"Common names and surnames are easy to guess": "Common names and surnames are easy to guess",
"Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess",
"Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess",
"There was an error joining the room": "There was an error joining the room",
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
"Failed to join room": "Failed to join room",
"Message Pinning": "Message Pinning",
"Custom user status messages": "Custom user status messages",
"Increase performance by only loading room members on first view": "Increase performance by only loading room members on first view",
"Backup of encryption keys to server": "Backup of encryption keys to server",
"Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing",
@ -563,8 +566,9 @@
"You are trying to access a room.": "You are trying to access a room.",
"<a>Click here</a> to join the discussion!": "<a>Click here</a> to join the discussion!",
"This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled",
"Secure Message Recovery": "Secure Message Recovery",
"To view your secure message history and ensure you can view new messages on future devices, set up Secure Message Recovery.": "To view your secure message history and ensure you can view new messages on future devices, set up Secure Message Recovery.",
"If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.": "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.",
"Secure Message Recovery": "Secure Message Recovery",
"Don't ask again": "Don't ask again",
"Set up": "Set up",
"To change the room's avatar, you must be a": "To change the room's avatar, you must be a",
@ -888,6 +892,7 @@
"What GitHub issue are these logs for?": "What GitHub issue are these logs for?",
"Notes:": "Notes:",
"Send logs": "Send logs",
"Unable to load commit detail: %(msg)s": "Unable to load commit detail: %(msg)s",
"Unavailable": "Unavailable",
"Changelog": "Changelog",
"Create a new chat or reuse an existing one": "Create a new chat or reuse an existing one",
@ -1064,6 +1069,8 @@
"Forget": "Forget",
"Low Priority": "Low Priority",
"Direct Chat": "Direct Chat",
"Set a new status...": "Set a new status...",
"Clear status": "Clear status",
"View Community": "View Community",
"Sorry, your browser is <b>not</b> able to run Riot.": "Sorry, your browser is <b>not</b> able to run Riot.",
"Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.",

View File

@ -83,6 +83,12 @@ export const SETTINGS = {
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_custom_status": {
isFeature: true,
displayName: _td("Custom user status messages"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_lazyloading": {
isFeature: true,
displayName: _td("Increase performance by only loading room members on first view"),

View File

@ -52,6 +52,8 @@ _td("This is similar to a commonly used password");
_td("A word by itself is easy to guess");
_td("Names and surnames by themselves are easy to guess");
_td("Common names and surnames are easy to guess");
_td("Straight rows of keys are easy to guess");
_td("Short keyboard patterns are easy to guess");
/**
* Wrapper around zxcvbn password strength estimation

View File

@ -54,7 +54,7 @@ describe('DecryptionFailureTracker', function() {
// Immediately track the newest failures
tracker.trackFailures();
expect(count).toNotBe(0, 'should track a failure for an event that failed decryption');
expect(count).not.toBe(0, 'should track a failure for an event that failed decryption');
done();
});

View File

@ -185,21 +185,21 @@ describe('GroupView', function() {
const avatar = ReactTestUtils.findRenderedComponentWithType(root, sdk.getComponent('avatars.GroupAvatar'));
const img = ReactTestUtils.findRenderedDOMComponentWithTag(avatar, 'img');
const avatarImgElement = ReactDOM.findDOMNode(img);
expect(avatarImgElement).toExist();
expect(avatarImgElement.src).toInclude(
expect(avatarImgElement).toBeTruthy();
expect(avatarImgElement.src).toContain(
'https://my.home.server/_matrix/media/v1/thumbnail/' +
'someavatarurl?width=48&height=48&method=crop',
);
const name = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_header_name');
const nameElement = ReactDOM.findDOMNode(name);
expect(nameElement).toExist();
expect(nameElement.innerText).toInclude('The name of a community');
expect(nameElement.innerText).toInclude(groupId);
expect(nameElement).toBeTruthy();
expect(nameElement.innerText).toContain('The name of a community');
expect(nameElement.innerText).toContain(groupId);
const shortDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_header_shortDesc');
const shortDescElement = ReactDOM.findDOMNode(shortDesc);
expect(shortDescElement).toExist();
expect(shortDescElement).toBeTruthy();
expect(shortDescElement.innerText).toBe('This is a community');
});
@ -219,7 +219,7 @@ describe('GroupView', function() {
const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc');
const longDescElement = ReactDOM.findDOMNode(longDesc);
expect(longDescElement).toExist();
expect(longDescElement).toBeTruthy();
expect(longDescElement.innerText).toBe('This is a LONG description.');
expect(longDescElement.innerHTML).toBe('<div dir="auto">This is a <b>LONG</b> description.</div>');
});
@ -239,7 +239,7 @@ describe('GroupView', function() {
const placeholder = ReactTestUtils
.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc_placeholder');
const placeholderElement = ReactDOM.findDOMNode(placeholder);
expect(placeholderElement).toExist();
expect(placeholderElement).toBeTruthy();
});
httpBackend
@ -258,15 +258,15 @@ describe('GroupView', function() {
const prom = waitForUpdate(groupView, 4).then(() => {
const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc');
const longDescElement = ReactDOM.findDOMNode(longDesc);
expect(longDescElement).toExist();
expect(longDescElement).toBeTruthy();
expect(longDescElement.innerHTML).toInclude('<h1>This is a more complicated group page</h1>');
expect(longDescElement.innerHTML).toInclude('<p>With paragraphs</p>');
expect(longDescElement.innerHTML).toInclude('<ul>');
expect(longDescElement.innerHTML).toInclude('<li>And lists!</li>');
expect(longDescElement.innerHTML).toContain('<h1>This is a more complicated group page</h1>');
expect(longDescElement.innerHTML).toContain('<p>With paragraphs</p>');
expect(longDescElement.innerHTML).toContain('<ul>');
expect(longDescElement.innerHTML).toContain('<li>And lists!</li>');
const imgSrc = "https://my.home.server/_matrix/media/v1/thumbnail/someimageurl?width=800&amp;height=600";
expect(longDescElement.innerHTML).toInclude('<img src="' + imgSrc + '">');
expect(longDescElement.innerHTML).toContain('<img src="' + imgSrc + '">');
});
httpBackend
@ -285,11 +285,11 @@ describe('GroupView', function() {
const prom = waitForUpdate(groupView, 4).then(() => {
const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc');
const longDescElement = ReactDOM.findDOMNode(longDesc);
expect(longDescElement).toExist();
expect(longDescElement).toBeTruthy();
// If this fails, the URL could be in an img `src`, which is what we care about but
// there's no harm in keeping this simple and checking the entire HTML string.
expect(longDescElement.innerHTML).toExclude('evilimageurl');
expect(longDescElement.innerHTML).not.toContain('evilimageurl');
});
httpBackend
@ -308,7 +308,7 @@ describe('GroupView', function() {
const prom = waitForUpdate(groupView, 4).then(() => {
const roomDetailList = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_RoomDetailList');
const roomDetailListElement = ReactDOM.findDOMNode(roomDetailList);
expect(roomDetailListElement).toExist();
expect(roomDetailListElement).toBeTruthy();
});
httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse);
@ -325,7 +325,7 @@ describe('GroupView', function() {
const prom = waitForUpdate(groupView, 4).then(() => {
const roomDetailList = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_RoomDetailList');
const roomDetailListElement = ReactDOM.findDOMNode(roomDetailList);
expect(roomDetailListElement).toExist();
expect(roomDetailListElement).toBeTruthy();
const roomDetailListRoomName = ReactTestUtils.findRenderedDOMComponentWithClass(
root,
@ -333,7 +333,7 @@ describe('GroupView', function() {
);
const roomDetailListRoomNameElement = ReactDOM.findDOMNode(roomDetailListRoomName);
expect(roomDetailListRoomNameElement).toExist();
expect(roomDetailListRoomNameElement).toBeTruthy();
expect(roomDetailListRoomNameElement.innerText).toEqual('Some room name');
});
@ -364,7 +364,7 @@ describe('GroupView', function() {
const prom = waitForUpdate(groupView, 3).then(() => {
const shortDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_header_shortDesc');
const shortDescElement = ReactDOM.findDOMNode(shortDesc);
expect(shortDescElement).toExist();
expect(shortDescElement).toBeTruthy();
expect(shortDescElement.innerText).toBe('This is a community');
});

View File

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const jest = require('jest-mock');
const React = require('react');
const ReactDOM = require('react-dom');
const ReactTestUtils = require('react-addons-test-utils');
@ -87,8 +88,8 @@ describe('Registration', function() {
});
it('should NOT track a referral following successful registration of a non-team member', function(done) {
const onLoggedIn = expect.createSpy().andCall(function(creds, teamToken) {
expect(teamToken).toNotExist();
const onLoggedIn = jest.fn(function(creds, teamToken) {
expect(teamToken).toBeFalsy();
done();
});

View File

@ -83,8 +83,8 @@ describe('InteractiveAuthDialog', function() {
submitNode = node;
}
}
expect(passwordNode).toExist();
expect(submitNode).toExist();
expect(passwordNode).toBeTruthy();
expect(submitNode).toBeTruthy();
// submit should be disabled
expect(submitNode.disabled).toBe(true);

View File

@ -114,7 +114,7 @@ describe("GroupMemberList", function() {
const memberList = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList_joined");
const memberListElement = ReactDOM.findDOMNode(memberList);
expect(memberListElement).toExist();
expect(memberListElement).toBeTruthy();
expect(memberListElement.innerText).toBe("Test");
});
@ -134,7 +134,7 @@ describe("GroupMemberList", function() {
const memberList = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList_joined");
const memberListElement = ReactDOM.findDOMNode(memberList);
expect(memberListElement).toExist();
expect(memberListElement).toBeTruthy();
expect(memberListElement.innerText).toBe("Failed to load group members");
});

View File

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const jest = require('jest-mock');
const React = require('react');
const ReactDOM = require("react-dom");
const ReactTestUtils = require('react-addons-test-utils');
@ -55,14 +56,14 @@ function doInputEmail(inputEmail, onTeamSelected) {
}
function expectTeamSelectedFromEmailInput(inputEmail, expectedTeam) {
const onTeamSelected = expect.createSpy();
const onTeamSelected = jest.fn();
doInputEmail(inputEmail, onTeamSelected);
expect(onTeamSelected).toHaveBeenCalledWith(expectedTeam);
}
function expectSupportFromEmailInput(inputEmail, isSupportShown) {
const onTeamSelected = expect.createSpy();
const onTeamSelected = jest.fn();
const res = doInputEmail(inputEmail, onTeamSelected);
expect(res.state.showSupportEmail).toBe(isSupportShown);

View File

@ -1,7 +1,7 @@
import React from 'react';
import ReactTestUtils from 'react-addons-test-utils';
import ReactDOM from 'react-dom';
import expect, {createSpy} from 'expect';
import expect from 'expect';
import sinon from 'sinon';
import Promise from 'bluebird';
import * as testUtils from '../../../test-utils';

View File

@ -69,7 +69,7 @@ describe('RoomList', () => {
ReactTestUtils.findRenderedComponentWithType(root, RoomList);
movingRoom = createRoom({name: 'Moving room'});
expect(movingRoom.roomId).toNotBe(null);
expect(movingRoom.roomId).not.toBe(null);
// Mock joined member
myMember = new RoomMember(movingRoomId, myUserId);
@ -139,7 +139,7 @@ describe('RoomList', () => {
throw err;
}
expect(expectedRoomTile).toExist();
expect(expectedRoomTile).toBeTruthy();
expect(expectedRoomTile.props.room).toBe(room);
}

View File

@ -1,6 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import expect, {createSpy} from 'expect';
import expect from 'expect';
import jest from 'jest-mock';
import Promise from 'bluebird';
import * as testUtils from '../../../test-utils';
import sdk from 'matrix-react-sdk';
@ -18,12 +19,12 @@ describe('RoomSettings', () => {
function expectSentStateEvent(roomId, eventType, expectedEventContent) {
let found = false;
for (const call of client.sendStateEvent.calls) {
for (const call of client.sendStateEvent.mock.calls) {
const [
actualRoomId,
actualEventType,
actualEventContent,
] = call.arguments.slice(0, 3);
] = call.slice(0, 3);
if (roomId === actualRoomId && actualEventType === eventType) {
expect(actualEventContent).toEqual(expectedEventContent);
@ -40,20 +41,20 @@ describe('RoomSettings', () => {
client = MatrixClientPeg.get();
client.credentials = {userId: '@me:domain.com'};
client.setRoomName = createSpy().andReturn(Promise.resolve());
client.setRoomTopic = createSpy().andReturn(Promise.resolve());
client.setRoomDirectoryVisibility = createSpy().andReturn(Promise.resolve());
client.setRoomName = jest.fn().mockReturnValue(Promise.resolve());
client.setRoomTopic = jest.fn().mockReturnValue(Promise.resolve());
client.setRoomDirectoryVisibility = jest.fn().mockReturnValue(Promise.resolve());
// Covers any room state event (e.g. name, avatar, topic)
client.sendStateEvent = createSpy().andReturn(Promise.resolve());
client.sendStateEvent = jest.fn().mockReturnValue(Promise.resolve());
// Covers room tagging
client.setRoomTag = createSpy().andReturn(Promise.resolve());
client.deleteRoomTag = createSpy().andReturn(Promise.resolve());
client.setRoomTag = jest.fn().mockReturnValue(Promise.resolve());
client.deleteRoomTag = jest.fn().mockReturnValue(Promise.resolve());
// Covers any setting in the SettingsStore
// (including local client settings not stored via matrix)
SettingsStore.setValue = createSpy().andReturn(Promise.resolve());
SettingsStore.setValue = jest.fn().mockReturnValue(Promise.resolve());
parentDiv = document.createElement('div');
document.body.appendChild(parentDiv);
@ -83,9 +84,9 @@ describe('RoomSettings', () => {
it('should not set when no setting is changed', (done) => {
roomSettings.save().then(() => {
expect(client.sendStateEvent).toNotHaveBeenCalled();
expect(client.setRoomTag).toNotHaveBeenCalled();
expect(client.deleteRoomTag).toNotHaveBeenCalled();
expect(client.sendStateEvent).not.toHaveBeenCalled();
expect(client.setRoomTag).not.toHaveBeenCalled();
expect(client.deleteRoomTag).not.toHaveBeenCalled();
done();
});
});
@ -93,7 +94,7 @@ describe('RoomSettings', () => {
// XXX: Apparently we do call SettingsStore.setValue
xit('should not settings via the SettingsStore when no setting is changed', (done) => {
roomSettings.save().then(() => {
expect(SettingsStore.setValue).toNotHaveBeenCalled();
expect(SettingsStore.setValue).not.toHaveBeenCalled();
done();
});
});
@ -103,7 +104,7 @@ describe('RoomSettings', () => {
roomSettings.setName(name);
roomSettings.save().then(() => {
expect(client.setRoomName.calls[0].arguments.slice(0, 2))
expect(client.setRoomName.mock.calls[0].slice(0, 2))
.toEqual(['!DdJkzRliezrwpNebLk:matrix.org', name]);
done();
@ -115,7 +116,7 @@ describe('RoomSettings', () => {
roomSettings.setTopic(topic);
roomSettings.save().then(() => {
expect(client.setRoomTopic.calls[0].arguments.slice(0, 2))
expect(client.setRoomTopic.mock.calls[0].slice(0, 2))
.toEqual(['!DdJkzRliezrwpNebLk:matrix.org', topic]);
done();

View File

@ -39,7 +39,7 @@ describe('matrix-to', function() {
it('should pick no candidate servers when the room is not found', function() {
peg.get().getRoom = () => null;
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers).toBeTruthy();
expect(pickedServers.length).toBe(0);
});
@ -50,7 +50,7 @@ describe('matrix-to', function() {
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers).toBeTruthy();
expect(pickedServers.length).toBe(0);
});
@ -74,7 +74,7 @@ describe('matrix-to', function() {
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers).toBeTruthy();
expect(pickedServers.length).toBe(3);
expect(pickedServers[0]).toBe("pl_95");
// we don't check the 2nd and 3rd servers because that is done by the next test
@ -112,7 +112,7 @@ describe('matrix-to', function() {
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers).toBeTruthy();
expect(pickedServers.length).toBe(3);
expect(pickedServers[0]).toBe("first");
expect(pickedServers[1]).toBe("second");
@ -143,7 +143,7 @@ describe('matrix-to', function() {
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers).toBeTruthy();
expect(pickedServers.length).toBe(3);
expect(pickedServers[0]).toBe("first");
expect(pickedServers[1]).toBe("second");
@ -178,7 +178,7 @@ describe('matrix-to', function() {
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers).toBeTruthy();
expect(pickedServers.length).toBe(3);
});
@ -194,7 +194,7 @@ describe('matrix-to', function() {
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers).toBeTruthy();
expect(pickedServers.length).toBe(0);
});
@ -210,7 +210,7 @@ describe('matrix-to', function() {
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers).toBeTruthy();
expect(pickedServers.length).toBe(0);
});
@ -226,7 +226,7 @@ describe('matrix-to', function() {
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers).toBeTruthy();
expect(pickedServers.length).toBe(0);
});
@ -242,7 +242,7 @@ describe('matrix-to', function() {
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers).toBeTruthy();
expect(pickedServers.length).toBe(0);
});
@ -258,7 +258,7 @@ describe('matrix-to', function() {
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers).toBeTruthy();
expect(pickedServers.length).toBe(1);
expect(pickedServers[0]).toBe("example.org:8448");
});
@ -292,7 +292,7 @@ describe('matrix-to', function() {
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers).toBeTruthy();
expect(pickedServers.length).toBe(0);
});
@ -325,7 +325,7 @@ describe('matrix-to', function() {
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers).toBeTruthy();
expect(pickedServers.length).toBe(0);
});
@ -358,7 +358,7 @@ describe('matrix-to', function() {
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers).toBeTruthy();
expect(pickedServers.length).toBe(1);
expect(pickedServers[0]).toEqual("evilcorp.com");
});
@ -392,7 +392,7 @@ describe('matrix-to', function() {
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers).toBeTruthy();
expect(pickedServers.length).toBe(1);
expect(pickedServers[0]).toEqual("evilcorp.com");
});