Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/alpha_room_list
Conflicts: src/components/views/rooms/RoomList.js src/stores/RoomListStore.js
This commit is contained in:
commit
a1908a208e
104 changed files with 1810 additions and 680 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
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -1175,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);
|
||||
|
@ -1374,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);
|
||||
|
|
|
@ -873,6 +873,11 @@ class CreationGrouper {
|
|||
}
|
||||
|
||||
getTiles() {
|
||||
// If we don't have any events to group, don't even try to group them. The logic
|
||||
// below assumes that we have a group of events to deal with, but we might not if
|
||||
// the events we were supposed to group were redacted.
|
||||
if (!this.events || !this.events.length) return [];
|
||||
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
|
||||
|
||||
|
@ -959,6 +964,11 @@ class MemberGrouper {
|
|||
}
|
||||
|
||||
getTiles() {
|
||||
// If we don't have any events to group, don't even try to group them. The logic
|
||||
// below assumes that we have a group of events to deal with, but we might not if
|
||||
// the events we were supposed to group were redacted.
|
||||
if (!this.events || !this.events.length) return [];
|
||||
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
||||
|
||||
|
|
|
@ -342,7 +342,7 @@ export default createReactClass({
|
|||
peekLoading: false,
|
||||
});
|
||||
this._onRoomLoaded(room);
|
||||
}, (err) => {
|
||||
}).catch((err) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
@ -355,7 +355,7 @@ export default createReactClass({
|
|||
// This won't necessarily be a MatrixError, but we duck-type
|
||||
// here and say if it's got an 'errcode' key with the right value,
|
||||
// it means we can't peek.
|
||||
if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") {
|
||||
if (err.errcode === "M_GUEST_ACCESS_FORBIDDEN" || err.errcode === 'M_FORBIDDEN') {
|
||||
// This is fine: the room just isn't peekable (we assume).
|
||||
this.setState({
|
||||
peekLoading: false,
|
||||
|
@ -365,8 +365,6 @@ export default createReactClass({
|
|||
}
|
||||
});
|
||||
} else if (room) {
|
||||
//viewing a previously joined room, try to lazy load members
|
||||
|
||||
// Stop peeking because we have joined this room previously
|
||||
MatrixClientPeg.get().stopPeeking();
|
||||
this.setState({isPeeking: false});
|
||||
|
@ -460,8 +458,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());
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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>,
|
||||
},
|
||||
|
|
|
@ -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>,
|
||||
}),
|
||||
|
|
|
@ -90,7 +90,8 @@ export default createReactClass({
|
|||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', '');
|
||||
if (!pinnedEvent) return false;
|
||||
return pinnedEvent.getContent().pinned.includes(this.props.mxEvent.getId());
|
||||
const content = pinnedEvent.getContent();
|
||||
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
|
||||
},
|
||||
|
||||
onResendClick: function() {
|
||||
|
@ -420,7 +421,7 @@ export default createReactClass({
|
|||
onClick={this.onPermalinkClick}
|
||||
href={permalink}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
|
||||
? _t('Share Permalink') : _t('Share Message') }
|
||||
|
@ -445,7 +446,7 @@ export default createReactClass({
|
|||
element="a"
|
||||
className="mx_MessageContextMenu_field"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noreferrer noopener"
|
||||
onClick={this.closeMenu}
|
||||
href={mxEvent.event.content.external_url}
|
||||
>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -34,6 +34,7 @@ import {humanizeTime} from "../../../utils/humanize";
|
|||
import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore";
|
||||
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
|
@ -332,7 +333,23 @@ export default class InviteDialog extends React.PureComponent {
|
|||
}
|
||||
|
||||
_buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals();
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
||||
|
||||
// Also pull in all the rooms tagged as TAG_DM so we don't miss anything. Sometimes the
|
||||
// room list doesn't tag the room for the DMRoomMap, but does for the room list.
|
||||
const taggedRooms = RoomListStore.getRoomLists();
|
||||
const dmTaggedRooms = taggedRooms[TAG_DM];
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
for (const dmRoom of dmTaggedRooms) {
|
||||
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);
|
||||
for (const member of otherMembers) {
|
||||
if (rooms[member.userId]) continue; // already have a room
|
||||
|
||||
console.warn(`Adding DM room for ${member.userId} as ${dmRoom.roomId} from tag, not DM map`);
|
||||
rooms[member.userId] = dmRoom;
|
||||
}
|
||||
}
|
||||
|
||||
const recents = [];
|
||||
for (const userId in rooms) {
|
||||
// Filter out user IDs that are already in the room / should be excluded
|
||||
|
@ -512,9 +529,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);
|
||||
|
@ -544,9 +579,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 => {
|
||||
|
@ -573,7 +611,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) {
|
||||
|
@ -630,13 +670,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'],
|
||||
|
@ -645,6 +686,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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -769,7 +818,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) {
|
||||
|
@ -1014,7 +1063,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;
|
||||
|
@ -1023,12 +1072,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'
|
||||
|
@ -1045,7 +1099,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>
|
||||
|
|
|
@ -23,6 +23,7 @@ import VerificationRequestDialog from './VerificationRequestDialog';
|
|||
import BaseDialog from './BaseDialog';
|
||||
import DialogButtons from '../elements/DialogButtons';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
@replaceableComponent("views.dialogs.NewSessionReviewDialog")
|
||||
export default class NewSessionReviewDialog extends React.PureComponent {
|
||||
|
@ -33,7 +34,24 @@ export default class NewSessionReviewDialog extends React.PureComponent {
|
|||
}
|
||||
|
||||
onCancelClick = () => {
|
||||
this.props.onFinished(false);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog("Verification failed", "insecure", ErrorDialog, {
|
||||
headerImage: require("../../../../res/img/e2e/warning.svg"),
|
||||
title: _t("Your account is not secure"),
|
||||
description: <div>
|
||||
{_t("One of the following may be compromised:")}
|
||||
<ul>
|
||||
<li>{_t("Your password")}</li>
|
||||
<li>{_t("Your homeserver")}</li>
|
||||
<li>{_t("This session, or the other session")}</li>
|
||||
<li>{_t("The internet connection either session is using")}</li>
|
||||
</ul>
|
||||
<div>
|
||||
{_t("We recommend you change your password and recovery key in Settings immediately")}
|
||||
</div>
|
||||
</div>,
|
||||
onFinished: () => this.props.onFinished(false),
|
||||
});
|
||||
}
|
||||
|
||||
onContinueClick = async () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
@ -129,7 +128,7 @@ const Pill = createReactClass({
|
|||
const localRoom = resourceId[0] === '#' ?
|
||||
MatrixClientPeg.get().getRooms().find((r) => {
|
||||
return r.getCanonicalAlias() === resourceId ||
|
||||
r.getAliases().includes(resourceId);
|
||||
r.getAltAliases().includes(resourceId);
|
||||
}) : MatrixClientPeg.get().getRoom(resourceId);
|
||||
room = localRoom;
|
||||
if (!localRoom) {
|
||||
|
@ -237,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} aria-hidden="true" />;
|
||||
}
|
||||
pillClass = 'mx_RoomPill';
|
||||
}
|
||||
pillClass = 'mx_RoomPill';
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_GROUP_MENTION: {
|
||||
|
|
|
@ -17,89 +17,87 @@ 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
|
||||
otherUserDeviceKey: PropTypes.string, // Base64 key of the other user's device (or what we think it is; optional)
|
||||
requestEventId: PropTypes.string, // for DM verification only
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
action: "verify",
|
||||
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 async getPropsForRequest(verificationRequest: VerificationRequest) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const myUserId = cli.getUserId();
|
||||
const otherUserId = verificationRequest.otherUserId;
|
||||
const myDeviceId = cli.getDeviceId();
|
||||
const otherDevice = verificationRequest.targetDevice;
|
||||
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
|
||||
|
||||
const qrProps = {
|
||||
secret: verificationRequest.encodedSharedSecret,
|
||||
keyholderUserId: myUserId,
|
||||
action: "verify",
|
||||
keys: [], // array of pairs: keyId, base64Key
|
||||
otherUserKey: "", // base64key
|
||||
otherUserDeviceKey: "", // base64key
|
||||
requestEventId: "", // we figure this out in a moment
|
||||
};
|
||||
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 requestEvent = verificationRequest.requestEvent;
|
||||
qrProps.requestEventId = requestEvent.getId()
|
||||
const transactionId = requestEvent.getId()
|
||||
? requestEvent.getId()
|
||||
: ToDeviceChannel.getTransactionId(requestEvent);
|
||||
|
||||
// Populate the keys we need depending on which direction and users are involved in the verification.
|
||||
if (myUserId === otherUserId) {
|
||||
if (!otherDeviceId) {
|
||||
// Existing scanning New session's QR code
|
||||
qrProps.otherUserDeviceKey = null;
|
||||
} else {
|
||||
// New scanning Existing session's QR code
|
||||
const myDevices = (await cli.getStoredDevicesForUser(myUserId)) || [];
|
||||
const device = myDevices.find(d => d.deviceId === otherDeviceId);
|
||||
if (device) qrProps.otherUserDeviceKey = device.getFingerprint();
|
||||
}
|
||||
const qrProps = {
|
||||
prefix: BINARY_PREFIX,
|
||||
version: CODE_VERSION,
|
||||
mode,
|
||||
transactionId,
|
||||
firstKeyB64: '', // worked out shortly
|
||||
secondKeyB64: '', // worked out shortly
|
||||
secretB64: verificationRequest.encodedSharedSecret,
|
||||
};
|
||||
|
||||
// Either direction shares these next few props
|
||||
const myCrossSigningInfo = cli.getStoredCrossSigningForUser(myUserId);
|
||||
const myDevices = (await cli.getStoredDevicesForUser(myUserId)) || [];
|
||||
|
||||
const xsignInfo = cli.getStoredCrossSigningForUser(myUserId);
|
||||
qrProps.otherUserKey = xsignInfo.getId("master");
|
||||
if (mode === MODE_VERIFY_OTHER_USER) {
|
||||
// First key is our master cross signing key
|
||||
qrProps.firstKeyB64 = myCrossSigningInfo.getId("master");
|
||||
|
||||
qrProps.keys = [
|
||||
[myDeviceId, cli.getDeviceEd25519Key()],
|
||||
[xsignInfo.getId("master"), xsignInfo.getId("master")],
|
||||
];
|
||||
} else {
|
||||
// Doesn't matter which direction the verification is, we always show the same QR code
|
||||
// for not-ourself verification.
|
||||
const myXsignInfo = cli.getStoredCrossSigningForUser(myUserId);
|
||||
const otherXsignInfo = cli.getStoredCrossSigningForUser(otherUserId);
|
||||
const otherDevices = (await cli.getStoredDevicesForUser(otherUserId)) || [];
|
||||
const otherDevice = otherDevices.find(d => d.deviceId === otherDeviceId);
|
||||
// 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");
|
||||
|
||||
qrProps.keys = [
|
||||
[myDeviceId, cli.getDeviceEd25519Key()],
|
||||
[myXsignInfo.getId("master"), myXsignInfo.getId("master")],
|
||||
];
|
||||
qrProps.otherUserKey = otherXsignInfo.getId("master");
|
||||
if (otherDevice) qrProps.otherUserDeviceKey = otherDevice.getFingerprint();
|
||||
// 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;
|
||||
|
@ -107,21 +105,63 @@ export default class VerificationQRCode extends React.PureComponent {
|
|||
|
||||
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(2);
|
||||
tmpBuf.writeInt16BE(i, 0);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
const appendStr = (s: string, enc: string, withLengthPrefix = true) => {
|
||||
const tmpBuf = Buffer.from(s, enc);
|
||||
if (withLengthPrefix) 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", false);
|
||||
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() {
|
||||
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];
|
||||
if (!this.state.dataUri) {
|
||||
return <div className='mx_VerificationQRCode'><Spinner /></div>;
|
||||
}
|
||||
|
||||
const uri = `https://matrix.to/#/${this.props.keyholderUserId}?${qs.stringify(query)}`;
|
||||
|
||||
return <QRCode value={uri} size={512} logoWidth={64} logo={require("../../../../../res/img/matrix-m.svg")} />;
|
||||
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);
|
||||
|
|
|
@ -297,7 +297,7 @@ export default createReactClass({
|
|||
} 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
|
||||
|
|
|
@ -62,8 +62,8 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
try {
|
||||
await request.accept();
|
||||
this._openRequest();
|
||||
await request.accept();
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
}
|
||||
|
@ -136,9 +136,9 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
} else if (request.cancelled) {
|
||||
stateLabel = this._cancelledLabel(request.cancellingUserId);
|
||||
} else if (request.accepting) {
|
||||
stateLabel = _t("accepting …");
|
||||
stateLabel = _t("Accepting …");
|
||||
} else if (request.declining) {
|
||||
stateLabel = _t("declining …");
|
||||
stateLabel = _t("Declining …");
|
||||
}
|
||||
stateNode = (<div className="mx_cryptoEvent_state">{stateLabel}</div>);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,12 +28,17 @@ export const PendingActionSpinner = ({text}) => {
|
|||
</div>;
|
||||
};
|
||||
|
||||
const EncryptionInfo = ({pending, member, onStartVerification}) => {
|
||||
const EncryptionInfo = ({waitingForOtherParty, waitingForNetwork, member, onStartVerification}) => {
|
||||
let content;
|
||||
if (pending) {
|
||||
const text = _t("Waiting for %(displayName)s to accept…", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
});
|
||||
if (waitingForOtherParty || waitingForNetwork) {
|
||||
let text;
|
||||
if (waitingForOtherParty) {
|
||||
text = _t("Waiting for %(displayName)s to accept…", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
});
|
||||
} else {
|
||||
text = _t("Accepting…");
|
||||
}
|
||||
content = <PendingActionSpinner text={text} />;
|
||||
} else {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
|
|
@ -32,11 +32,14 @@ const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
|
|||
|
||||
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)) {
|
||||
|
@ -73,8 +76,13 @@ const EncryptionPanel = ({verificationRequest, member, onClose, layout}) => {
|
|||
}, [member.userId]);
|
||||
|
||||
const requested = request && (phase === PHASE_REQUESTED || phase === PHASE_UNSENT || phase === undefined);
|
||||
const initiatedByMe = request && request.initiatedByMe;
|
||||
if (!request || requested) {
|
||||
return <EncryptionInfo onStartVerification={onStartVerification} member={member} pending={requested} />;
|
||||
return <EncryptionInfo
|
||||
onStartVerification={onStartVerification}
|
||||
member={member}
|
||||
waitingForOtherParty={requested && initiatedByMe}
|
||||
waitingForNetwork={requested && !initiatedByMe} />;
|
||||
} else {
|
||||
return (
|
||||
<VerificationPanel
|
||||
|
|
|
@ -42,6 +42,7 @@ import {textualPowerLevel} from '../../../Roles';
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
|
||||
import EncryptionPanel from "./EncryptionPanel";
|
||||
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
|
||||
|
||||
const _disambiguateDevices = (devices) => {
|
||||
const names = Object.create(null);
|
||||
|
@ -916,6 +917,12 @@ const useIsSynapseAdmin = (cli) => {
|
|||
return isAdmin;
|
||||
};
|
||||
|
||||
const useHomeserverSupportsCrossSigning = (cli) => {
|
||||
return useAsyncMemo(async () => {
|
||||
return cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
||||
}, [cli], false);
|
||||
};
|
||||
|
||||
function useRoomPermissions(cli, room, user) {
|
||||
const [roomPermissions, setRoomPermissions] = useState({
|
||||
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
|
||||
|
@ -1315,19 +1322,23 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
|
|||
text = _t("Messages in this room are end-to-end encrypted.");
|
||||
}
|
||||
|
||||
const userTrust = cli.checkUserTrust(member.userId);
|
||||
const userVerified = SettingsStore.isFeatureEnabled("feature_cross_signing") ?
|
||||
userTrust.isCrossSigningVerified() :
|
||||
userTrust.isVerified();
|
||||
const isMe = member.userId === cli.getUserId();
|
||||
|
||||
let verifyButton;
|
||||
if (isRoomEncrypted && !userVerified && !isMe) {
|
||||
verifyButton = (
|
||||
<AccessibleButton className="mx_UserInfo_field" onClick={() => verifyUser(member)}>
|
||||
{_t("Verify")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
const homeserverSupportsCrossSigning = useHomeserverSupportsCrossSigning(cli);
|
||||
if (
|
||||
SettingsStore.isFeatureEnabled("feature_cross_signing") &&
|
||||
homeserverSupportsCrossSigning
|
||||
) {
|
||||
const userTrust = cli.checkUserTrust(member.userId);
|
||||
const userVerified = userTrust.isCrossSigningVerified();
|
||||
const isMe = member.userId === cli.getUserId();
|
||||
|
||||
if (isRoomEncrypted && !userVerified && !isMe) {
|
||||
verifyButton = (
|
||||
<AccessibleButton className="mx_UserInfo_field" onClick={() => verifyUser(member)}>
|
||||
{_t("Verify")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let devicesSection;
|
||||
|
|
|
@ -270,6 +270,8 @@ export default class VerificationPanel extends React.PureComponent {
|
|||
};
|
||||
|
||||
_onVerifierShowSas = (sasEvent) => {
|
||||
const {request} = this.props;
|
||||
request.verifier.off('show_sas', this._onVerifierShowSas);
|
||||
this.setState({sasEvent});
|
||||
};
|
||||
|
||||
|
@ -278,7 +280,7 @@ export default class VerificationPanel extends React.PureComponent {
|
|||
const hadVerifier = this._hasVerifier;
|
||||
this._hasVerifier = !!request.verifier;
|
||||
if (!hadVerifier && this._hasVerifier) {
|
||||
request.verifier.once('show_sas', this._onVerifierShowSas);
|
||||
request.verifier.on('show_sas', this._onVerifierShowSas);
|
||||
try {
|
||||
// on the requester side, this is also awaited in _startSAS,
|
||||
// but that's ok as verify should return the same promise.
|
||||
|
@ -299,6 +301,10 @@ export default class VerificationPanel extends React.PureComponent {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.request.off("change", this._onRequestChange);
|
||||
const {request} = this.props;
|
||||
if (request.verifier) {
|
||||
request.verifier.off('show_sas', this._onVerifierShowSas);
|
||||
}
|
||||
request.off("change", this._onRequestChange);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -457,10 +471,14 @@ export default class BasicMessageEditor extends React.Component {
|
|||
const addedLen = range.replace([partCreator.pillCandidate(range.text)]);
|
||||
return model.positionForOffset(caret.offset + addedLen, true);
|
||||
});
|
||||
await model.autoComplete.onTab();
|
||||
if (!model.autoComplete.hasSelection()) {
|
||||
this.setState({showVisualBell: true});
|
||||
model.autoComplete.close();
|
||||
|
||||
// Don't try to do things with the autocomplete if there is none shown
|
||||
if (model.autoComplete) {
|
||||
await model.autoComplete.onTab();
|
||||
if (!model.autoComplete.hasSelection()) {
|
||||
this.setState({showVisualBell: true});
|
||||
model.autoComplete.close();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
@ -490,6 +508,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);
|
||||
|
|
|
@ -100,6 +100,17 @@ export function getHandlerTile(ev) {
|
|||
}
|
||||
}
|
||||
|
||||
// sometimes MKeyVerificationConclusion declines to render. Jankily decline to render and
|
||||
// fall back to showing hidden events, if we're viewing hidden events
|
||||
// XXX: This is extremely a hack. Possibly these components should have an interface for
|
||||
// declining to render?
|
||||
if (type === "m.key.verification.cancel" && SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
||||
const MKeyVerificationConclusion = sdk.getComponent("messages.MKeyVerificationConclusion");
|
||||
if (!MKeyVerificationConclusion.prototype._shouldRender.call(null, ev, ev.request)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type];
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,8 @@ export default class InviteOnlyIcon extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const classes = this.props.collapsedPanel ? "mx_InviteOnlyIcon_small": "mx_InviteOnlyIcon_large";
|
||||
|
||||
if (!SettingsStore.isFeatureEnabled("feature_invite_only_padlocks")) {
|
||||
return null;
|
||||
}
|
||||
|
@ -46,7 +48,7 @@ export default class InviteOnlyIcon extends React.Component {
|
|||
if (this.state.hover) {
|
||||
tooltip = <Tooltip className="mx_InviteOnlyIcon_tooltip" label={_t("Invite only")} dir="auto" />;
|
||||
}
|
||||
return (<div className="mx_InviteOnlyIcon"
|
||||
return (<div className={classes}
|
||||
onMouseEnter={this.onHoverStart}
|
||||
onMouseLeave={this.onHoverEnd}
|
||||
>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -28,7 +28,7 @@ import rate_limited_func from "../../../ratelimitedfunc";
|
|||
import * as Rooms from '../../../Rooms';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import TagOrderStore from '../../../stores/TagOrderStore';
|
||||
import RoomListStore from '../../../stores/RoomListStore';
|
||||
import RoomListStore, {TAG_DM} from '../../../stores/RoomListStore';
|
||||
import CustomRoomTagStore from '../../../stores/CustomRoomTagStore';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import RoomSubList from '../../structures/RoomSubList';
|
||||
|
@ -715,10 +715,10 @@ export default createReactClass({
|
|||
incomingCall: incomingCallIfTaggedAs('m.favourite'),
|
||||
},
|
||||
{
|
||||
list: this.state.lists['im.vector.fake.direct'],
|
||||
list: this.state.lists[TAG_DM],
|
||||
label: _t('Direct Messages'),
|
||||
tagName: "im.vector.fake.direct",
|
||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'),
|
||||
tagName: TAG_DM,
|
||||
incomingCall: incomingCallIfTaggedAs(TAG_DM),
|
||||
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'});},
|
||||
addRoomLabel: _t("Start chat"),
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -528,7 +528,7 @@ export default createReactClass({
|
|||
let privateIcon = null;
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
if (this.state.joinRule == "invite" && !dmUserId) {
|
||||
privateIcon = <InviteOnlyIcon />;
|
||||
privateIcon = <InviteOnlyIcon collapsedPanel={this.props.collapsed} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -72,11 +72,14 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
const crossSigningPublicKeysOnDevice = crossSigning.getId();
|
||||
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
|
||||
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
||||
const homeserverSupportsCrossSigning =
|
||||
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
||||
|
||||
this.setState({
|
||||
crossSigningPublicKeysOnDevice,
|
||||
crossSigningPrivateKeysInStorage,
|
||||
secretStorageKeyInAccount,
|
||||
homeserverSupportsCrossSigning,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -120,6 +123,7 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
crossSigningPublicKeysOnDevice,
|
||||
crossSigningPrivateKeysInStorage,
|
||||
secretStorageKeyInAccount,
|
||||
homeserverSupportsCrossSigning,
|
||||
} = this.state;
|
||||
|
||||
let errorSection;
|
||||
|
@ -127,14 +131,19 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
errorSection = <div className="error">{error.toString()}</div>;
|
||||
}
|
||||
|
||||
const enabled = (
|
||||
crossSigningPublicKeysOnDevice &&
|
||||
// Whether the various keys exist on your account (but not necessarily
|
||||
// on this device).
|
||||
const enabledForAccount = (
|
||||
crossSigningPrivateKeysInStorage &&
|
||||
secretStorageKeyInAccount
|
||||
);
|
||||
|
||||
let summarisedStatus;
|
||||
if (enabled) {
|
||||
if (!homeserverSupportsCrossSigning) {
|
||||
summarisedStatus = <p>{_t(
|
||||
"Your homeserver does not support cross-signing.",
|
||||
)}</p>;
|
||||
} else if (enabledForAccount && crossSigningPublicKeysOnDevice) {
|
||||
summarisedStatus = <p>✅ {_t(
|
||||
"Cross-signing and secret storage are enabled.",
|
||||
)}</p>;
|
||||
|
@ -149,19 +158,28 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
)}</p>;
|
||||
}
|
||||
|
||||
let resetButton;
|
||||
if (enabledForAccount) {
|
||||
resetButton = (
|
||||
<div className="mx_CrossSigningPanel_buttonRow">
|
||||
<AccessibleButton kind="danger" onClick={this._destroySecureSecretStorage}>
|
||||
{_t("Reset cross-signing and secret storage")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let bootstrapButton;
|
||||
if (!enabled) {
|
||||
bootstrapButton = <div className="mx_CrossSigningPanel_buttonRow">
|
||||
<AccessibleButton kind="primary" onClick={this._bootstrapSecureSecretStorage}>
|
||||
{_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>;
|
||||
if (
|
||||
(!enabledForAccount || !crossSigningPublicKeysOnDevice) &&
|
||||
homeserverSupportsCrossSigning
|
||||
) {
|
||||
bootstrapButton = (
|
||||
<div className="mx_CrossSigningPanel_buttonRow">
|
||||
<AccessibleButton kind="primary" onClick={this._bootstrapSecureSecretStorage}>
|
||||
{_t("Bootstrap cross-signing and secret storage")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -182,10 +200,15 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
<td>{_t("Secret storage public key:")}</td>
|
||||
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
<tr>
|
||||
<td>{_t("Homeserver feature support:")}</td>
|
||||
<td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</details>
|
||||
{errorSection}
|
||||
{bootstrapButton}
|
||||
{resetButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -172,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>,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -188,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>,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -127,7 +127,6 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
||||
{
|
||||
secureSecretStorage: SettingsStore.isFeatureEnabled("feature_cross_signing"),
|
||||
onFinished: () => {
|
||||
this._loadBackupStatus();
|
||||
},
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -97,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>;
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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>;
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -42,6 +42,12 @@ export default class UnverifiedSessionToast extends React.PureComponent {
|
|||
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
|
||||
userId: MatrixClientPeg.get().getUserId(),
|
||||
device,
|
||||
onFinished: (r) => {
|
||||
if (!r) {
|
||||
/* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */
|
||||
this._onLaterClick();
|
||||
}
|
||||
},
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
};
|
||||
|
||||
|
|
|
@ -78,7 +78,6 @@ export default class VerificationRequestToast extends React.PureComponent {
|
|||
// no room id for to_device requests
|
||||
const cli = MatrixClientPeg.get();
|
||||
try {
|
||||
await request.accept();
|
||||
if (request.channel.roomId) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
|
@ -99,6 +98,7 @@ export default class VerificationRequestToast extends React.PureComponent {
|
|||
verificationRequest: request,
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
}
|
||||
await request.accept();
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
}
|
||||
|
|
|
@ -48,6 +48,11 @@ export default class VerificationShowSas extends React.Component {
|
|||
this.props.onDone();
|
||||
};
|
||||
|
||||
onDontMatchClick = () => {
|
||||
this.setState({ cancelling: true });
|
||||
this.props.onCancel();
|
||||
};
|
||||
|
||||
render() {
|
||||
let sasDisplay;
|
||||
let sasCaption;
|
||||
|
@ -98,9 +103,14 @@ export default class VerificationShowSas extends React.Component {
|
|||
}
|
||||
|
||||
let confirm;
|
||||
if (this.state.pending) {
|
||||
const {displayName} = this.props;
|
||||
const text = _t("Waiting for %(displayName)s to verify…", {displayName});
|
||||
if (this.state.pending || this.state.cancelling) {
|
||||
let text;
|
||||
if (this.state.pending) {
|
||||
const {displayName} = this.props;
|
||||
text = _t("Waiting for %(displayName)s to verify…", {displayName});
|
||||
} else {
|
||||
text = _t("Cancelling…");
|
||||
}
|
||||
confirm = <PendingActionSpinner text={text} />;
|
||||
} else {
|
||||
// FIXME: stop using DialogButtons here once this component is only used in the right panel verification
|
||||
|
@ -109,7 +119,7 @@ export default class VerificationShowSas extends React.Component {
|
|||
onPrimaryButtonClick={this.onMatchClick}
|
||||
primaryButtonClass="mx_UserInfo_wideButton"
|
||||
cancelButton={_t("They don't match")}
|
||||
onCancel={this.props.onCancel}
|
||||
onCancel={this.onDontMatchClick}
|
||||
cancelButtonClass="mx_UserInfo_wideButton"
|
||||
/>;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue