Merge remote-tracking branch 'origin/experimental' into travis/fix-memberlist-order

This commit is contained in:
Travis Ralston 2019-01-03 20:01:28 -07:00
commit cc8fa7911b
46 changed files with 788 additions and 193 deletions

View file

@ -204,6 +204,19 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
const data = await client.login(loginType, loginParams);
const wellknown = data.well_known;
if (wellknown) {
if (wellknown["m.homeserver"] && wellknown["m.homeserver"]["base_url"]) {
hsUrl = wellknown["m.homeserver"]["base_url"];
console.log(`Overrode homeserver setting with ${hsUrl} from login response`);
}
if (wellknown["m.identity_server"] && wellknown["m.identity_server"]["base_url"]) {
// TODO: should we prompt here?
isUrl = wellknown["m.identity_server"]["base_url"];
console.log(`Overrode IS setting with ${isUrl} from login response`);
}
}
return {
homeserverUrl: hsUrl,
identityServerUrl: isUrl,

View file

@ -289,11 +289,6 @@ const Notifier = {
const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
if (actions && actions.notify) {
dis.dispatch({
action: "event_notification",
event: ev,
room: room,
});
if (this.isEnabled()) {
this._displayPopupNotification(ev, room);
}

View file

@ -392,7 +392,7 @@ class Tinter {
// XXX: we could just move this all into TintableSvg, but as it's so similar
// to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
// keeping it here for now.
calcSvgFixups(svgs, forceColors) {
calcSvgFixups(svgs) {
// go through manually fixing up SVG colours.
// we could do this by stylesheets, but keeping the stylesheets
// updated would be a PITA, so just brute-force search for the
@ -420,21 +420,13 @@ class Tinter {
const tag = tags[j];
for (let k = 0; k < this.svgAttrs.length; k++) {
const attr = this.svgAttrs[k];
for (let m = 0; m < this.keyHex.length; m++) { // dev note: don't use L please.
// We use a different attribute from the one we're setting
// because we may also be using forceColors. If we were to
// check the keyHex against a forceColors value, it may not
// match and therefore not change when we need it to.
const valAttrName = "mx-val-" + attr;
let attribute = tag.getAttribute(valAttrName);
if (!attribute) attribute = tag.getAttribute(attr); // fall back to the original
if (attribute && (attribute.toUpperCase() === this.keyHex[m] || attribute.toLowerCase() === this.keyRgb[m])) {
for (let l = 0; l < this.keyHex.length; l++) {
if (tag.getAttribute(attr) &&
tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) {
fixups.push({
node: tag,
attr: attr,
refAttr: valAttrName,
index: m,
forceColors: forceColors,
index: l,
});
}
}
@ -450,9 +442,7 @@ class Tinter {
if (DEBUG) console.log("applySvgFixups start for " + fixups);
for (let i = 0; i < fixups.length; i++) {
const svgFixup = fixups[i];
const forcedColor = svgFixup.forceColors ? svgFixup.forceColors[svgFixup.index] : null;
svgFixup.node.setAttribute(svgFixup.attr, forcedColor ? forcedColor : this.colors[svgFixup.index]);
svgFixup.node.setAttribute(svgFixup.refAttr, this.colors[svgFixup.index]);
svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]);
}
if (DEBUG) console.log("applySvgFixups end");
}

View file

@ -239,17 +239,19 @@ export default React.createClass({
<p>{_t("You'll need it if you log out or lose access to this device.")}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseHelp">
{strengthMeter}
{helpText}
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
<input type="password"
onChange={this._onPassPhraseChange}
onKeyPress={this._onPassPhraseKeyPress}
value={this.state.passPhrase}
className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Enter a passphrase...")}
/>
<div className="mx_CreateKeyBackupDialog_passPhraseHelp">
{strengthMeter}
{helpText}
</div>
</div>
<input type="password"
onChange={this._onPassPhraseChange}
onKeyPress={this._onPassPhraseKeyPress}
value={this.state.passPhrase}
className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Enter a passphrase...")}
/>
</div>
<DialogButtons primaryButton={_t('Next')}
@ -317,16 +319,18 @@ export default React.createClass({
"somewhere safe.",
)}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
{passPhraseMatch}
<div>
<input type="password"
onChange={this._onPassPhraseConfirmChange}
onKeyPress={this._onPassPhraseConfirmKeyPress}
value={this.state.passPhraseConfirm}
className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Repeat your passphrase...")}
autoFocus={true}
/>
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
<div>
<input type="password"
onChange={this._onPassPhraseConfirmChange}
onKeyPress={this._onPassPhraseConfirmKeyPress}
value={this.state.passPhraseConfirm}
className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Repeat your passphrase...")}
autoFocus={true}
/>
</div>
{passPhraseMatch}
</div>
</div>
<DialogButtons primaryButton={_t('Next')}
@ -351,21 +355,21 @@ export default React.createClass({
<p>{_t("Make a copy of this Recovery Key and keep it safe.")}</p>
<p>{bodyText}</p>
<p className="mx_CreateKeyBackupDialog_primaryContainer">
<div>{_t("Your Recovery Key")}</div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
<button onClick={this._onCopyClick}>
{_t("Copy to clipboard")}
</button>
{
// FIXME REDESIGN: buttons should be adjacent but insufficient room in current design
}
<br /><br />
<button onClick={this._onDownloadClick}>
{_t("Download")}
</button>
<div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
{_t("Your Recovery Key")}
</div>
<div className="mx_CreateKeyBackupDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._keyBackupInfo.recovery_key}</code>
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._keyBackupInfo.recovery_key}</code>
</div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
<button className="mx_Dialog_primary" onClick={this._onCopyClick}>
{_t("Copy to clipboard")}
</button>
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
{_t("Download")}
</button>
</div>
</div>
</p>
<br />

View file

@ -91,11 +91,15 @@ class HomePage extends React.Component {
this._unmounted = true;
}
onLoginClick() {
onLoginClick(ev) {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({ action: 'start_login' });
}
onRegisterClick() {
onRegisterClick(ev) {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({ action: 'start_registration' });
}

View file

@ -927,6 +927,10 @@ export default React.createClass({
},
_viewHome: function() {
// The home page requires the "logged in" view, so we'll set that.
this.setStateForNewView({
view: VIEWS.LOGGED_IN,
});
this._setPage(PageTypes.HomePage);
this.notifyNewScreen('home');
},
@ -1183,10 +1187,7 @@ export default React.createClass({
* @param {string} teamToken
*/
_onLoggedIn: async function(teamToken) {
this.setState({
view: VIEWS.LOGGED_IN,
});
this.setStateForNewView({view: VIEWS.LOGGED_IN});
if (teamToken) {
// A team member has logged in, not a guest
this._teamToken = teamToken;

View file

@ -163,6 +163,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
MatrixClientPeg.get().on("accountData", this.onAccountData);
MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus);
this._fetchMediaConfig();
// Start listening for RoomViewStore updates
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
@ -451,6 +452,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
MatrixClientPeg.get().removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus);
}
window.removeEventListener('beforeunload', this.onPageUnload);
@ -620,6 +622,11 @@ module.exports = React.createClass({
false,
);
}
},
onKeyBackupStatus() {
// Key backup status changes affect whether the in-room recovery
// reminder is displayed.
this.forceUpdate();
},

View file

@ -162,6 +162,18 @@ module.exports = React.createClass({
this.setState(newState);
},
onLoginClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
this.props.onLoginClick();
},
onRegisterClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
this.props.onRegisterClick();
},
showErrorDialog: function(body, title) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, {
@ -253,10 +265,10 @@ module.exports = React.createClass({
</form>
{ serverConfigSection }
{ errorText }
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
<a className="mx_Login_create" onClick={this.onLoginClick} href="#">
{ _t('Return to login screen') }
</a>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
<a className="mx_Login_create" onClick={this.onRegisterClick} href="#">
{ _t('Create an account') }
</a>
<LanguageSelector />

View file

@ -214,7 +214,10 @@ module.exports = React.createClass({
}).done();
},
_onLoginAsGuestClick: function() {
_onLoginAsGuestClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
const self = this;
self.setState({
busy: true,
@ -297,6 +300,12 @@ module.exports = React.createClass({
});
},
onRegisterClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
this.props.onRegisterClick();
},
_tryWellKnownDiscovery: async function(serverName) {
if (!serverName.trim()) {
// Nothing to discover
@ -567,7 +576,7 @@ module.exports = React.createClass({
{ errorTextSection }
{ this.componentForStep(this.state.currentFlow) }
{ serverConfig }
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
<a className="mx_Login_create" onClick={this.onRegisterClick} href="#">
{ _t('Create an account') }
</a>
{ loginAsGuestJsx }

View file

@ -363,6 +363,12 @@ module.exports = React.createClass({
}
},
onLoginClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
this.props.onLoginClick();
},
_makeRegisterRequest: function(auth) {
// Only send the bind params if we're sending username / pw params
// (Since we need to send no params at all to use the ones saved in the
@ -468,7 +474,7 @@ module.exports = React.createClass({
let signIn;
if (!this.state.doingUIAuth) {
signIn = (
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
<a className="mx_Login_create" onClick={this.onLoginClick} href="#">
{ theme === 'status' ? _t('Sign in') : _t('I already have an account') }
</a>
);

View file

@ -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 <MemberAvatar member={this.props.member}
width={this.props.width}
height={this.props.height}
resizeMethod={this.props.resizeMethod} />;
}
const hasStatus = this.props.member.user ? !!this.props.member.user._unstable_statusMessage : false;
const classes = classNames({
"mx_MemberStatusMessageAvatar": true,
"mx_MemberStatusMessageAvatar_hasStatus": hasStatus,
});
return <AccessibleButton onClick={this._onClick} className={classes} element="div">
<MemberAvatar member={this.props.member}
width={this.props.width}
height={this.props.height}
resizeMethod={this.props.resizeMethod} />
</AccessibleButton>;
}
}

View file

@ -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 = <form className="mx_StatusMessageContextMenu_form" onSubmit={this._onSubmit} autoComplete="off">
<input type="text" key="message" placeholder={_t("Set a new status...")} autoFocus={true}
className="mx_StatusMessageContextMenu_message"
value={this.state.message} onChange={this._onStatusChange} maxLength="60" />
<AccessibleButton onClick={this._onSubmit} element="div" className={formSubmitClasses}>
<img src="img/icons-checkmark.svg" width="22" height="22" />
</AccessibleButton>
</form>;
const clearIcon = this.state.message ? "img/cancel-red.svg" : "img/cancel.svg";
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 }
<hr />
{ clearButton }
</div>;
}
}

View file

@ -36,8 +36,12 @@ export default class ChangelogDialog extends React.Component {
for (let i=0; i<REPOS.length; i++) {
const oldVersion = version2[2*i];
const newVersion = version[2*i];
request(`https://api.github.com/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`, (a, b, body) => {
if (body == null) return;
const url = `https://api.github.com/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`;
request(url, (err, response, body) => {
if (response.statusCode < 200 || response.statusCode >= 300) {
this.setState({ [REPOS[i]]: response.statusText });
return;
}
this.setState({[REPOS[i]]: JSON.parse(body).commits});
});
}
@ -58,13 +62,20 @@ export default class ChangelogDialog extends React.Component {
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
const logs = REPOS.map(repo => {
if (this.state[repo] == null) return <Spinner key={repo} />;
let content;
if (this.state[repo] == null) {
content = <Spinner key={repo} />;
} else if (typeof this.state[repo] === "string") {
content = _t("Unable to load commit detail: %(msg)s", {
msg: this.state[repo],
});
} else {
content = this.state[repo].map(this._elementsForCommit);
}
return (
<div key={repo}>
<h2>{repo}</h2>
<ul>
{this.state[repo].map(this._elementsForCommit)}
</ul>
<ul>{content}</ul>
</div>
);
});

View file

@ -29,7 +29,6 @@ var TintableSvg = React.createClass({
width: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
className: PropTypes.string,
forceColors: PropTypes.arrayOf(PropTypes.string),
},
statics: {
@ -51,12 +50,6 @@ var TintableSvg = React.createClass({
delete TintableSvg.mounts[this.id];
},
componentDidUpdate: function(prevProps, prevState) {
if (prevProps.forceColors !== this.props.forceColors) {
this.calcAndApplyFixups(this.refs.svgContainer);
}
},
tint: function() {
// TODO: only bother running this if the global tint settings have changed
// since we loaded!
@ -64,13 +57,8 @@ var TintableSvg = React.createClass({
},
onLoad: function(event) {
this.calcAndApplyFixups(event.target);
},
calcAndApplyFixups: function(target) {
if (!target) return;
// console.log("TintableSvg.calcAndApplyFixups for " + this.props.src);
this.fixups = Tinter.calcSvgFixups([target], this.props.forceColors);
// console.log("TintableSvg.onLoad for " + this.props.src);
this.fixups = Tinter.calcSvgFixups([event.target]);
Tinter.applySvgFixups(this.fixups);
},
@ -83,7 +71,6 @@ var TintableSvg = React.createClass({
height={this.props.height}
onLoad={this.onLoad}
tabIndex="-1"
ref="svgContainer"
/>
);
},

View file

@ -85,8 +85,8 @@ export default React.createClass({
_getDisplayedGroups(userGroups, relatedGroups) {
let displayedGroups = userGroups || [];
if (relatedGroups && relatedGroups.length > 0) {
displayedGroups = displayedGroups.filter((groupId) => {
return relatedGroups.includes(groupId);
displayedGroups = relatedGroups.filter((groupId) => {
return displayedGroups.includes(groupId);
});
} else {
displayedGroups = [];

View file

@ -70,6 +70,7 @@ const EntityTile = React.createClass({
onClick: PropTypes.func,
suppressOnHover: PropTypes.bool,
showPresence: PropTypes.bool,
subtextLabel: PropTypes.string,
},
getDefaultProps: function() {
@ -129,6 +130,9 @@ const EntityTile = React.createClass({
presenceState={this.props.presenceState} />;
nameClasses += ' mx_EntityTile_name_hover';
}
if (this.props.subtextLabel) {
presenceLabel = <span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>;
}
nameEl = (
<div className="mx_EntityTile_details">
<EmojiText element="div" className={nameClasses} dir="auto">
@ -137,6 +141,15 @@ const EntityTile = React.createClass({
{presenceLabel}
</div>
);
} else if (this.props.subtextLabel) {
nameEl = (
<div className="mx_EntityTile_details">
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">
{name}
</EmojiText>
<span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>
</div>
);
} else {
nameEl = (
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">{ name }</EmojiText>

View file

@ -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',
@ -889,11 +890,16 @@ 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;
if (SettingsStore.isFeatureEnabled("feature_custom_status")) {
statusMessage = this.props.member.user._unstable_statusMessage;
}
}
const room = this.props.matrixClient.getRoom(this.props.member.roomId);
@ -915,6 +921,11 @@ module.exports = withMatrixClient(React.createClass({
presenceState={presenceState} />;
}
let statusLabel = null;
if (statusMessage) {
statusLabel = <span className="mx_MemberInfo_statusMessage">{ statusMessage }</span>;
}
let roomMemberDetails = null;
if (this.props.member.roomId) { // is in room
const PowerSelector = sdk.getComponent('elements.PowerSelector');
@ -931,6 +942,7 @@ module.exports = withMatrixClient(React.createClass({
</div>
<div className="mx_MemberInfo_profileField">
{presenceLabel}
{statusLabel}
</div>
</div>;
}

View file

@ -16,6 +16,8 @@ limitations under the License.
'use strict';
import SettingsStore from "../../../settings/SettingsStore";
const React = require('react');
import PropTypes from 'prop-types';
@ -85,6 +87,11 @@ module.exports = React.createClass({
const active = -1;
const presenceState = member.user ? member.user.presence : null;
let statusMessage = null;
if (member.user && SettingsStore.isFeatureEnabled("feature_custom_status")) {
statusMessage = member.user._unstable_statusMessage;
}
const av = (
<MemberAvatar member={member} width={36} height={36} />
);
@ -106,7 +113,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}
/>
);
},
});

View file

@ -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(
<div key="controls_avatar" className="mx_MessageComposer_avatar">
<MemberAvatar member={this.state.me} width={24} height={24} />
<MemberStatusMessageAvatar member={this.state.me} width={24} height={24} />
</div>,
);
}
@ -349,6 +349,34 @@ export default class MessageComposer extends React.Component {
const canSendMessages = !this.state.tombstone &&
this.props.room.maySendMessage();
// TODO: Remove temporary logging for riot-web#7838
// Note: we rip apart the power level event ourselves because we don't want to
// log too much data about it - just the bits we care about. Many of the variables
// logged here are to help figure out where in the stack the 'cannot post in room'
// warning is coming from. This means logging various numbers from the PL event to
// verify RoomState._maySendEventOfType is doing the right thing.
const room = this.props.room;
const plEvent = room.currentState.getStateEvents('m.room.power_levels', '');
let plEventString = "<no power level event>";
if (plEvent) {
const content = plEvent.getContent();
if (!content) {
plEventString = "<no event content>";
} else {
const stringifyFalsey = (v) => v === null ? '<null>' : (v === undefined ? '<undefined>' : v);
const actualUserPl = stringifyFalsey(content.users ? content.users[room.myUserId] : "<no users in content>");
const usersPl = stringifyFalsey(content.users_default);
const actualEventPl = stringifyFalsey(content.events ? content.events['m.room.message'] : "<no events in content>");
const eventPl = stringifyFalsey(content.events_default);
plEventString = `actualUserPl=${actualUserPl} defaultUserPl=${usersPl} actualEventPl=${actualEventPl} defaultEventPl=${eventPl}`;
}
}
console.log(
`[riot-web#7838] renderComposer() hasTombstone=${!!this.state.tombstone} maySendMessage=${room.maySendMessage()}` +
` myMembership=${room.getMyMembership()} maySendEvent=${room.currentState.maySendEvent('m.room.message', room.myUserId)}` +
` myUserId=${room.myUserId} roomId=${room.roomId} hasPlEvent=${!!plEvent} powerLevels='${plEventString}'`
);
if (canSendMessages) {
// This also currently includes the call buttons. Really we should
// check separately for whether we can call, but this is slightly
@ -425,6 +453,8 @@ export default class MessageComposer extends React.Component {
</div>
</div>);
} else {
// TODO: Remove temporary logging for riot-web#7838
console.log("[riot-web#7838] Falling back to showing cannot post in room error");
controls.push(
<div key="controls_error" className="mx_MessageComposer_noperm_error">
{ _t('You do not have permission to post to this room') }

View file

@ -86,6 +86,7 @@ module.exports = React.createClass({
incomingCallTag: null,
incomingCall: null,
selectedTags: [],
hover: false,
};
},
@ -294,6 +295,17 @@ module.exports = React.createClass({
this.forceUpdate();
},
onMouseEnter: function(ev) {
this.setState({hover: true});
},
onMouseLeave: function(ev) {
this.setState({hover: false});
// Refresh the room list just in case the user missed something.
this._delayedRefreshRoomList();
},
_delayedRefreshRoomList: new rate_limited_func(function() {
this.refreshRoomList();
}, 500),
@ -346,6 +358,11 @@ module.exports = React.createClass({
},
refreshRoomList: function() {
if (this.state.hover) {
// Don't re-sort the list if we're hovering over the list
return;
}
// TODO: ideally we'd calculate this once at start, and then maintain
// any changes to it incrementally, updating the appropriate sublists
// as needed.
@ -693,9 +710,10 @@ module.exports = React.createClass({
const subListComponents = this._mapSubListProps(subLists);
return (
<div ref={this._collectResizeContainer} className="mx_RoomList">
<div ref={this._collectResizeContainer} className="mx_RoomList"
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
{ subListComponents }
</div>
);
},
});
});

View file

@ -19,13 +19,76 @@ import PropTypes from "prop-types";
import sdk from "../../../index";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import MatrixClientPeg from "../../../MatrixClientPeg";
export default class RoomRecoveryReminder extends React.PureComponent {
static propTypes = {
onFinished: PropTypes.func.isRequired,
}
showKeyBackupDialog = () => {
constructor(props) {
super(props);
this.state = {
loading: true,
error: null,
unverifiedDevice: null,
};
}
componentWillMount() {
this._loadBackupStatus();
}
async _loadBackupStatus() {
let backupSigStatus;
try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
} catch (e) {
console.log("Unable to fetch key backup status", e);
this.setState({
loading: false,
error: e,
});
return;
}
let unverifiedDevice;
for (const sig of backupSigStatus.sigs) {
if (!sig.device.isVerified()) {
unverifiedDevice = sig.device;
break;
}
}
this.setState({
loading: false,
unverifiedDevice,
});
}
showSetupDialog = () => {
if (this.state.unverifiedDevice) {
// A key backup exists for this account, but the creating device is not
// verified, so we'll show the device verify dialog.
// TODO: Should change to a restore key backup flow that checks the recovery
// passphrase while at the same time also cross-signing the device as well in
// a single flow (for cases where a key backup exists but the backup creating
// device is unverified). Since we don't have that yet, we'll look for an
// unverified device and verify it. Note that this means we won't restore
// keys yet; instead we'll only trust the backup for sending our own new keys
// to it.
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
userId: MatrixClientPeg.get().credentials.userId,
device: this.state.unverifiedDevice,
onFinished: this.props.onFinished,
});
return;
}
// The default case assumes that a key backup doesn't exist for this account, so
// we'll show the create key backup flow.
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
{
@ -46,29 +109,51 @@ export default class RoomRecoveryReminder extends React.PureComponent {
this.props.onFinished(false);
},
onSetup: () => {
this.showKeyBackupDialog();
this.showSetupDialog();
},
},
);
}
onSetupClick = () => {
this.showKeyBackupDialog();
this.showSetupDialog();
}
render() {
if (this.state.loading) {
return null;
}
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
let body;
if (this.state.error) {
body = <div className="error">
{_t("Unable to load key backup status")}
</div>;
} else if (this.state.unverifiedDevice) {
// A key backup exists for this account, but the creating device is not
// verified.
body = _t(
"To view your secure message history and ensure you can view new " +
"messages on future devices, set up Secure Message Recovery.",
);
} else {
// The default case assumes that a key backup doesn't exist for this account.
// (This component doesn't currently check that itself.)
body = _t(
"If you log out or use another device, you'll lose your " +
"secure message history. To prevent this, set up Secure " +
"Message Recovery.",
);
}
return (
<div className="mx_RoomRecoveryReminder">
<div className="mx_RoomRecoveryReminder_header">{_t(
"Secure Message Recovery",
)}</div>
<div className="mx_RoomRecoveryReminder_body">{_t(
"If you log out or use another device, you'll lose your " +
"secure message history. To prevent this, set up Secure " +
"Message Recovery.",
)}</div>
<div className="mx_RoomRecoveryReminder_body">{body}</div>
<div className="mx_RoomRecoveryReminder_buttons">
<AccessibleButton className="mx_RoomRecoveryReminder_button mx_RoomRecoveryReminder_secondary"
onClick={this.onDontAskAgainClick}>

View file

@ -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',
@ -251,6 +252,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.getInvitedAndJoinedMemberCount() === 2;
let subtext = null;
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) {
subtext = otherMember.user._unstable_statusMessage;
}
}
const classes = classNames({
'mx_RoomTile': true,
'mx_RoomTile_selected': this.state.selected,
@ -261,6 +273,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.collapsed,
});
const avatarClasses = classNames({
@ -286,6 +299,7 @@ module.exports = React.createClass({
const EmojiText = sdk.getComponent('elements.EmojiText');
let label;
let subtextLabel;
let tooltip;
if (!this.props.collapsed) {
const nameClasses = classNames({
@ -294,6 +308,8 @@ module.exports = React.createClass({
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
});
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
if (this.state.selected) {
const nameSelected = <EmojiText>{ name }</EmojiText>;
@ -337,9 +353,14 @@ module.exports = React.createClass({
{ dmIndicator }
</div>
</div>
{ label }
{ contextMenuButton }
{ badge }
<div className="mx_RoomTile_nameContainer">
<div className="mx_RoomTile_labelContainer">
{ label }
{ subtextLabel }
</div>
{ contextMenuButton }
{ badge }
</div>
{ /* { incomingCallBox } */ }
{ tooltip }
</AccessibleButton>;

View file

@ -250,11 +250,14 @@
"A word by itself is easy to guess": "A word by itself is easy to guess",
"Names and surnames by themselves are easy to guess": "Names and surnames by themselves are easy to guess",
"Common names and surnames are easy to guess": "Common names and surnames are easy to guess",
"Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess",
"Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess",
"There was an error joining the room": "There was an error joining the room",
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
"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",
@ -563,8 +566,9 @@
"You are trying to access a room.": "You are trying to access a room.",
"<a>Click here</a> to join the discussion!": "<a>Click here</a> to join the discussion!",
"This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled",
"Secure Message Recovery": "Secure Message Recovery",
"To view your secure message history and ensure you can view new messages on future devices, set up Secure Message Recovery.": "To view your secure message history and ensure you can view new messages on future devices, set up Secure Message Recovery.",
"If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.": "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.",
"Secure Message Recovery": "Secure Message Recovery",
"Don't ask again": "Don't ask again",
"Set up": "Set up",
"To change the room's avatar, you must be a": "To change the room's avatar, you must be a",
@ -888,6 +892,7 @@
"What GitHub issue are these logs for?": "What GitHub issue are these logs for?",
"Notes:": "Notes:",
"Send logs": "Send logs",
"Unable to load commit detail: %(msg)s": "Unable to load commit detail: %(msg)s",
"Unavailable": "Unavailable",
"Changelog": "Changelog",
"Create a new chat or reuse an existing one": "Create a new chat or reuse an existing one",
@ -1064,6 +1069,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 <b>not</b> able to run Riot.": "Sorry, your browser is <b>not</b> 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.",

View file

@ -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"),

View file

@ -52,6 +52,8 @@ _td("This is similar to a commonly used password");
_td("A word by itself is easy to guess");
_td("Names and surnames by themselves are easy to guess");
_td("Common names and surnames are easy to guess");
_td("Straight rows of keys are easy to guess");
_td("Short keyboard patterns are easy to guess");
/**
* Wrapper around zxcvbn password strength estimation