Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into t3chguy/cs_verification_decoration

 Conflicts:
	src/components/views/right_panel/VerificationPanel.js
	src/components/views/toasts/VerificationRequestToast.js
This commit is contained in:
Michael Telatynski 2020-01-24 16:19:41 +00:00
commit 342fcb09c4
36 changed files with 1043 additions and 495 deletions

View file

@ -1,8 +1,10 @@
steps: steps:
- label: ":eslint: JS Lint" - label: ":eslint: JS Lint"
command: command:
# We fetch the develop js-sdk to get our latest eslint rules
- "echo '--- Install js-sdk'" - "echo '--- Install js-sdk'"
- "./scripts/ci/install-deps.sh" - "./scripts/ci/install-deps.sh --ignore-scripts"
- "echo '+++ Lint'"
- "yarn lint:js" - "yarn lint:js"
plugins: plugins:
- docker#v3.0.1: - docker#v3.0.1:
@ -10,8 +12,9 @@ steps:
- label: ":eslint: TS Lint" - label: ":eslint: TS Lint"
command: command:
- "echo '--- Install js-sdk'" - "echo '--- Install'"
- "./scripts/ci/install-deps.sh" - "yarn install --ignore-scripts"
- "echo '+++ Lint'"
- "yarn lint:ts" - "yarn lint:ts"
plugins: plugins:
- docker#v3.0.1: - docker#v3.0.1:
@ -19,12 +22,21 @@ steps:
- label: ":eslint: Types Lint" - label: ":eslint: Types Lint"
command: command:
- "echo '--- Install js-sdk'" - "echo '--- Install'"
- "./scripts/ci/install-deps.sh" - "yarn install --ignore-scripts"
- "echo '+++ Lint'"
- "yarn lint:types" - "yarn lint:types"
plugins: plugins:
- docker#v3.0.1: - docker#v3.0.1:
image: "node:12" image: "node:12"
- label: ":stylelint: Style Lint"
command:
- "echo '--- Install'"
- "yarn install --ignore-scripts"
- "yarn lint:style"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: ":jest: Tests" - label: ":jest: Tests"
agents: agents:
@ -33,13 +45,11 @@ steps:
queue: "medium" queue: "medium"
command: command:
- "echo '--- Install js-sdk'" - "echo '--- Install js-sdk'"
# TODO: Remove hacky chmod for BuildKite # We don't use the babel-ed output for anything so we can --ignore-scripts
- "chmod +x ./scripts/ci/*.sh" # to save transpiling the files. We run the transpile step explicitly in
- "chmod +x ./scripts/*" # the 'build' job.
- "echo '--- Installing Dependencies'" - "./scripts/ci/install-deps.sh --ignore-scripts"
- "./scripts/ci/install-deps.sh" - "yarn run reskindex"
- "echo '--- Running initial build steps'"
- "yarn build"
- "echo '+++ Running Tests'" - "echo '+++ Running Tests'"
- "yarn test" - "yarn test"
plugins: plugins:
@ -48,10 +58,8 @@ steps:
- label: "🛠 Build" - label: "🛠 Build"
command: command:
- "echo '--- Install js-sdk'" - "echo '+++ Install & Build'"
- "./scripts/ci/install-deps.sh" - "yarn install"
- "echo '+++ Building Project'"
- "yarn build"
plugins: plugins:
- docker#v3.0.1: - docker#v3.0.1:
image: "node:12" image: "node:12"
@ -62,14 +70,8 @@ steps:
# e2e tests otherwise take +-8min # e2e tests otherwise take +-8min
queue: "xlarge" queue: "xlarge"
command: command:
# TODO: Remove hacky chmod for BuildKite
- "echo '--- Setup'"
- "chmod +x ./scripts/ci/*.sh"
- "chmod +x ./scripts/*"
- "echo '--- Install js-sdk'" - "echo '--- Install js-sdk'"
- "./scripts/ci/install-deps.sh" - "./scripts/ci/install-deps.sh --ignore-scripts"
- "echo '--- Running initial build steps'"
- "yarn build"
- "echo '+++ Running Tests'" - "echo '+++ Running Tests'"
- "./scripts/ci/end-to-end-tests.sh" - "./scripts/ci/end-to-end-tests.sh"
plugins: plugins:
@ -88,9 +90,6 @@ steps:
# webpack loves to gorge itself on resources. # webpack loves to gorge itself on resources.
queue: "medium" queue: "medium"
command: command:
# TODO: Remove hacky chmod for BuildKite
- "chmod +x ./scripts/ci/*.sh"
- "chmod +x ./scripts/*"
- "echo '+++ Running Tests'" - "echo '+++ Running Tests'"
- "./scripts/ci/riot-unit-tests.sh" - "./scripts/ci/riot-unit-tests.sh"
plugins: plugins:
@ -102,7 +101,7 @@ steps:
- label: "🌐 i18n" - label: "🌐 i18n"
command: command:
- "echo '--- Fetching Dependencies'" - "echo '--- Fetching Dependencies'"
- "yarn install" - "yarn install --ignore-scripts"
- "echo '+++ Testing i18n output'" - "echo '+++ Testing i18n output'"
- "yarn diff-i18n" - "yarn diff-i18n"
plugins: plugins:

View file

@ -150,6 +150,7 @@
@import "./views/rooms/_AuxPanel.scss"; @import "./views/rooms/_AuxPanel.scss";
@import "./views/rooms/_BasicMessageComposer.scss"; @import "./views/rooms/_BasicMessageComposer.scss";
@import "./views/rooms/_E2EIcon.scss"; @import "./views/rooms/_E2EIcon.scss";
@import "./views/rooms/_InviteOnlyIcon.scss";
@import "./views/rooms/_EditMessageComposer.scss"; @import "./views/rooms/_EditMessageComposer.scss";
@import "./views/rooms/_EntityTile.scss"; @import "./views/rooms/_EntityTile.scss";
@import "./views/rooms/_EventTile.scss"; @import "./views/rooms/_EventTile.scss";

View file

@ -0,0 +1,38 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_InviteOnlyIcon {
width: 12px;
height: 12px;
position: relative;
display: block !important;
// Align the padlock with unencrypted room names
margin-left: 6px;
&::before {
background-color: $roomtile-name-color;
mask-image: url('$(res)/img/feather-customised/lock-solid.svg');
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
}

View file

@ -76,6 +76,8 @@ limitations under the License.
left: 60px; left: 60px;
margin-right: 0; // Counteract the E2EIcon class margin-right: 0; // Counteract the E2EIcon class
margin-left: 3px; // Counteract the E2EIcon class margin-left: 3px; // Counteract the E2EIcon class
width: 12px;
height: 12px;
} }
.mx_MessageComposer_noperm_error { .mx_MessageComposer_noperm_error {

View file

@ -21,8 +21,10 @@ limitations under the License.
.mx_E2EIcon { .mx_E2EIcon {
margin: 0; margin: 0;
position: absolute; position: absolute;
bottom: 0; bottom: -1px;
right: -5px; right: -2px;
height: 10px;
width: 10px;
} }
} }
@ -267,24 +269,3 @@ limitations under the License.
.mx_RoomHeader_pinsIndicatorUnread { .mx_RoomHeader_pinsIndicatorUnread {
background-color: $pinned-unread-color; background-color: $pinned-unread-color;
} }
.mx_RoomHeader_PrivateIcon.mx_RoomHeader_isPrivate {
width: 12px;
height: 12px;
position: relative;
display: block !important;
&::before {
background-color: $roomtile-name-color;
mask-image: url('$(res)/img/feather-customised/lock-solid.svg');
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
}

View file

@ -98,6 +98,19 @@ limitations under the License.
z-index: 2; z-index: 2;
} }
// Note we match .mx_E2EIcon to make sure this matches more tightly than just
// .mx_E2EIcon on its own
.mx_RoomTile_e2eIcon.mx_E2EIcon {
height: 10px;
width: 10px;
display: block;
position: absolute;
bottom: -1px;
right: -2px;
z-index: 1;
margin: 0;
}
.mx_RoomTile_name { .mx_RoomTile_name {
font-size: 14px; font-size: 14px;
padding: 0 6px; padding: 0 6px;
@ -202,30 +215,7 @@ limitations under the License.
flex: 1; flex: 1;
} }
.mx_RoomTile.mx_RoomTile.mx_RoomTile_isPrivate .mx_RoomTile_name { .mx_InviteOnlyIcon + .mx_RoomTile_nameContainer .mx_RoomTile_name {
// Scoot the padding in a bit from 6px to make it look better // Scoot the padding in a bit from 6px to make it look better
padding-left: 3px; padding-left: 3px;
} }
.mx_RoomTile.mx_RoomTile_isPrivate .mx_RoomTile_PrivateIcon {
width: 12px;
height: 12px;
position: relative;
display: block !important;
// Align the padlock with unencrypted room names
margin-left: 6px;
&::before {
background-color: $roomtile-name-color;
mask-image: url('$(res)/img/feather-customised/lock-solid.svg');
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
}

View file

@ -6,9 +6,9 @@ scripts/fetchdep.sh matrix-org matrix-js-sdk
pushd matrix-js-sdk pushd matrix-js-sdk
yarn link yarn link
yarn install yarn install $@
yarn build yarn build
popd popd
yarn link matrix-js-sdk yarn link matrix-js-sdk
yarn install yarn install $@

0
scripts/ci/layered-riot-web.sh Normal file → Executable file
View file

92
src/AsyncWrapper.js Normal file
View file

@ -0,0 +1,92 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
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 createReactClass from 'create-react-class';
import * as sdk from './index';
import PropTypes from 'prop-types';
import { _t } from './languageHandler';
/**
* Wrap an asynchronous loader function with a react component which shows a
* spinner until the real component loads.
*/
export default createReactClass({
propTypes: {
/** A promise which resolves with the real component
*/
prom: PropTypes.object.isRequired,
},
getInitialState: function() {
return {
component: null,
error: null,
};
},
componentWillMount: function() {
this._unmounted = false;
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log('Starting load of AsyncWrapper for modal');
this.props.prom.then((result) => {
if (this._unmounted) {
return;
}
// Take the 'default' member if it's there, then we support
// passing in just an import()ed module, since ES6 async import
// always returns a module *namespace*.
const component = result.default ? result.default : result;
this.setState({component});
}).catch((e) => {
console.warn('AsyncWrapper promise failed', e);
this.setState({error: e});
});
},
componentWillUnmount: function() {
this._unmounted = true;
},
_onWrapperCancelClick: function() {
this.props.onFinished(false);
},
render: function() {
if (this.state.component) {
const Component = this.state.component;
return <Component {...this.props} />;
} else if (this.state.error) {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <BaseDialog onFinished={this.props.onFinished}
title={_t("Error")}
>
{_t("Unable to load! Check your network connectivity and try again.")}
<DialogButtons primaryButton={_t("Dismiss")}
onPrimaryButtonClick={this._onWrapperCancelClick}
hasCancel={false}
/>
</BaseDialog>;
} else {
// show a spinner until the component is loaded.
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />;
}
},
});

View file

@ -592,8 +592,11 @@ async function startMatrixClient(startSyncing=true) {
Mjolnir.sharedInstance().start(); Mjolnir.sharedInstance().start();
if (startSyncing) { if (startSyncing) {
await MatrixClientPeg.start(); // The client might want to populate some views with events from the
// index (e.g. the FilePanel), therefore initialize the event index
// before the client.
await EventIndexPeg.init(); await EventIndexPeg.init();
await MatrixClientPeg.start();
} else { } else {
console.warn("Caller requested only auxiliary services be started"); console.warn("Caller requested only auxiliary services be started");
await MatrixClientPeg.assign(); await MatrixClientPeg.assign();

View file

@ -217,7 +217,7 @@ class _MatrixClientPeg {
timelineSupport: true, timelineSupport: true,
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false), forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false),
fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'),
verificationMethods: [verificationMethods.SAS], verificationMethods: [verificationMethods.SAS, verificationMethods.QR_CODE_SHOW],
unstableClientRelationAggregation: true, unstableClientRelationAggregation: true,
identityServer: new IdentityAuthClient(), identityServer: new IdentityAuthClient(),
}; };

View file

@ -17,87 +17,14 @@ limitations under the License.
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import Analytics from './Analytics'; import Analytics from './Analytics';
import * as sdk from './index';
import dis from './dispatcher'; import dis from './dispatcher';
import { _t } from './languageHandler'; import {defer} from './utils/promise';
import {defer} from "./utils/promise"; import AsyncWrapper from './AsyncWrapper';
const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
/**
* Wrap an asynchronous loader function with a react component which shows a
* spinner until the real component loads.
*/
const AsyncWrapper = createReactClass({
propTypes: {
/** A promise which resolves with the real component
*/
prom: PropTypes.object.isRequired,
},
getInitialState: function() {
return {
component: null,
error: null,
};
},
componentWillMount: function() {
this._unmounted = false;
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log('Starting load of AsyncWrapper for modal');
this.props.prom.then((result) => {
if (this._unmounted) {
return;
}
// Take the 'default' member if it's there, then we support
// passing in just an import()ed module, since ES6 async import
// always returns a module *namespace*.
const component = result.default ? result.default : result;
this.setState({component});
}).catch((e) => {
console.warn('AsyncWrapper promise failed', e);
this.setState({error: e});
});
},
componentWillUnmount: function() {
this._unmounted = true;
},
_onWrapperCancelClick: function() {
this.props.onFinished(false);
},
render: function() {
if (this.state.component) {
const Component = this.state.component;
return <Component {...this.props} />;
} else if (this.state.error) {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <BaseDialog onFinished={this.props.onFinished}
title={_t("Error")}
>
{_t("Unable to load! Check your network connectivity and try again.")}
<DialogButtons primaryButton={_t("Dismiss")}
onPrimaryButtonClick={this._onWrapperCancelClick}
hasCancel={false}
/>
</BaseDialog>;
} else {
// show a spinner until the component is loaded.
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />;
}
},
});
class ModalManager { class ModalManager {
constructor() { constructor() {
this._counter = 0; this._counter = 0;

View file

@ -20,13 +20,8 @@ import React from 'react';
import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import MultiInviter from './utils/MultiInviter'; import MultiInviter from './utils/MultiInviter';
import Modal from './Modal'; import Modal from './Modal';
import { getAddressType } from './UserAddress';
import createRoom from './createRoom';
import * as sdk from './'; import * as sdk from './';
import dis from './dispatcher';
import DMRoomMap from './utils/DMRoomMap';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import SettingsStore from "./settings/SettingsStore";
import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog"; import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
/** /**
@ -44,64 +39,21 @@ export function inviteMultipleToRoom(roomId, addrs) {
} }
export function showStartChatInviteDialog() { export function showStartChatInviteDialog() {
if (SettingsStore.isFeatureEnabled("feature_ftue_dms")) { // This dialog handles the room creation internally - we don't need to worry about it.
// This new dialog handles the room creation internally - we don't need to worry about it. const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
const InviteDialog = sdk.getComponent("dialogs.InviteDialog"); Modal.createTrackedDialog(
Modal.createTrackedDialog( 'Start DM', '', InviteDialog, {kind: KIND_DM},
'Start DM', '', InviteDialog, {kind: KIND_DM}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true, );
);
return;
}
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, {
title: _t('Start a chat'),
description: _t("Who would you like to communicate with?"),
placeholder: (validAddressTypes) => {
// The set of valid address type can be mutated inside the dialog
// when you first have no IS but agree to use one in the dialog.
if (validAddressTypes.includes('email')) {
return _t("Email, name or Matrix ID");
}
return _t("Name or Matrix ID");
},
validAddressTypes: ['mx-user-id', 'email'],
button: _t("Start Chat"),
onFinished: _onStartDmFinished,
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
} }
export function showRoomInviteDialog(roomId) { export function showRoomInviteDialog(roomId) {
if (SettingsStore.isFeatureEnabled("feature_ftue_dms")) { // This dialog handles the room creation internally - we don't need to worry about it.
// This new dialog handles the room creation internally - we don't need to worry about it. const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
const InviteDialog = sdk.getComponent("dialogs.InviteDialog"); Modal.createTrackedDialog(
Modal.createTrackedDialog( 'Invite Users', '', InviteDialog, {kind: KIND_INVITE, roomId},
'Invite Users', '', InviteDialog, {kind: KIND_INVITE, roomId}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true, );
);
return;
}
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
Modal.createTrackedDialog('Chat Invite', '', AddressPickerDialog, {
title: _t('Invite new room members'),
button: _t('Send Invites'),
placeholder: (validAddressTypes) => {
// The set of valid address type can be mutated inside the dialog
// when you first have no IS but agree to use one in the dialog.
if (validAddressTypes.includes('email')) {
return _t("Email, name or Matrix ID");
}
return _t("Name or Matrix ID");
},
validAddressTypes: ['mx-user-id', 'email'],
onFinished: (shouldInvite, addrs) => {
_onRoomInviteFinished(roomId, shouldInvite, addrs);
},
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
} }
/** /**
@ -122,60 +74,6 @@ export function isValid3pidInvite(event) {
return true; return true;
} }
// TODO: Canonical DMs replaces this
function _onStartDmFinished(shouldInvite, addrs) {
if (!shouldInvite) return;
const addrTexts = addrs.map((addr) => addr.address);
if (_isDmChat(addrTexts)) {
const rooms = _getDirectMessageRooms(addrTexts[0]);
if (rooms.length > 0) {
// A Direct Message room already exists for this user, so reuse it
dis.dispatch({
action: 'view_room',
room_id: rooms[0],
should_peek: false,
joining: false,
});
} else {
// Start a new DM chat
createRoom({dmUserId: addrTexts[0]}).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to start chat', '', ErrorDialog, {
title: _t("Failed to start chat"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
}
} else if (addrTexts.length === 1) {
// Start a new DM chat
createRoom({dmUserId: addrTexts[0]}).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to start chat', '', ErrorDialog, {
title: _t("Failed to start chat"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
} else {
// Start multi user chat
let room;
createRoom().then((roomId) => {
room = MatrixClientPeg.get().getRoom(roomId);
return inviteMultipleToRoom(roomId, addrTexts);
}).then((result) => {
return _showAnyInviteErrors(result.states, room, result.inviter);
}).catch((err) => {
console.error(err.stack);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
title: _t("Failed to invite"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
}
}
export function inviteUsersToRoom(roomId, userIds) { export function inviteUsersToRoom(roomId, userIds) {
return inviteMultipleToRoom(roomId, userIds).then((result) => { return inviteMultipleToRoom(roomId, userIds).then((result) => {
const room = MatrixClientPeg.get().getRoom(roomId); const room = MatrixClientPeg.get().getRoom(roomId);
@ -190,24 +88,6 @@ export function inviteUsersToRoom(roomId, userIds) {
}); });
} }
function _onRoomInviteFinished(roomId, shouldInvite, addrs) {
if (!shouldInvite) return;
const addrTexts = addrs.map((addr) => addr.address);
// Invite new users to a room
inviteUsersToRoom(roomId, addrTexts);
}
// TODO: Immutable DMs replaces this
function _isDmChat(addrTexts) {
if (addrTexts.length === 1 && getAddressType(addrTexts[0]) === 'mx-user-id') {
return true;
} else {
return false;
}
}
function _showAnyInviteErrors(addrs, room, inviter) { function _showAnyInviteErrors(addrs, room, inviter) {
// Show user any errors // Show user any errors
const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error'); const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error');
@ -243,15 +123,3 @@ function _showAnyInviteErrors(addrs, room, inviter) {
return addrs; return addrs;
} }
function _getDirectMessageRooms(addr) {
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
const dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
const rooms = dmRooms.filter((dmRoom) => {
const room = MatrixClientPeg.get().getRoom(dmRoom);
if (room) {
return room.getMyMembership() === 'join';
}
});
return rooms;
}

View file

@ -19,9 +19,10 @@ import React from 'react';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Matrix from 'matrix-js-sdk'; import {Filter} from 'matrix-js-sdk';
import * as sdk from '../../index'; import * as sdk from '../../index';
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import EventIndexPeg from "../../indexing/EventIndexPeg";
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
/* /*
@ -29,6 +30,9 @@ import { _t } from '../../languageHandler';
*/ */
const FilePanel = createReactClass({ const FilePanel = createReactClass({
displayName: 'FilePanel', displayName: 'FilePanel',
// This is used to track if a decrypted event was a live event and should be
// added to the timeline.
decryptingEvents: new Set(),
propTypes: { propTypes: {
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
@ -40,42 +44,147 @@ const FilePanel = createReactClass({
}; };
}, },
componentDidMount: function() { onRoomTimeline(ev, room, toStartOfTimeline, removed, data) {
this.updateTimelineSet(this.props.roomId); if (room.roomId !== this.props.roomId) return;
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
if (ev.isBeingDecrypted()) {
this.decryptingEvents.add(ev.getId());
} else {
this.addEncryptedLiveEvent(ev);
}
}, },
updateTimelineSet: function(roomId) { onEventDecrypted(ev, err) {
if (ev.getRoomId() !== this.props.roomId) return;
const eventId = ev.getId();
if (!this.decryptingEvents.delete(eventId)) return;
if (err) return;
this.addEncryptedLiveEvent(ev);
},
addEncryptedLiveEvent(ev, toStartOfTimeline) {
if (!this.state.timelineSet) return;
const timeline = this.state.timelineSet.getLiveTimeline();
if (ev.getType() !== "m.room.message") return;
if (["m.file", "m.image", "m.video", "m.audio"].indexOf(ev.getContent().msgtype) == -1) {
return;
}
if (!this.state.timelineSet.eventIdToTimeline(ev.getId())) {
this.state.timelineSet.addEventToTimeline(ev, timeline, false);
}
},
async componentDidMount() {
const client = MatrixClientPeg.get();
await this.updateTimelineSet(this.props.roomId);
if (!MatrixClientPeg.get().isRoomEncrypted(this.props.roomId)) return;
// The timelineSets filter makes sure that encrypted events that contain
// URLs never get added to the timeline, even if they are live events.
// These methods are here to manually listen for such events and add
// them despite the filter's best efforts.
//
// We do this only for encrypted rooms and if an event index exists,
// this could be made more general in the future or the filter logic
// could be fixed.
if (EventIndexPeg.get() !== null) {
client.on('Room.timeline', this.onRoomTimeline.bind(this));
client.on('Event.decrypted', this.onEventDecrypted.bind(this));
}
},
componentWillUnmount() {
const client = MatrixClientPeg.get();
if (client === null) return;
if (!MatrixClientPeg.get().isRoomEncrypted(this.props.roomId)) return;
if (EventIndexPeg.get() !== null) {
client.removeListener('Room.timeline', this.onRoomTimeline.bind(this));
client.removeListener('Event.decrypted', this.onEventDecrypted.bind(this));
}
},
async fetchFileEventsServer(room) {
const client = MatrixClientPeg.get();
const filter = new Filter(client.credentials.userId);
filter.setDefinition(
{
"room": {
"timeline": {
"contains_url": true,
"types": [
"m.room.message",
],
},
},
},
);
const filterId = await client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter);
filter.filterId = filterId;
const timelineSet = room.getOrCreateFilteredTimelineSet(filter);
return timelineSet;
},
onPaginationRequest(timelineWindow, direction, limit) {
const client = MatrixClientPeg.get();
const eventIndex = EventIndexPeg.get();
const roomId = this.props.roomId;
const room = client.getRoom(roomId);
// We override the pagination request for encrypted rooms so that we ask
// the event index to fulfill the pagination request. Asking the server
// to paginate won't ever work since the server can't correctly filter
// out events containing URLs
if (client.isRoomEncrypted(roomId) && eventIndex !== null) {
return eventIndex.paginateTimelineWindow(room, timelineWindow, direction, limit);
} else {
return timelineWindow.paginate(direction, limit);
}
},
async updateTimelineSet(roomId: string) {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(roomId); const room = client.getRoom(roomId);
const eventIndex = EventIndexPeg.get();
this.noRoom = !room; this.noRoom = !room;
if (room) { if (room) {
const filter = new Matrix.Filter(client.credentials.userId); let timelineSet;
filter.setDefinition(
{
"room": {
"timeline": {
"contains_url": true,
"types": [
"m.room.message",
],
},
},
},
);
// FIXME: we shouldn't be doing this every time we change room - see comment above. try {
client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter).then( timelineSet = await this.fetchFileEventsServer(room);
(filterId)=>{
filter.filterId = filterId; // If this room is encrypted the file panel won't be populated
const timelineSet = room.getOrCreateFilteredTimelineSet(filter); // correctly since the defined filter doesn't support encrypted
this.setState({ timelineSet: timelineSet }); // events and the server can't check if encrypted events contain
}, // URLs.
(error)=>{ //
console.error("Failed to get or create file panel filter", error); // This is where our event index comes into place, we ask the
}, // event index to populate the timelineSet for us. This call
); // will add 10 events to the live timeline of the set. More can
// be requested using pagination.
if (client.isRoomEncrypted(roomId) && eventIndex !== null) {
const timeline = timelineSet.getLiveTimeline();
await eventIndex.populateFileTimeline(timelineSet, timeline, room, 10);
}
this.setState({ timelineSet: timelineSet });
} catch (error) {
console.error("Failed to get or create file panel filter", error);
}
} else { } else {
console.error("Failed to add filtered timelineSet for FilePanel as no room!"); console.error("Failed to add filtered timelineSet for FilePanel as no room!");
} }
@ -111,6 +220,7 @@ const FilePanel = createReactClass({
manageReadMarkers={false} manageReadMarkers={false}
timelineSet={this.state.timelineSet} timelineSet={this.state.timelineSet}
showUrlPreview = {false} showUrlPreview = {false}
onPaginationRequest={this.onPaginationRequest}
tileShape="file_grid" tileShape="file_grid"
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
empty={_t('There are no visible files in this room')} empty={_t('There are no visible files in this room')}

View file

@ -796,6 +796,7 @@ 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 e2eMembers = await room.getEncryptionTargetMembers();
const verified = []; const verified = [];
@ -812,10 +813,10 @@ export default createReactClass({
/* Check all verified user devices. */ /* Check all verified user devices. */
for (const userId of verified) { for (const userId of verified) {
const devices = await cli.getStoredDevicesForUser(userId); const devices = await cli.getStoredDevicesForUser(userId);
const allDevicesVerified = devices.every(({deviceId}) => { const anyDeviceNotVerified = devices.some(({deviceId}) => {
return cli.checkDeviceTrust(userId, deviceId).isVerified(); return !cli.checkDeviceTrust(userId, deviceId).isVerified();
}); });
if (!allDevicesVerified) { if (anyDeviceNotVerified) {
this.setState({ this.setState({
e2eStatus: "warning", e2eStatus: "warning",
}); });

View file

@ -94,6 +94,10 @@ const TimelinePanel = createReactClass({
// callback which is called when the read-up-to mark is updated. // callback which is called when the read-up-to mark is updated.
onReadMarkerUpdated: PropTypes.func, onReadMarkerUpdated: PropTypes.func,
// callback which is called when we wish to paginate the timeline
// window.
onPaginationRequest: PropTypes.func,
// maximum number of events to show in a timeline // maximum number of events to show in a timeline
timelineCap: PropTypes.number, timelineCap: PropTypes.number,
@ -338,6 +342,14 @@ const TimelinePanel = createReactClass({
} }
}, },
onPaginationRequest(timelineWindow, direction, size) {
if (this.props.onPaginationRequest) {
return this.props.onPaginationRequest(timelineWindow, direction, size);
} else {
return timelineWindow.paginate(direction, size);
}
},
// set off a pagination request. // set off a pagination request.
onMessageListFillRequest: function(backwards) { onMessageListFillRequest: function(backwards) {
if (!this._shouldPaginate()) return Promise.resolve(false); if (!this._shouldPaginate()) return Promise.resolve(false);
@ -360,7 +372,7 @@ const TimelinePanel = createReactClass({
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards); debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
this.setState({[paginatingKey]: true}); this.setState({[paginatingKey]: true});
return this._timelineWindow.paginate(dir, PAGINATE_SIZE).then((r) => { return this.onPaginationRequest(this._timelineWindow, dir, PAGINATE_SIZE).then((r) => {
if (this.unmounted) { return; } if (this.unmounted) { return; }
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r); debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);

View file

@ -338,19 +338,31 @@ export default class InviteDialog extends React.PureComponent {
const recents = []; const recents = [];
for (const userId in rooms) { for (const userId in rooms) {
// Filter out user IDs that are already in the room / should be excluded // Filter out user IDs that are already in the room / should be excluded
if (excludedTargetIds.includes(userId)) continue; if (excludedTargetIds.includes(userId)) {
console.warn(`[Invite:Recents] Excluding ${userId} from recents`);
continue;
}
const room = rooms[userId]; const room = rooms[userId];
const member = room.getMember(userId); const member = room.getMember(userId);
if (!member) continue; // just skip people who don't have memberships for some reason if (!member) {
// just skip people who don't have memberships for some reason
console.warn(`[Invite:Recents] ${userId} is missing a member object in their own DM (${room.roomId})`);
continue;
}
const lastEventTs = room.timeline && room.timeline.length const lastEventTs = room.timeline && room.timeline.length
? room.timeline[room.timeline.length - 1].getTs() ? room.timeline[room.timeline.length - 1].getTs()
: 0; : 0;
if (!lastEventTs) continue; // something weird is going on with this room if (!lastEventTs) {
// something weird is going on with this room
console.warn(`[Invite:Recents] ${userId} (${room.roomId}) has a weird last timestamp: ${lastEventTs}`);
continue;
}
recents.push({userId, user: member, lastActive: lastEventTs}); recents.push({userId, user: member, lastActive: lastEventTs});
} }
if (!recents) console.warn("[Invite:Recents] No recents to suggest!");
// Sort the recents by last active to save us time later // Sort the recents by last active to save us time later
recents.sort((a, b) => b.lastActive - a.lastActive); recents.sort((a, b) => b.lastActive - a.lastActive);
@ -713,11 +725,16 @@ export default class InviteDialog extends React.PureComponent {
}; };
_toggleMember = (member: Member) => { _toggleMember = (member: Member) => {
let filterText = this.state.filterText;
const targets = this.state.targets.map(t => t); // cheap clone for mutation const targets = this.state.targets.map(t => t); // cheap clone for mutation
const idx = targets.indexOf(member); const idx = targets.indexOf(member);
if (idx >= 0) targets.splice(idx, 1); if (idx >= 0) {
else targets.push(member); targets.splice(idx, 1);
this.setState({targets}); } else {
targets.push(member);
filterText = ""; // clear the filter when the user accepts a suggestion
}
this.setState({targets, filterText});
}; };
_removeMember = (member: Member) => { _removeMember = (member: Member) => {
@ -917,7 +934,7 @@ export default class InviteDialog extends React.PureComponent {
key={"input"} key={"input"}
rows={1} rows={1}
onChange={this._updateFilter} onChange={this._updateFilter}
defaultValue={this.state.filterText} value={this.state.filterText}
ref={this._editorRef} ref={this._editorRef}
onPaste={this._onPaste} onPaste={this._onPaste}
/> />
@ -985,7 +1002,7 @@ export default class InviteDialog extends React.PureComponent {
title = _t("Direct Messages"); title = _t("Direct Messages");
helpText = _t( helpText = _t(
"If you can't find someone, ask them for their username, or share your " + "If you can't find someone, ask them for their username, share your " +
"username (%(userId)s) or <a>profile link</a>.", "username (%(userId)s) or <a>profile link</a>.",
{userId}, {userId},
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>}, {a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>},

View file

@ -0,0 +1,56 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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 {replaceableComponent} from "../../../../utils/replaceableComponent";
import * as qs from "qs";
import QRCode from "qrcode-react";
@replaceableComponent("views.elements.crypto.VerificationQRCode")
export default class VerificationQRCode extends React.PureComponent {
static propTypes = {
// Common for all kinds of QR codes
keys: PropTypes.array.isRequired, // array of [Key ID, Base64 Key] pairs
action: PropTypes.string.isRequired,
keyholderUserId: PropTypes.string.isRequired,
// User verification use case only
secret: PropTypes.string,
otherUserKey: PropTypes.string, // Base64 key being verified
requestEventId: PropTypes.string,
};
static defaultProps = {
action: "verify",
};
render() {
const query = {
request: this.props.requestEventId,
action: this.props.action,
other_user_key: this.props.otherUserKey,
secret: this.props.secret,
};
for (const key of this.props.keys) {
query[`key_${key[0]}`] = key[1];
}
const uri = `https://matrix.to/#/${this.props.keyholderUserId}?${qs.stringify(query)}`;
return <QRCode value={uri} size={256} logoWidth={48} logo={require("../../../../../res/img/matrix-m.svg")} />;
}
}

View file

@ -93,7 +93,7 @@ export default class MKeyVerificationConclusion extends React.Component {
} }
if (title) { if (title) {
const subtitle = userLabelForEventRoom(request.otherUserId, mxEvent); const subtitle = userLabelForEventRoom(request.otherUserId, mxEvent.getRoomId());
const classes = classNames("mx_EventTile_bubble", "mx_KeyVerification", "mx_KeyVerification_icon", { const classes = classNames("mx_EventTile_bubble", "mx_KeyVerification", "mx_KeyVerification_icon", {
mx_KeyVerification_icon_verified: request.done, mx_KeyVerification_icon_verified: request.done,
}); });

View file

@ -86,7 +86,7 @@ export default class MKeyVerificationRequest extends React.Component {
if (userId === myUserId) { if (userId === myUserId) {
return _t("You accepted"); return _t("You accepted");
} else { } else {
return _t("%(name)s accepted", {name: getNameForEventRoom(userId, this.props.mxEvent)}); return _t("%(name)s accepted", {name: getNameForEventRoom(userId, this.props.mxEvent.getRoomId())});
} }
} }
@ -96,7 +96,7 @@ export default class MKeyVerificationRequest extends React.Component {
if (userId === myUserId) { if (userId === myUserId) {
return _t("You cancelled"); return _t("You cancelled");
} else { } else {
return _t("%(name)s cancelled", {name: getNameForEventRoom(userId, this.props.mxEvent)}); return _t("%(name)s cancelled", {name: getNameForEventRoom(userId, this.props.mxEvent.getRoomId())});
} }
} }
@ -129,10 +129,11 @@ export default class MKeyVerificationRequest extends React.Component {
} }
if (!request.initiatedByMe) { if (!request.initiatedByMe) {
const name = getNameForEventRoom(request.requestingUserId, mxEvent.getRoomId());
title = (<div className="mx_KeyVerification_title">{ title = (<div className="mx_KeyVerification_title">{
_t("%(name)s wants to verify", {name: getNameForEventRoom(request.requestingUserId, mxEvent)})}</div>); _t("%(name)s wants to verify", {name})}</div>);
subtitle = (<div className="mx_KeyVerification_subtitle">{ subtitle = (<div className="mx_KeyVerification_subtitle">{
userLabelForEventRoom(request.requestingUserId, mxEvent)}</div>); userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId())}</div>);
if (request.requested && !request.observeOnly) { if (request.requested && !request.observeOnly) {
stateNode = (<div className="mx_KeyVerification_buttons"> stateNode = (<div className="mx_KeyVerification_buttons">
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} /> <FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
@ -143,7 +144,7 @@ export default class MKeyVerificationRequest extends React.Component {
title = (<div className="mx_KeyVerification_title">{ title = (<div className="mx_KeyVerification_title">{
_t("You sent a verification request")}</div>); _t("You sent a verification request")}</div>);
subtitle = (<div className="mx_KeyVerification_subtitle">{ subtitle = (<div className="mx_KeyVerification_subtitle">{
userLabelForEventRoom(request.receivingUserId, mxEvent)}</div>); userLabelForEventRoom(request.receivingUserId, mxEvent.getRoomId())}</div>);
} }
if (title) { if (title) {

View file

@ -18,6 +18,9 @@ import React from 'react';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import E2EIcon from "../rooms/E2EIcon"; import E2EIcon from "../rooms/E2EIcon";

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,76 +15,102 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {useState} from "react";
import PropTypes from "prop-types";
import classNames from 'classnames'; import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import SettingsStore from '../../../settings/SettingsStore';
export default function(props) { import {_t, _td} from '../../../languageHandler';
const { isUser } = props; import {useFeatureEnabled} from "../../../hooks/useSettings";
const isNormal = props.status === "normal"; import AccessibleButton from "../elements/AccessibleButton";
const isWarning = props.status === "warning"; import Tooltip from "../elements/Tooltip";
const isVerified = props.status === "verified";
const e2eIconClasses = classNames({ export const E2E_STATE = {
VERIFIED: "verified",
WARNING: "warning",
UNKNOWN: "unknown",
NORMAL: "normal",
};
const crossSigningUserTitles = {
[E2E_STATE.WARNING]: _td("This user has not verified all of their devices."),
[E2E_STATE.NORMAL]: _td("You have not verified this user. This user has verified all of their devices."),
[E2E_STATE.VERIFIED]: _td("You have verified this user. This user has verified all of their devices."),
};
const crossSigningRoomTitles = {
[E2E_STATE.WARNING]: _td("Someone is using an unknown device"),
[E2E_STATE.NORMAL]: _td("This room is end-to-end encrypted"),
[E2E_STATE.VERIFIED]: _td("Everyone in this room is verified"),
};
const legacyUserTitles = {
[E2E_STATE.WARNING]: _td("Some devices for this user are not trusted"),
[E2E_STATE.VERIFIED]: _td("All devices for this user are trusted"),
};
const legacyRoomTitles = {
[E2E_STATE.WARNING]: _td("Some devices in this encrypted room are not trusted"),
[E2E_STATE.VERIFIED]: _td("All devices in this encrypted room are trusted"),
};
const E2EIcon = ({isUser, status, className, size, onClick}) => {
const [hover, setHover] = useState(false);
const classes = classNames({
mx_E2EIcon: true, mx_E2EIcon: true,
mx_E2EIcon_warning: isWarning, mx_E2EIcon_warning: status === E2E_STATE.WARNING,
mx_E2EIcon_normal: isNormal, mx_E2EIcon_normal: status === E2E_STATE.NORMAL,
mx_E2EIcon_verified: isVerified, mx_E2EIcon_verified: status === E2E_STATE.VERIFIED,
}, props.className); }, className);
let e2eTitle; let e2eTitle;
const crossSigning = useFeatureEnabled("feature_cross_signing");
const crossSigning = SettingsStore.isFeatureEnabled("feature_cross_signing");
if (crossSigning && isUser) { if (crossSigning && isUser) {
if (isWarning) { e2eTitle = crossSigningUserTitles[status];
e2eTitle = _t(
"This user has not verified all of their devices.",
);
} else if (isNormal) {
e2eTitle = _t(
"You have not verified this user. " +
"This user has verified all of their devices.",
);
} else if (isVerified) {
e2eTitle = _t(
"You have verified this user. " +
"This user has verified all of their devices.",
);
}
} else if (crossSigning && !isUser) { } else if (crossSigning && !isUser) {
if (isWarning) { e2eTitle = crossSigningRoomTitles[status];
e2eTitle = _t(
"Some users in this encrypted room are not verified by you or " +
"they have not verified their own devices.",
);
} else if (isVerified) {
e2eTitle = _t(
"All users in this encrypted room are verified by you and " +
"they have verified their own devices.",
);
}
} else if (!crossSigning && isUser) { } else if (!crossSigning && isUser) {
if (isWarning) { e2eTitle = legacyUserTitles[status];
e2eTitle = _t("Some devices for this user are not trusted");
} else if (isVerified) {
e2eTitle = _t("All devices for this user are trusted");
}
} else if (!crossSigning && !isUser) { } else if (!crossSigning && !isUser) {
if (isWarning) { e2eTitle = legacyRoomTitles[status];
e2eTitle = _t("Some devices in this encrypted room are not trusted");
} else if (isVerified) {
e2eTitle = _t("All devices in this encrypted room are trusted");
}
} }
let style = null; let style;
if (props.size) { if (size) {
style = {width: `${props.size}px`, height: `${props.size}px`}; style = {width: `${size}px`, height: `${size}px`};
} }
const icon = (<div className={e2eIconClasses} style={style} title={e2eTitle} />); const onMouseOver = () => setHover(true);
if (props.onClick) { const onMouseOut = () => setHover(false);
return (<AccessibleButton onClick={props.onClick}>{ icon }</AccessibleButton>);
} else { let tip;
return icon; if (hover) {
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} />;
} }
}
if (onClick) {
return (
<AccessibleButton
onClick={onClick}
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
className={classes}
style={style}
>
{ tip }
</AccessibleButton>
);
}
return <div onMouseOver={onMouseOver} onMouseOut={onMouseOut} className={classes} style={style}>
{ tip }
</div>;
};
E2EIcon.propTypes = {
isUser: PropTypes.bool,
status: PropTypes.oneOf(Object.values(E2E_STATE)),
className: PropTypes.string,
size: PropTypes.number,
onClick: PropTypes.func,
};
export default E2EIcon;

View file

@ -33,6 +33,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList"; import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
import * as ObjectUtils from "../../../ObjectUtils"; import * as ObjectUtils from "../../../ObjectUtils";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {E2E_STATE} from "./E2EIcon";
const eventTileTypes = { const eventTileTypes = {
'm.room.message': 'messages.MessageEvent', 'm.room.message': 'messages.MessageEvent',
@ -66,13 +67,6 @@ const stateEventTileTypes = {
'm.room.related_groups': 'messages.TextualEvent', 'm.room.related_groups': 'messages.TextualEvent',
}; };
const E2E_STATE = {
VERIFIED: "verified",
WARNING: "warning",
UNKNOWN: "unknown",
NORMAL: "normal",
};
// Add all the Mjolnir stuff to the renderer // Add all the Mjolnir stuff to the renderer
for (const evType of ALL_RULE_TYPES) { for (const evType of ALL_RULE_TYPES) {
stateEventTileTypes[evType] = 'messages.TextualEvent'; stateEventTileTypes[evType] = 'messages.TextualEvent';
@ -578,6 +572,7 @@ export default createReactClass({
console.error("EventTile attempted to get relations for an event without an ID"); console.error("EventTile attempted to get relations for an event without an ID");
// Use event's special `toJSON` method to log key data. // Use event's special `toJSON` method to log key data.
console.log(JSON.stringify(this.props.mxEvent, null, 4)); console.log(JSON.stringify(this.props.mxEvent, null, 4));
console.trace("Stacktrace for https://github.com/vector-im/riot-web/issues/11120");
} }
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction"); return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
}, },

View file

@ -0,0 +1,51 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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 { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
export default class InviteOnlyIcon extends React.Component {
constructor() {
super();
this.state = {
hover: false,
};
}
onHoverStart = () => {
this.setState({hover: true});
};
onHoverEnd = () => {
this.setState({hover: false});
};
render() {
const Tooltip = sdk.getComponent("elements.Tooltip");
let tooltip;
if (this.state.hover) {
tooltip = <Tooltip className="mx_InviteOnlyIcon_tooltip" label={_t("Invite only")} dir="auto" />;
}
return (<div className="mx_InviteOnlyIcon"
onMouseEnter={this.onHoverStart}
onMouseLeave={this.onHoverEnd}
>
{ tooltip }
</div>);
}
}

View file

@ -26,6 +26,7 @@ import Stickerpicker from './Stickerpicker';
import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks'; import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
import ContentMessages from '../../../ContentMessages'; import ContentMessages from '../../../ContentMessages';
import E2EIcon from './E2EIcon'; import E2EIcon from './E2EIcon';
import SettingsStore from "../../../settings/SettingsStore";
function ComposerAvatar(props) { function ComposerAvatar(props) {
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
@ -168,7 +169,6 @@ export default class MessageComposer extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.onInputStateChanged = this.onInputStateChanged.bind(this); this.onInputStateChanged = this.onInputStateChanged.bind(this);
this.onEvent = this.onEvent.bind(this);
this._onRoomStateEvents = this._onRoomStateEvents.bind(this); this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this); this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
this._onTombstoneClick = this._onTombstoneClick.bind(this); this._onTombstoneClick = this._onTombstoneClick.bind(this);
@ -182,11 +182,6 @@ export default class MessageComposer extends React.Component {
} }
componentDidMount() { componentDidMount() {
// N.B. using 'event' rather than 'RoomEvents' otherwise the crypto handler
// for 'event' fires *after* 'RoomEvent', and our room won't have yet been
// marked as encrypted.
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
MatrixClientPeg.get().on("event", this.onEvent);
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents); MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._waitForOwnMember(); this._waitForOwnMember();
@ -210,7 +205,6 @@ export default class MessageComposer extends React.Component {
componentWillUnmount() { componentWillUnmount() {
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("event", this.onEvent);
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents); MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
} }
if (this._roomStoreToken) { if (this._roomStoreToken) {
@ -218,13 +212,6 @@ export default class MessageComposer extends React.Component {
} }
} }
onEvent(event) {
if (event.getType() !== 'm.room.encryption') return;
if (event.getRoomId() !== this.props.room.roomId) return;
// TODO: put (encryption state??) in state
this.forceUpdate();
}
_onRoomStateEvents(ev, state) { _onRoomStateEvents(ev, state) {
if (ev.getRoomId() !== this.props.room.roomId) return; if (ev.getRoomId() !== this.props.room.roomId) return;
@ -282,18 +269,33 @@ export default class MessageComposer extends React.Component {
} }
renderPlaceholderText() { renderPlaceholderText() {
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (this.state.isQuoting) { if (this.state.isQuoting) {
if (roomIsEncrypted) { if (this.props.e2eStatus) {
return _t('Send an encrypted reply…'); return _t('Send an encrypted reply…');
} else {
return _t('Send a reply…');
}
} else { } else {
return _t('Send a reply (unencrypted)…'); if (this.props.e2eStatus) {
return _t('Send an encrypted message…');
} else {
return _t('Send a message…');
}
} }
} else { } else {
if (roomIsEncrypted) { if (this.state.isQuoting) {
return _t('Send an encrypted message…'); if (this.props.e2eStatus) {
return _t('Send an encrypted reply…');
} else {
return _t('Send a reply (unencrypted)…');
}
} else { } else {
return _t('Send a message (unencrypted)…'); if (this.props.e2eStatus) {
return _t('Send an encrypted message…');
} else {
return _t('Send a message (unencrypted)…');
}
} }
} }
} }

View file

@ -32,6 +32,7 @@ import {CancelButton} from './SimpleRoomHeader';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
import E2EIcon from './E2EIcon'; import E2EIcon from './E2EIcon';
import InviteOnlyIcon from './InviteOnlyIcon';
export default createReactClass({ export default createReactClass({
displayName: 'RoomHeader', displayName: 'RoomHeader',
@ -162,11 +163,12 @@ export default createReactClass({
const joinRules = this.props.room && this.props.room.currentState.getStateEvents("m.room.join_rules", ""); const joinRules = this.props.room && this.props.room.currentState.getStateEvents("m.room.join_rules", "");
const joinRule = joinRules && joinRules.getContent().join_rule; const joinRule = joinRules && joinRules.getContent().join_rule;
const joinRuleClass = classNames("mx_RoomHeader_PrivateIcon", let privateIcon;
{"mx_RoomHeader_isPrivate": joinRule === "invite"}); if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
const privateIcon = SettingsStore.isFeatureEnabled("feature_cross_signing") ? if (joinRule == "invite") {
<div className={joinRuleClass} /> : privateIcon = <InviteOnlyIcon />;
undefined; }
}
if (this.props.onCancelClick) { if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />; cancelButton = <CancelButton onClick={this.props.onCancelClick} />;

View file

@ -719,7 +719,7 @@ export default createReactClass({
}, },
{ {
list: this.state.lists['im.vector.fake.direct'], list: this.state.lists['im.vector.fake.direct'],
label: _t('People'), label: _t('Direct Messages'),
tagName: "im.vector.fake.direct", tagName: "im.vector.fake.direct",
order: "recent", order: "recent",
incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'), incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'),

View file

@ -33,6 +33,10 @@ import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex"; import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
import E2EIcon from './E2EIcon';
import InviteOnlyIcon from './InviteOnlyIcon';
// eslint-disable-next-line camelcase
import rate_limited_func from '../../../ratelimitedfunc';
export default createReactClass({ export default createReactClass({
displayName: 'RoomTile', displayName: 'RoomTile',
@ -70,6 +74,7 @@ export default createReactClass({
notificationCount: this.props.room.getUnreadNotificationCount(), notificationCount: this.props.room.getUnreadNotificationCount(),
selected: this.props.room.roomId === RoomViewStore.getRoomId(), selected: this.props.room.roomId === RoomViewStore.getRoomId(),
statusMessage: this._getStatusMessage(), statusMessage: this._getStatusMessage(),
e2eStatus: null,
}); });
}, },
@ -102,6 +107,83 @@ export default createReactClass({
return statusUser._unstable_statusMessage; return statusUser._unstable_statusMessage;
}, },
onRoomStateMember: function(ev, state, member) {
// we only care about leaving users
// because trust state will change if someone joins a megolm session anyway
if (member.membership !== "leave") {
return;
}
// ignore members in other rooms
if (member.roomId !== this.props.room.roomId) {
return;
}
this._updateE2eStatus();
},
onUserVerificationChanged: function(userId, _trustStatus) {
if (!this.props.room.getMember(userId)) {
// Not in this room
return;
}
this._updateE2eStatus();
},
onRoomTimeline: function(ev, room) {
if (!room) return;
if (room.roomId != this.props.room.roomId) return;
if (ev.getType() !== "m.room.encryption") return;
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
this.onFindingRoomToBeEncrypted();
},
onFindingRoomToBeEncrypted: function() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.members", this.onRoomStateMember);
cli.on("userTrustStatusChanged", this.onUserVerificationChanged);
this._updateE2eStatus();
},
_updateE2eStatus: async function() {
const cli = MatrixClientPeg.get();
if (!cli.isRoomEncrypted(this.props.room.roomId)) {
return;
}
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
return;
}
// Duplication between here and _updateE2eStatus in RoomView
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. */
for (const userId of verified) {
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({
e2eStatus: unverified.length === 0 ? "verified" : "normal",
});
},
onRoomName: function(room) { onRoomName: function(room) {
if (room !== this.props.room) return; if (room !== this.props.room) return;
this.setState({ this.setState({
@ -151,10 +233,19 @@ export default createReactClass({
}, },
componentDidMount: function() { componentDidMount: function() {
/* We bind here rather than in the definition because otherwise we wind up with the
method only being callable once every 500ms across all instances, which would be wrong */
this._updateE2eStatus = rate_limited_func(this._updateE2eStatus, 500);
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
cli.on("accountData", this.onAccountData); cli.on("accountData", this.onAccountData);
cli.on("Room.name", this.onRoomName); cli.on("Room.name", this.onRoomName);
cli.on("RoomState.events", this.onJoinRule); cli.on("RoomState.events", this.onJoinRule);
if (cli.isRoomEncrypted(this.props.room.roomId)) {
this.onFindingRoomToBeEncrypted();
} else {
cli.on("Room.timeline", this.onRoomTimeline);
}
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);
@ -172,6 +263,9 @@ export default createReactClass({
MatrixClientPeg.get().removeListener("accountData", this.onAccountData); MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
cli.removeListener("RoomState.events", this.onJoinRule); cli.removeListener("RoomState.events", this.onJoinRule);
cli.removeListener("RoomState.members", this.onRoomStateMember);
cli.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
cli.removeListener("Room.timeline", this.onRoomTimeline);
} }
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange); ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
@ -318,7 +412,6 @@ export default createReactClass({
'mx_RoomTile_noBadges': !badges, 'mx_RoomTile_noBadges': !badges,
'mx_RoomTile_transparent': this.props.transparent, 'mx_RoomTile_transparent': this.props.transparent,
'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed, 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
'mx_RoomTile_isPrivate': this.state.joinRule == "invite" && !dmUserId,
}); });
const avatarClasses = classNames({ const avatarClasses = classNames({
@ -385,7 +478,8 @@ export default createReactClass({
let dmIndicator; let dmIndicator;
let dmOnline; let dmOnline;
if (dmUserId) { // If we can place a shield, do that instead
if (dmUserId && !this.state.e2eStatus) {
dmIndicator = <img dmIndicator = <img
src={require("../../../../res/img/icon_person.svg")} src={require("../../../../res/img/icon_person.svg")}
className="mx_RoomTile_dm" className="mx_RoomTile_dm"
@ -430,7 +524,14 @@ export default createReactClass({
let privateIcon = null; let privateIcon = null;
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
privateIcon = <div className="mx_RoomTile_PrivateIcon" />; if (this.state.joinRule == "invite" && !dmUserId) {
privateIcon = <InviteOnlyIcon />;
}
}
let e2eIcon = null;
if (this.state.e2eStatus) {
e2eIcon = <E2EIcon status={this.state.e2eStatus} className="mx_RoomTile_e2eIcon" />;
} }
return <React.Fragment> return <React.Fragment>
@ -453,6 +554,7 @@ export default createReactClass({
<div className="mx_RoomTile_avatar_container"> <div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24} /> <RoomAvatar room={this.props.room} width={24} height={24} />
{ dmIndicator } { dmIndicator }
{ e2eIcon }
</div> </div>
</div> </div>
{ privateIcon } { privateIcon }

View file

@ -23,6 +23,7 @@ import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver"; import {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver";
import dis from "../../../dispatcher"; import dis from "../../../dispatcher";
import ToastStore from "../../../stores/ToastStore"; import ToastStore from "../../../stores/ToastStore";
import Modal from "../../../Modal";
export default class VerificationRequestToast extends React.PureComponent { export default class VerificationRequestToast extends React.PureComponent {
constructor(props) { constructor(props) {
@ -65,18 +66,16 @@ export default class VerificationRequestToast extends React.PureComponent {
accept = async () => { accept = async () => {
ToastStore.sharedInstance().dismissToast(this.props.toastKey); ToastStore.sharedInstance().dismissToast(this.props.toastKey);
const {request} = this.props; const {request} = this.props;
const {event} = request;
// no room id for to_device requests // no room id for to_device requests
if (event.getRoomId()) {
dis.dispatch({
action: 'view_room',
room_id: event.getRoomId(),
should_peek: false,
});
}
try { try {
await request.accept(); if (request.channel.roomId) {
const cli = MatrixClientPeg.get(); dis.dispatch({
action: 'view_room',
room_id: request.channel.roomId,
should_peek: false,
});
await request.accept();
const cli = MatrixClientPeg.get();
dis.dispatch({ dis.dispatch({
action: "set_right_panel_phase", action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel, phase: RIGHT_PANEL_PHASES.EncryptionPanel,
@ -84,7 +83,13 @@ export default class VerificationRequestToast extends React.PureComponent {
verificationRequest: request, verificationRequest: request,
member: cli.getUser(request.otherUserId), member: cli.getUser(request.otherUserId),
}, },
}); });} else if (request.channel.deviceId && request.verifier) {
// show to_device verifications in dialog still
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
verifier: request.verifier,
}, null, /* priority = */ false, /* static = */ true);
}
} catch (err) { } catch (err) {
console.error(err.message); console.error(err.message);
} }
@ -93,13 +98,13 @@ export default class VerificationRequestToast extends React.PureComponent {
render() { render() {
const FormButton = sdk.getComponent("elements.FormButton"); const FormButton = sdk.getComponent("elements.FormButton");
const {request} = this.props; const {request} = this.props;
const {event} = request;
const userId = request.otherUserId; const userId = request.otherUserId;
let nameLabel = event.getRoomId() ? userLabelForEventRoom(userId, event) : userId; const roomId = request.channel.roomId;
let nameLabel = roomId ? userLabelForEventRoom(userId, roomId) : userId;
// for legacy to_device verification requests // for legacy to_device verification requests
if (nameLabel === userId) { if (nameLabel === userId) {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const user = client.getUser(event.getSender()); const user = client.getUser(userId);
if (user && user.displayName) { if (user && user.displayName) {
nameLabel = _t("%(name)s (%(userId)s)", {name: user.displayName, userId}); nameLabel = _t("%(name)s (%(userId)s)", {name: user.displayName, userId});
} }

52
src/hooks/useSettings.js Normal file
View file

@ -0,0 +1,52 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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 {useEffect, useState} from "react";
import SettingsStore from '../settings/SettingsStore';
// Hook to fetch the value of a setting and dynamically update when it changes
export const useSettingValue = (settingName, roomId = null, excludeDefault = false) => {
const [value, setValue] = useState(SettingsStore.getValue(settingName, roomId, excludeDefault));
useEffect(() => {
const ref = SettingsStore.watchSetting(settingName, roomId, () => {
setValue(SettingsStore.getValue(settingName, roomId, excludeDefault));
});
// clean-up
return () => {
SettingsStore.unwatchSetting(ref);
};
}, [settingName, roomId, excludeDefault]);
return value;
};
// Hook to fetch whether a feature is enabled and dynamically update when that changes
export const useFeatureEnabled = (featureName, roomId = null) => {
const [enabled, setEnabled] = useState(SettingsStore.isFeatureEnabled(featureName, roomId));
useEffect(() => {
const ref = SettingsStore.watchSetting(featureName, roomId, () => {
setEnabled(SettingsStore.isFeatureEnabled(featureName, roomId));
});
// clean-up
return () => {
SettingsStore.unwatchSetting(ref);
};
}, [featureName, roomId]);
return enabled;
};

View file

@ -21,6 +21,9 @@
"Analytics": "Analytics", "Analytics": "Analytics",
"The information being sent to us to help make Riot.im better includes:": "The information being sent to us to help make Riot.im better includes:", "The information being sent to us to help make Riot.im better includes:": "The information being sent to us to help make Riot.im better includes:",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.", "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.",
"Error": "Error",
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
"Dismiss": "Dismiss",
"Call Failed": "Call Failed", "Call Failed": "Call Failed",
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.", "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.",
"Review Devices": "Review Devices", "Review Devices": "Review Devices",
@ -105,9 +108,6 @@
"This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.", "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.",
"Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.", "Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.",
"Trust": "Trust", "Trust": "Trust",
"Error": "Error",
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
"Dismiss": "Dismiss",
"Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings", "Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings",
"Riot was not given permission to send notifications - please try again": "Riot was not given permission to send notifications - please try again", "Riot was not given permission to send notifications - please try again": "Riot was not given permission to send notifications - please try again",
"Unable to enable Notifications": "Unable to enable Notifications", "Unable to enable Notifications": "Unable to enable Notifications",
@ -121,15 +121,8 @@
"Moderator": "Moderator", "Moderator": "Moderator",
"Admin": "Admin", "Admin": "Admin",
"Custom (%(level)s)": "Custom (%(level)s)", "Custom (%(level)s)": "Custom (%(level)s)",
"Start a chat": "Start a chat",
"Who would you like to communicate with?": "Who would you like to communicate with?",
"Email, name or Matrix ID": "Email, name or Matrix ID",
"Start Chat": "Start Chat",
"Invite new room members": "Invite new room members",
"Send Invites": "Send Invites",
"Failed to start chat": "Failed to start chat",
"Operation failed": "Operation failed",
"Failed to invite": "Failed to invite", "Failed to invite": "Failed to invite",
"Operation failed": "Operation failed",
"Failed to invite users to the room:": "Failed to invite users to the room:", "Failed to invite users to the room:": "Failed to invite users to the room:",
"Failed to invite the following users to the %(roomName)s room:": "Failed to invite the following users to the %(roomName)s room:", "Failed to invite the following users to the %(roomName)s room:": "Failed to invite the following users to the %(roomName)s room:",
"You need to be logged in.": "You need to be logged in.", "You need to be logged in.": "You need to be logged in.",
@ -375,7 +368,6 @@
"Render simple counters in room header": "Render simple counters in room header", "Render simple counters in room header": "Render simple counters in room header",
"Multiple integration managers": "Multiple integration managers", "Multiple integration managers": "Multiple integration managers",
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
"New invite dialog": "New invite dialog",
"Show a presence dot next to DMs in the room list": "Show a presence dot next to DMs in the room list", "Show a presence dot next to DMs in the room list": "Show a presence dot next to DMs in the room list",
"Enable cross-signing to verify per-user instead of per-device (in development)": "Enable cross-signing to verify per-user instead of per-device (in development)", "Enable cross-signing to verify per-user instead of per-device (in development)": "Enable cross-signing to verify per-user instead of per-device (in development)",
"Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)", "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)",
@ -890,8 +882,9 @@
"This user has not verified all of their devices.": "This user has not verified all of their devices.", "This user has not verified all of their devices.": "This user has not verified all of their devices.",
"You have not verified this user. This user has verified all of their devices.": "You have not verified this user. This user has verified all of their devices.", "You have not verified this user. This user has verified all of their devices.": "You have not verified this user. This user has verified all of their devices.",
"You have verified this user. This user has verified all of their devices.": "You have verified this user. This user has verified all of their devices.", "You have verified this user. This user has verified all of their devices.": "You have verified this user. This user has verified all of their devices.",
"Some users in this encrypted room are not verified by you or they have not verified their own devices.": "Some users in this encrypted room are not verified by you or they have not verified their own devices.", "Someone is using an unknown device": "Someone is using an unknown device",
"All users in this encrypted room are verified by you and they have verified their own devices.": "All users in this encrypted room are verified by you and they have verified their own devices.", "This room is end-to-end encrypted": "This room is end-to-end encrypted",
"Everyone in this room is verified": "Everyone in this room is verified",
"Some devices for this user are not trusted": "Some devices for this user are not trusted", "Some devices for this user are not trusted": "Some devices for this user are not trusted",
"All devices for this user are trusted": "All devices for this user are trusted", "All devices for this user are trusted": "All devices for this user are trusted",
"Some devices in this encrypted room are not trusted": "Some devices in this encrypted room are not trusted", "Some devices in this encrypted room are not trusted": "Some devices in this encrypted room are not trusted",
@ -911,6 +904,7 @@
"Unencrypted": "Unencrypted", "Unencrypted": "Unencrypted",
"Encrypted by a deleted device": "Encrypted by a deleted device", "Encrypted by a deleted device": "Encrypted by a deleted device",
"Please select the destination room for this message": "Please select the destination room for this message", "Please select the destination room for this message": "Please select the destination room for this message",
"Invite only": "Invite only",
"Scroll to bottom of page": "Scroll to bottom of page", "Scroll to bottom of page": "Scroll to bottom of page",
"Close preview": "Close preview", "Close preview": "Close preview",
"device id: ": "device id: ", "device id: ": "device id: ",
@ -949,6 +943,7 @@
"Invite": "Invite", "Invite": "Invite",
"Share Link to User": "Share Link to User", "Share Link to User": "Share Link to User",
"User Options": "User Options", "User Options": "User Options",
"Start a chat": "Start a chat",
"Direct chats": "Direct chats", "Direct chats": "Direct chats",
"Remove recent messages": "Remove recent messages", "Remove recent messages": "Remove recent messages",
"Unmute": "Unmute", "Unmute": "Unmute",
@ -967,8 +962,10 @@
"Hangup": "Hangup", "Hangup": "Hangup",
"Upload file": "Upload file", "Upload file": "Upload file",
"Send an encrypted reply…": "Send an encrypted reply…", "Send an encrypted reply…": "Send an encrypted reply…",
"Send a reply (unencrypted)…": "Send a reply (unencrypted)…", "Send a reply…": "Send a reply…",
"Send an encrypted message…": "Send an encrypted message…", "Send an encrypted message…": "Send an encrypted message…",
"Send a message…": "Send a message…",
"Send a reply (unencrypted)…": "Send a reply (unencrypted)…",
"Send a message (unencrypted)…": "Send a message (unencrypted)…", "Send a message (unencrypted)…": "Send a message (unencrypted)…",
"The conversation continues here.": "The conversation continues here.", "The conversation continues here.": "The conversation continues here.",
"This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.", "This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
@ -1015,7 +1012,7 @@
"Community Invites": "Community Invites", "Community Invites": "Community Invites",
"Invites": "Invites", "Invites": "Invites",
"Favourites": "Favourites", "Favourites": "Favourites",
"People": "People", "Direct Messages": "Direct Messages",
"Start chat": "Start chat", "Start chat": "Start chat",
"Rooms": "Rooms", "Rooms": "Rooms",
"Low priority": "Low priority", "Low priority": "Low priority",
@ -1482,8 +1479,7 @@
"Suggestions": "Suggestions", "Suggestions": "Suggestions",
"Recently Direct Messaged": "Recently Direct Messaged", "Recently Direct Messaged": "Recently Direct Messaged",
"Show more": "Show more", "Show more": "Show more",
"Direct Messages": "Direct Messages", "If you can't find someone, ask them for their username, share your username (%(userId)s) or <a>profile link</a>.": "If you can't find someone, ask them for their username, share your username (%(userId)s) or <a>profile link</a>.",
"If you can't find someone, ask them for their username, or share your username (%(userId)s) or <a>profile link</a>.": "If you can't find someone, ask them for their username, or share your username (%(userId)s) or <a>profile link</a>.",
"Go": "Go", "Go": "Go",
"If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.": "If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.", "If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.": "If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.",
"You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.", "You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.",

View file

@ -62,11 +62,18 @@ export interface SearchArgs {
room_id: ?string; room_id: ?string;
} }
export interface HistoricEvent { export interface EventAndProfile {
event: MatrixEvent; event: MatrixEvent;
profile: MatrixProfile; profile: MatrixProfile;
} }
export interface LoadArgs {
roomId: string;
limit: number;
fromEvent: string;
direction: string;
}
/** /**
* Base class for classes that provide platform-specific event indexing. * Base class for classes that provide platform-specific event indexing.
* *
@ -145,7 +152,7 @@ export default class BaseEventIndexManager {
* *
* This is used to add a batch of events to the index. * This is used to add a batch of events to the index.
* *
* @param {[HistoricEvent]} events The list of events and profiles that * @param {[EventAndProfile]} events The list of events and profiles that
* should be added to the event index. * should be added to the event index.
* @param {[CrawlerCheckpoint]} checkpoint A new crawler checkpoint that * @param {[CrawlerCheckpoint]} checkpoint A new crawler checkpoint that
* should be stored in the index which should be used to continue crawling * should be stored in the index which should be used to continue crawling
@ -158,7 +165,7 @@ export default class BaseEventIndexManager {
* were already added to the index, false otherwise. * were already added to the index, false otherwise.
*/ */
async addHistoricEvents( async addHistoricEvents(
events: [HistoricEvent], events: [EventAndProfile],
checkpoint: CrawlerCheckpoint | null, checkpoint: CrawlerCheckpoint | null,
oldCheckpoint: CrawlerCheckpoint | null, oldCheckpoint: CrawlerCheckpoint | null,
): Promise<bool> { ): Promise<bool> {
@ -201,6 +208,26 @@ export default class BaseEventIndexManager {
throw new Error("Unimplemented"); throw new Error("Unimplemented");
} }
/** Load events that contain an mxc URL to a file from the index.
*
* @param {object} args Arguments object for the method.
* @param {string} args.roomId The ID of the room for which the events
* should be loaded.
* @param {number} args.limit The maximum number of events to return.
* @param {string} args.fromEvent An event id of a previous event returned
* by this method. Passing this means that we are going to continue loading
* events from this point in the history.
* @param {string} args.direction The direction to which we should continue
* loading events from. This is used only if fromEvent is used as well.
*
* @return {Promise<[EventAndProfile]>} A promise that will resolve to an
* array of Matrix events that contain mxc URLs accompanied with the
* historic profile of the sender.
*/
async loadFileEvents(args: LoadArgs): Promise<[EventAndProfile]> {
throw new Error("Unimplemented");
}
/** /**
* close our event index. * close our event index.
* *

View file

@ -16,6 +16,7 @@ limitations under the License.
import PlatformPeg from "../PlatformPeg"; import PlatformPeg from "../PlatformPeg";
import {MatrixClientPeg} from "../MatrixClientPeg"; import {MatrixClientPeg} from "../MatrixClientPeg";
import {EventTimeline, RoomMember} from 'matrix-js-sdk';
/* /*
* Event indexing class that wraps the platform specific event indexing. * Event indexing class that wraps the platform specific event indexing.
@ -170,7 +171,9 @@ export default class EventIndex {
return; return;
} }
const e = ev.toJSON().decrypted; const jsonEvent = ev.toJSON();
const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent;
const profile = { const profile = {
displayname: ev.sender.rawDisplayName, displayname: ev.sender.rawDisplayName,
avatar_url: ev.sender.getMxcAvatarUrl(), avatar_url: ev.sender.getMxcAvatarUrl(),
@ -305,10 +308,7 @@ export default class EventIndex {
// consume. // consume.
const events = filteredEvents.map((ev) => { const events = filteredEvents.map((ev) => {
const jsonEvent = ev.toJSON(); const jsonEvent = ev.toJSON();
const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent;
let e;
if (ev.isEncrypted()) e = jsonEvent.decrypted;
else e = jsonEvent;
let profile = {}; let profile = {};
if (e.sender in profiles) profile = profiles[e.sender]; if (e.sender in profiles) profile = profiles[e.sender];
@ -406,4 +406,198 @@ export default class EventIndex {
const indexManager = PlatformPeg.get().getEventIndexingManager(); const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.searchEventIndex(searchArgs); return indexManager.searchEventIndex(searchArgs);
} }
/**
* Load events that contain URLs from the event index.
*
* @param {Room} room The room for which we should fetch events containing
* URLs
*
* @param {number} limit The maximum number of events to fetch.
*
* @param {string} fromEvent From which event should we continue fetching
* events from the index. This is only needed if we're continuing to fill
* the timeline, e.g. if we're paginating. This needs to be set to a event
* id of an event that was previously fetched with this function.
*
* @param {string} direction The direction in which we will continue
* fetching events. EventTimeline.BACKWARDS to continue fetching events that
* are older than the event given in fromEvent, EventTimeline.FORWARDS to
* fetch newer events.
*
* @returns {Promise<MatrixEvent[]>} Resolves to an array of events that
* contain URLs.
*/
async loadFileEvents(room, limit = 10, fromEvent = null, direction = EventTimeline.BACKWARDS) {
const client = MatrixClientPeg.get();
const indexManager = PlatformPeg.get().getEventIndexingManager();
const loadArgs = {
roomId: room.roomId,
limit: limit,
};
if (fromEvent) {
loadArgs.fromEvent = fromEvent;
loadArgs.direction = direction;
}
let events;
// Get our events from the event index.
try {
events = await indexManager.loadFileEvents(loadArgs);
} catch (e) {
console.log("EventIndex: Error getting file events", e);
return [];
}
const eventMapper = client.getEventMapper();
// Turn the events into MatrixEvent objects.
const matrixEvents = events.map(e => {
const matrixEvent = eventMapper(e.event);
const member = new RoomMember(room.roomId, matrixEvent.getSender());
// We can't really reconstruct the whole room state from our
// EventIndex to calculate the correct display name. Use the
// disambiguated form always instead.
member.name = e.profile.displayname + " (" + matrixEvent.getSender() + ")";
// This is sets the avatar URL.
const memberEvent = eventMapper(
{
content: {
membership: "join",
avatar_url: e.profile.avatar_url,
displayname: e.profile.displayname,
},
type: "m.room.member",
event_id: matrixEvent.getId() + ":eventIndex",
room_id: matrixEvent.getRoomId(),
sender: matrixEvent.getSender(),
origin_server_ts: matrixEvent.getTs(),
state_key: matrixEvent.getSender(),
},
);
// We set this manually to avoid emitting RoomMember.membership and
// RoomMember.name events.
member.events.member = memberEvent;
matrixEvent.sender = member;
return matrixEvent;
});
return matrixEvents;
}
/**
* Fill a timeline with events that contain URLs.
*
* @param {TimelineSet} timelineSet The TimelineSet the Timeline belongs to,
* used to check if we're adding duplicate events.
*
* @param {Timeline} timeline The Timeline which should be filed with
* events.
*
* @param {Room} room The room for which we should fetch events containing
* URLs
*
* @param {number} limit The maximum number of events to fetch.
*
* @param {string} fromEvent From which event should we continue fetching
* events from the index. This is only needed if we're continuing to fill
* the timeline, e.g. if we're paginating. This needs to be set to a event
* id of an event that was previously fetched with this function.
*
* @param {string} direction The direction in which we will continue
* fetching events. EventTimeline.BACKWARDS to continue fetching events that
* are older than the event given in fromEvent, EventTimeline.FORWARDS to
* fetch newer events.
*
* @returns {Promise<boolean>} Resolves to true if events were added to the
* timeline, false otherwise.
*/
async populateFileTimeline(timelineSet, timeline, room, limit = 10,
fromEvent = null, direction = EventTimeline.BACKWARDS) {
const matrixEvents = await this.loadFileEvents(room, limit, fromEvent, direction);
// If this is a normal fill request, not a pagination request, we need
// to get our events in the BACKWARDS direction but populate them in the
// forwards direction.
// This needs to happen because a fill request might come with an
// exisitng timeline e.g. if you close and re-open the FilePanel.
if (fromEvent === null) {
matrixEvents.reverse();
direction = direction == EventTimeline.BACKWARDS ? EventTimeline.FORWARDS: EventTimeline.BACKWARDS;
}
// Add the events to the timeline of the file panel.
matrixEvents.forEach(e => {
if (!timelineSet.eventIdToTimeline(e.getId())) {
timelineSet.addEventToTimeline(e, timeline, direction == EventTimeline.BACKWARDS);
}
});
// Set the pagination token to the oldest event that we retrieved.
if (matrixEvents.length > 0) {
timeline.setPaginationToken(matrixEvents[matrixEvents.length - 1].getId(), EventTimeline.BACKWARDS);
return true;
} else {
timeline.setPaginationToken("", EventTimeline.BACKWARDS);
return false;
}
}
/**
* Emulate a TimelineWindow pagination() request with the event index as the event source
*
* Might not fetch events from the index if the timeline already contains
* events that the window isn't showing.
*
* @param {Room} room The room for which we should fetch events containing
* URLs
*
* @param {TimelineWindow} timelineWindow The timeline window that should be
* populated with new events.
*
* @param {string} direction The direction in which we should paginate.
* EventTimeline.BACKWARDS to paginate back, EventTimeline.FORWARDS to
* paginate forwards.
*
* @param {number} limit The maximum number of events to fetch while
* paginating.
*
* @returns {Promise<boolean>} Resolves to a boolean which is true if more
* events were successfully retrieved.
*/
paginateTimelineWindow(room, timelineWindow, direction, limit) {
const tl = timelineWindow.getTimelineIndex(direction);
if (!tl) return Promise.resolve(false);
if (tl.pendingPaginate) return tl.pendingPaginate;
if (timelineWindow.extend(direction, limit)) {
return Promise.resolve(true);
}
const paginationMethod = async (timelineWindow, timeline, room, direction, limit) => {
const timelineSet = timelineWindow._timelineSet;
const token = timeline.timeline.getPaginationToken(direction);
const ret = await this.populateFileTimeline(timelineSet, timeline.timeline, room, limit, token, direction);
timeline.pendingPaginate = null;
timelineWindow.extend(direction, limit);
return ret;
};
const paginationPromise = paginationMethod(timelineWindow, tl, room, direction, limit);
tl.pendingPaginate = paginationPromise;
return paginationPromise;
}
} }

View file

@ -128,12 +128,6 @@ export const SETTINGS = {
supportedLevels: LEVELS_FEATURE, supportedLevels: LEVELS_FEATURE,
default: false, default: false,
}, },
"feature_ftue_dms": {
isFeature: true,
displayName: _td("New invite dialog"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_presence_in_room_list": { "feature_presence_in_room_list": {
isFeature: true, isFeature: true,
displayName: _td("Show a presence dot next to DMs in the room list"), displayName: _td("Show a presence dot next to DMs in the room list"),

View file

@ -17,16 +17,15 @@ limitations under the License.
import {MatrixClientPeg} from '../MatrixClientPeg'; import {MatrixClientPeg} from '../MatrixClientPeg';
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
export function getNameForEventRoom(userId, mxEvent) { export function getNameForEventRoom(userId, roomId) {
const roomId = mxEvent.getRoomId();
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(roomId); const room = client.getRoom(roomId);
const member = room.getMember(userId); const member = room && room.getMember(userId);
return member ? member.name : userId; return member ? member.name : userId;
} }
export function userLabelForEventRoom(userId, mxEvent) { export function userLabelForEventRoom(userId, roomId) {
const name = getNameForEventRoom(userId, mxEvent); const name = getNameForEventRoom(userId, roomId);
if (name !== userId) { if (name !== userId) {
return _t("%(name)s (%(userId)s)", {name, userId}); return _t("%(name)s (%(userId)s)", {name, userId});
} else { } else {

View file

@ -31,10 +31,11 @@ module.exports = async function invite(session, userId) {
} }
const inviteButton = await session.query(".mx_MemberList_invite"); const inviteButton = await session.query(".mx_MemberList_invite");
await inviteButton.click(); await inviteButton.click();
const inviteTextArea = await session.query(".mx_AddressPickerDialog textarea"); const inviteTextArea = await session.query(".mx_InviteDialog_editor textarea");
await inviteTextArea.type(userId); await inviteTextArea.type(userId);
await inviteTextArea.press("Enter"); const selectUserItem = await session.query(".mx_InviteDialog_roomTile");
const confirmButton = await session.query(".mx_Dialog_primary"); await selectUserItem.click();
const confirmButton = await session.query(".mx_InviteDialog_goButton");
await confirmButton.click(); await confirmButton.click();
session.log.done(); session.log.done();
}; };