Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into mac-redo

This commit is contained in:
Aaron Raimist 2020-03-17 13:52:43 -05:00
commit 038888f2d4
178 changed files with 6343 additions and 3514 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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}
>

View file

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

View file

@ -72,7 +72,7 @@ export default createReactClass({
<button onClick={this._onInviteNeverWarnClicked}>
{ _t('Invite anyway and never warn me again') }
</button>
<button onClick={this._onInviteClicked} autoFocus="true">
<button onClick={this._onInviteClicked} autoFocus={true}>
{ _t('Invite anyway') }
</button>
</div>

View file

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

View file

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

View file

@ -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";
@ -218,7 +219,7 @@ class DMRoomTile extends React.PureComponent {
}
// Push any text we missed (end of text)
if (i < (str.length - 1)) {
if (i < str.length) {
result.push(<span key={i + 'end'}>{str.substring(i)}</span>);
}
@ -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);
@ -529,7 +564,7 @@ export default class InviteDialog extends React.PureComponent {
return;
}
const createRoomOptions = {};
const createRoomOptions = {inlineErrors: true};
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
// Check whether all users have uploaded device keys before.
@ -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) {
@ -857,24 +906,24 @@ export default class InviteDialog extends React.PureComponent {
// Mix in the server results if we have any, but only if we're searching. We track the additional
// members separately because we want to filter sourceMembers but trust the mixin arrays to have
// the right members in them.
let additionalMembers = [];
let priorityAdditionalMembers = []; // Shows up before our own suggestions, higher quality
let otherAdditionalMembers = []; // Shows up after our own suggestions, lower quality
const hasMixins = this.state.serverResultsMixin || this.state.threepidResultsMixin;
if (this.state.filterText && hasMixins && kind === 'suggestions') {
// We don't want to duplicate members though, so just exclude anyone we've already seen.
const notAlreadyExists = (u: Member): boolean => {
return !sourceMembers.some(m => m.userId === u.userId)
&& !additionalMembers.some(m => m.userId === u.userId);
&& !priorityAdditionalMembers.some(m => m.userId === u.userId)
&& !otherAdditionalMembers.some(m => m.userId === u.userId);
};
const uniqueServerResults = this.state.serverResultsMixin.filter(notAlreadyExists);
additionalMembers = additionalMembers.concat(...uniqueServerResults);
const uniqueThreepidResults = this.state.threepidResultsMixin.filter(notAlreadyExists);
additionalMembers = additionalMembers.concat(...uniqueThreepidResults);
otherAdditionalMembers = this.state.serverResultsMixin.filter(notAlreadyExists);
priorityAdditionalMembers = this.state.threepidResultsMixin.filter(notAlreadyExists);
}
const hasAdditionalMembers = priorityAdditionalMembers.length > 0 || otherAdditionalMembers.length > 0;
// Hide the section if there's nothing to filter by
if (sourceMembers.length === 0 && additionalMembers.length === 0) return null;
if (sourceMembers.length === 0 && !hasAdditionalMembers) return null;
// Do some simple filtering on the input before going much further. If we get no results, say so.
if (this.state.filterText) {
@ -882,7 +931,7 @@ export default class InviteDialog extends React.PureComponent {
sourceMembers = sourceMembers
.filter(m => m.user.name.toLowerCase().includes(filterBy) || m.userId.toLowerCase().includes(filterBy));
if (sourceMembers.length === 0 && additionalMembers.length === 0) {
if (sourceMembers.length === 0 && !hasAdditionalMembers) {
return (
<div className='mx_InviteDialog_section'>
<h3>{sectionName}</h3>
@ -894,7 +943,7 @@ export default class InviteDialog extends React.PureComponent {
// Now we mix in the additional members. Again, we presume these have already been filtered. We
// also assume they are more relevant than our suggestions and prepend them to the list.
sourceMembers = [...additionalMembers, ...sourceMembers];
sourceMembers = [...priorityAdditionalMembers, ...sourceMembers, ...otherAdditionalMembers];
// If we're going to hide one member behind 'show more', just use up the space of the button
// with the member's tile instead.
@ -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>

View file

@ -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,20 +34,38 @@ 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 () => {
onContinueClick = () => {
const { userId, device } = this.props;
const cli = MatrixClientPeg.get();
const request = await cli.requestVerification(
const requestPromise = cli.requestVerification(
userId,
[device.deviceId],
);
this.props.onFinished(true);
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
verificationRequest: request,
verificationRequestPromise: requestPromise,
member: cli.getUser(userId),
});
}

View file

@ -32,6 +32,7 @@ export default createReactClass({
focus: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
headerImage: PropTypes.string,
quitOnly: PropTypes.bool, // quitOnly doesn't show the cancel button just the quit [x].
},
getDefaultProps: function() {
@ -42,6 +43,7 @@ export default createReactClass({
focus: true,
hasCancelButton: true,
danger: false,
quitOnly: false,
};
},
@ -73,7 +75,7 @@ export default createReactClass({
<DialogButtons primaryButton={this.props.button || _t('OK')}
primaryButtonClass={primaryButtonClass}
cancelButton={this.props.cancelButton}
hasCancel={this.props.hasCancelButton}
hasCancel={this.props.hasCancelButton && !this.props.quitOnly}
onPrimaryButtonClick={this.onOk}
focus={this.props.focus}
onCancel={this.onCancel}

View file

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

View file

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

View file

@ -22,7 +22,8 @@ import { _t } from '../../../languageHandler';
export default class VerificationRequestDialog extends React.Component {
static propTypes = {
verificationRequest: PropTypes.object.isRequired,
verificationRequest: PropTypes.object,
verificationRequestPromise: PropTypes.object,
onFinished: PropTypes.func.isRequired,
};
@ -34,6 +35,8 @@ export default class VerificationRequestDialog extends React.Component {
render() {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
const member = this.props.member ||
MatrixClientPeg.get().getUser(this.props.verificationRequest.otherUserId);
return <BaseDialog className="mx_InfoDialog" onFinished={this.onFinished}
contentId="mx_Dialog_content"
title={_t("Verification Request")}
@ -42,14 +45,19 @@ export default class VerificationRequestDialog extends React.Component {
<EncryptionPanel
layout="dialog"
verificationRequest={this.props.verificationRequest}
verificationRequestPromise={this.props.verificationRequestPromise}
onClose={this.props.onFinished}
member={MatrixClientPeg.get().getUser(this.props.verificationRequest.otherUserId)}
member={member}
/>
</BaseDialog>;
}
onFinished() {
this.props.verificationRequest.cancel();
async onFinished() {
this.props.onFinished();
let request = this.props.verificationRequest;
if (!request && this.props.verificationRequestPromise) {
request = await this.props.verificationRequestPromise;
}
request.cancel();
}
}

View file

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

View file

@ -78,8 +78,7 @@ export class EditableItem extends React.Component {
return (
<div className="mx_EditableItem">
<img src={require("../../../../res/img/feather-customised/cancel.svg")} width={14} height={14}
onClick={this._onRemove} className="mx_EditableItem_delete" alt={_t("Remove")} />
<div onClick={this._onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
<span className="mx_EditableItem_item">{this.props.value}</span>
</div>
);
@ -123,8 +122,9 @@ export default class EditableItemList extends React.Component {
<form onSubmit={this._onItemAdded} autoComplete="off"
noValidate={true} className="mx_EditableItemList_newItem">
<Field id={`mx_EditableItemList_new_${this.props.id}`} label={this.props.placeholder} type="text"
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged} />
<AccessibleButton onClick={this._onItemAdded} kind="primary">
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
list={this.props.suggestionsListId} />
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit">
{_t("Add")}
</AccessibleButton>
</form>

View file

@ -32,6 +32,8 @@ export default class Field extends React.PureComponent {
element: PropTypes.oneOf(["input", "select", "textarea"]),
// The field's type (when used as an <input>). Defaults to "text".
type: PropTypes.string,
// id of a <datalist> element for suggestions
list: PropTypes.string,
// The field's label string.
label: PropTypes.string,
// The field's placeholder string. Defaults to the label.
@ -157,7 +159,7 @@ export default class Field extends React.PureComponent {
render() {
const {
element, prefix, postfix, className, onValidate, children,
tooltipContent, flagInvalid, tooltipClassName, ...inputProps} = this.props;
tooltipContent, flagInvalid, tooltipClassName, list, ...inputProps} = this.props;
const inputElement = element || "input";
@ -169,6 +171,7 @@ export default class Field extends React.PureComponent {
inputProps.onFocus = this.onFocus;
inputProps.onChange = this.onChange;
inputProps.onBlur = this.onBlur;
inputProps.list = list;
const fieldInput = React.createElement(inputElement, inputProps, children);

View file

@ -91,7 +91,7 @@ export default class ImageView extends React.Component {
getName() {
let name = this.props.name;
if (name && this.props.link) {
name = <a href={ this.props.link } target="_blank" rel="noopener">{ name }</a>;
name = <a href={ this.props.link } target="_blank" rel="noreferrer noopener">{ name }</a>;
}
return name;
}

View file

@ -23,7 +23,6 @@ import classNames from 'classnames';
import { Room, RoomMember } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { getDisplayAliasForRoom } from '../../../Rooms';
import FlairStore from "../../../stores/FlairStore";
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
@ -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: {

View file

@ -92,6 +92,7 @@ export default class RoomAliasField extends React.PureComponent {
invalid: () => _t("Please provide a room alias"),
}, {
key: "taken",
final: true,
test: async ({value}) => {
if (!value) {
return true;

View file

@ -0,0 +1,41 @@
/*
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 PlatformPeg from "../../../PlatformPeg";
import AccessibleButton from "./AccessibleButton";
import {_t} from "../../../languageHandler";
const SSOButton = ({matrixClient, loginType, ...props}) => {
const onClick = () => {
PlatformPeg.get().startSingleSignOn(matrixClient, loginType);
};
return (
<AccessibleButton {...props} kind="primary" onClick={onClick}>
{_t("Sign in with single sign-on")}
</AccessibleButton>
);
};
SSOButton.propTypes = {
matrixClient: PropTypes.object.isRequired, // does not use context as may use a temporary client
loginType: PropTypes.oneOf(["sso", "cas"]), // defaults to "sso" in base-apis
};
export default SSOButton;

View file

@ -28,9 +28,11 @@ import classNames from 'classnames';
* An array of rules describing how to check to input value. Each rule in an object
* and may have the following properties:
* - `key`: A unique ID for the rule. Required.
* - `skip`: A function used to determine whether the rule should even be evaluated.
* - `test`: A function used to determine the rule's current validity. Required.
* - `valid`: Function returning text to show when the rule is valid. Only shown if set.
* - `invalid`: Function returning text to show when the rule is invalid. Only shown if set.
* - `final`: A Boolean if true states that this rule will only be considered if all rules before it returned valid.
* @returns {Function}
* A validation function that takes in the current input value and returns
* the overall validity and a feedback UI that can be rendered for more detail.
@ -51,9 +53,20 @@ export default function withValidation({ description, rules }) {
if (!rule.key || !rule.test) {
continue;
}
if (!valid && rule.final) {
continue;
}
const data = { value, allowEmpty };
if (rule.skip && rule.skip.call(this, data)) {
continue;
}
// We're setting `this` to whichever component holds the validation
// function. That allows rules to access the state of the component.
const ruleValid = await rule.test.call(this, { value, allowEmpty });
const ruleValid = await rule.test.call(this, data);
valid = valid && ruleValid;
if (ruleValid && rule.valid) {
// If the rule's result is valid and has text to show for

View file

@ -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' />;
}
}

View file

@ -60,6 +60,7 @@ export default createReactClass({
const av = (
<BaseAvatar
aria-hidden="true"
name={this.props.member.displayname || this.props.member.userId}
idName={this.props.member.userId}
width={36} height={36}

View file

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

View file

@ -247,6 +247,8 @@ export default createReactClass({
});
};
// This button should actually Download because usercontent/ will try to click itself
// but it is not guaranteed between various browsers' settings.
return (
<span className="mx_MFileBody">
<div className="mx_MFileBody_download">
@ -269,6 +271,8 @@ export default createReactClass({
// We can't provide a Content-Disposition header like we would for HTTP.
download: fileName,
textContent: _t("Download %(text)s", { text: text }),
// only auto-download if a user triggered this iframe explicitly
auto: !this.props.decryptedBlob,
}, "*");
};
@ -290,14 +294,14 @@ export default createReactClass({
src={`${url}?origin=${encodeURIComponent(window.location.origin)}`}
onLoad={onIframeLoad}
ref={this._iframe}
sandbox="allow-scripts allow-downloads" />
sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation" />
</div>
</span>
);
} else if (contentUrl) {
const downloadProps = {
target: "_blank",
rel: "noopener",
rel: "noreferrer noopener",
// We set the href regardless of whether or not we intercept the download
// because we don't really want to convert the file to a blob eagerly, and

View file

@ -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>);
}

View file

@ -121,10 +121,12 @@ export default class MessageActionBar extends React.PureComponent {
componentDidMount() {
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
this.props.mxEvent.on("Event.beforeRedaction", this.onBeforeRedaction);
}
componentWillUnmount() {
this.props.mxEvent.removeListener("Event.decrypted", this.onDecrypted);
this.props.mxEvent.removeListener("Event.beforeRedaction", this.onBeforeRedaction);
}
onDecrypted = () => {
@ -133,6 +135,11 @@ export default class MessageActionBar extends React.PureComponent {
this.forceUpdate();
};
onBeforeRedaction = () => {
// When an event is redacted, we can't edit it so update the available actions.
this.forceUpdate();
};
onFocusChange = (focused) => {
if (!this.props.onFocusChange) {
return;

View file

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

View file

@ -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');

View file

@ -30,13 +30,32 @@ 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, layout}) => {
const EncryptionPanel = ({verificationRequest, verificationRequestPromise, member, onClose, layout}) => {
const [request, setRequest] = useState(verificationRequest);
// state to show a spinner immediately after clicking "start verification",
// before we have a request
const [isRequesting, setRequesting] = useState(false);
const [phase, setPhase] = useState(request && request.phase);
useEffect(() => {
setRequest(verificationRequest);
if (verificationRequest) {
setRequesting(false);
setPhase(verificationRequest.phase);
}
}, [verificationRequest]);
const [phase, setPhase] = useState(request && request.phase);
useEffect(() => {
async function awaitPromise() {
setRequesting(true);
const request = await verificationRequestPromise;
setRequesting(false);
setRequest(request);
setPhase(request.phase);
}
if (verificationRequestPromise) {
awaitPromise();
}
}, [verificationRequestPromise]);
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)) {
@ -65,6 +84,7 @@ const EncryptionPanel = ({verificationRequest, member, onClose, layout}) => {
useEventEmitter(request, "change", changeHandler);
const onStartVerification = useCallback(async () => {
setRequesting(true);
const cli = MatrixClientPeg.get();
const roomId = await ensureDMExists(cli, member.userId);
const verificationRequest = await cli.requestVerificationDM(member.userId, roomId);
@ -72,9 +92,16 @@ const EncryptionPanel = ({verificationRequest, member, onClose, layout}) => {
setPhase(verificationRequest.phase);
}, [member.userId]);
const requested = request && (phase === PHASE_REQUESTED || phase === PHASE_UNSENT || phase === undefined);
const requested =
(!request && isRequesting) ||
(request && (phase === PHASE_REQUESTED || phase === PHASE_UNSENT || phase === undefined));
if (!request || requested) {
return <EncryptionInfo onStartVerification={onStartVerification} member={member} pending={requested} />;
const initiatedByMe = (!request && isRequesting) || (request && request.initiatedByMe);
return <EncryptionInfo
onStartVerification={onStartVerification}
member={member}
waitingForOtherParty={requested && initiatedByMe}
waitingForNetwork={requested && !initiatedByMe} />;
} else {
return (
<VerificationPanel

View file

@ -25,7 +25,7 @@ import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import createRoom, {findDMForUser} from '../../../createRoom';
import createRoom from '../../../createRoom';
import DMRoomMap from '../../../utils/DMRoomMap';
import AccessibleButton from '../elements/AccessibleButton';
import SdkConfig from '../../../SdkConfig';
@ -42,6 +42,8 @@ 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';
import { verifyUser, legacyVerifyUser, verifyDevice } from '../../../verification';
const _disambiguateDevices = (devices) => {
const names = Object.create(null);
@ -135,54 +137,21 @@ function useIsEncrypted(cli, room) {
return isEncrypted;
}
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,
verificationRequest: existingRequest,
},
});
function useHasCrossSigningKeys(cli, member, canVerify, setUpdating) {
return useAsyncMemo(async () => {
if (!canVerify) {
return false;
}
setUpdating(true);
try {
await cli.downloadKeys([member.userId]);
const xsi = cli.getStoredCrossSigningForUser(member.userId);
const key = xsi && xsi.getId();
return !!key;
} finally {
setUpdating(false);
}
}, [cli, member, canVerify], false);
}
function DeviceItem({userId, device}) {
@ -211,7 +180,7 @@ function DeviceItem({userId, device}) {
const onDeviceClick = () => {
if (!isVerified) {
verifyDevice(userId, device);
verifyDevice(cli.getUser(userId), device);
}
};
@ -916,6 +885,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,16 +1290,31 @@ 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) {
const homeserverSupportsCrossSigning = useHomeserverSupportsCrossSigning(cli);
const userTrust = cli.checkUserTrust(member.userId);
const userVerified = userTrust.isCrossSigningVerified();
const isMe = member.userId === cli.getUserId();
const canVerify = SettingsStore.isFeatureEnabled("feature_cross_signing") &&
homeserverSupportsCrossSigning &&
isRoomEncrypted && !userVerified && !isMe;
const setUpdating = (updating) => {
setPendingUpdateCount(count => count + (updating ? 1 : -1));
};
const hasCrossSigningKeys =
useHasCrossSigningKeys(cli, member, canVerify, setUpdating );
if (canVerify) {
verifyButton = (
<AccessibleButton className="mx_UserInfo_field" onClick={() => verifyUser(member)}>
<AccessibleButton className="mx_UserInfo_field" onClick={() => {
if (hasCrossSigningKeys) {
verifyUser(member);
} else {
legacyVerifyUser(member);
}
}}>
{_t("Verify")}
</AccessibleButton>
);

View file

@ -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);
}
}

View file

@ -25,6 +25,7 @@ import Field from "../elements/Field";
import ErrorDialog from "../dialogs/ErrorDialog";
import AccessibleButton from "../elements/AccessibleButton";
import Modal from "../../../Modal";
import RoomPublishSetting from "./RoomPublishSetting";
class EditableAliasesList extends EditableItemList {
constructor(props) {
@ -46,6 +47,11 @@ class EditableAliasesList extends EditableItemList {
};
_renderNewItemField() {
// if we don't need the RoomAliasField,
// we don't need to overriden version of _renderNewItemField
if (!this.props.domain) {
return super._renderNewItemField();
}
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
const onChange = (alias) => this._onNewItemChanged({target: {value: alias}});
return (
@ -87,91 +93,64 @@ export default class AliasSettings extends React.Component {
super(props);
const state = {
domainToAliases: {}, // { domain.com => [#alias1:domain.com, #alias2:domain.com] }
remoteDomains: [], // [ domain.com, foobar.com ]
canonicalAlias: null, // #canonical:domain.com
altAliases: [], // [ #alias:domain.tld, ... ]
localAliases: [], // [ #alias:my-hs.tld, ... ]
canonicalAlias: null, // #canonical:domain.tld
updatingCanonicalAlias: false,
localAliasesLoading: true,
localAliasesLoading: false,
detailsOpen: false,
};
if (props.canonicalAliasEvent) {
state.canonicalAlias = props.canonicalAliasEvent.getContent().alias;
const content = props.canonicalAliasEvent.getContent();
const altAliases = content.alt_aliases;
if (Array.isArray(altAliases)) {
state.altAliases = altAliases.slice();
}
state.canonicalAlias = content.alias;
}
this.state = state;
}
async componentWillMount() {
const cli = MatrixClientPeg.get();
componentDidMount() {
if (this.props.canSetCanonicalAlias) {
// load local aliases for providing recommendations
// for the canonical alias and alt_aliases
this.loadLocalAliases();
}
}
async loadLocalAliases() {
this.setState({ localAliasesLoading: true });
try {
const cli = MatrixClientPeg.get();
let localAliases = [];
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);
if (Array.isArray(response.aliases)) {
localAliases = response.aliases;
}
}
this.setState({ localAliases });
} finally {
this.setState({localAliasesLoading: false});
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) => {
dict[event.getStateKey()] = (
(event.getContent().aliases || []).slice() // shallow-copy
);
});
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;
const oldAlias = this.state.canonicalAlias;
this.setState({
canonicalAlias: alias,
updatingCanonicalAlias: true,
});
const eventContent = {};
const altAliases = this._getAltAliases();
if (altAliases) eventContent["alt_aliases"] = altAliases;
const eventContent = {
alt_aliases: this.state.altAliases,
};
if (alias) eventContent["alias"] = alias;
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.canonical_alias",
@ -184,6 +163,39 @@ export default class AliasSettings extends React.Component {
"or a temporary failure occurred.",
),
});
this.setState({canonicalAlias: oldAlias});
}).finally(() => {
this.setState({updatingCanonicalAlias: false});
});
}
changeAltAliases(altAliases) {
if (!this.props.canSetCanonicalAlias) return;
this.setState({
updatingCanonicalAlias: true,
altAliases,
});
const eventContent = {};
if (this.state.canonicalAlias) {
eventContent.alias = this.state.canonicalAlias;
}
if (altAliases) {
eventContent["alt_aliases"] = altAliases;
}
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.canonical_alias",
eventContent, "").catch((err) => {
console.error(err);
Modal.createTrackedDialog('Error updating alternative addresses', '', ErrorDialog, {
title: _t("Error updating main address"),
description: _t(
"There was an error updating the room's alternative addresses. " +
"It may not be allowed by the server or a temporary failure occurred.",
),
});
}).finally(() => {
this.setState({updatingCanonicalAlias: false});
});
@ -200,16 +212,10 @@ export default class AliasSettings extends React.Component {
if (!alias.includes(':')) alias += ':' + localDomain;
MatrixClientPeg.get().createAlias(alias, this.props.roomId).then(() => {
const localAliases = this.state.domainToAliases[localDomain] || [];
const domainAliases = Object.assign({}, this.state.domainToAliases);
domainAliases[localDomain] = [...localAliases, alias];
this.setState({
domainToAliases: domainAliases,
// Reset the add field
newAlias: "",
localAliases: this.state.localAliases.concat(alias),
newAlias: null,
});
if (!this.state.canonicalAlias) {
this.changeCanonicalAlias(alias);
}
@ -226,38 +232,78 @@ export default class AliasSettings extends React.Component {
};
onLocalAliasDeleted = (index) => {
const localDomain = MatrixClientPeg.get().getDomain();
const alias = this.state.domainToAliases[localDomain][index];
const alias = this.state.localAliases[index];
// TODO: In future, we should probably be making sure that the alias actually belongs
// to this room. See https://github.com/vector-im/riot-web/issues/7353
MatrixClientPeg.get().deleteAlias(alias).then(() => {
const localAliases = this.state.domainToAliases[localDomain].filter((a) => a !== alias);
const domainAliases = Object.assign({}, this.state.domainToAliases);
domainAliases[localDomain] = localAliases;
this.setState({domainToAliases: domainAliases});
const localAliases = this.state.localAliases.slice();
localAliases.splice(index, 1);
this.setState({localAliases});
if (this.state.canonicalAlias === alias) {
this.changeCanonicalAlias(null);
}
}).catch((err) => {
console.error(err);
Modal.createTrackedDialog('Error removing alias', '', ErrorDialog, {
title: _t("Error removing alias"),
description: _t(
let description;
if (err.errcode === "M_FORBIDDEN") {
description = _t("You don't have permission to delete the alias.");
} else {
description = _t(
"There was an error removing that alias. It may no longer exist or a temporary " +
"error occurred.",
),
);
}
Modal.createTrackedDialog('Error removing alias', '', ErrorDialog, {
title: _t("Error removing alias"),
description,
});
});
};
onLocalAliasesToggled = (event) => {
// expanded
if (event.target.open) {
// if local aliases haven't been preloaded yet at component mount
if (!this.props.canSetCanonicalAlias && this.state.localAliases.length === 0) {
this.loadLocalAliases();
}
}
this.setState({detailsOpen: event.target.open});
};
onCanonicalAliasChange = (event) => {
this.changeCanonicalAlias(event.target.value);
};
onNewAltAliasChanged = (value) => {
this.setState({newAltAlias: value});
}
onAltAliasAdded = (alias) => {
const altAliases = this.state.altAliases.slice();
if (!altAliases.some(a => a.trim() === alias.trim())) {
altAliases.push(alias.trim());
this.changeAltAliases(altAliases);
this.setState({newAltAlias: ""});
}
}
onAltAliasDeleted = (index) => {
const altAliases = this.state.altAliases.slice();
altAliases.splice(index, 1);
this.changeAltAliases(altAliases);
}
_getAliases() {
return this.state.altAliases.concat(this._getLocalNonAltAliases());
}
_getLocalNonAltAliases() {
const {altAliases} = this.state;
return this.state.localAliases.filter(alias => !altAliases.includes(alias));
}
render() {
const localDomain = MatrixClientPeg.get().getDomain();
@ -269,15 +315,13 @@ export default class AliasSettings extends React.Component {
element='select' id='canonicalAlias' label={_t('Main address')}>
<option value="" key="unset">{ _t('not specified') }</option>
{
Object.keys(this.state.domainToAliases).map((domain, i) => {
return this.state.domainToAliases[domain].map((alias, j) => {
if (alias === this.state.canonicalAlias) found = true;
return (
<option value={alias} key={i + "_" + j}>
{ alias }
</option>
);
});
this._getAliases().map((alias, i) => {
if (alias === this.state.canonicalAlias) found = true;
return (
<option value={alias} key={i}>
{ alias }
</option>
);
})
}
{
@ -289,53 +333,60 @@ export default class AliasSettings extends React.Component {
</Field>
);
let remoteAliasesSection;
if (this.state.remoteDomains.length) {
remoteAliasesSection = (
<div>
<div>
{ _t("Remote addresses for this room:") }
</div>
<ul>
{ this.state.remoteDomains.map((domain, i) => {
return this.state.domainToAliases[domain].map((alias, j) => {
return <li key={i + "_" + j}>{alias}</li>;
});
}) }
</ul>
</div>
);
}
let localAliasesList;
if (this.state.localAliasesLoading) {
const Spinner = sdk.getComponent("elements.Spinner");
localAliasesList = <Spinner />;
} else {
localAliasesList = <EditableAliasesList
localAliasesList = (<EditableAliasesList
id="roomAliases"
className={"mx_RoomSettings_localAliases"}
items={this.state.domainToAliases[localDomain] || []}
items={this.state.localAliases}
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},
)}
placeholder={_t('Local address')}
domain={localDomain}
/>;
/>);
}
return (
<div className='mx_AliasSettings'>
<span className='mx_SettingsTab_subheading'>{_t("Published Addresses")}</span>
<p>{_t("Published addresses can be used by anyone on any server to join your room. " +
"To publish an address, it needs to be set as a local address first.")}</p>
{canonicalAliasSection}
{localAliasesList}
{remoteAliasesSection}
<RoomPublishSetting roomId={this.props.roomId} canSetCanonicalAlias={this.props.canSetCanonicalAlias} />
<datalist id="mx_AliasSettings_altRecommendations">
{this._getLocalNonAltAliases().map(alias => {
return <option value={alias} key={alias} />;
})};
</datalist>
<EditableAliasesList
id="roomAltAliases"
className={"mx_RoomSettings_altAliases"}
items={this.state.altAliases}
newItem={this.state.newAltAlias}
onNewItemChanged={this.onNewAltAliasChanged}
canRemove={this.props.canSetCanonicalAlias}
canEdit={this.props.canSetCanonicalAlias}
onItemAdded={this.onAltAliasAdded}
onItemRemoved={this.onAltAliasDeleted}
suggestionsListId="mx_AliasSettings_altRecommendations"
itemsLabel={_t('Other published addresses:')}
noItemsLabel={_t('No other published addresses yet, add one below')}
placeholder={_t('New published address (e.g. #alias:server)')}
/>
<span className='mx_SettingsTab_subheading mx_AliasSettings_localAliasHeader'>{_t("Local Addresses")}</span>
<p>{_t("Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", {localDomain})}</p>
<details onToggle={this.onLocalAliasesToggled}>
<summary>{ this.state.detailsOpen ? _t('Show less') : _t("Show more")}</summary>
{localAliasesList}
</details>
</div>
);
}

View file

@ -0,0 +1,60 @@
/*
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 LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import {_t} from "../../../languageHandler";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
export default class RoomPublishSetting extends React.PureComponent {
constructor(props) {
super(props);
this.state = {isRoomPublished: false};
}
onRoomPublishChange = (e) => {
const valueBefore = this.state.isRoomPublished;
const newValue = !valueBefore;
this.setState({isRoomPublished: newValue});
const client = MatrixClientPeg.get();
client.setRoomDirectoryVisibility(
this.props.roomId,
newValue ? 'public' : 'private',
).catch(() => {
// Roll back the local echo on the change
this.setState({isRoomPublished: valueBefore});
});
};
componentDidMount() {
const client = MatrixClientPeg.get();
client.getRoomDirectoryVisibility(this.props.roomId).then((result => {
this.setState({isRoomPublished: result.visibility === 'public'});
}));
}
render() {
const client = MatrixClientPeg.get();
return (<LabelledToggleSwitch value={this.state.isRoomPublished}
onChange={this.onRoomPublishChange}
disabled={!this.props.canSetCanonicalAlias}
label={_t("Publish this room to the public in %(domain)s's room directory?", {
domain: client.getDomain(),
})} />);
}
}

View file

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

View file

@ -393,6 +393,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 && !event.shiftKey) {
setSelection(this._editorRef, model, {
index: 0,
offset: 0,
});
handled = true;
// move selection to end of composer
} else if (modKey && event.key === Key.END && !event.shiftKey) {
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;
@ -458,10 +472,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);
@ -491,6 +509,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);
@ -545,6 +564,7 @@ export default class BasicMessageEditor extends React.Component {
return;
}
this.historyManager.ensureLastChangesPushed(this.props.model);
this._modifiedFlag = true;
switch (action) {
case "bold":
toggleInlineFormat(range, "**");

View file

@ -175,7 +175,8 @@ const EntityTile = createReactClass({
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
const av = this.props.avatarJsx ||
<BaseAvatar name={this.props.name} width={36} height={36} aria-hidden="true" />;
// The wrapping div is required to make the magic mouse listener work, for some reason.
return (

View file

@ -48,8 +48,6 @@ const eventTileTypes = {
const stateEventTileTypes = {
'm.room.encryption': 'messages.EncryptionEvent',
'm.room.aliases': 'messages.TextualEvent',
// 'm.room.aliases': 'messages.RoomAliasesEvent', // too complex
'm.room.canonical_alias': 'messages.TextualEvent',
'm.room.create': 'messages.RoomCreate',
'm.room.member': 'messages.TextualEvent',
@ -100,6 +98,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];
}

View file

@ -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}
>

View file

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

View file

@ -208,7 +208,7 @@ export default createReactClass({
}
const av = (
<MemberAvatar member={member} width={36} height={36} />
<MemberAvatar member={member} width={36} height={36} aria-hidden="true" />
);
if (member.user) {

View file

@ -178,6 +178,7 @@ export default class MessageComposer extends React.Component {
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
tombstone: this._getRoomTombstone(),
canSendMessages: this.props.room.maySendMessage(),
showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
};
}
@ -325,10 +326,20 @@ export default class MessageComposer extends React.Component {
permalinkCreator={this.props.permalinkCreator} />,
<Stickerpicker key='stickerpicker_controls_button' room={this.props.room} />,
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
callInProgress ? <HangupButton key="controls_hangup" roomId={this.props.room.roomId} /> : null,
callInProgress ? null : <CallButton key="controls_call" roomId={this.props.room.roomId} />,
callInProgress ? null : <VideoCallButton key="controls_videocall" roomId={this.props.room.roomId} />,
);
if (this.state.showCallButtons) {
if (callInProgress) {
controls.push(
<HangupButton key="controls_hangup" roomId={this.props.room.roomId} />,
);
} else {
controls.push(
<CallButton key="controls_call" roomId={this.props.room.roomId} />,
<VideoCallButton key="controls_videocall" roomId={this.props.room.roomId} />,
);
}
}
} else if (this.state.tombstone) {
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
@ -341,7 +352,7 @@ export default class MessageComposer extends React.Component {
</a>
) : '';
controls.push(<div className="mx_MessageComposer_replaced_wrapper">
controls.push(<div className="mx_MessageComposer_replaced_wrapper" key="room_replaced">
<div className="mx_MessageComposer_replaced_valign">
<img className="mx_MessageComposer_roomReplaced_icon" src={require("../../../../res/img/room_replaced.svg")} />
<span className="mx_MessageComposer_roomReplaced_header">

View file

@ -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';
@ -700,13 +700,11 @@ export default createReactClass({
list: [],
extraTiles: this._makeGroupInviteTiles(this.props.searchFilter),
label: _t('Community Invites'),
order: "recent",
isInvite: true,
},
{
list: this.state.lists['im.vector.fake.invite'],
label: _t('Invites'),
order: "recent",
incomingCall: incomingCallIfTaggedAs('im.vector.fake.invite'),
isInvite: true,
},
@ -714,22 +712,19 @@ export default createReactClass({
list: this.state.lists['m.favourite'],
label: _t('Favourites'),
tagName: "m.favourite",
order: "manual",
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",
order: "recent",
incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'),
tagName: TAG_DM,
incomingCall: incomingCallIfTaggedAs(TAG_DM),
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'});},
addRoomLabel: _t("Start chat"),
},
{
list: this.state.lists['im.vector.fake.recent'],
label: _t('Rooms'),
order: "recent",
incomingCall: incomingCallIfTaggedAs('im.vector.fake.recent'),
onAddRoom: () => {dis.dispatch({action: 'view_create_room'});},
},
@ -744,7 +739,6 @@ export default createReactClass({
key: tagName,
label: labelForTagName(tagName),
tagName: tagName,
order: "manual",
incomingCall: incomingCallIfTaggedAs(tagName),
};
});
@ -754,13 +748,11 @@ export default createReactClass({
list: this.state.lists['m.lowpriority'],
label: _t('Low priority'),
tagName: "m.lowpriority",
order: "recent",
incomingCall: incomingCallIfTaggedAs('m.lowpriority'),
},
{
list: this.state.lists['im.vector.fake.archived'],
label: _t('Historical'),
order: "recent",
incomingCall: incomingCallIfTaggedAs('im.vector.fake.archived'),
startAsHidden: true,
showSpinner: this.state.isLoadingLeftRooms,
@ -770,7 +762,6 @@ export default createReactClass({
list: this.state.lists['m.server_notice'],
label: _t('System Alerts'),
tagName: "m.lowpriority",
order: "recent",
incomingCall: incomingCallIfTaggedAs('m.server_notice'),
},
]);
@ -787,6 +778,9 @@ export default createReactClass({
className="mx_RoomList"
role="tree"
aria-label={_t("Rooms")}
// Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order.
tabIndex="-1"
onMouseMove={this.onMouseMove}
onMouseLeave={this.onMouseLeave}
>

View file

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

View file

@ -490,16 +490,16 @@ export default createReactClass({
height="13"
alt="dm"
/>;
}
const { room } = this.props;
const member = room.getMember(dmUserId);
if (
member && member.membership === "join" && room.getJoinedMemberCount() === 2 &&
SettingsStore.isFeatureEnabled("feature_presence_in_room_list")
) {
const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot');
dmOnline = <UserOnlineDot userId={dmUserId} />;
}
const { room } = this.props;
const member = room.getMember(dmUserId);
if (
member && member.membership === "join" && room.getJoinedMemberCount() === 2 &&
SettingsStore.isFeatureEnabled("feature_presence_in_room_list")
) {
const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot');
dmOnline = <UserOnlineDot userId={dmUserId} />;
}
// The following labels are written in such a fashion to increase screen reader efficiency (speed).
@ -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} />;
}
}

View file

@ -27,6 +27,7 @@ export default createReactClass({
propTypes: {
onScrollUpClick: PropTypes.func,
onCloseClick: PropTypes.func,
},
render: function() {
@ -36,6 +37,10 @@ export default createReactClass({
title={_t('Jump to first unread message.')}
onClick={this.props.onScrollUpClick}>
</AccessibleButton>
<AccessibleButton className="mx_TopUnreadMessagesBar_markAsRead"
title={_t('Mark all as read')}
onClick={this.props.onCloseClick}>
</AccessibleButton>
</div>
);
},

View file

@ -119,7 +119,7 @@ export default createReactClass({
'In future this will be improved.',
) }
{' '}
<a href="https://github.com/vector-im/riot-web/issues/2671" target="_blank" rel="noopener">
<a href="https://github.com/vector-im/riot-web/issues/2671" target="_blank" rel="noreferrer noopener">
https://github.com/vector-im/riot-web/issues/2671
</a>
</div>,

View file

@ -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>
);
}

View file

@ -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>,
},
)
}

View file

@ -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();
},
@ -278,25 +277,25 @@ export default class KeyBackupPanel extends React.PureComponent {
"Backup has an <validity>invalid</validity> signature from this session",
{}, { validity },
);
} else if (sig.valid && sig.device.isVerified()) {
} else if (sig.valid && sig.deviceTrust.isVerified()) {
sigStatus = _t(
"Backup has a <validity>valid</validity> signature from " +
"<verify>verified</verify> session <device></device>",
{}, { validity, verify, device },
);
} else if (sig.valid && !sig.device.isVerified()) {
} else if (sig.valid && !sig.deviceTrust.isVerified()) {
sigStatus = _t(
"Backup has a <validity>valid</validity> signature from " +
"<verify>unverified</verify> session <device></device>",
{}, { validity, verify, device },
);
} else if (!sig.valid && sig.device.isVerified()) {
} else if (!sig.valid && sig.deviceTrust.isVerified()) {
sigStatus = _t(
"Backup has an <validity>invalid</validity> signature from " +
"<verify>verified</verify> session <device></device>",
{}, { validity, verify, device },
);
} else if (!sig.valid && !sig.device.isVerified()) {
} else if (!sig.valid && !sig.deviceTrust.isVerified()) {
sigStatus = _t(
"Backup has an <validity>invalid</validity> signature from " +
"<verify>unverified</verify> session <device></device>",

View file

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

View file

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

View file

@ -21,7 +21,6 @@ import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
import * as sdk from "../../../../..";
import AccessibleButton from "../../../elements/AccessibleButton";
import dis from "../../../../../dispatcher";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
export default class GeneralRoomSettingsTab extends React.Component {
@ -39,26 +38,6 @@ export default class GeneralRoomSettingsTab extends React.Component {
};
}
componentWillMount() {
this.context.getRoomDirectoryVisibility(this.props.roomId).then((result => {
this.setState({isRoomPublished: result.visibility === 'public'});
}));
}
onRoomPublishChange = (e) => {
const valueBefore = this.state.isRoomPublished;
const newValue = !valueBefore;
this.setState({isRoomPublished: newValue});
this.context.setRoomDirectoryVisibility(
this.props.roomId,
newValue ? 'public' : 'private',
).catch(() => {
// Roll back the local echo on the change
this.setState({isRoomPublished: valueBefore});
});
};
_onLeaveClick = () => {
dis.dispatch({
action: 'leave_room',
@ -75,7 +54,6 @@ export default class GeneralRoomSettingsTab extends React.Component {
const room = client.getRoom(this.props.roomId);
const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this
const canActuallySetAliases = room.currentState.mayClientSendStateEvent("m.room.aliases", client);
const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", '');
const aliasEvents = room.currentState.getStateEvents("m.room.aliases");
@ -90,21 +68,13 @@ export default class GeneralRoomSettingsTab extends React.Component {
<RoomProfileSettings roomId={this.props.roomId} />
</div>
<span className='mx_SettingsTab_subheading'>{_t("Room Addresses")}</span>
<div className="mx_SettingsTab_heading">{_t("Room Addresses")}</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<AliasSettings roomId={this.props.roomId}
canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases}
canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} />
</div>
<div className='mx_SettingsTab_section'>
<LabelledToggleSwitch value={this.state.isRoomPublished}
onChange={this.onRoomPublishChange}
disabled={!canActuallySetAliases}
label={_t("Publish this room to the public in %(domain)s's room directory?", {
domain: client.getDomain(),
})} />
</div>
<div className="mx_SettingsTab_heading">{_t("Other")}</div>
<span className='mx_SettingsTab_subheading'>{_t("Flair")}</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<RelatedGroupSettings roomId={room.roomId}

View file

@ -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>;
},
},

View file

@ -61,6 +61,8 @@ export default class GeneralUserSettingsTab extends React.Component {
emails: [],
msisdns: [],
...this._calculateThemeState(),
customThemeUrl: "",
customThemeMessage: {isError: false, text: ""},
};
this.dispatcherRef = dis.register(this._onAction);
@ -274,6 +276,41 @@ export default class GeneralUserSettingsTab extends React.Component {
});
};
_onAddCustomTheme = async () => {
let currentThemes = SettingsStore.getValue("custom_themes");
if (!currentThemes) currentThemes = [];
currentThemes = currentThemes.map(c => c); // cheap clone
if (this._themeTimer) {
clearTimeout(this._themeTimer);
}
try {
const r = await fetch(this.state.customThemeUrl);
const themeInfo = await r.json();
if (!themeInfo || typeof(themeInfo['name']) !== 'string' || typeof(themeInfo['colors']) !== 'object') {
this.setState({customThemeMessage: {text: _t("Invalid theme schema."), isError: true}});
return;
}
currentThemes.push(themeInfo);
} catch (e) {
console.error(e);
this.setState({customThemeMessage: {text: _t("Error downloading theme information."), isError: true}});
return; // Don't continue on error
}
await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes);
this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}});
this._themeTimer = setTimeout(() => {
this.setState({customThemeMessage: {text: "", isError: false}});
}, 3000);
};
_onCustomThemeChange = (e) => {
this.setState({customThemeUrl: e.target.value});
};
_renderProfileSection() {
return (
<div className="mx_SettingsTab_section">
@ -368,6 +405,45 @@ export default class GeneralUserSettingsTab extends React.Component {
/>
</div>;
}
let customThemeForm;
if (SettingsStore.isFeatureEnabled("feature_custom_themes")) {
let messageElement = null;
if (this.state.customThemeMessage.text) {
if (this.state.customThemeMessage.isError) {
messageElement = <div className='text-error'>{this.state.customThemeMessage.text}</div>;
} else {
messageElement = <div className='text-success'>{this.state.customThemeMessage.text}</div>;
}
}
customThemeForm = (
<div className='mx_SettingsTab_section'>
<form onSubmit={this._onAddCustomTheme}>
<Field
label={_t("Custom theme URL")}
type='text'
id='mx_GeneralUserSettingsTab_customThemeInput'
autoComplete="off"
onChange={this._onCustomThemeChange}
value={this.state.customThemeUrl}
/>
<AccessibleButton
onClick={this._onAddCustomTheme}
type="submit" kind="primary_sm"
disabled={!this.state.customThemeUrl.trim()}
>{_t("Add theme")}</AccessibleButton>
{messageElement}
</form>
</div>
);
}
const themes = Object.entries(enumerateThemes())
.map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
const customThemes = themes.filter(p => !builtInThemes.includes(p))
.sort((a, b) => a.name.localeCompare(b.name));
const orderedThemes = [...builtInThemes, ...customThemes];
return (
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_themeSection">
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
@ -376,10 +452,11 @@ export default class GeneralUserSettingsTab extends React.Component {
value={this.state.theme} onChange={this._onThemeChange}
disabled={this.state.useSystemTheme}
>
{Object.entries(enumerateThemes()).map(([theme, text]) => {
return <option key={theme} value={theme}>{text}</option>;
{orderedThemes.map(theme => {
return <option key={theme.id} value={theme.id}>{theme.name}</option>;
})}
</Field>
{customThemeForm}
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} />
</div>
);

View file

@ -25,21 +25,6 @@ import Modal from "../../../../../Modal";
import * as sdk from "../../../../../";
import PlatformPeg from "../../../../../PlatformPeg";
// Simple method to help prettify GH Release Tags and Commit Hashes.
const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i;
const ghVersionLabel = function(repo, token='') {
const match = token.match(semVerRegex);
let url;
if (match && match[1]) { // basic semVer string possibly with commit hash
url = (match.length > 1 && match[2])
? `https://github.com/${repo}/commit/${match[2]}`
: `https://github.com/${repo}/releases/tag/v${match[1]}`;
} else {
url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
}
return <a target="_blank" rel="noopener" href={url}>{ token }</a>;
};
export default class HelpUserSettingsTab extends React.Component {
static propTypes = {
closeSettingsFn: PropTypes.func.isRequired,
@ -110,7 +95,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 +117,27 @@ export default class HelpUserSettingsTab extends React.Component {
<span className='mx_SettingsTab_subheading'>{_t("Credits")}</span>
<ul>
<li>
The <a href="themes/riot/img/backgrounds/valley.jpg" rel="noopener" target="_blank">
The <a href="themes/riot/img/backgrounds/valley.jpg" rel="noreferrer noopener" target="_blank">
default cover photo</a> is ©&nbsp;
<a href="https://www.flickr.com/golan" rel="noopener" target="_blank">Jesús Roncero</a>{' '}
<a href="https://www.flickr.com/golan" rel="noreferrer noopener" target="_blank">Jesús Roncero</a>{' '}
used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noopener" target="_blank">
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noreferrer noopener" target="_blank">
CC-BY-SA 4.0</a>.
</li>
<li>
The <a href="https://github.com/matrix-org/twemoji-colr" rel="noopener" target="_blank">
twemoji-colr</a> font is ©&nbsp;
<a href="https://mozilla.org" rel="noopener" target="_blank">Mozilla Foundation</a>{' '}
The <a href="https://github.com/matrix-org/twemoji-colr" rel="noreferrer noopener"
target="_blank"> twemoji-colr</a> font is ©&nbsp;
<a href="https://mozilla.org" rel="noreferrer noopener" target="_blank">Mozilla Foundation</a>{' '}
used under the terms of&nbsp;
<a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noopener" target="_blank">
<a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener" target="_blank">
Apache 2.0</a>.
</li>
<li>
The <a href="https://twemoji.twitter.com/" rel="noopener" target="_blank">
The <a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank">
Twemoji</a> emoji art is ©&nbsp;
<a href="https://twemoji.twitter.com/" rel="noopener" target="_blank">Twitter, Inc and other
<a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank">Twitter, Inc and other
contributors</a> used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by/4.0/" rel="noopener" target="_blank">
<a href="https://creativecommons.org/licenses/by/4.0/" rel="noreferrer noopener" target="_blank">
CC-BY 4.0</a>.
</li>
</ul>
@ -162,7 +147,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 +156,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>,
})
}
@ -183,9 +169,7 @@ export default class HelpUserSettingsTab extends React.Component {
);
}
const vectorVersion = this.state.vectorVersion
? ghVersionLabel('vector-im/riot-web', this.state.vectorVersion)
: 'unknown';
const vectorVersion = this.state.vectorVersion || 'unknown';
let olmVersion = MatrixClientPeg.get().olmVersion;
olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : '<not-enabled>';
@ -224,6 +208,15 @@ export default class HelpUserSettingsTab extends React.Component {
{_t("Clear cache and reload")}
</AccessibleButton>
</div>
{
_t( "To report a Matrix-related security issue, please read the Matrix.org " +
"<a>Security Disclosure Policy</a>.", {},
{
'a': (sub) =>
<a href="https://matrix.org/security-disclosure-policy/"
rel="noreferrer noopener" target="_blank">{sub}</a>,
})
}
</div>
</div>
<div className='mx_SettingsTab_section'>

View file

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

View file

@ -25,6 +25,12 @@ import * as sdk from "../../../../..";
import PlatformPeg from "../../../../../PlatformPeg";
export default class PreferencesUserSettingsTab extends React.Component {
static ROOM_LIST_SETTINGS = [
'RoomList.orderAlphabetically',
'RoomList.orderByImportance',
'breadcrumbs',
];
static COMPOSER_SETTINGS = [
'MessageComposerInput.autoReplaceEmoji',
'MessageComposerInput.suggestEmoji',
@ -47,11 +53,6 @@ export default class PreferencesUserSettingsTab extends React.Component {
'showImages',
];
static ROOM_LIST_SETTINGS = [
'RoomList.orderByImportance',
'breadcrumbs',
];
static ADVANCED_SETTINGS = [
'alwaysShowEncryptionIcons',
'Pill.shouldShowPillAvatar',
@ -173,15 +174,21 @@ export default class PreferencesUserSettingsTab extends React.Component {
<div className="mx_SettingsTab_heading">{_t("Preferences")}</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
{this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
<span className="mx_SettingsTab_subheading">{_t("Timeline")}</span>
{this._renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
<span className="mx_SettingsTab_subheading">{_t("Room list")}</span>
{this._renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
{this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Timeline")}</span>
{this._renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Advanced")}</span>
{this._renderGroup(PreferencesUserSettingsTab.ADVANCED_SETTINGS)}
{minimizeToTrayOption}

View file

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

View file

@ -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);
};

View file

@ -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);
}

View file

@ -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"
/>;
}