Merge branch 'develop' into feature/padLockInviteOnly

This commit is contained in:
Travis Ralston 2020-02-24 09:50:06 -07:00
commit d2e7cc7f19
138 changed files with 4634 additions and 1211 deletions

View file

@ -255,7 +255,7 @@ export class ContextMenu extends React.Component {
if (chevronFace === 'top' || chevronFace === 'bottom') {
chevronOffset.left = props.chevronOffset;
} else {
} else if (position.top !== undefined) {
const target = position.top;
// By default, no adjustment is made

View file

@ -95,8 +95,8 @@ const FilePanel = createReactClass({
// 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));
client.on('Room.timeline', this.onRoomTimeline);
client.on('Event.decrypted', this.onEventDecrypted);
}
},
@ -107,8 +107,8 @@ const FilePanel = createReactClass({
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));
client.removeListener('Room.timeline', this.onRoomTimeline);
client.removeListener('Event.decrypted', this.onEventDecrypted);
}
},

View file

@ -821,10 +821,10 @@ export default createReactClass({
{_t(
"Want more than a community? <a>Get your own server</a>", {},
{
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener">{sub}</a>,
a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{sub}</a>,
},
)}
<a href={hostingSignupLink} target="_blank" rel="noopener">
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
<img src={require("../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a>
</div>;

View file

@ -65,6 +65,7 @@ import { ThemeWatcher } from "../../theme";
import { storeRoomAliasInCache } from '../../RoomAliasCache';
import { defer } from "../../utils/promise";
import ToastStore from "../../stores/ToastStore";
import * as StorageManager from "../../utils/StorageManager";
/** constants for MatrixChat.state.view */
export const VIEWS = {
@ -1174,6 +1175,7 @@ export default createReactClass({
* Called when a new logged in session has started
*/
_onLoggedIn: async function() {
ThemeController.isLogin = false;
this.setStateForNewView({ view: VIEWS.LOGGED_IN });
if (MatrixClientPeg.currentUserIsJustRegistered()) {
MatrixClientPeg.setJustRegisteredUserId(null);
@ -1193,6 +1195,8 @@ export default createReactClass({
} else {
this._showScreenAfterLogin();
}
StorageManager.tryPersistStorage();
},
_showScreenAfterLogin: function() {
@ -1371,7 +1375,8 @@ export default createReactClass({
cancelButton: _t('Dismiss'),
onFinished: (confirmed) => {
if (confirmed) {
window.open(consentUri, '_blank');
const wnd = window.open(consentUri, '_blank');
wnd.opener = null;
}
},
}, null, true);

View file

@ -115,6 +115,7 @@ export default class MessagePanel extends React.Component {
// previous positions the read marker has been in, so we can
// display 'ghost' read markers that are animating away
ghostReadMarkers: [],
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
};
// opaque readreceipt info for each userId; used by ReadReceiptMarker
@ -164,6 +165,9 @@ export default class MessagePanel extends React.Component {
this._readMarkerNode = createRef();
this._whoIsTyping = createRef();
this._scrollPanel = createRef();
this._showTypingNotificationsWatcherRef =
SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange);
}
componentDidMount() {
@ -172,6 +176,7 @@ export default class MessagePanel extends React.Component {
componentWillUnmount() {
this._isMounted = false;
SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
}
componentDidUpdate(prevProps, prevState) {
@ -184,6 +189,12 @@ export default class MessagePanel extends React.Component {
}
}
onShowTypingNotificationsChange = () => {
this.setState({
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
});
};
/* get the DOM node representing the given event */
getNodeForEventId(eventId) {
if (!this.eventNodes) {
@ -402,10 +413,6 @@ export default class MessagePanel extends React.Component {
};
_getEventTiles() {
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
this.eventNodes = {};
let i;
@ -447,199 +454,48 @@ export default class MessagePanel extends React.Component {
this._readReceiptsByEvent = this._getReadReceiptsByShownEvent();
}
let grouper = null;
for (i = 0; i < this.props.events.length; i++) {
const mxEv = this.props.events[i];
const eventId = mxEv.getId();
const last = (mxEv === lastShownEvent);
// Wrap initial room creation events into an EventListSummary
// Grouping only events sent by the same user that sent the `m.room.create` and only until
// the first non-state event or membership event which is not regarding the sender of the `m.room.create` event
const shouldGroup = (ev) => {
if (ev.getType() === "m.room.member"
&& (ev.getStateKey() !== mxEv.getSender() || ev.getContent()["membership"] !== "join")) {
return false;
}
if (ev.isState() && ev.getSender() === mxEv.getSender()) {
return true;
}
return false;
};
// events that we include in the group but then eject out and place
// above the group.
const shouldEject = (ev) => {
if (ev.getType() === "m.room.encryption") return true;
return false;
};
if (mxEv.getType() === "m.room.create") {
let summaryReadMarker = null;
const ts1 = mxEv.getTs();
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} /></li>;
ret.push(dateSeparator);
if (grouper) {
if (grouper.shouldGroup(mxEv)) {
grouper.add(mxEv);
continue;
} else {
// not part of group, so get the group tiles, close the
// group, and continue like a normal event
ret.push(...grouper.getTiles());
prevEvent = grouper.getNewPrevEvent();
grouper = null;
}
// If RM event is the first in the summary, append the RM after the summary
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId());
// If this m.room.create event should be shown (room upgrade) then show it before the summary
if (this._shouldShowEvent(mxEv)) {
// pass in the mxEv as prevEvent as well so no extra DateSeparator is rendered
ret.push(...this._getTilesForEvent(mxEv, mxEv, false));
}
const summarisedEvents = []; // Don't add m.room.create here as we don't want it inside the summary
const ejectedEvents = [];
for (;i + 1 < this.props.events.length; i++) {
const collapsedMxEv = this.props.events[i + 1];
// Ignore redacted/hidden member events
if (!this._shouldShowEvent(collapsedMxEv)) {
// If this hidden event is the RM and in or at end of a summary put RM after the summary.
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
continue;
}
if (!shouldGroup(collapsedMxEv) || this._wantsDateSeparator(mxEv, collapsedMxEv.getDate())) {
break;
}
// If RM event is in the summary, mark it as such and the RM will be appended after the summary.
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
if (shouldEject(collapsedMxEv)) {
ejectedEvents.push(collapsedMxEv);
} else {
summarisedEvents.push(collapsedMxEv);
}
}
// At this point, i = the index of the last event in the summary sequence
const eventTiles = summarisedEvents.map((e) => {
// In order to prevent DateSeparators from appearing in the expanded form
// of EventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
return this._getTilesForEvent(e, e, e === lastShownEvent);
}).reduce((a, b) => a.concat(b), []);
for (const ejected of ejectedEvents) {
ret.push(...this._getTilesForEvent(mxEv, ejected, last));
}
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
const ev = this.props.events[i];
ret.push(<EventListSummary
key="roomcreationsummary"
events={summarisedEvents}
onToggle={this._onHeightChanged} // Update scroll state
summaryMembers={[ev.sender]}
summaryText={_t("%(creator)s created and configured the room.", {
creator: ev.sender ? ev.sender.name : ev.getSender(),
})}
>
{ eventTiles }
</EventListSummary>);
if (summaryReadMarker) {
ret.push(summaryReadMarker);
}
prevEvent = mxEv;
continue;
}
const wantTile = this._shouldShowEvent(mxEv);
// Wrap consecutive member events in a ListSummary, ignore if redacted
if (isMembershipChange(mxEv) && wantTile) {
let summaryReadMarker = null;
const ts1 = mxEv.getTs();
// Ensure that the key of the MemberEventListSummary does not change with new
// member events. This will prevent it from being re-created unnecessarily, and
// instead will allow new props to be provided. In turn, the shouldComponentUpdate
// method on MELS can be used to prevent unnecessary renderings.
//
// Whilst back-paginating with a MELS at the top of the panel, prevEvent will be null,
// so use the key "membereventlistsummary-initial". Otherwise, use the ID of the first
// membership event, which will not change during forward pagination.
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} /></li>;
ret.push(dateSeparator);
for (const Grouper of groupers) {
if (Grouper.canStartGroup(this, mxEv)) {
grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent);
}
// If RM event is the first in the MELS, append the RM after MELS
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId());
const summarisedEvents = [mxEv];
for (;i + 1 < this.props.events.length; i++) {
const collapsedMxEv = this.props.events[i + 1];
// Ignore redacted/hidden member events
if (!this._shouldShowEvent(collapsedMxEv)) {
// If this hidden event is the RM and in or at end of a MELS put RM after MELS.
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
continue;
}
if (!isMembershipChange(collapsedMxEv) ||
this._wantsDateSeparator(mxEv, collapsedMxEv.getDate())) {
break;
}
// If RM event is in MELS mark it as such and the RM will be appended after MELS.
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
summarisedEvents.push(collapsedMxEv);
}
let highlightInMels = false;
// At this point, i = the index of the last event in the summary sequence
let eventTiles = summarisedEvents.map((e) => {
if (e.getId() === this.props.highlightedEventId) {
highlightInMels = true;
}
// In order to prevent DateSeparators from appearing in the expanded form
// of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
return this._getTilesForEvent(e, e, e === lastShownEvent);
}).reduce((a, b) => a.concat(b), []);
if (eventTiles.length === 0) {
eventTiles = null;
}
ret.push(<MemberEventListSummary key={key}
events={summarisedEvents}
onToggle={this._onHeightChanged} // Update scroll state
startExpanded={highlightInMels}
>
{ eventTiles }
</MemberEventListSummary>);
if (summaryReadMarker) {
ret.push(summaryReadMarker);
}
prevEvent = mxEv;
continue;
}
if (!grouper) {
const wantTile = this._shouldShowEvent(mxEv);
if (wantTile) {
// make sure we unpack the array returned by _getTilesForEvent,
// otherwise react will auto-generate keys and we will end up
// replacing all of the DOM elements every time we paginate.
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last));
prevEvent = mxEv;
}
if (wantTile) {
// make sure we unpack the array returned by _getTilesForEvent,
// otherwise react will auto-generate keys and we will end up
// replacing all of the DOM elements every time we paginate.
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last));
prevEvent = mxEv;
const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
if (readMarker) ret.push(readMarker);
}
}
const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
if (readMarker) ret.push(readMarker);
if (grouper) {
ret.push(...grouper.getTiles());
}
return ret;
@ -921,7 +777,7 @@ export default class MessagePanel extends React.Component {
);
let whoIsTyping;
if (this.props.room && !this.props.tileShape) {
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
whoIsTyping = (<WhoIsTypingTile
room={this.props.room}
onShown={this._onTypingShown}
@ -950,3 +806,222 @@ export default class MessagePanel extends React.Component {
);
}
}
/* Grouper classes determine when events can be grouped together in a summary.
* Groupers should have the following methods:
* - canStartGroup (static): determines if a new group should be started with the
* given event
* - shouldGroup: determines if the given event should be added to an existing group
* - add: adds an event to an existing group (should only be called if shouldGroup
* return true)
* - getTiles: returns the tiles that represent the group
* - getNewPrevEvent: returns the event that should be used as the new prevEvent
* when determining things such as whether a date separator is necessary
*/
// Wrap initial room creation events into an EventListSummary
// Grouping only events sent by the same user that sent the `m.room.create` and only until
// the first non-state event or membership event which is not regarding the sender of the `m.room.create` event
class CreationGrouper {
static canStartGroup = function(panel, ev) {
return ev.getType() === "m.room.create";
};
constructor(panel, createEvent, prevEvent, lastShownEvent) {
this.panel = panel;
this.createEvent = createEvent;
this.prevEvent = prevEvent;
this.lastShownEvent = lastShownEvent;
this.events = [];
// events that we include in the group but then eject out and place
// above the group.
this.ejectedEvents = [];
this.readMarker = panel._readMarkerForEvent(createEvent.getId());
}
shouldGroup(ev) {
const panel = this.panel;
const createEvent = this.createEvent;
if (!panel._shouldShowEvent(ev)) {
this.readMarker = this.readMarker || panel._readMarkerForEvent(ev.getId());
return true;
}
if (panel._wantsDateSeparator(this.createEvent, ev.getDate())) {
return false;
}
if (ev.getType() === "m.room.member"
&& (ev.getStateKey() !== createEvent.getSender() || ev.getContent()["membership"] !== "join")) {
return false;
}
if (ev.isState() && ev.getSender() === createEvent.getSender()) {
return true;
}
return false;
}
add(ev) {
const panel = this.panel;
this.readMarker = this.readMarker || panel._readMarkerForEvent(ev.getId());
if (!panel._shouldShowEvent(ev)) {
return;
}
if (ev.getType() === "m.room.encryption") {
this.ejectedEvents.push(ev);
} else {
this.events.push(ev);
}
}
getTiles() {
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
const panel = this.panel;
const ret = [];
const createEvent = this.createEvent;
const lastShownEvent = this.lastShownEvent;
if (panel._wantsDateSeparator(this.prevEvent, createEvent.getDate())) {
const ts = createEvent.getTs();
ret.push(
<li key={ts+'~'}><DateSeparator key={ts+'~'} ts={ts} /></li>,
);
}
// If this m.room.create event should be shown (room upgrade) then show it before the summary
if (panel._shouldShowEvent(createEvent)) {
// pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered
ret.push(...panel._getTilesForEvent(createEvent, createEvent, false));
}
for (const ejected of this.ejectedEvents) {
ret.push(...panel._getTilesForEvent(
createEvent, ejected, createEvent === lastShownEvent,
));
}
const eventTiles = this.events.map((e) => {
// In order to prevent DateSeparators from appearing in the expanded form
// of EventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
return panel._getTilesForEvent(e, e, e === lastShownEvent);
}).reduce((a, b) => a.concat(b), []);
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
const ev = this.events[this.events.length - 1];
ret.push(
<EventListSummary
key="roomcreationsummary"
events={this.events}
onToggle={panel._onHeightChanged} // Update scroll state
summaryMembers={[ev.sender]}
summaryText={_t("%(creator)s created and configured the room.", {
creator: ev.sender ? ev.sender.name : ev.getSender(),
})}
>
{ eventTiles }
</EventListSummary>,
);
if (this.readMarker) {
ret.push(this.readMarker);
}
return ret;
}
getNewPrevEvent() {
return this.createEvent;
}
}
// Wrap consecutive member events in a ListSummary, ignore if redacted
class MemberGrouper {
static canStartGroup = function(panel, ev) {
return panel._shouldShowEvent(ev) && isMembershipChange(ev);
}
constructor(panel, ev, prevEvent, lastShownEvent) {
this.panel = panel;
this.readMarker = panel._readMarkerForEvent(ev.getId());
this.events = [ev];
this.prevEvent = prevEvent;
this.lastShownEvent = lastShownEvent;
}
shouldGroup(ev) {
return isMembershipChange(ev);
}
add(ev) {
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(ev.getId());
this.events.push(ev);
}
getTiles() {
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
const panel = this.panel;
const lastShownEvent = this.lastShownEvent;
const ret = [];
if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
const ts = this.events[0].getTs();
ret.push(
<li key={ts+'~'}><DateSeparator key={ts+'~'} ts={ts} /></li>,
);
}
// Ensure that the key of the MemberEventListSummary does not change with new
// member events. This will prevent it from being re-created unnecessarily, and
// instead will allow new props to be provided. In turn, the shouldComponentUpdate
// method on MELS can be used to prevent unnecessary renderings.
//
// Whilst back-paginating with a MELS at the top of the panel, prevEvent will be null,
// so use the key "membereventlistsummary-initial". Otherwise, use the ID of the first
// membership event, which will not change during forward pagination.
const key = "membereventlistsummary-" + (
this.prevEvent ? this.events[0].getId() : "initial"
);
let highlightInMels;
let eventTiles = this.events.map((e) => {
if (e.getId() === panel.props.highlightedEventId) {
highlightInMels = true;
}
// In order to prevent DateSeparators from appearing in the expanded form
// of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
return panel._getTilesForEvent(e, e, e === lastShownEvent);
}).reduce((a, b) => a.concat(b), []);
if (eventTiles.length === 0) {
eventTiles = null;
}
ret.push(
<MemberEventListSummary key={key}
events={this.events}
onToggle={panel._onHeightChanged} // Update scroll state
startExpanded={highlightInMels}
>
{ eventTiles }
</MemberEventListSummary>,
);
if (this.readMarker) {
ret.push(this.readMarker);
}
return ret;
}
getNewPrevEvent() {
return this.events[0];
}
}
// all the grouper classes that we use
const groupers = [CreationGrouper, MemberGrouper];

View file

@ -92,6 +92,7 @@ export default class RightPanel extends React.Component {
// not mounted in time to get the dispatch.
// Until then, let this code serve as a warning from history.
if (
rps.roomPanelPhaseParams.member &&
userForPanel.userId === rps.roomPanelPhaseParams.member.userId &&
rps.roomPanelPhaseParams.verificationRequest
) {
@ -285,7 +286,7 @@ export default class RightPanel extends React.Component {
});
return (
<aside className={classes}>
<aside className={classes} id="mx_RightPanel">
{ panel }
</aside>
);

View file

@ -460,8 +460,6 @@ export default createReactClass({
// (We could use isMounted, but facebook have deprecated that.)
this.unmounted = true;
SettingsStore.unwatchSetting(this._ciderWatcherRef);
// update the scroll map before we get unmounted
if (this.state.roomId) {
RoomScrollStateStore.setScrollState(this.state.roomId, this._getScrollState());
@ -811,7 +809,9 @@ export default createReactClass({
debuglog("e2e verified", verified, "unverified", unverified);
/* Check all verified user devices. */
for (const userId of [...verified, cli.getUserId()]) {
/* Don't alarm if no other users are verified */
const targets = (verified.length > 0) ? [...verified, cli.getUserId()] : verified;
for (const userId of targets) {
const devices = await cli.getStoredDevicesForUser(userId);
const anyDeviceNotVerified = devices.some(({deviceId}) => {
return !cli.checkDeviceTrust(userId, deviceId).isVerified();

View file

@ -160,6 +160,7 @@ export default createReactClass({
onKeyDown={ this._onKeyDown }
onBlur={this._onBlur}
placeholder={ placeholder }
autoComplete="off"
/>
{ clearButton }
</div>

View file

@ -83,12 +83,13 @@ export default class CompleteSecurity extends React.Component {
}
}
onVerificationRequest = (request) => {
onVerificationRequest = async (request) => {
if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
if (this.state.verificationRequest) {
this.state.verificationRequest.off("change", this.onVerificationRequestChange);
}
await request.accept();
request.on("change", this.onVerificationRequestChange);
this.setState({
verificationRequest: request,
@ -138,9 +139,12 @@ export default class CompleteSecurity extends React.Component {
let body;
if (this.state.verificationRequest) {
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
body = <IncomingSasDialog verifier={this.state.verificationRequest.verifier}
onFinished={this.props.onFinished}
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
body = <EncryptionPanel
layout="dialog"
verificationRequest={this.state.verificationRequest}
onClose={this.props.onFinished}
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
/>;
} else if (phase === PHASE_INTRO) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;

View file

@ -481,7 +481,7 @@ export default createReactClass({
"Either use HTTPS or <a>enable unsafe scripts</a>.", {},
{
'a': (sub) => {
return <a target="_blank" rel="noopener"
return <a target="_blank" rel="noreferrer noopener"
href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
>
{ sub }
@ -496,11 +496,10 @@ export default createReactClass({
"<a>homeserver's SSL certificate</a> is trusted, and that a browser extension " +
"is not blocking requests.", {},
{
'a': (sub) => {
return <a target="_blank" rel="noopener" href={this.props.serverConfig.hsUrl}>
'a': (sub) =>
<a target="_blank" rel="noreferrer noopener" href={this.props.serverConfig.hsUrl}>
{ sub }
</a>;
},
</a>,
},
) }
</span>;

View file

@ -26,7 +26,7 @@ export default createReactClass({
render: function() {
return (
<div className="mx_AuthFooter">
<a href="https://matrix.org" target="_blank" rel="noopener">{ _t("powered by Matrix") }</a>
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">{ _t("powered by Matrix") }</a>
</div>
);
},

View file

@ -61,13 +61,9 @@ export default createReactClass({
} else {
console.log("Loading recaptcha script...");
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
let protocol = global.location.protocol;
if (protocol !== "http:") {
protocol = "https:";
}
const scriptTag = document.createElement('script');
scriptTag.setAttribute(
'src', `${protocol}//www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
'src', `https://www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
);
this._recaptchaContainer.current.appendChild(scriptTag);
}

View file

@ -331,7 +331,7 @@ export const TermsAuthEntry = createReactClass({
checkboxes.push(
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
<a href={policy.url} target="_blank" rel="noopener">{ policy.name }</a>
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
</label>,
);
}
@ -604,6 +604,7 @@ export const FallbackAuthEntry = createReactClass({
this.props.authSessionId,
);
this._popupWindow = window.open(url);
this._popupWindow.opener = null;
},
_onReceiveMessage: function(event) {

View file

@ -99,7 +99,7 @@ export default class ModularServerConfig extends ServerConfig {
"Enter the location of your Modular homeserver. It may use your own " +
"domain name or be a subdomain of <a>modular.im</a>.",
{}, {
a: sub => <a href={MODULAR_URL} target="_blank" rel="noopener">
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
{sub}
</a>,
},

View file

@ -486,6 +486,7 @@ export default createReactClass({
id="mx_RegistrationForm_password"
ref={field => this[FIELD_PASSWORD] = field}
type="password"
autoComplete="new-password"
label={_t("Password")}
value={this.state.password}
onChange={this.onPasswordChange}
@ -499,6 +500,7 @@ export default createReactClass({
id="mx_RegistrationForm_passwordConfirm"
ref={field => this[FIELD_PASSWORD_CONFIRM] = field}
type="password"
autoComplete="new-password"
label={_t("Confirm")}
value={this.state.passwordConfirm}
onChange={this.onPasswordConfirmChange}

View file

@ -274,15 +274,13 @@ export default class ServerConfig extends React.PureComponent {
: null;
return (
<div className="mx_ServerConfig">
<form className="mx_ServerConfig" onSubmit={this.onSubmit} autoComplete="off">
<h3>{_t("Other servers")}</h3>
{errorText}
{this._renderHomeserverSection()}
{this._renderIdentityServerSection()}
<form onSubmit={this.onSubmit} autoComplete="off" action={null}>
{submitButton}
</form>
</div>
{submitButton}
</form>
);
}
}

View file

@ -46,7 +46,7 @@ export const TYPES = {
label: () => _t('Premium'),
logo: () => <img src={require('../../../../res/img/modular-bw-logo.svg')} />,
description: () => _t('Premium hosting for organisations <a>Learn more</a>', {}, {
a: sub => <a href={MODULAR_URL} target="_blank" rel="noopener">
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
{sub}
</a>,
}),

View file

@ -414,11 +414,16 @@ export default createReactClass({
}
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
const permalinkButton = (
<MenuItem className="mx_MessageContextMenu_field">
<a href={permalink} target="_blank" rel="noopener" onClick={this.onPermalinkClick} tabIndex={-1}>
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
? _t('Share Permalink') : _t('Share Message') }
</a>
<MenuItem
element="a"
className="mx_MessageContextMenu_field"
onClick={this.onPermalinkClick}
href={permalink}
target="_blank"
rel="noreferrer noopener"
>
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
? _t('Share Permalink') : _t('Share Message') }
</MenuItem>
);
@ -436,16 +441,15 @@ export default createReactClass({
isUrlPermitted(mxEvent.event.content.external_url)
) {
externalURLButton = (
<MenuItem className="mx_MessageContextMenu_field">
<a
href={mxEvent.event.content.external_url}
target="_blank"
rel="noopener"
onClick={this.closeMenu}
tabIndex={-1}
>
{ _t('Source URL') }
</a>
<MenuItem
element="a"
className="mx_MessageContextMenu_field"
target="_blank"
rel="noreferrer noopener"
onClick={this.closeMenu}
href={mxEvent.event.content.external_url}
>
{ _t('Source URL') }
</MenuItem>
);
}

View file

@ -68,10 +68,11 @@ export default class TopLeftMenu extends React.Component {
{_t(
"<a>Upgrade</a> to your own domain", {},
{
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener" tabIndex={-1}>{sub}</a>,
a: sub =>
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener" tabIndex={-1}>{sub}</a>,
},
)}
<a href={hostingSignupLink} target="_blank" rel="noopener" role="presentation" aria-hidden={true} tabIndex={-1}>
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener" role="presentation" aria-hidden={true} tabIndex={-1}>
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a>
</div>;

View file

@ -52,7 +52,7 @@ export default class ChangelogDialog extends React.Component {
_elementsForCommit(commit) {
return (
<li key={commit.sha} className="mx_ChangelogDialog_li">
<a href={commit.html_url} target="_blank" rel="noopener">
<a href={commit.html_url} target="_blank" rel="noreferrer noopener">
{commit.commit.message.split('\n')[0]}
</a>
</li>

View file

@ -0,0 +1,65 @@
/*
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 {_t} from "../../../languageHandler";
import * as sdk from "../../../index";
export default class ConfirmDestroyCrossSigningDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
_onConfirm = () => {
this.props.onFinished(true);
};
_onDecline = () => {
this.props.onFinished(false);
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog
className='mx_ConfirmDestroyCrossSigningDialog'
hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Destroy cross-signing keys?")}>
<div className='mx_ConfirmDestroyCrossSigningDialog_content'>
<p>
{_t(
"Deleting cross-signing keys is permanent. " +
"Anyone you have verified with will see security alerts. " +
"You almost certainly don't want to do this, unless " +
"you've lost every device you can cross-sign from.",
)}
</p>
</div>
<DialogButtons
primaryButton={_t("Clear cross-signing keys")}
onPrimaryButtonClick={this._onConfirm}
primaryButtonClass="danger"
cancelButton={_t("Cancel")}
onCancel={this._onDecline}
/>
</BaseDialog>
);
}
}

View file

@ -118,6 +118,7 @@ export default class DeactivateAccountDialog extends React.Component {
const Field = sdk.getComponent('elements.Field');
// this is on purpose not a <form /> to prevent Enter triggering submission, to further prevent accidents
return (
<BaseDialog className="mx_DeactivateAccountDialog"
onFinished={this.props.onFinished}

View file

@ -27,16 +27,19 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import {ensureDMExists} from "../../../createRoom";
import dis from "../../../dispatcher";
import SettingsStore from '../../../settings/SettingsStore';
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
import VerificationQREmojiOptions from "../verification/VerificationQREmojiOptions";
const MODE_LEGACY = 'legacy';
const MODE_SAS = 'sas';
const PHASE_START = 0;
const PHASE_WAIT_FOR_PARTNER_TO_ACCEPT = 1;
const PHASE_SHOW_SAS = 2;
const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 3;
const PHASE_VERIFIED = 4;
const PHASE_CANCELLED = 5;
const PHASE_PICK_VERIFICATION_OPTION = 2;
const PHASE_SHOW_SAS = 3;
const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 4;
const PHASE_VERIFIED = 5;
const PHASE_CANCELLED = 6;
export default class DeviceVerifyDialog extends React.Component {
static propTypes = {
@ -49,6 +52,7 @@ export default class DeviceVerifyDialog extends React.Component {
super();
this._verifier = null;
this._showSasEvent = null;
this._request = null;
this.state = {
phase: PHASE_START,
mode: MODE_SAS,
@ -80,6 +84,25 @@ export default class DeviceVerifyDialog extends React.Component {
this.props.onFinished(false);
}
_onUseSasClick = async () => {
try {
this._verifier = this._request.beginKeyVerification(verificationMethods.SAS);
this._verifier.on('show_sas', this._onVerifierShowSas);
// throws upon cancellation
await this._verifier.verify();
this.setState({phase: PHASE_VERIFIED});
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier = null;
} catch (e) {
console.log("Verification failed", e);
this.setState({
phase: PHASE_CANCELLED,
});
this._verifier = null;
this._request = null;
}
};
_onLegacyFinished = (confirm) => {
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
@ -100,7 +123,7 @@ export default class DeviceVerifyDialog extends React.Component {
const roomId = await ensureDMExistsAndOpen(this.props.userId);
// throws upon cancellation before having started
const request = await client.requestVerificationDM(
this.props.userId, roomId, [verificationMethods.SAS],
this.props.userId, roomId,
);
await request.waitFor(r => r.ready || r.started);
if (request.ready) {
@ -108,11 +131,21 @@ export default class DeviceVerifyDialog extends React.Component {
} else {
this._verifier = request.verifier;
}
} else if (verifyingOwnDevice && SettingsStore.isFeatureEnabled("feature_cross_signing")) {
this._request = await client.requestVerification(this.props.userId, [
verificationMethods.SAS,
SHOW_QR_CODE_METHOD,
verificationMethods.RECIPROCATE_QR_CODE,
]);
await this._request.waitFor(r => r.ready || r.started);
this.setState({phase: PHASE_PICK_VERIFICATION_OPTION});
} else {
this._verifier = client.beginKeyVerification(
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
);
}
if (!this._verifier) return;
this._verifier.on('show_sas', this._onVerifierShowSas);
// throws upon cancellation
await this._verifier.verify();
@ -150,10 +183,13 @@ export default class DeviceVerifyDialog extends React.Component {
let body;
switch (this.state.phase) {
case PHASE_START:
body = this._renderSasVerificationPhaseStart();
body = this._renderVerificationPhaseStart();
break;
case PHASE_WAIT_FOR_PARTNER_TO_ACCEPT:
body = this._renderSasVerificationPhaseWaitAccept();
body = this._renderVerificationPhaseWaitAccept();
break;
case PHASE_PICK_VERIFICATION_OPTION:
body = this._renderVerificationPhasePick();
break;
case PHASE_SHOW_SAS:
body = this._renderSasVerificationPhaseShowSas();
@ -162,10 +198,10 @@ export default class DeviceVerifyDialog extends React.Component {
body = this._renderSasVerificationPhaseWaitForPartnerToConfirm();
break;
case PHASE_VERIFIED:
body = this._renderSasVerificationPhaseVerified();
body = this._renderVerificationPhaseVerified();
break;
case PHASE_CANCELLED:
body = this._renderSasVerificationPhaseCancelled();
body = this._renderVerificationPhaseCancelled();
break;
}
@ -180,7 +216,7 @@ export default class DeviceVerifyDialog extends React.Component {
);
}
_renderSasVerificationPhaseStart() {
_renderVerificationPhaseStart() {
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
@ -206,7 +242,7 @@ export default class DeviceVerifyDialog extends React.Component {
);
}
_renderSasVerificationPhaseWaitAccept() {
_renderVerificationPhaseWaitAccept() {
const Spinner = sdk.getComponent("views.elements.Spinner");
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
@ -227,6 +263,14 @@ export default class DeviceVerifyDialog extends React.Component {
);
}
_renderVerificationPhasePick() {
return <VerificationQREmojiOptions
request={this._request}
onCancel={this._onCancelClick}
onStartEmoji={this._onUseSasClick}
/>;
}
_renderSasVerificationPhaseShowSas() {
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
return <VerificationShowSas
@ -234,6 +278,7 @@ export default class DeviceVerifyDialog extends React.Component {
onCancel={this._onCancelClick}
onDone={this._onSasMatchesClick}
isSelf={MatrixClientPeg.get().getUserId() === this.props.userId}
onStartEmoji={this._onUseSasClick}
/>;
}
@ -247,12 +292,12 @@ export default class DeviceVerifyDialog extends React.Component {
</div>;
}
_renderSasVerificationPhaseVerified() {
_renderVerificationPhaseVerified() {
const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete');
return <VerificationComplete onDone={this._onVerifiedDoneClick} />;
}
_renderSasVerificationPhaseCancelled() {
_renderVerificationPhaseCancelled() {
const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled');
return <VerificationCancelled onDone={this._onCancelClick} />;
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {useState, useEffect} from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import SyntaxHighlight from '../elements/SyntaxHighlight';
@ -22,6 +22,16 @@ import { _t } from '../../../languageHandler';
import { Room } from "matrix-js-sdk";
import Field from "../elements/Field";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {
PHASE_UNSENT,
PHASE_REQUESTED,
PHASE_READY,
PHASE_DONE,
PHASE_STARTED,
PHASE_CANCELLED,
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
class GenericEditor extends React.PureComponent {
// static propTypes = {onBack: PropTypes.func.isRequired};
@ -605,12 +615,97 @@ class ServersInRoomList extends React.PureComponent {
}
}
const PHASE_MAP = {
[PHASE_UNSENT]: "unsent",
[PHASE_REQUESTED]: "requested",
[PHASE_READY]: "ready",
[PHASE_DONE]: "done",
[PHASE_STARTED]: "started",
[PHASE_CANCELLED]: "cancelled",
};
function VerificationRequest({txnId, request}) {
const [, updateState] = useState();
const [timeout, setRequestTimeout] = useState(request.timeout);
/* Re-render if something changes state */
useEventEmitter(request, "change", updateState);
/* Keep re-rendering if there's a timeout */
useEffect(() => {
if (request.timeout == 0) return;
/* Note that request.timeout is a getter, so its value changes */
const id = setInterval(() => {
setRequestTimeout(request.timeout);
}, 500);
return () => { clearInterval(id); };
}, [request]);
return (<div className="mx_DevTools_VerificationRequest">
<dl>
<dt>Transaction</dt>
<dd>{txnId}</dd>
<dt>Phase</dt>
<dd>{PHASE_MAP[request.phase] || request.phase}</dd>
<dt>Timeout</dt>
<dd>{Math.floor(timeout / 1000)}</dd>
<dt>Methods</dt>
<dd>{request.methods && request.methods.join(", ")}</dd>
<dt>requestingUserId</dt>
<dd>{request.requestingUserId}</dd>
<dt>observeOnly</dt>
<dd>{JSON.stringify(request.observeOnly)}</dd>
</dl>
</div>);
}
class VerificationExplorer extends React.Component {
static getLabel() {
return _t("Verification Requests");
}
/* Ensure this.context is the cli */
static contextType = MatrixClientContext;
onNewRequest = () => {
this.forceUpdate();
}
componentDidMount() {
const cli = this.context;
cli.on("crypto.verification.request", this.onNewRequest);
}
componentWillUnmount() {
const cli = this.context;
cli.off("crypto.verification.request", this.onNewRequest);
}
render() {
const cli = this.context;
const room = this.props.room;
const inRoomChannel = cli._crypto._inRoomVerificationRequests;
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
return (<div>
<div className="mx_Dialog_content">
{Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) =>
<VerificationRequest txnId={txnId} request={request} key={txnId} />,
)}
</div>
</div>);
}
}
const Entries = [
SendCustomEvent,
RoomStateExplorer,
SendAccountData,
AccountDataExplorer,
ServersInRoomList,
VerificationExplorer,
];
export default class DevtoolsDialog extends React.PureComponent {

View file

@ -31,7 +31,7 @@ import dis from "../../../dispatcher";
import IdentityAuthClient from "../../../IdentityAuthClient";
import Modal from "../../../Modal";
import {humanizeTime} from "../../../utils/humanize";
import createRoom from "../../../createRoom";
import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
import {inviteMultipleToRoom} from "../../../RoomInvite";
import SettingsStore from '../../../settings/SettingsStore';
@ -512,9 +512,27 @@ export default class InviteDialog extends React.PureComponent {
return false;
}
_convertFilter(): Member[] {
// Check to see if there's anything to convert first
if (!this.state.filterText || !this.state.filterText.includes('@')) return this.state.targets || [];
let newMember: Member;
if (this.state.filterText.startsWith('@')) {
// Assume mxid
newMember = new DirectoryMember({user_id: this.state.filterText, display_name: null, avatar_url: null});
} else {
// Assume email
newMember = new ThreepidMember(this.state.filterText);
}
const newTargets = [...(this.state.targets || []), newMember];
this.setState({targets: newTargets, filterText: ''});
return newTargets;
}
_startDm = async () => {
this.setState({busy: true});
const targetIds = this.state.targets.map(t => t.userId);
const targets = this._convertFilter();
const targetIds = targets.map(t => t.userId);
// Check if there is already a DM with these people and reuse it if possible.
const existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
@ -535,11 +553,7 @@ export default class InviteDialog extends React.PureComponent {
// Check whether all users have uploaded device keys before.
// If so, enable encryption in the new room.
const client = MatrixClientPeg.get();
const usersToDevicesMap = await client.downloadKeys(targetIds);
const allHaveDeviceKeys = Object.values(usersToDevicesMap).every(devices => {
// `devices` is an object of the form { deviceId: deviceInfo, ... }.
return Object.keys(devices).length > 0;
});
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
if (allHaveDeviceKeys) {
createRoomOptions.encryption = true;
}
@ -548,9 +562,12 @@ export default class InviteDialog extends React.PureComponent {
// Check if it's a traditional DM and create the room if required.
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
let createRoomPromise = Promise.resolve();
if (targetIds.length === 1) {
const isSelf = targetIds.length === 1 && targetIds[0] === MatrixClientPeg.get().getUserId();
if (targetIds.length === 1 && !isSelf) {
createRoomOptions.dmUserId = targetIds[0];
createRoomPromise = createRoom(createRoomOptions);
} else if (isSelf) {
createRoomPromise = createRoom(createRoomOptions);
} else {
// Create a boring room and try to invite the targets manually.
createRoomPromise = createRoom(createRoomOptions).then(roomId => {
@ -577,7 +594,9 @@ export default class InviteDialog extends React.PureComponent {
_inviteUsers = () => {
this.setState({busy: true});
const targetIds = this.state.targets.map(t => t.userId);
this._convertFilter();
const targets = this._convertFilter();
const targetIds = targets.map(t => t.userId);
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (!room) {
@ -634,13 +653,14 @@ export default class InviteDialog extends React.PureComponent {
// While we're here, try and autocomplete a search result for the mxid itself
// if there's no matches (and the input looks like a mxid).
if (term[0] === '@' && term.indexOf(':') > 1 && r.results.length === 0) {
if (term[0] === '@' && term.indexOf(':') > 1) {
try {
const profile = await MatrixClientPeg.get().getProfileInfo(term);
if (profile) {
// If we have a profile, we have enough information to assume that
// the mxid can be invited - add it to the list
r.results.push({
// the mxid can be invited - add it to the list. We stick it at the
// top so it is most obviously presented to the user.
r.results.splice(0, 0, {
user_id: term,
display_name: profile['displayname'],
avatar_url: profile['avatar_url'],
@ -649,6 +669,14 @@ export default class InviteDialog extends React.PureComponent {
} catch (e) {
console.warn("Non-fatal error trying to make an invite for a user ID");
console.warn(e);
// Add a result anyways, just without a profile. We stick it at the
// top so it is most obviously presented to the user.
r.results.splice(0, 0, {
user_id: term,
display_name: term,
avatar_url: null,
});
}
}
@ -773,7 +801,7 @@ export default class InviteDialog extends React.PureComponent {
];
const toAdd = [];
const failed = [];
const potentialAddresses = text.split(/[\s,]+/);
const potentialAddresses = text.split(/[\s,]+/).map(p => p.trim()).filter(p => !!p); // filter empty strings
for (const address of potentialAddresses) {
const member = possibleMembers.find(m => m.userId === address);
if (member) {
@ -1018,7 +1046,7 @@ export default class InviteDialog extends React.PureComponent {
"If you can't find someone, ask them for their username, share your " +
"username (%(userId)s) or <a>profile link</a>.",
{userId},
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>},
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{sub}</a>},
);
buttonText = _t("Go");
goButtonFn = this._startDm;
@ -1027,12 +1055,17 @@ export default class InviteDialog extends React.PureComponent {
helpText = _t(
"If you can't find someone, ask them for their username (e.g. @user:server.com) or " +
"<a>share this room</a>.", {},
{a: (sub) => <a href={makeRoomPermalink(this.props.roomId)} rel="noopener" target="_blank">{sub}</a>},
{
a: (sub) =>
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
},
);
buttonText = _t("Invite");
goButtonFn = this._inviteUsers;
}
const hasSelection = this.state.targets.length > 0
|| (this.state.filterText && this.state.filterText.includes('@'));
return (
<BaseDialog
className='mx_InviteDialog'
@ -1049,7 +1082,7 @@ export default class InviteDialog extends React.PureComponent {
kind="primary"
onClick={goButtonFn}
className='mx_InviteDialog_goButton'
disabled={this.state.busy}
disabled={this.state.busy || !hasSelection}
>
{buttonText}
</AccessibleButton>

View file

@ -19,9 +19,10 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { replaceableComponent } from '../../../utils/replaceableComponent';
import DeviceVerifyDialog from './DeviceVerifyDialog';
import VerificationRequestDialog from './VerificationRequestDialog';
import BaseDialog from './BaseDialog';
import DialogButtons from '../elements/DialogButtons';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
@replaceableComponent("views.dialogs.NewSessionReviewDialog")
export default class NewSessionReviewDialog extends React.PureComponent {
@ -35,12 +36,18 @@ export default class NewSessionReviewDialog extends React.PureComponent {
this.props.onFinished(false);
}
onContinueClick = () => {
onContinueClick = async () => {
const { userId, device } = this.props;
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', DeviceVerifyDialog, {
const cli = MatrixClientPeg.get();
const request = await cli.requestVerification(
userId,
device,
}, null, /* priority = */ false, /* static = */ true);
[device.deviceId],
);
this.props.onFinished(true);
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
verificationRequest: request,
});
}
render() {

View file

@ -31,6 +31,7 @@ export default createReactClass({
danger: PropTypes.bool,
focus: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
headerImage: PropTypes.string,
},
getDefaultProps: function() {
@ -63,6 +64,7 @@ export default createReactClass({
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
title={this.props.title}
contentId='mx_Dialog_content'
headerImage={this.props.headerImage}
hasCancel={this.props.hasCancelButton}
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>

View file

@ -218,7 +218,7 @@ export default class ShareDialog extends React.Component {
</div>
<div className="mx_ShareDialog_social_container">
{
socials.map((social) => <a rel="noopener"
socials.map((social) => <a rel="noreferrer noopener"
target="_blank"
key={social.name}
name={social.name}

View file

@ -135,7 +135,7 @@ export default class TermsDialog extends React.PureComponent {
rows.push(<tr key={termDoc[termsLang].url}>
<td className="mx_TermsDialog_service">{serviceName}</td>
<td className="mx_TermsDialog_summary">{summary}</td>
<td>{termDoc[termsLang].name} <a rel="noopener" target="_blank" href={termDoc[termsLang].url}>
<td>{termDoc[termsLang].name} <a rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
<span className="mx_TermsDialog_link" />
</a></td>
<td><TermsCheckbox

View file

@ -0,0 +1,55 @@
/*
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 {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default class VerificationRequestDialog extends React.Component {
static propTypes = {
verificationRequest: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
};
constructor(...args) {
super(...args);
this.onFinished = this.onFinished.bind(this);
}
render() {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
return <BaseDialog className="mx_InfoDialog" onFinished={this.onFinished}
contentId="mx_Dialog_content"
title={_t("Verification Request")}
hasCancel={true}
>
<EncryptionPanel
layout="dialog"
verificationRequest={this.props.verificationRequest}
onClose={this.props.onFinished}
member={MatrixClientPeg.get().getUser(this.props.verificationRequest.otherUserId)}
/>
</BaseDialog>;
}
onFinished() {
this.props.verificationRequest.cancel();
this.props.onFinished();
}
}

View file

@ -22,7 +22,6 @@ import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import { MatrixClient } from 'matrix-js-sdk';
import Modal from '../../../../Modal';
import { _t } from '../../../../languageHandler';
import {Key} from "../../../../Keyboard";
import { accessSecretStorage } from '../../../../CrossSigningManager';
const RESTORE_TYPE_PASSPHRASE = 0;
@ -125,6 +124,8 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
}
_onRecoveryKeyNext = async () => {
if (!this.state.recoveryKeyValid) return;
this.setState({
loading: true,
restoreError: null,
@ -157,18 +158,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
});
}
_onPassPhraseKeyPress = (e) => {
if (e.key === Key.ENTER) {
this._onPassPhraseNext();
}
}
_onRecoveryKeyKeyPress = (e) => {
if (e.key === Key.ENTER && this.state.recoveryKeyValid) {
this._onRecoveryKeyNext();
}
}
async _restoreWithSecretStorage() {
this.setState({
loading: true,
@ -305,21 +294,22 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
"messaging by entering your recovery passphrase.",
)}</p>
<div className="mx_RestoreKeyBackupDialog_primaryContainer">
<form className="mx_RestoreKeyBackupDialog_primaryContainer">
<input type="password"
className="mx_RestoreKeyBackupDialog_passPhraseInput"
onChange={this._onPassPhraseChange}
onKeyPress={this._onPassPhraseKeyPress}
value={this.state.passPhrase}
autoFocus={true}
/>
<DialogButtons primaryButton={_t('Next')}
<DialogButtons
primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseNext}
primaryIsSubmit={true}
hasCancel={true}
onCancel={this._onCancel}
focus={false}
/>
</div>
</form>
{_t(
"If you've forgotten your recovery passphrase you can "+
"<button1>use your recovery key</button1> or " +
@ -371,7 +361,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
<div className="mx_RestoreKeyBackupDialog_primaryContainer">
<input className="mx_RestoreKeyBackupDialog_recoveryKeyInput"
onChange={this._onRecoveryKeyChange}
onKeyPress={this._onRecoveryKeyKeyPress}
value={this.state.recoveryKey}
autoFocus={true}
/>

View file

@ -1,6 +1,6 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 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.
@ -21,7 +21,6 @@ import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import { _t } from '../../../../languageHandler';
import { Key } from "../../../../Keyboard";
/*
* Access Secure Secret Storage by requesting the user's passphrase.
@ -68,7 +67,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
});
}
_onPassPhraseNext = async () => {
_onPassPhraseNext = async (e) => {
e.preventDefault();
if (this.state.passPhrase.length <= 0) return;
this.setState({ keyMatches: null });
const input = { passphrase: this.state.passPhrase };
const keyMatches = await this.props.checkPrivateKey(input);
@ -79,7 +82,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
}
}
_onRecoveryKeyNext = async () => {
_onRecoveryKeyNext = async (e) => {
e.preventDefault();
if (!this.state.recoveryKeyValid) return;
this.setState({ keyMatches: null });
const input = { recoveryKey: this.state.recoveryKey };
const keyMatches = await this.props.checkPrivateKey(input);
@ -97,18 +104,6 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
});
}
_onPassPhraseKeyPress = (e) => {
if (e.key === Key.ENTER && this.state.passPhrase.length > 0) {
this._onPassPhraseNext();
}
}
_onRecoveryKeyKeyPress = (e) => {
if (e.key === Key.ENTER && this.state.recoveryKeyValid) {
this._onRecoveryKeyNext();
}
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
@ -135,7 +130,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
)}
</div>;
} else {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus"></div>;
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus" />;
}
content = <div>
@ -149,23 +144,25 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
"identity for verifying other sessions by entering your passphrase.",
)}</p>
<div className="mx_AccessSecretStorageDialog_primaryContainer">
<input type="password"
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onPassPhraseNext}>
<input
type="password"
className="mx_AccessSecretStorageDialog_passPhraseInput"
onChange={this._onPassPhraseChange}
onKeyPress={this._onPassPhraseKeyPress}
value={this.state.passPhrase}
autoFocus={true}
autoComplete="new-password"
/>
{keyStatus}
<DialogButtons primaryButton={_t('Next')}
<DialogButtons
primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseNext}
hasCancel={true}
onCancel={this._onCancel}
focus={false}
primaryDisabled={this.state.passPhrase.length === 0}
/>
</div>
</form>
{_t(
"If you've forgotten your passphrase you can "+
"<button1>use your recovery key</button1> or " +
@ -192,11 +189,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
let keyStatus;
if (this.state.recoveryKey.length === 0) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus"></div>;
} else if (this.state.recoveryKeyValid) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
</div>;
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus" />;
} else if (this.state.keyMatches === false) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t(
@ -204,6 +197,10 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
"entered the correct recovery key.",
)}
</div>;
} else if (this.state.recoveryKeyValid) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
</div>;
} else {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
@ -221,22 +218,22 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
"identity for verifying other sessions by entering your recovery key.",
)}</p>
<div className="mx_AccessSecretStorageDialog_primaryContainer">
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onRecoveryKeyNext}>
<input className="mx_AccessSecretStorageDialog_recoveryKeyInput"
onChange={this._onRecoveryKeyChange}
onKeyPress={this._onRecoveryKeyKeyPress}
value={this.state.recoveryKey}
autoFocus={true}
/>
{keyStatus}
<DialogButtons primaryButton={_t('Next')}
<DialogButtons
primaryButton={_t('Next')}
onPrimaryButtonClick={this._onRecoveryKeyNext}
hasCancel={true}
onCancel={this._onCancel}
focus={false}
primaryDisabled={!this.state.recoveryKeyValid}
/>
</div>
</form>
{_t(
"If you've forgotten your recovery key you can "+
"<button>set up new recovery options</button>."

View file

@ -552,7 +552,7 @@ export default class AppTile extends React.Component {
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
// window.open(this._getSafeUrl(), '_blank', 'noopener=yes');
Object.assign(document.createElement('a'),
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener'}).click();
{ target: '_blank', href: this._getSafeUrl(), rel: 'noreferrer noopener'}).click();
}
_onReloadWidgetClick() {

View file

@ -91,7 +91,7 @@ export default class ImageView extends React.Component {
getName() {
let name = this.props.name;
if (name && this.props.link) {
name = <a href={ this.props.link } target="_blank" rel="noopener">{ name }</a>;
name = <a href={ this.props.link } target="_blank" rel="noreferrer noopener">{ name }</a>;
}
return name;
}
@ -216,7 +216,7 @@ export default class ImageView extends React.Component {
{ this.getName() }
</div>
{ eventMeta }
<a className="mx_ImageView_link" href={ this.props.src } download={ this.props.name } target="_blank" rel="noopener">
<a className="mx_ImageView_link" href={ this.props.src } download={ this.props.name } rel="noreferrer noopener">
<div className="mx_ImageView_download">
{ _t('Download this file') }<br />
<span className="mx_ImageView_size">{ sizeRes }</span>

View file

@ -23,7 +23,6 @@ import classNames from 'classnames';
import { Room, RoomMember } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { getDisplayAliasForRoom } from '../../../Rooms';
import FlairStore from "../../../stores/FlairStore";
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
@ -128,7 +127,8 @@ const Pill = createReactClass({
case Pill.TYPE_ROOM_MENTION: {
const localRoom = resourceId[0] === '#' ?
MatrixClientPeg.get().getRooms().find((r) => {
return r.getAliases().includes(resourceId);
return r.getCanonicalAlias() === resourceId ||
r.getAltAliases().includes(resourceId);
}) : MatrixClientPeg.get().getRoom(resourceId);
room = localRoom;
if (!localRoom) {
@ -211,7 +211,7 @@ const Pill = createReactClass({
if (room) {
linkText = "@room";
if (this.props.shouldShowPillAvatar) {
avatar = <RoomAvatar room={room} width={16} height={16} />;
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
}
pillClass = 'mx_AtRoomPill';
}
@ -225,7 +225,7 @@ const Pill = createReactClass({
member.rawDisplayName = member.rawDisplayName || '';
linkText = member.rawDisplayName;
if (this.props.shouldShowPillAvatar) {
avatar = <MemberAvatar member={member} width={16} height={16} />;
avatar = <MemberAvatar member={member} width={16} height={16} aria-hidden="true" />;
}
pillClass = 'mx_UserPill';
href = null;
@ -236,12 +236,12 @@ const Pill = createReactClass({
case Pill.TYPE_ROOM_MENTION: {
const room = this.state.room;
if (room) {
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
linkText = resource;
if (this.props.shouldShowPillAvatar) {
avatar = <RoomAvatar room={room} width={16} height={16} />;
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
}
pillClass = 'mx_RoomPill';
}
pillClass = 'mx_RoomPill';
}
break;
case Pill.TYPE_GROUP_MENTION: {
@ -251,7 +251,7 @@ const Pill = createReactClass({
linkText = groupId;
if (this.props.shouldShowPillAvatar) {
avatar = <BaseAvatar name={name || groupId} width={16} height={16}
avatar = <BaseAvatar name={name || groupId} width={16} height={16} aria-hidden="true"
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 16, 16) : null} />;
}
pillClass = 'mx_GroupPill';

View file

@ -17,40 +17,151 @@ 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";
import {MatrixClientPeg} from "../../../../MatrixClientPeg";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import {ToDeviceChannel} from "matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel";
import {decodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
import Spinner from "../Spinner";
import * as QRCode from "qrcode";
const CODE_VERSION = 0x02; // the version of binary QR codes we support
const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
const MODE_VERIFY_OTHER_USER = 0x00; // Verifying someone who isn't us
const MODE_VERIFY_SELF_TRUSTED = 0x01; // We trust the master key
const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key
@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,
prefix: PropTypes.string.isRequired,
version: PropTypes.number.isRequired,
mode: PropTypes.number.isRequired,
transactionId: PropTypes.string.isRequired, // or requestEventId
firstKeyB64: PropTypes.string.isRequired,
secondKeyB64: PropTypes.string.isRequired,
secretB64: PropTypes.string.isRequired,
};
static defaultProps = {
action: "verify",
};
static async getPropsForRequest(verificationRequest: VerificationRequest) {
const cli = MatrixClientPeg.get();
const myUserId = cli.getUserId();
const otherUserId = verificationRequest.otherUserId;
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];
let mode = MODE_VERIFY_OTHER_USER;
if (myUserId === otherUserId) {
// Mode changes depending on whether or not we trust the master cross signing key
const myTrust = cli.checkUserTrust(myUserId);
if (myTrust.isCrossSigningVerified()) {
mode = MODE_VERIFY_SELF_TRUSTED;
} else {
mode = MODE_VERIFY_SELF_UNTRUSTED;
}
}
const uri = `https://matrix.to/#/${this.props.keyholderUserId}?${qs.stringify(query)}`;
const requestEvent = verificationRequest.requestEvent;
const transactionId = requestEvent.getId()
? requestEvent.getId()
: ToDeviceChannel.getTransactionId(requestEvent);
return <QRCode value={uri} size={512} logoWidth={64} logo={require("../../../../../res/img/matrix-m.svg")} />;
const qrProps = {
prefix: BINARY_PREFIX,
version: CODE_VERSION,
mode,
transactionId,
firstKeyB64: '', // worked out shortly
secondKeyB64: '', // worked out shortly
secretB64: verificationRequest.encodedSharedSecret,
};
const myCrossSigningInfo = cli.getStoredCrossSigningForUser(myUserId);
const myDevices = (await cli.getStoredDevicesForUser(myUserId)) || [];
if (mode === MODE_VERIFY_OTHER_USER) {
// First key is our master cross signing key
qrProps.firstKeyB64 = myCrossSigningInfo.getId("master");
// Second key is the other user's master cross signing key
const otherUserCrossSigningInfo = cli.getStoredCrossSigningForUser(otherUserId);
qrProps.secondKeyB64 = otherUserCrossSigningInfo.getId("master");
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
// First key is our master cross signing key
qrProps.firstKeyB64 = myCrossSigningInfo.getId("master");
// Second key is the other device's device key
const otherDevice = verificationRequest.targetDevice;
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
const device = myDevices.find(d => d.deviceId === otherDeviceId);
qrProps.secondKeyB64 = device.getFingerprint();
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
// First key is our device's key
qrProps.firstKeyB64 = cli.getDeviceEd25519Key();
// Second key is what we think our master cross signing key is
qrProps.secondKeyB64 = myCrossSigningInfo.getId("master");
}
return qrProps;
}
constructor(props) {
super(props);
this.state = {
dataUri: null,
};
this.generateQrCode();
}
componentDidUpdate(prevProps): void {
if (JSON.stringify(this.props) === JSON.stringify(prevProps)) return; // No prop change
this.generateQRCode();
}
async generateQrCode() {
let buf = Buffer.alloc(0); // we'll concat our way through life
const appendByte = (b: number) => {
const tmpBuf = Buffer.from([b]);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendInt = (i: number) => {
const tmpBuf = Buffer.alloc(4);
tmpBuf.writeInt8(i, 0);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendStr = (s: string, enc: string) => {
const tmpBuf = Buffer.from(s, enc);
appendInt(tmpBuf.byteLength);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendEncBase64 = (b64: string) => {
const b = decodeBase64(b64);
const tmpBuf = Buffer.from(b);
buf = Buffer.concat([buf, tmpBuf]);
};
// Actually build the buffer for the QR code
appendStr(this.props.prefix, "ascii");
appendByte(this.props.version);
appendByte(this.props.mode);
appendStr(this.props.transactionId, "utf-8");
appendEncBase64(this.props.firstKeyB64);
appendEncBase64(this.props.secondKeyB64);
appendEncBase64(this.props.secretB64);
// Now actually assemble the QR code's data URI
const uri = await QRCode.toDataURL([{data: buf, mode: 'byte'}], {
errorCorrectionLevel: 'L', // we want it as trivial-looking as possible
});
this.setState({dataUri: uri});
}
render() {
if (!this.state.dataUri) {
return <div className='mx_VerificationQRCode'><Spinner /></div>;
}
return <img src={this.state.dataUri} className='mx_VerificationQRCode' />;
}
}

View file

@ -20,7 +20,7 @@ import * as HtmlUtils from '../../../HtmlUtils';
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
import {formatTime} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk';
import {pillifyLinks} from '../../../utils/pillify';
import {pillifyLinks, unmountPills} from '../../../utils/pillify';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
@ -53,6 +53,7 @@ export default class EditHistoryMessage extends React.PureComponent {
this.state = {canRedact, sendStatus: event.getAssociatedStatus()};
this._content = createRef();
this._pills = [];
}
_onAssociatedStatusChanged = () => {
@ -81,7 +82,7 @@ export default class EditHistoryMessage extends React.PureComponent {
pillifyLinks() {
// not present for redacted events
if (this._content.current) {
pillifyLinks(this._content.current.children, this.props.mxEvent);
pillifyLinks(this._content.current.children, this.props.mxEvent, this._pills);
}
}
@ -90,6 +91,7 @@ export default class EditHistoryMessage extends React.PureComponent {
}
componentWillUnmount() {
unmountPills(this._pills);
const event = this.props.mxEvent;
if (event.localRedactionEvent()) {
event.localRedactionEvent().off("status", this._onAssociatedStatusChanged);

View file

@ -26,7 +26,7 @@ import {decryptFile} from '../../../utils/DecryptFile';
import Tinter from '../../../Tinter';
import request from 'browser-request';
import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
import AccessibleButton from "../elements/AccessibleButton";
// A cached tinted copy of require("../../../../res/img/download.svg")
@ -94,84 +94,6 @@ Tinter.registerTintable(updateTintedDownloadImage);
// The downside of using a second domain is that it complicates hosting,
// the downside of using a sandboxed iframe is that the browers are overly
// restrictive in what you are allowed to do with the generated URL.
//
// For now given how unusable the blobs generated in sandboxed iframes are we
// default to using a renderer hosted on "usercontent.riot.im". This is
// overridable so that people running their own version of the client can
// choose a different renderer.
//
// To that end the current version of the blob generation is the following
// html:
//
// <html><head><script>
// var params = window.location.search.substring(1).split('&');
// var lockOrigin;
// for (var i = 0; i < params.length; ++i) {
// var parts = params[i].split('=');
// if (parts[0] == 'origin') lockOrigin = decodeURIComponent(parts[1]);
// }
// window.onmessage=function(e){
// if (lockOrigin === undefined || e.origin === lockOrigin) eval("("+e.data.code+")")(e);
// }
// </script></head><body></body></html>
//
// This waits to receive a message event sent using the window.postMessage API.
// When it receives the event it evals a javascript function in data.code and
// runs the function passing the event as an argument. This version adds
// support for a query parameter controlling the origin from which messages
// will be processed as an extra layer of security (note that the default URL
// is still 'v1' since it is backwards compatible).
//
// In particular it means that the rendering function can be written as a
// ordinary javascript function which then is turned into a string using
// toString().
//
const DEFAULT_CROSS_ORIGIN_RENDERER = "https://usercontent.riot.im/v1.html";
/**
* Render the attachment inside the iframe.
* We can't use imported libraries here so this has to be vanilla JS.
*/
function remoteRender(event) {
const data = event.data;
const img = document.createElement("img");
img.id = "img";
img.src = data.imgSrc;
const a = document.createElement("a");
a.id = "a";
a.rel = data.rel;
a.target = data.target;
a.download = data.download;
a.style = data.style;
a.style.fontFamily = "Arial, Helvetica, Sans-Serif";
a.href = window.URL.createObjectURL(data.blob);
a.appendChild(img);
a.appendChild(document.createTextNode(data.textContent));
const body = document.body;
// Don't display scrollbars if the link takes more than one line
// to display.
body.style = "margin: 0px; overflow: hidden";
body.appendChild(a);
}
/**
* Update the tint inside the iframe.
* We can't use imported libraries here so this has to be vanilla JS.
*/
function remoteSetTint(event) {
const data = event.data;
const img = document.getElementById("img");
img.src = data.imgSrc;
img.style = data.imgStyle;
const a = document.getElementById("a");
a.style = data.style;
}
/**
* Get the current CSS style for a DOMElement.
@ -283,7 +205,6 @@ export default createReactClass({
// will be inside the iframe so we wont be able to update
// it directly.
this._iframe.current.contentWindow.postMessage({
code: remoteSetTint.toString(),
imgSrc: tintedDownloadImageURL,
style: computedStyle(this._dummyLink.current),
}, "*");
@ -306,7 +227,7 @@ export default createReactClass({
// Wait for the user to click on the link before downloading
// and decrypting the attachment.
let decrypting = false;
const decrypt = () => {
const decrypt = (e) => {
if (decrypting) {
return false;
}
@ -323,16 +244,15 @@ export default createReactClass({
});
}).finally(() => {
decrypting = false;
return;
});
};
return (
<span className="mx_MFileBody">
<div className="mx_MFileBody_download">
<a href="javascript:void(0)" onClick={decrypt}>
<AccessibleButton onClick={decrypt}>
{ _t("Decrypt %(text)s", { text: text }) }
</a>
</AccessibleButton>
</div>
</span>
);
@ -341,7 +261,6 @@ export default createReactClass({
// When the iframe loads we tell it to render a download link
const onIframeLoad = (ev) => {
ev.target.contentWindow.postMessage({
code: remoteRender.toString(),
imgSrc: tintedDownloadImageURL,
style: computedStyle(this._dummyLink.current),
blob: this.state.decryptedBlob,
@ -349,19 +268,13 @@ export default createReactClass({
// will have the correct name when the user tries to download it.
// We can't provide a Content-Disposition header like we would for HTTP.
download: fileName,
rel: "noopener",
target: "_blank",
textContent: _t("Download %(text)s", { text: text }),
}, "*");
};
// If the attachment is encryped then put the link inside an iframe.
let renderer_url = DEFAULT_CROSS_ORIGIN_RENDERER;
const appConfig = SdkConfig.get();
if (appConfig && appConfig.cross_origin_renderer_url) {
renderer_url = appConfig.cross_origin_renderer_url;
}
renderer_url += "?origin=" + encodeURIComponent(window.location.origin);
const url = "usercontent/"; // XXX: this path should probably be passed from the skin
// If the attachment is encrypted then put the link inside an iframe.
return (
<span className="mx_MFileBody">
<div className="mx_MFileBody_download">
@ -373,14 +286,18 @@ export default createReactClass({
*/ }
<a ref={this._dummyLink} />
</div>
<iframe src={renderer_url} onLoad={onIframeLoad} ref={this._iframe} />
<iframe
src={`${url}?origin=${encodeURIComponent(window.location.origin)}`}
onLoad={onIframeLoad}
ref={this._iframe}
sandbox="allow-scripts allow-downloads" />
</div>
</span>
);
} else if (contentUrl) {
const downloadProps = {
target: "_blank",
rel: "noopener",
rel: "noreferrer noopener",
// We set the href regardless of whether or not we intercept the download
// because we don't really want to convert the file to a blob eagerly, and

View file

@ -40,7 +40,10 @@ export default class MKeyVerificationConclusion extends React.Component {
if (request) {
request.off("change", this._onRequestChanged);
}
MatrixClientPeg.removeListener("userTrustStatusChanged", this._onTrustChanged);
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("userTrustStatusChanged", this._onTrustChanged);
}
}
_onRequestChanged = () => {

View file

@ -27,6 +27,7 @@ import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
export default class MKeyVerificationRequest extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
@ -93,10 +94,20 @@ export default class MKeyVerificationRequest extends React.Component {
_cancelledLabel(userId) {
const client = MatrixClientPeg.get();
const myUserId = client.getUserId();
const {cancellationCode} = this.props.mxEvent.verificationRequest;
const declined = cancellationCode === "m.user";
if (userId === myUserId) {
return _t("You cancelled");
if (declined) {
return _t("You declined");
} else {
return _t("You cancelled");
}
} else {
return _t("%(name)s cancelled", {name: getNameForEventRoom(userId, this.props.mxEvent.getRoomId())});
if (declined) {
return _t("%(name)s declined", {name: getNameForEventRoom(userId, this.props.mxEvent.getRoomId())});
} else {
return _t("%(name)s cancelled", {name: getNameForEventRoom(userId, this.props.mxEvent.getRoomId())});
}
}
}
@ -115,15 +126,19 @@ export default class MKeyVerificationRequest extends React.Component {
let subtitle;
let stateNode;
const accepted = request.ready || request.started || request.done;
if (accepted || request.cancelled) {
if (!request.canAccept) {
let stateLabel;
const accepted = request.ready || request.started || request.done;
if (accepted) {
stateLabel = (<AccessibleButton onClick={this._openRequest}>
{this._acceptedLabel(request.receivingUserId)}
</AccessibleButton>);
} else {
} else if (request.cancelled) {
stateLabel = this._cancelledLabel(request.cancellingUserId);
} else if (request.accepting) {
stateLabel = _t("accepting …");
} else if (request.declining) {
stateLabel = _t("declining …");
}
stateNode = (<div className="mx_cryptoEvent_state">{stateLabel}</div>);
}
@ -134,7 +149,7 @@ export default class MKeyVerificationRequest extends React.Component {
_t("%(name)s wants to verify", {name})}</div>);
subtitle = (<div className="mx_cryptoEvent_subtitle">{
userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId())}</div>);
if (request.requested && !request.observeOnly) {
if (request.canAccept) {
stateNode = (<div className="mx_cryptoEvent_buttons">
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
<FormButton onClick={this._onAcceptClicked} label={_t("Accept")} />

View file

@ -30,7 +30,7 @@ import { _t } from '../../../languageHandler';
import * as ContextMenu from '../../structures/ContextMenu';
import SettingsStore from "../../../settings/SettingsStore";
import ReplyThread from "../elements/ReplyThread";
import {pillifyLinks} from '../../../utils/pillify';
import {pillifyLinks, unmountPills} from '../../../utils/pillify';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
import {toRightOf} from "../../structures/ContextMenu";
@ -92,6 +92,7 @@ export default createReactClass({
componentDidMount: function() {
this._unmounted = false;
this._pills = [];
if (!this.props.editState) {
this._applyFormatting();
}
@ -103,7 +104,7 @@ export default createReactClass({
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
// are still sent as plaintext URLs. If these are ever pillified in the composer,
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
pillifyLinks([this._content.current], this.props.mxEvent);
pillifyLinks([this._content.current], this.props.mxEvent, this._pills);
HtmlUtils.linkifyElement(this._content.current);
this.calculateUrlPreview();
@ -146,6 +147,7 @@ export default createReactClass({
componentWillUnmount: function() {
this._unmounted = true;
unmountPills(this._pills);
},
shouldComponentUpdate: function(nextProps, nextState) {
@ -372,7 +374,9 @@ export default createReactClass({
const height = window.screen.height > 800 ? 800 : window.screen.height;
const left = (window.screen.width - width) / 2;
const top = (window.screen.height - height) / 2;
window.open(completeUrl, '_blank', `height=${height}, width=${width}, top=${top}, left=${left},`);
const features = `height=${height}, width=${width}, top=${top}, left=${left},`;
const wnd = window.open(completeUrl, '_blank', features);
wnd.opener = null;
},
});
});

View file

@ -23,20 +23,23 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {ensureDMExists} from "../../../createRoom";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import Modal from "../../../Modal";
import {PHASE_REQUESTED} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import {PHASE_REQUESTED, PHASE_UNSENT} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import * as sdk from "../../../index";
import {_t} from "../../../languageHandler";
// cancellation codes which constitute a key mismatch
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
const EncryptionPanel = ({verificationRequest, member, onClose}) => {
const EncryptionPanel = ({verificationRequest, member, onClose, layout}) => {
const [request, setRequest] = useState(verificationRequest);
useEffect(() => {
setRequest(verificationRequest);
}, [verificationRequest]);
const [phase, setPhase] = useState(request && request.phase);
useEffect(() => {
setRequest(verificationRequest);
if (verificationRequest) {
setPhase(verificationRequest.phase);
}
}, [verificationRequest]);
const changeHandler = useCallback(() => {
// handle transitions -> cancelled for mismatches which fire a modal instead of showing a card
if (request && request.cancelled && MISMATCHES.includes(request.cancellationCode)) {
@ -69,14 +72,16 @@ const EncryptionPanel = ({verificationRequest, member, onClose}) => {
const roomId = await ensureDMExists(cli, member.userId);
const verificationRequest = await cli.requestVerificationDM(member.userId, roomId);
setRequest(verificationRequest);
setPhase(verificationRequest.phase);
}, [member.userId]);
const requested = request && (phase === PHASE_REQUESTED || phase === undefined);
const requested = request && (phase === PHASE_REQUESTED || phase === PHASE_UNSENT || phase === undefined);
if (!request || requested) {
return <EncryptionInfo onStartVerification={onStartVerification} member={member} pending={requested} />;
} else {
return (
<VerificationPanel
layout={layout}
onClose={onClose}
member={member}
request={request}
@ -89,6 +94,7 @@ EncryptionPanel.propTypes = {
member: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
verificationRequest: PropTypes.object,
layout: PropTypes.string,
};
export default EncryptionPanel;

View file

@ -25,7 +25,7 @@ import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import createRoom from '../../../createRoom';
import createRoom, {findDMForUser} from '../../../createRoom';
import DMRoomMap from '../../../utils/DMRoomMap';
import AccessibleButton from '../elements/AccessibleButton';
import SdkConfig from '../../../SdkConfig';
@ -135,19 +135,53 @@ function useIsEncrypted(cli, room) {
return isEncrypted;
}
function verifyDevice(userId, device) {
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
userId: userId,
device: device,
}, null, /* priority = */ false, /* static = */ true);
async function verifyDevice(userId, device) {
const cli = MatrixClientPeg.get();
const member = cli.getUser(userId);
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog("Verification warning", "unverified session", QuestionDialog, {
headerImage: require("../../../../res/img/e2e/warning.svg"),
title: _t("Not Trusted"),
description: <div>
<p>{_t("%(name)s (%(userId)s) signed in to a new session without verifying it:", {name: member.displayName, userId})}</p>
<p>{device.getDisplayName()} ({device.deviceId})</p>
<p>{_t("Ask this user to verify their session, or manually verify it below.")}</p>
</div>,
onFinished: async (doneClicked) => {
const manuallyVerifyClicked = !doneClicked;
if (!manuallyVerifyClicked) {
return;
}
const cli = MatrixClientPeg.get();
const verificationRequest = await cli.requestVerification(
userId,
[device.deviceId],
);
dis.dispatch({
action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
refireParams: {member, verificationRequest},
});
},
primaryButton: _t("Done"),
cancelButton: _t("Manually Verify"),
});
}
function verifyUser(user) {
const cli = MatrixClientPeg.get();
const dmRoom = findDMForUser(cli, user.userId);
let existingRequest;
if (dmRoom) {
existingRequest = cli.findVerificationRequestDMInProgress(dmRoom.roomId);
}
dis.dispatch({
action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
refireParams: {member: user},
refireParams: {
member: user,
verificationRequest: existingRequest,
},
});
}

View file

@ -19,8 +19,9 @@ import PropTypes from "prop-types";
import * as sdk from '../../../index';
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {_t} from "../../../languageHandler";
import E2EIcon from "../rooms/E2EIcon";
import {
@ -29,12 +30,13 @@ import {
PHASE_READY,
PHASE_DONE,
PHASE_STARTED,
PHASE_CANCELLED,
PHASE_CANCELLED, VerificationRequest,
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import Spinner from "../elements/Spinner";
export default class VerificationPanel extends React.PureComponent {
static propTypes = {
layout: PropTypes.string,
request: PropTypes.object.isRequired,
member: PropTypes.object.isRequired,
phase: PropTypes.oneOf([
@ -50,68 +52,122 @@ export default class VerificationPanel extends React.PureComponent {
constructor(props) {
super(props);
this.state = {};
this.state = {
qrCodeProps: null, // generated by the VerificationQRCode component itself
};
this._hasVerifier = false;
if (this.props.request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD)) {
this._generateQRCodeProps(props.request);
}
}
async _generateQRCodeProps(verificationRequest: VerificationRequest) {
try {
this.setState({qrCodeProps: await VerificationQRCode.getPropsForRequest(verificationRequest)});
} catch (e) {
console.error(e);
// Do nothing - we won't render a QR code.
}
}
renderQRPhase(pending) {
const {member, request} = this.props;
const showSAS = request.methods.includes(verificationMethods.SAS);
const showQR = this.props.request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD);
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let button;
if (pending) {
button = <Spinner />;
} else {
button = (
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this._startSAS}>
{_t("Verify by emoji")}
</AccessibleButton>
const noCommonMethodError = !showSAS && !showQR ?
<p>{_t("The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what Riot supports. Try with a different client.")}</p> :
null;
if (this.props.layout === 'dialog') {
// HACK: This is a terrible idea.
let qrBlock;
let sasBlock;
if (showQR) {
let qrCode;
if (this.state.qrCodeProps) {
qrCode = <VerificationQRCode {...this.state.qrCodeProps} />;
} else {
qrCode = <div className='mx_VerificationPanel_QRPhase_noQR'><Spinner /></div>;
}
qrBlock =
<div className='mx_VerificationPanel_QRPhase_startOption'>
<p>{_t("Scan this unique code")}</p>
{qrCode}
</div>;
}
if (showSAS) {
sasBlock =
<div className='mx_VerificationPanel_QRPhase_startOption'>
<p>{_t("Compare unique emoji")}</p>
<span className='mx_VerificationPanel_QRPhase_helpText'>{_t("Compare a unique set of emoji if you don't have a camera on either device")}</span>
<AccessibleButton disabled={this.state.emojiButtonClicked} onClick={this._startSAS} kind='primary'>
{_t("Start")}
</AccessibleButton>
</div>;
}
const or = qrBlock && sasBlock ?
<div className='mx_VerificationPanel_QRPhase_betweenText'>{_t("or")}</div> : null;
return (
<div>
{_t("Verify this session by completing one of the following:")}
<div className='mx_VerificationPanel_QRPhase_startOptions'>
{qrBlock}
{or}
{sasBlock}
{noCommonMethodError}
</div>
</div>
);
}
const cli = MatrixClientPeg.get();
const crossSigningInfo = cli.getStoredCrossSigningForUser(request.otherUserId);
if (!crossSigningInfo || !request.requestEvent || !request.requestEvent.getId()) {
// for whatever reason we can't generate a QR code, offer only SAS Verification
return <div className="mx_UserInfo_container">
<h3>Verify by emoji</h3>
<p>{_t("Verify by comparing unique emoji.")}</p>
{ button }
</div>;
}
const myKeyId = cli.getCrossSigningId();
const qrCodeKeys = [
[cli.getDeviceId(), cli.getDeviceEd25519Key()],
[myKeyId, myKeyId],
];
// TODO: add way to open camera to scan a QR code
return <React.Fragment>
<div className="mx_UserInfo_container">
<h3>Verify by scanning</h3>
let qrBlock;
if (this.state.qrCodeProps) {
qrBlock = <div className="mx_UserInfo_container">
<h3>{_t("Verify by scanning")}</h3>
<p>{_t("Ask %(displayName)s to scan your code:", {
displayName: member.displayName || member.name || member.userId,
})}</p>
<div className="mx_VerificationPanel_qrCode">
<VerificationQRCode
keyholderUserId={MatrixClientPeg.get().getUserId()}
requestEventId={request.requestEvent.getId()}
otherUserKey={crossSigningInfo.getId("master")}
secret={request.encodedSharedSecret}
keys={qrCodeKeys}
/>
<VerificationQRCode {...this.state.qrCodeProps} />
</div>
</div>
<div className="mx_UserInfo_container">
<h3>Verify by emoji</h3>
<p>{_t("If you can't scan the code above, verify by comparing unique emoji.")}</p>
</div>;
}
let sasBlock;
if (showSAS) {
let button;
if (pending) {
button = <Spinner />;
} else {
const disabled = this.state.emojiButtonClicked;
button = (
<AccessibleButton disabled={disabled} kind="primary" className="mx_UserInfo_wideButton" onClick={this._startSAS}>
{_t("Verify by emoji")}
</AccessibleButton>
);
}
const sasLabel = this.state.qrCodeProps ?
_t("If you can't scan the code above, verify by comparing unique emoji.") :
_t("Verify by comparing unique emoji.");
sasBlock = <div className="mx_UserInfo_container">
<h3>{_t("Verify by emoji")}</h3>
<p>{sasLabel}</p>
{ button }
</div>
</div>;
}
const noCommonMethodBlock = noCommonMethodError ?
<div className="mx_UserInfo_container">{noCommonMethodError}</div> :
null;
// TODO: add way to open camera to scan a QR code
return <React.Fragment>
{qrBlock}
{sasBlock}
{noCommonMethodBlock}
</React.Fragment>;
}
@ -125,7 +181,7 @@ export default class VerificationPanel extends React.PureComponent {
<p>{_t("You've successfully verified %(displayName)s!", {
displayName: member.displayName || member.name || member.userId,
})}</p>
<E2EIcon isUser={true} status="verified" size={128} />
<E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
<p>Verify all users in a room to ensure it's secure.</p>
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
@ -196,6 +252,7 @@ export default class VerificationPanel extends React.PureComponent {
}
_startSAS = async () => {
this.setState({emojiButtonClicked: true});
const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS);
try {
await verifier.verify();
@ -233,7 +290,11 @@ export default class VerificationPanel extends React.PureComponent {
};
componentDidMount() {
this.props.request.on("change", this._onRequestChange);
const {request} = this.props;
request.on("change", this._onRequestChange);
if (request.verifier) {
this.setState({sasEvent: request.verifier.sasEvent});
}
this._onRequestChange();
}

View file

@ -74,7 +74,6 @@ export default class AliasSettings extends React.Component {
roomId: PropTypes.string.isRequired,
canSetCanonicalAlias: PropTypes.bool.isRequired,
canSetAliases: PropTypes.bool.isRequired,
aliasEvents: PropTypes.array, // [MatrixEvent]
canonicalAliasEvent: PropTypes.object, // MatrixEvent
};
@ -92,14 +91,9 @@ export default class AliasSettings extends React.Component {
remoteDomains: [], // [ domain.com, foobar.com ]
canonicalAlias: null, // #canonical:domain.com
updatingCanonicalAlias: false,
localAliasesLoading: true,
};
const localDomain = MatrixClientPeg.get().getDomain();
state.domainToAliases = this.aliasEventsToDictionary(props.aliasEvents || []);
state.remoteDomains = Object.keys(state.domainToAliases).filter((domain) => {
return domain !== localDomain && state.domainToAliases[domain].length > 0;
});
if (props.canonicalAliasEvent) {
state.canonicalAlias = props.canonicalAliasEvent.getContent().alias;
}
@ -107,6 +101,46 @@ export default class AliasSettings extends React.Component {
this.state = state;
}
async componentWillMount() {
const cli = MatrixClientPeg.get();
try {
if (await cli.doesServerSupportUnstableFeature("org.matrix.msc2432")) {
const response = await cli.unstableGetLocalAliases(this.props.roomId);
const localAliases = response.aliases;
const localDomain = cli.getDomain();
const domainToAliases = Object.assign(
{},
// FIXME, any localhost alt_aliases will be ignored as they are overwritten by localAliases
this.aliasesToDictionary(this._getAltAliases()),
{[localDomain]: localAliases || []},
);
const remoteDomains = Object.keys(domainToAliases).filter((domain) => {
return domain !== localDomain && domainToAliases[domain].length > 0;
});
this.setState({ domainToAliases, remoteDomains });
} else {
const state = {};
const localDomain = cli.getDomain();
state.domainToAliases = this.aliasEventsToDictionary(this.props.aliasEvents || []);
state.remoteDomains = Object.keys(state.domainToAliases).filter((domain) => {
return domain !== localDomain && state.domainToAliases[domain].length > 0;
});
this.setState(state);
}
} finally {
this.setState({localAliasesLoading: false});
}
}
aliasesToDictionary(aliases) {
return aliases.reduce((dict, alias) => {
const domain = alias.split(":")[1];
dict[domain] = dict[domain] || [];
dict[domain].push(alias);
return dict;
}, {});
}
aliasEventsToDictionary(aliasEvents) { // m.room.alias events
const dict = {};
aliasEvents.forEach((event) => {
@ -117,6 +151,16 @@ export default class AliasSettings extends React.Component {
return dict;
}
_getAltAliases() {
if (this.props.canonicalAliasEvent) {
const altAliases = this.props.canonicalAliasEvent.getContent().alt_aliases;
if (Array.isArray(altAliases)) {
return altAliases;
}
}
return [];
}
changeCanonicalAlias(alias) {
if (!this.props.canSetCanonicalAlias) return;
@ -126,6 +170,8 @@ export default class AliasSettings extends React.Component {
});
const eventContent = {};
const altAliases = this._getAltAliases();
if (altAliases) eventContent["alt_aliases"] = altAliases;
if (alias) eventContent["alias"] = alias;
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.canonical_alias",
@ -261,26 +307,34 @@ export default class AliasSettings extends React.Component {
);
}
let localAliasesList;
if (this.state.localAliasesLoading) {
const Spinner = sdk.getComponent("elements.Spinner");
localAliasesList = <Spinner />;
} else {
localAliasesList = <EditableAliasesList
id="roomAliases"
className={"mx_RoomSettings_localAliases"}
items={this.state.domainToAliases[localDomain] || []}
newItem={this.state.newAlias}
onNewItemChanged={this.onNewAliasChanged}
canRemove={this.props.canSetAliases}
canEdit={this.props.canSetAliases}
onItemAdded={this.onLocalAliasAdded}
onItemRemoved={this.onLocalAliasDeleted}
itemsLabel={_t('Local addresses for this room:')}
noItemsLabel={_t('This room has no local addresses')}
placeholder={_t(
'New address (e.g. #foo:%(localDomain)s)', {localDomain: localDomain},
)}
domain={localDomain}
/>;
}
return (
<div className='mx_AliasSettings'>
{canonicalAliasSection}
<EditableAliasesList
id="roomAliases"
className={"mx_RoomSettings_localAliases"}
items={this.state.domainToAliases[localDomain] || []}
newItem={this.state.newAlias}
onNewItemChanged={this.onNewAliasChanged}
canRemove={this.props.canSetAliases}
canEdit={this.props.canSetAliases}
onItemAdded={this.onLocalAliasAdded}
onItemRemoved={this.onLocalAliasDeleted}
itemsLabel={_t('Local addresses for this room:')}
noItemsLabel={_t('This room has no local addresses')}
placeholder={_t(
'New address (e.g. #foo:%(localDomain)s)', {localDomain: localDomain},
)}
domain={localDomain}
/>
{localAliasesList}
{remoteAliasesSection}
</div>
);

View file

@ -219,7 +219,7 @@ export default createReactClass({
if (link) {
span = (
<a href={link} target="_blank" rel="noopener">
<a href={link} target="_blank" rel="noreferrer noopener">
{ span }
</a>
);

View file

@ -392,6 +392,20 @@ export default class BasicMessageEditor extends React.Component {
} else if (event.key === Key.ENTER && (event.shiftKey || (IS_MAC && event.altKey))) {
this._insertText("\n");
handled = true;
// move selection to start of composer
} else if (modKey && event.key === Key.HOME) {
setSelection(this._editorRef, model, {
index: 0,
offset: 0,
});
handled = true;
// move selection to end of composer
} else if (modKey && event.key === Key.END) {
setSelection(this._editorRef, model, {
index: model.parts.length - 1,
offset: model.parts[model.parts.length - 1].text.length,
});
handled = true;
// autocomplete or enter to send below shouldn't have any modifier keys pressed.
} else {
const metaOrAltPressed = event.metaKey || event.altKey;
@ -490,6 +504,7 @@ export default class BasicMessageEditor extends React.Component {
}
componentWillUnmount() {
document.removeEventListener("selectionchange", this._onSelectionChange);
this._editorRef.removeEventListener("input", this._onInput, true);
this._editorRef.removeEventListener("compositionstart", this._onCompositionStart, true);
this._editorRef.removeEventListener("compositionend", this._onCompositionEnd, true);

View file

@ -51,7 +51,7 @@ const legacyRoomTitles = {
[E2E_STATE.VERIFIED]: _td("All sessions in this encrypted room are trusted"),
};
const E2EIcon = ({isUser, status, className, size, onClick}) => {
const E2EIcon = ({isUser, status, className, size, onClick, hideTooltip}) => {
const [hover, setHover] = useState(false);
const classes = classNames({
@ -82,7 +82,7 @@ const E2EIcon = ({isUser, status, className, size, onClick}) => {
const onMouseOut = () => setHover(false);
let tip;
if (hover) {
if (hover && !hideTooltip) {
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} />;
}

View file

@ -136,7 +136,7 @@ export default createReactClass({
<div className="mx_LinkPreviewWidget" >
{ img }
<div className="mx_LinkPreviewWidget_caption">
<div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noopener">{ p["og:title"] }</a></div>
<div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noreferrer noopener">{ p["og:title"] }</a></div>
<div className="mx_LinkPreviewWidget_siteName">{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }</div>
<div className="mx_LinkPreviewWidget_description" ref={this._description}>
{ description }

View file

@ -341,7 +341,7 @@ export default class MessageComposer extends React.Component {
</a>
) : '';
controls.push(<div className="mx_MessageComposer_replaced_wrapper">
controls.push(<div className="mx_MessageComposer_replaced_wrapper" key="room_replaced">
<div className="mx_MessageComposer_replaced_valign">
<img className="mx_MessageComposer_roomReplaced_icon" src={require("../../../../res/img/room_replaced.svg")} />
<span className="mx_MessageComposer_roomReplaced_header">

View file

@ -314,7 +314,7 @@ export default createReactClass({
return (
<div className="mx_RoomHeader light-panel">
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_wrapper" aria-owns="mx_RightPanel">
<div className="mx_RoomHeader_avatar">{ roomAvatar }{ e2eIcon }</div>
{ privateIcon }
{ name }

View file

@ -509,7 +509,7 @@ export default createReactClass({
"<issueLink>submit a bug report</issueLink>.",
{ errcode: this.props.error.errcode },
{ issueLink: label => <a href="https://github.com/vector-im/riot-web/issues/new/choose"
target="_blank" rel="noopener">{ label }</a> },
target="_blank" rel="noreferrer noopener">{ label }</a> },
),
];
break;

View file

@ -166,7 +166,9 @@ export default createReactClass({
});
/* Check all verified user devices. */
for (const userId of [...verified, cli.getUserId()]) {
/* Don't alarm if no other users are verified */
const targets = (verified.length > 0) ? [...verified, cli.getUserId()] : verified;
for (const userId of targets) {
const devices = await cli.getStoredDevicesForUser(userId);
const allDevicesVerified = devices.every(({deviceId}) => {
return cli.checkDeviceTrust(userId, deviceId).isVerified();

View file

@ -213,7 +213,7 @@ export default createReactClass({
}
return (
<li className="mx_WhoIsTypingTile">
<li className="mx_WhoIsTypingTile" aria-atomic="true">
<div className="mx_WhoIsTypingTile_avatars">
{ this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
</div>

View file

@ -119,7 +119,7 @@ export default createReactClass({
'In future this will be improved.',
) }
{' '}
<a href="https://github.com/vector-im/riot-web/issues/2671" target="_blank" rel="noopener">
<a href="https://github.com/vector-im/riot-web/issues/2671" target="_blank" rel="noreferrer noopener">
https://github.com/vector-im/riot-web/issues/2671
</a>
</div>,
@ -253,20 +253,24 @@ export default createReactClass({
<form className={this.props.className} onSubmit={this.onClickChange}>
{ currentPassword }
<div className={rowClassName}>
<Field id="mx_ChangePassword_newPassword"
<Field
id="mx_ChangePassword_newPassword"
type="password"
label={passwordLabel}
value={this.state.newPassword}
autoFocus={this.props.autoFocusNewPasswordInput}
onChange={this.onChangeNewPassword}
autoComplete="new-password"
/>
</div>
<div className={rowClassName}>
<Field id="mx_ChangePassword_newPasswordConfirm"
<Field
id="mx_ChangePassword_newPasswordConfirm"
type="password"
label={_t("Confirm password")}
value={this.state.newPasswordConfirm}
onChange={this.onChangeNewPasswordConfirm}
autoComplete="new-password"
/>
</div>
<AccessibleButton className={buttonClassName} kind={this.props.buttonKind} onClick={this.onClickChange}>

View file

@ -20,6 +20,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import { accessSecretStorage } from '../../../CrossSigningManager';
import Modal from '../../../Modal';
export default class CrossSigningPanel extends React.PureComponent {
constructor(props) {
@ -86,11 +87,12 @@ export default class CrossSigningPanel extends React.PureComponent {
* 2. Access existing secret storage by requesting passphrase and accessing
* cross-signing keys as needed.
* 3. All keys are loaded and there's nothing to do.
* @param {bool} [force] Bootstrap again even if keys already present
*/
_bootstrapSecureSecretStorage = async () => {
_bootstrapSecureSecretStorage = async (force=false) => {
this.setState({ error: null });
try {
await accessSecretStorage();
await accessSecretStorage(() => undefined, force);
} catch (e) {
this.setState({ error: e });
console.error("Error bootstrapping secret storage", e);
@ -99,6 +101,18 @@ export default class CrossSigningPanel extends React.PureComponent {
this._getUpdatedStatus();
}
onDestroyStorage = (act) => {
if (!act) return;
this._bootstrapSecureSecretStorage(true);
}
_destroySecureSecretStorage = () => {
const ConfirmDestoryCrossSigningDialog = sdk.getComponent("dialogs.ConfirmDestroyCrossSigningDialog");
Modal.createDialog(ConfirmDestoryCrossSigningDialog, {
onFinished: this.onDestroyStorage,
});
}
render() {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const {
@ -114,13 +128,12 @@ export default class CrossSigningPanel extends React.PureComponent {
}
const enabled = (
crossSigningPublicKeysOnDevice &&
crossSigningPrivateKeysInStorage &&
secretStorageKeyInAccount
);
let summarisedStatus;
if (enabled) {
if (enabled && crossSigningPublicKeysOnDevice) {
summarisedStatus = <p> {_t(
"Cross-signing and secret storage are enabled.",
)}</p>;
@ -142,6 +155,12 @@ export default class CrossSigningPanel extends React.PureComponent {
{_t("Bootstrap cross-signing and secret storage")}
</AccessibleButton>
</div>;
} else {
bootstrapButton = <div className="mx_CrossSigningPanel_buttonRow">
<AccessibleButton kind="danger" onClick={this._destroySecureSecretStorage}>
{_t("Reset cross-signing and secret storage")}
</AccessibleButton>
</div>;
}
return (

View file

@ -37,21 +37,29 @@ export default class EventIndexPanel extends React.Component {
};
}
async updateCurrentRoom(room) {
updateCurrentRoom = async (room) => {
const eventIndex = EventIndexPeg.get();
const stats = await eventIndex.getStats();
let stats;
try {
stats = await eventIndex.getStats();
} catch {
// This call may fail if sporadically, not a huge issue as we will
// try later again and probably succeed.
return;
}
this.setState({
eventIndexSize: stats.size,
roomCount: stats.roomCount,
});
}
};
componentWillUnmount(): void {
const eventIndex = EventIndexPeg.get();
if (eventIndex !== null) {
eventIndex.removeListener("changedCheckpoint", this.updateCurrentRoom.bind(this));
eventIndex.removeListener("changedCheckpoint", this.updateCurrentRoom);
}
}
@ -68,11 +76,17 @@ export default class EventIndexPanel extends React.Component {
let roomCount = 0;
if (eventIndex !== null) {
eventIndex.on("changedCheckpoint", this.updateCurrentRoom.bind(this));
eventIndex.on("changedCheckpoint", this.updateCurrentRoom);
const stats = await eventIndex.getStats();
eventIndexSize = stats.size;
roomCount = stats.roomCount;
try {
const stats = await eventIndex.getStats();
eventIndexSize = stats.size;
roomCount = stats.roomCount;
} catch {
// This call may fail if sporadically, not a huge issue as we
// will try later again in the updateCurrentRoom call and
// probably succeed.
}
}
this.setState({
@ -158,7 +172,7 @@ export default class EventIndexPanel extends React.Component {
{},
{
'nativeLink': (sub) => <a href={nativeLink} target="_blank"
rel="noopener">{sub}</a>,
rel="noreferrer noopener">{sub}</a>,
},
)
}
@ -174,7 +188,7 @@ export default class EventIndexPanel extends React.Component {
{},
{
'riotLink': (sub) => <a href="https://riot.im/download/desktop"
target="_blank" rel="noopener">{sub}</a>,
target="_blank" rel="noreferrer noopener">{sub}</a>,
},
)
}

View file

@ -132,10 +132,10 @@ export default class ProfileSettings extends React.Component {
{_t(
"<a>Upgrade</a> to your own domain", {},
{
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener">{sub}</a>,
a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{sub}</a>,
},
)}
<a href={hostingSignupLink} target="_blank" rel="noopener">
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a>
</span>;

View file

@ -68,7 +68,7 @@ export default class BridgeSettingsTab extends React.Component {
{
// TODO: We don't have this link yet: this will prevent the translators
// having to re-translate the string when we do.
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noopener">{sub}</a>,
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{sub}</a>,
},
)}</p>
<ul className="mx_RoomSettingsDialog_BridgeList">
@ -82,7 +82,7 @@ export default class BridgeSettingsTab extends React.Component {
{
// TODO: We don't have this link yet: this will prevent the translators
// having to re-translate the string when we do.
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noopener">{sub}</a>,
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{sub}</a>,
},
)}</p>;
}

View file

@ -107,20 +107,20 @@ export default class RolesRoomSettingsTab extends React.Component {
};
componentDidMount(): void {
MatrixClientPeg.get().on("RoomState.members", this._onRoomMembership.bind(this));
MatrixClientPeg.get().on("RoomState.members", this._onRoomMembership);
}
componentWillUnmount(): void {
const client = MatrixClientPeg.get();
if (client) {
client.removeListener("RoomState.members", this._onRoomMembership.bind(this));
client.removeListener("RoomState.members", this._onRoomMembership);
}
}
_onRoomMembership(event, state, member) {
_onRoomMembership = (event, state, member) => {
if (state.roomId !== this.props.roomId) return;
this.forceUpdate();
}
};
_populateDefaultPlEvents(eventsSection, stateLevel, eventsLevel) {
for (const desiredEvent of Object.keys(plEventsToShow)) {

View file

@ -36,11 +36,12 @@ export default class SecurityRoomSettingsTab extends React.Component {
joinRule: "invite",
guestAccess: "can_join",
history: "shared",
hasAliases: false,
encrypted: false,
};
}
componentWillMount(): void {
async componentWillMount(): void {
MatrixClientPeg.get().on("RoomState.events", this._onStateEvent);
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
@ -63,6 +64,8 @@ export default class SecurityRoomSettingsTab extends React.Component {
);
const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
this.setState({joinRule, guestAccess, history, encrypted});
const hasAliases = await this._hasAliases();
this.setState({hasAliases});
}
_pullContentPropertyFromEvent(event, key, defaultValue) {
@ -94,7 +97,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
{},
{
'a': (sub) => {
return <a rel='noopener' target='_blank'
return <a rel='noreferrer noopener' target='_blank'
href='https://about.riot.im/help#end-to-end-encryption'>{sub}</a>;
},
},
@ -201,13 +204,25 @@ export default class SecurityRoomSettingsTab extends React.Component {
MatrixClientPeg.get().getRoom(this.props.roomId).setBlacklistUnverifiedDevices(checked);
};
async _hasAliases() {
const cli = MatrixClientPeg.get();
if (await cli.doesServerSupportUnstableFeature("org.matrix.msc2432")) {
const response = await cli.unstableGetLocalAliases(this.props.roomId);
const localAliases = response.aliases;
return Array.isArray(localAliases) && localAliases.length !== 0;
} else {
const room = cli.getRoom(this.props.roomId);
const aliasEvents = room.currentState.getStateEvents("m.room.aliases") || [];
const hasAliases = !!aliasEvents.find((ev) => (ev.getContent().aliases || []).length > 0);
return hasAliases;
}
}
_renderRoomAccess() {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const joinRule = this.state.joinRule;
const guestAccess = this.state.guestAccess;
const aliasEvents = room.currentState.getStateEvents("m.room.aliases") || [];
const hasAliases = !!aliasEvents.find((ev) => (ev.getContent().aliases || []).length > 0);
const canChangeAccess = room.currentState.mayClientSendStateEvent("m.room.join_rules", client)
&& room.currentState.mayClientSendStateEvent("m.room.guest_access", client);
@ -226,7 +241,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
}
let aliasWarning = null;
if (joinRule === 'public' && !hasAliases) {
if (joinRule === 'public' && !this.state.hasAliases) {
aliasWarning = (
<div className='mx_SecurityRoomSettingsTab_warning'>
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />

View file

@ -37,7 +37,7 @@ const ghVersionLabel = function(repo, token='') {
} else {
url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
}
return <a target="_blank" rel="noopener" href={url}>{ token }</a>;
return <a target="_blank" rel="noreferrer noopener" href={url}>{ token }</a>;
};
export default class HelpUserSettingsTab extends React.Component {
@ -110,7 +110,7 @@ export default class HelpUserSettingsTab extends React.Component {
const legalLinks = [];
for (const tocEntry of SdkConfig.get().terms_and_conditions_links) {
legalLinks.push(<div key={tocEntry.url}>
<a href={tocEntry.url} rel="noopener" target="_blank">{tocEntry.text}</a>
<a href={tocEntry.url} rel="noreferrer noopener" target="_blank">{tocEntry.text}</a>
</div>);
}
@ -132,27 +132,27 @@ export default class HelpUserSettingsTab extends React.Component {
<span className='mx_SettingsTab_subheading'>{_t("Credits")}</span>
<ul>
<li>
The <a href="themes/riot/img/backgrounds/valley.jpg" rel="noopener" target="_blank">
The <a href="themes/riot/img/backgrounds/valley.jpg" rel="noreferrer noopener" target="_blank">
default cover photo</a> is ©&nbsp;
<a href="https://www.flickr.com/golan" rel="noopener" target="_blank">Jesús Roncero</a>{' '}
<a href="https://www.flickr.com/golan" rel="noreferrer noopener" target="_blank">Jesús Roncero</a>{' '}
used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noopener" target="_blank">
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noreferrer noopener" target="_blank">
CC-BY-SA 4.0</a>.
</li>
<li>
The <a href="https://github.com/matrix-org/twemoji-colr" rel="noopener" target="_blank">
twemoji-colr</a> font is ©&nbsp;
<a href="https://mozilla.org" rel="noopener" target="_blank">Mozilla Foundation</a>{' '}
The <a href="https://github.com/matrix-org/twemoji-colr" rel="noreferrer noopener"
target="_blank"> twemoji-colr</a> font is ©&nbsp;
<a href="https://mozilla.org" rel="noreferrer noopener" target="_blank">Mozilla Foundation</a>{' '}
used under the terms of&nbsp;
<a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noopener" target="_blank">
<a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener" target="_blank">
Apache 2.0</a>.
</li>
<li>
The <a href="https://twemoji.twitter.com/" rel="noopener" target="_blank">
The <a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank">
Twemoji</a> emoji art is ©&nbsp;
<a href="https://twemoji.twitter.com/" rel="noopener" target="_blank">Twitter, Inc and other
<a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank">Twitter, Inc and other
contributors</a> used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by/4.0/" rel="noopener" target="_blank">
<a href="https://creativecommons.org/licenses/by/4.0/" rel="noreferrer noopener" target="_blank">
CC-BY 4.0</a>.
</li>
</ul>
@ -162,7 +162,8 @@ export default class HelpUserSettingsTab extends React.Component {
render() {
let faqText = _t('For help with using Riot, click <a>here</a>.', {}, {
'a': (sub) => <a href="https://about.riot.im/need-help/" rel='noopener' target='_blank'>{sub}</a>,
'a': (sub) =>
<a href="https://about.riot.im/need-help/" rel='noreferrer noopener' target='_blank'>{sub}</a>,
});
if (SdkConfig.get().welcomeUserId && getCurrentLanguage().startsWith('en')) {
faqText = (
@ -170,7 +171,7 @@ export default class HelpUserSettingsTab extends React.Component {
{
_t('For help with using Riot, click <a>here</a> or start a chat with our ' +
'bot using the button below.', {}, {
'a': (sub) => <a href="https://about.riot.im/need-help/" rel='noopener'
'a': (sub) => <a href="https://about.riot.im/need-help/" rel='noreferrer noopener'
target='_blank'>{sub}</a>,
})
}

View file

@ -55,7 +55,7 @@ export default class LabsUserSettingsTab extends React.Component {
'<a>Learn more</a>.', {}, {
'a': (sub) => {
return <a href="https://github.com/vector-im/riot-web/blob/develop/docs/labs.md"
rel='noopener' target='_blank'>{sub}</a>;
rel='noreferrer noopener' target='_blank'>{sub}</a>;
},
})
}

View file

@ -32,6 +32,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
];
static TIMELINE_SETTINGS = [
'showTypingNotifications',
'autoplayGifsAndVideos',
'urlPreviewsEnabled',
'TextualBody.enableBigEmoji',

View file

@ -77,7 +77,7 @@ export default class InlineTermsAgreement extends React.Component {
"Accept <policyLink /> to continue:", {}, {
policyLink: () => {
return (
<a href={policy.url} rel='noopener' target='_blank'>
<a href={policy.url} rel='noreferrer noopener' target='_blank'>
{policy.name}
<span className='mx_InlineTermsAgreement_link' />
</a>

View file

@ -58,10 +58,7 @@ export default class VerificationRequestToast extends React.PureComponent {
_checkRequestIsPending = () => {
const {request} = this.props;
const isPendingInRoomRequest = request.channel.roomId &&
!(request.ready || request.started || request.done || request.cancelled || request.observeOnly);
const isPendingDeviceRequest = request.channel.deviceId && request.started;
if (!isPendingInRoomRequest && !isPendingDeviceRequest) {
if (!request.canAccept) {
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
}
};
@ -79,15 +76,15 @@ export default class VerificationRequestToast extends React.PureComponent {
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
const {request} = this.props;
// no room id for to_device requests
const cli = MatrixClientPeg.get();
try {
await request.accept();
if (request.channel.roomId) {
dis.dispatch({
action: 'view_room',
room_id: request.channel.roomId,
should_peek: false,
});
await request.accept();
const cli = MatrixClientPeg.get();
dis.dispatch({
action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
@ -96,11 +93,10 @@ export default class VerificationRequestToast extends React.PureComponent {
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,
} else {
const VerificationRequestDialog = sdk.getComponent("views.dialogs.VerificationRequestDialog");
Modal.createTrackedDialog('Incoming Verification', '', VerificationRequestDialog, {
verificationRequest: request,
}, null, /* priority = */ false, /* static = */ true);
}
} catch (err) {

View file

@ -0,0 +1,82 @@
/*
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 { _t } from '../../../languageHandler';
import AccessibleButton from "../elements/AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import Spinner from "../elements/Spinner";
@replaceableComponent("views.verification.VerificationQREmojiOptions")
export default class VerificationQREmojiOptions extends React.Component {
static propTypes = {
request: PropTypes.object.isRequired,
onCancel: PropTypes.func.isRequired,
onStartEmoji: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
qrProps: null,
};
this._prepareQrCode(props.request);
}
async _prepareQrCode(request: VerificationRequest) {
try {
const props = await VerificationQRCode.getPropsForRequest(request);
this.setState({qrProps: props});
} catch (e) {
console.error(e);
// We just won't show a QR code
}
}
render() {
let qrCode = <div className='mx_VerificationQREmojiOptions_noQR'><Spinner /></div>;
if (this.state.qrProps) {
qrCode = <VerificationQRCode {...this.state.qrProps} />;
}
return (
<div>
{_t("Verify this session by completing one of the following:")}
<div className='mx_IncomingSasDialog_startOptions'>
<div className='mx_IncomingSasDialog_startOption'>
<p>{_t("Scan this unique code")}</p>
{qrCode}
</div>
<div className='mx_IncomingSasDialog_betweenText'>{_t("or")}</div>
<div className='mx_IncomingSasDialog_startOption'>
<p>{_t("Compare unique emoji")}</p>
<span className='mx_IncomingSasDialog_helpText'>{_t("Compare a unique set of emoji if you don't have a camera on either device")}</span>
<AccessibleButton onClick={this.props.onStartEmoji} kind='primary'>
{_t("Start")}
</AccessibleButton>
</div>
</div>
<AccessibleButton onClick={this.props.onCancel} kind='danger'>
{_t("Cancel")}
</AccessibleButton>
</div>
);
}
}