From ce7969e3d5a2ed4fb6b98539c1bdeb58e915c655 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 11 Dec 2018 21:40:11 -0700 Subject: [PATCH 01/29] Display custom status messages in the UI Part of https://github.com/vector-im/riot-web/issues/1528 --- res/css/views/rooms/_RoomTile.scss | 19 ++++++++++++++++++- src/components/views/rooms/EntityTile.js | 21 +++++++++++++++++---- src/components/views/rooms/MemberTile.js | 5 ++++- src/components/views/rooms/RoomTile.js | 16 ++++++++++++++++ 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index ccd3afe26c..2014bb6404 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -35,7 +35,20 @@ limitations under the License. .mx_RoomTile_nameContainer { display: inline-block; width: 180px; - height: 24px; + //height: 24px; + vertical-align: middle; +} + +.mx_RoomTile_subtext { + display: inline-block; + font-size: 0.8em; + padding: 0 0 0 7px; + margin: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: clip; + position: relative; + bottom: 4px; } .mx_RoomTile_avatar_container { @@ -76,6 +89,10 @@ limitations under the License. text-overflow: ellipsis; } +.mx_RoomTile_hasSubtext .mx_RoomTile_avatar { + padding-top: 0; +} + .mx_RoomTile_invite { /* color: rgba(69, 69, 69, 0.5); */ } diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index 6b3264d123..27215430a1 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() { @@ -125,19 +126,31 @@ const EntityTile = React.createClass({ let nameClasses = 'mx_EntityTile_name'; if (this.props.showPresence) { presenceLabel = ; + currentlyActive={this.props.presenceCurrentlyActive} + presenceState={this.props.presenceState}/>; nameClasses += ' mx_EntityTile_name_hover'; } + if (this.props.subtextLabel) { + presenceLabel = {this.props.subtextLabel}; + } nameEl = (
- + - { name } + {name} {presenceLabel}
); + } else if (this.props.subtextLabel) { + nameEl = ( +
+ + {name} + + {this.props.subtextLabel} +
+ ); } else { nameEl = ( { name } diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 2359bc242c..d246b37234 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -84,6 +84,7 @@ module.exports = React.createClass({ const name = this._getDisplayName(); const active = -1; const presenceState = member.user ? member.user.presence : null; + const statusMessage = member.user ? member.user.statusMessage : null; const av = ( @@ -106,7 +107,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/RoomTile.js b/src/components/views/rooms/RoomTile.js index 54044e8d65..2c48862ee9 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -251,6 +251,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.currentState.getMembers().length === 2; + let subtext = null; + if (!isInvite && isJoined && looksLikeDm) { + const selfId = MatrixClientPeg.get().getUserId(); + const otherMember = this.props.room.currentState.getMembersExcept([selfId])[0]; + if (otherMember.user && otherMember.user.statusMessage) { + subtext = otherMember.user.statusMessage; + } + } + const classes = classNames({ 'mx_RoomTile': true, 'mx_RoomTile_selected': this.state.selected, @@ -261,6 +272,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.isCollapsed, }); const avatarClasses = classNames({ @@ -291,6 +303,7 @@ module.exports = React.createClass({ const EmojiText = sdk.getComponent('elements.EmojiText'); let label; + let subtextLabel; let tooltip; if (!this.props.collapsed) { const nameClasses = classNames({ @@ -299,6 +312,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 }; @@ -339,6 +354,7 @@ module.exports = React.createClass({
{ label } + { subtextLabel } { badge }
{ /* { incomingCallBox } */ } From cd9ea2b2d79ad01cbd719be69c9e1c185f23d503 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 Dec 2018 12:57:48 -0700 Subject: [PATCH 02/29] Fix alignment of avatars and status messages also introduce the status message to the MemberInfo pane Part of https://github.com/vector-im/riot-web/issues/1528 --- res/css/views/rooms/_EntityTile.scss | 8 ++++++++ res/css/views/rooms/_MemberInfo.scss | 7 +++++++ res/css/views/rooms/_RoomTile.scss | 12 ++++++------ src/components/views/rooms/EntityTile.js | 4 ++-- src/components/views/rooms/MemberInfo.js | 8 ++++++++ src/components/views/rooms/RoomTile.js | 2 +- 6 files changed, 32 insertions(+), 9 deletions(-) diff --git a/res/css/views/rooms/_EntityTile.scss b/res/css/views/rooms/_EntityTile.scss index 031894afde..90d5dc9aa5 100644 --- a/res/css/views/rooms/_EntityTile.scss +++ b/res/css/views/rooms/_EntityTile.scss @@ -111,4 +111,12 @@ limitations under the License. opacity: 0.25; } +.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 5d47275efe..2270e83743 100644 --- a/res/css/views/rooms/_MemberInfo.scss +++ b/res/css/views/rooms/_MemberInfo.scss @@ -110,3 +110,10 @@ limitations under the License. margin-left: 8px; } +.mx_MemberInfo_statusMessage { + font-size: 11px; + opacity: 0.5; + overflow: hidden; + white-space: nowrap; + text-overflow: clip; +} diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 2014bb6404..6a89636d15 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -41,7 +41,7 @@ limitations under the License. .mx_RoomTile_subtext { display: inline-block; - font-size: 0.8em; + font-size: 11px; padding: 0 0 0 7px; margin: 0; overflow: hidden; @@ -62,10 +62,14 @@ limitations under the License. padding-left: 16px; padding-right: 6px; width: 24px; - height: 24px; vertical-align: middle; } +.mx_RoomTile_hasSubtext .mx_RoomTile_avatar { + padding-top: 0; + vertical-align: super; +} + .mx_RoomTile_dm { display: block; position: absolute; @@ -89,10 +93,6 @@ limitations under the License. text-overflow: ellipsis; } -.mx_RoomTile_hasSubtext .mx_RoomTile_avatar { - padding-top: 0; -} - .mx_RoomTile_invite { /* color: rgba(69, 69, 69, 0.5); */ } diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index 27215430a1..a5b75b89bf 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -131,7 +131,7 @@ const EntityTile = React.createClass({ nameClasses += ' mx_EntityTile_name_hover'; } if (this.props.subtextLabel) { - presenceLabel = {this.props.subtextLabel}; + presenceLabel = {this.props.subtextLabel}; } nameEl = (
@@ -148,7 +148,7 @@ const EntityTile = React.createClass({ {name} - {this.props.subtextLabel} + {this.props.subtextLabel}
); } else { diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 17b1311c4f..4eea33e952 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -889,11 +889,13 @@ 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; + statusMessage = this.props.member.user.statusMessage; } const room = this.props.matrixClient.getRoom(this.props.member.roomId); @@ -915,6 +917,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 +938,7 @@ module.exports = withMatrixClient(React.createClass({
{presenceLabel} + {statusLabel}
; } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 2c48862ee9..2f95aab97a 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -272,7 +272,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.isCollapsed, + 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed, }); const avatarClasses = classNames({ From dd382ecb05d85cb37f33f3d93ef84f64e69f02f1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 Dec 2018 13:20:10 -0700 Subject: [PATCH 03/29] Fix a bug with determining 1:1 rooms We shouldn't consider rooms where people have left or been banned --- src/components/views/rooms/RoomTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 2f95aab97a..fa18a0687b 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -252,7 +252,7 @@ module.exports = React.createClass({ const badges = notifBadges || mentionBadges; const isJoined = this.props.room.getMyMembership() === "join"; - const looksLikeDm = this.props.room.currentState.getMembers().length === 2; + const looksLikeDm = this.props.room.getInvitedAndJoinedMemberCount() === 2; let subtext = null; if (!isInvite && isJoined && looksLikeDm) { const selfId = MatrixClientPeg.get().getUserId(); From a91963e5eeaadac4f2f24e50bf1f911ebcd1d7b4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 Dec 2018 18:03:30 -0700 Subject: [PATCH 04/29] Replace the avatar next to the composer with a status entry menu The checkmark might change, and there appears to be some state tracking mishaps that need to be worked out. Part of https://github.com/vector-im/riot-web/issues/1528 --- res/css/_components.scss | 1 + .../avatars/_MemberStatusMessageAvatar.scss | 54 ++++++ res/img/icons-checkmark.svg | 94 ++++++++++ .../avatars/MemberStatusMessageAvatar.js | 165 ++++++++++++++++++ src/components/views/rooms/MessageComposer.js | 4 +- src/i18n/strings/en_EN.json | 2 + 6 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 res/css/views/avatars/_MemberStatusMessageAvatar.scss create mode 100644 res/img/icons-checkmark.svg create mode 100644 src/components/views/avatars/MemberStatusMessageAvatar.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 579856f880..7975a71e4f 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -24,6 +24,7 @@ @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/_TagTileContextMenu.scss"; diff --git a/res/css/views/avatars/_MemberStatusMessageAvatar.scss b/res/css/views/avatars/_MemberStatusMessageAvatar.scss new file mode 100644 index 0000000000..166dc1a2c7 --- /dev/null +++ b/res/css/views/avatars/_MemberStatusMessageAvatar.scss @@ -0,0 +1,54 @@ +/* +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_MemberStatusMessageAvatar { +} + +.mx_MemberStatusMessageAvatar_contextMenu_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_MemberStatusMessageAvatar_contextMenu_submit { + display: inline-block; +} + +.mx_MemberStatusMessageAvatar_contextMenu_submit img { + vertical-align: middle; + margin-left: 8px; +} + +.mx_MemberStatusMessageAvatar_contextMenu hr { + border: 0.5px solid $menu-border-color; +} + +.mx_MemberStatusMessageAvatar_contextMenu_clearIcon { + margin: 5px 15px 5px 5px; + vertical-align: middle; +} + +.mx_MemberStatusMessageAvatar_contextMenu_clear { + padding: 2px; +} + +.mx_MemberStatusMessageAvatar_contextMenu_hasStatus .mx_MemberStatusMessageAvatar_contextMenu_clear { + color: $warning-color; +} diff --git a/res/img/icons-checkmark.svg b/res/img/icons-checkmark.svg new file mode 100644 index 0000000000..748dc61995 --- /dev/null +++ b/res/img/icons-checkmark.svg @@ -0,0 +1,94 @@ + + + +image/svg+xml + + + +icons_create_room +Created with sketchtool. + + + + + + + + + + + + diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js new file mode 100644 index 0000000000..66122f9eee --- /dev/null +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -0,0 +1,165 @@ +/* +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 MemberAvatar from '../avatars/MemberAvatar'; +import classNames from 'classnames'; +import * as ContextualMenu from "../../structures/ContextualMenu"; +import GenericElementContextMenu from "../context_menus/GenericElementContextMenu"; + +export default class MemberStatusMessageAvatar extends React.Component { + constructor(props, context) { + super(props, context); + this._onRoomStateEvents = this._onRoomStateEvents.bind(this); + this._onClick = this._onClick.bind(this); + this._onClearClick = this._onClearClick.bind(this); + this._onSubmit = this._onSubmit.bind(this); + this._onStatusChange = this._onStatusChange.bind(this); + } + + 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.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.statusMessage` + this.setState({message: ev.getContent()["status"]}); + this.forceUpdate(); + } + + _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 + + const contextMenu = this._renderContextMenu(); + + ContextualMenu.createMenu(GenericElementContextMenu, { + chevronOffset: chevronOffset, + chevronFace: 'bottom', + left: x, + top: y, + menuWidth: 190, + element: contextMenu, + }); + } + + async _onClearClick(e) { + await MatrixClientPeg.get().setStatusMessage(""); + this.setState({message: ""}); + } + + _onSubmit(e) { + e.preventDefault(); + MatrixClientPeg.get().setStatusMessage(this.state.message); + } + + _onStatusChange(e) { + this.setState({message: e.target.value}); + } + + _renderContextMenu() { + 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_MemberStatusMessageAvatar_contextMenu": true, + "mx_MemberStatusMessageAvatar_contextMenu_hasStatus": this.state.message, + }); + + return
+ { form } +
+ { clearButton } +
; + } + + render() { + const hasStatus = this.props.member.user ? !!this.props.member.user.statusMessage : false; + + const classes = classNames({ + "mx_MemberStatusMessageAvatar": true, + "mx_MemberStatusMessageAvatar_hasStatus": hasStatus, + }); + + return + + ; + } +} + +MemberStatusMessageAvatar.propTypes = { + member: PropTypes.object.isRequired, + width: PropTypes.number, + height: PropTypes.number, + resizeMethod: PropTypes.string, +}; + +MemberStatusMessageAvatar.defaultProps = { + width: 40, + height: 40, + resizeMethod: 'crop', +}; diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 3fa0f888df..2fc35d80cc 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(
- +
, ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0df81b8e2a..e81ee82ca7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1054,6 +1054,8 @@ "Low Priority": "Low Priority", "Direct Chat": "Direct Chat", "View Community": "View Community", + "Clear status": "Clear status", + "Set a new status...": "Set a new status...", "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.", "Please install Chrome or Firefox for the best experience.": "Please install Chrome or Firefox for the best experience.", From 99f5b9e39b7d81d9f62553e945ce019224464650 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 Dec 2018 18:18:43 -0700 Subject: [PATCH 05/29] Misc cleanup of whitespace --- res/css/views/avatars/_MemberStatusMessageAvatar.scss | 3 --- src/components/views/rooms/EntityTile.js | 8 ++++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/res/css/views/avatars/_MemberStatusMessageAvatar.scss b/res/css/views/avatars/_MemberStatusMessageAvatar.scss index 166dc1a2c7..4027bfa514 100644 --- a/res/css/views/avatars/_MemberStatusMessageAvatar.scss +++ b/res/css/views/avatars/_MemberStatusMessageAvatar.scss @@ -14,9 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_MemberStatusMessageAvatar { -} - .mx_MemberStatusMessageAvatar_contextMenu_message { display: inline-block; border-radius: 3px 0 0 3px; diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index a5b75b89bf..46c5502310 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -126,8 +126,8 @@ const EntityTile = React.createClass({ let nameClasses = 'mx_EntityTile_name'; if (this.props.showPresence) { presenceLabel = ; + currentlyActive={this.props.presenceCurrentlyActive} + presenceState={this.props.presenceState} />; nameClasses += ' mx_EntityTile_name_hover'; } if (this.props.subtextLabel) { @@ -135,9 +135,9 @@ const EntityTile = React.createClass({ } nameEl = (
- + - {name} + { name } {presenceLabel}
From b0b7932f5fc6960262bdbd3b54bf0d1e119a6677 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 Dec 2018 22:26:39 -0700 Subject: [PATCH 06/29] Move status context menu to its own component This fixes a lot of the state bugs such as buttons not updating, etc. This commit also adds the border around the avatar to indicate a status is set. --- res/css/_components.scss | 1 + .../avatars/_MemberStatusMessageAvatar.scss | 37 +------- .../_StatusMessageContextMenu.scss | 51 +++++++++++ .../avatars/MemberStatusMessageAvatar.js | 61 +------------- .../context_menus/StatusMessageContextMenu.js | 84 +++++++++++++++++++ 5 files changed, 143 insertions(+), 91 deletions(-) create mode 100644 res/css/views/context_menus/_StatusMessageContextMenu.scss create mode 100644 src/components/views/context_menus/StatusMessageContextMenu.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 7975a71e4f..7271038444 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -27,6 +27,7 @@ @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/dialogs/_BugReportDialog.scss"; @import "./views/dialogs/_ChangelogDialog.scss"; diff --git a/res/css/views/avatars/_MemberStatusMessageAvatar.scss b/res/css/views/avatars/_MemberStatusMessageAvatar.scss index 4027bfa514..c857b9807b 100644 --- a/res/css/views/avatars/_MemberStatusMessageAvatar.scss +++ b/res/css/views/avatars/_MemberStatusMessageAvatar.scss @@ -14,38 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_MemberStatusMessageAvatar_contextMenu_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_MemberStatusMessageAvatar_contextMenu_submit { - display: inline-block; -} - -.mx_MemberStatusMessageAvatar_contextMenu_submit img { - vertical-align: middle; - margin-left: 8px; -} - -.mx_MemberStatusMessageAvatar_contextMenu hr { - border: 0.5px solid $menu-border-color; -} - -.mx_MemberStatusMessageAvatar_contextMenu_clearIcon { - margin: 5px 15px 5px 5px; - vertical-align: middle; -} - -.mx_MemberStatusMessageAvatar_contextMenu_clear { - padding: 2px; -} - -.mx_MemberStatusMessageAvatar_contextMenu_hasStatus .mx_MemberStatusMessageAvatar_contextMenu_clear { - color: $warning-color; +.mx_MemberStatusMessageAvatar_hasStatus { + border: 2px solid $accent-color; + border-radius: 40px; } diff --git a/res/css/views/context_menus/_StatusMessageContextMenu.scss b/res/css/views/context_menus/_StatusMessageContextMenu.scss new file mode 100644 index 0000000000..465f1b53e4 --- /dev/null +++ b/res/css/views/context_menus/_StatusMessageContextMenu.scss @@ -0,0 +1,51 @@ +/* +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_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/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index 66122f9eee..00b25b3c73 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -16,22 +16,18 @@ 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 MemberAvatar from '../avatars/MemberAvatar'; import classNames from 'classnames'; import * as ContextualMenu from "../../structures/ContextualMenu"; -import GenericElementContextMenu from "../context_menus/GenericElementContextMenu"; +import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu"; export default class MemberStatusMessageAvatar extends React.Component { constructor(props, context) { super(props, context); this._onRoomStateEvents = this._onRoomStateEvents.bind(this); this._onClick = this._onClick.bind(this); - this._onClearClick = this._onClearClick.bind(this); - this._onSubmit = this._onSubmit.bind(this); - this._onStatusChange = this._onStatusChange.bind(this); } componentWillMount() { @@ -75,64 +71,16 @@ export default class MemberStatusMessageAvatar extends React.Component { let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset; y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron - const contextMenu = this._renderContextMenu(); - - ContextualMenu.createMenu(GenericElementContextMenu, { + ContextualMenu.createMenu(StatusMessageContextMenu, { chevronOffset: chevronOffset, chevronFace: 'bottom', left: x, top: y, menuWidth: 190, - element: contextMenu, + user: this.props.member.user, }); } - async _onClearClick(e) { - await MatrixClientPeg.get().setStatusMessage(""); - this.setState({message: ""}); - } - - _onSubmit(e) { - e.preventDefault(); - MatrixClientPeg.get().setStatusMessage(this.state.message); - } - - _onStatusChange(e) { - this.setState({message: e.target.value}); - } - - _renderContextMenu() { - 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_MemberStatusMessageAvatar_contextMenu": true, - "mx_MemberStatusMessageAvatar_contextMenu_hasStatus": this.state.message, - }); - - return
- { form } -
- { clearButton } -
; - } - render() { const hasStatus = this.props.member.user ? !!this.props.member.user.statusMessage : false; @@ -145,8 +93,7 @@ export default class MemberStatusMessageAvatar extends React.Component { + resizeMethod={this.props.resizeMethod} /> ; } } diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js new file mode 100644 index 0000000000..f77669329f --- /dev/null +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -0,0 +1,84 @@ +/* +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 { + constructor(props, context) { + super(props, context); + this._onClearClick = this._onClearClick.bind(this); + this._onSubmit = this._onSubmit.bind(this); + this._onStatusChange = this._onStatusChange.bind(this); + + this.state = { + message: props.user ? props.user.statusMessage : "", + }; + } + + async _onClearClick(e) { + await MatrixClientPeg.get().setStatusMessage(""); + this.setState({message: ""}); + } + + _onSubmit(e) { + e.preventDefault(); + MatrixClientPeg.get().setStatusMessage(this.state.message); + } + + _onStatusChange(e) { + this.setState({message: e.target.value}); + } + + render() { + 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 } +
; + } +} + +StatusMessageContextMenu.propTypes = { + // js-sdk User object. Not required because it might not exist. + user: PropTypes.object, +}; From f2649f7807cd65b787a03e0a78a3f2b77af22987 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 Dec 2018 23:07:03 -0700 Subject: [PATCH 07/29] Use the now-prefixed js-sdk status message API See https://github.com/matrix-org/matrix-js-sdk/commit/08b3dfa3b5b5d0b63272f0b80b9fdd88d0795c45 --- src/components/views/avatars/MemberStatusMessageAvatar.js | 6 +++--- .../views/context_menus/StatusMessageContextMenu.js | 6 +++--- src/components/views/rooms/MemberInfo.js | 2 +- src/components/views/rooms/MemberTile.js | 2 +- src/components/views/rooms/RoomTile.js | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index 00b25b3c73..2a2c97ee7c 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -40,7 +40,7 @@ export default class MemberStatusMessageAvatar extends React.Component { MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents); if (this.props.member.user) { - this.setState({message: this.props.member.user.statusMessage}); + this.setState({message: this.props.member.user._unstable_statusMessage}); } else { this.setState({message: ""}); } @@ -55,7 +55,7 @@ export default class MemberStatusMessageAvatar extends React.Component { _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.statusMessage` + // TODO: We should be relying on `this.props.member.user._unstable_statusMessage` this.setState({message: ev.getContent()["status"]}); this.forceUpdate(); } @@ -82,7 +82,7 @@ export default class MemberStatusMessageAvatar extends React.Component { } render() { - const hasStatus = this.props.member.user ? !!this.props.member.user.statusMessage : false; + const hasStatus = this.props.member.user ? !!this.props.member.user._unstable_statusMessage : false; const classes = classNames({ "mx_MemberStatusMessageAvatar": true, diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js index f77669329f..a3b31420f6 100644 --- a/src/components/views/context_menus/StatusMessageContextMenu.js +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -29,18 +29,18 @@ export default class StatusMessageContextMenu extends React.Component { this._onStatusChange = this._onStatusChange.bind(this); this.state = { - message: props.user ? props.user.statusMessage : "", + message: props.user ? props.user._unstable_statusMessage : "", }; } async _onClearClick(e) { - await MatrixClientPeg.get().setStatusMessage(""); + await MatrixClientPeg.get()._unstable_setStatusMessage(""); this.setState({message: ""}); } _onSubmit(e) { e.preventDefault(); - MatrixClientPeg.get().setStatusMessage(this.state.message); + MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message); } _onStatusChange(e) { diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 4eea33e952..6bcdc53d4c 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -895,7 +895,7 @@ module.exports = withMatrixClient(React.createClass({ presenceState = this.props.member.user.presence; presenceLastActiveAgo = this.props.member.user.lastActiveAgo; presenceCurrentlyActive = this.props.member.user.currentlyActive; - statusMessage = this.props.member.user.statusMessage; + statusMessage = this.props.member.user._unstable_statusMessage; } const room = this.props.matrixClient.getRoom(this.props.member.roomId); diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index d246b37234..96a8e0b515 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -84,7 +84,7 @@ module.exports = React.createClass({ const name = this._getDisplayName(); const active = -1; const presenceState = member.user ? member.user.presence : null; - const statusMessage = member.user ? member.user.statusMessage : null; + const statusMessage = member.user ? member.user._unstable_statusMessage : null; const av = ( diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index fa18a0687b..91c5d0321d 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -257,8 +257,8 @@ module.exports = React.createClass({ if (!isInvite && isJoined && looksLikeDm) { const selfId = MatrixClientPeg.get().getUserId(); const otherMember = this.props.room.currentState.getMembersExcept([selfId])[0]; - if (otherMember.user && otherMember.user.statusMessage) { - subtext = otherMember.user.statusMessage; + if (otherMember.user && otherMember.user._unstable_statusMessage) { + subtext = otherMember.user._unstable_statusMessage; } } From c6f35428d751432403a71d2e0e025e92723f73af Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 13 Dec 2018 09:37:54 -0700 Subject: [PATCH 08/29] Update checkmark icon --- res/img/icons-checkmark.svg | 109 ++++++------------------------------ 1 file changed, 16 insertions(+), 93 deletions(-) diff --git a/res/img/icons-checkmark.svg b/res/img/icons-checkmark.svg index 748dc61995..3c5392003d 100644 --- a/res/img/icons-checkmark.svg +++ b/res/img/icons-checkmark.svg @@ -1,94 +1,17 @@ - - - -image/svg+xml - - - -icons_create_room -Created with sketchtool. - - - - - - - - - - - + + + + Tick + Created with Sketch. + + + + + + + + + + + From 63658e0441e9be4ad1b26797e9621cc921041768 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 13 Dec 2018 14:29:12 -0700 Subject: [PATCH 09/29] Add a missing null check --- src/components/views/rooms/RoomTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 91c5d0321d..676edc1ea2 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -257,7 +257,7 @@ module.exports = React.createClass({ if (!isInvite && isJoined && looksLikeDm) { const selfId = MatrixClientPeg.get().getUserId(); const otherMember = this.props.room.currentState.getMembersExcept([selfId])[0]; - if (otherMember.user && otherMember.user._unstable_statusMessage) { + if (otherMember && otherMember.user && otherMember.user._unstable_statusMessage) { subtext = otherMember.user._unstable_statusMessage; } } From 7efd82f7138393e8a0d2caa84bf808ff5c9c888e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 14 Dec 2018 13:44:40 -0700 Subject: [PATCH 10/29] Disable password managers on the status form --- src/components/views/context_menus/StatusMessageContextMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js index a3b31420f6..5f137a12a5 100644 --- a/src/components/views/context_menus/StatusMessageContextMenu.js +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -48,7 +48,7 @@ export default class StatusMessageContextMenu extends React.Component { } render() { - const form =
+ const form =
From 7b0766a30352c0ebf06e91e11ed7c2ab0c993ad5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 14 Dec 2018 13:49:35 -0700 Subject: [PATCH 11/29] Apply 50% opacity to the checkmark when there is no status --- res/css/views/context_menus/_StatusMessageContextMenu.scss | 4 ++++ .../views/context_menus/StatusMessageContextMenu.js | 7 ++++++- src/i18n/strings/en_EN.json | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/res/css/views/context_menus/_StatusMessageContextMenu.scss b/res/css/views/context_menus/_StatusMessageContextMenu.scss index 465f1b53e4..873ad99495 100644 --- a/res/css/views/context_menus/_StatusMessageContextMenu.scss +++ b/res/css/views/context_menus/_StatusMessageContextMenu.scss @@ -28,6 +28,10 @@ limitations under the License. display: inline-block; } +.mx_StatusMessageContextMenu_submitFaded { + opacity: 0.5; +} + .mx_StatusMessageContextMenu_submit img { vertical-align: middle; margin-left: 8px; diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js index 5f137a12a5..243164301d 100644 --- a/src/components/views/context_menus/StatusMessageContextMenu.js +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -48,11 +48,16 @@ export default class StatusMessageContextMenu extends React.Component { } render() { + const formSubmitClasses = classNames({ + "mx_StatusMessageContextMenu_submit": true, + "mx_StatusMessageContextMenu_submitFaded": !this.state.message, // no message == faded + }); + const form =
- +
; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e81ee82ca7..5c7e067f95 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1053,9 +1053,9 @@ "Forget": "Forget", "Low Priority": "Low Priority", "Direct Chat": "Direct Chat", - "View Community": "View Community", - "Clear status": "Clear status", "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.", "Please install Chrome or Firefox for the best experience.": "Please install Chrome or Firefox for the best experience.", From d304c35b38e49a3a6b40c5d1ece74c650b51b08f Mon Sep 17 00:00:00 2001 From: Willem Mulder Date: Fri, 14 Dec 2018 14:26:35 +0100 Subject: [PATCH 12/29] Allow widgets to autoplay media This is useful for e.g. webcam streams in widgets. Signed-off-by: Willem Mulder --- src/components/views/elements/AppTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 23b24adbb4..e36561cc15 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -544,7 +544,7 @@ export default class AppTile extends React.Component { // Additional iframe feature pemissions // (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/) - const iframeFeatures = "microphone; camera; encrypted-media;"; + const iframeFeatures = "microphone; camera; encrypted-media; autoplay;"; const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' '); From 1c4621c98e7b09b14bc7da6aa0cf4fc4e075f2d5 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 18 Dec 2018 00:26:25 +0000 Subject: [PATCH 13/29] Link to CONTRIBUTING from JS SDK The JS SDK's CONTRIBUTING file is a bit simpler to read. The Synapse version previously used includes mentions of Python lint tools that don't apply here. Signed-off-by: J. Ryan Stinnett --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 99025f0e0a..f7c8c8b1c5 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,4 +1,4 @@ Contributing code to The React SDK ================================== -matrix-react-sdk follows the same pattern as https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst +matrix-react-sdk follows the same pattern as https://github.com/matrix-org/matrix-js-sdk/blob/master/CONTRIBUTING.rst From c6da61f1de8bd992715a82184eb45cbe80399104 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 17 Dec 2018 18:47:33 -0700 Subject: [PATCH 14/29] Make sure to grab the InlineSpinner object --- src/components/structures/GroupView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index e2fd15aa89..56e6575793 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -1077,6 +1077,7 @@ export default React.createClass({ }, _getJoinableNode: function() { + const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); return this.state.editing ?

{ _t('Who can join this community?') } From 2b14f2af5c3f54b61a743968c696d2ffc989ef73 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 13 Dec 2018 12:10:08 +0000 Subject: [PATCH 15/29] Clean up when new key backup version fails to backup If creating a new key backup version succeeds but backing up to it fails, delete the version to avoid surprises. In addition, this converts the creation of a new key backup to async / await style. Signed-off-by: J. Ryan Stinnett --- .../keybackup/CreateKeyBackupDialog.js | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 6b115b890f..0db9d0699b 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -92,25 +92,33 @@ export default React.createClass({ }); }, - _createBackup: function() { + _createBackup: async function() { this.setState({ phase: PHASE_BACKINGUP, error: null, }); - this._createBackupPromise = MatrixClientPeg.get().createKeyBackupVersion( - this._keyBackupInfo, - ).then((info) => { - return MatrixClientPeg.get().backupAllGroupSessions(info.version); - }).then(() => { + let info; + try { + info = await MatrixClientPeg.get().createKeyBackupVersion( + this._keyBackupInfo, + ); + await MatrixClientPeg.get().backupAllGroupSessions(info.version); this.setState({ phase: PHASE_DONE, }); - }).catch(e => { + } catch (e) { console.log("Error creating key backup", e); + // TODO: If creating a version succeeds, but backup fails, should we + // delete the version, disable backup, or do nothing? If we just + // disable without deleting, we'll enable on next app reload since + // it is trusted. + if (info) { + MatrixClientPeg.get().deleteKeyBackupVersion(info.version); + } this.setState({ error: e, }); - }); + } }, _onCancel: function() { From acc2e98355fcde7b1cfee2b7e49bea088c02af5d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 13 Dec 2018 15:55:48 +0000 Subject: [PATCH 16/29] Add New Recovery Method dialog Adds a New Recovery Method dialog which is shown when key backup fails because of a version mismatch / version not found error. The set up button in the dialog currently only marks a device as verified (via a verification prompt) instead of the eventual restore and cross-sign flow, since those pieces don't exist yet. Signed-off-by: J. Ryan Stinnett --- res/css/_common.scss | 2 +- res/css/_components.scss | 1 + .../keybackup/_NewRecoveryMethodDialog.scss | 41 +++++++ res/img/e2e/lock-warning.svg | 1 + .../keybackup/NewRecoveryMethodDialog.js | 110 ++++++++++++++++++ src/components/structures/MatrixChat.js | 5 + src/components/views/dialogs/BaseDialog.js | 3 +- .../views/settings/KeyBackupPanel.js | 5 +- src/i18n/strings/en_EN.json | 8 +- 9 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss create mode 100644 res/img/e2e/lock-warning.svg create mode 100644 src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js diff --git a/res/css/_common.scss b/res/css/_common.scss index 11e04f5dc0..97ae5412e1 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -34,7 +34,7 @@ body { -webkit-font-smoothing: subpixel-antialiased; } -div.error, div.warning { +.error, .warning { color: $warning-color; } diff --git a/res/css/_components.scss b/res/css/_components.scss index 579856f880..48aa211fd8 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -47,6 +47,7 @@ @import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_UnknownDeviceDialog.scss"; @import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss"; +@import "./views/dialogs/keybackup/_NewRecoveryMethodDialog.scss"; @import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss"; @import "./views/directory/_NetworkDropdown.scss"; @import "./views/elements/_AccessibleButton.scss"; diff --git a/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss b/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss new file mode 100644 index 0000000000..370f82d9ab --- /dev/null +++ b/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss @@ -0,0 +1,41 @@ +/* +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_NewRecoveryMethodDialog .mx_Dialog_title { + margin-bottom: 32px; +} + +.mx_NewRecoveryMethodDialog_title { + position: relative; + padding-left: 45px; + padding-bottom: 10px; + + &:before { + mask: url("../../../img/e2e/lock-warning.svg"); + mask-repeat: no-repeat; + background-color: $primary-fg-color; + content: ""; + position: absolute; + top: -6px; + right: 0; + bottom: 0; + left: 0; + } +} + +.mx_NewRecoveryMethodDialog .mx_Dialog_buttons { + margin-top: 36px; +} diff --git a/res/img/e2e/lock-warning.svg b/res/img/e2e/lock-warning.svg new file mode 100644 index 0000000000..a984ed85a0 --- /dev/null +++ b/res/img/e2e/lock-warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js new file mode 100644 index 0000000000..e88e0444bc --- /dev/null +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -0,0 +1,110 @@ +/* +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 sdk from "../../../../index"; +import MatrixClientPeg from '../../../../MatrixClientPeg'; +import dis from "../../../../dispatcher"; +import { _t } from "../../../../languageHandler"; +import Modal from "../../../../Modal"; + +export default class NewRecoveryMethodDialog extends React.PureComponent { + static propTypes = { + onFinished: PropTypes.func.isRequired, + } + + onGoToSettingsClick = () => { + this.props.onFinished(); + dis.dispatch({ action: 'view_user_settings' }); + } + + onSetupClick = async() => { + // 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. 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. + 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); + return; + } + + let unverifiedDevice; + for (const sig of backupSigStatus.sigs) { + if (!sig.device.isVerified()) { + unverifiedDevice = sig.device; + break; + } + } + if (!unverifiedDevice) { + console.log("Unable to find a device to verify."); + return; + } + + const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); + Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, { + userId: MatrixClientPeg.get().credentials.userId, + device: unverifiedDevice, + onFinished: this.props.onFinished, + }); + } + + render() { + const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); + const DialogButtons = sdk.getComponent("views.elements.DialogButtons"); + const title = + {_t("New Recovery Method")} + ; + + return ( + +
+

{_t( + "A new recovery passphrase and key for Secure " + + "Messages has been detected.", + )}

+

{_t( + "Setting up Secure Messages on this device " + + "will re-encrypt this device's message history with " + + "the new recovery method.", + )}

+

{_t( + "If you didn't set the new recovery method, an " + + "attacker may be trying to access your account. " + + "Change your account password and set a new recovery " + + "method immediately in Settings.", + )}

+ +
+
+ ); + } +} diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4517304453..fd95276445 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1430,6 +1430,11 @@ export default React.createClass({ break; } }); + cli.on("crypto.keyBackupFailed", () => { + Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method', + import('../../async-components/views/dialogs/keybackup/NewRecoveryMethodDialog'), + ); + }); // Fire the tinter right on startup to ensure the default theme is applied // A later sync can/will correct the tint to be the right value for the user diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 8ec417a59b..3e9052cc34 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -57,8 +57,7 @@ export default React.createClass({ className: PropTypes.string, // Title for the dialog. - // (could probably actually be something more complicated than a string if desired) - title: PropTypes.string.isRequired, + title: PropTypes.node.isRequired, // children should be the content of the dialog children: PropTypes.node, diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index b08f4d0e78..03b98d28a0 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -154,6 +154,7 @@ export default class KeyBackupPanel extends React.Component { } let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => { + const deviceName = sig.device.getDisplayName() || sig.device.deviceId; const sigStatusSubstitutions = { validity: sub => @@ -163,7 +164,7 @@ export default class KeyBackupPanel extends React.Component { {sub} , - device: sub => {sig.device.getDisplayName()}, + device: sub => {deviceName}, }; let sigStatus; if (sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()) { @@ -174,7 +175,7 @@ export default class KeyBackupPanel extends React.Component { } else if (sig.valid && sig.device.isVerified()) { sigStatus = _t( "Backup has a valid signature from " + - "verified device x", + "verified device ", {}, sigStatusSubstitutions, ); } else if (sig.valid && !sig.device.isVerified()) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 00f781ea5b..b5a5762cc4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -351,7 +351,7 @@ "This device is uploading keys to this backup": "This device is uploading keys to this backup", "This device is not uploading keys to this backup": "This device is not uploading keys to this backup", "Backup has a valid signature from this device": "Backup has a valid signature from this device", - "Backup has a valid signature from verified device x": "Backup has a valid signature from verified device x", + "Backup has a valid signature from verified device ": "Backup has a valid signature from verified device ", "Backup has a valid signature from unverified device ": "Backup has a valid signature from unverified device ", "Backup has an invalid signature from verified device ": "Backup has an invalid signature from verified device ", "Backup has an invalid signature from unverified device ": "Backup has an invalid signature from unverified device ", @@ -1401,6 +1401,12 @@ "Retry": "Retry", "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.", "If you don't want to set this up now, you can later in Settings.": "If you don't want to set this up now, you can later in Settings.", + "New Recovery Method": "New Recovery Method", + "A new recovery passphrase and key for Secure Messages has been detected.": "A new recovery passphrase and key for Secure Messages has been detected.", + "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.": "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.", + "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", + "Set up Secure Messages": "Set up Secure Messages", + "Go to Settings": "Go to Settings", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" From b036e59021d95163c1816346f4a1014ae7667927 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 18 Dec 2018 15:14:55 +0000 Subject: [PATCH 17/29] Enable ESLint rule to require defined components in JSX Signed-off-by: J. Ryan Stinnett --- .eslintrc.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 62d24ea707..971809f851 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -47,6 +47,9 @@ module.exports = { }], "react/jsx-key": ["error"], + // Components in JSX should always be defined. + "react/jsx-no-undef": "error", + // Assert no spacing in JSX curly brackets // // From 37b3644fd91e49a8d39b779bd9c1ade4286ee450 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Dec 2018 17:40:30 +0000 Subject: [PATCH 18/29] React-sdk changes to support sandboxed electron --- src/BasePlatform.js | 6 +-- src/components/structures/UserSettings.js | 45 ++++++++++++----------- src/components/views/elements/AppTile.js | 34 ----------------- 3 files changed, 25 insertions(+), 60 deletions(-) diff --git a/src/BasePlatform.js b/src/BasePlatform.js index abc9aa0bed..79f0d69e2c 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -3,6 +3,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2016 OpenMarket Ltd +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. @@ -105,11 +106,6 @@ export default class BasePlatform { return "Not implemented"; } - isElectron(): boolean { return false; } - - setupScreenSharingForIframe() { - } - /** * Restarts the application, without neccessarily reloading * any application code diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 6f932d71e1..b9dbe345c5 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -188,9 +188,11 @@ module.exports = React.createClass({ phase: "UserSettings.LOADING", // LOADING, DISPLAY email_add_pending: false, vectorVersion: undefined, + canSelfUpdate: null, rejectingInvites: false, mediaDevices: null, ignoredUsers: [], + autoLaunchEnabled: null, }; }, @@ -209,6 +211,13 @@ module.exports = React.createClass({ }, (e) => { console.log("Failed to fetch app version", e); }); + + PlatformPeg.get().canSelfUpdate().then((canUpdate) => { + if (this._unmounted) return; + this.setState({ + canSelfUpdate: canUpdate, + }); + }); } this._refreshMediaDevices(); @@ -227,11 +236,12 @@ module.exports = React.createClass({ }); this._refreshFromServer(); - if (PlatformPeg.get().isElectron()) { - const {ipcRenderer} = require('electron'); - - ipcRenderer.on('settings', this._electronSettings); - ipcRenderer.send('settings_get'); + if (PlatformPeg.get().supportsAutoLaunch()) { + PlatformPeg.get().getAutoLaunchEnabled().then(enabled => { + this.setState({ + autoLaunchEnabled: enabled, + }); + }); } this.setState({ @@ -262,11 +272,6 @@ module.exports = React.createClass({ if (cli) { cli.removeListener("RoomMember.membership", this._onInviteStateChange); } - - if (PlatformPeg.get().isElectron()) { - const {ipcRenderer} = require('electron'); - ipcRenderer.removeListener('settings', this._electronSettings); - } }, // `UserSettings` assumes that the client peg will not be null, so give it some @@ -285,10 +290,6 @@ module.exports = React.createClass({ }); }, - _electronSettings: function(ev, settings) { - this.setState({ electron_settings: settings }); - }, - _refreshMediaDevices: function(stream) { if (stream) { // kill stream so that we don't leave it lingering around with webcam enabled etc @@ -967,7 +968,7 @@ module.exports = React.createClass({ _renderCheckUpdate: function() { const platform = PlatformPeg.get(); - if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) { + if (this.state.canSelfUpdate) { return

{ _t('Updates') }

@@ -1012,8 +1013,7 @@ module.exports = React.createClass({ }, _renderElectronSettings: function() { - const settings = this.state.electron_settings; - if (!settings) return; + if (!PlatformPeg.get().supportsAutoLaunch()) return; // TODO: This should probably be a granular setting, but it only applies to electron // and ends up being get/set outside of matrix anyways (local system setting). @@ -1023,7 +1023,7 @@ module.exports = React.createClass({
@@ -1033,8 +1033,11 @@ module.exports = React.createClass({ }, _onAutoLaunchChanged: function(e) { - const {ipcRenderer} = require('electron'); - ipcRenderer.send('settings_set', 'auto-launch', e.target.checked); + PlatformPeg.get().setAutoLaunchEnabled(e.target.checked).then(() => { + this.setState({ + autoLaunchEnabled: e.target.checked, + }); + }); }, _mapWebRtcDevicesToSpans: function(devices) { @@ -1393,7 +1396,7 @@ module.exports = React.createClass({ { this._renderBulkOptions() } { this._renderBugReport() } - { PlatformPeg.get().isElectron() && this._renderElectronSettings() } + { this._renderElectronSettings() } { this._renderAnalyticsControl() } diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 23b24adbb4..7eae17ace8 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -49,7 +49,6 @@ export default class AppTile extends React.Component { this.state = this._getNewState(props); this._onAction = this._onAction.bind(this); - this._onMessage = this._onMessage.bind(this); this._onLoaded = this._onLoaded.bind(this); this._onEditClick = this._onEditClick.bind(this); this._onDeleteClick = this._onDeleteClick.bind(this); @@ -143,10 +142,6 @@ export default class AppTile extends React.Component { } componentDidMount() { - // Legacy Jitsi widget messaging -- TODO replace this with standard widget - // postMessaging API - window.addEventListener('message', this._onMessage, false); - // Widget action listeners this.dispatcherRef = dis.register(this._onAction); } @@ -155,9 +150,6 @@ export default class AppTile extends React.Component { // Widget action listeners dis.unregister(this.dispatcherRef); - // Jitsi listener - window.removeEventListener('message', this._onMessage); - // if it's not remaining on screen, get rid of the PersistedElement container if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) { ActiveWidgetStore.destroyPersistentWidget(); @@ -233,32 +225,6 @@ export default class AppTile extends React.Component { } } - // Legacy Jitsi widget messaging - // TODO -- This should be replaced with the new widget postMessaging API - _onMessage(event) { - if (this.props.type !== 'jitsi') { - return; - } - if (!event.origin) { - event.origin = event.originalEvent.origin; - } - - const widgetUrlObj = url.parse(this.state.widgetUrl); - const eventOrigin = url.parse(event.origin); - if ( - eventOrigin.protocol !== widgetUrlObj.protocol || - eventOrigin.host !== widgetUrlObj.host - ) { - return; - } - - if (event.data.widgetAction === 'jitsi_iframe_loaded') { - const iframe = this.refs.appFrame.contentWindow - .document.querySelector('iframe[id^="jitsiConferenceFrame"]'); - PlatformPeg.get().setupScreenSharingForIframe(iframe); - } - } - _canUserModify() { // User widgets should always be modifiable by their creator if (this.props.userWidget && MatrixClientPeg.get().credentials.userId === this.props.creatorUserId) { From ef60a34180c973511f4a436cd21faa98f297a845 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 18 Dec 2018 10:53:37 -0700 Subject: [PATCH 19/29] Clean up and follow code style --- res/css/views/rooms/_RoomTile.scss | 1 - .../avatars/MemberStatusMessageAvatar.js | 39 +++++++++---------- .../context_menus/StatusMessageContextMenu.js | 25 ++++++------ 3 files changed, 29 insertions(+), 36 deletions(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 6a89636d15..b5ac9aadc6 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -35,7 +35,6 @@ limitations under the License. .mx_RoomTile_nameContainer { display: inline-block; width: 180px; - //height: 24px; vertical-align: middle; } diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index 2a2c97ee7c..189641eb8a 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -23,11 +23,22 @@ import classNames from 'classnames'; import * as ContextualMenu from "../../structures/ContextualMenu"; import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu"; -export default class MemberStatusMessageAvatar extends React.Component { +export default class MemberStatusMessageAvatar extends React.PureComponent { + 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); - this._onRoomStateEvents = this._onRoomStateEvents.bind(this); - this._onClick = this._onClick.bind(this); } componentWillMount() { @@ -52,15 +63,14 @@ export default class MemberStatusMessageAvatar extends React.Component { } } - _onRoomStateEvents(ev, state) { + _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` this.setState({message: ev.getContent()["status"]}); - this.forceUpdate(); - } + }; - _onClick(e) { + _onClick = (e) => { e.stopPropagation(); const elementRect = e.target.getBoundingClientRect(); @@ -79,7 +89,7 @@ export default class MemberStatusMessageAvatar extends React.Component { menuWidth: 190, user: this.props.member.user, }); - } + }; render() { const hasStatus = this.props.member.user ? !!this.props.member.user._unstable_statusMessage : false; @@ -97,16 +107,3 @@ export default class MemberStatusMessageAvatar extends React.Component { ; } } - -MemberStatusMessageAvatar.propTypes = { - member: PropTypes.object.isRequired, - width: PropTypes.number, - height: PropTypes.number, - resizeMethod: PropTypes.string, -}; - -MemberStatusMessageAvatar.defaultProps = { - width: 40, - height: 40, - resizeMethod: 'crop', -}; diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js index 243164301d..d062fc2a3e 100644 --- a/src/components/views/context_menus/StatusMessageContextMenu.js +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -22,30 +22,32 @@ 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._onClearClick = this._onClearClick.bind(this); - this._onSubmit = this._onSubmit.bind(this); - this._onStatusChange = this._onStatusChange.bind(this); this.state = { message: props.user ? props.user._unstable_statusMessage : "", }; } - async _onClearClick(e) { + _onClearClick = async (e) => { await MatrixClientPeg.get()._unstable_setStatusMessage(""); this.setState({message: ""}); - } + }; - _onSubmit(e) { + _onSubmit = (e) => { e.preventDefault(); MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message); - } + }; - _onStatusChange(e) { + _onStatusChange = (e) => { this.setState({message: e.target.value}); - } + }; render() { const formSubmitClasses = classNames({ @@ -82,8 +84,3 @@ export default class StatusMessageContextMenu extends React.Component {
; } } - -StatusMessageContextMenu.propTypes = { - // js-sdk User object. Not required because it might not exist. - user: PropTypes.object, -}; From 3a8b9ab8501aed59808146f11dd802b72f7398de Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Dec 2018 17:57:23 +0000 Subject: [PATCH 20/29] unused import --- src/components/views/elements/AppTile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 7eae17ace8..1ce32c852c 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -22,7 +22,6 @@ import qs from 'querystring'; import React from 'react'; import PropTypes from 'prop-types'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import PlatformPeg from '../../../PlatformPeg'; import ScalarAuthClient from '../../../ScalarAuthClient'; import WidgetMessaging from '../../../WidgetMessaging'; import TintableSvgButton from './TintableSvgButton'; From d20a934642a117253ae8b138a4ca2de71bf05866 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 18 Dec 2018 11:04:16 -0700 Subject: [PATCH 21/29] Appease the linter --- src/components/views/context_menus/StatusMessageContextMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js index d062fc2a3e..f07220db44 100644 --- a/src/components/views/context_menus/StatusMessageContextMenu.js +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -35,7 +35,7 @@ export default class StatusMessageContextMenu extends React.Component { }; } - _onClearClick = async (e) => { + _onClearClick = async(e) => { await MatrixClientPeg.get()._unstable_setStatusMessage(""); this.setState({message: ""}); }; From 04c9fff6ce9cb4b5ac69adddbac8f709cf6afdb3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 18 Dec 2018 15:11:08 -0700 Subject: [PATCH 22/29] Add a feature flag for custom status messages --- .../views/avatars/MemberStatusMessageAvatar.js | 10 +++++++++- src/components/views/rooms/MemberInfo.js | 6 +++++- src/components/views/rooms/MemberTile.js | 8 +++++++- src/components/views/rooms/RoomTile.js | 3 ++- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.js | 6 ++++++ 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index 189641eb8a..814144b64d 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -22,8 +22,9 @@ 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.PureComponent { +export default class MemberStatusMessageAvatar extends React.Component { static propTypes = { member: PropTypes.object.isRequired, width: PropTypes.number, @@ -92,6 +93,13 @@ export default class MemberStatusMessageAvatar extends React.PureComponent { }; render() { + if (!SettingsStore.isFeatureEnabled("feature_custom_status")) { + return ; + } + const hasStatus = this.props.member.user ? !!this.props.member.user._unstable_statusMessage : false; const classes = classNames({ diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 6bcdc53d4c..1829413dfd 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', @@ -895,7 +896,10 @@ module.exports = withMatrixClient(React.createClass({ presenceState = this.props.member.user.presence; presenceLastActiveAgo = this.props.member.user.lastActiveAgo; presenceCurrentlyActive = this.props.member.user.currentlyActive; - statusMessage = this.props.member.user._unstable_statusMessage; + + if (SettingsStore.isFeatureEnabled("feature_custom_status")) { + statusMessage = this.props.member.user._unstable_statusMessage; + } } const room = this.props.matrixClient.getRoom(this.props.member.roomId); diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 96a8e0b515..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'; @@ -84,7 +86,11 @@ module.exports = React.createClass({ const name = this._getDisplayName(); const active = -1; const presenceState = member.user ? member.user.presence : null; - const statusMessage = member.user ? member.user._unstable_statusMessage : null; + + let statusMessage = null; + if (member.user && SettingsStore.isFeatureEnabled("feature_custom_status")) { + statusMessage = member.user._unstable_statusMessage; + } const av = ( diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 676edc1ea2..a054246b4f 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', @@ -254,7 +255,7 @@ module.exports = React.createClass({ const isJoined = this.props.room.getMyMembership() === "join"; const looksLikeDm = this.props.room.getInvitedAndJoinedMemberCount() === 2; let subtext = null; - if (!isInvite && isJoined && looksLikeDm) { + 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) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5c7e067f95..0bd1858b90 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -255,6 +255,7 @@ "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", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index c9a4ecdebe..1cac8559d1 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"), From 45f05092ed99a15d28ad3a5b9af57ed2fde1a7a9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 19 Dec 2018 10:21:43 -0700 Subject: [PATCH 23/29] Add a comment to describe why we're not using the property we should be --- src/components/views/avatars/MemberStatusMessageAvatar.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index 814144b64d..aebd1741b7 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -68,6 +68,9 @@ export default class MemberStatusMessageAvatar extends React.Component { 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"]}); }; From a22a9492a0d1eb74a6cb3b0a6eaca55e998d799a Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 18 Dec 2018 14:34:02 +0000 Subject: [PATCH 24/29] Remove duplicate CSS file for CreateKeyBackupDialog Signed-off-by: J. Ryan Stinnett --- res/css/_components.scss | 1 - .../views/dialogs/_CreateKeyBackupDialog.scss | 25 ------------------- .../keybackup/_CreateKeyBackupDialog.scss | 9 ++++++- 3 files changed, 8 insertions(+), 27 deletions(-) delete mode 100644 res/css/views/dialogs/_CreateKeyBackupDialog.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index 48aa211fd8..9f50856ce0 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -33,7 +33,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/dialogs/_CreateKeyBackupDialog.scss deleted file mode 100644 index a422cf858c..0000000000 --- a/res/css/views/dialogs/_CreateKeyBackupDialog.scss +++ /dev/null @@ -1,25 +0,0 @@ -/* -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_CreateKeyBackupDialog { - padding-right: 40px; -} - -.mx_CreateKeyBackupDialog_recoveryKey { - padding: 20px; - color: $info-plinth-fg-color; - background-color: $info-plinth-bg-color; -} diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index 2cb6b11c0c..1addb99792 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 @@ -54,4 +58,7 @@ limitations under the License. .mx_CreateKeyBackupDialog_recoveryKey { width: 300px; + padding: 20px; + color: $info-plinth-fg-color; + background-color: $info-plinth-bg-color; } From 9c4ff4048a596b7bd824fd9b6362a4fb23c1a5ae Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 18 Dec 2018 17:26:11 +0000 Subject: [PATCH 25/29] Convert show recovery key to flexbox This allows the buttons to fit on a single line and flows a bit better at low widths. Signed-off-by: J. Ryan Stinnett --- .../keybackup/_CreateKeyBackupDialog.scss | 17 +++++++++-- .../keybackup/CreateKeyBackupDialog.js | 28 +++++++++---------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index 1addb99792..0b686dbcdc 100644 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -52,13 +52,24 @@ limitations under the License. float: right; } -.mx_CreateKeyBackupDialog_recoveryKeyButtons { - float: right; +.mx_CreateKeyBackupDialog_recoveryKeyHeader { + margin-bottom: 1em; +} + +.mx_CreateKeyBackupDialog_recoveryKeyContainer { + display: flex; } .mx_CreateKeyBackupDialog_recoveryKey { - width: 300px; + width: 275px; padding: 20px; color: $info-plinth-fg-color; background-color: $info-plinth-bg-color; + margin-right: 8px; +} + +.mx_CreateKeyBackupDialog_recoveryKeyButtons { + flex: 1; + display: flex; + align-items: center; } diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 0db9d0699b..6593472670 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -351,21 +351,21 @@ export default React.createClass({

{_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} +
+
+ + +


From a597ad10b0a8632688d1da40d6db5cabb0532722 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 19 Dec 2018 10:44:31 +0000 Subject: [PATCH 26/29] Add a few more zxcvbn strings Signed-off-by: J. Ryan Stinnett --- src/i18n/strings/en_EN.json | 2 ++ src/utils/PasswordScorer.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b5a5762cc4..5f436a4610 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -250,6 +250,8 @@ "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.", 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 From 24f0123dedb8eb8b6e9ea39efcb8c43fd8ec4465 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 19 Dec 2018 10:56:50 +0000 Subject: [PATCH 27/29] Convert pass phrase entry to flexbox Signed-off-by: J. Ryan Stinnett --- .../keybackup/_CreateKeyBackupDialog.scss | 16 ++++--- .../keybackup/CreateKeyBackupDialog.js | 44 ++++++++++--------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index 0b686dbcdc..424ffbd0a8 100644 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -29,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%; @@ -42,14 +46,16 @@ 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_recoveryKeyHeader { @@ -61,11 +67,11 @@ limitations under the License. } .mx_CreateKeyBackupDialog_recoveryKey { - width: 275px; + width: 262px; padding: 20px; color: $info-plinth-fg-color; background-color: $info-plinth-bg-color; - margin-right: 8px; + margin-right: 12px; } .mx_CreateKeyBackupDialog_recoveryKeyButtons { diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 6593472670..dda378e792 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}
Date: Wed, 19 Dec 2018 11:06:47 +0000 Subject: [PATCH 28/29] Use primary styling on download / clipboard key actions Signed-off-by: J. Ryan Stinnett --- .../views/dialogs/keybackup/CreateKeyBackupDialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index dda378e792..a097e84cdb 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -363,10 +363,10 @@ export default React.createClass({ {this._keyBackupInfo.recovery_key}
- -
From fd94dc686f66feddc1e336de9cfa83e24fabab8d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 19 Dec 2018 17:26:40 +0000 Subject: [PATCH 29/29] Handle errors when fetching commits for changelog It's possible to get errors when fetching commits (for example, if the rate limit is exceeded), so this will handle the error case and display it instead of an infinite spinner. Signed-off-by: J. Ryan Stinnett --- .../views/dialogs/ChangelogDialog.js | 23 ++++++++++++++----- src/i18n/strings/en_EN.json | 1 + 2 files changed, 18 insertions(+), 6 deletions(-) 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/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5f436a4610..22a869dc6e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -889,6 +889,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",