diff --git a/.travis-test-riot.sh b/.travis-test-riot.sh index 88b3719b3a..d1c2804b2a 100755 --- a/.travis-test-riot.sh +++ b/.travis-test-riot.sh @@ -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 diff --git a/package.json b/package.json index 7a63d55415..6dc9a6bfcf 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/res/css/_components.scss b/res/css/_components.scss index 63b1bde2d6..d8f966603d 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -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"; diff --git a/res/css/views/dialogs/_CreateKeyBackupDialog.scss b/res/css/views/avatars/_MemberStatusMessageAvatar.scss similarity index 74% rename from res/css/views/dialogs/_CreateKeyBackupDialog.scss rename to res/css/views/avatars/_MemberStatusMessageAvatar.scss index a422cf858c..29cae9df34 100644 --- a/res/css/views/dialogs/_CreateKeyBackupDialog.scss +++ b/res/css/views/avatars/_MemberStatusMessageAvatar.scss @@ -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 */ } diff --git a/res/css/views/context_menus/_StatusMessageContextMenu.scss b/res/css/views/context_menus/_StatusMessageContextMenu.scss new file mode 100644 index 0000000000..873ad99495 --- /dev/null +++ b/res/css/views/context_menus/_StatusMessageContextMenu.scss @@ -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; +} diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index 2cb6b11c0c..424ffbd0a8 100644 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -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; } diff --git a/res/css/views/rooms/_EntityTile.scss b/res/css/views/rooms/_EntityTile.scss index 4289b3f2cf..c4d4d944a6 100644 --- a/res/css/views/rooms/_EntityTile.scss +++ b/res/css/views/rooms/_EntityTile.scss @@ -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; +} diff --git a/res/css/views/rooms/_MemberInfo.scss b/res/css/views/rooms/_MemberInfo.scss index a027c575dd..4af181a464 100644 --- a/res/css/views/rooms/_MemberInfo.scss +++ b/res/css/views/rooms/_MemberInfo.scss @@ -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; } diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 232b715f7a..70d505e4ea 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -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; diff --git a/res/img/icons-checkmark.svg b/res/img/icons-checkmark.svg new file mode 100644 index 0000000000..3c5392003d --- /dev/null +++ b/res/img/icons-checkmark.svg @@ -0,0 +1,17 @@ + + + + Tick + Created with Sketch. + + + + + + + + + + + + diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index f20bfe8920..95fc4b0603 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -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 diff --git a/src/Login.js b/src/Login.js index 330eb8a8f5..ca045e36cd 100644 --- a/src/Login.js +++ b/src/Login.js @@ -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, diff --git a/src/Notifier.js b/src/Notifier.js index 8550f3bf95..80e8be1084 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -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); } diff --git a/src/Tinter.js b/src/Tinter.js index 80375dead2..de9ae94097 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -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"); } diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 0db9d0699b..a097e84cdb 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -239,17 +239,19 @@ export default React.createClass({

{_t("You'll need it if you log out or lose access to this device.")}

-
- {strengthMeter} - {helpText} +
+ +
+ {strengthMeter} + {helpText} +
-
- {passPhraseMatch} -
- +
+
+ +
+ {passPhraseMatch}
{_t("Make a copy of this Recovery Key and keep it safe.")}

{bodyText}

-

{_t("Your Recovery Key")}
-
- - { - // FIXME REDESIGN: buttons should be adjacent but insufficient room in current design - } -

- +
+ {_t("Your Recovery Key")}
-
- {this._keyBackupInfo.recovery_key} +
+
+ {this._keyBackupInfo.recovery_key} +
+
+ + +


diff --git a/src/components/structures/HomePage.js b/src/components/structures/HomePage.js index 01aabf6115..8f0c270513 100644 --- a/src/components/structures/HomePage.js +++ b/src/components/structures/HomePage.js @@ -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' }); } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b01174a91c..a03265da1c 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -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; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 2358ed5906..2ee4d8e596 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -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(); }, diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js index 559136948a..5c0e428339 100644 --- a/src/components/structures/login/ForgotPassword.js +++ b/src/components/structures/login/ForgotPassword.js @@ -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({ { serverConfigSection } { errorText } - + { _t('Return to login screen') } - + { _t('Create an account') } diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index b94a1759cf..321084389b 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -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 } - + { _t('Create an account') } { loginAsGuestJsx } diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index ad3ea5f19c..fa5a02e881 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -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 = ( - + { theme === 'status' ? _t('Sign in') : _t('I already have an account') } ); diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js new file mode 100644 index 0000000000..aebd1741b7 --- /dev/null +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -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 ; + } + + const hasStatus = this.props.member.user ? !!this.props.member.user._unstable_statusMessage : false; + + const classes = classNames({ + "mx_MemberStatusMessageAvatar": true, + "mx_MemberStatusMessageAvatar_hasStatus": hasStatus, + }); + + return + + ; + } +} diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js new file mode 100644 index 0000000000..f07220db44 --- /dev/null +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -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 =
+ + + + +
; + + const clearIcon = this.state.message ? "img/cancel-red.svg" : "img/cancel.svg"; + const clearButton = + {_t('Clear + {_t("Clear status")} + ; + + const menuClasses = classNames({ + "mx_StatusMessageContextMenu": true, + "mx_StatusMessageContextMenu_hasStatus": this.state.message, + }); + + return
+ { form } +
+ { clearButton } +
; + } +} diff --git a/src/components/views/dialogs/ChangelogDialog.js b/src/components/views/dialogs/ChangelogDialog.js index b93678b2ab..3c9414fd88 100644 --- a/src/components/views/dialogs/ChangelogDialog.js +++ b/src/components/views/dialogs/ChangelogDialog.js @@ -36,8 +36,12 @@ export default class ChangelogDialog extends React.Component { for (let i=0; i { - 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 ; + let content; + if (this.state[repo] == null) { + content = ; + } 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 (

{repo}

-
    - {this.state[repo].map(this._elementsForCommit)} -
+
    {content}
); }); diff --git a/src/components/views/elements/TintableSvg.js b/src/components/views/elements/TintableSvg.js index 08628c8ca9..e04bf87793 100644 --- a/src/components/views/elements/TintableSvg.js +++ b/src/components/views/elements/TintableSvg.js @@ -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" /> ); }, diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index a40addf0d1..a8c52e06b1 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -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 = []; diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index 851352aa17..3f3bdbf47a 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -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 = {this.props.subtextLabel}; + } nameEl = (
@@ -137,6 +141,15 @@ const EntityTile = React.createClass({ {presenceLabel}
); + } else if (this.props.subtextLabel) { + nameEl = ( +
+ + {name} + + {this.props.subtextLabel} +
+ ); } else { nameEl = ( { name } diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 6c53470645..12dc2117a0 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -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 = { statusMessage }; + } + 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({
{presenceLabel} + {statusLabel}
; } diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 2359bc242c..ba951792d0 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -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 = ( ); @@ -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} + /> ); }, }); diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 3c4a63ed27..d4b607a93a 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -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(
- +
, ); } @@ -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 = ""; + if (plEvent) { + const content = plEvent.getContent(); + if (!content) { + plEventString = ""; + } else { + const stringifyFalsey = (v) => v === null ? '' : (v === undefined ? '' : v); + const actualUserPl = stringifyFalsey(content.users ? content.users[room.myUserId] : ""); + const usersPl = stringifyFalsey(content.users_default); + const actualEventPl = stringifyFalsey(content.events ? content.events['m.room.message'] : ""); + 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 {
); } 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(
{ _t('You do not have permission to post to this room') } diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index df2a242852..dbfe95dadf 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -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 ( -
+
{ subListComponents }
); }, -}); +}); \ No newline at end of file diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js index 265bfd3ee3..d03c5fc96d 100644 --- a/src/components/views/rooms/RoomRecoveryReminder.js +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -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 =
+ {_t("Unable to load key backup status")} +
; + } 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 (
{_t( "Secure Message Recovery", )}
-
{_t( - "If you log out or use another device, you'll lose your " + - "secure message history. To prevent this, set up Secure " + - "Message Recovery.", - )}
+
{body}
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 4e05fbcb8b..bce4d15f16 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -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 ? { subtext } : null; + if (this.state.selected) { const nameSelected = { name }; @@ -337,9 +353,14 @@ module.exports = React.createClass({ { dmIndicator }
- { label } - { contextMenuButton } - { badge } +
+
+ { label } + { subtextLabel } +
+ { contextMenuButton } + { badge } +
{ /* { incomingCallBox } */ } { tooltip } ; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fe4b3f4c19..be2f950156 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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.", "Click here to join the discussion!": "Click here 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 not able to run Riot.": "Sorry, your browser is not 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.", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 56e66844dc..14f4bdc6dd 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -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"), diff --git a/src/utils/PasswordScorer.js b/src/utils/PasswordScorer.js index e4bbec1637..545686cdb6 100644 --- a/src/utils/PasswordScorer.js +++ b/src/utils/PasswordScorer.js @@ -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 diff --git a/test/DecryptionFailureTracker-test.js b/test/DecryptionFailureTracker-test.js index 617c9d5d68..baa0545f77 100644 --- a/test/DecryptionFailureTracker-test.js +++ b/test/DecryptionFailureTracker-test.js @@ -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(); }); diff --git a/test/components/structures/GroupView-test.js b/test/components/structures/GroupView-test.js index 89632dcc48..b49c335bdf 100644 --- a/test/components/structures/GroupView-test.js +++ b/test/components/structures/GroupView-test.js @@ -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('
This is a LONG description.
'); }); @@ -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('

This is a more complicated group page

'); - expect(longDescElement.innerHTML).toInclude('

With paragraphs

'); - expect(longDescElement.innerHTML).toInclude('