From ce7969e3d5a2ed4fb6b98539c1bdeb58e915c655 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 11 Dec 2018 21:40:11 -0700 Subject: [PATCH 01/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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 ef60a34180c973511f4a436cd21faa98f297a845 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 18 Dec 2018 10:53:37 -0700 Subject: [PATCH 12/15] 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 d20a934642a117253ae8b138a4ca2de71bf05866 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 18 Dec 2018 11:04:16 -0700 Subject: [PATCH 13/15] 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 14/15] 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 15/15] 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"]}); };