Merge branch 'develop' into feature-multi-language-spell-check
This commit is contained in:
commit
aba5ef100f
29 changed files with 671 additions and 109 deletions
|
@ -76,6 +76,8 @@
|
||||||
"highlight.js": "^10.1.2",
|
"highlight.js": "^10.1.2",
|
||||||
"html-entities": "^1.3.1",
|
"html-entities": "^1.3.1",
|
||||||
"is-ip": "^2.0.0",
|
"is-ip": "^2.0.0",
|
||||||
|
"katex": "^0.12.0",
|
||||||
|
"cheerio": "^1.0.0-rc.3",
|
||||||
"linkifyjs": "^2.1.9",
|
"linkifyjs": "^2.1.9",
|
||||||
"lodash": "^4.17.19",
|
"lodash": "^4.17.19",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||||
|
|
|
@ -231,9 +231,29 @@ limitations under the License.
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mx_UserMenu_contextMenu_guestPrompts,
|
||||||
&.mx_UserMenu_contextMenu_hostingLink {
|
&.mx_UserMenu_contextMenu_hostingLink {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mx_UserMenu_contextMenu_guestPrompts {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
font-weight: 600;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
& + span {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_link {
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: inherit;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_IconizedContextMenu_icon {
|
.mx_IconizedContextMenu_icon {
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
// Sticker picker depends on the fixed height previously used for all tiles
|
// Sticker picker depends on the fixed height previously used for all tiles
|
||||||
height: 273px;
|
height: 283px; // height of the popout minus the AppTile menu bar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,6 +80,7 @@ import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType }
|
||||||
import Analytics from './Analytics';
|
import Analytics from './Analytics';
|
||||||
import CountlyAnalytics from "./CountlyAnalytics";
|
import CountlyAnalytics from "./CountlyAnalytics";
|
||||||
import {UIFeature} from "./settings/UIFeature";
|
import {UIFeature} from "./settings/UIFeature";
|
||||||
|
import { CallError } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
|
||||||
enum AudioID {
|
enum AudioID {
|
||||||
Ring = 'ringAudio',
|
Ring = 'ringAudio',
|
||||||
|
@ -226,11 +227,17 @@ export default class CallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
private setCallListeners(call: MatrixCall) {
|
private setCallListeners(call: MatrixCall) {
|
||||||
call.on(CallEvent.Error, (err) => {
|
call.on(CallEvent.Error, (err: CallError) => {
|
||||||
if (!this.matchesCallForThisRoom(call)) return;
|
if (!this.matchesCallForThisRoom(call)) return;
|
||||||
|
|
||||||
Analytics.trackEvent('voip', 'callError', 'error', err);
|
Analytics.trackEvent('voip', 'callError', 'error', err.toString());
|
||||||
console.error("Call error:", err);
|
console.error("Call error:", err);
|
||||||
|
|
||||||
|
if (err.code === CallErrorCode.NoUserMedia) {
|
||||||
|
this.showMediaCaptureError(call);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
MatrixClientPeg.get().getTurnServers().length === 0 &&
|
MatrixClientPeg.get().getTurnServers().length === 0 &&
|
||||||
SettingsStore.getValue("fallbackICEServerAllowed") === null
|
SettingsStore.getValue("fallbackICEServerAllowed") === null
|
||||||
|
@ -377,6 +384,34 @@ export default class CallHandler {
|
||||||
}, null, true);
|
}, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private showMediaCaptureError(call: MatrixCall) {
|
||||||
|
let title;
|
||||||
|
let description;
|
||||||
|
|
||||||
|
if (call.type === CallType.Voice) {
|
||||||
|
title = _t("Unable to access microphone");
|
||||||
|
description = <div>
|
||||||
|
{_t(
|
||||||
|
"Call failed because no microphone could not be accessed. " +
|
||||||
|
"Check that a microphone is plugged in and set up correctly.",
|
||||||
|
)}
|
||||||
|
</div>;
|
||||||
|
} else if (call.type === CallType.Video) {
|
||||||
|
title = _t("Unable to access webcam / microphone");
|
||||||
|
description = <div>
|
||||||
|
{_t("Call failed because no webcam or microphone could not be accessed. Check that:")}
|
||||||
|
<ul>
|
||||||
|
<li>{_t("A microphone and webcam are plugged in and set up correctly")}</li>
|
||||||
|
<li>{_t("Permission is granted to use the webcam")}</li>
|
||||||
|
<li>{_t("No other application is using the webcam")}</li>
|
||||||
|
</ul>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.createTrackedDialog('Media capture failed', '', ErrorDialog, {
|
||||||
|
title, description,
|
||||||
|
}, null, true);
|
||||||
|
}
|
||||||
|
|
||||||
private placeCall(
|
private placeCall(
|
||||||
roomId: string, type: PlaceCallType,
|
roomId: string, type: PlaceCallType,
|
||||||
|
|
|
@ -27,9 +27,12 @@ import _linkifyString from 'linkifyjs/string';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import EMOJIBASE_REGEX from 'emojibase-regex';
|
import EMOJIBASE_REGEX from 'emojibase-regex';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
|
import katex from 'katex';
|
||||||
|
import { AllHtmlEntities } from 'html-entities';
|
||||||
|
import SettingsStore from './settings/SettingsStore';
|
||||||
|
import cheerio from 'cheerio';
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
import SettingsStore from './settings/SettingsStore';
|
|
||||||
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
||||||
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
|
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
|
||||||
import ReplyThread from "./components/views/elements/ReplyThread";
|
import ReplyThread from "./components/views/elements/ReplyThread";
|
||||||
|
@ -240,7 +243,8 @@ const sanitizeHtmlParams: IExtendedSanitizeOptions = {
|
||||||
allowedAttributes: {
|
allowedAttributes: {
|
||||||
// custom ones first:
|
// custom ones first:
|
||||||
font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
||||||
span: ['data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix
|
span: ['data-mx-maths', 'data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix
|
||||||
|
div: ['data-mx-maths'],
|
||||||
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
|
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
|
||||||
img: ['src', 'width', 'height', 'alt', 'title'],
|
img: ['src', 'width', 'height', 'alt', 'title'],
|
||||||
ol: ['start'],
|
ol: ['start'],
|
||||||
|
@ -414,6 +418,21 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
|
||||||
if (isHtmlMessage) {
|
if (isHtmlMessage) {
|
||||||
isDisplayedWithHtml = true;
|
isDisplayedWithHtml = true;
|
||||||
safeBody = sanitizeHtml(formattedBody, sanitizeParams);
|
safeBody = sanitizeHtml(formattedBody, sanitizeParams);
|
||||||
|
|
||||||
|
if (SettingsStore.getValue("feature_latex_maths")) {
|
||||||
|
const phtml = cheerio.load(safeBody,
|
||||||
|
{ _useHtmlParser2: true, decodeEntities: false })
|
||||||
|
phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
|
||||||
|
return katex.renderToString(
|
||||||
|
AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')),
|
||||||
|
{
|
||||||
|
throwOnError: false,
|
||||||
|
displayMode: e.name == 'div',
|
||||||
|
output: "htmlAndMathml",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
safeBody = phtml.html();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
delete sanitizeParams.textFilter;
|
delete sanitizeParams.textFilter;
|
||||||
|
@ -515,7 +534,6 @@ export function checkBlockNode(node: Node) {
|
||||||
case "H6":
|
case "H6":
|
||||||
case "PRE":
|
case "PRE":
|
||||||
case "BLOCKQUOTE":
|
case "BLOCKQUOTE":
|
||||||
case "DIV":
|
|
||||||
case "P":
|
case "P":
|
||||||
case "UL":
|
case "UL":
|
||||||
case "OL":
|
case "OL":
|
||||||
|
@ -528,6 +546,9 @@ export function checkBlockNode(node: Node) {
|
||||||
case "TH":
|
case "TH":
|
||||||
case "TD":
|
case "TD":
|
||||||
return true;
|
return true;
|
||||||
|
case "DIV":
|
||||||
|
// don't treat math nodes as block nodes for deserializing
|
||||||
|
return !(node as HTMLElement).hasAttribute("data-mx-maths");
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform";
|
||||||
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
|
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
|
||||||
import CountlyAnalytics from "./CountlyAnalytics";
|
import CountlyAnalytics from "./CountlyAnalytics";
|
||||||
import CallHandler from './CallHandler';
|
import CallHandler from './CallHandler';
|
||||||
|
import LifecycleCustomisations from "./customisations/Lifecycle";
|
||||||
|
|
||||||
const HOMESERVER_URL_KEY = "mx_hs_url";
|
const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||||
const ID_SERVER_URL_KEY = "mx_is_url";
|
const ID_SERVER_URL_KEY = "mx_is_url";
|
||||||
|
@ -589,9 +590,9 @@ export function logout(): void {
|
||||||
|
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
// logout doesn't work for guest sessions
|
// logout doesn't work for guest sessions
|
||||||
// Also we sometimes want to re-log in a guest session
|
// Also we sometimes want to re-log in a guest session if we abort the login.
|
||||||
// if we abort the login
|
// defer until next tick because it calls a synchronous dispatch and we are likely here from a dispatch.
|
||||||
onLoggedOut();
|
setImmediate(() => onLoggedOut());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -716,6 +717,7 @@ export async function onLoggedOut(): Promise<void> {
|
||||||
dis.dispatch({action: 'on_logged_out'}, true);
|
dis.dispatch({action: 'on_logged_out'}, true);
|
||||||
stopMatrixClient();
|
stopMatrixClient();
|
||||||
await clearStorage({deleteEverything: true});
|
await clearStorage({deleteEverything: true});
|
||||||
|
LifecycleCustomisations.onLoggedOutAndStorageCleared?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -23,6 +23,11 @@ const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
|
||||||
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
||||||
|
|
||||||
function is_allowed_html_tag(node) {
|
function is_allowed_html_tag(node) {
|
||||||
|
if (node.literal != null &&
|
||||||
|
node.literal.match('^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$') != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Regex won't work for tags with attrs, but we only
|
// Regex won't work for tags with attrs, but we only
|
||||||
// allow <del> anyway.
|
// allow <del> anyway.
|
||||||
const matches = /^<\/?(.*)>$/.exec(node.literal);
|
const matches = /^<\/?(.*)>$/.exec(node.literal);
|
||||||
|
@ -30,6 +35,7 @@ function is_allowed_html_tag(node) {
|
||||||
const tag = matches[1];
|
const tag = matches[1];
|
||||||
return ALLOWED_HTML_TAGS.indexOf(tag) > -1;
|
return ALLOWED_HTML_TAGS.indexOf(tag) > -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ import LogoutDialog from "../views/dialogs/LogoutDialog";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import {getCustomTheme} from "../../theme";
|
import {getCustomTheme} from "../../theme";
|
||||||
import {getHostingLink} from "../../utils/HostingLink";
|
import {getHostingLink} from "../../utils/HostingLink";
|
||||||
import {ButtonEvent} from "../views/elements/AccessibleButton";
|
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
import {getHomePageUrl} from "../../utils/pages";
|
import {getHomePageUrl} from "../../utils/pages";
|
||||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||||
|
@ -205,6 +205,16 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
this.setState({contextMenuPosition: null}); // also close the menu
|
this.setState({contextMenuPosition: null}); // also close the menu
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onSignInClick = () => {
|
||||||
|
dis.dispatch({ action: 'start_login' });
|
||||||
|
this.setState({contextMenuPosition: null}); // also close the menu
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRegisterClick = () => {
|
||||||
|
dis.dispatch({ action: 'start_registration' });
|
||||||
|
this.setState({contextMenuPosition: null}); // also close the menu
|
||||||
|
};
|
||||||
|
|
||||||
private onHomeClick = (ev: ButtonEvent) => {
|
private onHomeClick = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
@ -261,10 +271,29 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||||
|
|
||||||
let hostingLink;
|
let topSection;
|
||||||
const signupLink = getHostingLink("user-context-menu");
|
const signupLink = getHostingLink("user-context-menu");
|
||||||
if (signupLink) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
hostingLink = (
|
topSection = (
|
||||||
|
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_guestPrompts">
|
||||||
|
{_t("Got an account? <a>Sign in</a>", {}, {
|
||||||
|
a: sub => (
|
||||||
|
<AccessibleButton kind="link" onClick={this.onSignInClick}>
|
||||||
|
{sub}
|
||||||
|
</AccessibleButton>
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
{_t("New here? <a>Create an account</a>", {}, {
|
||||||
|
a: sub => (
|
||||||
|
<AccessibleButton kind="link" onClick={this.onRegisterClick}>
|
||||||
|
{sub}
|
||||||
|
</AccessibleButton>
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else if (signupLink) {
|
||||||
|
topSection = (
|
||||||
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_hostingLink">
|
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_hostingLink">
|
||||||
{_t(
|
{_t(
|
||||||
"<a>Upgrade</a> to your own domain", {},
|
"<a>Upgrade</a> to your own domain", {},
|
||||||
|
@ -422,6 +451,20 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
</IconizedContextMenuOptionList>
|
</IconizedContextMenuOptionList>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
|
} else if (MatrixClientPeg.get().isGuest()) {
|
||||||
|
primaryOptionList = (
|
||||||
|
<React.Fragment>
|
||||||
|
<IconizedContextMenuOptionList>
|
||||||
|
{ homeButton }
|
||||||
|
<IconizedContextMenuOption
|
||||||
|
iconClassName="mx_UserMenu_iconSettings"
|
||||||
|
label={_t("Settings")}
|
||||||
|
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||||
|
/>
|
||||||
|
{ feedbackButton }
|
||||||
|
</IconizedContextMenuOptionList>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
|
@ -451,7 +494,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
/>
|
/>
|
||||||
</AccessibleTooltipButton>
|
</AccessibleTooltipButton>
|
||||||
</div>
|
</div>
|
||||||
{hostingLink}
|
{topSection}
|
||||||
{primaryOptionList}
|
{primaryOptionList}
|
||||||
{secondarySection}
|
{secondarySection}
|
||||||
</IconizedContextMenu>;
|
</IconizedContextMenu>;
|
||||||
|
|
|
@ -38,6 +38,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||||
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
|
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
|
||||||
import { arrayFastClone } from "../../../utils/arrays";
|
import { arrayFastClone } from "../../../utils/arrays";
|
||||||
|
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
widgetDefinition: IModalWidgetOpenRequestData;
|
widgetDefinition: IModalWidgetOpenRequestData;
|
||||||
|
@ -64,7 +65,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.widget = new Widget({
|
this.widget = new ElementWidget({
|
||||||
...this.props.widgetDefinition,
|
...this.props.widgetDefinition,
|
||||||
creatorUserId: MatrixClientPeg.get().getUserId(),
|
creatorUserId: MatrixClientPeg.get().getUserId(),
|
||||||
id: `modal_${this.props.sourceWidgetId}`,
|
id: `modal_${this.props.sourceWidgetId}`,
|
||||||
|
@ -161,7 +162,9 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
||||||
this.state.messaging.notifyModalWidgetButtonClicked(def.id);
|
this.state.messaging.notifyModalWidgetButtonClicked(def.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <AccessibleButton key={def.id} kind={kind} onClick={onClick}>
|
const isDisabled = this.state.disabledButtonIds.includes(def.id);
|
||||||
|
|
||||||
|
return <AccessibleButton key={def.id} kind={kind} onClick={onClick} disabled={isDisabled}>
|
||||||
{ def.label }
|
{ def.label }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,18 +17,17 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {_t} from "../../../languageHandler";
|
import {_t} from "../../../languageHandler";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
import {Widget} from "matrix-widget-api";
|
||||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
import {OIDCState, WidgetPermissionStore} from "../../../stores/widgets/WidgetPermissionStore";
|
||||||
|
|
||||||
export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
widgetUrl: PropTypes.string.isRequired,
|
widget: PropTypes.objectOf(Widget).isRequired,
|
||||||
widgetId: PropTypes.string.isRequired,
|
widgetKind: PropTypes.string.isRequired, // WidgetKind from widget-api
|
||||||
isUserWidget: PropTypes.bool.isRequired,
|
inRoomId: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -51,16 +50,10 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
||||||
if (this.state.rememberSelection) {
|
if (this.state.rememberSelection) {
|
||||||
console.log(`Remembering ${this.props.widgetId} as allowed=${allowed} for OpenID`);
|
console.log(`Remembering ${this.props.widgetId} as allowed=${allowed} for OpenID`);
|
||||||
|
|
||||||
const currentValues = SettingsStore.getValue("widgetOpenIDPermissions");
|
WidgetPermissionStore.instance.setOIDCState(
|
||||||
if (!currentValues.allow) currentValues.allow = [];
|
this.props.widget, this.props.widgetKind, this.props.inRoomId,
|
||||||
if (!currentValues.deny) currentValues.deny = [];
|
allowed ? OIDCState.Allowed : OIDCState.Denied,
|
||||||
|
);
|
||||||
const securityKey = WidgetUtils.getWidgetSecurityKey(
|
|
||||||
this.props.widgetId,
|
|
||||||
this.props.widgetUrl,
|
|
||||||
this.props.isUserWidget);
|
|
||||||
(allowed ? currentValues.allow : currentValues.deny).push(securityKey);
|
|
||||||
SettingsStore.setValue("widgetOpenIDPermissions", null, SettingLevel.DEVICE, currentValues);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onFinished(allowed);
|
this.props.onFinished(allowed);
|
||||||
|
@ -84,7 +77,7 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
||||||
"A widget located at %(widgetUrl)s would like to verify your identity. " +
|
"A widget located at %(widgetUrl)s would like to verify your identity. " +
|
||||||
"By allowing this, the widget will be able to verify your user ID, but not " +
|
"By allowing this, the widget will be able to verify your user ID, but not " +
|
||||||
"perform actions as you.", {
|
"perform actions as you.", {
|
||||||
widgetUrl: this.props.widgetUrl.split("?")[0],
|
widgetUrl: this.props.widget.templateUrl.split("?")[0],
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -375,17 +375,20 @@ export default class AppTile extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// all widgets can theoretically be allowed to remain on screen, so we wrap
|
if (!this.props.userWidget) {
|
||||||
// them all in a PersistedElement from the get-go. If we wait, the iframe will
|
// All room widgets can theoretically be allowed to remain on screen, so we
|
||||||
// be re-mounted later, which means the widget has to start over, which is bad.
|
// wrap them all in a PersistedElement from the get-go. If we wait, the iframe
|
||||||
|
// will be re-mounted later, which means the widget has to start over, which is
|
||||||
|
// bad.
|
||||||
|
|
||||||
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
||||||
// AppTile's border is in the wrong place
|
// AppTile's border is in the wrong place
|
||||||
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
||||||
<PersistedElement persistKey={this._persistKey}>
|
<PersistedElement persistKey={this._persistKey}>
|
||||||
{appTileBody}
|
{appTileBody}
|
||||||
</PersistedElement>
|
</PersistedElement>
|
||||||
</div>;
|
</div>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,9 +21,18 @@ import PropTypes from 'prop-types';
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import Spinner from '../elements/Spinner';
|
import Spinner from '../elements/Spinner';
|
||||||
|
import withValidation from '../elements/Validation';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
|
import PassphraseField from "../auth/PassphraseField";
|
||||||
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
|
|
||||||
|
const FIELD_OLD_PASSWORD = 'field_old_password';
|
||||||
|
const FIELD_NEW_PASSWORD = 'field_new_password';
|
||||||
|
const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
|
||||||
|
|
||||||
|
const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
|
||||||
|
|
||||||
export default class ChangePassword extends React.Component {
|
export default class ChangePassword extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -63,6 +72,7 @@ export default class ChangePassword extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
fieldValid: {},
|
||||||
phase: ChangePassword.Phases.Edit,
|
phase: ChangePassword.Phases.Edit,
|
||||||
oldPassword: "",
|
oldPassword: "",
|
||||||
newPassword: "",
|
newPassword: "",
|
||||||
|
@ -168,26 +178,84 @@ export default class ChangePassword extends React.Component {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
markFieldValid(fieldID, valid) {
|
||||||
|
const { fieldValid } = this.state;
|
||||||
|
fieldValid[fieldID] = valid;
|
||||||
|
this.setState({
|
||||||
|
fieldValid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onChangeOldPassword = (ev) => {
|
onChangeOldPassword = (ev) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
oldPassword: ev.target.value,
|
oldPassword: ev.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onOldPasswordValidate = async fieldState => {
|
||||||
|
const result = await this.validateOldPasswordRules(fieldState);
|
||||||
|
this.markFieldValid(FIELD_OLD_PASSWORD, result.valid);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
validateOldPasswordRules = withValidation({
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
key: "required",
|
||||||
|
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||||
|
invalid: () => _t("Passwords can't be empty"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
onChangeNewPassword = (ev) => {
|
onChangeNewPassword = (ev) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
newPassword: ev.target.value,
|
newPassword: ev.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onNewPasswordValidate = result => {
|
||||||
|
this.markFieldValid(FIELD_NEW_PASSWORD, result.valid);
|
||||||
|
};
|
||||||
|
|
||||||
onChangeNewPasswordConfirm = (ev) => {
|
onChangeNewPasswordConfirm = (ev) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
newPasswordConfirm: ev.target.value,
|
newPasswordConfirm: ev.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onClickChange = (ev) => {
|
onNewPasswordConfirmValidate = async fieldState => {
|
||||||
|
const result = await this.validatePasswordConfirmRules(fieldState);
|
||||||
|
this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
validatePasswordConfirmRules = withValidation({
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
key: "required",
|
||||||
|
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||||
|
invalid: () => _t("Confirm password"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "match",
|
||||||
|
test({ value }) {
|
||||||
|
return !value || value === this.state.newPassword;
|
||||||
|
},
|
||||||
|
invalid: () => _t("Passwords don't match"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
onClickChange = async (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
|
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
||||||
|
if (!allFieldsValid) {
|
||||||
|
CountlyAnalytics.instance.track("onboarding_registration_submit_failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const oldPassword = this.state.oldPassword;
|
const oldPassword = this.state.oldPassword;
|
||||||
const newPassword = this.state.newPassword;
|
const newPassword = this.state.newPassword;
|
||||||
const confirmPassword = this.state.newPasswordConfirm;
|
const confirmPassword = this.state.newPasswordConfirm;
|
||||||
|
@ -201,9 +269,75 @@ export default class ChangePassword extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
async verifyFieldsBeforeSubmit() {
|
||||||
// TODO: Live validation on `new pw == confirm pw`
|
// Blur the active element if any, so we first run its blur validation,
|
||||||
|
// which is less strict than the pass we're about to do below for all fields.
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
if (activeElement) {
|
||||||
|
activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldIDsInDisplayOrder = [
|
||||||
|
FIELD_OLD_PASSWORD,
|
||||||
|
FIELD_NEW_PASSWORD,
|
||||||
|
FIELD_NEW_PASSWORD_CONFIRM,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Run all fields with stricter validation that no longer allows empty
|
||||||
|
// values for required fields.
|
||||||
|
for (const fieldID of fieldIDsInDisplayOrder) {
|
||||||
|
const field = this[fieldID];
|
||||||
|
if (!field) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// We must wait for these validations to finish before queueing
|
||||||
|
// up the setState below so our setState goes in the queue after
|
||||||
|
// all the setStates from these validate calls (that's how we
|
||||||
|
// know they've finished).
|
||||||
|
await field.validate({ allowEmpty: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation and state updates are async, so we need to wait for them to complete
|
||||||
|
// first. Queue a `setState` callback and wait for it to resolve.
|
||||||
|
await new Promise(resolve => this.setState({}, resolve));
|
||||||
|
|
||||||
|
if (this.allFieldsValid()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidField = this.findFirstInvalidField(fieldIDsInDisplayOrder);
|
||||||
|
|
||||||
|
if (!invalidField) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus the first invalid field and show feedback in the stricter mode
|
||||||
|
// that no longer allows empty values for required fields.
|
||||||
|
invalidField.focus();
|
||||||
|
invalidField.validate({ allowEmpty: false, focused: true });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
allFieldsValid() {
|
||||||
|
const keys = Object.keys(this.state.fieldValid);
|
||||||
|
for (let i = 0; i < keys.length; ++i) {
|
||||||
|
if (!this.state.fieldValid[keys[i]]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
findFirstInvalidField(fieldIDs) {
|
||||||
|
for (const fieldID of fieldIDs) {
|
||||||
|
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
|
||||||
|
return this[fieldID];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
const rowClassName = this.props.rowClassName;
|
const rowClassName = this.props.rowClassName;
|
||||||
const buttonClassName = this.props.buttonClassName;
|
const buttonClassName = this.props.buttonClassName;
|
||||||
|
|
||||||
|
@ -213,28 +347,35 @@ export default class ChangePassword extends React.Component {
|
||||||
<form className={this.props.className} onSubmit={this.onClickChange}>
|
<form className={this.props.className} onSubmit={this.onClickChange}>
|
||||||
<div className={rowClassName}>
|
<div className={rowClassName}>
|
||||||
<Field
|
<Field
|
||||||
|
ref={field => this[FIELD_OLD_PASSWORD] = field}
|
||||||
type="password"
|
type="password"
|
||||||
label={_t('Current password')}
|
label={_t('Current password')}
|
||||||
value={this.state.oldPassword}
|
value={this.state.oldPassword}
|
||||||
onChange={this.onChangeOldPassword}
|
onChange={this.onChangeOldPassword}
|
||||||
|
onValidate={this.onOldPasswordValidate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={rowClassName}>
|
<div className={rowClassName}>
|
||||||
<Field
|
<PassphraseField
|
||||||
|
fieldRef={field => this[FIELD_NEW_PASSWORD] = field}
|
||||||
type="password"
|
type="password"
|
||||||
label={_t('New Password')}
|
label='New Password'
|
||||||
|
minScore={PASSWORD_MIN_SCORE}
|
||||||
value={this.state.newPassword}
|
value={this.state.newPassword}
|
||||||
autoFocus={this.props.autoFocusNewPasswordInput}
|
autoFocus={this.props.autoFocusNewPasswordInput}
|
||||||
onChange={this.onChangeNewPassword}
|
onChange={this.onChangeNewPassword}
|
||||||
|
onValidate={this.onNewPasswordValidate}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={rowClassName}>
|
<div className={rowClassName}>
|
||||||
<Field
|
<Field
|
||||||
|
ref={field => this[FIELD_NEW_PASSWORD_CONFIRM] = field}
|
||||||
type="password"
|
type="password"
|
||||||
label={_t("Confirm password")}
|
label={_t("Confirm password")}
|
||||||
value={this.state.newPasswordConfirm}
|
value={this.state.newPasswordConfirm}
|
||||||
onChange={this.onChangeNewPasswordConfirm}
|
onChange={this.onChangeNewPasswordConfirm}
|
||||||
|
onValidate={this.onNewPasswordConfirmValidate}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
30
src/customisations/Lifecycle.ts
Normal file
30
src/customisations/Lifecycle.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function onLoggedOutAndStorageCleared(): void {
|
||||||
|
// E.g. redirect user or call other APIs after logout
|
||||||
|
}
|
||||||
|
|
||||||
|
// This interface summarises all available customisation points and also marks
|
||||||
|
// them all as optional. This allows customisers to only define and export the
|
||||||
|
// customisations they need while still maintaining type safety.
|
||||||
|
export interface ILifecycleCustomisations {
|
||||||
|
onLoggedOutAndStorageCleared?: typeof onLoggedOutAndStorageCleared;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A real customisation module will define and export one or more of the
|
||||||
|
// customisation points that make up `ILifecycleCustomisations`.
|
||||||
|
export default {} as ILifecycleCustomisations;
|
45
src/customisations/RoomList.ts
Normal file
45
src/customisations/RoomList.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* 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 { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
// Populate this file with the details of your customisations when copying it.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a room is visible in the room list or not. By default,
|
||||||
|
* all rooms are visible. Where special handling is performed by Element,
|
||||||
|
* those rooms will not be able to override their visibility in the room
|
||||||
|
* list - Element will make the decision without calling this function.
|
||||||
|
*
|
||||||
|
* This function should be as fast as possible to avoid slowing down the
|
||||||
|
* client.
|
||||||
|
* @param {Room} room The room to check the visibility of.
|
||||||
|
* @returns {boolean} True if the room should be visible, false otherwise.
|
||||||
|
*/
|
||||||
|
function isRoomVisible(room: Room): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This interface summarises all available customisation points and also marks
|
||||||
|
// them all as optional. This allows customisers to only define and export the
|
||||||
|
// customisations they need while still maintaining type safety.
|
||||||
|
export interface IRoomListCustomisations {
|
||||||
|
isRoomVisible?: typeof isRoomVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A real customisation module will define and export one or more of the
|
||||||
|
// customisation points that make up the interface above.
|
||||||
|
export const RoomListCustomisations: IRoomListCustomisations = {};
|
|
@ -67,24 +67,13 @@ function setupEncryptionNeeded(kind: SetupEncryptionKind): boolean {
|
||||||
// them all as optional. This allows customisers to only define and export the
|
// them all as optional. This allows customisers to only define and export the
|
||||||
// customisations they need while still maintaining type safety.
|
// customisations they need while still maintaining type safety.
|
||||||
export interface ISecurityCustomisations {
|
export interface ISecurityCustomisations {
|
||||||
examineLoginResponse?: (
|
examineLoginResponse?: typeof examineLoginResponse;
|
||||||
response: any,
|
persistCredentials?: typeof persistCredentials;
|
||||||
credentials: IMatrixClientCreds,
|
createSecretStorageKey?: typeof createSecretStorageKey,
|
||||||
) => void;
|
getSecretStorageKey?: typeof getSecretStorageKey,
|
||||||
persistCredentials?: (
|
catchAccessSecretStorageError?: typeof catchAccessSecretStorageError,
|
||||||
credentials: IMatrixClientCreds,
|
setupEncryptionNeeded?: typeof setupEncryptionNeeded,
|
||||||
) => void;
|
getDehydrationKey?: typeof getDehydrationKey,
|
||||||
createSecretStorageKey?: () => Uint8Array,
|
|
||||||
getSecretStorageKey?: () => Uint8Array,
|
|
||||||
catchAccessSecretStorageError?: (
|
|
||||||
e: Error,
|
|
||||||
) => void,
|
|
||||||
setupEncryptionNeeded?: (
|
|
||||||
kind: SetupEncryptionKind,
|
|
||||||
) => boolean,
|
|
||||||
getDehydrationKey?: (
|
|
||||||
keyInfo: ISecretStorageKeyInfo,
|
|
||||||
) => Promise<Uint8Array>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A real customisation module will define and export one or more of the
|
// A real customisation module will define and export one or more of the
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { walkDOMDepthFirst } from "./dom";
|
||||||
import { checkBlockNode } from "../HtmlUtils";
|
import { checkBlockNode } from "../HtmlUtils";
|
||||||
import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks";
|
import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks";
|
||||||
import { PartCreator } from "./parts";
|
import { PartCreator } from "./parts";
|
||||||
|
import SdkConfig from "../SdkConfig";
|
||||||
|
|
||||||
function parseAtRoomMentions(text: string, partCreator: PartCreator) {
|
function parseAtRoomMentions(text: string, partCreator: PartCreator) {
|
||||||
const ATROOM = "@room";
|
const ATROOM = "@room";
|
||||||
|
@ -130,6 +131,23 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "DIV":
|
||||||
|
case "SPAN": {
|
||||||
|
// 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'] || "$$";
|
||||||
|
const delimRight = (n.nodeName == "SPAN") ?
|
||||||
|
(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)) {
|
||||||
|
return partCreator.plain(n.textContent);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "OL":
|
case "OL":
|
||||||
state.listIndex.push((<HTMLOListElement>n).start || 1);
|
state.listIndex.push((<HTMLOListElement>n).start || 1);
|
||||||
/* falls through */
|
/* falls through */
|
||||||
|
|
|
@ -18,6 +18,10 @@ limitations under the License.
|
||||||
import Markdown from '../Markdown';
|
import Markdown from '../Markdown';
|
||||||
import {makeGenericPermalink} from "../utils/permalinks/Permalinks";
|
import {makeGenericPermalink} from "../utils/permalinks/Permalinks";
|
||||||
import EditorModel from "./model";
|
import EditorModel from "./model";
|
||||||
|
import { AllHtmlEntities } from 'html-entities';
|
||||||
|
import SettingsStore from '../settings/SettingsStore';
|
||||||
|
import SdkConfig from '../SdkConfig';
|
||||||
|
import cheerio from 'cheerio';
|
||||||
|
|
||||||
export function mdSerialize(model: EditorModel) {
|
export function mdSerialize(model: EditorModel) {
|
||||||
return model.parts.reduce((html, part) => {
|
return model.parts.reduce((html, part) => {
|
||||||
|
@ -38,10 +42,43 @@ export function mdSerialize(model: EditorModel) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) {
|
export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) {
|
||||||
const md = mdSerialize(model);
|
let md = mdSerialize(model);
|
||||||
|
|
||||||
|
if (SettingsStore.getValue("feature_latex_maths")) {
|
||||||
|
const displayPattern = (SdkConfig.get()['latex_maths_delims'] || {})['display_pattern'] ||
|
||||||
|
"\\$\\$(([^$]|\\\\\\$)*)\\$\\$";
|
||||||
|
const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] ||
|
||||||
|
"\\$(([^$]|\\\\\\$)*)\\$";
|
||||||
|
|
||||||
|
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`;
|
||||||
|
});
|
||||||
|
|
||||||
|
md = md.replace(RegExp(inlinePattern, "gm"), function(m, p1) {
|
||||||
|
const p1e = AllHtmlEntities.encode(p1);
|
||||||
|
return `<span data-mx-maths="${p1e}"></span>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// make sure div tags always start on a new line, otherwise it will confuse
|
||||||
|
// the markdown parser
|
||||||
|
md = md.replace(/(.)<div/g, function(m, p1) { return `${p1}\n<div`; });
|
||||||
|
}
|
||||||
|
|
||||||
const parser = new Markdown(md);
|
const parser = new Markdown(md);
|
||||||
if (!parser.isPlainText() || forceHTML) {
|
if (!parser.isPlainText() || forceHTML) {
|
||||||
return parser.toHTML();
|
// feed Markdown output to HTML parser
|
||||||
|
const phtml = cheerio.load(parser.toHTML(),
|
||||||
|
{ _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>`)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return phtml.html();
|
||||||
}
|
}
|
||||||
// ensure removal of escape backslashes in non-Markdown messages
|
// ensure removal of escape backslashes in non-Markdown messages
|
||||||
if (md.indexOf("\\") > -1) {
|
if (md.indexOf("\\") > -1) {
|
||||||
|
|
|
@ -46,6 +46,13 @@
|
||||||
"Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.",
|
"Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.",
|
||||||
"Try using turn.matrix.org": "Try using turn.matrix.org",
|
"Try using turn.matrix.org": "Try using turn.matrix.org",
|
||||||
"OK": "OK",
|
"OK": "OK",
|
||||||
|
"Unable to access microphone": "Unable to access microphone",
|
||||||
|
"Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.",
|
||||||
|
"Unable to access webcam / microphone": "Unable to access webcam / microphone",
|
||||||
|
"Call failed because no webcam or microphone could not be accessed. Check that:": "Call failed because no webcam or microphone could not be accessed. Check that:",
|
||||||
|
"A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly",
|
||||||
|
"Permission is granted to use the webcam": "Permission is granted to use the webcam",
|
||||||
|
"No other application is using the webcam": "No other application is using the webcam",
|
||||||
"Unable to capture screen": "Unable to capture screen",
|
"Unable to capture screen": "Unable to capture screen",
|
||||||
"Existing Call": "Existing Call",
|
"Existing Call": "Existing Call",
|
||||||
"You are already in a call.": "You are already in a call.",
|
"You are already in a call.": "You are already in a call.",
|
||||||
|
@ -755,6 +762,7 @@
|
||||||
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
|
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
|
||||||
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
|
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
|
||||||
"Change notification settings": "Change notification settings",
|
"Change notification settings": "Change notification settings",
|
||||||
|
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
|
||||||
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
|
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
|
||||||
"New spinner design": "New spinner design",
|
"New spinner design": "New spinner design",
|
||||||
"Message Pinning": "Message Pinning",
|
"Message Pinning": "Message Pinning",
|
||||||
|
@ -954,9 +962,9 @@
|
||||||
"Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.",
|
"Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.",
|
||||||
"Export E2E room keys": "Export E2E room keys",
|
"Export E2E room keys": "Export E2E room keys",
|
||||||
"Do you want to set an email address?": "Do you want to set an email address?",
|
"Do you want to set an email address?": "Do you want to set an email address?",
|
||||||
"Current password": "Current password",
|
|
||||||
"New Password": "New Password",
|
|
||||||
"Confirm password": "Confirm password",
|
"Confirm password": "Confirm password",
|
||||||
|
"Passwords don't match": "Passwords don't match",
|
||||||
|
"Current password": "Current password",
|
||||||
"Change Password": "Change Password",
|
"Change Password": "Change Password",
|
||||||
"Your homeserver does not support cross-signing.": "Your homeserver does not support cross-signing.",
|
"Your homeserver does not support cross-signing.": "Your homeserver does not support cross-signing.",
|
||||||
"Cross-signing is ready for use.": "Cross-signing is ready for use.",
|
"Cross-signing is ready for use.": "Cross-signing is ready for use.",
|
||||||
|
@ -2304,7 +2312,6 @@
|
||||||
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
|
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
|
||||||
"Use an email address to recover your account": "Use an email address to recover your account",
|
"Use an email address to recover your account": "Use an email address to recover your account",
|
||||||
"Enter email address (required on this homeserver)": "Enter email address (required on this homeserver)",
|
"Enter email address (required on this homeserver)": "Enter email address (required on this homeserver)",
|
||||||
"Passwords don't match": "Passwords don't match",
|
|
||||||
"Other users can invite you to rooms using your contact details": "Other users can invite you to rooms using your contact details",
|
"Other users can invite you to rooms using your contact details": "Other users can invite you to rooms using your contact details",
|
||||||
"Enter phone number (required on this homeserver)": "Enter phone number (required on this homeserver)",
|
"Enter phone number (required on this homeserver)": "Enter phone number (required on this homeserver)",
|
||||||
"Use lowercase letters, numbers, dashes and underscores only": "Use lowercase letters, numbers, dashes and underscores only",
|
"Use lowercase letters, numbers, dashes and underscores only": "Use lowercase letters, numbers, dashes and underscores only",
|
||||||
|
@ -2460,6 +2467,8 @@
|
||||||
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
|
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
|
||||||
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
|
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
|
||||||
"Failed to find the general chat for this community": "Failed to find the general chat for this community",
|
"Failed to find the general chat for this community": "Failed to find the general chat for this community",
|
||||||
|
"Got an account? <a>Sign in</a>": "Got an account? <a>Sign in</a>",
|
||||||
|
"New here? <a>Create an account</a>": "New here? <a>Create an account</a>",
|
||||||
"Notification settings": "Notification settings",
|
"Notification settings": "Notification settings",
|
||||||
"Security & privacy": "Security & privacy",
|
"Security & privacy": "Security & privacy",
|
||||||
"All settings": "All settings",
|
"All settings": "All settings",
|
||||||
|
@ -2482,6 +2491,7 @@
|
||||||
"Your Matrix account on <underlinedServerName />": "Your Matrix account on <underlinedServerName />",
|
"Your Matrix account on <underlinedServerName />": "Your Matrix account on <underlinedServerName />",
|
||||||
"No identity server is configured: add one in server settings to reset your password.": "No identity server is configured: add one in server settings to reset your password.",
|
"No identity server is configured: add one in server settings to reset your password.": "No identity server is configured: add one in server settings to reset your password.",
|
||||||
"Sign in instead": "Sign in instead",
|
"Sign in instead": "Sign in instead",
|
||||||
|
"New Password": "New Password",
|
||||||
"A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.",
|
"A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.",
|
||||||
"Send Reset Email": "Send Reset Email",
|
"Send Reset Email": "Send Reset Email",
|
||||||
"An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.",
|
"An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.",
|
||||||
|
|
|
@ -117,6 +117,12 @@ export interface ISetting {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SETTINGS: {[setting: string]: ISetting} = {
|
export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
|
"feature_latex_maths": {
|
||||||
|
isFeature: true,
|
||||||
|
displayName: _td("Render LaTeX maths in messages"),
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"feature_communities_v2_prototypes": {
|
"feature_communities_v2_prototypes": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
displayName: _td(
|
displayName: _td(
|
||||||
|
|
|
@ -64,7 +64,7 @@ export class ModalWidgetStore extends AsyncStoreWithClient<IState> {
|
||||||
this.openSourceWidgetId = null;
|
this.openSourceWidgetId = null;
|
||||||
this.modalInstance = null;
|
this.modalInstance = null;
|
||||||
},
|
},
|
||||||
});
|
}, null, /* priority = */ false, /* static = */ true);
|
||||||
};
|
};
|
||||||
|
|
||||||
public closeModalWidget = (sourceWidget: Widget, data?: IModalWidgetReturnData) => {
|
public closeModalWidget = (sourceWidget: Widget, data?: IModalWidgetReturnData) => {
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { MarkedExecution } from "../../utils/MarkedExecution";
|
||||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||||
import { NameFilterCondition } from "./filters/NameFilterCondition";
|
import { NameFilterCondition } from "./filters/NameFilterCondition";
|
||||||
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
|
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
|
||||||
|
import { VisibilityProvider } from "./filters/VisibilityProvider";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
tagsEnabled?: boolean;
|
tagsEnabled?: boolean;
|
||||||
|
@ -401,6 +402,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<any> {
|
private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<any> {
|
||||||
|
if (!VisibilityProvider.instance.isRoomVisible(room)) {
|
||||||
|
return; // don't do anything on rooms that aren't visible
|
||||||
|
}
|
||||||
|
|
||||||
const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
|
const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate) {
|
||||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||||
|
@ -544,7 +549,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
public async regenerateAllLists({trigger = true}) {
|
public async regenerateAllLists({trigger = true}) {
|
||||||
console.warn("Regenerating all room lists");
|
console.warn("Regenerating all room lists");
|
||||||
|
|
||||||
const rooms = this.matrixClient.getVisibleRooms();
|
const rooms = this.matrixClient.getVisibleRooms()
|
||||||
|
.filter(r => VisibilityProvider.instance.isRoomVisible(r));
|
||||||
const customTags = new Set<TagID>();
|
const customTags = new Set<TagID>();
|
||||||
if (this.state.tagsEnabled) {
|
if (this.state.tagsEnabled) {
|
||||||
for (const room of rooms) {
|
for (const room of rooms) {
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } f
|
||||||
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
|
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
|
||||||
import { getListAlgorithmInstance } from "./list-ordering";
|
import { getListAlgorithmInstance } from "./list-ordering";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import { VisibilityProvider } from "../filters/VisibilityProvider";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fired when the Algorithm has determined a list has been updated.
|
* Fired when the Algorithm has determined a list has been updated.
|
||||||
|
@ -188,6 +189,10 @@ export class Algorithm extends EventEmitter {
|
||||||
// Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
|
// Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
|
||||||
// otherwise we risk duplicating rooms.
|
// otherwise we risk duplicating rooms.
|
||||||
|
|
||||||
|
if (val && !VisibilityProvider.instance.isRoomVisible(val)) {
|
||||||
|
val = null; // the room isn't visible - lie to the rest of this function
|
||||||
|
}
|
||||||
|
|
||||||
// Set the last sticky room to indicate that we're in a change. The code throughout the
|
// Set the last sticky room to indicate that we're in a change. The code throughout the
|
||||||
// class can safely handle a null room, so this should be safe to do as a backup.
|
// class can safely handle a null room, so this should be safe to do as a backup.
|
||||||
this._lastStickyRoom = this._stickyRoom || <IStickyRoom>{};
|
this._lastStickyRoom = this._stickyRoom || <IStickyRoom>{};
|
||||||
|
|
54
src/stores/room-list/filters/VisibilityProvider.ts
Normal file
54
src/stores/room-list/filters/VisibilityProvider.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* 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 {Room} from "matrix-js-sdk/src/models/room";
|
||||||
|
import { RoomListCustomisations } from "../../../customisations/RoomList";
|
||||||
|
|
||||||
|
export class VisibilityProvider {
|
||||||
|
private static internalInstance: VisibilityProvider;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static get instance(): VisibilityProvider {
|
||||||
|
if (!VisibilityProvider.internalInstance) {
|
||||||
|
VisibilityProvider.internalInstance = new VisibilityProvider();
|
||||||
|
}
|
||||||
|
return VisibilityProvider.internalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isRoomVisible(room: Room): boolean {
|
||||||
|
/* eslint-disable prefer-const */
|
||||||
|
let isVisible = true; // Returned at the end of this function
|
||||||
|
let forced = false; // When true, this function won't bother calling the customisation points
|
||||||
|
/* eslint-enable prefer-const */
|
||||||
|
|
||||||
|
// ------
|
||||||
|
// TODO: The `if` statements to control visibility of custom room types
|
||||||
|
// would go here. The remainder of this function assumes that the statements
|
||||||
|
// will be here.
|
||||||
|
//
|
||||||
|
// When removing this comment block, please remove the lint disable lines in the area.
|
||||||
|
// ------
|
||||||
|
|
||||||
|
const isVisibleFn = RoomListCustomisations.isRoomVisible;
|
||||||
|
if (!forced && isVisibleFn) {
|
||||||
|
isVisible = isVisibleFn(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isVisible;
|
||||||
|
}
|
||||||
|
}
|
|
@ -68,7 +68,7 @@ interface IAppTileProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Don't use this because it's wrong
|
// TODO: Don't use this because it's wrong
|
||||||
class ElementWidget extends Widget {
|
export class ElementWidget extends Widget {
|
||||||
constructor(private rawDefinition: IWidget) {
|
constructor(private rawDefinition: IWidget) {
|
||||||
super(rawDefinition);
|
super(rawDefinition);
|
||||||
}
|
}
|
||||||
|
@ -246,7 +246,7 @@ export class StopGapWidget extends EventEmitter {
|
||||||
public start(iframe: HTMLIFrameElement) {
|
public start(iframe: HTMLIFrameElement) {
|
||||||
if (this.started) return;
|
if (this.started) return;
|
||||||
const allowedCapabilities = this.appTileProps.whitelistCapabilities || [];
|
const allowedCapabilities = this.appTileProps.whitelistCapabilities || [];
|
||||||
const driver = new StopGapWidgetDriver( allowedCapabilities, this.mockWidget, this.kind);
|
const driver = new StopGapWidgetDriver(allowedCapabilities, this.mockWidget, this.kind, this.roomId);
|
||||||
this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
|
this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
|
||||||
this.messaging.on("preparing", () => this.emit("preparing"));
|
this.messaging.on("preparing", () => this.emit("preparing"));
|
||||||
this.messaging.on("ready", () => this.emit("ready"));
|
this.messaging.on("ready", () => this.emit("ready"));
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Capability,
|
Capability,
|
||||||
|
EventDirection,
|
||||||
IOpenIDCredentials,
|
IOpenIDCredentials,
|
||||||
IOpenIDUpdate,
|
IOpenIDUpdate,
|
||||||
ISendEventDetails,
|
ISendEventDetails,
|
||||||
|
@ -24,19 +25,21 @@ import {
|
||||||
SimpleObservable,
|
SimpleObservable,
|
||||||
Widget,
|
Widget,
|
||||||
WidgetDriver,
|
WidgetDriver,
|
||||||
|
WidgetEventCapability,
|
||||||
WidgetKind,
|
WidgetKind,
|
||||||
} from "matrix-widget-api";
|
} from "matrix-widget-api";
|
||||||
import { iterableDiff, iterableUnion } from "../../utils/iterables";
|
import { iterableDiff, iterableUnion } from "../../utils/iterables";
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import ActiveRoomObserver from "../../ActiveRoomObserver";
|
import ActiveRoomObserver from "../../ActiveRoomObserver";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import WidgetUtils from "../../utils/WidgetUtils";
|
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
|
||||||
import WidgetOpenIDPermissionsDialog from "../../components/views/dialogs/WidgetOpenIDPermissionsDialog";
|
import WidgetOpenIDPermissionsDialog from "../../components/views/dialogs/WidgetOpenIDPermissionsDialog";
|
||||||
import WidgetCapabilitiesPromptDialog, {
|
import WidgetCapabilitiesPromptDialog, {
|
||||||
getRememberedCapabilitiesForWidget,
|
getRememberedCapabilitiesForWidget,
|
||||||
} from "../../components/views/dialogs/WidgetCapabilitiesPromptDialog";
|
} from "../../components/views/dialogs/WidgetCapabilitiesPromptDialog";
|
||||||
import { WidgetPermissionCustomisations } from "../../customisations/WidgetPermissions";
|
import { WidgetPermissionCustomisations } from "../../customisations/WidgetPermissions";
|
||||||
|
import { OIDCState, WidgetPermissionStore } from "./WidgetPermissionStore";
|
||||||
|
import { WidgetType } from "../../widgets/WidgetType";
|
||||||
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
// TODO: Purge this from the universe
|
// TODO: Purge this from the universe
|
||||||
|
|
||||||
|
@ -44,13 +47,27 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
private allowedCapabilities: Set<Capability>;
|
private allowedCapabilities: Set<Capability>;
|
||||||
|
|
||||||
// TODO: Refactor widgetKind into the Widget class
|
// TODO: Refactor widgetKind into the Widget class
|
||||||
constructor(allowedCapabilities: Capability[], private forWidget: Widget, private forWidgetKind: WidgetKind) {
|
constructor(
|
||||||
|
allowedCapabilities: Capability[],
|
||||||
|
private forWidget: Widget,
|
||||||
|
private forWidgetKind: WidgetKind,
|
||||||
|
private inRoomId?: string,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
// Always allow screenshots to be taken because it's a client-induced flow. The widget can't
|
// Always allow screenshots to be taken because it's a client-induced flow. The widget can't
|
||||||
// spew screenshots at us and can't request screenshots of us, so it's up to us to provide the
|
// spew screenshots at us and can't request screenshots of us, so it's up to us to provide the
|
||||||
// button if the widget says it supports screenshots.
|
// button if the widget says it supports screenshots.
|
||||||
this.allowedCapabilities = new Set([...allowedCapabilities, MatrixCapabilities.Screenshots]);
|
this.allowedCapabilities = new Set([...allowedCapabilities, MatrixCapabilities.Screenshots]);
|
||||||
|
|
||||||
|
// Grant the permissions that are specific to given widget types
|
||||||
|
if (WidgetType.JITSI.matches(this.forWidget.type) && forWidgetKind === WidgetKind.Room) {
|
||||||
|
this.allowedCapabilities.add(MatrixCapabilities.AlwaysOnScreen);
|
||||||
|
} else if (WidgetType.STICKERPICKER.matches(this.forWidget.type) && forWidgetKind === WidgetKind.Account) {
|
||||||
|
const stickerSendingCap = WidgetEventCapability.forRoomEvent(EventDirection.Send, EventType.Sticker).raw;
|
||||||
|
this.allowedCapabilities.add(MatrixCapabilities.StickerSending); // legacy as far as MSC2762 is concerned
|
||||||
|
this.allowedCapabilities.add(stickerSendingCap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async validateCapabilities(requested: Set<Capability>): Promise<Set<Capability>> {
|
public async validateCapabilities(requested: Set<Capability>): Promise<Set<Capability>> {
|
||||||
|
@ -112,28 +129,27 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>) {
|
public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>) {
|
||||||
const isUserWidget = this.forWidgetKind !== WidgetKind.Room; // modal and account widgets are "user" widgets
|
const oidcState = WidgetPermissionStore.instance.getOIDCState(
|
||||||
const rawUrl = this.forWidget.templateUrl;
|
this.forWidget, this.forWidgetKind, this.inRoomId,
|
||||||
const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.forWidget.id, rawUrl, isUserWidget);
|
);
|
||||||
|
|
||||||
const getToken = (): Promise<IOpenIDCredentials> => {
|
const getToken = (): Promise<IOpenIDCredentials> => {
|
||||||
return MatrixClientPeg.get().getOpenIdToken();
|
return MatrixClientPeg.get().getOpenIdToken();
|
||||||
};
|
};
|
||||||
|
|
||||||
const settings = SettingsStore.getValue("widgetOpenIDPermissions");
|
if (oidcState === OIDCState.Denied) {
|
||||||
if (settings?.deny?.includes(widgetSecurityKey)) {
|
|
||||||
return observer.update({state: OpenIDRequestState.Blocked});
|
return observer.update({state: OpenIDRequestState.Blocked});
|
||||||
}
|
}
|
||||||
if (settings?.allow?.includes(widgetSecurityKey)) {
|
if (oidcState === OIDCState.Allowed) {
|
||||||
return observer.update({state: OpenIDRequestState.Allowed, token: await getToken()});
|
return observer.update({state: OpenIDRequestState.Allowed, token: await getToken()});
|
||||||
}
|
}
|
||||||
|
|
||||||
observer.update({state: OpenIDRequestState.PendingUserConfirmation});
|
observer.update({state: OpenIDRequestState.PendingUserConfirmation});
|
||||||
|
|
||||||
Modal.createTrackedDialog("OpenID widget permissions", '', WidgetOpenIDPermissionsDialog, {
|
Modal.createTrackedDialog("OpenID widget permissions", '', WidgetOpenIDPermissionsDialog, {
|
||||||
widgetUrl: rawUrl,
|
widget: this.forWidget,
|
||||||
widgetId: this.forWidget.id,
|
widgetKind: this.forWidgetKind,
|
||||||
isUserWidget: isUserWidget,
|
inRoomId: this.inRoomId,
|
||||||
|
|
||||||
onFinished: async (confirm) => {
|
onFinished: async (confirm) => {
|
||||||
if (!confirm) {
|
if (!confirm) {
|
||||||
|
|
88
src/stores/widgets/WidgetPermissionStore.ts
Normal file
88
src/stores/widgets/WidgetPermissionStore.ts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* 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 SettingsStore from "../../settings/SettingsStore";
|
||||||
|
import { Widget, WidgetKind } from "matrix-widget-api";
|
||||||
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
|
import { SettingLevel } from "../../settings/SettingLevel";
|
||||||
|
|
||||||
|
export enum OIDCState {
|
||||||
|
Allowed, // user has set the remembered value as allowed
|
||||||
|
Denied, // user has set the remembered value as disallowed
|
||||||
|
Unknown, // user has not set a remembered value
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WidgetPermissionStore {
|
||||||
|
private static internalInstance: WidgetPermissionStore;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static get instance(): WidgetPermissionStore {
|
||||||
|
if (!WidgetPermissionStore.internalInstance) {
|
||||||
|
WidgetPermissionStore.internalInstance = new WidgetPermissionStore();
|
||||||
|
}
|
||||||
|
return WidgetPermissionStore.internalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO (all functions here): Merge widgetKind with the widget definition
|
||||||
|
|
||||||
|
private packSettingKey(widget: Widget, kind: WidgetKind, roomId?: string): string {
|
||||||
|
let location = roomId;
|
||||||
|
if (kind !== WidgetKind.Room) {
|
||||||
|
location = MatrixClientPeg.get().getUserId();
|
||||||
|
}
|
||||||
|
if (kind === WidgetKind.Modal) {
|
||||||
|
location = '*MODAL*-' + location; // to guarantee differentiation from whatever spawned it
|
||||||
|
}
|
||||||
|
if (!location) {
|
||||||
|
throw new Error("Failed to determine a location to check the widget's OIDC state with");
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodeURIComponent(`${location}::${widget.templateUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getOIDCState(widget: Widget, kind: WidgetKind, roomId?: string): OIDCState {
|
||||||
|
const settingsKey = this.packSettingKey(widget, kind, roomId);
|
||||||
|
const settings = SettingsStore.getValue("widgetOpenIDPermissions");
|
||||||
|
if (settings?.deny?.includes(settingsKey)) {
|
||||||
|
return OIDCState.Denied;
|
||||||
|
}
|
||||||
|
if (settings?.allow?.includes(settingsKey)) {
|
||||||
|
return OIDCState.Allowed;
|
||||||
|
}
|
||||||
|
return OIDCState.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setOIDCState(widget: Widget, kind: WidgetKind, roomId: string, newState: OIDCState) {
|
||||||
|
const settingsKey = this.packSettingKey(widget, kind, roomId);
|
||||||
|
|
||||||
|
const currentValues = SettingsStore.getValue("widgetOpenIDPermissions");
|
||||||
|
if (!currentValues.allow) currentValues.allow = [];
|
||||||
|
if (!currentValues.deny) currentValues.deny = [];
|
||||||
|
|
||||||
|
if (newState === OIDCState.Allowed) {
|
||||||
|
currentValues.allow.push(settingsKey);
|
||||||
|
} else if (newState === OIDCState.Denied) {
|
||||||
|
currentValues.deny.push(settingsKey);
|
||||||
|
} else {
|
||||||
|
currentValues.allow = currentValues.allow.filter(c => c !== settingsKey);
|
||||||
|
currentValues.deny = currentValues.deny.filter(c => c !== settingsKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsStore.setValue("widgetOpenIDPermissions", null, SettingLevel.DEVICE, currentValues);
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,6 @@ import SdkConfig from "../SdkConfig";
|
||||||
import dis from '../dispatcher/dispatcher';
|
import dis from '../dispatcher/dispatcher';
|
||||||
import WidgetEchoStore from '../stores/WidgetEchoStore';
|
import WidgetEchoStore from '../stores/WidgetEchoStore';
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
|
||||||
import {IntegrationManagers} from "../integrations/IntegrationManagers";
|
import {IntegrationManagers} from "../integrations/IntegrationManagers";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
import {WidgetType} from "../widgets/WidgetType";
|
import {WidgetType} from "../widgets/WidgetType";
|
||||||
|
@ -457,27 +456,6 @@ export default class WidgetUtils {
|
||||||
return capWhitelist;
|
return capWhitelist;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getWidgetSecurityKey(widgetId: string, widgetUrl: string, isUserWidget: boolean): string {
|
|
||||||
let widgetLocation = ActiveWidgetStore.getRoomId(widgetId);
|
|
||||||
|
|
||||||
if (isUserWidget) {
|
|
||||||
const userWidget = WidgetUtils.getUserWidgetsArray()
|
|
||||||
.find((w) => w.id === widgetId && w.content && w.content.url === widgetUrl);
|
|
||||||
|
|
||||||
if (!userWidget) {
|
|
||||||
throw new Error("No matching user widget to form security key");
|
|
||||||
}
|
|
||||||
|
|
||||||
widgetLocation = userWidget.sender;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!widgetLocation) {
|
|
||||||
throw new Error("Failed to locate where the widget resides");
|
|
||||||
}
|
|
||||||
|
|
||||||
return encodeURIComponent(`${widgetLocation}::${widgetUrl}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getLocalJitsiWrapperUrl(opts: {forLocalRender?: boolean, auth?: string} = {}) {
|
static getLocalJitsiWrapperUrl(opts: {forLocalRender?: boolean, auth?: string} = {}) {
|
||||||
// NB. we can't just encodeURIComponent all of these because the $ signs need to be there
|
// NB. we can't just encodeURIComponent all of these because the $ signs need to be there
|
||||||
const queryStringParts = [
|
const queryStringParts = [
|
||||||
|
|
|
@ -36,6 +36,7 @@ describe("<TextualBody />", () => {
|
||||||
MatrixClientPeg.matrixClient = {
|
MatrixClientPeg.matrixClient = {
|
||||||
getRoom: () => mkStubRoom("room_id"),
|
getRoom: () => mkStubRoom("room_id"),
|
||||||
getAccountData: () => undefined,
|
getAccountData: () => undefined,
|
||||||
|
isGuest: () => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ev = mkEvent({
|
const ev = mkEvent({
|
||||||
|
@ -59,6 +60,7 @@ describe("<TextualBody />", () => {
|
||||||
MatrixClientPeg.matrixClient = {
|
MatrixClientPeg.matrixClient = {
|
||||||
getRoom: () => mkStubRoom("room_id"),
|
getRoom: () => mkStubRoom("room_id"),
|
||||||
getAccountData: () => undefined,
|
getAccountData: () => undefined,
|
||||||
|
isGuest: () => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ev = mkEvent({
|
const ev = mkEvent({
|
||||||
|
@ -83,6 +85,7 @@ describe("<TextualBody />", () => {
|
||||||
MatrixClientPeg.matrixClient = {
|
MatrixClientPeg.matrixClient = {
|
||||||
getRoom: () => mkStubRoom("room_id"),
|
getRoom: () => mkStubRoom("room_id"),
|
||||||
getAccountData: () => undefined,
|
getAccountData: () => undefined,
|
||||||
|
isGuest: () => false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -135,6 +138,7 @@ describe("<TextualBody />", () => {
|
||||||
getHomeserverUrl: () => "https://my_server/",
|
getHomeserverUrl: () => "https://my_server/",
|
||||||
on: () => undefined,
|
on: () => undefined,
|
||||||
removeListener: () => undefined,
|
removeListener: () => undefined,
|
||||||
|
isGuest: () => false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6206,6 +6206,13 @@ jsx-ast-utils@^2.4.1:
|
||||||
array-includes "^3.1.1"
|
array-includes "^3.1.1"
|
||||||
object.assign "^4.1.0"
|
object.assign "^4.1.0"
|
||||||
|
|
||||||
|
katex@^0.12.0:
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/katex/-/katex-0.12.0.tgz#2fb1c665dbd2b043edcf8a1f5c555f46beaa0cb9"
|
||||||
|
integrity sha512-y+8btoc/CK70XqcHqjxiGWBOeIL8upbS0peTPXTvgrh21n1RiWWcIpSWM+4uXq+IAgNh9YYQWdc7LVDPDAEEAg==
|
||||||
|
dependencies:
|
||||||
|
commander "^2.19.0"
|
||||||
|
|
||||||
kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
|
kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
|
||||||
version "3.2.2"
|
version "3.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
|
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
|
||||||
|
|
Loading…
Reference in a new issue