Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/socials
Conflicts: src/i18n/strings/en_EN.jsonpull/21833/head
commit
eb25c39318
|
@ -76,10 +76,12 @@
|
|||
"highlight.js": "^10.1.2",
|
||||
"html-entities": "^1.3.1",
|
||||
"is-ip": "^2.0.0",
|
||||
"katex": "^0.12.0",
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
"linkifyjs": "^2.1.9",
|
||||
"lodash": "^4.17.19",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-widget-api": "^0.1.0-beta.9",
|
||||
"matrix-widget-api": "^0.1.0-beta.10",
|
||||
"minimist": "^1.2.5",
|
||||
"pako": "^1.0.11",
|
||||
"parse5": "^5.1.1",
|
||||
|
|
|
@ -231,9 +231,29 @@ limitations under the License.
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
&.mx_UserMenu_contextMenu_guestPrompts,
|
||||
&.mx_UserMenu_contextMenu_hostingLink {
|
||||
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 {
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
iframe {
|
||||
// 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 CountlyAnalytics from "./CountlyAnalytics";
|
||||
import {UIFeature} from "./settings/UIFeature";
|
||||
import { CallError } from "matrix-js-sdk/src/webrtc/call";
|
||||
|
||||
enum AudioID {
|
||||
Ring = 'ringAudio',
|
||||
|
@ -226,11 +227,17 @@ export default class CallHandler {
|
|||
}
|
||||
|
||||
private setCallListeners(call: MatrixCall) {
|
||||
call.on(CallEvent.Error, (err) => {
|
||||
call.on(CallEvent.Error, (err: CallError) => {
|
||||
if (!this.matchesCallForThisRoom(call)) return;
|
||||
|
||||
Analytics.trackEvent('voip', 'callError', 'error', err);
|
||||
Analytics.trackEvent('voip', 'callError', 'error', err.toString());
|
||||
console.error("Call error:", err);
|
||||
|
||||
if (err.code === CallErrorCode.NoUserMedia) {
|
||||
this.showMediaCaptureError(call);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
MatrixClientPeg.get().getTurnServers().length === 0 &&
|
||||
SettingsStore.getValue("fallbackICEServerAllowed") === null
|
||||
|
@ -299,8 +306,9 @@ export default class CallHandler {
|
|||
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
|
||||
title, description,
|
||||
});
|
||||
} else if (call.hangupReason === CallErrorCode.AnsweredElsewhere) {
|
||||
this.play(AudioID.Busy);
|
||||
} else if (
|
||||
call.hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting
|
||||
) {
|
||||
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
|
||||
title: _t("Answered Elsewhere"),
|
||||
description: _t("The call was answered on another device."),
|
||||
|
@ -377,6 +385,34 @@ export default class CallHandler {
|
|||
}, 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(
|
||||
roomId: string, type: PlaceCallType,
|
||||
|
|
|
@ -27,9 +27,12 @@ import _linkifyString from 'linkifyjs/string';
|
|||
import classNames from 'classnames';
|
||||
import EMOJIBASE_REGEX from 'emojibase-regex';
|
||||
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 SettingsStore from './settings/SettingsStore';
|
||||
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
||||
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
|
||||
import ReplyThread from "./components/views/elements/ReplyThread";
|
||||
|
@ -240,7 +243,8 @@ const sanitizeHtmlParams: IExtendedSanitizeOptions = {
|
|||
allowedAttributes: {
|
||||
// custom ones first:
|
||||
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
|
||||
img: ['src', 'width', 'height', 'alt', 'title'],
|
||||
ol: ['start'],
|
||||
|
@ -414,6 +418,21 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
|
|||
if (isHtmlMessage) {
|
||||
isDisplayedWithHtml = true;
|
||||
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 {
|
||||
delete sanitizeParams.textFilter;
|
||||
|
@ -515,7 +534,6 @@ export function checkBlockNode(node: Node) {
|
|||
case "H6":
|
||||
case "PRE":
|
||||
case "BLOCKQUOTE":
|
||||
case "DIV":
|
||||
case "P":
|
||||
case "UL":
|
||||
case "OL":
|
||||
|
@ -528,6 +546,9 @@ export function checkBlockNode(node: Node) {
|
|||
case "TH":
|
||||
case "TD":
|
||||
return true;
|
||||
case "DIV":
|
||||
// don't treat math nodes as block nodes for deserializing
|
||||
return !(node as HTMLElement).hasAttribute("data-mx-maths");
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform";
|
|||
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
|
||||
import CountlyAnalytics from "./CountlyAnalytics";
|
||||
import CallHandler from './CallHandler';
|
||||
import LifecycleCustomisations from "./customisations/Lifecycle";
|
||||
|
||||
const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||
const ID_SERVER_URL_KEY = "mx_is_url";
|
||||
|
@ -589,9 +590,9 @@ export function logout(): void {
|
|||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
// logout doesn't work for guest sessions
|
||||
// Also we sometimes want to re-log in a guest session
|
||||
// if we abort the login
|
||||
onLoggedOut();
|
||||
// Also we sometimes want to re-log in a guest session if we abort the login.
|
||||
// defer until next tick because it calls a synchronous dispatch and we are likely here from a dispatch.
|
||||
setImmediate(() => onLoggedOut());
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -716,6 +717,7 @@ export async function onLoggedOut(): Promise<void> {
|
|||
dis.dispatch({action: 'on_logged_out'}, true);
|
||||
stopMatrixClient();
|
||||
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'];
|
||||
|
||||
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
|
||||
// allow <del> anyway.
|
||||
const matches = /^<\/?(.*)>$/.exec(node.literal);
|
||||
|
@ -30,6 +35,7 @@ function is_allowed_html_tag(node) {
|
|||
const tag = matches[1];
|
||||
return ALLOWED_HTML_TAGS.indexOf(tag) > -1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -455,7 +455,7 @@ function textForWidgetEvent(event) {
|
|||
let widgetName = name || prevName || type || prevType || '';
|
||||
// Apply sentence case to widget name
|
||||
if (widgetName && widgetName.length > 0) {
|
||||
widgetName = widgetName[0].toUpperCase() + widgetName.slice(1) + ' ';
|
||||
widgetName = widgetName[0].toUpperCase() + widgetName.slice(1);
|
||||
}
|
||||
|
||||
// If the widget was removed, its content should be {}, but this is sufficiently
|
||||
|
|
|
@ -29,7 +29,7 @@ import LogoutDialog from "../views/dialogs/LogoutDialog";
|
|||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import {getCustomTheme} from "../../theme";
|
||||
import {getHostingLink} from "../../utils/HostingLink";
|
||||
import {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import {getHomePageUrl} from "../../utils/pages";
|
||||
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
|
||||
};
|
||||
|
||||
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) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -261,10 +271,29 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
|
||||
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
|
||||
let hostingLink;
|
||||
let topSection;
|
||||
const signupLink = getHostingLink("user-context-menu");
|
||||
if (signupLink) {
|
||||
hostingLink = (
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
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">
|
||||
{_t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
|
@ -422,6 +451,20 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
</IconizedContextMenuOptionList>
|
||||
</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({
|
||||
|
@ -451,7 +494,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
/>
|
||||
</AccessibleTooltipButton>
|
||||
</div>
|
||||
{hostingLink}
|
||||
{topSection}
|
||||
{primaryOptionList}
|
||||
{secondarySection}
|
||||
</IconizedContextMenu>;
|
||||
|
|
|
@ -38,6 +38,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
|
||||
import { arrayFastClone } from "../../../utils/arrays";
|
||||
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
|
||||
|
||||
interface IProps {
|
||||
widgetDefinition: IModalWidgetOpenRequestData;
|
||||
|
@ -64,7 +65,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.widget = new Widget({
|
||||
this.widget = new ElementWidget({
|
||||
...this.props.widgetDefinition,
|
||||
creatorUserId: MatrixClientPeg.get().getUserId(),
|
||||
id: `modal_${this.props.sourceWidgetId}`,
|
||||
|
@ -161,7 +162,9 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
|||
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 }
|
||||
</AccessibleButton>;
|
||||
});
|
||||
|
|
|
@ -17,18 +17,17 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import * as sdk from "../../../index";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||
import {Widget} from "matrix-widget-api";
|
||||
import {OIDCState, WidgetPermissionStore} from "../../../stores/widgets/WidgetPermissionStore";
|
||||
|
||||
export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
widgetUrl: PropTypes.string.isRequired,
|
||||
widgetId: PropTypes.string.isRequired,
|
||||
isUserWidget: PropTypes.bool.isRequired,
|
||||
widget: PropTypes.objectOf(Widget).isRequired,
|
||||
widgetKind: PropTypes.string.isRequired, // WidgetKind from widget-api
|
||||
inRoomId: PropTypes.string,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
@ -51,16 +50,10 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
|||
if (this.state.rememberSelection) {
|
||||
console.log(`Remembering ${this.props.widgetId} as allowed=${allowed} for OpenID`);
|
||||
|
||||
const currentValues = SettingsStore.getValue("widgetOpenIDPermissions");
|
||||
if (!currentValues.allow) currentValues.allow = [];
|
||||
if (!currentValues.deny) currentValues.deny = [];
|
||||
|
||||
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);
|
||||
WidgetPermissionStore.instance.setOIDCState(
|
||||
this.props.widget, this.props.widgetKind, this.props.inRoomId,
|
||||
allowed ? OIDCState.Allowed : OIDCState.Denied,
|
||||
);
|
||||
}
|
||||
|
||||
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. " +
|
||||
"By allowing this, the widget will be able to verify your user ID, but not " +
|
||||
"perform actions as you.", {
|
||||
widgetUrl: this.props.widgetUrl.split("?")[0],
|
||||
widgetUrl: this.props.widget.templateUrl.split("?")[0],
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
|
|
|
@ -375,17 +375,20 @@ export default class AppTile extends React.Component {
|
|||
</div>
|
||||
);
|
||||
|
||||
// all widgets can theoretically be allowed to remain on screen, so we 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.
|
||||
if (!this.props.userWidget) {
|
||||
// All room widgets can theoretically be allowed to remain on screen, so we
|
||||
// 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
|
||||
// AppTile's border is in the wrong place
|
||||
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
||||
<PersistedElement persistKey={this._persistKey}>
|
||||
{appTileBody}
|
||||
</PersistedElement>
|
||||
</div>;
|
||||
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
||||
// AppTile's border is in the wrong place
|
||||
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
||||
<PersistedElement persistKey={this._persistKey}>
|
||||
{appTileBody}
|
||||
</PersistedElement>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -745,13 +745,22 @@ export default class EventTile extends React.Component {
|
|||
}
|
||||
|
||||
if (this.props.mxEvent.sender && avatarSize) {
|
||||
let member;
|
||||
// set member to receiver (target) if it is a 3PID invite
|
||||
// so that the correct avatar is shown as the text is
|
||||
// `$target accepted the invitation for $email`
|
||||
if (this.props.mxEvent.getContent().third_party_invite) {
|
||||
member = this.props.mxEvent.target;
|
||||
} else {
|
||||
member = this.props.mxEvent.sender;
|
||||
}
|
||||
avatar = (
|
||||
<div className="mx_EventTile_avatar">
|
||||
<MemberAvatar member={this.props.mxEvent.sender}
|
||||
width={avatarSize} height={avatarSize}
|
||||
viewUserOnClick={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_EventTile_avatar">
|
||||
<MemberAvatar member={member}
|
||||
width={avatarSize} height={avatarSize}
|
||||
viewUserOnClick={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -21,9 +21,18 @@ import PropTypes from 'prop-types';
|
|||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Spinner from '../elements/Spinner';
|
||||
import withValidation from '../elements/Validation';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from "../../../index";
|
||||
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 {
|
||||
static propTypes = {
|
||||
|
@ -63,6 +72,7 @@ export default class ChangePassword extends React.Component {
|
|||
}
|
||||
|
||||
state = {
|
||||
fieldValid: {},
|
||||
phase: ChangePassword.Phases.Edit,
|
||||
oldPassword: "",
|
||||
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) => {
|
||||
this.setState({
|
||||
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) => {
|
||||
this.setState({
|
||||
newPassword: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onNewPasswordValidate = result => {
|
||||
this.markFieldValid(FIELD_NEW_PASSWORD, result.valid);
|
||||
};
|
||||
|
||||
onChangeNewPasswordConfirm = (ev) => {
|
||||
this.setState({
|
||||
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();
|
||||
|
||||
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
||||
if (!allFieldsValid) {
|
||||
CountlyAnalytics.instance.track("onboarding_registration_submit_failed");
|
||||
return;
|
||||
}
|
||||
|
||||
const oldPassword = this.state.oldPassword;
|
||||
const newPassword = this.state.newPassword;
|
||||
const confirmPassword = this.state.newPasswordConfirm;
|
||||
|
@ -201,9 +269,75 @@ export default class ChangePassword extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
// TODO: Live validation on `new pw == confirm pw`
|
||||
async verifyFieldsBeforeSubmit() {
|
||||
// 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 buttonClassName = this.props.buttonClassName;
|
||||
|
||||
|
@ -213,28 +347,35 @@ export default class ChangePassword extends React.Component {
|
|||
<form className={this.props.className} onSubmit={this.onClickChange}>
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
ref={field => this[FIELD_OLD_PASSWORD] = field}
|
||||
type="password"
|
||||
label={_t('Current password')}
|
||||
value={this.state.oldPassword}
|
||||
onChange={this.onChangeOldPassword}
|
||||
onValidate={this.onOldPasswordValidate}
|
||||
/>
|
||||
</div>
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
<PassphraseField
|
||||
fieldRef={field => this[FIELD_NEW_PASSWORD] = field}
|
||||
type="password"
|
||||
label={_t('New Password')}
|
||||
label='New Password'
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
value={this.state.newPassword}
|
||||
autoFocus={this.props.autoFocusNewPasswordInput}
|
||||
onChange={this.onChangeNewPassword}
|
||||
onValidate={this.onNewPasswordValidate}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
ref={field => this[FIELD_NEW_PASSWORD_CONFIRM] = field}
|
||||
type="password"
|
||||
label={_t("Confirm password")}
|
||||
value={this.state.newPasswordConfirm}
|
||||
onChange={this.onChangeNewPasswordConfirm}
|
||||
onValidate={this.onNewPasswordConfirmValidate}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -394,7 +394,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
className="mx_AppearanceUserSettingsTab_AdvancedToggle"
|
||||
onClick={() => this.setState({showAdvanced: !this.state.showAdvanced})}
|
||||
>
|
||||
{this.state.showAdvanced ? "Hide advanced" : "Show advanced"}
|
||||
{this.state.showAdvanced ? _t("Hide advanced") : _t("Show advanced")}
|
||||
</div>;
|
||||
|
||||
let advanced: React.ReactNode;
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
// customisations they need while still maintaining type safety.
|
||||
export interface ISecurityCustomisations {
|
||||
examineLoginResponse?: (
|
||||
response: any,
|
||||
credentials: IMatrixClientCreds,
|
||||
) => void;
|
||||
persistCredentials?: (
|
||||
credentials: IMatrixClientCreds,
|
||||
) => void;
|
||||
createSecretStorageKey?: () => Uint8Array,
|
||||
getSecretStorageKey?: () => Uint8Array,
|
||||
catchAccessSecretStorageError?: (
|
||||
e: Error,
|
||||
) => void,
|
||||
setupEncryptionNeeded?: (
|
||||
kind: SetupEncryptionKind,
|
||||
) => boolean,
|
||||
getDehydrationKey?: (
|
||||
keyInfo: ISecretStorageKeyInfo,
|
||||
) => Promise<Uint8Array>,
|
||||
examineLoginResponse?: typeof examineLoginResponse;
|
||||
persistCredentials?: typeof persistCredentials;
|
||||
createSecretStorageKey?: typeof createSecretStorageKey,
|
||||
getSecretStorageKey?: typeof getSecretStorageKey,
|
||||
catchAccessSecretStorageError?: typeof catchAccessSecretStorageError,
|
||||
setupEncryptionNeeded?: typeof setupEncryptionNeeded,
|
||||
getDehydrationKey?: typeof getDehydrationKey,
|
||||
}
|
||||
|
||||
// A real customisation module will define and export one or more of the
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Populate this class with the details of your customisations when copying it.
|
||||
import { Capability, Widget } from "matrix-widget-api";
|
||||
|
||||
/**
|
||||
* Approves the widget for capabilities that it requested, if any can be
|
||||
* approved. Typically this will be used to give certain widgets capabilities
|
||||
* without having to prompt the user to approve them. This cannot reject
|
||||
* capabilities that Element will be automatically granting, such as the
|
||||
* ability for Jitsi widgets to stay on screen - those will be approved
|
||||
* regardless.
|
||||
* @param {Widget} widget The widget to approve capabilities for.
|
||||
* @param {Set<Capability>} requestedCapabilities The capabilities the widget requested.
|
||||
* @returns {Set<Capability>} Resolves to the capabilities that are approved for use
|
||||
* by the widget. If none are approved, this should return an empty Set.
|
||||
*/
|
||||
async function preapproveCapabilities(
|
||||
widget: Widget,
|
||||
requestedCapabilities: Set<Capability>,
|
||||
): Promise<Set<Capability>> {
|
||||
return new Set(); // no additional capabilities approved
|
||||
}
|
||||
|
||||
// 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 IWidgetPermissionCustomisations {
|
||||
preapproveCapabilities?: typeof preapproveCapabilities;
|
||||
}
|
||||
|
||||
// A real customisation module will define and export one or more of the
|
||||
// customisation points that make up the interface above.
|
||||
export const WidgetPermissionCustomisations: IWidgetPermissionCustomisations = {};
|
|
@ -21,6 +21,7 @@ import { walkDOMDepthFirst } from "./dom";
|
|||
import { checkBlockNode } from "../HtmlUtils";
|
||||
import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks";
|
||||
import { PartCreator } from "./parts";
|
||||
import SdkConfig from "../SdkConfig";
|
||||
|
||||
function parseAtRoomMentions(text: string, partCreator: PartCreator) {
|
||||
const ATROOM = "@room";
|
||||
|
@ -130,6 +131,23 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl
|
|||
}
|
||||
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":
|
||||
state.listIndex.push((<HTMLOListElement>n).start || 1);
|
||||
/* falls through */
|
||||
|
|
|
@ -18,6 +18,10 @@ limitations under the License.
|
|||
import Markdown from '../Markdown';
|
||||
import {makeGenericPermalink} from "../utils/permalinks/Permalinks";
|
||||
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) {
|
||||
return model.parts.reduce((html, part) => {
|
||||
|
@ -38,10 +42,43 @@ export function mdSerialize(model: EditorModel) {
|
|||
}
|
||||
|
||||
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);
|
||||
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
|
||||
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.",
|
||||
"Try using turn.matrix.org": "Try using turn.matrix.org",
|
||||
"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",
|
||||
"Existing Call": "Existing 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: %(stickerName)s": "%(senderName)s: %(stickerName)s",
|
||||
"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.",
|
||||
"New spinner design": "New spinner design",
|
||||
"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.",
|
||||
"Export E2E room keys": "Export E2E room keys",
|
||||
"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",
|
||||
"Passwords don't match": "Passwords don't match",
|
||||
"Current password": "Current password",
|
||||
"Change Password": "Change Password",
|
||||
"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.",
|
||||
|
@ -1128,6 +1136,8 @@
|
|||
"Message layout": "Message layout",
|
||||
"Compact": "Compact",
|
||||
"Modern": "Modern",
|
||||
"Hide advanced": "Hide advanced",
|
||||
"Show advanced": "Show advanced",
|
||||
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
|
||||
"Customise your appearance": "Customise your appearance",
|
||||
"Appearance Settings only affect this %(brand)s session.": "Appearance Settings only affect this %(brand)s session.",
|
||||
|
@ -1887,11 +1897,6 @@
|
|||
"This address is available to use": "This address is available to use",
|
||||
"This address is already in use": "This address is already in use",
|
||||
"Room directory": "Room directory",
|
||||
"Server Options": "Server Options",
|
||||
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.": "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.",
|
||||
"Join millions for free on the largest public server": "Join millions for free on the largest public server",
|
||||
"Homeserver": "Homeserver",
|
||||
"Continue with %(provider)s": "Continue with %(provider)s",
|
||||
"Sign in with single sign-on": "Sign in with single sign-on",
|
||||
"And %(count)s more...|other": "And %(count)s more...",
|
||||
"Home": "Home",
|
||||
|
@ -1986,8 +1991,6 @@
|
|||
"Name": "Name",
|
||||
"Topic (optional)": "Topic (optional)",
|
||||
"Make this room public": "Make this room public",
|
||||
"Hide advanced": "Hide advanced",
|
||||
"Show advanced": "Show advanced",
|
||||
"Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.",
|
||||
"Create Room": "Create Room",
|
||||
"Sign out": "Sign out",
|
||||
|
@ -2110,10 +2113,6 @@
|
|||
"Use this session to verify your new one, granting it access to encrypted messages:": "Use this session to verify your new one, granting it access to encrypted messages:",
|
||||
"If you didn’t sign in to this session, your account may be compromised.": "If you didn’t sign in to this session, your account may be compromised.",
|
||||
"This wasn't me": "This wasn't me",
|
||||
"Doesn't look like a valid email address": "Doesn't look like a valid email address",
|
||||
"Continuing without email": "Continuing without email",
|
||||
"Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.": "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.",
|
||||
"Email (optional)": "Email (optional)",
|
||||
"Please fill why you're reporting.": "Please fill why you're reporting.",
|
||||
"Report Content to Your Homeserver Administrator": "Report Content to Your Homeserver Administrator",
|
||||
"Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.",
|
||||
|
@ -2147,15 +2146,6 @@
|
|||
"A connection error occurred while trying to contact the server.": "A connection error occurred while trying to contact the server.",
|
||||
"The server is not configured to indicate what the problem is (CORS).": "The server is not configured to indicate what the problem is (CORS).",
|
||||
"Recent changes that have not yet been received": "Recent changes that have not yet been received",
|
||||
"Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server",
|
||||
"Specify a homeserver": "Specify a homeserver",
|
||||
"Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.",
|
||||
"Sign into your homeserver": "Sign into your homeserver",
|
||||
"We call the places you where you can host your account ‘homeservers’.": "We call the places you where you can host your account ‘homeservers’.",
|
||||
"Other homeserver": "Other homeserver",
|
||||
"Use your preferred Matrix homeserver if you have one, or host your own.": "Use your preferred Matrix homeserver if you have one, or host your own.",
|
||||
"Learn more": "Learn more",
|
||||
"About homeservers": "About homeservers",
|
||||
"Sign out and remove encryption keys?": "Sign out and remove encryption keys?",
|
||||
"Clear Storage and Sign Out": "Clear Storage and Sign Out",
|
||||
"Send Logs": "Send Logs",
|
||||
|
@ -2284,6 +2274,8 @@
|
|||
"powered by Matrix": "powered by Matrix",
|
||||
"This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.",
|
||||
"Country Dropdown": "Country Dropdown",
|
||||
"Custom Server Options": "Custom Server Options",
|
||||
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.",
|
||||
"Confirm your identity by entering your account password below.": "Confirm your identity by entering your account password below.",
|
||||
"Password": "Password",
|
||||
"Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.",
|
||||
|
@ -2297,31 +2289,48 @@
|
|||
"Code": "Code",
|
||||
"Submit": "Submit",
|
||||
"Start authentication": "Start authentication",
|
||||
"Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server",
|
||||
"Enter the location of your Element Matrix Services homeserver. It may use your own domain name or be a subdomain of <a>element.io</a>.": "Enter the location of your Element Matrix Services homeserver. It may use your own domain name or be a subdomain of <a>element.io</a>.",
|
||||
"Server Name": "Server Name",
|
||||
"Enter password": "Enter password",
|
||||
"Nice, strong password!": "Nice, strong password!",
|
||||
"Password is allowed, but unsafe": "Password is allowed, but unsafe",
|
||||
"Keep going...": "Keep going...",
|
||||
"Enter username": "Enter username",
|
||||
"Enter email address": "Enter email address",
|
||||
"Doesn't look like a valid email address": "Doesn't look like a valid email address",
|
||||
"Enter phone number": "Enter phone number",
|
||||
"That phone number doesn't look quite right, please check and try again": "That phone number doesn't look quite right, please check and try again",
|
||||
"Doesn't look like a valid phone number": "Doesn't look like a valid phone number",
|
||||
"Email": "Email",
|
||||
"Username": "Username",
|
||||
"Phone": "Phone",
|
||||
"Forgot password?": "Forgot password?",
|
||||
"Not sure of your password? <a>Set a new one</a>": "Not sure of your password? <a>Set a new one</a>",
|
||||
"Sign in with": "Sign in with",
|
||||
"Sign in": "Sign in",
|
||||
"No identity server is configured so you cannot add an email address in order to reset your password in the future.": "No identity server is configured so you cannot add an email address in order to reset your password in the future.",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"Email (optional)": "Email (optional)",
|
||||
"Phone (optional)": "Phone (optional)",
|
||||
"Register": "Register",
|
||||
"Add an email to be able to reset your password.": "Add an email to be able to reset your password.",
|
||||
"Use email or phone to optionally be discoverable by existing contacts.": "Use email or phone to optionally be discoverable by existing contacts.",
|
||||
"Use email to optionally be discoverable by existing contacts.": "Use email to optionally be discoverable by existing contacts.",
|
||||
"Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.",
|
||||
"Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.",
|
||||
"Enter your custom homeserver URL <a>What does this mean?</a>": "Enter your custom homeserver URL <a>What does this mean?</a>",
|
||||
"Homeserver URL": "Homeserver URL",
|
||||
"Enter your custom identity server URL <a>What does this mean?</a>": "Enter your custom identity server URL <a>What does this mean?</a>",
|
||||
"Identity Server URL": "Identity Server URL",
|
||||
"Other servers": "Other servers",
|
||||
"Free": "Free",
|
||||
"Join millions for free on the largest public server": "Join millions for free on the largest public server",
|
||||
"Premium": "Premium",
|
||||
"Premium hosting for organisations <a>Learn more</a>": "Premium hosting for organisations <a>Learn more</a>",
|
||||
"Find other public servers or use a custom server": "Find other public servers or use a custom server",
|
||||
"Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s",
|
||||
"Sign in to your Matrix account on <underlinedServerName />": "Sign in to your Matrix account on <underlinedServerName />",
|
||||
"Sign in with SSO": "Sign in with SSO",
|
||||
"Couldn't load page": "Couldn't load page",
|
||||
"You must <a>register</a> to use this functionality": "You must <a>register</a> to use this functionality",
|
||||
|
@ -2457,6 +2466,8 @@
|
|||
"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",
|
||||
"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",
|
||||
"Security & privacy": "Security & privacy",
|
||||
"All settings": "All settings",
|
||||
|
@ -2475,9 +2486,13 @@
|
|||
"A new password must be entered.": "A new password must be entered.",
|
||||
"New passwords must match each other.": "New passwords must match each other.",
|
||||
"Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.",
|
||||
"Your Matrix account on %(serverName)s": "Your Matrix account on %(serverName)s",
|
||||
"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.",
|
||||
"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.",
|
||||
"Send Reset Email": "Send Reset Email",
|
||||
"Sign in instead": "Sign in instead",
|
||||
"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.",
|
||||
"I have verified my email address": "I have verified my email address",
|
||||
"Your password has been reset.": "Your password has been reset.",
|
||||
|
@ -2499,28 +2514,24 @@
|
|||
"Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.",
|
||||
"Failed to perform homeserver discovery": "Failed to perform homeserver discovery",
|
||||
"This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.",
|
||||
"There was a problem communicating with the homeserver, please try again later.": "There was a problem communicating with the homeserver, please try again later.",
|
||||
"Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.",
|
||||
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.",
|
||||
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
|
||||
"Syncing...": "Syncing...",
|
||||
"Signing In...": "Signing In...",
|
||||
"If you've joined lots of rooms, this might take a while": "If you've joined lots of rooms, this might take a while",
|
||||
"New? <a>Create account</a>": "New? <a>Create account</a>",
|
||||
"Create account": "Create account",
|
||||
"Unable to query for supported registration methods.": "Unable to query for supported registration methods.",
|
||||
"Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.",
|
||||
"This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.",
|
||||
"That username already exists, please try another.": "That username already exists, please try another.",
|
||||
"Continue with %(ssoButtons)s": "Continue with %(ssoButtons)s",
|
||||
"%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s Or %(usernamePassword)s",
|
||||
"Already have an account? <a>Sign in here</a>": "Already have an account? <a>Sign in here</a>",
|
||||
"Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).",
|
||||
"Continue with previous account": "Continue with previous account",
|
||||
"<a>Log in</a> to your new account.": "<a>Log in</a> to your new account.",
|
||||
"You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.",
|
||||
"Registration Successful": "Registration Successful",
|
||||
"Create account": "Create account",
|
||||
"Host account on": "Host account on",
|
||||
"Decide where your account is hosted": "Decide where your account is hosted",
|
||||
"Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s",
|
||||
"Create your Matrix account on <underlinedServerName />": "Create your Matrix account on <underlinedServerName />",
|
||||
"Create your account": "Create your account",
|
||||
"Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase",
|
||||
"Use Recovery Key": "Use Recovery Key",
|
||||
"Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.",
|
||||
|
|
|
@ -117,6 +117,12 @@ export interface 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": {
|
||||
isFeature: true,
|
||||
displayName: _td(
|
||||
|
|
|
@ -64,7 +64,7 @@ export class ModalWidgetStore extends AsyncStoreWithClient<IState> {
|
|||
this.openSourceWidgetId = null;
|
||||
this.modalInstance = null;
|
||||
},
|
||||
});
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
};
|
||||
|
||||
public closeModalWidget = (sourceWidget: Widget, data?: IModalWidgetReturnData) => {
|
||||
|
|
|
@ -34,6 +34,7 @@ import { MarkedExecution } from "../../utils/MarkedExecution";
|
|||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||
import { NameFilterCondition } from "./filters/NameFilterCondition";
|
||||
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
|
||||
import { VisibilityProvider } from "./filters/VisibilityProvider";
|
||||
|
||||
interface IState {
|
||||
tagsEnabled?: boolean;
|
||||
|
@ -401,6 +402,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
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);
|
||||
if (shouldUpdate) {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
|
@ -544,7 +549,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
public async regenerateAllLists({trigger = true}) {
|
||||
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>();
|
||||
if (this.state.tagsEnabled) {
|
||||
for (const room of rooms) {
|
||||
|
|
|
@ -34,6 +34,7 @@ import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } f
|
|||
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
|
||||
import { getListAlgorithmInstance } from "./list-ordering";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { VisibilityProvider } from "../filters/VisibilityProvider";
|
||||
|
||||
/**
|
||||
* 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,
|
||||
// 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
|
||||
// class can safely handle a null room, so this should be safe to do as a backup.
|
||||
this._lastStickyRoom = this._stickyRoom || <IStickyRoom>{};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -17,8 +17,6 @@
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import {
|
||||
ClientWidgetApi,
|
||||
IGetOpenIDActionRequest,
|
||||
IGetOpenIDActionResponseData,
|
||||
IStickerActionRequest,
|
||||
IStickyActionRequest,
|
||||
ITemplateParams,
|
||||
|
@ -27,10 +25,8 @@ import {
|
|||
IWidgetApiRequestEmptyData,
|
||||
IWidgetData,
|
||||
MatrixCapabilities,
|
||||
OpenIDRequestState,
|
||||
runTemplate,
|
||||
Widget,
|
||||
WidgetApiToWidgetAction,
|
||||
WidgetApiFromWidgetAction,
|
||||
IModalWidgetOpenRequest,
|
||||
IWidgetApiErrorResponseData,
|
||||
|
@ -50,8 +46,6 @@ import ActiveWidgetStore from "../ActiveWidgetStore";
|
|||
import { objectShallowClone } from "../../utils/objects";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { ElementWidgetActions, IViewRoomApiRequest } from "./ElementWidgetActions";
|
||||
import Modal from "../../Modal";
|
||||
import WidgetOpenIDPermissionsDialog from "../../components/views/dialogs/WidgetOpenIDPermissionsDialog";
|
||||
import {ModalWidgetStore} from "../ModalWidgetStore";
|
||||
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
|
||||
import {getCustomTheme} from "../../theme";
|
||||
|
@ -74,7 +68,7 @@ interface IAppTileProps {
|
|||
}
|
||||
|
||||
// TODO: Don't use this because it's wrong
|
||||
class ElementWidget extends Widget {
|
||||
export class ElementWidget extends Widget {
|
||||
constructor(private rawDefinition: IWidget) {
|
||||
super(rawDefinition);
|
||||
}
|
||||
|
@ -235,55 +229,6 @@ export class StopGapWidget extends EventEmitter {
|
|||
return this.messaging.widget.id;
|
||||
}
|
||||
|
||||
private onOpenIdReq = async (ev: CustomEvent<IGetOpenIDActionRequest>) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const rawUrl = this.appTileProps.app.url;
|
||||
const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, rawUrl, this.appTileProps.userWidget);
|
||||
|
||||
const settings = SettingsStore.getValue("widgetOpenIDPermissions");
|
||||
if (settings.deny && settings.deny.includes(widgetSecurityKey)) {
|
||||
this.messaging.transport.reply(ev.detail, <IGetOpenIDActionResponseData>{
|
||||
state: OpenIDRequestState.Blocked,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (settings.allow && settings.allow.includes(widgetSecurityKey)) {
|
||||
const credentials = await MatrixClientPeg.get().getOpenIdToken();
|
||||
this.messaging.transport.reply(ev.detail, <IGetOpenIDActionResponseData>{
|
||||
state: OpenIDRequestState.Allowed,
|
||||
...credentials,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm that we received the request
|
||||
this.messaging.transport.reply(ev.detail, <IGetOpenIDActionResponseData>{
|
||||
state: OpenIDRequestState.PendingUserConfirmation,
|
||||
});
|
||||
|
||||
// Actually ask for permission to send the user's data
|
||||
Modal.createTrackedDialog("OpenID widget permissions", '', WidgetOpenIDPermissionsDialog, {
|
||||
widgetUrl: rawUrl,
|
||||
widgetId: this.widgetId,
|
||||
isUserWidget: this.appTileProps.userWidget,
|
||||
|
||||
onFinished: async (confirm) => {
|
||||
const responseBody: IGetOpenIDActionResponseData = {
|
||||
state: confirm ? OpenIDRequestState.Allowed : OpenIDRequestState.Blocked,
|
||||
original_request_id: ev.detail.requestId, // eslint-disable-line camelcase
|
||||
};
|
||||
if (confirm) {
|
||||
const credentials = await MatrixClientPeg.get().getOpenIdToken();
|
||||
Object.assign(responseBody, credentials);
|
||||
}
|
||||
this.messaging.transport.send(WidgetApiToWidgetAction.OpenIDCredentials, responseBody).catch(error => {
|
||||
console.error("Failed to send OpenID credentials: ", error);
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private onOpenModal = async (ev: CustomEvent<IModalWidgetOpenRequest>) => {
|
||||
ev.preventDefault();
|
||||
if (ModalWidgetStore.instance.canOpenModalWidget()) {
|
||||
|
@ -301,11 +246,10 @@ export class StopGapWidget extends EventEmitter {
|
|||
public start(iframe: HTMLIFrameElement) {
|
||||
if (this.started) return;
|
||||
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.on("preparing", () => this.emit("preparing"));
|
||||
this.messaging.on("ready", () => this.emit("ready"));
|
||||
this.messaging.on(`action:${WidgetApiFromWidgetAction.GetOpenIDCredentials}`, this.onOpenIdReq);
|
||||
this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal);
|
||||
WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);
|
||||
|
||||
|
|
|
@ -16,19 +16,30 @@
|
|||
|
||||
import {
|
||||
Capability,
|
||||
EventDirection,
|
||||
IOpenIDCredentials,
|
||||
IOpenIDUpdate,
|
||||
ISendEventDetails,
|
||||
MatrixCapabilities,
|
||||
OpenIDRequestState,
|
||||
SimpleObservable,
|
||||
Widget,
|
||||
WidgetDriver,
|
||||
WidgetEventCapability,
|
||||
WidgetKind,
|
||||
} from "matrix-widget-api";
|
||||
import { iterableDiff, iterableUnion } from "../../utils/iterables";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import ActiveRoomObserver from "../../ActiveRoomObserver";
|
||||
import Modal from "../../Modal";
|
||||
import WidgetOpenIDPermissionsDialog from "../../components/views/dialogs/WidgetOpenIDPermissionsDialog";
|
||||
import WidgetCapabilitiesPromptDialog, {
|
||||
getRememberedCapabilitiesForWidget,
|
||||
} from "../../components/views/dialogs/WidgetCapabilitiesPromptDialog";
|
||||
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
|
||||
|
||||
|
@ -36,13 +47,27 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
|||
private allowedCapabilities: Set<Capability>;
|
||||
|
||||
// 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();
|
||||
|
||||
// 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
|
||||
// button if the widget says it supports 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>> {
|
||||
|
@ -52,7 +77,19 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
|||
const diff = iterableDiff(requested, this.allowedCapabilities);
|
||||
const missing = new Set(diff.removed); // "removed" is "in A (requested) but not in B (allowed)"
|
||||
const allowedSoFar = new Set(this.allowedCapabilities);
|
||||
getRememberedCapabilitiesForWidget(this.forWidget).forEach(cap => allowedSoFar.add(cap));
|
||||
getRememberedCapabilitiesForWidget(this.forWidget).forEach(cap => {
|
||||
allowedSoFar.add(cap);
|
||||
missing.delete(cap);
|
||||
});
|
||||
if (WidgetPermissionCustomisations.preapproveCapabilities) {
|
||||
const approved = await WidgetPermissionCustomisations.preapproveCapabilities(this.forWidget, requested);
|
||||
if (approved) {
|
||||
approved.forEach(cap => {
|
||||
allowedSoFar.add(cap);
|
||||
missing.delete(cap);
|
||||
});
|
||||
}
|
||||
}
|
||||
// TODO: Do something when the widget requests new capabilities not yet asked for
|
||||
if (missing.size > 0) {
|
||||
try {
|
||||
|
@ -79,7 +116,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
|||
|
||||
if (!client || !roomId) throw new Error("Not in a room or not attached to a client");
|
||||
|
||||
let r: {event_id: string} = null; // eslint-disable-line camelcase
|
||||
let r: { event_id: string } = null; // eslint-disable-line camelcase
|
||||
if (stateKey !== null) {
|
||||
// state event
|
||||
r = await client.sendStateEvent(roomId, eventType, content, stateKey);
|
||||
|
@ -90,4 +127,37 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
|||
|
||||
return {roomId, eventId: r.event_id};
|
||||
}
|
||||
|
||||
public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>) {
|
||||
const oidcState = WidgetPermissionStore.instance.getOIDCState(
|
||||
this.forWidget, this.forWidgetKind, this.inRoomId,
|
||||
);
|
||||
|
||||
const getToken = (): Promise<IOpenIDCredentials> => {
|
||||
return MatrixClientPeg.get().getOpenIdToken();
|
||||
};
|
||||
|
||||
if (oidcState === OIDCState.Denied) {
|
||||
return observer.update({state: OpenIDRequestState.Blocked});
|
||||
}
|
||||
if (oidcState === OIDCState.Allowed) {
|
||||
return observer.update({state: OpenIDRequestState.Allowed, token: await getToken()});
|
||||
}
|
||||
|
||||
observer.update({state: OpenIDRequestState.PendingUserConfirmation});
|
||||
|
||||
Modal.createTrackedDialog("OpenID widget permissions", '', WidgetOpenIDPermissionsDialog, {
|
||||
widget: this.forWidget,
|
||||
widgetKind: this.forWidgetKind,
|
||||
inRoomId: this.inRoomId,
|
||||
|
||||
onFinished: async (confirm) => {
|
||||
if (!confirm) {
|
||||
return observer.update({state: OpenIDRequestState.Blocked});
|
||||
}
|
||||
|
||||
return observer.update({state: OpenIDRequestState.Allowed, token: await getToken()});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 WidgetEchoStore from '../stores/WidgetEchoStore';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
||||
import {IntegrationManagers} from "../integrations/IntegrationManagers";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {WidgetType} from "../widgets/WidgetType";
|
||||
|
@ -457,27 +456,6 @@ export default class WidgetUtils {
|
|||
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} = {}) {
|
||||
// NB. we can't just encodeURIComponent all of these because the $ signs need to be there
|
||||
const queryStringParts = [
|
||||
|
|
|
@ -36,6 +36,7 @@ describe("<TextualBody />", () => {
|
|||
MatrixClientPeg.matrixClient = {
|
||||
getRoom: () => mkStubRoom("room_id"),
|
||||
getAccountData: () => undefined,
|
||||
isGuest: () => false,
|
||||
};
|
||||
|
||||
const ev = mkEvent({
|
||||
|
@ -59,6 +60,7 @@ describe("<TextualBody />", () => {
|
|||
MatrixClientPeg.matrixClient = {
|
||||
getRoom: () => mkStubRoom("room_id"),
|
||||
getAccountData: () => undefined,
|
||||
isGuest: () => false,
|
||||
};
|
||||
|
||||
const ev = mkEvent({
|
||||
|
@ -83,6 +85,7 @@ describe("<TextualBody />", () => {
|
|||
MatrixClientPeg.matrixClient = {
|
||||
getRoom: () => mkStubRoom("room_id"),
|
||||
getAccountData: () => undefined,
|
||||
isGuest: () => false,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -135,6 +138,7 @@ describe("<TextualBody />", () => {
|
|||
getHomeserverUrl: () => "https://my_server/",
|
||||
on: () => undefined,
|
||||
removeListener: () => undefined,
|
||||
isGuest: () => false,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
15
yarn.lock
15
yarn.lock
|
@ -6206,6 +6206,13 @@ jsx-ast-utils@^2.4.1:
|
|||
array-includes "^3.1.1"
|
||||
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:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
|
||||
|
@ -6532,10 +6539,10 @@ matrix-react-test-utils@^0.2.2:
|
|||
resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853"
|
||||
integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
|
||||
|
||||
matrix-widget-api@^0.1.0-beta.9:
|
||||
version "0.1.0-beta.9"
|
||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.9.tgz#83952132c1610e013acb3e695f923f971ddd5637"
|
||||
integrity sha512-nXo4iaquSya6hYLXccX8o1K960ckSQ0YXIubRDha+YmB+L09F5a7bUPS5JN2tYANOMzyfFAzWVuFwjHv4+K+rg==
|
||||
matrix-widget-api@^0.1.0-beta.10:
|
||||
version "0.1.0-beta.10"
|
||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.10.tgz#2e4d658d90ff3152c5567089b4ddd21fb44ec1dd"
|
||||
integrity sha512-yX2UURjM1zVp7snPiOFcH9+FDBdHfAdt5HEAyDUHGJ7w/F2zOtcK/y0dMlZ1+XhxY7Wv0IBZH0US8X/ioJRX1A==
|
||||
dependencies:
|
||||
events "^3.2.0"
|
||||
|
||||
|
|
Loading…
Reference in New Issue