Merge branch 'experimental' into bwindels/searchmakeover

This commit is contained in:
Bruno Windels 2019-01-16 11:17:38 +01:00
commit 899ee265e3
18 changed files with 325 additions and 157 deletions

View file

@ -48,7 +48,6 @@ src/components/views/rooms/LinkPreviewWidget.js
src/components/views/rooms/MemberDeviceInfo.js src/components/views/rooms/MemberDeviceInfo.js
src/components/views/rooms/MemberInfo.js src/components/views/rooms/MemberInfo.js
src/components/views/rooms/MemberList.js src/components/views/rooms/MemberList.js
src/components/views/rooms/MemberTile.js
src/components/views/rooms/MessageComposer.js src/components/views/rooms/MessageComposer.js
src/components/views/rooms/PinnedEventTile.js src/components/views/rooms/PinnedEventTile.js
src/components/views/rooms/RoomList.js src/components/views/rooms/RoomList.js

View file

@ -30,8 +30,8 @@ limitations under the License.
} }
.mx_ContextualMenu { .mx_ContextualMenu {
border-radius: 2px; border-radius: 4px;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.21); box-shadow: 4px 4px 12px 0 rgba(118, 131, 156, 0.6);;
background-color: $menu-bg-color; background-color: $menu-bg-color;
color: $primary-fg-color; color: $primary-fg-color;
position: absolute; position: absolute;

View file

@ -14,8 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_MemberStatusMessageAvatar_hasStatus { .mx_MessageComposer_avatar .mx_BaseAvatar {
border: 2px solid $accent-color; padding: 2px;
border-radius: 40px; border: 1px solid transparent;
padding-right: 0 !important; /* Override AccessibleButton styling */ border-radius: 15px;
}
.mx_MessageComposer_avatar .mx_BaseAvatar_initial {
left: 2px;
}
.mx_MemberStatusMessageAvatar_hasStatus .mx_BaseAvatar {
border-color: $accent-color;
} }

View file

@ -14,42 +14,43 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_StatusMessageContextMenu_message { .mx_StatusMessageContextMenu {
display: inline-block; padding: 10px;
border-radius: 3px 0 0 3px; }
.mx_StatusMessageContextMenu_form {
display: flex;
flex-direction: column;
}
input.mx_StatusMessageContextMenu_message {
border-radius: 4px;
border: 1px solid $input-border-color; border: 1px solid $input-border-color;
font-size: 13px; padding: 6.5px 11px;
padding: 7px 7px 7px 9px; background-color: $primary-bg-color;
width: 135px; font-weight: normal;
background-color: $primary-bg-color !important; margin: 0 0 10px;
} }
.mx_StatusMessageContextMenu_submit { .mx_StatusMessageContextMenu_message::placeholder {
display: inline-block; color: $memberstatus-placeholder-color;
} }
.mx_StatusMessageContextMenu_submitFaded { .mx_StatusMessageContextMenu_submit,
opacity: 0.5; .mx_StatusMessageContextMenu_clear {
@mixin mx_DialogButton;
align-self: start;
font-size: 12px;
padding: 6px 1em;
border: 1px solid transparent;
} }
.mx_StatusMessageContextMenu_submit img { .mx_StatusMessageContextMenu_submit[disabled] {
vertical-align: middle; opacity: 0.49;
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 { .mx_StatusMessageContextMenu_clear {
padding: 2px;
}
.mx_StatusMessageContextMenu_hasStatus .mx_StatusMessageContextMenu_clear {
color: $warning-color; color: $warning-color;
background-color: transparent;
border: 1px solid $warning-color;
} }

View file

@ -58,17 +58,13 @@ limitations under the License.
} }
.mx_MessageComposer .mx_MessageComposer_avatar { .mx_MessageComposer .mx_MessageComposer_avatar {
padding: 0 28px; padding: 0 27px;
} }
.mx_MessageComposer .mx_MessageComposer_avatar .mx_BaseAvatar { .mx_MessageComposer .mx_MessageComposer_avatar .mx_BaseAvatar {
display: block; display: block;
} }
.mx_MessageComposer .mx_AccessibleButton {
padding: 0 12px 0 0;
}
.mx_MessageComposer_composecontrols { .mx_MessageComposer_composecontrols {
width: 100%; width: 100%;
} }
@ -185,7 +181,7 @@ limitations under the License.
/*display: table-cell;*/ /*display: table-cell;*/
/*vertical-align: middle;*/ /*vertical-align: middle;*/
/*padding-left: 10px;*/ /*padding-left: 10px;*/
padding-right: 5px; padding-right: 12px;
cursor: pointer; cursor: pointer;
padding-top: 4px; padding-top: 4px;
} }

View file

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.5 (67469) - http://www.bohemiancoding.com/sketch -->
<title>Tick</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Custom-Status-Copy" transform="translate(-529.000000, -917.000000)" fill-rule="nonzero">
<g id="Tick" transform="translate(530.000000, 918.000000)">
<circle id="Oval" stroke="#6AAC8C" fill="#75CFA6" cx="9" cy="9" r="9"></circle>
<g id="Glyph" transform="translate(8.949747, 7.949747) rotate(-45.000000) translate(-8.949747, -7.949747) translate(4.449747, 5.449747)" fill="#FFFFFF">
<rect id="Rectangle" x="0" y="0" width="2" height="5"></rect>
<rect id="Rectangle" x="0" y="3" width="9" height="2"></rect>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -68,7 +68,7 @@ $event-selected-color: #f7f7f7;
$primary-hairline-color: #e5e5e5; $primary-hairline-color: #e5e5e5;
// used for the border of input text fields // used for the border of input text fields
$input-border-color: #f0f0f0; $input-border-color: #e7e7e7;
$input-darker-bg-color: rgba(193, 201, 214, 0.29); $input-darker-bg-color: rgba(193, 201, 214, 0.29);
$input-darker-fg-color: #9fa9ba; $input-darker-fg-color: #9fa9ba;
$input-lighter-bg-color: #f2f5f8; $input-lighter-bg-color: #f2f5f8;
@ -192,6 +192,8 @@ $progressbar-color: #000;
$room-warning-bg-color: #fff8e3; $room-warning-bg-color: #fff8e3;
$memberstatus-placeholder-color: $roomtile-name-color;
/*** form elements ***/ /*** form elements ***/
// .mx_textinput is a container for a text input // .mx_textinput is a container for a text input

View file

@ -188,6 +188,8 @@ $progressbar-color: #000;
$room-warning-bg-color: #fff8e3; $room-warning-bg-color: #fff8e3;
$memberstatus-placeholder-color: $roomtile-name-color;
// ***** Mixins! ***** // ***** Mixins! *****
@define-mixin mx_DialogButton { @define-mixin mx_DialogButton {

View file

@ -34,6 +34,9 @@ import { _t } from '../../languageHandler';
import {instanceForInstanceId, protocolNameForInstanceId} from '../../utils/DirectoryUtils'; import {instanceForInstanceId, protocolNameForInstanceId} from '../../utils/DirectoryUtils';
const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 160;
linkifyMatrix(linkify); linkifyMatrix(linkify);
module.exports = React.createClass({ module.exports = React.createClass({
@ -390,7 +393,6 @@ module.exports = React.createClass({
const self = this; const self = this;
let guestRead; let guestJoin; let perms; let guestRead; let guestJoin; let perms;
for (let i = 0; i < rooms.length; i++) { for (let i = 0; i < rooms.length; i++) {
const name = rooms[i].name || get_display_alias_for_room(rooms[i]) || _t('Unnamed room');
guestRead = null; guestRead = null;
guestJoin = null; guestJoin = null;
@ -410,7 +412,15 @@ module.exports = React.createClass({
perms = <div className="mx_RoomDirectory_perms">{guestRead}{guestJoin}</div>; perms = <div className="mx_RoomDirectory_perms">{guestRead}{guestJoin}</div>;
} }
let name = rooms[i].name || get_display_alias_for_room(rooms[i]) || _t('Unnamed room');
if (name.length > MAX_NAME_LENGTH) {
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
}
let topic = rooms[i].topic || ''; let topic = rooms[i].topic || '';
if (topic.length > MAX_TOPIC_LENGTH) {
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
}
topic = linkifyString(sanitizeHtml(topic)); topic = linkifyString(sanitizeHtml(topic));
rows.push( rows.push(

View file

@ -82,11 +82,10 @@ const SIMPLE_SETTINGS = [
{ id: "VideoView.flipVideoHorizontally" }, { id: "VideoView.flipVideoHorizontally" },
{ id: "TagPanel.disableTagPanel" }, { id: "TagPanel.disableTagPanel" },
{ id: "enableWidgetScreenshots" }, { id: "enableWidgetScreenshots" },
{ id: "RoomSubList.showEmpty" },
{ id: "pinMentionedRooms" }, { id: "pinMentionedRooms" },
{ id: "pinUnreadRooms" }, { id: "pinUnreadRooms" },
{ id: "showDeveloperTools" }, { id: "showDeveloperTools" },
{ id: "alwaysRetryInvites" }, { id: "alwaysInviteUnknownUsers" },
]; ];
// These settings must be defined in SettingsStore // These settings must be defined in SettingsStore

View file

@ -40,38 +40,50 @@ export default class MemberStatusMessageAvatar extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = {
hasStatus: this.hasStatus,
};
} }
componentWillMount() { componentWillMount() {
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) { if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user"); throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
} }
} if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
return;
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: ""});
} }
} const { user } = this.props.member;
if (!user) {
componentWillUnmount() { return;
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
} }
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
} }
_onRoomStateEvents = (ev, state) => { componentWillUmount() {
if (ev.getStateKey() !== MatrixClientPeg.get().getUserId()) return; const { user } = this.props.member;
if (ev.getType() !== "im.vector.user_status") return; if (!user) {
// TODO: We should be relying on `this.props.member.user._unstable_statusMessage` return;
// 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 user.removeListener(
// the im.vector.user_status stuff and replace it with a complete solution. "User._unstable_statusMessage",
this.setState({message: ev.getContent()["status"]}); this._onStatusMessageCommitted,
);
}
get hasStatus() {
const { user } = this.props.member;
if (!user) {
return false;
}
return !!user._unstable_statusMessage;
}
_onStatusMessageCommitted = () => {
// The `User` object has observed a status message change.
this.setState({
hasStatus: this.hasStatus,
});
}; };
_onClick = (e) => { _onClick = (e) => {
@ -79,42 +91,43 @@ export default class MemberStatusMessageAvatar extends React.Component {
const elementRect = e.target.getBoundingClientRect(); 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);
const x = (elementRect.left + window.pageXOffset) - (elementRect.width / 2) + 3; const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom
const chevronOffset = 12; const chevronOffset = (elementRect.width - chevronWidth) / 2;
let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset; const chevronMargin = 1; // Add some spacing away from target
y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron const y = elementRect.top + window.pageYOffset - chevronMargin;
ContextualMenu.createMenu(StatusMessageContextMenu, { ContextualMenu.createMenu(StatusMessageContextMenu, {
chevronOffset: chevronOffset, chevronOffset: chevronOffset,
chevronFace: 'bottom', chevronFace: 'bottom',
left: x, left: x,
top: y, top: y,
menuWidth: 190, menuWidth: 226,
user: this.props.member.user, user: this.props.member.user,
}); });
}; };
render() { render() {
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) { const avatar = <MemberAvatar
return <MemberAvatar member={this.props.member} member={this.props.member}
width={this.props.width} width={this.props.width}
height={this.props.height} height={this.props.height}
resizeMethod={this.props.resizeMethod} />; resizeMethod={this.props.resizeMethod}
} />;
const hasStatus = this.props.member.user ? !!this.props.member.user._unstable_statusMessage : false; if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
return avatar;
}
const classes = classNames({ const classes = classNames({
"mx_MemberStatusMessageAvatar": true, "mx_MemberStatusMessageAvatar": true,
"mx_MemberStatusMessageAvatar_hasStatus": hasStatus, "mx_MemberStatusMessageAvatar_hasStatus": this.state.hasStatus,
}); });
return <AccessibleButton onClick={this._onClick} className={classes} element="div"> return <AccessibleButton className={classes}
<MemberAvatar member={this.props.member} element="div" onClick={this._onClick}
width={this.props.width} >
height={this.props.height} {avatar}
resizeMethod={this.props.resizeMethod} />
</AccessibleButton>; </AccessibleButton>;
} }
} }

View file

@ -19,7 +19,6 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import classNames from 'classnames';
export default class StatusMessageContextMenu extends React.Component { export default class StatusMessageContextMenu extends React.Component {
static propTypes = { static propTypes = {
@ -31,13 +30,42 @@ export default class StatusMessageContextMenu extends React.Component {
super(props, context); super(props, context);
this.state = { this.state = {
message: props.user ? props.user._unstable_statusMessage : "", message: this.comittedStatusMessage,
}; };
} }
componentWillMount() {
const { user } = this.props;
if (!user) {
return;
}
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
}
componentWillUmount() {
const { user } = this.props;
if (!user) {
return;
}
user.removeListener(
"User._unstable_statusMessage",
this._onStatusMessageCommitted,
);
}
get comittedStatusMessage() {
return this.props.user ? this.props.user._unstable_statusMessage : "";
}
_onStatusMessageCommitted = () => {
// The `User` object has observed a status message change.
this.setState({
message: this.comittedStatusMessage,
});
};
_onClearClick = async (e) => { _onClearClick = async (e) => {
await MatrixClientPeg.get()._unstable_setStatusMessage(""); await MatrixClientPeg.get()._unstable_setStatusMessage("");
this.setState({message: ""});
}; };
_onSubmit = (e) => { _onSubmit = (e) => {
@ -46,41 +74,49 @@ export default class StatusMessageContextMenu extends React.Component {
}; };
_onStatusChange = (e) => { _onStatusChange = (e) => {
this.setState({message: e.target.value}); // The input field's value was changed.
this.setState({
message: e.target.value,
});
}; };
render() { render() {
const formSubmitClasses = classNames({ let actionButton;
"mx_StatusMessageContextMenu_submit": true, if (this.comittedStatusMessage) {
"mx_StatusMessageContextMenu_submitFaded": !this.state.message, // no message == faded if (this.state.message === this.comittedStatusMessage) {
}); actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_clear"
onClick={this._onClearClick}
>
<span>{_t("Clear status")}</span>
</AccessibleButton>;
} else {
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
onClick={this._onSubmit}
>
<span>{_t("Update status")}</span>
</AccessibleButton>;
}
} else {
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
disabled={!this.state.message} onClick={this._onSubmit}
>
<span>{_t("Set status")}</span>
</AccessibleButton>;
}
const form = <form className="mx_StatusMessageContextMenu_form" onSubmit={this._onSubmit} autoComplete="off"> const form = <form className="mx_StatusMessageContextMenu_form"
<input type="text" key="message" placeholder={_t("Set a new status...")} autoFocus={true} autoComplete="off" onSubmit={this._onSubmit}
className="mx_StatusMessageContextMenu_message" >
value={this.state.message} onChange={this._onStatusChange} maxLength="60" /> <input type="text" className="mx_StatusMessageContextMenu_message"
<AccessibleButton onClick={this._onSubmit} element="div" className={formSubmitClasses}> key="message" placeholder={_t("Set a new status...")}
<img src="img/icons-checkmark.svg" width="22" height="22" /> autoFocus={true} maxLength="60" value={this.state.message}
</AccessibleButton> onChange={this._onStatusChange}
/>
{actionButton}
</form>; </form>;
const clearIcon = this.state.message ? "img/cancel-red.svg" : "img/cancel.svg"; return <div className="mx_StatusMessageContextMenu">
const clearButton = <AccessibleButton onClick={this._onClearClick} disabled={!this.state.message}
className="mx_StatusMessageContextMenu_clear">
<img src={clearIcon} alt={_t('Clear status')} width="12" height="12"
className="mx_filterFlipColor mx_StatusMessageContextMenu_clearIcon" />
<span>{_t("Clear status")}</span>
</AccessibleButton>;
const menuClasses = classNames({
"mx_StatusMessageContextMenu": true,
"mx_StatusMessageContextMenu_hasStatus": this.state.message,
});
return <div className={menuClasses}>
{ form } { form }
<hr />
{ clearButton }
</div>; </div>;
} }
} }

View file

@ -36,7 +36,7 @@ export default class ChangelogDialog extends React.Component {
for (let i=0; i<REPOS.length; i++) { for (let i=0; i<REPOS.length; i++) {
const oldVersion = version2[2*i]; const oldVersion = version2[2*i];
const newVersion = version[2*i]; const newVersion = version[2*i];
const url = `https://api.github.com/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`; const url = `https://riot.im/github/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`;
request(url, (err, response, body) => { request(url, (err, response, body) => {
if (response.statusCode < 200 || response.statusCode >= 300) { if (response.statusCode < 200 || response.statusCode >= 300) {
this.setState({ [REPOS[i]]: response.statusText }); this.setState({ [REPOS[i]]: response.statusText });

View file

@ -21,10 +21,8 @@ import SettingsStore from "../../../settings/SettingsStore";
const React = require('react'); const React = require('react');
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
const MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require('../../../index'); const sdk = require('../../../index');
const dis = require('../../../dispatcher'); const dis = require('../../../dispatcher');
const Modal = require("../../../Modal");
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
@ -42,7 +40,46 @@ module.exports = React.createClass({
}, },
getInitialState: function() { getInitialState: function() {
return {}; return {
statusMessage: this.getStatusMessage(),
};
},
componentDidMount() {
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
return;
}
const { user } = this.props.member;
if (!user) {
return;
}
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
},
componentWillUmount() {
const { user } = this.props.member;
if (!user) {
return;
}
user.removeListener(
"User._unstable_statusMessage",
this._onStatusMessageCommitted,
);
},
getStatusMessage() {
const { user } = this.props.member;
if (!user) {
return "";
}
return user._unstable_statusMessage;
},
_onStatusMessageCommitted() {
// The `User` object has observed a status message change.
this.setState({
statusMessage: this.getStatusMessage(),
});
}, },
shouldComponentUpdate: function(nextProps, nextState) { shouldComponentUpdate: function(nextProps, nextState) {
@ -74,22 +111,23 @@ module.exports = React.createClass({
}, },
getPowerLabel: function() { getPowerLabel: function() {
return _t("%(userName)s (power %(powerLevelNumber)s)", {userName: this.props.member.userId, powerLevelNumber: this.props.member.powerLevel}); return _t("%(userName)s (power %(powerLevelNumber)s)", {
userName: this.props.member.userId,
powerLevelNumber: this.props.member.powerLevel,
});
}, },
render: function() { render: function() {
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const EntityTile = sdk.getComponent('rooms.EntityTile'); const EntityTile = sdk.getComponent('rooms.EntityTile');
const member = this.props.member; const member = this.props.member;
const name = this._getDisplayName(); const name = this._getDisplayName();
const active = -1;
const presenceState = member.user ? member.user.presence : null; const presenceState = member.user ? member.user.presence : null;
let statusMessage = null; let statusMessage = null;
if (member.user && SettingsStore.isFeatureEnabled("feature_custom_status")) { if (member.user && SettingsStore.isFeatureEnabled("feature_custom_status")) {
statusMessage = member.user._unstable_statusMessage; statusMessage = this.state.statusMessage;
} }
const av = ( const av = (

View file

@ -62,6 +62,7 @@ module.exports = React.createClass({
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
notificationCount: this.props.room.getUnreadNotificationCount(), notificationCount: this.props.room.getUnreadNotificationCount(),
selected: this.props.room.roomId === ActiveRoomObserver.getActiveRoomId(), selected: this.props.room.roomId === ActiveRoomObserver.getActiveRoomId(),
statusMessage: this._getStatusMessage(),
}); });
}, },
@ -79,6 +80,33 @@ module.exports = React.createClass({
return Boolean(dmRooms); return Boolean(dmRooms);
}, },
_shouldShowStatusMessage() {
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
return false;
}
const isInvite = this.props.room.getMyMembership() === "invite";
const isJoined = this.props.room.getMyMembership() === "join";
const looksLikeDm = this.props.room.getInvitedAndJoinedMemberCount() === 2;
return !isInvite && isJoined && looksLikeDm;
},
_getStatusMessageUser() {
const selfId = MatrixClientPeg.get().getUserId();
const otherMember = this.props.room.currentState.getMembersExcept([selfId])[0];
if (!otherMember) {
return null;
}
return otherMember.user;
},
_getStatusMessage() {
const statusUser = this._getStatusMessageUser();
if (!statusUser) {
return "";
}
return statusUser._unstable_statusMessage;
},
onRoomTimeline: function(ev, room) { onRoomTimeline: function(ev, room) {
if (room !== this.props.room) return; if (room !== this.props.room) return;
this.setState({ this.setState({
@ -112,7 +140,13 @@ module.exports = React.createClass({
this.setState({ this.setState({
notificationCount: this.props.room.getUnreadNotificationCount(), notificationCount: this.props.room.getUnreadNotificationCount(),
}); });
break; break;
// RoomTiles are one of the few components that may show custom status and
// also remain on screen while in Settings toggling the feature. This ensures
// you can clearly see the status hide and show when toggling the feature.
case 'feature_custom_status_changed':
this.forceUpdate();
break;
} }
}, },
@ -128,6 +162,16 @@ module.exports = React.createClass({
MatrixClientPeg.get().on("Room.name", this.onRoomName); MatrixClientPeg.get().on("Room.name", this.onRoomName);
ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange); ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
if (this._shouldShowStatusMessage()) {
const statusUser = this._getStatusMessageUser();
if (statusUser) {
statusUser.on(
"User._unstable_statusMessage",
this._onStatusMessageCommitted,
);
}
}
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
@ -139,6 +183,16 @@ module.exports = React.createClass({
} }
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange); ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
if (this._shouldShowStatusMessage()) {
const statusUser = this._getStatusMessageUser();
if (statusUser) {
statusUser.removeListener(
"User._unstable_statusMessage",
this._onStatusMessageCommitted,
);
}
}
}, },
componentWillReceiveProps: function(props) { componentWillReceiveProps: function(props) {
@ -166,6 +220,13 @@ module.exports = React.createClass({
return false; return false;
}, },
_onStatusMessageCommitted() {
// The status message `User` object has observed a message change.
this.setState({
statusMessage: this._getStatusMessage(),
});
},
onClick: function(ev) { onClick: function(ev) {
if (this.props.onClick) { if (this.props.onClick) {
this.props.onClick(this.props.room.roomId, ev); this.props.onClick(this.props.room.roomId, ev);
@ -251,15 +312,9 @@ module.exports = React.createClass({
const mentionBadges = this.props.highlight && this._shouldShowMentionBadge(); const mentionBadges = this.props.highlight && this._shouldShowMentionBadge();
const badges = notifBadges || mentionBadges; const badges = notifBadges || mentionBadges;
const isJoined = this.props.room.getMyMembership() === "join";
const looksLikeDm = this.props.room.getInvitedAndJoinedMemberCount() === 2;
let subtext = null; let subtext = null;
if (!isInvite && isJoined && looksLikeDm && SettingsStore.isFeatureEnabled("feature_custom_status")) { if (this._shouldShowStatusMessage()) {
const selfId = MatrixClientPeg.get().getUserId(); subtext = this.state.statusMessage;
const otherMember = this.props.room.currentState.getMembersExcept([selfId])[0];
if (otherMember && otherMember.user && otherMember.user._unstable_statusMessage) {
subtext = otherMember.user._unstable_statusMessage;
}
} }
const classes = classNames({ const classes = classNames({

View file

@ -296,7 +296,6 @@
"Pin rooms I'm mentioned in to the top of the room list": "Pin rooms I'm mentioned in to the top of the room list", "Pin rooms I'm mentioned in to the top of the room list": "Pin rooms I'm mentioned in to the top of the room list",
"Pin unread rooms to the top of the room list": "Pin unread rooms to the top of the room list", "Pin unread rooms to the top of the room list": "Pin unread rooms to the top of the room list",
"Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets", "Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets",
"Show empty room list headings": "Show empty room list headings",
"Always invite users which may not exist": "Always invite users which may not exist", "Always invite users which may not exist": "Always invite users which may not exist",
"Show developer tools": "Show developer tools", "Show developer tools": "Show developer tools",
"Collecting app version information": "Collecting app version information", "Collecting app version information": "Collecting app version information",
@ -1088,8 +1087,10 @@
"Forget": "Forget", "Forget": "Forget",
"Low Priority": "Low Priority", "Low Priority": "Low Priority",
"Direct Chat": "Direct Chat", "Direct Chat": "Direct Chat",
"Set a new status...": "Set a new status...",
"Clear status": "Clear status", "Clear status": "Clear status",
"Update status": "Update status",
"Set status": "Set status",
"Set a new status...": "Set a new status...",
"View as Grid": "View as Grid", "View as Grid": "View as Grid",
"View Community": "View Community", "View Community": "View Community",
"Sorry, your browser is <b>not</b> able to run Riot.": "Sorry, your browser is <b>not</b> able to run Riot.", "Sorry, your browser is <b>not</b> able to run Riot.": "Sorry, your browser is <b>not</b> able to run Riot.",

View file

@ -22,6 +22,7 @@ import {
NotificationsEnabledController, NotificationsEnabledController,
} from "./controllers/NotificationControllers"; } from "./controllers/NotificationControllers";
import LazyLoadingController from "./controllers/LazyLoadingController"; import LazyLoadingController from "./controllers/LazyLoadingController";
import CustomStatusController from "./controllers/CustomStatusController";
// These are just a bunch of helper arrays to avoid copy/pasting a bunch of times // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
const LEVELS_ROOM_SETTINGS = ['device', 'room-device', 'room-account', 'account', 'config']; const LEVELS_ROOM_SETTINGS = ['device', 'room-device', 'room-account', 'account', 'config'];
@ -88,6 +89,7 @@ export const SETTINGS = {
displayName: _td("Custom user status messages"), displayName: _td("Custom user status messages"),
supportedLevels: LEVELS_FEATURE, supportedLevels: LEVELS_FEATURE,
default: false, default: false,
controller: new CustomStatusController(),
}, },
"feature_lazyloading": { "feature_lazyloading": {
isFeature: true, isFeature: true,
@ -324,11 +326,6 @@ export const SETTINGS = {
supportedLevels: ['room-device'], supportedLevels: ['room-device'],
default: false, default: false,
}, },
"RoomSubList.showEmpty": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Show empty room list headings'),
default: true,
},
"alwaysInviteUnknownUsers": { "alwaysInviteUnknownUsers": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS, supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Always invite users which may not exist'), displayName: _td('Always invite users which may not exist'),

View file

@ -0,0 +1,28 @@
/*
Copyright 2019 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 SettingController from "./SettingController";
import dis from "../../dispatcher";
export default class CustomStatusController extends SettingController {
onChange(level, roomId, newValue) {
// Dispatch setting change so that some components that are still visible when the
// Settings page is open (such as RoomTiles) can reflect the change.
dis.dispatch({
action: "feature_custom_status_changed",
});
}
}