diff --git a/res/css/_components.scss b/res/css/_components.scss
index 9f50856ce0..5633b7ec8f 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -24,8 +24,10 @@
@import "./structures/_ViewSource.scss";
@import "./structures/login/_Login.scss";
@import "./views/avatars/_BaseAvatar.scss";
+@import "./views/avatars/_MemberStatusMessageAvatar.scss";
@import "./views/context_menus/_MessageContextMenu.scss";
@import "./views/context_menus/_RoomTileContextMenu.scss";
+@import "./views/context_menus/_StatusMessageContextMenu.scss";
@import "./views/context_menus/_TagTileContextMenu.scss";
@import "./views/dialogs/_BugReportDialog.scss";
@import "./views/dialogs/_ChangelogDialog.scss";
diff --git a/res/css/views/avatars/_MemberStatusMessageAvatar.scss b/res/css/views/avatars/_MemberStatusMessageAvatar.scss
new file mode 100644
index 0000000000..c857b9807b
--- /dev/null
+++ b/res/css/views/avatars/_MemberStatusMessageAvatar.scss
@@ -0,0 +1,20 @@
+/*
+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_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..873ad99495
--- /dev/null
+++ b/res/css/views/context_menus/_StatusMessageContextMenu.scss
@@ -0,0 +1,55 @@
+/*
+Copyright 2018 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_StatusMessageContextMenu_message {
+ display: inline-block;
+ border-radius: 3px 0 0 3px;
+ border: 1px solid $input-border-color;
+ font-size: 13px;
+ padding: 7px 7px 7px 9px;
+ width: 135px;
+ background-color: $primary-bg-color !important;
+}
+
+.mx_StatusMessageContextMenu_submit {
+ display: inline-block;
+}
+
+.mx_StatusMessageContextMenu_submitFaded {
+ opacity: 0.5;
+}
+
+.mx_StatusMessageContextMenu_submit img {
+ vertical-align: middle;
+ margin-left: 8px;
+}
+
+.mx_StatusMessageContextMenu hr {
+ border: 0.5px solid $menu-border-color;
+}
+
+.mx_StatusMessageContextMenu_clearIcon {
+ margin: 5px 15px 5px 5px;
+ vertical-align: middle;
+}
+
+.mx_StatusMessageContextMenu_clear {
+ padding: 2px;
+}
+
+.mx_StatusMessageContextMenu_hasStatus .mx_StatusMessageContextMenu_clear {
+ color: $warning-color;
+}
diff --git a/res/css/views/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 ccd3afe26c..b5ac9aadc6 100644
--- a/res/css/views/rooms/_RoomTile.scss
+++ b/res/css/views/rooms/_RoomTile.scss
@@ -35,7 +35,19 @@ limitations under the License.
.mx_RoomTile_nameContainer {
display: inline-block;
width: 180px;
- height: 24px;
+ vertical-align: middle;
+}
+
+.mx_RoomTile_subtext {
+ display: inline-block;
+ font-size: 11px;
+ padding: 0 0 0 7px;
+ margin: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: clip;
+ position: relative;
+ bottom: 4px;
}
.mx_RoomTile_avatar_container {
@@ -49,10 +61,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;
diff --git a/res/img/icons-checkmark.svg b/res/img/icons-checkmark.svg
new file mode 100644
index 0000000000..3c5392003d
--- /dev/null
+++ b/res/img/icons-checkmark.svg
@@ -0,0 +1,17 @@
+
+
diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js
new file mode 100644
index 0000000000..aebd1741b7
--- /dev/null
+++ b/src/components/views/avatars/MemberStatusMessageAvatar.js
@@ -0,0 +1,120 @@
+/*
+Copyright 2018 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import MatrixClientPeg from '../../../MatrixClientPeg';
+import AccessibleButton from '../elements/AccessibleButton';
+import MemberAvatar from '../avatars/MemberAvatar';
+import classNames from 'classnames';
+import * as ContextualMenu from "../../structures/ContextualMenu";
+import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
+import SettingsStore from "../../../settings/SettingsStore";
+
+export default class MemberStatusMessageAvatar extends React.Component {
+ static propTypes = {
+ member: PropTypes.object.isRequired,
+ width: PropTypes.number,
+ height: PropTypes.number,
+ resizeMethod: PropTypes.string,
+ };
+
+ static defaultProps = {
+ width: 40,
+ height: 40,
+ resizeMethod: 'crop',
+ };
+
+ constructor(props, context) {
+ super(props, context);
+ }
+
+ componentWillMount() {
+ if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
+ throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
+ }
+ }
+
+ componentDidMount() {
+ MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
+
+ if (this.props.member.user) {
+ this.setState({message: this.props.member.user._unstable_statusMessage});
+ } else {
+ this.setState({message: ""});
+ }
+ }
+
+ componentWillUnmount() {
+ if (MatrixClientPeg.get()) {
+ MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
+ }
+ }
+
+ _onRoomStateEvents = (ev, state) => {
+ if (ev.getStateKey() !== MatrixClientPeg.get().getUserId()) return;
+ if (ev.getType() !== "im.vector.user_status") return;
+ // TODO: We should be relying on `this.props.member.user._unstable_statusMessage`
+ // We don't currently because the js-sdk doesn't emit a specific event for this
+ // change, and we don't want to race it. This should be improved when we rip out
+ // the im.vector.user_status stuff and replace it with a complete solution.
+ this.setState({message: ev.getContent()["status"]});
+ };
+
+ _onClick = (e) => {
+ e.stopPropagation();
+
+ const elementRect = e.target.getBoundingClientRect();
+
+ // The window X and Y offsets are to adjust position when zoomed in to page
+ const x = (elementRect.left + window.pageXOffset) - (elementRect.width / 2) + 3;
+ const chevronOffset = 12;
+ let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
+ y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron
+
+ ContextualMenu.createMenu(StatusMessageContextMenu, {
+ chevronOffset: chevronOffset,
+ chevronFace: 'bottom',
+ left: x,
+ top: y,
+ menuWidth: 190,
+ user: this.props.member.user,
+ });
+ };
+
+ render() {
+ if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
+ return ;
+ }
+
+ const hasStatus = this.props.member.user ? !!this.props.member.user._unstable_statusMessage : false;
+
+ const classes = classNames({
+ "mx_MemberStatusMessageAvatar": true,
+ "mx_MemberStatusMessageAvatar_hasStatus": hasStatus,
+ });
+
+ return
+
+ ;
+ }
+}
diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js
new file mode 100644
index 0000000000..f07220db44
--- /dev/null
+++ b/src/components/views/context_menus/StatusMessageContextMenu.js
@@ -0,0 +1,86 @@
+/*
+Copyright 2018 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import { _t } from '../../../languageHandler';
+import MatrixClientPeg from '../../../MatrixClientPeg';
+import AccessibleButton from '../elements/AccessibleButton';
+import classNames from 'classnames';
+
+export default class StatusMessageContextMenu extends React.Component {
+ static propTypes = {
+ // js-sdk User object. Not required because it might not exist.
+ user: PropTypes.object,
+ };
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ message: props.user ? props.user._unstable_statusMessage : "",
+ };
+ }
+
+ _onClearClick = async(e) => {
+ await MatrixClientPeg.get()._unstable_setStatusMessage("");
+ this.setState({message: ""});
+ };
+
+ _onSubmit = (e) => {
+ e.preventDefault();
+ MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message);
+ };
+
+ _onStatusChange = (e) => {
+ this.setState({message: e.target.value});
+ };
+
+ render() {
+ const formSubmitClasses = classNames({
+ "mx_StatusMessageContextMenu_submit": true,
+ "mx_StatusMessageContextMenu_submitFaded": !this.state.message, // no message == faded
+ });
+
+ const form =
{ /* { incomingCallBox } */ }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 22a869dc6e..81fcd963b3 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -257,6 +257,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",
@@ -1061,6 +1062,8 @@
"Forget": "Forget",
"Low Priority": "Low Priority",
"Direct Chat": "Direct Chat",
+ "Set a new status...": "Set a new status...",
+ "Clear status": "Clear status",
"View Community": "View Community",
"Sorry, your browser is not able to run Riot.": "Sorry, your browser is not able to run Riot.",
"Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.",
diff --git a/src/settings/Settings.js b/src/settings/Settings.js
index 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"),