Merge branch 'develop' into feature/padLockInviteOnly
This commit is contained in:
commit
d2e7cc7f19
138 changed files with 4634 additions and 1211 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -160,6 +160,7 @@ export default createReactClass({
|
|||
onKeyDown={ this._onKeyDown }
|
||||
onBlur={this._onBlur}
|
||||
placeholder={ placeholder }
|
||||
autoComplete="off"
|
||||
/>
|
||||
{ clearButton }
|
||||
</div>
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>,
|
||||
},
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}),
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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'>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
55
src/components/views/dialogs/VerificationRequestDialog.js
Normal file
55
src/components/views/dialogs/VerificationRequestDialog.js
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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>."
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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' />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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")} />
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) : ""} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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>,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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 ©
|
||||
<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
|
||||
<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 ©
|
||||
<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 ©
|
||||
<a href="https://mozilla.org" rel="noreferrer noopener" target="_blank">Mozilla Foundation</a>{' '}
|
||||
used under the terms of
|
||||
<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 ©
|
||||
<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
|
||||
<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>,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
];
|
||||
|
||||
static TIMELINE_SETTINGS = [
|
||||
'showTypingNotifications',
|
||||
'autoplayGifsAndVideos',
|
||||
'urlPreviewsEnabled',
|
||||
'TextualBody.enableBigEmoji',
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue