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

This commit is contained in:
Michael Telatynski 2020-02-19 13:30:47 +00:00
commit 4278d44059
25 changed files with 1264 additions and 390 deletions

View file

@ -57,6 +57,8 @@ function getRedactedUrl() {
}
const customVariables = {
// The Matomo installation at https://matomo.riot.im is currently configured
// with a limit of 10 custom variables.
'App Platform': {
id: 1,
expl: _td('The platform you\'re on'),
@ -64,7 +66,7 @@ const customVariables = {
},
'App Version': {
id: 2,
expl: _td('The version of Riot.im'),
expl: _td('The version of Riot'),
example: '15.0.0',
},
'User Type': {
@ -87,20 +89,25 @@ const customVariables = {
expl: _td('Whether or not you\'re using the Richtext mode of the Rich Text Editor'),
example: 'off',
},
'Breadcrumbs': {
id: 9,
expl: _td("Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)"),
example: 'disabled',
},
'Homeserver URL': {
id: 7,
expl: _td('Your homeserver\'s URL'),
example: 'https://matrix.org',
},
'Identity Server URL': {
'Touch Input': {
id: 8,
expl: _td('Your identity server\'s URL'),
example: 'https://vector.im',
expl: _td("Whether you're using Riot on a device where touch is the primary input mechanism"),
example: 'false',
},
'Breadcrumbs': {
id: 9,
expl: _td("Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)"),
example: 'disabled',
},
'Installed PWA': {
id: 10,
expl: _td("Whether you're using Riot as an installed Progressive Web App"),
example: 'false',
},
};
@ -190,6 +197,20 @@ class Analytics {
this._setVisitVariable('Instance', window.location.pathname);
}
let installedPWA = "unknown";
try {
// Known to work at least for desktop Chrome
installedPWA = window.matchMedia('(display-mode: standalone)').matches;
} catch (e) { }
this._setVisitVariable('Installed PWA', installedPWA);
let touchInput = "unknown";
try {
// MDN claims broad support across browsers
touchInput = window.matchMedia('(pointer: coarse)').matches;
} catch (e) { }
this._setVisitVariable('Touch Input', touchInput);
// start heartbeat
this._heartbeatIntervalID = window.setInterval(this.ping.bind(this), HEARTBEAT_INTERVAL);
}
@ -291,11 +312,9 @@ class Analytics {
if (!config.piwik) return;
const whitelistedHSUrls = config.piwik.whitelistedHSUrls || [];
const whitelistedISUrls = config.piwik.whitelistedISUrls || [];
this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
this._setVisitVariable('Identity Server URL', whitelistRedact(whitelistedISUrls, identityServerUrl));
}
setBreadcrumbs(state) {
@ -328,7 +347,7 @@ class Analytics {
},
),
},
{ expl: _td('Your User Agent'), value: navigator.userAgent },
{ expl: _td('Your user agent'), value: navigator.userAgent },
{ expl: _td('Your device resolution'), value: resolution },
];
@ -337,7 +356,7 @@ class Analytics {
title: _t('Analytics'),
description: <div className="mx_AnalyticsModal">
<div>
{ _t('The information being sent to us to help make Riot.im better includes:') }
{ _t('The information being sent to us to help make Riot better includes:') }
</div>
<table>
{ rows.map((row) => <tr key={row[0]}>

View file

@ -435,7 +435,7 @@ async function _doSetLoggedIn(credentials, clearStorage) {
}
}
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl, credentials.identityServerUrl);
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
if (localStorage) {
try {

View file

@ -413,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;
@ -458,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;
@ -961,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

@ -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';
@ -535,11 +535,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;
}

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,7 +286,11 @@ 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>
);

View file

@ -59,7 +59,6 @@ export default class MKeyVerificationRequest extends React.Component {
};
_onAcceptClicked = async () => {
this.setState({acceptOrCancelClicked: true});
const request = this.props.mxEvent.verificationRequest;
if (request) {
try {
@ -72,7 +71,6 @@ export default class MKeyVerificationRequest extends React.Component {
};
_onRejectClicked = async () => {
this.setState({acceptOrCancelClicked: true});
const request = this.props.mxEvent.verificationRequest;
if (request) {
try {
@ -96,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())});
}
}
}
@ -118,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>);
}
@ -137,11 +149,10 @@ 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) {
const disabled = this.state.acceptOrCancelClicked;
if (request.canAccept) {
stateNode = (<div className="mx_cryptoEvent_buttons">
<FormButton disabled={disabled} kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
<FormButton disabled={disabled} onClick={this._onAcceptClicked} label={_t("Accept")} />
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
<FormButton onClick={this._onAcceptClicked} label={_t("Accept")} />
</div>);
}
} else { // request sent by us

View file

@ -23,7 +23,7 @@ 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";
@ -69,9 +69,10 @@ const EncryptionPanel = ({verificationRequest, member, onClose, layout}) => {
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 {

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';
@ -169,10 +169,19 @@ async function verifyDevice(userId, device) {
}
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,6 +19,8 @@ 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 {_t} from "../../../languageHandler";
import E2EIcon from "../rooms/E2EIcon";
@ -54,7 +56,9 @@ export default class VerificationPanel extends React.PureComponent {
qrCodeProps: null, // generated by the VerificationQRCode component itself
};
this._hasVerifier = false;
this._generateQRCodeProps(props.request);
if (this.props.request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD)) {
this._generateQRCodeProps(props.request);
}
}
async _generateQRCodeProps(verificationRequest: VerificationRequest) {
@ -67,59 +71,60 @@ export default class VerificationPanel extends React.PureComponent {
}
renderQRPhase(pending) {
const {member} = this.props;
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');
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 qrCode = <div className='mx_VerificationPanel_QRPhase_noQR'><Spinner /></div>;
if (this.state.qrCodeProps) {
qrCode = <VerificationQRCode {...this.state.qrCodeProps} />;
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'>
<div className='mx_VerificationPanel_QRPhase_startOption'>
<p>{_t("Scan this unique code")}</p>
{qrCode}
</div>
<div className='mx_VerificationPanel_QRPhase_betweenText'>{_t("or")}</div>
<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>
{qrBlock}
{or}
{sasBlock}
{noCommonMethodError}
</div>
</div>
);
}
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>
);
}
if (!this.state.qrCodeProps) {
return <div className="mx_UserInfo_container">
<h3>{_t("Verify by emoji")}</h3>
<p>{_t("Verify by comparing unique emoji.")}</p>
{ button }
</div>;
}
// TODO: add way to open camera to scan a QR code
return <React.Fragment>
<div className="mx_UserInfo_container">
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,
@ -128,14 +133,41 @@ export default class VerificationPanel extends React.PureComponent {
<div className="mx_VerificationPanel_qrCode">
<VerificationQRCode {...this.state.qrCodeProps} />
</div>
</div>
</div>;
}
<div className="mx_UserInfo_container">
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>{_t("If you can't scan the code above, verify by comparing unique emoji.")}</p>
<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>;
}
@ -258,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
};
@ -94,12 +93,6 @@ export default class AliasSettings extends React.Component {
updatingCanonicalAlias: false,
};
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 +100,42 @@ export default class AliasSettings extends React.Component {
this.state = state;
}
async componentWillMount() {
const cli = MatrixClientPeg.get();
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);
}
}
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 +146,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 +165,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",

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) {
@ -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

@ -58,7 +58,7 @@ export default class VerificationRequestToast extends React.PureComponent {
_checkRequestIsPending = () => {
const {request} = this.props;
if (request.ready || request.done || request.cancelled || request.observeOnly) {
if (!request.canAccept) {
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
}
};

View file

@ -23,6 +23,7 @@ import dis from "./dispatcher";
import * as Rooms from "./Rooms";
import DMRoomMap from "./utils/DMRoomMap";
import {getAddressType} from "./UserAddress";
import SettingsStore from "./settings/SettingsStore";
/**
* Create a new room, and switch to it.
@ -159,7 +160,7 @@ export default function createRoom(opts) {
});
}
export async function ensureDMExists(client, userId) {
export function findDMForUser(client, userId) {
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
const rooms = roomIds.map(id => client.getRoom(id));
const suitableDMRooms = rooms.filter(r => {
@ -169,12 +170,60 @@ export async function ensureDMExists(client, userId) {
}
return false;
});
let roomId;
if (suitableDMRooms.length) {
const room = suitableDMRooms[0];
roomId = room.roomId;
return suitableDMRooms[0];
}
}
/*
* Try to ensure the user is already in the megolm session before continuing
* NOTE: this assumes you've just created the room and there's not been an opportunity
* for other code to run, so we shouldn't miss RoomState.newMember when it comes by.
*/
export async function _waitForMember(client, roomId, userId, opts = { timeout: 1500 }) {
const { timeout } = opts;
let handler;
return new Promise((resolve) => {
handler = function(_event, _roomstate, member) {
if (member.userId !== userId) return;
if (member.roomId !== roomId) return;
resolve(true);
};
client.on("RoomState.newMember", handler);
/* We don't want to hang if this goes wrong, so we proceed and hope the other
user is already in the megolm session */
setTimeout(resolve, timeout, false);
}).finally(() => {
client.removeListener("RoomState.newMember", handler);
});
}
/*
* Ensure that for every user in a room, there is at least one device that we
* can encrypt to.
*/
export async function canEncryptToAllUsers(client, userIds) {
const usersDeviceMap = await client.downloadKeys(userIds);
// { "@user:host": { "DEVICE": {...}, ... }, ... }
return Object.values(usersDeviceMap).every((userDevices) =>
// { "DEVICE": {...}, ... }
Object.keys(userDevices).length > 0,
);
}
export async function ensureDMExists(client, userId) {
const existingDMRoom = findDMForUser(client, userId);
let roomId;
if (existingDMRoom) {
roomId = existingDMRoom.roomId;
} else {
roomId = await createRoom({dmUserId: userId, spinner: false, andView: false});
let encryption;
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
encryption = canEncryptToAllUsers(client, [userId]);
}
roomId = await createRoom({encryption, dmUserId: userId, spinner: false, andView: false});
await _waitForMember(client, roomId, userId);
}
return roomId;
}

View file

@ -5,21 +5,22 @@
"Failed to verify email address: make sure you clicked the link in the email": "Failed to verify email address: make sure you clicked the link in the email",
"Add Phone Number": "Add Phone Number",
"The platform you're on": "The platform you're on",
"The version of Riot.im": "The version of Riot.im",
"The version of Riot": "The version of Riot",
"Whether or not you're logged in (we don't record your username)": "Whether or not you're logged in (we don't record your username)",
"Your language of choice": "Your language of choice",
"Which officially provided instance you are using, if any": "Which officially provided instance you are using, if any",
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Whether or not you're using the Richtext mode of the Rich Text Editor",
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)",
"Your homeserver's URL": "Your homeserver's URL",
"Your identity server's URL": "Your identity server's URL",
"Whether you're using Riot on a device where touch is the primary input mechanism": "Whether you're using Riot on a device where touch is the primary input mechanism",
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)",
"Whether you're using Riot as an installed Progressive Web App": "Whether you're using Riot as an installed Progressive Web App",
"e.g. %(exampleValue)s": "e.g. %(exampleValue)s",
"Every page you use in the app": "Every page you use in the app",
"e.g. <CurrentPageURL>": "e.g. <CurrentPageURL>",
"Your User Agent": "Your User Agent",
"Your user agent": "Your user agent",
"Your device resolution": "Your device resolution",
"Analytics": "Analytics",
"The information being sent to us to help make Riot.im better includes:": "The information being sent to us to help make Riot.im better includes:",
"The information being sent to us to help make Riot better includes:": "The information being sent to us to help make Riot better includes:",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.",
"Error": "Error",
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
@ -1198,11 +1199,12 @@
"This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.",
"Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.",
"Security": "Security",
"Verify by emoji": "Verify by emoji",
"Verify by comparing unique emoji.": "Verify by comparing unique emoji.",
"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.": "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.",
"Verify by scanning": "Verify by scanning",
"Ask %(displayName)s to scan your code:": "Ask %(displayName)s to scan your code:",
"Verify by emoji": "Verify by emoji",
"If you can't scan the code above, verify by comparing unique emoji.": "If you can't scan the code above, verify by comparing unique emoji.",
"Verify by comparing unique emoji.": "Verify by comparing unique emoji.",
"You've successfully verified %(displayName)s!": "You've successfully verified %(displayName)s!",
"Got it": "Got it",
"Verification timed out. Start verification again from their profile.": "Verification timed out. Start verification again from their profile.",
@ -1240,8 +1242,12 @@
"%(name)s cancelled verifying": "%(name)s cancelled verifying",
"You accepted": "You accepted",
"%(name)s accepted": "%(name)s accepted",
"You declined": "You declined",
"You cancelled": "You cancelled",
"%(name)s declined": "%(name)s declined",
"%(name)s cancelled": "%(name)s cancelled",
"accepting …": "accepting …",
"declining …": "declining …",
"%(name)s wants to verify": "%(name)s wants to verify",
"You sent a verification request": "You sent a verification request",
"Error decrypting video": "Error decrypting video",

View file

@ -67,6 +67,18 @@ export default async function sendBugReport(bugReportEndpoint, opts) {
userAgent = window.navigator.userAgent;
}
let installedPWA = "UNKNOWN";
try {
// Known to work at least for desktop Chrome
installedPWA = window.matchMedia('(display-mode: standalone)').matches;
} catch (e) { }
let touchInput = "UNKNOWN";
try {
// MDN claims broad support across browsers
touchInput = window.matchMedia('(pointer: coarse)').matches;
} catch (e) { }
const client = MatrixClientPeg.get();
console.log("Sending bug report.");
@ -76,6 +88,8 @@ export default async function sendBugReport(bugReportEndpoint, opts) {
body.append('app', 'riot-web');
body.append('version', version);
body.append('user_agent', userAgent);
body.append('installed_pwa', installedPWA);
body.append('touch_input', touchInput);
if (client) {
body.append('user_id', client.credentials.userId);

View file

@ -0,0 +1,12 @@
<html>
<head>
<!--
Hello! If you're reading this, perhaps you're wondering what this
file is doing and why your Riot is using it.
In short, this allows Riot to isolate potentially unsafe encrypted
attachments into their own origin, away from your Riot.
Stay curious!
-->
</head>
<body></body>
</html>

49
src/usercontent/index.js Normal file
View file

@ -0,0 +1,49 @@
const params = window.location.search.substring(1).split('&');
let lockOrigin;
for (let i = 0; i < params.length; ++i) {
const parts = params[i].split('=');
if (parts[0] === 'origin') lockOrigin = decodeURIComponent(parts[1]);
}
function remoteRender(event) {
const data = event.data;
const img = document.createElement("img");
img.id = "img";
img.src = data.imgSrc;
img.style = data.imgStyle;
const a = document.createElement("a");
a.id = "a";
a.rel = "noopener";
a.target = "_blank";
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);
}
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;
}
window.onmessage = function(e) {
if (e.origin === lockOrigin) {
if (e.data.blob) remoteRender(e);
else remoteSetTint(e);
}
};