Merge branch 'develop' into t3chguy/socials
This commit is contained in:
commit
ba542f291b
33 changed files with 795 additions and 173 deletions
|
@ -76,10 +76,12 @@
|
||||||
"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",
|
||||||
"matrix-widget-api": "^0.1.0-beta.9",
|
"matrix-widget-api": "^0.1.0-beta.10",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"pako": "^1.0.11",
|
"pako": "^1.0.11",
|
||||||
"parse5": "^5.1.1",
|
"parse5": "^5.1.1",
|
||||||
|
|
|
@ -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
|
||||||
|
@ -299,8 +306,9 @@ export default class CallHandler {
|
||||||
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
|
||||||
title, description,
|
title, description,
|
||||||
});
|
});
|
||||||
} else if (call.hangupReason === CallErrorCode.AnsweredElsewhere) {
|
} else if (
|
||||||
this.play(AudioID.Busy);
|
call.hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting
|
||||||
|
) {
|
||||||
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
|
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
|
||||||
title: _t("Answered Elsewhere"),
|
title: _t("Answered Elsewhere"),
|
||||||
description: _t("The call was answered on another device."),
|
description: _t("The call was answered on another device."),
|
||||||
|
@ -377,6 +385,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -455,7 +455,7 @@ function textForWidgetEvent(event) {
|
||||||
let widgetName = name || prevName || type || prevType || '';
|
let widgetName = name || prevName || type || prevType || '';
|
||||||
// Apply sentence case to widget name
|
// Apply sentence case to widget name
|
||||||
if (widgetName && widgetName.length > 0) {
|
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
|
// 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 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,9 +375,11 @@ 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
|
||||||
|
@ -388,6 +390,7 @@ export default class AppTile extends React.Component {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let appTileClasses;
|
let appTileClasses;
|
||||||
if (this.props.miniMode) {
|
if (this.props.miniMode) {
|
||||||
|
|
|
@ -745,9 +745,18 @@ export default class EventTile extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.mxEvent.sender && avatarSize) {
|
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 = (
|
avatar = (
|
||||||
<div className="mx_EventTile_avatar">
|
<div className="mx_EventTile_avatar">
|
||||||
<MemberAvatar member={this.props.mxEvent.sender}
|
<MemberAvatar member={member}
|
||||||
width={avatarSize} height={avatarSize}
|
width={avatarSize} height={avatarSize}
|
||||||
viewUserOnClick={true}
|
viewUserOnClick={true}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -394,7 +394,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
className="mx_AppearanceUserSettingsTab_AdvancedToggle"
|
className="mx_AppearanceUserSettingsTab_AdvancedToggle"
|
||||||
onClick={() => this.setState({showAdvanced: !this.state.showAdvanced})}
|
onClick={() => this.setState({showAdvanced: !this.state.showAdvanced})}
|
||||||
>
|
>
|
||||||
{this.state.showAdvanced ? "Hide advanced" : "Show advanced"}
|
{this.state.showAdvanced ? _t("Hide advanced") : _t("Show advanced")}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
let advanced: React.ReactNode;
|
let advanced: React.ReactNode;
|
||||||
|
|
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
|
||||||
|
|
48
src/customisations/WidgetPermissions.ts
Normal file
48
src/customisations/WidgetPermissions.ts
Normal file
|
@ -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 { 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.",
|
||||||
|
@ -1128,6 +1136,8 @@
|
||||||
"Message layout": "Message layout",
|
"Message layout": "Message layout",
|
||||||
"Compact": "Compact",
|
"Compact": "Compact",
|
||||||
"Modern": "Modern",
|
"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.",
|
"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",
|
"Customise your appearance": "Customise your appearance",
|
||||||
"Appearance Settings only affect this %(brand)s session.": "Appearance Settings only affect this %(brand)s session.",
|
"Appearance Settings only affect this %(brand)s session.": "Appearance Settings only affect this %(brand)s session.",
|
||||||
|
@ -1986,8 +1996,6 @@
|
||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"Topic (optional)": "Topic (optional)",
|
"Topic (optional)": "Topic (optional)",
|
||||||
"Make this room public": "Make this room public",
|
"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.",
|
"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",
|
"Create Room": "Create Room",
|
||||||
"Sign out": "Sign out",
|
"Sign out": "Sign out",
|
||||||
|
@ -2313,7 +2321,6 @@
|
||||||
"Sign in": "Sign in",
|
"Sign in": "Sign in",
|
||||||
"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",
|
||||||
|
@ -2457,6 +2464,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",
|
||||||
|
@ -2475,6 +2484,11 @@
|
||||||
"A new password must be entered.": "A new password must be entered.",
|
"A new password must be entered.": "A new password must be entered.",
|
||||||
"New passwords must match each other.": "New passwords must match each other.",
|
"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.",
|
"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.",
|
"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",
|
||||||
"Sign in instead": "Sign in instead",
|
"Sign in instead": "Sign in instead",
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,8 +17,6 @@
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import {
|
import {
|
||||||
ClientWidgetApi,
|
ClientWidgetApi,
|
||||||
IGetOpenIDActionRequest,
|
|
||||||
IGetOpenIDActionResponseData,
|
|
||||||
IStickerActionRequest,
|
IStickerActionRequest,
|
||||||
IStickyActionRequest,
|
IStickyActionRequest,
|
||||||
ITemplateParams,
|
ITemplateParams,
|
||||||
|
@ -27,10 +25,8 @@ import {
|
||||||
IWidgetApiRequestEmptyData,
|
IWidgetApiRequestEmptyData,
|
||||||
IWidgetData,
|
IWidgetData,
|
||||||
MatrixCapabilities,
|
MatrixCapabilities,
|
||||||
OpenIDRequestState,
|
|
||||||
runTemplate,
|
runTemplate,
|
||||||
Widget,
|
Widget,
|
||||||
WidgetApiToWidgetAction,
|
|
||||||
WidgetApiFromWidgetAction,
|
WidgetApiFromWidgetAction,
|
||||||
IModalWidgetOpenRequest,
|
IModalWidgetOpenRequest,
|
||||||
IWidgetApiErrorResponseData,
|
IWidgetApiErrorResponseData,
|
||||||
|
@ -50,8 +46,6 @@ import ActiveWidgetStore from "../ActiveWidgetStore";
|
||||||
import { objectShallowClone } from "../../utils/objects";
|
import { objectShallowClone } from "../../utils/objects";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import { ElementWidgetActions, IViewRoomApiRequest } from "./ElementWidgetActions";
|
import { ElementWidgetActions, IViewRoomApiRequest } from "./ElementWidgetActions";
|
||||||
import Modal from "../../Modal";
|
|
||||||
import WidgetOpenIDPermissionsDialog from "../../components/views/dialogs/WidgetOpenIDPermissionsDialog";
|
|
||||||
import {ModalWidgetStore} from "../ModalWidgetStore";
|
import {ModalWidgetStore} from "../ModalWidgetStore";
|
||||||
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
|
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
|
||||||
import {getCustomTheme} from "../../theme";
|
import {getCustomTheme} from "../../theme";
|
||||||
|
@ -74,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);
|
||||||
}
|
}
|
||||||
|
@ -235,55 +229,6 @@ export class StopGapWidget extends EventEmitter {
|
||||||
return this.messaging.widget.id;
|
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>) => {
|
private onOpenModal = async (ev: CustomEvent<IModalWidgetOpenRequest>) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (ModalWidgetStore.instance.canOpenModalWidget()) {
|
if (ModalWidgetStore.instance.canOpenModalWidget()) {
|
||||||
|
@ -301,11 +246,10 @@ 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"));
|
||||||
this.messaging.on(`action:${WidgetApiFromWidgetAction.GetOpenIDCredentials}`, this.onOpenIdReq);
|
|
||||||
this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal);
|
this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal);
|
||||||
WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);
|
WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);
|
||||||
|
|
||||||
|
|
|
@ -16,19 +16,30 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Capability,
|
Capability,
|
||||||
|
EventDirection,
|
||||||
|
IOpenIDCredentials,
|
||||||
|
IOpenIDUpdate,
|
||||||
ISendEventDetails,
|
ISendEventDetails,
|
||||||
MatrixCapabilities,
|
MatrixCapabilities,
|
||||||
|
OpenIDRequestState,
|
||||||
|
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 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 { 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
|
||||||
|
|
||||||
|
@ -36,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>> {
|
||||||
|
@ -52,7 +77,19 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
const diff = iterableDiff(requested, this.allowedCapabilities);
|
const diff = iterableDiff(requested, this.allowedCapabilities);
|
||||||
const missing = new Set(diff.removed); // "removed" is "in A (requested) but not in B (allowed)"
|
const missing = new Set(diff.removed); // "removed" is "in A (requested) but not in B (allowed)"
|
||||||
const allowedSoFar = new Set(this.allowedCapabilities);
|
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
|
// TODO: Do something when the widget requests new capabilities not yet asked for
|
||||||
if (missing.size > 0) {
|
if (missing.size > 0) {
|
||||||
try {
|
try {
|
||||||
|
@ -90,4 +127,37 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
|
|
||||||
return {roomId, eventId: r.event_id};
|
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()});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
15
yarn.lock
15
yarn.lock
|
@ -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"
|
||||||
|
@ -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"
|
resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853"
|
||||||
integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
|
integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
|
||||||
|
|
||||||
matrix-widget-api@^0.1.0-beta.9:
|
matrix-widget-api@^0.1.0-beta.10:
|
||||||
version "0.1.0-beta.9"
|
version "0.1.0-beta.10"
|
||||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.9.tgz#83952132c1610e013acb3e695f923f971ddd5637"
|
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.10.tgz#2e4d658d90ff3152c5567089b4ddd21fb44ec1dd"
|
||||||
integrity sha512-nXo4iaquSya6hYLXccX8o1K960ckSQ0YXIubRDha+YmB+L09F5a7bUPS5JN2tYANOMzyfFAzWVuFwjHv4+K+rg==
|
integrity sha512-yX2UURjM1zVp7snPiOFcH9+FDBdHfAdt5HEAyDUHGJ7w/F2zOtcK/y0dMlZ1+XhxY7Wv0IBZH0US8X/ioJRX1A==
|
||||||
dependencies:
|
dependencies:
|
||||||
events "^3.2.0"
|
events "^3.2.0"
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue