Merge branch 'develop' into show-room-name
This commit is contained in:
commit
f2137fe93f
54 changed files with 3522 additions and 579 deletions
291
src/components/views/dialogs/HostSignupDialog.tsx
Normal file
291
src/components/views/dialogs/HostSignupDialog.tsx
Normal file
|
@ -0,0 +1,291 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Modal from "../../../Modal";
|
||||
import PersistedElement from "../elements/PersistedElement";
|
||||
import QuestionDialog from './QuestionDialog';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import classNames from "classnames";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { HostSignupStore } from "../../../stores/HostSignupStore";
|
||||
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
||||
import {
|
||||
IHostSignupConfig,
|
||||
IPostmessage,
|
||||
IPostmessageResponseData,
|
||||
PostmessageAction,
|
||||
} from "./HostSignupDialogTypes";
|
||||
|
||||
const HOST_SIGNUP_KEY = "host_signup";
|
||||
|
||||
interface IProps {}
|
||||
|
||||
interface IState {
|
||||
completed: boolean;
|
||||
error: string;
|
||||
minimized: boolean;
|
||||
}
|
||||
|
||||
export default class HostSignupDialog extends React.PureComponent<IProps, IState> {
|
||||
private iframeRef: React.RefObject<HTMLIFrameElement> = React.createRef();
|
||||
private readonly config: IHostSignupConfig;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
completed: false,
|
||||
error: null,
|
||||
minimized: false,
|
||||
};
|
||||
|
||||
this.config = SdkConfig.get().hostSignup;
|
||||
}
|
||||
|
||||
private messageHandler = async (message: IPostmessage) => {
|
||||
if (!this.config.url.startsWith(message.origin)) {
|
||||
return;
|
||||
}
|
||||
switch (message.data.action) {
|
||||
case PostmessageAction.HostSignupAccountDetailsRequest:
|
||||
this.onAccountDetailsRequest();
|
||||
break;
|
||||
case PostmessageAction.Maximize:
|
||||
this.setState({
|
||||
minimized: false,
|
||||
});
|
||||
break;
|
||||
case PostmessageAction.Minimize:
|
||||
this.setState({
|
||||
minimized: true,
|
||||
});
|
||||
break;
|
||||
case PostmessageAction.SetupComplete:
|
||||
this.setState({
|
||||
completed: true,
|
||||
});
|
||||
break;
|
||||
case PostmessageAction.CloseDialog:
|
||||
return this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private maximizeDialog = () => {
|
||||
this.setState({
|
||||
minimized: false,
|
||||
});
|
||||
// Send this action to the iframe so it can act accordingly
|
||||
this.sendMessage({
|
||||
action: PostmessageAction.Maximize,
|
||||
});
|
||||
}
|
||||
|
||||
private minimizeDialog = () => {
|
||||
this.setState({
|
||||
minimized: true,
|
||||
});
|
||||
// Send this action to the iframe so it can act accordingly
|
||||
this.sendMessage({
|
||||
action: PostmessageAction.Minimize,
|
||||
});
|
||||
}
|
||||
|
||||
private closeDialog = async () => {
|
||||
window.removeEventListener("message", this.messageHandler);
|
||||
// Ensure we destroy the host signup persisted element
|
||||
PersistedElement.destroyElement("host_signup");
|
||||
// Finally clear the flag in
|
||||
return HostSignupStore.instance.setHostSignupActive(false);
|
||||
}
|
||||
|
||||
private onCloseClick = async () => {
|
||||
if (this.state.completed) {
|
||||
// We're done, close
|
||||
return this.closeDialog();
|
||||
} else {
|
||||
Modal.createDialog(
|
||||
QuestionDialog,
|
||||
{
|
||||
title: _t("Confirm abort of host creation"),
|
||||
description: _t(
|
||||
"Are you sure you wish to abort creation of the host? The process cannot be continued.",
|
||||
),
|
||||
button: _t("Abort"),
|
||||
onFinished: result => {
|
||||
if (result) {
|
||||
return this.closeDialog();
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private sendMessage = (message: IPostmessageResponseData) => {
|
||||
this.iframeRef.current.contentWindow.postMessage(message, this.config.url);
|
||||
}
|
||||
|
||||
private async sendAccountDetails() {
|
||||
const openIdToken = await MatrixClientPeg.get().getOpenIdToken();
|
||||
if (!openIdToken || !openIdToken.access_token) {
|
||||
console.warn("Failed to connect to homeserver for OpenID token.")
|
||||
this.setState({
|
||||
completed: true,
|
||||
error: _t("Failed to connect to your homeserver. Please close this dialog and try again."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.sendMessage({
|
||||
action: PostmessageAction.HostSignupAccountDetails,
|
||||
account: {
|
||||
accessToken: await MatrixClientPeg.get().getAccessToken(),
|
||||
name: OwnProfileStore.instance.displayName,
|
||||
openIdToken: openIdToken.access_token,
|
||||
serverName: await MatrixClientPeg.get().getDomain(),
|
||||
userLocalpart: await MatrixClientPeg.get().getUserIdLocalpart(),
|
||||
termsAccepted: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private onAccountDetailsDialogFinished = async (result) => {
|
||||
if (result) {
|
||||
return this.sendAccountDetails();
|
||||
}
|
||||
return this.closeDialog();
|
||||
}
|
||||
|
||||
private onAccountDetailsRequest = () => {
|
||||
const textComponent = (
|
||||
<>
|
||||
<p>
|
||||
{_t("Continuing temporarily allows the %(hostSignupBrand)s setup process to access your " +
|
||||
"account to fetch verified email addresses. This data is not stored.", {
|
||||
hostSignupBrand: this.config.brand,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{_t("Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.",
|
||||
{},
|
||||
{
|
||||
cookiePolicyLink: () => (
|
||||
<a href={this.config.cookiePolicyUrl} target="_blank" rel="noreferrer noopener">
|
||||
{_t("Cookie Policy")}
|
||||
</a>
|
||||
),
|
||||
privacyPolicyLink: () => (
|
||||
<a href={this.config.privacyPolicyUrl} target="_blank" rel="noreferrer noopener">
|
||||
{_t("Privacy Policy")}
|
||||
</a>
|
||||
),
|
||||
termsOfServiceLink: () => (
|
||||
<a href={this.config.termsOfServiceUrl} target="_blank" rel="noreferrer noopener">
|
||||
{_t("Terms of Service")}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
Modal.createDialog(
|
||||
QuestionDialog,
|
||||
{
|
||||
title: _t("You should know"),
|
||||
description: textComponent,
|
||||
button: _t("Continue"),
|
||||
onFinished: this.onAccountDetailsDialogFinished,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
window.addEventListener("message", this.messageHandler);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (HostSignupStore.instance.isHostSignupActive) {
|
||||
// Run the close dialog actions if we're still active, otherwise good to go
|
||||
return this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_HostSignup_persisted">
|
||||
<PersistedElement key={HOST_SIGNUP_KEY} persistKey={HOST_SIGNUP_KEY}>
|
||||
<div className={classNames({ "mx_Dialog_wrapper": !this.state.minimized })}>
|
||||
<div
|
||||
className={classNames("mx_Dialog",
|
||||
{
|
||||
"mx_HostSignupDialog_minimized": this.state.minimized,
|
||||
"mx_HostSignupDialog": !this.state.minimized,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{this.state.minimized &&
|
||||
<div className="mx_Dialog_header mx_Dialog_headerWithButton">
|
||||
<div className="mx_Dialog_title">
|
||||
{_t("%(hostSignupBrand)s Setup", {
|
||||
hostSignupBrand: this.config.brand,
|
||||
})}
|
||||
</div>
|
||||
<AccessibleButton
|
||||
className="mx_HostSignup_maximize_button"
|
||||
onClick={this.maximizeDialog}
|
||||
aria-label={_t("Maximize dialog")}
|
||||
title={_t("Maximize dialog")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{!this.state.minimized &&
|
||||
<div className="mx_Dialog_header mx_Dialog_headerWithCancel">
|
||||
<AccessibleButton
|
||||
onClick={this.minimizeDialog}
|
||||
className="mx_HostSignup_minimize_button"
|
||||
aria-label={_t("Minimize dialog")}
|
||||
title={_t("Minimize dialog")}
|
||||
/>
|
||||
<AccessibleButton
|
||||
onClick={this.onCloseClick}
|
||||
className="mx_Dialog_cancelButton"
|
||||
aria-label={_t("Close dialog")}
|
||||
title={_t("Close dialog")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{this.state.error &&
|
||||
<div>
|
||||
{this.state.error}
|
||||
</div>
|
||||
}
|
||||
{!this.state.error &&
|
||||
<iframe
|
||||
src={this.config.url}
|
||||
ref={this.iframeRef}
|
||||
sandbox="allow-forms allow-scripts allow-same-origin allow-popups"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</PersistedElement>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
56
src/components/views/dialogs/HostSignupDialogTypes.ts
Normal file
56
src/components/views/dialogs/HostSignupDialogTypes.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export enum PostmessageAction {
|
||||
CloseDialog = "close_dialog",
|
||||
HostSignupAccountDetails = "host_signup_account_details",
|
||||
HostSignupAccountDetailsRequest = "host_signup_account_details_request",
|
||||
Minimize = "host_signup_minimize",
|
||||
Maximize = "host_signup_maximize",
|
||||
SetupComplete = "setup_complete",
|
||||
}
|
||||
|
||||
interface IAccountData {
|
||||
accessToken: string;
|
||||
name: string;
|
||||
openIdToken: string;
|
||||
serverName: string;
|
||||
userLocalpart: string;
|
||||
termsAccepted: boolean;
|
||||
}
|
||||
|
||||
export interface IPostmessageRequestData {
|
||||
action: PostmessageAction;
|
||||
}
|
||||
|
||||
export interface IPostmessageResponseData {
|
||||
action: PostmessageAction;
|
||||
account?: IAccountData;
|
||||
}
|
||||
|
||||
export interface IPostmessage {
|
||||
data: IPostmessageRequestData;
|
||||
origin: string;
|
||||
}
|
||||
|
||||
export interface IHostSignupConfig {
|
||||
brand: string;
|
||||
cookiePolicyUrl: string;
|
||||
domains: Array<string>;
|
||||
privacyPolicyUrl: string;
|
||||
termsOfServiceUrl: string;
|
||||
url: string;
|
||||
}
|
|
@ -18,6 +18,7 @@ import React, {createRef} from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import Field from "../elements/Field";
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
|
||||
export default class TextInputDialog extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -29,6 +30,7 @@ export default class TextInputDialog extends React.Component {
|
|||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
button: PropTypes.string,
|
||||
busyMessage: PropTypes.string, // pass _td string
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
hasCancel: PropTypes.bool,
|
||||
|
@ -40,6 +42,7 @@ export default class TextInputDialog extends React.Component {
|
|||
title: "",
|
||||
value: "",
|
||||
description: "",
|
||||
busyMessage: _td("Loading..."),
|
||||
focus: true,
|
||||
hasCancel: true,
|
||||
};
|
||||
|
@ -51,6 +54,7 @@ export default class TextInputDialog extends React.Component {
|
|||
|
||||
this.state = {
|
||||
value: this.props.value,
|
||||
busy: false,
|
||||
valid: false,
|
||||
};
|
||||
}
|
||||
|
@ -66,11 +70,13 @@ export default class TextInputDialog extends React.Component {
|
|||
onOk = async ev => {
|
||||
ev.preventDefault();
|
||||
if (this.props.validator) {
|
||||
this.setState({ busy: true });
|
||||
await this._field.current.validate({ allowEmpty: false });
|
||||
|
||||
if (!this._field.current.state.valid) {
|
||||
this._field.current.focus();
|
||||
this._field.current.validate({ allowEmpty: false, focused: true });
|
||||
this.setState({ busy: false });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -125,7 +131,8 @@ export default class TextInputDialog extends React.Component {
|
|||
</div>
|
||||
</form>
|
||||
<DialogButtons
|
||||
primaryButton={this.props.button}
|
||||
primaryButton={this.state.busy ? _t(this.props.busyMessage) : this.props.button}
|
||||
disabled={this.state.busy}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
onCancel={this.onCancel}
|
||||
hasCancel={this.props.hasCancel}
|
||||
|
|
173
src/components/views/elements/DesktopCapturerSourcePicker.tsx
Normal file
173
src/components/views/elements/DesktopCapturerSourcePicker.tsx
Normal file
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import BaseDialog from "..//dialogs/BaseDialog"
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import {getDesktopCapturerSources} from "matrix-js-sdk/src/webrtc/call";
|
||||
|
||||
export interface DesktopCapturerSource {
|
||||
id: string;
|
||||
name: string;
|
||||
thumbnailURL;
|
||||
}
|
||||
|
||||
export enum Tabs {
|
||||
Screens = "screens",
|
||||
Windows = "windows",
|
||||
}
|
||||
|
||||
export interface DesktopCapturerSourceIProps {
|
||||
source: DesktopCapturerSource;
|
||||
onSelect(source: DesktopCapturerSource): void;
|
||||
}
|
||||
|
||||
export class ExistingSource extends React.Component<DesktopCapturerSourceIProps> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onClick = (ev) => {
|
||||
this.props.onSelect(this.props.source);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_desktopCapturerSourcePicker_stream_button"
|
||||
title={this.props.source.name}
|
||||
onClick={this.onClick} >
|
||||
<img
|
||||
className="mx_desktopCapturerSourcePicker_stream_thumbnail"
|
||||
src={this.props.source.thumbnailURL}
|
||||
/>
|
||||
<span className="mx_desktopCapturerSourcePicker_stream_name">{this.props.source.name}</span>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface DesktopCapturerSourcePickerIState {
|
||||
selectedTab: Tabs;
|
||||
sources: Array<DesktopCapturerSource>;
|
||||
}
|
||||
export interface DesktopCapturerSourcePickerIProps {
|
||||
onFinished(source: DesktopCapturerSource): void;
|
||||
}
|
||||
|
||||
export default class DesktopCapturerSourcePicker extends React.Component<
|
||||
DesktopCapturerSourcePickerIProps,
|
||||
DesktopCapturerSourcePickerIState
|
||||
> {
|
||||
interval;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
selectedTab: Tabs.Screens,
|
||||
sources: [],
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// setInterval() first waits and then executes, therefore
|
||||
// we call getDesktopCapturerSources() here without any delay.
|
||||
// Otherwise the dialog would be left empty for some time.
|
||||
this.setState({
|
||||
sources: await getDesktopCapturerSources(),
|
||||
});
|
||||
|
||||
// We update the sources every 500ms to get newer thumbnails
|
||||
this.interval = setInterval(async () => {
|
||||
this.setState({
|
||||
sources: await getDesktopCapturerSources(),
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
onSelect = (source) => {
|
||||
this.props.onFinished(source);
|
||||
}
|
||||
|
||||
onScreensClick = (ev) => {
|
||||
this.setState({selectedTab: Tabs.Screens});
|
||||
}
|
||||
|
||||
onWindowsClick = (ev) => {
|
||||
this.setState({selectedTab: Tabs.Windows});
|
||||
}
|
||||
|
||||
onCloseClick = (ev) => {
|
||||
this.props.onFinished(null);
|
||||
}
|
||||
|
||||
render() {
|
||||
let sources;
|
||||
if (this.state.selectedTab === Tabs.Screens) {
|
||||
sources = this.state.sources
|
||||
.filter((source) => {
|
||||
return source.id.startsWith("screen");
|
||||
})
|
||||
.map((source) => {
|
||||
return <ExistingSource source={source} onSelect={this.onSelect} key={source.id} />;
|
||||
});
|
||||
} else {
|
||||
sources = this.state.sources
|
||||
.filter((source) => {
|
||||
return source.id.startsWith("window");
|
||||
})
|
||||
.map((source) => {
|
||||
return <ExistingSource source={source} onSelect={this.onSelect} key={source.id} />;
|
||||
});
|
||||
}
|
||||
|
||||
const buttonStyle = "mx_desktopCapturerSourcePicker_tabLabel";
|
||||
const screensButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Screens) ? "_selected" : "");
|
||||
const windowsButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Windows) ? "_selected" : "");
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_desktopCapturerSourcePicker"
|
||||
onFinished={this.onCloseClick}
|
||||
title={_t("Share your screen")}
|
||||
>
|
||||
<div className="mx_desktopCapturerSourcePicker_tabLabels">
|
||||
<AccessibleButton
|
||||
className={screensButtonStyle}
|
||||
onClick={this.onScreensClick}
|
||||
>
|
||||
{_t("Screens")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className={windowsButtonStyle}
|
||||
onClick={this.onWindowsClick}
|
||||
>
|
||||
{_t("Windows")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_desktopCapturerSourcePicker_panel">
|
||||
{ sources }
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
36
src/components/views/host_signup/HostSignupContainer.tsx
Normal file
36
src/components/views/host_signup/HostSignupContainer.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import HostSignupDialog from "../dialogs/HostSignupDialog";
|
||||
import { HostSignupStore } from "../../../stores/HostSignupStore";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
|
||||
const HostSignupContainer = () => {
|
||||
const [isActive, setIsActive] = useState(HostSignupStore.instance.isHostSignupActive);
|
||||
useEventEmitter(HostSignupStore.instance, UPDATE_EVENT, () => {
|
||||
setIsActive(HostSignupStore.instance.isHostSignupActive);
|
||||
});
|
||||
|
||||
return <div className="mx_HostSignupContainer">
|
||||
{isActive &&
|
||||
<HostSignupDialog />
|
||||
}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default HostSignupContainer
|
|
@ -81,6 +81,7 @@ export default class TextualBody extends React.Component {
|
|||
}
|
||||
|
||||
_applyFormatting() {
|
||||
const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers");
|
||||
this.activateSpoilers([this._content.current]);
|
||||
|
||||
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
||||
|
@ -91,29 +92,140 @@ export default class TextualBody extends React.Component {
|
|||
this.calculateUrlPreview();
|
||||
|
||||
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
|
||||
const blocks = ReactDOM.findDOMNode(this).getElementsByTagName("code");
|
||||
if (blocks.length > 0) {
|
||||
// Handle expansion and add buttons
|
||||
const pres = ReactDOM.findDOMNode(this).getElementsByTagName("pre");
|
||||
if (pres.length > 0) {
|
||||
for (let i = 0; i < pres.length; i++) {
|
||||
// If there already is a div wrapping the codeblock we want to skip this.
|
||||
// This happens after the codeblock was edited.
|
||||
if (pres[i].parentNode.className == "mx_EventTile_pre_container") continue;
|
||||
// Wrap a div around <pre> so that the copy button can be correctly positioned
|
||||
// when the <pre> overflows and is scrolled horizontally.
|
||||
const div = this._wrapInDiv(pres[i]);
|
||||
this._handleCodeBlockExpansion(pres[i]);
|
||||
this._addCodeExpansionButton(div, pres[i]);
|
||||
this._addCodeCopyButton(div);
|
||||
if (showLineNumbers) {
|
||||
this._addLineNumbers(pres[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Highlight code
|
||||
const codes = ReactDOM.findDOMNode(this).getElementsByTagName("code");
|
||||
if (codes.length > 0) {
|
||||
// Do this asynchronously: parsing code takes time and we don't
|
||||
// need to block the DOM update on it.
|
||||
setTimeout(() => {
|
||||
if (this._unmounted) return;
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
|
||||
highlight.highlightBlock(blocks[i]);
|
||||
} else {
|
||||
// Only syntax highlight if there's a class starting with language-
|
||||
const classes = blocks[i].className.split(/\s+/).filter(function(cl) {
|
||||
return cl.startsWith('language-') && !cl.startsWith('language-_');
|
||||
});
|
||||
|
||||
if (classes.length != 0) {
|
||||
highlight.highlightBlock(blocks[i]);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
// If the code already has the hljs class we want to skip this.
|
||||
// This happens after the codeblock was edited.
|
||||
if (codes[i].className.includes("hljs")) continue;
|
||||
this._highlightCode(codes[i]);
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
this._addCodeCopyButton();
|
||||
}
|
||||
}
|
||||
|
||||
_addCodeExpansionButton(div, pre) {
|
||||
// Calculate how many percent does the pre element take up.
|
||||
// If it's less than 30% we don't add the expansion button.
|
||||
const percentageOfViewport = pre.offsetHeight / window.innerHeight * 100;
|
||||
if (percentageOfViewport < 30) return;
|
||||
|
||||
const button = document.createElement("span");
|
||||
button.className = "mx_EventTile_button ";
|
||||
if (pre.className == "mx_EventTile_collapsedCodeBlock") {
|
||||
button.className += "mx_EventTile_expandButton";
|
||||
} else {
|
||||
button.className += "mx_EventTile_collapseButton";
|
||||
}
|
||||
|
||||
button.onclick = async () => {
|
||||
button.className = "mx_EventTile_button ";
|
||||
if (pre.className == "mx_EventTile_collapsedCodeBlock") {
|
||||
pre.className = "";
|
||||
button.className += "mx_EventTile_collapseButton";
|
||||
} else {
|
||||
pre.className = "mx_EventTile_collapsedCodeBlock";
|
||||
button.className += "mx_EventTile_expandButton";
|
||||
}
|
||||
|
||||
// By expanding/collapsing we changed
|
||||
// the height, therefore we call this
|
||||
this.props.onHeightChanged();
|
||||
};
|
||||
|
||||
div.appendChild(button);
|
||||
}
|
||||
|
||||
_addCodeCopyButton(div) {
|
||||
const button = document.createElement("span");
|
||||
button.className = "mx_EventTile_button mx_EventTile_copyButton ";
|
||||
|
||||
// Check if expansion button exists. If so
|
||||
// we put the copy button to the bottom
|
||||
const expansionButtonExists = div.getElementsByClassName("mx_EventTile_button");
|
||||
if (expansionButtonExists.length > 0) button.className += "mx_EventTile_buttonBottom";
|
||||
|
||||
button.onclick = async () => {
|
||||
const copyCode = button.parentNode.getElementsByTagName("code")[0];
|
||||
const successful = await copyPlaintext(copyCode.textContent);
|
||||
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
button.onmouseleave = close;
|
||||
};
|
||||
|
||||
div.appendChild(button);
|
||||
}
|
||||
|
||||
_wrapInDiv(pre) {
|
||||
const div = document.createElement("div");
|
||||
div.className = "mx_EventTile_pre_container";
|
||||
|
||||
// Insert containing div in place of <pre> block
|
||||
pre.parentNode.replaceChild(div, pre);
|
||||
// Append <pre> block and copy button to container
|
||||
div.appendChild(pre);
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
_handleCodeBlockExpansion(pre) {
|
||||
if (!SettingsStore.getValue("expandCodeByDefault")) {
|
||||
pre.className = "mx_EventTile_collapsedCodeBlock";
|
||||
}
|
||||
}
|
||||
|
||||
_addLineNumbers(pre) {
|
||||
pre.innerHTML = '<span class="mx_EventTile_lineNumbers"></span>' + pre.innerHTML + '<span></span>';
|
||||
const lineNumbers = pre.getElementsByClassName("mx_EventTile_lineNumbers")[0];
|
||||
// Calculate number of lines in pre
|
||||
const number = pre.innerHTML.split(/\n/).length;
|
||||
// Iterate through lines starting with 1 (number of the first line is 1)
|
||||
for (let i = 1; i < number; i++) {
|
||||
lineNumbers.innerHTML += '<span class="mx_EventTile_lineNumber">' + i + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
_highlightCode(code) {
|
||||
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
|
||||
highlight.highlightBlock(code);
|
||||
} else {
|
||||
// Only syntax highlight if there's a class starting with language-
|
||||
const classes = code.className.split(/\s+/).filter(function(cl) {
|
||||
return cl.startsWith('language-') && !cl.startsWith('language-_');
|
||||
});
|
||||
|
||||
if (classes.length != 0) {
|
||||
highlight.highlightBlock(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,38 +366,6 @@ export default class TextualBody extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_addCodeCopyButton() {
|
||||
// Add 'copy' buttons to pre blocks
|
||||
Array.from(ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre')).forEach((p) => {
|
||||
const button = document.createElement("span");
|
||||
button.className = "mx_EventTile_copyButton";
|
||||
button.onclick = async () => {
|
||||
const copyCode = button.parentNode.getElementsByTagName("pre")[0];
|
||||
const successful = await copyPlaintext(copyCode.textContent);
|
||||
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
button.onmouseleave = close;
|
||||
};
|
||||
|
||||
// Wrap a div around <pre> so that the copy button can be correctly positioned
|
||||
// when the <pre> overflows and is scrolled horizontally.
|
||||
const div = document.createElement("div");
|
||||
div.className = "mx_EventTile_pre_container";
|
||||
|
||||
// Insert containing div in place of <pre> block
|
||||
p.parentNode.replaceChild(div, p);
|
||||
|
||||
// Append <pre> block and copy button to container
|
||||
div.appendChild(p);
|
||||
div.appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
onCancelClick = event => {
|
||||
this.setState({ widgetHidden: true });
|
||||
// FIXME: persist this somewhere smarter than local storage
|
||||
|
|
|
@ -111,7 +111,7 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
|
|||
appear={true} in={this.state.doAnimation} timeout={640}
|
||||
classNames='mx_RoomBreadcrumbs'
|
||||
>
|
||||
<Toolbar className='mx_RoomBreadcrumbs'>
|
||||
<Toolbar className='mx_RoomBreadcrumbs' aria-label={_t("Recently visited rooms")}>
|
||||
{tiles.slice(this.state.skipFirst ? 1 : 0)}
|
||||
</Toolbar>
|
||||
</CSSTransition>
|
||||
|
|
|
@ -47,6 +47,8 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
'alwaysShowTimestamps',
|
||||
'showRedactions',
|
||||
'enableSyntaxHighlightLanguageDetection',
|
||||
'expandCodeByDefault',
|
||||
'showCodeLineNumbers',
|
||||
'showJoinLeaves',
|
||||
'showAvatarChanges',
|
||||
'showDisplaynameChanges',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue