Merge branch 'develop' into travis/reset-passphrase

This commit is contained in:
Travis Ralston 2021-04-12 14:55:31 -06:00
commit 106de5f7ba
45 changed files with 423 additions and 198 deletions

View file

@ -95,9 +95,10 @@ function textForMemberEvent(ev) {
senderName,
targetName,
}) + ' ' + reason;
} else {
// sender is not target and made the target leave, if not from invite/ban then this is a kick
} else if (prevContent.membership === "join") {
return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason;
} else {
return "";
}
}
}

View file

@ -84,6 +84,7 @@ import {replaceableComponent} from "../../utils/replaceableComponent";
import RoomListStore from "../../stores/room-list/RoomListStore";
import {RoomUpdateCause} from "../../stores/room-list/models";
import defaultDispatcher from "../../dispatcher/dispatcher";
import SecurityCustomisations from "../../customisations/Security";
/** constants for MatrixChat.state.view */
export enum Views {
@ -395,7 +396,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId());
if (crossSigningIsSetUp) {
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
if (SecurityCustomisations.SHOW_ENCRYPTION_SETUP_UI === false) {
this.onLoggedIn();
} else {
this.setStateForNewView({view: Views.COMPLETE_SECURITY});
}
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
this.setStateForNewView({ view: Views.E2E_SETUP });
} else {

View file

@ -1137,10 +1137,16 @@ export default class RoomView extends React.Component<IProps, IState> {
ev.stopPropagation();
ev.preventDefault();
this.setState({
dragCounter: this.state.dragCounter + 1,
draggingFile: true,
});
// We always increment the counter no matter the types, because dragging is
// still happening. If we didn't, the drag counter would get out of sync.
this.setState({dragCounter: this.state.dragCounter + 1});
// See:
// https://docs.w3cub.com/dom/datatransfer/types
// https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#file
if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) {
this.setState({draggingFile: true});
}
};
private onDragLeave = ev => {
@ -1164,6 +1170,9 @@ export default class RoomView extends React.Component<IProps, IState> {
ev.dataTransfer.dropEffect = 'none';
// See:
// https://docs.w3cub.com/dom/datatransfer/types
// https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#file
if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) {
ev.dataTransfer.dropEffect = 'copy';
}

View file

@ -712,8 +712,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
this.props.onFinished();
}
if (cli.isRoomEncrypted(this.props.roomId) &&
SettingsStore.getValue("feature_room_history_key_sharing")) {
if (cli.isRoomEncrypted(this.props.roomId)) {
const visibilityEvent = room.currentState.getStateEvents(
"m.room.history_visibility", "",
);
@ -1344,8 +1343,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
buttonText = _t("Invite");
goButtonFn = this._inviteUsers;
if (SettingsStore.getValue("feature_room_history_key_sharing") &&
cli.isRoomEncrypted(this.props.roomId)) {
if (cli.isRoomEncrypted(this.props.roomId)) {
const room = cli.getRoom(this.props.roomId);
const visibilityEvent = room.currentState.getStateEvents(
"m.room.history_visibility", "",

View file

@ -55,22 +55,10 @@ interface IProps {
* The mxc:// avatar URL of the displayed user
*/
avatarUrl?: string;
/**
* Whether the EventTile should appear faded
*/
faded?: boolean;
/**
* Callback for when the component is clicked
*/
onClick?: () => void;
}
interface IState {
message: string;
faded: boolean;
eventTileKey: number;
}
const AVATAR_SIZE = 32;
@ -81,23 +69,9 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
super(props);
this.state = {
message: props.message,
faded: !!props.faded,
eventTileKey: 0,
};
}
changeMessage(message: string) {
this.setState({
message,
// Change the EventTile key to force React to create a new instance
eventTileKey: this.state.eventTileKey + 1,
});
}
unfade() {
this.setState({ faded: false });
}
private fakeEvent({message}: IState) {
// Fake it till we make it
/* eslint-disable quote-props */
@ -147,12 +121,10 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
const className = classnames(this.props.className, {
"mx_IRCLayout": this.props.layout == Layout.IRC,
"mx_GroupLayout": this.props.layout == Layout.Group,
"mx_EventTilePreview_faded": this.state.faded,
});
return <div className={className} onClick={this.props.onClick}>
return <div className={className}>
<EventTile
key={this.state.eventTileKey}
mxEvent={event}
layout={this.props.layout}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}

View file

@ -0,0 +1,62 @@
/*
Copyright 2021 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 classNames from "classnames";
import React from "react";
import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
reason: string;
}
interface IState {
hidden: boolean;
}
@replaceableComponent("views.elements.InviteReason")
export default class InviteReason extends React.PureComponent<IProps, IState> {
constructor(props) {
super(props);
this.state = {
// We hide the reason for invitation by default, since it can be a
// vector for spam/harassment.
hidden: true,
};
}
onViewClick = () => {
this.setState({
hidden: false,
});
}
render() {
const classes = classNames({
"mx_InviteReason": true,
"mx_InviteReason_hidden": this.state.hidden,
});
return <div className={classes}>
<div className="mx_InviteReason_reason">{this.props.reason}</div>
<div className="mx_InviteReason_view"
onClick={this.onViewClick}
>
{_t("View message")}
</div>
</div>;
}
}

View file

@ -1494,7 +1494,7 @@ const UserInfoHeader: React.FC<{
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
}
const displayName = member.name || member.displayname;
const displayName = member.rawDisplayName || member.displayname;
return <React.Fragment>
{ avatarElement }

View file

@ -1,7 +1,5 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2015-2021 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.
@ -25,10 +23,10 @@ import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import SdkConfig from "../../../SdkConfig";
import IdentityAuthClient from '../../../IdentityAuthClient';
import SettingsStore from "../../../settings/SettingsStore";
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import InviteReason from "../elements/InviteReason";
const MessageCase = Object.freeze({
NotLoggedIn: "NotLoggedIn",
@ -303,7 +301,6 @@ export default class RoomPreviewBar extends React.Component {
const brand = SdkConfig.get().brand;
const Spinner = sdk.getComponent('elements.Spinner');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const EventTilePreview = sdk.getComponent('elements.EventTilePreview');
let showSpinner = false;
let title;
@ -497,24 +494,7 @@ export default class RoomPreviewBar extends React.Component {
const myUserId = MatrixClientPeg.get().getUserId();
const reason = this.props.room.currentState.getMember(myUserId).events.member.event.content.reason;
if (reason) {
this.reasonElement = React.createRef();
// We hide the reason for invitation by default, since it can be a
// vector for spam/harassment.
const showReason = () => {
this.reasonElement.current.unfade();
this.reasonElement.current.changeMessage(reason);
};
reasonElement = <EventTilePreview
ref={this.reasonElement}
onClick={showReason}
className="mx_RoomPreviewBar_reason"
message={_t("Invite messages are hidden by default. Click to show the message.")}
layout={SettingsStore.getValue("layout")}
userId={inviteMember.userId}
displayName={inviteMember.rawDisplayName}
avatarUrl={inviteMember.events.member.event.content.avatar_url}
faded={true}
/>;
reasonElement = <InviteReason reason={reason} />;
}
primaryActionHandler = this.props.onJoinClick;

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019, 2021 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.
@ -22,17 +22,19 @@ import * as sdk from "../../../../..";
import AccessibleButton from "../../../elements/AccessibleButton";
import Modal from "../../../../../Modal";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import {EventType} from "matrix-js-sdk/src/@types/event";
const plEventsToLabels = {
// These will be translated for us later.
"m.room.avatar": _td("Change room avatar"),
"m.room.name": _td("Change room name"),
"m.room.canonical_alias": _td("Change main address for the room"),
"m.room.history_visibility": _td("Change history visibility"),
"m.room.power_levels": _td("Change permissions"),
"m.room.topic": _td("Change topic"),
"m.room.tombstone": _td("Upgrade the room"),
"m.room.encryption": _td("Enable room encryption"),
[EventType.RoomAvatar]: _td("Change room avatar"),
[EventType.RoomName]: _td("Change room name"),
[EventType.RoomCanonicalAlias]: _td("Change main address for the room"),
[EventType.RoomHistoryVisibility]: _td("Change history visibility"),
[EventType.RoomPowerLevels]: _td("Change permissions"),
[EventType.RoomTopic]: _td("Change topic"),
[EventType.RoomTombstone]: _td("Upgrade the room"),
[EventType.RoomEncryption]: _td("Enable room encryption"),
[EventType.RoomServerAcl]: _td("Change server ACLs"),
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
"im.vector.modular.widgets": _td("Modify widgets"),
@ -40,14 +42,15 @@ const plEventsToLabels = {
const plEventsToShow = {
// If an event is listed here, it will be shown in the PL settings. Defaults will be calculated.
"m.room.avatar": {isState: true},
"m.room.name": {isState: true},
"m.room.canonical_alias": {isState: true},
"m.room.history_visibility": {isState: true},
"m.room.power_levels": {isState: true},
"m.room.topic": {isState: true},
"m.room.tombstone": {isState: true},
"m.room.encryption": {isState: true},
[EventType.RoomAvatar]: {isState: true},
[EventType.RoomName]: {isState: true},
[EventType.RoomCanonicalAlias]: {isState: true},
[EventType.RoomHistoryVisibility]: {isState: true},
[EventType.RoomPowerLevels]: {isState: true},
[EventType.RoomTopic]: {isState: true},
[EventType.RoomTombstone]: {isState: true},
[EventType.RoomEncryption]: {isState: true},
[EventType.RoomServerAcl]: {isState: true},
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
"im.vector.modular.widgets": {isState: true},

View file

@ -74,8 +74,20 @@ export interface ISecurityCustomisations {
catchAccessSecretStorageError?: typeof catchAccessSecretStorageError,
setupEncryptionNeeded?: typeof setupEncryptionNeeded,
getDehydrationKey?: typeof getDehydrationKey,
/**
* When false, disables the post-login UI from showing. If there's
* an error during setup, that will be shown to the user.
*
* Note: when this is set to false then the app will assume the user's
* encryption is set up some other way which would circumvent the default
* UI, such as by presenting alternative UI.
*/
SHOW_ENCRYPTION_SETUP_UI?: boolean, // default true
}
// A real customisation module will define and export one or more of the
// customisation points that make up `ISecurityCustomisations`.
export default {} as ISecurityCustomisations;
export default {
SHOW_ENCRYPTION_SETUP_UI: true,
} as ISecurityCustomisations;

View file

@ -143,11 +143,11 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl
// math nodes are translated back into delimited latex strings
if (n.hasAttribute("data-mx-maths")) {
const delimLeft = (n.nodeName == "SPAN") ?
(SdkConfig.get()['latex_maths_delims'] || {})['inline_left'] || "$" :
(SdkConfig.get()['latex_maths_delims'] || {})['display_left'] || "$$";
((SdkConfig.get()['latex_maths_delims'] || {})['inline'] || {})['left'] || "\\(" :
((SdkConfig.get()['latex_maths_delims'] || {})['display'] || {})['left'] || "\\[";
const delimRight = (n.nodeName == "SPAN") ?
(SdkConfig.get()['latex_maths_delims'] || {})['inline_right'] || "$" :
(SdkConfig.get()['latex_maths_delims'] || {})['display_right'] || "$$";
((SdkConfig.get()['latex_maths_delims'] || {})['inline'] || {})['right'] || "\\)" :
((SdkConfig.get()['latex_maths_delims'] || {})['display'] || {})['right'] || "\\]";
const tex = n.getAttribute("data-mx-maths");
return partCreator.plain(delimLeft + tex + delimRight);
} else if (!checkDescendInto(n)) {

View file

@ -47,21 +47,65 @@ export function mdSerialize(model: EditorModel) {
export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) {
let md = mdSerialize(model);
// copy of raw input to remove unwanted math later
const orig = md;
if (SettingsStore.getValue("feature_latex_maths")) {
const displayPattern = (SdkConfig.get()['latex_maths_delims'] || {})['display_pattern'] ||
"\\$\\$(([^$]|\\\\\\$)*)\\$\\$";
const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] ||
"\\$(([^$]|\\\\\\$)*)\\$";
const patternNames = ['tex', 'latex'];
const patternTypes = ['display', 'inline'];
const patternDefaults = {
"tex": {
// detect math with tex delimiters, inline: $...$, display $$...$$
// preferably use negative lookbehinds, not supported in all major browsers:
// const displayPattern = "^(?<!\\\\)\\$\\$(?![ \\t])(([^$]|\\\\\\$)+?)\\$\\$$";
// const inlinePattern = "(?:^|\\s)(?<!\\\\)\\$(?!\\s)(([^$]|\\\\\\$)+?)(?<!\\\\|\\s)\\$";
md = md.replace(RegExp(displayPattern, "gm"), function(m, p1) {
const p1e = AllHtmlEntities.encode(p1);
return `<div data-mx-maths="${p1e}">\n\n</div>\n\n`;
});
// conditions for display math detection $$...$$:
// - pattern starts at beginning of line or is not prefixed with backslash or dollar
// - left delimiter ($$) is not escaped by backslash
"display": "(^|[^\\\\$])\\$\\$(([^$]|\\\\\\$)+?)\\$\\$",
md = md.replace(RegExp(inlinePattern, "gm"), function(m, p1) {
const p1e = AllHtmlEntities.encode(p1);
return `<span data-mx-maths="${p1e}"></span>`;
// conditions for inline math detection $...$:
// - pattern starts at beginning of line, follows whitespace character or punctuation
// - pattern is on a single line
// - left and right delimiters ($) are not escaped by backslashes
// - left delimiter is not followed by whitespace character
// - right delimiter is not prefixed with whitespace character
"inline":
"(^|\\s|[.,!?:;])(?!\\\\)\\$(?!\\s)(([^$\\n]|\\\\\\$)*([^\\\\\\s\\$]|\\\\\\$)(?:\\\\\\$)?)\\$",
},
"latex": {
// detect math with latex delimiters, inline: \(...\), display \[...\]
// conditions for display math detection \[...\]:
// - pattern starts at beginning of line or is not prefixed with backslash
// - pattern is not empty
"display": "(^|[^\\\\])\\\\\\[(?!\\\\\\])(.*?)\\\\\\]",
// conditions for inline math detection \(...\):
// - pattern starts at beginning of line or is not prefixed with backslash
// - pattern is not empty
"inline": "(^|[^\\\\])\\\\\\((?!\\\\\\))(.*?)\\\\\\)",
},
};
patternNames.forEach(function(patternName) {
patternTypes.forEach(function(patternType) {
// get the regex replace pattern from config or use the default
const pattern = (((SdkConfig.get()["latex_maths_delims"] ||
{})[patternType] || {})["pattern"] || {})[patternName] ||
patternDefaults[patternName][patternType];
md = md.replace(RegExp(pattern, "gms"), function(m, p1, p2) {
const p2e = AllHtmlEntities.encode(p2);
switch (patternType) {
case "display":
return `${p1}<div data-mx-maths="${p2e}">\n\n</div>\n\n`;
case "inline":
return `${p1}<span data-mx-maths="${p2e}"></span>`;
}
});
});
});
// make sure div tags always start on a new line, otherwise it will confuse
@ -73,15 +117,29 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
if (!parser.isPlainText() || forceHTML) {
// feed Markdown output to HTML parser
const phtml = cheerio.load(parser.toHTML(),
{ _useHtmlParser2: true, decodeEntities: false })
{ _useHtmlParser2: true, decodeEntities: false });
// add fallback output for latex math, which should not be interpreted as markdown
phtml('div, span').each(function(i, e) {
const tex = phtml(e).attr('data-mx-maths')
if (tex) {
phtml(e).html(`<code>${tex}</code>`)
}
});
if (SettingsStore.getValue("feature_latex_maths")) {
// original Markdown without LaTeX replacements
const parserOrig = new Markdown(orig);
const phtmlOrig = cheerio.load(parserOrig.toHTML(),
{ _useHtmlParser2: true, decodeEntities: false });
// since maths delimiters are handled before Markdown,
// code blocks could contain mangled content.
// replace code blocks with original content
phtmlOrig('code').each(function(i) {
phtml('code').eq(i).text(phtmlOrig('code').eq(i).text());
});
// add fallback output for latex math, which should not be interpreted as markdown
phtml('div, span').each(function(i, e) {
const tex = phtml(e).attr('data-mx-maths')
if (tex) {
phtml(e).html(`<code>${tex}</code>`)
}
});
}
return phtml.html();
}
// ensure removal of escape backslashes in non-Markdown messages

View file

@ -800,7 +800,6 @@
"Show message previews for reactions in DMs": "Show message previews for reactions in DMs",
"Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms",
"Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices",
"Share decryption keys for room history when inviting users": "Share decryption keys for room history when inviting users",
"Enable advanced debugging for the room list": "Enable advanced debugging for the room list",
"Show info about bridges in room settings": "Show info about bridges in room settings",
"Font size": "Font size",
@ -1361,6 +1360,7 @@
"Change topic": "Change topic",
"Upgrade the room": "Upgrade the room",
"Enable room encryption": "Enable room encryption",
"Change server ACLs": "Change server ACLs",
"Modify widgets": "Modify widgets",
"Failed to unban": "Failed to unban",
"Unban": "Unban",
@ -1578,7 +1578,6 @@
"Start chatting": "Start chatting",
"Do you want to join %(roomName)s?": "Do you want to join %(roomName)s?",
"<userName/> invited you": "<userName/> invited you",
"Invite messages are hidden by default. Click to show the message.": "Invite messages are hidden by default. Click to show the message.",
"Reject": "Reject",
"Reject & Ignore user": "Reject & Ignore user",
"You're previewing %(roomName)s. Want to join it?": "You're previewing %(roomName)s. Want to join it?",
@ -1925,6 +1924,7 @@
"Rotate clockwise": "Rotate clockwise",
"Download this file": "Download this file",
"Information": "Information",
"View message": "View message",
"Language Dropdown": "Language Dropdown",
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
"%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times",

View file

@ -220,12 +220,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_room_history_key_sharing": {
isFeature: true,
displayName: _td("Share decryption keys for room history when inviting users"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"advancedRoomListLogging": {
// TODO: Remove flag before launch: https://github.com/vector-im/element-web/issues/14231
displayName: _td("Enable advanced debugging for the room list"),

View file

@ -376,16 +376,16 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.onRoomsUpdate();
}
// if the user was looking at the room and then joined select that space
if (room.getMyMembership() === "join" && room.roomId === RoomViewStore.getRoomId()) {
this.setActiveSpace(room);
}
if (room.getMyMembership() === "join") {
const numSuggestedRooms = this._suggestedRooms.length;
this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId);
if (numSuggestedRooms !== this._suggestedRooms.length) {
this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
if (!room.isSpaceRoom()) {
const numSuggestedRooms = this._suggestedRooms.length;
this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId);
if (numSuggestedRooms !== this._suggestedRooms.length) {
this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
}
} else if (room.roomId === RoomViewStore.getRoomId()) {
// if the user was looking at the space and then joined: select that space
this.setActiveSpace(room);
}
}
};

View file

@ -65,12 +65,17 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
return this.matches(room.name);
}
public matches(val: string): boolean {
private normalize(val: string): string {
// Note: we have to match the filter with the removeHiddenChars() room name because the
// function strips spaces and other characters (M becomes RN for example, in lowercase).
// We also doubly convert to lowercase to work around oddities of the library.
const noSecretsFilter = removeHiddenChars(this.search.toLowerCase()).toLowerCase();
const noSecretsName = removeHiddenChars(val.toLowerCase()).toLowerCase();
return noSecretsName.includes(noSecretsFilter);
return removeHiddenChars(val.toLowerCase())
// Strip all punctuation
.replace(/[\\'!"#$%&()*+,\-./:;<=>?@[\]^_`{|}~\u2000-\u206f\u2e00-\u2e7f]/g, "")
// We also doubly convert to lowercase to work around oddities of the library.
.toLowerCase();
}
public matches(val: string): boolean {
return this.normalize(val).includes(this.normalize(this.search));
}
}