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.",