Merge branches 'develop' and 't3chguy/cmds' of github.com:matrix-org/matrix-react-sdk into t3chguy/cmds

 Conflicts:
	src/SlashCommands.tsx
This commit is contained in:
Michael Telatynski 2020-03-31 11:49:53 +01:00
commit 6e61761012
21 changed files with 498 additions and 111 deletions

View file

@ -1,3 +1,159 @@
Changes in [2.3.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.3.0) (2020-03-30)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.3.0-rc.1...v2.3.0)
* Upgrade JS SDK to 5.2.0
Changes in [2.3.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.3.0-rc.1) (2020-03-26)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.3...v2.3.0-rc.1)
* Upgrade JS SDK to 5.2.0-rc.1
* Add a flag to control whether cross-signing signatures are trusted
[\#4277](https://github.com/matrix-org/matrix-react-sdk/pull/4277)
* Update from Weblate
[\#4282](https://github.com/matrix-org/matrix-react-sdk/pull/4282)
* Update copy on SSSS symmetric upgrade toast
[\#4281](https://github.com/matrix-org/matrix-react-sdk/pull/4281)
* Wait for SSSS upgrade to complete
[\#4270](https://github.com/matrix-org/matrix-react-sdk/pull/4270)
* Update cross-signing verification copy and fix i18n
[\#4278](https://github.com/matrix-org/matrix-react-sdk/pull/4278)
* Fix soft-crash on bad permalinks
[\#4280](https://github.com/matrix-org/matrix-react-sdk/pull/4280)
* Fix: make self-verification wait for incoming request
[\#4267](https://github.com/matrix-org/matrix-react-sdk/pull/4267)
* Fall back to non-standard persisted api for Safari
[\#4272](https://github.com/matrix-org/matrix-react-sdk/pull/4272)
* Respond to backup key sharing requests
[\#4275](https://github.com/matrix-org/matrix-react-sdk/pull/4275)
* Log and display secret sharing cache state
[\#4268](https://github.com/matrix-org/matrix-react-sdk/pull/4268)
* Support sending config and ready events to capable widgets (Jitsi)
[\#4266](https://github.com/matrix-org/matrix-react-sdk/pull/4266)
* If cached keys are present in the key backup dialog, use them
[\#4273](https://github.com/matrix-org/matrix-react-sdk/pull/4273)
* Fix formatbar not hidden on highlighted message sent
[\#4265](https://github.com/matrix-org/matrix-react-sdk/pull/4265)
* Support Jitsi conferences sent/received on Riot Mobile and older Riot Webs
[\#4252](https://github.com/matrix-org/matrix-react-sdk/pull/4252)
* Use unified function to check cross-signing is ready
[\#4263](https://github.com/matrix-org/matrix-react-sdk/pull/4263)
* Migrate SSSS to symmetric
[\#4224](https://github.com/matrix-org/matrix-react-sdk/pull/4224)
* Migration to symmetric SSSS
[\#4242](https://github.com/matrix-org/matrix-react-sdk/pull/4242)
* Always display verification request toasts on top
[\#4262](https://github.com/matrix-org/matrix-react-sdk/pull/4262)
* Fix: assume SAS is supported when starting request with .start
[\#4249](https://github.com/matrix-org/matrix-react-sdk/pull/4249)
* Fix logout when Olm failed to load.
[\#4261](https://github.com/matrix-org/matrix-react-sdk/pull/4261)
* Improve naming of Jitsi conferences
[\#4251](https://github.com/matrix-org/matrix-react-sdk/pull/4251)
* Handle matrix.to user permalink in-room rather than solo
[\#4245](https://github.com/matrix-org/matrix-react-sdk/pull/4245)
* Fix: filter room list (again) by canonical and alternative aliases
[\#4260](https://github.com/matrix-org/matrix-react-sdk/pull/4260)
* EventIndex: Add some logging to the file panel populating.
[\#4250](https://github.com/matrix-org/matrix-react-sdk/pull/4250)
* Update from Weblate
[\#4259](https://github.com/matrix-org/matrix-react-sdk/pull/4259)
* Migrate RoomView to React Contexts in the hope for better temporal stability
[\#4258](https://github.com/matrix-org/matrix-react-sdk/pull/4258)
* Update WidgetUtils.js fix Jitsi path
[\#4256](https://github.com/matrix-org/matrix-react-sdk/pull/4256)
* Fix local jitsi build url fail and missing argument
[\#4255](https://github.com/matrix-org/matrix-react-sdk/pull/4255)
* Add shortcut CmdOrCtrl+. to toggle right panel
[\#4244](https://github.com/matrix-org/matrix-react-sdk/pull/4244)
* Improve Keyboard Shortcuts. Add alt-arrows & alt-shift-arrows
[\#4241](https://github.com/matrix-org/matrix-react-sdk/pull/4241)
* Bring back legacy verification by comparing public device keys
[\#4240](https://github.com/matrix-org/matrix-react-sdk/pull/4240)
* Searching: Return an empty result if the search term is an empty string.
[\#4248](https://github.com/matrix-org/matrix-react-sdk/pull/4248)
* Break continuation on showHiddenEvents-rendered events
[\#4247](https://github.com/matrix-org/matrix-react-sdk/pull/4247)
* Watch for show-RR settings changes, use room-specific and fix margins
[\#4246](https://github.com/matrix-org/matrix-react-sdk/pull/4246)
* Register Mac electron specific Cmd+, shortcut to User Settings
[\#4243](https://github.com/matrix-org/matrix-react-sdk/pull/4243)
* Use a local wrapper for Jitsi calls
[\#4234](https://github.com/matrix-org/matrix-react-sdk/pull/4234)
* Invite Dialog fixes
[\#4233](https://github.com/matrix-org/matrix-react-sdk/pull/4233)
* RoomPreviewBar word-break the sender name too
[\#4239](https://github.com/matrix-org/matrix-react-sdk/pull/4239)
* Report to the user when a key signature upload fails
[\#4229](https://github.com/matrix-org/matrix-react-sdk/pull/4229)
* pre-send megolm keys when possible when a user starts typing
[\#4235](https://github.com/matrix-org/matrix-react-sdk/pull/4235)
* we don't do mx_fadable anymore so get rid of broken RightPanel disabling
[\#4238](https://github.com/matrix-org/matrix-react-sdk/pull/4238)
* Fix left left panel overflowing vertically
[\#4237](https://github.com/matrix-org/matrix-react-sdk/pull/4237)
* Fix custom tags causing left panel to over-expand
[\#4236](https://github.com/matrix-org/matrix-react-sdk/pull/4236)
* Add Keyboard shortcuts dialog
[\#4231](https://github.com/matrix-org/matrix-react-sdk/pull/4231)
* Don't use buildkite agent to upload logs
[\#4232](https://github.com/matrix-org/matrix-react-sdk/pull/4232)
* Remove Gemini Scrollbars
[\#4217](https://github.com/matrix-org/matrix-react-sdk/pull/4217)
* Room Directory Explore Servers redesign
[\#4209](https://github.com/matrix-org/matrix-react-sdk/pull/4209)
* Fix redo keyboard shortcut on macOS
[\#4110](https://github.com/matrix-org/matrix-react-sdk/pull/4110)
* Fix: ensure local state for aliases doesn't get garbled up
[\#4230](https://github.com/matrix-org/matrix-react-sdk/pull/4230)
* Rename 'jump to bottom' to avoid ublock block
[\#4208](https://github.com/matrix-org/matrix-react-sdk/pull/4208)
* Restore key backup in background after complete security
[\#4225](https://github.com/matrix-org/matrix-react-sdk/pull/4225)
* Fix key backup trust text for cross-signing
[\#4223](https://github.com/matrix-org/matrix-react-sdk/pull/4223)
* Add default on config setting to control call button in composer
[\#4227](https://github.com/matrix-org/matrix-react-sdk/pull/4227)
* Fix: make alternative addresses UX less confusing
[\#4221](https://github.com/matrix-org/matrix-react-sdk/pull/4221)
* Wait for verification request on login
[\#4222](https://github.com/matrix-org/matrix-react-sdk/pull/4222)
* EventIndex: Add support to delete events from the index.
[\#4204](https://github.com/matrix-org/matrix-react-sdk/pull/4204)
* EventIndex: Remove a checkpoint if the HTTP request returns a 403.
[\#4214](https://github.com/matrix-org/matrix-react-sdk/pull/4214)
* Move to composer when typing letters with Shift held
[\#4216](https://github.com/matrix-org/matrix-react-sdk/pull/4216)
* Wrap large room names when previewing them
[\#4213](https://github.com/matrix-org/matrix-react-sdk/pull/4213)
* Rename Review Devices to Review Sessions
[\#4219](https://github.com/matrix-org/matrix-react-sdk/pull/4219)
* Fix typo in tabIndex to make React happy
[\#4215](https://github.com/matrix-org/matrix-react-sdk/pull/4215)
* Proof of concept for custom theme adding
[\#4148](https://github.com/matrix-org/matrix-react-sdk/pull/4148)
* Remove stuff that yarn install doesn't think we need
[\#4205](https://github.com/matrix-org/matrix-react-sdk/pull/4205)
* Declare jsx in tsconfig for IDEs
[\#4207](https://github.com/matrix-org/matrix-react-sdk/pull/4207)
* Fix: best-effort to join room without canonical alias over federation from
room directory
[\#4210](https://github.com/matrix-org/matrix-react-sdk/pull/4210)
* Test for cross-signing homeserver support during login, toasts
[\#4206](https://github.com/matrix-org/matrix-react-sdk/pull/4206)
* Send verification request to a single device in a way compatible with non-
cross-signing
[\#4202](https://github.com/matrix-org/matrix-react-sdk/pull/4202)
* Fixes for removing local alias
[\#4199](https://github.com/matrix-org/matrix-react-sdk/pull/4199)
* yarn upgrade
[\#4201](https://github.com/matrix-org/matrix-react-sdk/pull/4201)
* Support TypeScript for React components
[\#4203](https://github.com/matrix-org/matrix-react-sdk/pull/4203)
* When room name is changed, show both the old and new name
[\#4183](https://github.com/matrix-org/matrix-react-sdk/pull/4183)
Changes in [2.2.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.3) (2020-03-17) Changes in [2.2.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.3) (2020-03-17)
=================================================================================================== ===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.3-rc.1...v2.2.3) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.3-rc.1...v2.2.3)

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "2.2.3", "version": "2.3.0",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {

View file

@ -137,7 +137,7 @@ limitations under the License.
top: -8px; top: -8px;
border-radius: 8px; border-radius: 8px;
background-color: $neutral-badge-color; background-color: $neutral-badge-color;
color: #ffffff; color: #000;
font-weight: 600; font-weight: 600;
font-size: 10px; font-size: 10px;
text-align: center; text-align: center;

View file

@ -430,7 +430,7 @@ async function _startCallApp(roomId, type) {
return; return;
} }
const confId = `JitsiConference_${generateHumanReadableId()}`; const confId = `JitsiConference${generateHumanReadableId()}`;
const jitsiDomain = SdkConfig.get()['jitsi']['preferredDomain']; const jitsiDomain = SdkConfig.get()['jitsi']['preferredDomain'];
let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl(); let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl();

View file

@ -85,7 +85,7 @@ class Command {
aliases = [], aliases = [],
args = '', args = '',
description, description,
runFn=undefined, runFn = undefined,
category = CommandCategories.other, category = CommandCategories.other,
hideCompletionAfterSpace = false, hideCompletionAfterSpace = false,
}: { }: {
@ -160,6 +160,15 @@ export const Commands = [
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
}), }),
new Command({
command: 'html',
args: '<message>',
description: _td('Sends a message as html, without interpreting it as markdown'),
runFn: function(roomId, messages) {
return success(MatrixClientPeg.get().sendHtmlMessage(roomId, messages, messages));
},
category: CommandCategories.messages,
}),
new Command({ new Command({
command: 'ddg', command: 'ddg',
args: '<query>', args: '<query>',

View file

@ -67,8 +67,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this._keyInfo = null; this._recoveryKey = null;
this._encodedRecoveryKey = null;
this._recoveryKeyNode = null; this._recoveryKeyNode = null;
this._setZxcvbnResultTimeout = null; this._setZxcvbnResultTimeout = null;
@ -180,7 +179,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
} }
_onDownloadClick = () => { _onDownloadClick = () => {
const blob = new Blob([this._encodedRecoveryKey], { const blob = new Blob([this._recoveryKey.encodedPrivateKey], {
type: 'text/plain;charset=us-ascii', type: 'text/plain;charset=us-ascii',
}); });
FileSaver.saveAs(blob, 'recovery-key.txt'); FileSaver.saveAs(blob, 'recovery-key.txt');
@ -234,14 +233,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
if (force) { if (force) {
await cli.bootstrapSecretStorage({ await cli.bootstrapSecretStorage({
authUploadDeviceSigningKeys: this._doBootstrapUIAuth, authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
createSecretStorageKey: async () => this._keyInfo, createSecretStorageKey: async () => this._recoveryKey,
setupNewKeyBackup: true, setupNewKeyBackup: true,
setupNewSecretStorage: true, setupNewSecretStorage: true,
}); });
} else { } else {
await cli.bootstrapSecretStorage({ await cli.bootstrapSecretStorage({
authUploadDeviceSigningKeys: this._doBootstrapUIAuth, authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
createSecretStorageKey: async () => this._keyInfo, createSecretStorageKey: async () => this._recoveryKey,
keyBackupInfo: this.state.backupInfo, keyBackupInfo: this.state.backupInfo,
setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup, setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup,
getKeyBackupPassphrase: promptForBackupPassphrase, getKeyBackupPassphrase: promptForBackupPassphrase,
@ -299,10 +298,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
} }
_onSkipPassPhraseClick = async () => { _onSkipPassPhraseClick = async () => {
const [keyInfo, encodedRecoveryKey] = this._recoveryKey =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
this._keyInfo = keyInfo;
this._encodedRecoveryKey = encodedRecoveryKey;
this.setState({ this.setState({
copied: false, copied: false,
downloaded: false, downloaded: false,
@ -335,10 +332,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
if (this.state.passPhrase !== this.state.passPhraseConfirm) return; if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
const [keyInfo, encodedRecoveryKey] = this._recoveryKey =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase); await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase);
this._keyInfo = keyInfo;
this._encodedRecoveryKey = encodedRecoveryKey;
this.setState({ this.setState({
copied: false, copied: false,
downloaded: false, downloaded: false,
@ -613,7 +608,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div> </div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer"> <div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKey"> <div className="mx_CreateSecretStorageDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._encodedRecoveryKey}</code> <code ref={this._collectRecoveryKeyNode}>{this._recoveryKey.encodedPrivateKey}</code>
</div> </div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons"> <div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
<AccessibleButton kind='primary' className="mx_Dialog_primary" onClick={this._onCopyClick}> <AccessibleButton kind='primary' className="mx_Dialog_primary" onClick={this._onCopyClick}>

View file

@ -37,6 +37,8 @@ export default class EmbeddedPage extends React.PureComponent {
className: PropTypes.string, className: PropTypes.string,
// Whether to wrap the page in a scrollbar // Whether to wrap the page in a scrollbar
scrollbar: PropTypes.bool, scrollbar: PropTypes.bool,
// Map of keys to replace with values, e.g {$placeholder: "value"}
replaceMap: PropTypes.object,
}; };
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
@ -81,6 +83,13 @@ export default class EmbeddedPage extends React.PureComponent {
} }
body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1)); body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1));
if (this.props.replaceMap) {
Object.keys(this.props.replaceMap).forEach(key => {
body = body.split(key).join(this.props.replaceMap[key]);
});
}
this.setState({ page: body }); this.setState({ page: body });
}, },
); );

View file

@ -2022,7 +2022,7 @@ export default createReactClass({
} }
} else if (this.state.view === VIEWS.WELCOME) { } else if (this.state.view === VIEWS.WELCOME) {
const Welcome = sdk.getComponent('auth.Welcome'); const Welcome = sdk.getComponent('auth.Welcome');
view = <Welcome />; view = <Welcome {...this.getServerProperties()} />;
} else if (this.state.view === VIEWS.REGISTER) { } else if (this.state.view === VIEWS.REGISTER) {
const Registration = sdk.getComponent('structures.auth.Registration'); const Registration = sdk.getComponent('structures.auth.Registration');
view = ( view = (

View file

@ -55,6 +55,7 @@ import RightPanelStore from "../../stores/RightPanelStore";
import {haveTileForEvent} from "../views/rooms/EventTile"; import {haveTileForEvent} from "../views/rooms/EventTile";
import RoomContext from "../../contexts/RoomContext"; import RoomContext from "../../contexts/RoomContext";
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import { shieldStatusForRoom } from '../../utils/ShieldUtils';
const DEBUG = false; const DEBUG = false;
let debuglog = function() {}; let debuglog = function() {};
@ -817,40 +818,9 @@ export default createReactClass({
return; return;
} }
// Duplication between here and _updateE2eStatus in RoomTile
/* At this point, the user has encryption on and cross-signing on */ /* At this point, the user has encryption on and cross-signing on */
const e2eMembers = await room.getEncryptionTargetMembers();
const verified = [];
const unverified = [];
e2eMembers.map(({userId}) => userId)
.filter((userId) => userId !== this.context.getUserId())
.forEach((userId) => {
(this.context.checkUserTrust(userId).isCrossSigningVerified() ?
verified : unverified).push(userId);
});
debuglog("e2e verified", verified, "unverified", unverified);
/* Check all verified user devices. */
/* Don't alarm if no other users are verified */
const targets = (verified.length > 0) ? [...verified, this.context.getUserId()] : verified;
for (const userId of targets) {
const devices = await this.context.getStoredDevicesForUser(userId);
const anyDeviceNotVerified = devices.some(({deviceId}) => {
return !this.context.checkDeviceTrust(userId, deviceId).isVerified();
});
if (anyDeviceNotVerified) {
this.setState({
e2eStatus: "warning",
});
debuglog("e2e status set to warning as not all users trust all of their sessions." +
" Aborted on user", userId);
return;
}
}
this.setState({ this.setState({
e2eStatus: unverified.length === 0 ? "verified" : "normal", e2eStatus: await shieldStatusForRoom(this.context, room),
}); });
}, },

View file

@ -18,6 +18,12 @@ import React from 'react';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import AuthPage from "./AuthPage"; import AuthPage from "./AuthPage";
import * as Matrix from "matrix-js-sdk";
import {_td} from "../../../languageHandler";
import PlatformPeg from "../../../PlatformPeg";
// translatable strings for Welcome pages
_td("Sign in with SSO");
export default class Welcome extends React.PureComponent { export default class Welcome extends React.PureComponent {
render() { render() {
@ -33,11 +39,24 @@ export default class Welcome extends React.PureComponent {
pageUrl = 'welcome.html'; pageUrl = 'welcome.html';
} }
const {hsUrl, isUrl} = this.props.serverConfig;
const tmpClient = Matrix.createClient({
baseUrl: hsUrl,
idBaseUrl: isUrl,
});
const plaf = PlatformPeg.get();
const callbackUrl = plaf.getSSOCallbackUrl(tmpClient.getHomeserverUrl(), tmpClient.getIdentityServerUrl());
return ( return (
<AuthPage> <AuthPage>
<div className="mx_Welcome"> <div className="mx_Welcome">
<EmbeddedPage className="mx_WelcomePage" <EmbeddedPage
className="mx_WelcomePage"
url={pageUrl} url={pageUrl}
replaceMap={{
"$riot:ssoUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "sso"),
"$riot:casUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "cas"),
}}
/> />
<LanguageSelector /> <LanguageSelector />
</div> </div>

View file

@ -20,6 +20,7 @@ import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {MatrixEvent, RoomMember} from "matrix-js-sdk"; import {MatrixEvent, RoomMember} from "matrix-js-sdk";
import {useStateToggle} from "../../../hooks/useStateToggle"; import {useStateToggle} from "../../../hooks/useStateToggle";
import AccessibleButton from "./AccessibleButton";
const EventListSummary = ({events, children, threshold=3, onToggle, startExpanded, summaryMembers=[], summaryText}) => { const EventListSummary = ({events, children, threshold=3, onToggle, startExpanded, summaryMembers=[], summaryText}) => {
const [expanded, toggleExpanded] = useStateToggle(startExpanded); const [expanded, toggleExpanded] = useStateToggle(startExpanded);
@ -42,24 +43,15 @@ const EventListSummary = ({events, children, threshold=3, onToggle, startExpande
); );
} }
let body;
if (expanded) { if (expanded) {
return ( body = <React.Fragment>
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}> <div className="mx_EventListSummary_line">&nbsp;</div>
<div className={"mx_EventListSummary_toggle"} onClick={toggleExpanded}> { children }
{ _t('collapse') } </React.Fragment>;
</div> } else {
<div className="mx_EventListSummary_line">&nbsp;</div> const avatars = summaryMembers.map((m) => <MemberAvatar key={m.userId} member={m} width={14} height={14} />);
{ children } body = (
</div>
);
}
const avatars = summaryMembers.map((m) => <MemberAvatar key={m.userId} member={m} width={14} height={14} />);
return (
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
<div className={"mx_EventListSummary_toggle"} onClick={toggleExpanded}>
{ _t('expand') }
</div>
<div className="mx_EventTile_line"> <div className="mx_EventTile_line">
<div className="mx_EventTile_info"> <div className="mx_EventTile_info">
<span className="mx_EventListSummary_avatars" onClick={toggleExpanded}> <span className="mx_EventListSummary_avatars" onClick={toggleExpanded}>
@ -70,6 +62,15 @@ const EventListSummary = ({events, children, threshold=3, onToggle, startExpande
</span> </span>
</div> </div>
</div> </div>
);
}
return (
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
<AccessibleButton className="mx_EventListSummary_toggle" onClick={toggleExpanded} aria-expanded={expanded}>
{ expanded ? _t('collapse') : _t('expand') }
</AccessibleButton>
{ body }
</div> </div>
); );
}; };

View file

@ -32,7 +32,7 @@ function getId() {
export default class Field extends React.PureComponent { export default class Field extends React.PureComponent {
static propTypes = { static propTypes = {
// The field's ID, which binds the input and label together. Immutable. // The field's ID, which binds the input and label together. Immutable.
id: PropTypes.string.isRequired, id: PropTypes.string,
// The element to create. Defaults to "input". // The element to create. Defaults to "input".
// To define options for a select, use <Field><option ... /></Field> // To define options for a select, use <Field><option ... /></Field>
element: PropTypes.oneOf(["input", "select", "textarea"]), element: PropTypes.oneOf(["input", "select", "textarea"]),

View file

@ -68,8 +68,10 @@ export const getE2EStatus = (cli, userId, devices) => {
return hasUnverifiedDevice ? "warning" : "verified"; return hasUnverifiedDevice ? "warning" : "verified";
} }
const isMe = userId === cli.getUserId(); const isMe = userId === cli.getUserId();
const userVerified = cli.checkUserTrust(userId).isCrossSigningVerified(); const userTrust = cli.checkUserTrust(userId);
if (!userVerified) return "normal"; if (!userTrust.isCrossSigningVerified()) {
return userTrust.wasCrossSigningVerified() ? "warning" : "normal";
}
const anyDeviceUnverified = devices.some(device => { const anyDeviceUnverified = devices.some(device => {
const { deviceId } = device; const { deviceId } = device;

View file

@ -121,10 +121,10 @@ export default createReactClass({
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const { userId } = this.props.member; const { userId } = this.props.member;
const isMe = userId === cli.getUserId(); const isMe = userId === cli.getUserId();
const userVerified = cli.checkUserTrust(userId).isCrossSigningVerified(); const userTrust = cli.checkUserTrust(userId);
if (!userVerified) { if (!userTrust.isCrossSigningVerified()) {
this.setState({ this.setState({
e2eStatus: "normal", e2eStatus: userTrust.wasCrossSigningVerified() ? "warning" : "normal",
}); });
return; return;
} }

View file

@ -19,12 +19,13 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import classNames from 'classnames'; import classNames from 'classnames';
import AccessibleButton from "../elements/AccessibleButton";
export default class MessageComposerFormatBar extends React.PureComponent { export default class MessageComposerFormatBar extends React.PureComponent {
static propTypes = { static propTypes = {
onAction: PropTypes.func.isRequired, onAction: PropTypes.func.isRequired,
shortcuts: PropTypes.object.isRequired, shortcuts: PropTypes.object.isRequired,
} };
constructor(props) { constructor(props) {
super(props); super(props);
@ -64,7 +65,7 @@ class FormatButton extends React.PureComponent {
icon: PropTypes.string.isRequired, icon: PropTypes.string.isRequired,
shortcut: PropTypes.string, shortcut: PropTypes.string,
visible: PropTypes.bool, visible: PropTypes.bool,
} };
render() { render() {
const InteractiveTooltip = sdk.getComponent('elements.InteractiveTooltip'); const InteractiveTooltip = sdk.getComponent('elements.InteractiveTooltip');
@ -82,11 +83,12 @@ class FormatButton extends React.PureComponent {
return ( return (
<InteractiveTooltip content={tooltipContent} forceHidden={!this.props.visible}> <InteractiveTooltip content={tooltipContent} forceHidden={!this.props.visible}>
<span aria-label={this.props.label} <AccessibleButton
role="button" as="span"
onClick={this.props.onClick} role="button"
className={className}> onClick={this.props.onClick}
</span> aria-label={this.props.label}
className={className} />
</InteractiveTooltip> </InteractiveTooltip>
); );
} }

View file

@ -37,6 +37,7 @@ import E2EIcon from './E2EIcon';
import InviteOnlyIcon from './InviteOnlyIcon'; import InviteOnlyIcon from './InviteOnlyIcon';
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
import rate_limited_func from '../../../ratelimitedfunc'; import rate_limited_func from '../../../ratelimitedfunc';
import { shieldStatusForRoom } from '../../../utils/ShieldUtils';
export default createReactClass({ export default createReactClass({
displayName: 'RoomTile', displayName: 'RoomTile',
@ -154,35 +155,9 @@ export default createReactClass({
return; return;
} }
// Duplication between here and _updateE2eStatus in RoomView /* At this point, the user has encryption on and cross-signing on */
const e2eMembers = await this.props.room.getEncryptionTargetMembers();
const verified = [];
const unverified = [];
e2eMembers.map(({userId}) => userId)
.filter((userId) => userId !== cli.getUserId())
.forEach((userId) => {
(cli.checkUserTrust(userId).isCrossSigningVerified() ?
verified : unverified).push(userId);
});
/* Check all verified user devices. */
/* Don't alarm if no other users are verified */
const targets = (verified.length > 0) ? [...verified, cli.getUserId()] : verified;
for (const userId of targets) {
const devices = await cli.getStoredDevicesForUser(userId);
const allDevicesVerified = devices.every(({deviceId}) => {
return cli.checkDeviceTrust(userId, deviceId).isVerified();
});
if (!allDevicesVerified) {
this.setState({
e2eStatus: "warning",
});
return;
}
}
this.setState({ this.setState({
e2eStatus: unverified.length === 0 ? "verified" : "normal", e2eStatus: await shieldStatusForRoom(cli, this.props.room),
}); });
}, },

View file

@ -153,6 +153,7 @@
"Usage": "Usage", "Usage": "Usage",
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Prepends ¯\\_(ツ)_/¯ to a plain-text message", "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Prepends ¯\\_(ツ)_/¯ to a plain-text message",
"Sends a message as plain text, without interpreting it as markdown": "Sends a message as plain text, without interpreting it as markdown", "Sends a message as plain text, without interpreting it as markdown": "Sends a message as plain text, without interpreting it as markdown",
"Sends a message as html, without interpreting it as markdown": "Sends a message as html, without interpreting it as markdown",
"Searches DuckDuckGo for results": "Searches DuckDuckGo for results", "Searches DuckDuckGo for results": "Searches DuckDuckGo for results",
"/ddg is not a command": "/ddg is not a command", "/ddg is not a command": "/ddg is not a command",
"To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.", "To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.",
@ -1880,6 +1881,7 @@
"Find other public servers or use a custom server": "Find other public servers or use a custom server", "Find other public servers or use a custom server": "Find other public servers or use a custom server",
"Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s", "Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s",
"Sign in to your Matrix account on <underlinedServerName />": "Sign in to your Matrix account on <underlinedServerName />", "Sign in to your Matrix account on <underlinedServerName />": "Sign in to your Matrix account on <underlinedServerName />",
"Sign in with SSO": "Sign in with SSO",
"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.",
"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.", "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 <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.": "Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.", "Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.": "Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.",

View file

@ -469,6 +469,9 @@ export default class EventIndex extends EventEmitter {
// decryption keys, do we want to retry this checkpoint at a later // decryption keys, do we want to retry this checkpoint at a later
// stage? // stage?
const filteredEvents = matrixEvents.filter(this.isValidEvent); const filteredEvents = matrixEvents.filter(this.isValidEvent);
const undecryptableEvents = matrixEvents.filter((ev) => {
return ev.isDecryptionFailure();
});
// Collect the redaction events so we can delete the redacted events // Collect the redaction events so we can delete the redacted events
// from the index. // from the index.
@ -503,7 +506,10 @@ export default class EventIndex extends EventEmitter {
console.log( console.log(
"EventIndex: Crawled room", "EventIndex: Crawled room",
client.getRoom(checkpoint.roomId).name, client.getRoom(checkpoint.roomId).name,
"and fetched", events.length, "events.", "and fetched total", matrixEvents.length, "events of which",
events.length, "are being added,", redactionEvents.length,
"are redacted,", matrixEvents.length - events.length,
"are being skipped, undecryptable", undecryptableEvents.length,
); );
try { try {

58
src/utils/ShieldUtils.ts Normal file
View file

@ -0,0 +1,58 @@
import DMRoomMap from './DMRoomMap';
/* For now, a cut-down type spec for the client */
interface Client {
getUserId: () => string;
checkUserTrust: (userId: string) => {
isCrossSigningVerified: () => boolean
wasCrossSigningVerified: () => boolean
};
getStoredDevicesForUser: (userId: string) => Promise<[{ deviceId: string }]>;
checkDeviceTrust: (userId: string, deviceId: string) => {
isVerified: () => boolean
}
}
interface Room {
getEncryptionTargetMembers: () => Promise<[{userId: string}]>;
roomId: string;
}
export async function shieldStatusForRoom(client: Client, room: Room): Promise<string> {
const members = (await room.getEncryptionTargetMembers()).map(({userId}) => userId);
const inDMMap = !!DMRoomMap.shared().getUserIdForRoomId(room.roomId);
const verified: string[] = [];
const unverified: string[] = [];
members.filter((userId) => userId !== client.getUserId())
.forEach((userId) => {
(client.checkUserTrust(userId).isCrossSigningVerified() ?
verified : unverified).push(userId);
});
/* Alarm if any unverified users were verified before. */
for (const userId of unverified) {
if (client.checkUserTrust(userId).wasCrossSigningVerified()) {
return "warning";
}
}
/* Check all verified user devices. */
/* Don't alarm if no other users are verified */
const includeUser = (verified.length > 0) && // Don't alarm for self in rooms where nobody else is verified
!inDMMap && // Don't alarm for self in DMs with other users
(members.length !== 2) || // Don't alarm for self in 1:1 chats with other users
(members.length === 1); // Do alarm for self if we're alone in a room
const targets = includeUser ? [...verified, client.getUserId()] : verified;
for (const userId of targets) {
const devices = await client.getStoredDevicesForUser(userId);
const anyDeviceNotVerified = devices.some(({deviceId}) => {
return !client.checkDeviceTrust(userId, deviceId).isVerified();
});
if (anyDeviceNotVerified) {
return "warning";
}
}
return unverified.length === 0 ? "verified" : "normal";
}

View file

@ -0,0 +1,183 @@
import { shieldStatusForRoom } from '../../src/utils/ShieldUtils';
import DMRoomMap from '../../src/utils/DMRoomMap';
function mkClient(selfTrust) {
return {
getUserId: () => "@self:localhost",
checkUserTrust: (userId) => ({
isCrossSigningVerified: () => userId[1] == "T",
wasCrossSigningVerified: () => userId[1] == "T" || userId[1] == "W",
}),
checkDeviceTrust: (userId, deviceId) => ({
isVerified: () => userId === "@self:localhost" ? selfTrust : userId[2] == "T",
}),
getStoredDevicesForUser: async (userId) => ["DEVICE"],
};
}
describe("mkClient self-test", function() {
test.each([true, false])("behaves well for self-trust=%s", (v) => {
const client = mkClient(v);
expect(client.checkDeviceTrust("@self:localhost", "DEVICE").isVerified()).toBe(v);
});
test.each([
["@TT:h", true],
["@TF:h", true],
["@FT:h", false],
["@FF:h", false]],
)("behaves well for user trust %s", (userId, trust) => {
expect(mkClient().checkUserTrust(userId).isCrossSigningVerified()).toBe(trust);
});
test.each([
["@TT:h", true],
["@TF:h", false],
["@FT:h", true],
["@FF:h", false]],
)("behaves well for device trust %s", (userId, trust) => {
expect(mkClient().checkDeviceTrust(userId, "device").isVerified()).toBe(trust);
});
});
describe("shieldStatusForMembership self-trust behaviour", function() {
beforeAll(() => {
DMRoomMap._sharedInstance = {
getUserIdForRoomId: (roomId) => roomId === "DM" ? "@any:h" : null,
};
});
it.each(
[[true, true], [true, false],
[false, true], [false, false]],
)("2 unverified: returns 'normal', self-trust = %s, DM = %s", async (trusted, dm) => {
const client = mkClient(trusted);
const room = {
roomId: dm ? "DM" : "other",
getEncryptionTargetMembers: () => ["@self:localhost", "@FF1:h", "@FF2:h"].map((userId) => ({userId})),
};
const status = await shieldStatusForRoom(client, room);
expect(status).toEqual("normal");
});
it.each(
[["verified", true, true], ["verified", true, false],
["verified", false, true], ["warning", false, false]],
)("2 verified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
const client = mkClient(trusted);
const room = {
roomId: dm ? "DM" : "other",
getEncryptionTargetMembers: () => ["@self:localhost", "@TT1:h", "@TT2:h"].map((userId) => ({userId})),
};
const status = await shieldStatusForRoom(client, room);
expect(status).toEqual(result);
});
it.each(
[["normal", true, true], ["normal", true, false],
["normal", false, true], ["warning", false, false]],
)("2 mixed: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
const client = mkClient(trusted);
const room = {
roomId: dm ? "DM" : "other",
getEncryptionTargetMembers: () => ["@self:localhost", "@TT1:h", "@FF2:h"].map((userId) => ({userId})),
};
const status = await shieldStatusForRoom(client, room);
expect(status).toEqual(result);
});
it.each(
[["verified", true, true], ["verified", true, false],
["warning", false, true], ["warning", false, false]],
)("0 others: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
const client = mkClient(trusted);
const room = {
roomId: dm ? "DM" : "other",
getEncryptionTargetMembers: () => ["@self:localhost"].map((userId) => ({userId})),
};
const status = await shieldStatusForRoom(client, room);
expect(status).toEqual(result);
});
it.each(
[["verified", true, true], ["verified", true, false],
["verified", false, true], ["verified", false, false]],
)("1 verified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
const client = mkClient(trusted);
const room = {
roomId: dm ? "DM" : "other",
getEncryptionTargetMembers: () => ["@self:localhost", "@TT:h"].map((userId) => ({userId})),
};
const status = await shieldStatusForRoom(client, room);
expect(status).toEqual(result);
});
it.each(
[["normal", true, true], ["normal", true, false],
["normal", false, true], ["normal", false, false]],
)("1 unverified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
const client = mkClient(trusted);
const room = {
roomId: dm ? "DM" : "other",
getEncryptionTargetMembers: () => ["@self:localhost", "@FF:h"].map((userId) => ({userId})),
};
const status = await shieldStatusForRoom(client, room);
expect(status).toEqual(result);
});
});
describe("shieldStatusForMembership other-trust behaviour", function() {
beforeAll(() => {
DMRoomMap._sharedInstance = {
getUserIdForRoomId: (roomId) => roomId === "DM" ? "@any:h" : null,
};
});
it.each(
[["warning", true], ["warning", false]],
)("1 verified/untrusted: returns '%s', DM = %s", async (result, dm) => {
const client = mkClient(true);
const room = {
roomId: dm ? "DM" : "other",
getEncryptionTargetMembers: () => ["@self:localhost", "@TF:h"].map((userId) => ({userId})),
};
const status = await shieldStatusForRoom(client, room);
expect(status).toEqual(result);
});
it.each(
[["warning", true], ["warning", false]],
)("2 verified/untrusted: returns '%s', DM = %s", async (result, dm) => {
const client = mkClient(true);
const room = {
roomId: dm ? "DM" : "other",
getEncryptionTargetMembers: () => ["@self:localhost", "@TF:h", "@TT:h"].map((userId) => ({userId})),
};
const status = await shieldStatusForRoom(client, room);
expect(status).toEqual(result);
});
it.each(
[["normal", true], ["normal", false]],
)("2 unverified/untrusted: returns '%s', DM = %s", async (result, dm) => {
const client = mkClient(true);
const room = {
roomId: dm ? "DM" : "other",
getEncryptionTargetMembers: () => ["@self:localhost", "@FF:h", "@FT:h"].map((userId) => ({userId})),
};
const status = await shieldStatusForRoom(client, room);
expect(status).toEqual(result);
});
it.each(
[["warning", true], ["warning", false]],
)("2 was verified: returns '%s', DM = %s", async (result, dm) => {
const client = mkClient(true);
const room = {
roomId: dm ? "DM" : "other",
getEncryptionTargetMembers: () => ["@self:localhost", "@WF:h", "@FT:h"].map((userId) => ({userId})),
};
const status = await shieldStatusForRoom(client, room);
expect(status).toEqual(result);
});
});

View file

@ -5690,8 +5690,8 @@ mathml-tag-names@^2.0.1:
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
version "5.1.1" version "5.2.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/b2e154377a4268441a3b27b183dd7f7018187035" resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/223d37ffce674a23ca73702f04b9ba31cfd84196"
dependencies: dependencies:
"@babel/runtime" "^7.8.3" "@babel/runtime" "^7.8.3"
another-json "^0.2.0" another-json "^0.2.0"