Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into resizable-appsdrawer
Conflicts: res/css/views/rooms/_AppsDrawer.scss
This commit is contained in:
commit
ae65ed5c2e
1200 changed files with 50388 additions and 28790 deletions
|
@ -15,9 +15,36 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {Key} from '../../../Keyboard';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element>;
|
||||
|
||||
/**
|
||||
* children: React's magic prop. Represents all children given to the element.
|
||||
* element: (optional) The base element type. "div" by default.
|
||||
* onClick: (required) Event handler for button activation. Should be
|
||||
* implemented exactly like a normal onClick handler.
|
||||
*/
|
||||
interface IProps extends React.InputHTMLAttributes<Element> {
|
||||
inputRef?: React.Ref<Element>;
|
||||
element?: string;
|
||||
// The kind of button, similar to how Bootstrap works.
|
||||
// See available classes for AccessibleButton for options.
|
||||
kind?: string;
|
||||
// The ARIA role
|
||||
role?: string;
|
||||
// The tabIndex
|
||||
tabIndex?: number;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
onClick?(e?: ButtonEvent): void;
|
||||
}
|
||||
|
||||
interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> {
|
||||
ref?: React.Ref<Element>;
|
||||
}
|
||||
|
||||
/**
|
||||
* AccessibleButton is a generic wrapper for any element that should be treated
|
||||
|
@ -27,11 +54,19 @@ import {Key} from '../../../Keyboard';
|
|||
* @param {Object} props react element properties
|
||||
* @returns {Object} rendered react
|
||||
*/
|
||||
export default function AccessibleButton(props) {
|
||||
const {element, onClick, children, kind, disabled, ...restProps} = props;
|
||||
|
||||
export default function AccessibleButton({
|
||||
element,
|
||||
onClick,
|
||||
children,
|
||||
kind,
|
||||
disabled,
|
||||
inputRef,
|
||||
className,
|
||||
...restProps
|
||||
}: IProps) {
|
||||
const newProps: IAccessibleButtonProps = restProps;
|
||||
if (!disabled) {
|
||||
restProps.onClick = onClick;
|
||||
newProps.onClick = onClick;
|
||||
// We need to consume enter onKeyDown and space onKeyUp
|
||||
// otherwise we are risking also activating other keyboard focusable elements
|
||||
// that might receive focus as a result of the AccessibleButtonClick action
|
||||
|
@ -39,7 +74,7 @@ export default function AccessibleButton(props) {
|
|||
// And divs which we report as role button to assistive technologies.
|
||||
// Browsers handle space and enter keypresses differently and we are only adjusting to the
|
||||
// inconsistencies here
|
||||
restProps.onKeyDown = function(e) {
|
||||
newProps.onKeyDown = (e) => {
|
||||
if (e.key === Key.ENTER) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
@ -50,7 +85,7 @@ export default function AccessibleButton(props) {
|
|||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
restProps.onKeyUp = function(e) {
|
||||
newProps.onKeyUp = (e) => {
|
||||
if (e.key === Key.SPACE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
@ -64,57 +99,26 @@ export default function AccessibleButton(props) {
|
|||
}
|
||||
|
||||
// Pass through the ref - used for keyboard shortcut access to some buttons
|
||||
restProps.ref = restProps.inputRef;
|
||||
delete restProps.inputRef;
|
||||
newProps.ref = inputRef;
|
||||
|
||||
restProps.className = (restProps.className ? restProps.className + " " : "") + "mx_AccessibleButton";
|
||||
|
||||
if (kind) {
|
||||
// We apply a hasKind class to maintain backwards compatibility with
|
||||
// buttons which might not know about kind and break
|
||||
restProps.className += " mx_AccessibleButton_hasKind mx_AccessibleButton_kind_" + kind;
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
restProps.className += " mx_AccessibleButton_disabled";
|
||||
restProps["aria-disabled"] = true;
|
||||
}
|
||||
newProps.className = classnames(
|
||||
"mx_AccessibleButton",
|
||||
className,
|
||||
{
|
||||
"mx_AccessibleButton_hasKind": kind,
|
||||
[`mx_AccessibleButton_kind_${kind}`]: kind,
|
||||
"mx_AccessibleButton_disabled": disabled,
|
||||
},
|
||||
);
|
||||
|
||||
// React.createElement expects InputHTMLAttributes
|
||||
return React.createElement(element, restProps, children);
|
||||
}
|
||||
|
||||
/**
|
||||
* children: React's magic prop. Represents all children given to the element.
|
||||
* element: (optional) The base element type. "div" by default.
|
||||
* onClick: (required) Event handler for button activation. Should be
|
||||
* implemented exactly like a normal onClick handler.
|
||||
*/
|
||||
AccessibleButton.propTypes = {
|
||||
children: PropTypes.node,
|
||||
inputRef: PropTypes.oneOfType([
|
||||
// Either a function
|
||||
PropTypes.func,
|
||||
// Or the instance of a DOM native element
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]),
|
||||
element: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
|
||||
// The kind of button, similar to how Bootstrap works.
|
||||
// See available classes for AccessibleButton for options.
|
||||
kind: PropTypes.string,
|
||||
// The ARIA role
|
||||
role: PropTypes.string,
|
||||
// The tabIndex
|
||||
tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
AccessibleButton.defaultProps = {
|
||||
element: 'div',
|
||||
role: 'button',
|
||||
tabIndex: "0",
|
||||
tabIndex: 0,
|
||||
};
|
||||
|
||||
AccessibleButton.displayName = "AccessibleButton";
|
|
@ -16,47 +16,66 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import * as sdk from "../../../index";
|
||||
import Tooltip from './Tooltip';
|
||||
|
||||
export default class AccessibleTooltipButton extends React.PureComponent {
|
||||
static propTypes = {
|
||||
...AccessibleButton.propTypes,
|
||||
// The tooltip to render on hover
|
||||
title: PropTypes.string.isRequired,
|
||||
};
|
||||
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||
title: string;
|
||||
tooltip?: React.ReactNode;
|
||||
tooltipClassName?: string;
|
||||
forceHide?: boolean;
|
||||
}
|
||||
|
||||
state = {
|
||||
hover: false,
|
||||
};
|
||||
interface IState {
|
||||
hover: boolean;
|
||||
}
|
||||
|
||||
export default class AccessibleTooltipButton extends React.PureComponent<ITooltipProps, IState> {
|
||||
constructor(props: ITooltipProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hover: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<ITooltipProps>) {
|
||||
if (!prevProps.forceHide && this.props.forceHide && this.state.hover) {
|
||||
this.setState({
|
||||
hover: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMouseOver = () => {
|
||||
if (this.props.forceHide) return;
|
||||
this.setState({
|
||||
hover: true,
|
||||
});
|
||||
};
|
||||
|
||||
onMouseOut = () => {
|
||||
onMouseLeave = () => {
|
||||
this.setState({
|
||||
hover: false,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
const {title, children, ...props} = this.props;
|
||||
const {title, tooltip, children, tooltipClassName, ...props} = this.props;
|
||||
|
||||
const tip = this.state.hover ? <Tooltip
|
||||
className="mx_AccessibleTooltipButton_container"
|
||||
tooltipClassName="mx_AccessibleTooltipButton_tooltip"
|
||||
label={title}
|
||||
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
|
||||
label={tooltip || title}
|
||||
/> : <div />;
|
||||
return (
|
||||
<AccessibleButton {...props} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} aria-label={title}>
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
aria-label={title}
|
||||
>
|
||||
{ children }
|
||||
{ tip }
|
||||
</AccessibleButton>
|
|
@ -18,7 +18,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import * as sdk from '../../../index';
|
||||
import Analytics from '../../../Analytics';
|
||||
|
||||
|
|
|
@ -58,18 +58,6 @@ export default createReactClass({
|
|||
imgUrls.push(require("../../../../res/img/icon-email-user.svg"));
|
||||
}
|
||||
|
||||
// Removing networks for now as they're not really supported
|
||||
/*
|
||||
var network;
|
||||
if (this.props.networkUrl !== "") {
|
||||
network = (
|
||||
<div className="mx_AddressTile_network">
|
||||
<BaseAvatar width={25} height={25} name={this.props.networkName} title="Riot" url={this.props.networkUrl} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 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.
|
||||
|
@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
|
|||
import url from 'url';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
|
||||
|
@ -76,6 +77,7 @@ export default class AppPermission extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
|
||||
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||
|
@ -96,7 +98,7 @@ export default class AppPermission extends React.Component {
|
|||
<li>{_t("Your avatar URL")}</li>
|
||||
<li>{_t("Your user ID")}</li>
|
||||
<li>{_t("Your theme")}</li>
|
||||
<li>{_t("Riot URL")}</li>
|
||||
<li>{_t("%(brand)s URL", { brand })}</li>
|
||||
<li>{_t("Room ID")}</li>
|
||||
<li>{_t("Widget ID")}</li>
|
||||
</ul>
|
||||
|
|
|
@ -29,16 +29,19 @@ import { _t } from '../../../languageHandler';
|
|||
import * as sdk from '../../../index';
|
||||
import AppPermission from './AppPermission';
|
||||
import AppWarning from './AppWarning';
|
||||
import MessageSpinner from './MessageSpinner';
|
||||
import Spinner from './Spinner';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
import classNames from 'classnames';
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
||||
import PersistedElement from "./PersistedElement";
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
import {Capability} from "../../../widgets/WidgetApi";
|
||||
import {sleep} from "../../../utils/promise";
|
||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||
|
||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||
const ENABLE_REACT_PERF = false;
|
||||
|
@ -308,16 +311,16 @@ export default class AppTile extends React.Component {
|
|||
this.props.onEditClick();
|
||||
} else {
|
||||
// TODO: Open the right manager for the widget
|
||||
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
|
||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||
IntegrationManagers.sharedInstance().openAll(
|
||||
this.props.room,
|
||||
'type_' + this.props.type,
|
||||
'type_' + this.props.app.type,
|
||||
this.props.app.id,
|
||||
);
|
||||
} else {
|
||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(
|
||||
this.props.room,
|
||||
'type_' + this.props.type,
|
||||
'type_' + this.props.app.type,
|
||||
this.props.app.id,
|
||||
);
|
||||
}
|
||||
|
@ -341,23 +344,37 @@ export default class AppTile extends React.Component {
|
|||
/**
|
||||
* Ends all widget interaction, such as cancelling calls and disabling webcams.
|
||||
* @private
|
||||
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
|
||||
*/
|
||||
_endWidgetActions() {
|
||||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
||||
// its hold on the webcam. Without this, the widget holds a media
|
||||
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
|
||||
if (this._appFrame.current) {
|
||||
// In practice we could just do `+= ''` to trick the browser
|
||||
// into thinking the URL changed, however I can foresee this
|
||||
// being optimized out by a browser. Instead, we'll just point
|
||||
// the iframe at a page that is reasonably safe to use in the
|
||||
// event the iframe doesn't wink away.
|
||||
// This is relative to where the Riot instance is located.
|
||||
this._appFrame.current.src = 'about:blank';
|
||||
let terminationPromise;
|
||||
|
||||
if (this._hasCapability(Capability.ReceiveTerminate)) {
|
||||
// Wait for widget to terminate within a timeout
|
||||
const timeout = 2000;
|
||||
const messaging = ActiveWidgetStore.getWidgetMessaging(this.props.app.id);
|
||||
terminationPromise = Promise.race([messaging.terminate(), sleep(timeout)]);
|
||||
} else {
|
||||
terminationPromise = Promise.resolve();
|
||||
}
|
||||
|
||||
// Delete the widget from the persisted store for good measure.
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
return terminationPromise.finally(() => {
|
||||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
||||
// its hold on the webcam. Without this, the widget holds a media
|
||||
// stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
|
||||
if (this._appFrame.current) {
|
||||
// In practice we could just do `+= ''` to trick the browser
|
||||
// into thinking the URL changed, however I can foresee this
|
||||
// being optimized out by a browser. Instead, we'll just point
|
||||
// the iframe at a page that is reasonably safe to use in the
|
||||
// event the iframe doesn't wink away.
|
||||
// This is relative to where the Element instance is located.
|
||||
this._appFrame.current.src = 'about:blank';
|
||||
}
|
||||
|
||||
// Delete the widget from the persisted store for good measure.
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
});
|
||||
}
|
||||
|
||||
/* If user has permission to modify widgets, delete the widget,
|
||||
|
@ -381,12 +398,12 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
this.setState({deleting: true});
|
||||
|
||||
this._endWidgetActions();
|
||||
|
||||
WidgetUtils.setRoomWidget(
|
||||
this.props.room.roomId,
|
||||
this.props.app.id,
|
||||
).catch((e) => {
|
||||
this._endWidgetActions().then(() => {
|
||||
return WidgetUtils.setRoomWidget(
|
||||
this.props.room.roomId,
|
||||
this.props.app.id,
|
||||
);
|
||||
}).catch((e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
|
@ -424,13 +441,13 @@ export default class AppTile extends React.Component {
|
|||
_setupWidgetMessaging() {
|
||||
// FIXME: There's probably no reason to do this here: it should probably be done entirely
|
||||
// in ActiveWidgetStore.
|
||||
|
||||
// We use the app's URL over the rendered URL so that anything the widget does which could
|
||||
// lead to requesting a "security key" will pass accordingly. The only other thing this URL
|
||||
// is used for is to determine the origin we're talking to, and therefore we don't need the
|
||||
// fully templated URL.
|
||||
const widgetMessaging = new WidgetMessaging(
|
||||
this.props.app.id, this.props.app.url, this.props.userWidget, this._appFrame.current.contentWindow);
|
||||
this.props.app.id,
|
||||
this.props.app.url,
|
||||
this._getRenderedUrl(),
|
||||
this.props.userWidget,
|
||||
this._appFrame.current.contentWindow,
|
||||
);
|
||||
ActiveWidgetStore.setWidgetMessaging(this.props.app.id, widgetMessaging);
|
||||
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
||||
console.log(`Widget ${this.props.app.id} requested capabilities: ` + requestedCapabilities);
|
||||
|
@ -669,6 +686,17 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
|
||||
_onPopoutWidgetClick() {
|
||||
// Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
|
||||
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
|
||||
if (WidgetType.JITSI.matches(this.props.app.type) && this.props.show) {
|
||||
this._endWidgetActions().then(() => {
|
||||
if (this._appFrame.current) {
|
||||
// Reload iframe
|
||||
this._appFrame.current.src = this._getRenderedUrl();
|
||||
this.setState({});
|
||||
}
|
||||
});
|
||||
}
|
||||
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
|
||||
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
||||
Object.assign(document.createElement('a'),
|
||||
|
@ -677,6 +705,7 @@ export default class AppTile extends React.Component {
|
|||
|
||||
_onReloadWidgetClick() {
|
||||
// Reload iframe in this way to avoid cross-origin restrictions
|
||||
// eslint-disable-next-line no-self-assign
|
||||
this._appFrame.current.src = this._appFrame.current.src;
|
||||
}
|
||||
|
||||
|
@ -698,7 +727,7 @@ export default class AppTile extends React.Component {
|
|||
|
||||
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
|
||||
// because that would allow the iframe to programmatically remove the sandbox attribute, but
|
||||
// this would only be for content hosted on the same origin as the riot client: anything
|
||||
// this would only be for content hosted on the same origin as the element client: anything
|
||||
// hosted on the same origin as the client will get the same access as if you clicked
|
||||
// a link to it.
|
||||
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
||||
|
@ -713,7 +742,7 @@ export default class AppTile extends React.Component {
|
|||
if (this.props.show) {
|
||||
const loadingElement = (
|
||||
<div className="mx_AppLoading_spinner_fadeIn">
|
||||
<MessageSpinner msg='Loading...' />
|
||||
<Spinner message={_t("Loading...")} />
|
||||
</div>
|
||||
);
|
||||
if (!this.state.hasPermissionToLoad) {
|
||||
|
@ -897,7 +926,7 @@ AppTile.propTypes = {
|
|||
// Optionally show the reload widget icon
|
||||
// This is not currently intended for use with production widgets. However
|
||||
// it can be useful when developing persistent widgets in order to avoid
|
||||
// having to reload all of riot to get new widget content.
|
||||
// having to reload all of Element to get new widget content.
|
||||
showReload: PropTypes.bool,
|
||||
// Widget capabilities to allow by default (without user confirmation)
|
||||
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
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 * as sdk from '../../../index';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const CreateRoomButton = function(props) {
|
||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
return (
|
||||
<ActionButton action="view_create_room"
|
||||
mouseOverAction={props.callout ? "callout_create_room" : null}
|
||||
label={_t("Create new room")}
|
||||
iconPath={require("../../../../res/img/icons-create-room.svg")}
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
CreateRoomButton.propTypes = {
|
||||
size: PropTypes.string,
|
||||
tooltip: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default CreateRoomButton;
|
|
@ -19,8 +19,22 @@ import TagTile from './TagTile';
|
|||
|
||||
import React from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { ContextMenu, toRightOf, useContextMenu } from "../../structures/ContextMenu";
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
export default function DNDTagTile(props) {
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
let contextMenu = null;
|
||||
if (menuDisplayed && handle.current) {
|
||||
const elementRect = handle.current.getBoundingClientRect();
|
||||
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||
contextMenu = (
|
||||
<ContextMenu {...toRightOf(elementRect)} onFinished={closeMenu}>
|
||||
<TagTileContextMenu tag={props.tag} onFinished={closeMenu} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
return <div>
|
||||
<Draggable
|
||||
key={props.tag}
|
||||
|
@ -28,18 +42,21 @@ export default function DNDTagTile(props) {
|
|||
index={props.index}
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div>
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<TagTile {...props} />
|
||||
</div>
|
||||
{ provided.placeholder }
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<TagTile
|
||||
{...props}
|
||||
contextMenuButtonRef={handle}
|
||||
menuDisplayed={menuDisplayed}
|
||||
openMenu={openMenu}
|
||||
/>
|
||||
</div>
|
||||
) }
|
||||
)}
|
||||
</Draggable>
|
||||
{contextMenu}
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -1,127 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
|
||||
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 PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
// XXX: This component is *not* cross-signing aware. Once everything is
|
||||
// cross-signing, this component should just go away.
|
||||
export default createReactClass({
|
||||
displayName: 'DeviceVerifyButtons',
|
||||
|
||||
propTypes: {
|
||||
userId: PropTypes.string.isRequired,
|
||||
device: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
device: this.props.device,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
}
|
||||
},
|
||||
|
||||
onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) {
|
||||
if (userId === this.props.userId && deviceId === this.props.device.deviceId) {
|
||||
this.setState({ device: deviceInfo });
|
||||
}
|
||||
},
|
||||
|
||||
onVerifyClick: function() {
|
||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
|
||||
userId: this.props.userId,
|
||||
device: this.state.device,
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
},
|
||||
|
||||
onUnverifyClick: function() {
|
||||
MatrixClientPeg.get().setDeviceVerified(
|
||||
this.props.userId, this.state.device.deviceId, false,
|
||||
);
|
||||
},
|
||||
|
||||
onBlacklistClick: function() {
|
||||
MatrixClientPeg.get().setDeviceBlocked(
|
||||
this.props.userId, this.state.device.deviceId, true,
|
||||
);
|
||||
},
|
||||
|
||||
onUnblacklistClick: function() {
|
||||
MatrixClientPeg.get().setDeviceBlocked(
|
||||
this.props.userId, this.state.device.deviceId, false,
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let blacklistButton = null; let verifyButton = null;
|
||||
|
||||
if (this.state.device.isBlocked()) {
|
||||
blacklistButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
|
||||
onClick={this.onUnblacklistClick}>
|
||||
{ _t("Unblacklist") }
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
blacklistButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_blacklist"
|
||||
onClick={this.onBlacklistClick}>
|
||||
{ _t("Blacklist") }
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.device.isVerified()) {
|
||||
verifyButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
|
||||
onClick={this.onUnverifyClick}>
|
||||
{ _t("Unverify") }
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
verifyButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
|
||||
onClick={this.onVerifyClick}>
|
||||
{ _t("Verify...") }
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_DeviceVerifyButtons" >
|
||||
{ verifyButton }
|
||||
{ blacklistButton }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
81
src/components/views/elements/Draggable.tsx
Normal file
81
src/components/views/elements/Draggable.tsx
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
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 React from 'react';
|
||||
|
||||
interface IProps {
|
||||
className: string;
|
||||
dragFunc: (currentLocation: ILocationState, event: MouseEvent) => ILocationState;
|
||||
onMouseUp: (event: MouseEvent) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
onMouseMove: (event: MouseEvent) => void;
|
||||
onMouseUp: (event: MouseEvent) => void;
|
||||
location: ILocationState;
|
||||
}
|
||||
|
||||
export interface ILocationState {
|
||||
currentX: number;
|
||||
currentY: number;
|
||||
}
|
||||
|
||||
export default class Draggable extends React.Component<IProps, IState> {
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
onMouseMove: this.onMouseMove.bind(this),
|
||||
onMouseUp: this.onMouseUp.bind(this),
|
||||
location: {
|
||||
currentX: 0,
|
||||
currentY: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private onMouseDown = (event: MouseEvent): void => {
|
||||
this.setState({
|
||||
location: {
|
||||
currentX: event.clientX,
|
||||
currentY: event.clientY,
|
||||
},
|
||||
});
|
||||
|
||||
document.addEventListener("mousemove", this.state.onMouseMove);
|
||||
document.addEventListener("mouseup", this.state.onMouseUp);
|
||||
};
|
||||
|
||||
private onMouseUp = (event: MouseEvent): void => {
|
||||
document.removeEventListener("mousemove", this.state.onMouseMove);
|
||||
document.removeEventListener("mouseup", this.state.onMouseUp);
|
||||
this.props.onMouseUp(event);
|
||||
};
|
||||
|
||||
private onMouseMove(event: MouseEvent): void {
|
||||
const newLocation = this.props.dragFunc(this.state.location, event);
|
||||
|
||||
this.setState({
|
||||
location: newLocation,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className={this.props.className} onMouseDown={this.onMouseDown.bind(this)} />;
|
||||
}
|
||||
|
||||
}
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from '../../../languageHandler.js';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import Field from "./Field";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ export default class ErrorBoundary extends React.PureComponent {
|
|||
render() {
|
||||
if (this.state.error) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const newIssueUrl = "https://github.com/vector-im/riot-web/issues/new";
|
||||
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
|
||||
return <div className="mx_ErrorBoundary">
|
||||
<div className="mx_ErrorBoundary_body">
|
||||
<h1>{_t("Something went wrong!")}</h1>
|
||||
|
|
|
@ -66,12 +66,12 @@ const EventListSummary = ({events, children, threshold=3, onToggle, startExpande
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
|
||||
<li className="mx_EventListSummary" data-scroll-tokens={eventIds}>
|
||||
<AccessibleButton className="mx_EventListSummary_toggle" onClick={toggleExpanded} aria-expanded={expanded}>
|
||||
{ expanded ? _t('collapse') : _t('expand') }
|
||||
</AccessibleButton>
|
||||
{ body }
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -90,7 +90,7 @@ EventListSummary.propTypes = {
|
|||
// The list of room members for which to show avatars next to the summary
|
||||
summaryMembers: PropTypes.arrayOf(PropTypes.instanceOf(RoomMember)),
|
||||
// The text to show as the summary of this event list
|
||||
summaryText: PropTypes.string.isRequired,
|
||||
summaryText: PropTypes.string,
|
||||
};
|
||||
|
||||
export default EventListSummary;
|
||||
|
|
129
src/components/views/elements/EventTilePreview.tsx
Normal file
129
src/components/views/elements/EventTilePreview.tsx
Normal file
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
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 React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import EventTile from '../rooms/EventTile';
|
||||
|
||||
interface IProps {
|
||||
/**
|
||||
* The text to be displayed in the message preview
|
||||
*/
|
||||
message: string;
|
||||
|
||||
/**
|
||||
* Whether to use the irc layout or not
|
||||
*/
|
||||
useIRCLayout: boolean;
|
||||
|
||||
/**
|
||||
* classnames to apply to the wrapper of the preview
|
||||
*/
|
||||
className: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
userId: string;
|
||||
displayname: string;
|
||||
avatar_url: string;
|
||||
}
|
||||
|
||||
const AVATAR_SIZE = 32;
|
||||
|
||||
export default class EventTilePreview extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
userId: "@erim:fink.fink",
|
||||
displayname: "Erimayas Fink",
|
||||
avatar_url: null,
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// Fetch current user data
|
||||
const client = MatrixClientPeg.get();
|
||||
const userId = client.getUserId();
|
||||
const profileInfo = await client.getProfileInfo(userId);
|
||||
const avatar_url = Avatar.avatarUrlForUser(
|
||||
{avatarUrl: profileInfo.avatar_url},
|
||||
AVATAR_SIZE, AVATAR_SIZE, "crop");
|
||||
|
||||
this.setState({
|
||||
userId,
|
||||
displayname: profileInfo.displayname,
|
||||
avatar_url,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private fakeEvent({userId, displayname, avatar_url}: IState) {
|
||||
// Fake it till we make it
|
||||
const event = new MatrixEvent(JSON.parse(`{
|
||||
"type": "m.room.message",
|
||||
"sender": "${userId}",
|
||||
"content": {
|
||||
"m.new_content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "${this.props.message}",
|
||||
"displayname": "${displayname}",
|
||||
"avatar_url": "${avatar_url}"
|
||||
},
|
||||
"msgtype": "m.text",
|
||||
"body": "${this.props.message}",
|
||||
"displayname": "${displayname}",
|
||||
"avatar_url": "${avatar_url}"
|
||||
},
|
||||
"unsigned": {
|
||||
"age": 97
|
||||
},
|
||||
"event_id": "$9999999999999999999999999999999999999999999",
|
||||
"room_id": "!999999999999999999:matrix.org"
|
||||
}`));
|
||||
|
||||
// Fake it more
|
||||
event.sender = {
|
||||
name: displayname,
|
||||
userId: userId,
|
||||
getAvatarUrl: (..._) => {
|
||||
return avatar_url;
|
||||
},
|
||||
};
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const event = this.fakeEvent(this.state);
|
||||
|
||||
let className = classnames(
|
||||
this.props.className,
|
||||
{
|
||||
"mx_IRCLayout": this.props.useIRCLayout,
|
||||
"mx_GroupLayout": !this.props.useIRCLayout,
|
||||
}
|
||||
);
|
||||
|
||||
return <div className={className}>
|
||||
<EventTile mxEvent={event} useIRCLayout={this.props.useIRCLayout} />
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import * as sdk from '../../../index';
|
||||
import { debounce } from 'lodash';
|
||||
import {IFieldState, IValidationResult} from "./Validation";
|
||||
|
||||
// Invoke validation from user input (when typing, etc.) at most once every N ms.
|
||||
const VALIDATION_THROTTLE_MS = 200;
|
||||
|
@ -29,58 +29,111 @@ function getId() {
|
|||
return `${BASE_ID}_${count++}`;
|
||||
}
|
||||
|
||||
export default class Field extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The field's ID, which binds the input and label together. Immutable.
|
||||
id: PropTypes.string,
|
||||
// The element to create. Defaults to "input".
|
||||
// To define options for a select, use <Field><option ... /></Field>
|
||||
element: PropTypes.oneOf(["input", "select", "textarea"]),
|
||||
// The field's type (when used as an <input>). Defaults to "text".
|
||||
type: PropTypes.string,
|
||||
// id of a <datalist> element for suggestions
|
||||
list: PropTypes.string,
|
||||
// The field's label string.
|
||||
label: PropTypes.string,
|
||||
// The field's placeholder string. Defaults to the label.
|
||||
placeholder: PropTypes.string,
|
||||
// The field's value.
|
||||
// This is a controlled component, so the value is required.
|
||||
value: PropTypes.string.isRequired,
|
||||
// Optional component to include inside the field before the input.
|
||||
prefix: PropTypes.node,
|
||||
// Optional component to include inside the field after the input.
|
||||
postfix: PropTypes.node,
|
||||
// The callback called whenever the contents of the field
|
||||
// changes. Returns an object with `valid` boolean field
|
||||
// and a `feedback` react component field to provide feedback
|
||||
// to the user.
|
||||
onValidate: PropTypes.func,
|
||||
// If specified, overrides the value returned by onValidate.
|
||||
flagInvalid: PropTypes.bool,
|
||||
// If specified, contents will appear as a tooltip on the element and
|
||||
// validation feedback tooltips will be suppressed.
|
||||
tooltipContent: PropTypes.node,
|
||||
// If specified alongside tooltipContent, the class name to apply to the
|
||||
// tooltip itself.
|
||||
tooltipClassName: PropTypes.string,
|
||||
// If specified, an additional class name to apply to the field container
|
||||
className: PropTypes.string,
|
||||
// All other props pass through to the <input>.
|
||||
interface IProps {
|
||||
// The field's ID, which binds the input and label together. Immutable.
|
||||
id?: string;
|
||||
// The field's type (when used as an <input>). Defaults to "text".
|
||||
type?: string;
|
||||
// id of a <datalist> element for suggestions
|
||||
list?: string;
|
||||
// The field's label string.
|
||||
label?: string;
|
||||
// The field's placeholder string. Defaults to the label.
|
||||
placeholder?: string;
|
||||
// Optional component to include inside the field before the input.
|
||||
prefixComponent?: React.ReactNode;
|
||||
// Optional component to include inside the field after the input.
|
||||
postfixComponent?: React.ReactNode;
|
||||
// The callback called whenever the contents of the field
|
||||
// changes. Returns an object with `valid` boolean field
|
||||
// and a `feedback` react component field to provide feedback
|
||||
// to the user.
|
||||
onValidate?: (input: IFieldState) => Promise<IValidationResult>;
|
||||
// If specified, overrides the value returned by onValidate.
|
||||
forceValidity?: boolean;
|
||||
// If specified, contents will appear as a tooltip on the element and
|
||||
// validation feedback tooltips will be suppressed.
|
||||
tooltipContent?: React.ReactNode;
|
||||
// If specified the tooltip will be shown regardless of feedback
|
||||
forceTooltipVisible?: boolean;
|
||||
// If specified alongside tooltipContent, the class name to apply to the
|
||||
// tooltip itself.
|
||||
tooltipClassName?: string;
|
||||
// If specified, an additional class name to apply to the field container
|
||||
className?: string;
|
||||
// All other props pass through to the <input>.
|
||||
}
|
||||
|
||||
interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
|
||||
// The element to create. Defaults to "input".
|
||||
element?: "input";
|
||||
// The input's value. This is a controlled component, so the value is required.
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ISelectProps extends IProps, SelectHTMLAttributes<HTMLSelectElement> {
|
||||
// To define options for a select, use <Field><option ... /></Field>
|
||||
element: "select";
|
||||
// The select's value. This is a controlled component, so the value is required.
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ITextareaProps extends IProps, TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
element: "textarea";
|
||||
// The textarea's value. This is a controlled component, so the value is required.
|
||||
value: string;
|
||||
}
|
||||
|
||||
type PropShapes = IInputProps | ISelectProps | ITextareaProps;
|
||||
|
||||
interface IState {
|
||||
valid: boolean;
|
||||
feedback: React.ReactNode;
|
||||
feedbackVisible: boolean;
|
||||
focused: boolean;
|
||||
}
|
||||
|
||||
export default class Field extends React.PureComponent<PropShapes, IState> {
|
||||
private id: string;
|
||||
private input: HTMLInputElement;
|
||||
|
||||
public static readonly defaultProps = {
|
||||
element: "input",
|
||||
type: "text",
|
||||
};
|
||||
|
||||
/*
|
||||
* This was changed from throttle to debounce: this is more traditional for
|
||||
* form validation since it means that the validation doesn't happen at all
|
||||
* until the user stops typing for a bit (debounce defaults to not running on
|
||||
* the leading edge). If we're doing an HTTP hit on each validation, we have more
|
||||
* incentive to prevent validating input that's very unlikely to be valid.
|
||||
* We may find that we actually want different behaviour for registration
|
||||
* fields, in which case we can add some options to control it.
|
||||
*/
|
||||
private validateOnChange = debounce(() => {
|
||||
this.validate({
|
||||
focused: true,
|
||||
});
|
||||
}, VALIDATION_THROTTLE_MS);
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
valid: undefined,
|
||||
feedback: undefined,
|
||||
feedbackVisible: false,
|
||||
focused: false,
|
||||
};
|
||||
|
||||
this.id = this.props.id || getId();
|
||||
}
|
||||
|
||||
onFocus = (ev) => {
|
||||
public focus() {
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
private onFocus = (ev) => {
|
||||
this.setState({
|
||||
focused: true,
|
||||
});
|
||||
|
@ -93,7 +146,7 @@ export default class Field extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
onChange = (ev) => {
|
||||
private onChange = (ev) => {
|
||||
this.validateOnChange();
|
||||
// Parent component may have supplied its own `onChange` as well
|
||||
if (this.props.onChange) {
|
||||
|
@ -101,7 +154,7 @@ export default class Field extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
onBlur = (ev) => {
|
||||
private onBlur = (ev) => {
|
||||
this.setState({
|
||||
focused: false,
|
||||
});
|
||||
|
@ -114,11 +167,7 @@ export default class Field extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
focus() {
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
async validate({ focused, allowEmpty = true }) {
|
||||
private async validate({ focused, allowEmpty = true }: {focused: boolean, allowEmpty?: boolean}) {
|
||||
if (!this.props.onValidate) {
|
||||
return;
|
||||
}
|
||||
|
@ -149,59 +198,45 @@ export default class Field extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This was changed from throttle to debounce: this is more traditional for
|
||||
* form validation since it means that the validation doesn't happen at all
|
||||
* until the user stops typing for a bit (debounce defaults to not running on
|
||||
* the leading edge). If we're doing an HTTP hit on each validation, we have more
|
||||
* incentive to prevent validating input that's very unlikely to be valid.
|
||||
* We may find that we actually want different behaviour for registration
|
||||
* fields, in which case we can add some options to control it.
|
||||
*/
|
||||
validateOnChange = debounce(() => {
|
||||
this.validate({
|
||||
focused: true,
|
||||
});
|
||||
}, VALIDATION_THROTTLE_MS);
|
||||
|
||||
render() {
|
||||
|
||||
public render() {
|
||||
const {
|
||||
element, prefix, postfix, className, onValidate, children,
|
||||
tooltipContent, flagInvalid, tooltipClassName, list, ...inputProps} = this.props;
|
||||
|
||||
const inputElement = element || "input";
|
||||
element, prefixComponent, postfixComponent, className, onValidate, children,
|
||||
tooltipContent, forceValidity, tooltipClassName, list, ...inputProps} = this.props;
|
||||
|
||||
// Set some defaults for the <input> element
|
||||
inputProps.type = inputProps.type || "text";
|
||||
inputProps.ref = input => this.input = input;
|
||||
const ref = input => this.input = input;
|
||||
inputProps.placeholder = inputProps.placeholder || inputProps.label;
|
||||
inputProps.id = this.id; // this overwrites the id from props
|
||||
|
||||
inputProps.onFocus = this.onFocus;
|
||||
inputProps.onChange = this.onChange;
|
||||
inputProps.onBlur = this.onBlur;
|
||||
inputProps.list = list;
|
||||
|
||||
const fieldInput = React.createElement(inputElement, inputProps, children);
|
||||
// Appease typescript's inference
|
||||
const inputProps_ = {...inputProps, ref, list};
|
||||
|
||||
const fieldInput = React.createElement(this.props.element, inputProps_, children);
|
||||
|
||||
let prefixContainer = null;
|
||||
if (prefix) {
|
||||
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
|
||||
if (prefixComponent) {
|
||||
prefixContainer = <span className="mx_Field_prefix">{prefixComponent}</span>;
|
||||
}
|
||||
let postfixContainer = null;
|
||||
if (postfix) {
|
||||
postfixContainer = <span className="mx_Field_postfix">{postfix}</span>;
|
||||
if (postfixComponent) {
|
||||
postfixContainer = <span className="mx_Field_postfix">{postfixComponent}</span>;
|
||||
}
|
||||
|
||||
const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined;
|
||||
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, className, {
|
||||
const hasValidationFlag = forceValidity !== null && forceValidity !== undefined;
|
||||
const fieldClasses = classNames("mx_Field", `mx_Field_${this.props.element}`, className, {
|
||||
// If we have a prefix element, leave the label always at the top left and
|
||||
// don't animate it, as it looks a bit clunky and would add complexity to do
|
||||
// properly.
|
||||
mx_Field_labelAlwaysTopLeft: prefix,
|
||||
mx_Field_valid: onValidate && this.state.valid === true,
|
||||
mx_Field_labelAlwaysTopLeft: prefixComponent,
|
||||
mx_Field_valid: hasValidationFlag ? forceValidity : onValidate && this.state.valid === true,
|
||||
mx_Field_invalid: hasValidationFlag
|
||||
? flagInvalid
|
||||
? !forceValidity
|
||||
: onValidate && this.state.valid === false,
|
||||
});
|
||||
|
||||
|
@ -209,11 +244,11 @@ export default class Field extends React.PureComponent {
|
|||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
let fieldTooltip;
|
||||
if (tooltipContent || this.state.feedback) {
|
||||
const addlClassName = tooltipClassName ? tooltipClassName : '';
|
||||
fieldTooltip = <Tooltip
|
||||
tooltipClassName={`mx_Field_tooltip ${addlClassName}`}
|
||||
visible={this.state.feedbackVisible}
|
||||
tooltipClassName={classNames("mx_Field_tooltip", tooltipClassName)}
|
||||
visible={(this.state.focused && this.props.forceTooltipVisible) || this.state.feedbackVisible}
|
||||
label={tooltipContent || this.state.feedback}
|
||||
forceOnRight
|
||||
/>;
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
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 * as sdk from '../../../index';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const GroupsButton = function(props) {
|
||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
return (
|
||||
<ActionButton className="mx_GroupsButton" action="toggle_my_groups"
|
||||
label={_t("Communities")}
|
||||
size={props.size}
|
||||
tooltip={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
GroupsButton.propTypes = {
|
||||
size: PropTypes.string,
|
||||
};
|
||||
|
||||
export default GroupsButton;
|
92
src/components/views/elements/IRCTimelineProfileResizer.tsx
Normal file
92
src/components/views/elements/IRCTimelineProfileResizer.tsx
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
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 React from 'react';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import Draggable, {ILocationState} from './Draggable';
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
|
||||
interface IProps {
|
||||
// Current room
|
||||
roomId: string;
|
||||
minWidth: number;
|
||||
maxWidth: number;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
width: number;
|
||||
IRCLayoutRoot: HTMLElement;
|
||||
}
|
||||
|
||||
export default class IRCTimelineProfileResizer extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
width: SettingsStore.getValue("ircDisplayNameWidth", this.props.roomId),
|
||||
IRCLayoutRoot: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
IRCLayoutRoot: document.querySelector(".mx_IRCLayout") as HTMLElement,
|
||||
}, () => this.updateCSSWidth(this.state.width));
|
||||
}
|
||||
|
||||
private dragFunc = (location: ILocationState, event: React.MouseEvent<Element, MouseEvent>): ILocationState => {
|
||||
const offset = event.clientX - location.currentX;
|
||||
const newWidth = this.state.width + offset;
|
||||
|
||||
// If we're trying to go smaller than min width, don't.
|
||||
if (newWidth < this.props.minWidth) {
|
||||
return location;
|
||||
}
|
||||
|
||||
if (newWidth > this.props.maxWidth) {
|
||||
return location;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
width: newWidth,
|
||||
});
|
||||
|
||||
this.updateCSSWidth.bind(this)(newWidth);
|
||||
|
||||
return {
|
||||
currentX: event.clientX,
|
||||
currentY: location.currentY,
|
||||
};
|
||||
};
|
||||
|
||||
private updateCSSWidth(newWidth: number) {
|
||||
this.state.IRCLayoutRoot.style.setProperty("--name-width", newWidth + "px");
|
||||
}
|
||||
|
||||
private onMoueUp(event: MouseEvent) {
|
||||
if (this.props.roomId) {
|
||||
SettingsStore.setValue("ircDisplayNameWidth", this.props.roomId, SettingLevel.ROOM_DEVICE, this.state.width);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Draggable
|
||||
className="mx_ProfileResizer"
|
||||
dragFunc={this.dragFunc.bind(this)}
|
||||
onMouseUp={this.onMoueUp.bind(this)}
|
||||
/>;
|
||||
}
|
||||
}
|
|
@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
|
@ -27,6 +25,7 @@ import AccessibleButton from "./AccessibleButton";
|
|||
import Modal from "../../../Modal";
|
||||
import * as sdk from "../../../index";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import FocusLock from "react-focus-lock";
|
||||
|
||||
export default class ImageView extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -50,16 +49,6 @@ export default class ImageView extends React.Component {
|
|||
this.state = { rotationDegrees: 0 };
|
||||
}
|
||||
|
||||
// XXX: keyboard shortcuts for managing dialogs should be done by the modal
|
||||
// dialog base class somehow, surely...
|
||||
componentDidMount() {
|
||||
document.addEventListener("keydown", this.onKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("keydown", this.onKeyDown);
|
||||
}
|
||||
|
||||
onKeyDown = (ev) => {
|
||||
if (ev.key === Key.ESCAPE) {
|
||||
ev.stopPropagation();
|
||||
|
@ -73,6 +62,7 @@ export default class ImageView extends React.Component {
|
|||
Modal.createTrackedDialog('Confirm Redact Dialog', 'Image View', ConfirmRedactDialog, {
|
||||
onFinished: (proceed) => {
|
||||
if (!proceed) return;
|
||||
this.props.onFinished();
|
||||
MatrixClientPeg.get().redactEvent(
|
||||
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId(),
|
||||
).catch(function(e) {
|
||||
|
@ -194,7 +184,14 @@ export default class ImageView extends React.Component {
|
|||
const effectiveStyle = {transform: `rotate(${rotationDegrees}deg)`, ...style};
|
||||
|
||||
return (
|
||||
<div className="mx_ImageView">
|
||||
<FocusLock
|
||||
returnFocus={true}
|
||||
lockProps={{
|
||||
onKeyDown: this.onKeyDown,
|
||||
role: "dialog",
|
||||
}}
|
||||
className="mx_ImageView"
|
||||
>
|
||||
<div className="mx_ImageView_lhs">
|
||||
</div>
|
||||
<div className="mx_ImageView_content">
|
||||
|
@ -230,7 +227,7 @@ export default class ImageView extends React.Component {
|
|||
</div>
|
||||
<div className="mx_ImageView_rhs">
|
||||
</div>
|
||||
</div>
|
||||
</FocusLock>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
import createReactClass from 'create-react-class';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'InlineSpinner',
|
||||
|
@ -25,9 +27,22 @@ export default createReactClass({
|
|||
const h = this.props.h || 16;
|
||||
const imgClass = this.props.imgClassName || "";
|
||||
|
||||
let imageSource;
|
||||
if (SettingsStore.getValue('feature_new_spinner')) {
|
||||
imageSource = require("../../../../res/img/spinner.svg");
|
||||
} else {
|
||||
imageSource = require("../../../../res/img/spinner.gif");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_InlineSpinner">
|
||||
<img src={require("../../../../res/img/spinner.gif")} width={w} height={h} className={imgClass} />
|
||||
<img
|
||||
src={imageSource}
|
||||
width={w}
|
||||
height={h}
|
||||
className={imgClass}
|
||||
aria-label={_t("Loading...")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,336 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 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 ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const InteractiveTooltipContainerId = "mx_InteractiveTooltip_Container";
|
||||
|
||||
// If the distance from tooltip to window edge is below this value, the tooltip
|
||||
// will flip around to the other side of the target.
|
||||
const MIN_SAFE_DISTANCE_TO_WINDOW_EDGE = 20;
|
||||
|
||||
function getOrCreateContainer() {
|
||||
let container = document.getElementById(InteractiveTooltipContainerId);
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = InteractiveTooltipContainerId;
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
function isInRect(x, y, rect) {
|
||||
const { top, right, bottom, left } = rect;
|
||||
return x >= left && x <= right && y >= top && y <= bottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the positive slope of the diagonal of the rect.
|
||||
*
|
||||
* @param {DOMRect} rect
|
||||
* @return {integer}
|
||||
*/
|
||||
function getDiagonalSlope(rect) {
|
||||
const { top, right, bottom, left } = rect;
|
||||
return (bottom - top) / (right - left);
|
||||
}
|
||||
|
||||
function isInUpperLeftHalf(x, y, rect) {
|
||||
const { bottom, left } = rect;
|
||||
// Negative slope because Y values grow downwards and for this case, the
|
||||
// diagonal goes from larger to smaller Y values.
|
||||
const diagonalSlope = getDiagonalSlope(rect) * -1;
|
||||
return isInRect(x, y, rect) && (y <= bottom + diagonalSlope * (x - left));
|
||||
}
|
||||
|
||||
function isInLowerRightHalf(x, y, rect) {
|
||||
const { bottom, left } = rect;
|
||||
// Negative slope because Y values grow downwards and for this case, the
|
||||
// diagonal goes from larger to smaller Y values.
|
||||
const diagonalSlope = getDiagonalSlope(rect) * -1;
|
||||
return isInRect(x, y, rect) && (y >= bottom + diagonalSlope * (x - left));
|
||||
}
|
||||
|
||||
function isInUpperRightHalf(x, y, rect) {
|
||||
const { top, left } = rect;
|
||||
// Positive slope because Y values grow downwards and for this case, the
|
||||
// diagonal goes from smaller to larger Y values.
|
||||
const diagonalSlope = getDiagonalSlope(rect) * 1;
|
||||
return isInRect(x, y, rect) && (y <= top + diagonalSlope * (x - left));
|
||||
}
|
||||
|
||||
function isInLowerLeftHalf(x, y, rect) {
|
||||
const { top, left } = rect;
|
||||
// Positive slope because Y values grow downwards and for this case, the
|
||||
// diagonal goes from smaller to larger Y values.
|
||||
const diagonalSlope = getDiagonalSlope(rect) * 1;
|
||||
return isInRect(x, y, rect) && (y >= top + diagonalSlope * (x - left));
|
||||
}
|
||||
|
||||
/*
|
||||
* This style of tooltip takes a "target" element as its child and centers the
|
||||
* tooltip along one edge of the target.
|
||||
*/
|
||||
export default class InteractiveTooltip extends React.Component {
|
||||
static propTypes = {
|
||||
// Content to show in the tooltip
|
||||
content: PropTypes.node.isRequired,
|
||||
// Function to call when visibility of the tooltip changes
|
||||
onVisibilityChange: PropTypes.func,
|
||||
// flag to forcefully hide this tooltip
|
||||
forceHidden: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
contentRect: null,
|
||||
visible: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// Whenever this passthrough component updates, also render the tooltip
|
||||
// in a separate DOM tree. This allows the tooltip content to participate
|
||||
// the normal React rendering cycle: when this component re-renders, the
|
||||
// tooltip content re-renders.
|
||||
// Once we upgrade to React 16, this could be done a bit more naturally
|
||||
// using the portals feature instead.
|
||||
this.renderTooltip();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("mousemove", this.onMouseMove);
|
||||
}
|
||||
|
||||
collectContentRect = (element) => {
|
||||
// We don't need to clean up when unmounting, so ignore
|
||||
if (!element) return;
|
||||
|
||||
this.setState({
|
||||
contentRect: element.getBoundingClientRect(),
|
||||
});
|
||||
}
|
||||
|
||||
collectTarget = (element) => {
|
||||
this.target = element;
|
||||
}
|
||||
|
||||
canTooltipFitAboveTarget() {
|
||||
const { contentRect } = this.state;
|
||||
const targetRect = this.target.getBoundingClientRect();
|
||||
const targetTop = targetRect.top + window.pageYOffset;
|
||||
return (
|
||||
!contentRect ||
|
||||
(targetTop - contentRect.height > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE)
|
||||
);
|
||||
}
|
||||
|
||||
onMouseMove = (ev) => {
|
||||
const { clientX: x, clientY: y } = ev;
|
||||
const { contentRect } = this.state;
|
||||
const targetRect = this.target.getBoundingClientRect();
|
||||
|
||||
// When moving the mouse from the target to the tooltip, we create a
|
||||
// safe area that includes the tooltip, the target, and the trapezoid
|
||||
// ABCD between them:
|
||||
// ┌───────────┐
|
||||
// │ │
|
||||
// │ │
|
||||
// A └───E───F───┘ B
|
||||
// V
|
||||
// ┌─┐
|
||||
// │ │
|
||||
// C└─┘D
|
||||
//
|
||||
// As long as the mouse remains inside the safe area, the tooltip will
|
||||
// stay open.
|
||||
const buffer = 50;
|
||||
if (isInRect(x, y, targetRect)) {
|
||||
return;
|
||||
}
|
||||
if (this.canTooltipFitAboveTarget()) {
|
||||
const contentRectWithBuffer = {
|
||||
top: contentRect.top - buffer,
|
||||
right: contentRect.right + buffer,
|
||||
bottom: contentRect.bottom,
|
||||
left: contentRect.left - buffer,
|
||||
};
|
||||
const trapezoidLeft = {
|
||||
top: contentRect.bottom,
|
||||
right: targetRect.left,
|
||||
bottom: targetRect.bottom,
|
||||
left: contentRect.left - buffer,
|
||||
};
|
||||
const trapezoidCenter = {
|
||||
top: contentRect.bottom,
|
||||
right: targetRect.right,
|
||||
bottom: targetRect.bottom,
|
||||
left: targetRect.left,
|
||||
};
|
||||
const trapezoidRight = {
|
||||
top: contentRect.bottom,
|
||||
right: contentRect.right + buffer,
|
||||
bottom: targetRect.bottom,
|
||||
left: targetRect.right,
|
||||
};
|
||||
|
||||
if (
|
||||
isInRect(x, y, contentRectWithBuffer) ||
|
||||
isInUpperRightHalf(x, y, trapezoidLeft) ||
|
||||
isInRect(x, y, trapezoidCenter) ||
|
||||
isInUpperLeftHalf(x, y, trapezoidRight)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const contentRectWithBuffer = {
|
||||
top: contentRect.top,
|
||||
right: contentRect.right + buffer,
|
||||
bottom: contentRect.bottom + buffer,
|
||||
left: contentRect.left - buffer,
|
||||
};
|
||||
const trapezoidLeft = {
|
||||
top: targetRect.top,
|
||||
right: targetRect.left,
|
||||
bottom: contentRect.top,
|
||||
left: contentRect.left - buffer,
|
||||
};
|
||||
const trapezoidCenter = {
|
||||
top: targetRect.top,
|
||||
right: targetRect.right,
|
||||
bottom: contentRect.top,
|
||||
left: targetRect.left,
|
||||
};
|
||||
const trapezoidRight = {
|
||||
top: targetRect.top,
|
||||
right: contentRect.right + buffer,
|
||||
bottom: contentRect.top,
|
||||
left: targetRect.right,
|
||||
};
|
||||
|
||||
if (
|
||||
isInRect(x, y, contentRectWithBuffer) ||
|
||||
isInLowerRightHalf(x, y, trapezoidLeft) ||
|
||||
isInRect(x, y, trapezoidCenter) ||
|
||||
isInLowerLeftHalf(x, y, trapezoidRight)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.hideTooltip();
|
||||
}
|
||||
|
||||
onTargetMouseOver = (ev) => {
|
||||
this.showTooltip();
|
||||
}
|
||||
|
||||
showTooltip() {
|
||||
// Don't enter visible state if we haven't collected the target yet
|
||||
if (!this.target) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
visible: true,
|
||||
});
|
||||
if (this.props.onVisibilityChange) {
|
||||
this.props.onVisibilityChange(true);
|
||||
}
|
||||
document.addEventListener("mousemove", this.onMouseMove);
|
||||
}
|
||||
|
||||
hideTooltip() {
|
||||
this.setState({
|
||||
visible: false,
|
||||
});
|
||||
if (this.props.onVisibilityChange) {
|
||||
this.props.onVisibilityChange(false);
|
||||
}
|
||||
document.removeEventListener("mousemove", this.onMouseMove);
|
||||
}
|
||||
|
||||
renderTooltip() {
|
||||
const { contentRect, visible } = this.state;
|
||||
if (this.props.forceHidden === true || !visible) {
|
||||
ReactDOM.render(null, getOrCreateContainer());
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetRect = this.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const targetLeft = targetRect.left + window.pageXOffset;
|
||||
const targetBottom = targetRect.bottom + window.pageYOffset;
|
||||
const targetTop = targetRect.top + window.pageYOffset;
|
||||
|
||||
// Place the tooltip above the target by default. If we find that the
|
||||
// tooltip content would extend past the safe area towards the window
|
||||
// edge, flip around to below the target.
|
||||
const position = {};
|
||||
let chevronFace = null;
|
||||
if (this.canTooltipFitAboveTarget()) {
|
||||
position.bottom = window.innerHeight - targetTop;
|
||||
chevronFace = "bottom";
|
||||
} else {
|
||||
position.top = targetBottom;
|
||||
chevronFace = "top";
|
||||
}
|
||||
|
||||
// Center the tooltip horizontally with the target's center.
|
||||
position.left = targetLeft + targetRect.width / 2;
|
||||
|
||||
const chevron = <div className={"mx_InteractiveTooltip_chevron_" + chevronFace} />;
|
||||
|
||||
const menuClasses = classNames({
|
||||
'mx_InteractiveTooltip': true,
|
||||
'mx_InteractiveTooltip_withChevron_top': chevronFace === 'top',
|
||||
'mx_InteractiveTooltip_withChevron_bottom': chevronFace === 'bottom',
|
||||
});
|
||||
|
||||
const menuStyle = {};
|
||||
if (contentRect) {
|
||||
menuStyle.left = `-${contentRect.width / 2}px`;
|
||||
}
|
||||
|
||||
const tooltip = <div className="mx_InteractiveTooltip_wrapper" style={{...position}}>
|
||||
<div className={menuClasses}
|
||||
style={menuStyle}
|
||||
ref={this.collectContentRect}
|
||||
>
|
||||
{chevron}
|
||||
{this.props.content}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
ReactDOM.render(tooltip, getOrCreateContainer());
|
||||
}
|
||||
|
||||
render() {
|
||||
// We use `cloneElement` here to append some props to the child content
|
||||
// without using a wrapper element which could disrupt layout.
|
||||
return React.cloneElement(this.props.children, {
|
||||
ref: this.collectTarget,
|
||||
onMouseOver: this.onTargetMouseOver,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -17,10 +17,10 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
|
||||
export default class ManageIntegsButton extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -34,7 +34,7 @@ export default class ManageIntegsButton extends React.Component {
|
|||
if (!managers.hasManager()) {
|
||||
managers.openNoManagerDialog();
|
||||
} else {
|
||||
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
|
||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||
managers.openAll(this.props.room);
|
||||
} else {
|
||||
managers.getPrimaryManager().open(this.props.room);
|
||||
|
@ -45,9 +45,8 @@ export default class ManageIntegsButton extends React.Component {
|
|||
render() {
|
||||
let integrationsButton = <div />;
|
||||
if (IntegrationManagers.sharedInstance().hasManager()) {
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
integrationsButton = (
|
||||
<AccessibleButton
|
||||
<AccessibleTooltipButton
|
||||
className='mx_RoomHeader_button mx_RoomHeader_manageIntegsButton'
|
||||
title={_t("Manage Integrations")}
|
||||
onClick={this.onManageIntegrations}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
@ -23,6 +23,7 @@ import { _t } from '../../../languageHandler';
|
|||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
import * as sdk from "../../../index";
|
||||
import {MatrixEvent} from "matrix-js-sdk";
|
||||
import {isValid3pidInvite} from "../../../RoomInvite";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MemberEventListSummary',
|
||||
|
@ -284,6 +285,9 @@ export default createReactClass({
|
|||
_getTransition: function(e) {
|
||||
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
|
||||
// Handle 3pid invites the same as invites so they get bundled together
|
||||
if (!isValid3pidInvite(e.mxEvent)) {
|
||||
return 'invite_withdrawal';
|
||||
}
|
||||
return 'invited';
|
||||
}
|
||||
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
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 createReactClass from 'create-react-class';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MessageSpinner',
|
||||
|
||||
render: function() {
|
||||
const w = this.props.w || 32;
|
||||
const h = this.props.h || 32;
|
||||
const imgClass = this.props.imgClassName || "";
|
||||
const msg = this.props.msg || "Loading...";
|
||||
return (
|
||||
<div className="mx_Spinner">
|
||||
<div className="mx_Spinner_Msg">{ msg }</div>
|
||||
<img src={require("../../../../res/img/spinner.gif")} width={w} height={h} className={imgClass} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -20,7 +20,7 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import classNames from 'classnames';
|
||||
import { Room, RoomMember } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -26,6 +26,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
|||
import FlairStore from "../../../stores/FlairStore";
|
||||
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
||||
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
||||
|
@ -191,7 +192,7 @@ const Pill = createReactClass({
|
|||
|
||||
onUserPillClicked: function() {
|
||||
dis.dispatch({
|
||||
action: 'view_user',
|
||||
action: Action.ViewUser,
|
||||
member: this.state.member,
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 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 PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'ProgressBar',
|
||||
propTypes: {
|
||||
value: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// Would use an HTML5 progress tag but if that doesn't animate if you
|
||||
// use the HTML attributes rather than styles
|
||||
const progressStyle = {
|
||||
width: ((this.props.value / this.props.max) * 100)+"%",
|
||||
};
|
||||
return (
|
||||
<div className="mx_ProgressBar"><div className="mx_ProgressBar_fill" style={progressStyle}></div></div>
|
||||
);
|
||||
},
|
||||
});
|
28
src/components/views/elements/ProgressBar.tsx
Normal file
28
src/components/views/elements/ProgressBar.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
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 React from "react";
|
||||
|
||||
interface IProps {
|
||||
value: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
const ProgressBar: React.FC<IProps> = ({value, max}) => {
|
||||
return <progress className="mx_ProgressBar" max={max} value={value} />;
|
||||
};
|
||||
|
||||
export default ProgressBar;
|
51
src/components/views/elements/QRCode.tsx
Normal file
51
src/components/views/elements/QRCode.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
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 * as React from "react";
|
||||
import {toDataURL, QRCodeSegment, QRCodeToDataURLOptions} from "qrcode";
|
||||
import classNames from "classnames";
|
||||
|
||||
import {_t} from "../../../languageHandler";
|
||||
import Spinner from "./Spinner";
|
||||
|
||||
interface IProps extends QRCodeToDataURLOptions {
|
||||
data: string | QRCodeSegment[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultOptions: QRCodeToDataURLOptions = {
|
||||
errorCorrectionLevel: 'L', // we want it as trivial-looking as possible
|
||||
};
|
||||
|
||||
const QRCode: React.FC<IProps> = ({data, className, ...options}) => {
|
||||
const [dataUri, setUri] = React.useState<string>(null);
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
toDataURL(data, {...defaultOptions, ...options}).then(uri => {
|
||||
if (cancelled) return;
|
||||
setUri(uri);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [JSON.stringify(data), options]);
|
||||
|
||||
return <div className={classNames("mx_QRCode", className)}>
|
||||
{ dataUri ? <img src={dataUri} className="mx_VerificationQRCode" alt={_t("QR Code")} /> : <Spinner /> }
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default QRCode;
|
|
@ -19,13 +19,15 @@ import React from 'react';
|
|||
import * as sdk from '../../../index';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import PropTypes from 'prop-types';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import {wantsDateSeparator} from '../../../DateUtils';
|
||||
import {MatrixEvent} from 'matrix-js-sdk';
|
||||
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import escapeHtml from "escape-html";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
|
||||
// This component does no cycle detection, simply because the only way to make such a cycle would be to
|
||||
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
|
||||
|
@ -37,6 +39,8 @@ export default class ReplyThread extends React.Component {
|
|||
// called when the ReplyThread contents has changed, including EventTiles thereof
|
||||
onHeightChanged: PropTypes.func.isRequired,
|
||||
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
||||
// Specifies which layout to use.
|
||||
useIRCLayout: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
@ -89,7 +93,21 @@ export default class ReplyThread extends React.Component {
|
|||
|
||||
// Part of Replies fallback support
|
||||
static stripHTMLReply(html) {
|
||||
return html.replace(/^<mx-reply>[\s\S]+?<\/mx-reply>/, '');
|
||||
// Sanitize the original HTML for inclusion in <mx-reply>. We allow
|
||||
// any HTML, since the original sender could use special tags that we
|
||||
// don't recognize, but want to pass along to any recipients who do
|
||||
// recognize them -- recipients should be sanitizing before displaying
|
||||
// anyways. However, we sanitize to 1) remove any mx-reply, so that we
|
||||
// don't generate a nested mx-reply, and 2) make sure that the HTML is
|
||||
// properly formatted (e.g. tags are closed where necessary)
|
||||
return sanitizeHtml(
|
||||
html,
|
||||
{
|
||||
allowedTags: false, // false means allow everything
|
||||
allowedAttributes: false,
|
||||
exclusiveFilter: (frame) => frame.tag === "mx-reply",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Part of Replies fallback support
|
||||
|
@ -99,15 +117,19 @@ export default class ReplyThread extends React.Component {
|
|||
let {body, formatted_body: html} = ev.getContent();
|
||||
if (this.getParentEventId(ev)) {
|
||||
if (body) body = this.stripPlainReply(body);
|
||||
if (html) html = this.stripHTMLReply(html);
|
||||
}
|
||||
|
||||
if (!body) body = ""; // Always ensure we have a body, for reasons.
|
||||
|
||||
// Escape the body to use as HTML below.
|
||||
// We also run a nl2br over the result to fix the fallback representation. We do this
|
||||
// after converting the text to safe HTML to avoid user-provided BR's from being converted.
|
||||
if (!html) html = escapeHtml(body).replace(/\n/g, '<br/>');
|
||||
if (html) {
|
||||
// sanitize the HTML before we put it in an <mx-reply>
|
||||
html = this.stripHTMLReply(html);
|
||||
} else {
|
||||
// Escape the body to use as HTML below.
|
||||
// We also run a nl2br over the result to fix the fallback representation. We do this
|
||||
// after converting the text to safe HTML to avoid user-provided BR's from being converted.
|
||||
html = escapeHtml(body).replace(/\n/g, '<br/>');
|
||||
}
|
||||
|
||||
// dev note: do not rely on `body` being safe for HTML usage below.
|
||||
|
||||
|
@ -176,12 +198,17 @@ export default class ReplyThread extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref) {
|
||||
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, useIRCLayout) {
|
||||
if (!ReplyThread.getParentEventId(parentEv)) {
|
||||
return <div />;
|
||||
return <div className="mx_ReplyThread_wrapper_empty" />;
|
||||
}
|
||||
return <ReplyThread parentEv={parentEv} onHeightChanged={onHeightChanged}
|
||||
ref={ref} permalinkCreator={permalinkCreator} />;
|
||||
return <ReplyThread
|
||||
parentEv={parentEv}
|
||||
onHeightChanged={onHeightChanged}
|
||||
ref={ref}
|
||||
permalinkCreator={permalinkCreator}
|
||||
useIRCLayout={useIRCLayout}
|
||||
/>;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -283,7 +310,7 @@ export default class ReplyThread extends React.Component {
|
|||
events,
|
||||
}, this.loadNextEvent);
|
||||
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
dis.fire(Action.FocusComposer);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -331,11 +358,13 @@ export default class ReplyThread extends React.Component {
|
|||
onHeightChanged={this.props.onHeightChanged}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
isRedacted={ev.isRedacted()}
|
||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
|
||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||
useIRCLayout={this.props.useIRCLayout}
|
||||
/>
|
||||
</blockquote>;
|
||||
});
|
||||
|
||||
return <div>
|
||||
return <div className="mx_ReplyThread_wrapper">
|
||||
<div>{ header }</div>
|
||||
<div>{ evTiles }</div>
|
||||
</div>;
|
||||
|
|
|
@ -45,10 +45,10 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
|
||||
return (
|
||||
<Field
|
||||
label={_t("Room alias")}
|
||||
label={_t("Room address")}
|
||||
className="mx_RoomAliasField"
|
||||
prefix={poundSign}
|
||||
postfix={domain}
|
||||
prefixComponent={poundSign}
|
||||
postfixComponent={domain}
|
||||
ref={ref => this._fieldRef = ref}
|
||||
onValidate={this._onValidate}
|
||||
placeholder={_t("e.g. my-room")}
|
||||
|
@ -87,7 +87,7 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
}, {
|
||||
key: "required",
|
||||
test: async ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Please provide a room alias"),
|
||||
invalid: () => _t("Please provide a room address"),
|
||||
}, {
|
||||
key: "taken",
|
||||
final: true,
|
||||
|
@ -107,8 +107,8 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
return !!err.errcode;
|
||||
}
|
||||
},
|
||||
valid: () => _t("This alias is available to use"),
|
||||
invalid: () => _t("This alias is already in use"),
|
||||
valid: () => _t("This address is available to use"),
|
||||
invalid: () => _t("This address is already in use"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -18,11 +18,12 @@ import React from 'react';
|
|||
import * as sdk from '../../../index';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
||||
const RoomDirectoryButton = function(props) {
|
||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
return (
|
||||
<ActionButton action="view_room_directory"
|
||||
<ActionButton action={Action.ViewRoomDirectory}
|
||||
mouseOverAction={props.callout ? "callout_room_directory" : null}
|
||||
label={_t("Room directory")}
|
||||
iconPath={require("../../../../res/img/icons-directory.svg")}
|
||||
|
|
|
@ -21,9 +21,9 @@ import PlatformPeg from "../../../PlatformPeg";
|
|||
import AccessibleButton from "./AccessibleButton";
|
||||
import {_t} from "../../../languageHandler";
|
||||
|
||||
const SSOButton = ({matrixClient, loginType, ...props}) => {
|
||||
const SSOButton = ({matrixClient, loginType, fragmentAfterLogin, ...props}) => {
|
||||
const onClick = () => {
|
||||
PlatformPeg.get().startSingleSignOn(matrixClient, loginType);
|
||||
PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -36,6 +36,7 @@ const SSOButton = ({matrixClient, loginType, ...props}) => {
|
|||
SSOButton.propTypes = {
|
||||
matrixClient: PropTypes.object.isRequired, // does not use context as may use a temporary client
|
||||
loginType: PropTypes.oneOf(["sso", "cas"]), // defaults to "sso" in base-apis
|
||||
fragmentAfterLogin: PropTypes.string,
|
||||
};
|
||||
|
||||
export default SSOButton;
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
Copyright 2019 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 PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import ToggleSwitch from "./ToggleSwitch";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'SettingsFlag',
|
||||
propTypes: {
|
||||
name: PropTypes.string.isRequired,
|
||||
level: PropTypes.string.isRequired,
|
||||
roomId: PropTypes.string, // for per-room settings
|
||||
label: PropTypes.string, // untranslated
|
||||
onChange: PropTypes.func,
|
||||
isExplicit: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
value: SettingsStore.getValueAt(
|
||||
this.props.level,
|
||||
this.props.name,
|
||||
this.props.roomId,
|
||||
this.props.isExplicit,
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
onChange: function(checked) {
|
||||
if (this.props.group && !checked) return;
|
||||
|
||||
this.save(checked);
|
||||
this.setState({ value: checked });
|
||||
if (this.props.onChange) this.props.onChange(checked);
|
||||
},
|
||||
|
||||
save: function(val = undefined) {
|
||||
return SettingsStore.setValue(
|
||||
this.props.name,
|
||||
this.props.roomId,
|
||||
this.props.level,
|
||||
val !== undefined ? val : this.state.value,
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);
|
||||
|
||||
let label = this.props.label;
|
||||
if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level);
|
||||
else label = _t(label);
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsFlag">
|
||||
<span className="mx_SettingsFlag_label">{label}</span>
|
||||
<ToggleSwitch checked={this.state.value} onChange={this.onChange} disabled={!canChange} aria-label={label} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
104
src/components/views/elements/SettingsFlag.tsx
Normal file
104
src/components/views/elements/SettingsFlag.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
Copyright 2019 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 SettingsStore from "../../../settings/SettingsStore";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import ToggleSwitch from "./ToggleSwitch";
|
||||
import StyledCheckbox from "./StyledCheckbox";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
|
||||
interface IProps {
|
||||
// The setting must be a boolean
|
||||
name: string;
|
||||
level: SettingLevel;
|
||||
roomId?: string; // for per-room settings
|
||||
label?: string; // untranslated
|
||||
isExplicit?: boolean;
|
||||
// XXX: once design replaces all toggles make this the default
|
||||
useCheckbox?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange?(checked: boolean): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
value: boolean;
|
||||
}
|
||||
|
||||
export default class SettingsFlag extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
value: SettingsStore.getValueAt(
|
||||
this.props.level,
|
||||
this.props.name,
|
||||
this.props.roomId,
|
||||
this.props.isExplicit,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private onChange = async (checked: boolean) => {
|
||||
await this.save(checked);
|
||||
this.setState({ value: checked });
|
||||
if (this.props.onChange) this.props.onChange(checked);
|
||||
};
|
||||
|
||||
private checkBoxOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.onChange(e.target.checked);
|
||||
};
|
||||
|
||||
private save = async (val?: boolean) => {
|
||||
await SettingsStore.setValue(
|
||||
this.props.name,
|
||||
this.props.roomId,
|
||||
this.props.level,
|
||||
val !== undefined ? val : this.state.value,
|
||||
);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);
|
||||
|
||||
let label = this.props.label;
|
||||
if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level);
|
||||
else label = _t(label);
|
||||
|
||||
if (this.props.useCheckbox) {
|
||||
return <StyledCheckbox
|
||||
checked={this.state.value}
|
||||
onChange={this.checkBoxOnChange}
|
||||
disabled={this.props.disabled || !canChange}
|
||||
>
|
||||
{label}
|
||||
</StyledCheckbox>;
|
||||
} else {
|
||||
return (
|
||||
<div className="mx_SettingsFlag">
|
||||
<span className="mx_SettingsFlag_label">{label}</span>
|
||||
<ToggleSwitch
|
||||
checked={this.state.value}
|
||||
onChange={this.onChange}
|
||||
disabled={this.props.disabled || !canChange}
|
||||
aria-label={label}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
146
src/components/views/elements/Slider.tsx
Normal file
146
src/components/views/elements/Slider.tsx
Normal file
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
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 * as React from 'react';
|
||||
|
||||
interface IProps {
|
||||
// A callback for the selected value
|
||||
onSelectionChange: (value: number) => void;
|
||||
|
||||
// The current value of the slider
|
||||
value: number;
|
||||
|
||||
// The range and values of the slider
|
||||
// Currently only supports an ascending, constant interval range
|
||||
values: number[];
|
||||
|
||||
// A function for formatting the the values
|
||||
displayFunc: (value: number) => string;
|
||||
|
||||
// Whether the slider is disabled
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export default class Slider extends React.Component<IProps> {
|
||||
// offset is a terrible inverse approximation.
|
||||
// if the values represents some function f(x) = y where x is the
|
||||
// index of the array and y = values[x] then offset(f, y) = x
|
||||
// s.t f(x) = y.
|
||||
// it assumes a monotonic function and interpolates linearly between
|
||||
// y values.
|
||||
// Offset is used for finding the location of a value on a
|
||||
// non linear slider.
|
||||
private offset(values: number[], value: number): number {
|
||||
// the index of the first number greater than value.
|
||||
let closest = values.reduce((prev, curr) => {
|
||||
return (value > curr ? prev + 1 : prev);
|
||||
}, 0);
|
||||
|
||||
// Off the left
|
||||
if (closest === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Off the right
|
||||
if (closest === values.length) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
// Now
|
||||
const closestLessValue = values[closest - 1];
|
||||
const closestGreaterValue = values[closest];
|
||||
|
||||
const intervalWidth = 1 / (values.length - 1);
|
||||
|
||||
const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue);
|
||||
|
||||
return 100 * (closest - 1 + linearInterpolation) * intervalWidth;
|
||||
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
const dots = this.props.values.map(v =>
|
||||
<Dot active={v <= this.props.value}
|
||||
label={this.props.displayFunc(v)}
|
||||
onClick={this.props.disabled ? () => {} : () => this.props.onSelectionChange(v)}
|
||||
key={v}
|
||||
disabled={this.props.disabled}
|
||||
/>);
|
||||
|
||||
let selection = null;
|
||||
|
||||
if (!this.props.disabled) {
|
||||
const offset = this.offset(this.props.values, this.props.value);
|
||||
selection = <div className="mx_Slider_selection">
|
||||
<div className="mx_Slider_selectionDot" style={{left: "calc(-0.55em + " + offset + "%)"}} />
|
||||
<hr style={{width: offset + "%"}} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div className="mx_Slider">
|
||||
<div>
|
||||
<div className="mx_Slider_bar">
|
||||
<hr onClick={this.props.disabled ? () => {} : this.onClick.bind(this)}/>
|
||||
{ selection }
|
||||
</div>
|
||||
<div className="mx_Slider_dotContainer">
|
||||
{dots}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
onClick(event: React.MouseEvent) {
|
||||
const width = (event.target as HTMLElement).clientWidth;
|
||||
// nativeEvent is safe to use because https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/offsetX
|
||||
// is supported by all modern browsers
|
||||
const relativeClick = (event.nativeEvent.offsetX / width);
|
||||
const nearestValue = this.props.values[Math.round(relativeClick * (this.props.values.length - 1))];
|
||||
this.props.onSelectionChange(nearestValue);
|
||||
}
|
||||
}
|
||||
|
||||
interface IDotProps {
|
||||
// Callback for behavior onclick
|
||||
onClick: () => void;
|
||||
|
||||
// Whether the dot should appear active
|
||||
active: boolean;
|
||||
|
||||
// The label on the dot
|
||||
label: string;
|
||||
|
||||
// Whether the slider is disabled
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
class Dot extends React.PureComponent<IDotProps> {
|
||||
render(): React.ReactNode {
|
||||
let className = "mx_Slider_dot";
|
||||
if (!this.props.disabled && this.props.active) {
|
||||
className += " mx_Slider_dotActive";
|
||||
}
|
||||
|
||||
return <span onClick={this.props.onClick} className="mx_Slider_dotValue">
|
||||
<div className={className} />
|
||||
<div className="mx_Slider_labelContainer">
|
||||
<div className="mx_Slider_label">
|
||||
{this.props.label}
|
||||
</div>
|
||||
</div>
|
||||
</span>;
|
||||
}
|
||||
}
|
|
@ -16,19 +16,36 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from "prop-types";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'Spinner',
|
||||
const Spinner = ({w = 32, h = 32, imgClassName, message}) => {
|
||||
let imageSource;
|
||||
if (SettingsStore.getValue('feature_new_spinner')) {
|
||||
imageSource = require("../../../../res/img/spinner.svg");
|
||||
} else {
|
||||
imageSource = require("../../../../res/img/spinner.gif");
|
||||
}
|
||||
|
||||
render: function() {
|
||||
const w = this.props.w || 32;
|
||||
const h = this.props.h || 32;
|
||||
const imgClass = this.props.imgClassName || "";
|
||||
return (
|
||||
<div className="mx_Spinner">
|
||||
<img src={require("../../../../res/img/spinner.gif")} width={w} height={h} className={imgClass} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
return (
|
||||
<div className="mx_Spinner">
|
||||
{ message && <React.Fragment><div className="mx_Spinner_Msg">{ message}</div> </React.Fragment> }
|
||||
<img
|
||||
src={imageSource}
|
||||
width={w}
|
||||
height={h}
|
||||
className={imgClassName}
|
||||
aria-label={_t("Loading...")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Spinner.propTypes = {
|
||||
w: PropTypes.number,
|
||||
h: PropTypes.number,
|
||||
imgClassName: PropTypes.string,
|
||||
message: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Spinner;
|
||||
|
|
56
src/components/views/elements/StyledCheckbox.tsx
Normal file
56
src/components/views/elements/StyledCheckbox.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
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 React from "react";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
|
||||
const CHECK_BOX_SVG = require("../../../../res/img/feather-customised/check.svg");
|
||||
|
||||
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
}
|
||||
|
||||
interface IState {
|
||||
}
|
||||
|
||||
export default class StyledCheckbox extends React.PureComponent<IProps, IState> {
|
||||
private id: string;
|
||||
|
||||
public static readonly defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
// 56^10 so unlikely chance of collision.
|
||||
this.id = "checkbox_" + randomString(10);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { children, className, ...otherProps } = this.props;
|
||||
return <span className={"mx_Checkbox " + className}>
|
||||
<input id={this.id} {...otherProps} type="checkbox" />
|
||||
<label htmlFor={this.id}>
|
||||
{/* Using the div to center the image */}
|
||||
<div className="mx_Checkbox_background">
|
||||
<img src={CHECK_BOX_SVG}/>
|
||||
</div>
|
||||
<div>
|
||||
{ this.props.children }
|
||||
</div>
|
||||
</label>
|
||||
</span>;
|
||||
}
|
||||
}
|
51
src/components/views/elements/StyledRadioButton.tsx
Normal file
51
src/components/views/elements/StyledRadioButton.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
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 React from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
outlined?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
}
|
||||
|
||||
export default class StyledRadioButton extends React.PureComponent<IProps, IState> {
|
||||
public static readonly defaultProps = {
|
||||
className: '',
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { children, className, disabled, outlined, ...otherProps } = this.props;
|
||||
const _className = classnames(
|
||||
'mx_RadioButton',
|
||||
className,
|
||||
{
|
||||
"mx_RadioButton_disabled": disabled,
|
||||
"mx_RadioButton_enabled": !disabled,
|
||||
"mx_RadioButton_checked": this.props.checked,
|
||||
"mx_RadioButton_outlined": outlined,
|
||||
});
|
||||
return <label className={_className}>
|
||||
<input type='radio' disabled={disabled} {...otherProps} />
|
||||
{/* Used to render the radio button circle */}
|
||||
<div><div /></div>
|
||||
<div className="mx_RadioButton_content">{children}</div>
|
||||
<div className="mx_RadioButton_spacer" />
|
||||
</label>;
|
||||
}
|
||||
}
|
63
src/components/views/elements/StyledRadioGroup.tsx
Normal file
63
src/components/views/elements/StyledRadioGroup.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
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 React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import StyledRadioButton from "./StyledRadioButton";
|
||||
|
||||
interface IDefinition<T extends string> {
|
||||
value: T;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
label: React.ReactChild;
|
||||
description?: React.ReactChild;
|
||||
checked?: boolean; // If provided it will override the value comparison done in the group
|
||||
}
|
||||
|
||||
interface IProps<T extends string> {
|
||||
name: string;
|
||||
className?: string;
|
||||
definitions: IDefinition<T>[];
|
||||
value?: T; // if not provided no options will be selected
|
||||
outlined?: boolean;
|
||||
onChange(newValue: T): void;
|
||||
}
|
||||
|
||||
function StyledRadioGroup<T extends string>({name, definitions, value, className, outlined, onChange}: IProps<T>) {
|
||||
const _onChange = e => {
|
||||
onChange(e.target.value);
|
||||
};
|
||||
|
||||
return <React.Fragment>
|
||||
{definitions.map(d => <React.Fragment key={d.value}>
|
||||
<StyledRadioButton
|
||||
className={classNames(className, d.className)}
|
||||
onChange={_onChange}
|
||||
checked={d.checked !== undefined ? d.checked : d.value === value}
|
||||
name={name}
|
||||
value={d.value}
|
||||
disabled={d.disabled}
|
||||
outlined={outlined}
|
||||
>
|
||||
{d.label}
|
||||
</StyledRadioButton>
|
||||
{d.description}
|
||||
</React.Fragment>)}
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
export default StyledRadioGroup;
|
|
@ -16,20 +16,20 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import classNames from 'classnames';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import TagOrderStore from '../../../stores/TagOrderStore';
|
||||
import {ContextMenu, toRightOf} from "../../structures/ContextMenu";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
|
||||
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
|
||||
// a thing to click on for the user to filter the visible rooms in the RoomList to:
|
||||
|
@ -43,6 +43,9 @@ export default createReactClass({
|
|||
// A string tag such as "m.favourite" or a group ID such as "+groupid:domain.bla"
|
||||
// For now, only group IDs are handled.
|
||||
tag: PropTypes.string,
|
||||
contextMenuButtonRef: PropTypes.object,
|
||||
openMenu: PropTypes.func,
|
||||
menuDisplayed: PropTypes.bool,
|
||||
},
|
||||
|
||||
statics: {
|
||||
|
@ -55,14 +58,10 @@ export default createReactClass({
|
|||
hover: false,
|
||||
// The profile data of the group if this.props.tag is a group ID
|
||||
profile: null,
|
||||
// Whether or not the context menu is open
|
||||
menuDisplayed: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
this._contextMenuButton = createRef();
|
||||
|
||||
this.unmounted = false;
|
||||
if (this.props.tag[0] === '+') {
|
||||
FlairStore.addListener('updateGroupProfile', this._onFlairStoreUpdated);
|
||||
|
@ -86,7 +85,7 @@ export default createReactClass({
|
|||
this.props.tag,
|
||||
).then((profile) => {
|
||||
if (this.unmounted) return;
|
||||
this.setState({profile});
|
||||
this.setState({ profile });
|
||||
}).catch((err) => {
|
||||
console.warn('Could not fetch group profile for ' + this.props.tag, err);
|
||||
});
|
||||
|
@ -113,35 +112,26 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
onMouseOver: function() {
|
||||
this.setState({hover: true});
|
||||
this.setState({ hover: true });
|
||||
},
|
||||
|
||||
onMouseOut: function() {
|
||||
this.setState({hover: false});
|
||||
onMouseLeave: function() {
|
||||
this.setState({ hover: false });
|
||||
},
|
||||
|
||||
openMenu: function(e) {
|
||||
// Prevent the TagTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({
|
||||
menuDisplayed: true,
|
||||
hover: false,
|
||||
});
|
||||
},
|
||||
|
||||
closeMenu: function() {
|
||||
this.setState({
|
||||
menuDisplayed: false,
|
||||
});
|
||||
this.setState({ hover: false });
|
||||
this.props.openMenu();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const profile = this.state.profile || {};
|
||||
const name = profile.name || this.props.tag;
|
||||
const avatarHeight = 40;
|
||||
const avatarHeight = 32;
|
||||
|
||||
const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp(
|
||||
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
|
||||
|
@ -154,7 +144,7 @@ export default createReactClass({
|
|||
|
||||
const badge = TagOrderStore.getGroupBadge(this.props.tag);
|
||||
let badgeElement;
|
||||
if (badge && !this.state.hover) {
|
||||
if (badge && !this.state.hover && !this.props.menuDisplayed) {
|
||||
const badgeClasses = classNames({
|
||||
"mx_TagTile_badge": true,
|
||||
"mx_TagTile_badgeHighlight": badge.highlight,
|
||||
|
@ -162,40 +152,38 @@ export default createReactClass({
|
|||
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>);
|
||||
}
|
||||
|
||||
// FIXME: this ought to use AccessibleButton for a11y but that causes onMouseOut/onMouseOver to fire too much
|
||||
const contextButton = this.state.hover || this.state.menuDisplayed ?
|
||||
<div className="mx_TagTile_context_button" onClick={this.openMenu} ref={this._contextMenuButton}>
|
||||
{ "\u00B7\u00B7\u00B7" }
|
||||
</div> : <div ref={this._contextMenuButton} />;
|
||||
|
||||
let contextMenu;
|
||||
if (this.state.menuDisplayed) {
|
||||
const elementRect = this._contextMenuButton.current.getBoundingClientRect();
|
||||
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||
contextMenu = (
|
||||
<ContextMenu {...toRightOf(elementRect)} onFinished={this.closeMenu}>
|
||||
<TagTileContextMenu tag={this.props.tag} onFinished={this.closeMenu} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
const contextButton = this.state.hover || this.props.menuDisplayed ?
|
||||
<AccessibleButton
|
||||
className="mx_TagTile_context_button"
|
||||
onClick={this.openMenu}
|
||||
inputRef={this.props.contextMenuButtonRef}
|
||||
>
|
||||
{"\u00B7\u00B7\u00B7"}
|
||||
</AccessibleButton> : <div ref={this.props.contextMenuButtonRef} />;
|
||||
|
||||
const AccessibleTooltipButton = sdk.getComponent("elements.AccessibleTooltipButton");
|
||||
return <React.Fragment>
|
||||
<AccessibleTooltipButton className={className} onClick={this.onClick} onContextMenu={this.openMenu} title={name}>
|
||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
idName={this.props.tag}
|
||||
url={httpUrl}
|
||||
width={avatarHeight}
|
||||
height={avatarHeight}
|
||||
/>
|
||||
{ contextButton }
|
||||
{ badgeElement }
|
||||
</div>
|
||||
</AccessibleTooltipButton>
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
return <AccessibleTooltipButton
|
||||
className={className}
|
||||
onClick={this.onClick}
|
||||
onContextMenu={this.openMenu}
|
||||
title={name}
|
||||
>
|
||||
<div
|
||||
className="mx_TagTile_avatar"
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
idName={this.props.tag}
|
||||
url={httpUrl}
|
||||
width={avatarHeight}
|
||||
height={avatarHeight}
|
||||
/>
|
||||
{contextButton}
|
||||
{badgeElement}
|
||||
</div>
|
||||
</AccessibleTooltipButton>;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -37,7 +37,7 @@ export default class TextWithTooltip extends React.Component {
|
|||
this.setState({hover: true});
|
||||
};
|
||||
|
||||
onMouseOut = () => {
|
||||
onMouseLeave = () => {
|
||||
this.setState({hover: false});
|
||||
};
|
||||
|
||||
|
@ -45,13 +45,12 @@ export default class TextWithTooltip extends React.Component {
|
|||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
|
||||
return (
|
||||
<span onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} className={this.props.class}>
|
||||
<span onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={this.props.class}>
|
||||
{this.props.children}
|
||||
<Tooltip
|
||||
{this.state.hover && <Tooltip
|
||||
label={this.props.tooltip}
|
||||
visible={this.state.hover}
|
||||
tooltipClassName={this.props.tooltipClass}
|
||||
className={"mx_TextWithTooltip_tooltip"} />
|
||||
className={"mx_TextWithTooltip_tooltip"} /> }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,13 +16,23 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from "classnames";
|
||||
import * as sdk from "../../../index";
|
||||
|
||||
interface IProps {
|
||||
// Whether or not this toggle is in the 'on' position.
|
||||
checked: boolean;
|
||||
|
||||
// Whether or not the user can interact with the switch
|
||||
disabled: boolean;
|
||||
|
||||
// Called when the checked state changes. First argument will be the new state.
|
||||
onChange(checked: boolean): void;
|
||||
}
|
||||
|
||||
// Controlled Toggle Switch element, written with Accessibility in mind
|
||||
const ToggleSwitch = ({checked, disabled=false, onChange, ...props}) => {
|
||||
const _onClick = (e) => {
|
||||
export default ({checked, disabled = false, onChange, ...props}: IProps) => {
|
||||
const _onClick = () => {
|
||||
if (disabled) return;
|
||||
onChange(!checked);
|
||||
};
|
||||
|
@ -46,16 +56,3 @@ const ToggleSwitch = ({checked, disabled=false, onChange, ...props}) => {
|
|||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
|
||||
ToggleSwitch.propTypes = {
|
||||
// Whether or not this toggle is in the 'on' position.
|
||||
checked: PropTypes.bool.isRequired,
|
||||
|
||||
// Whether or not the user can interact with the switch
|
||||
disabled: PropTypes.bool,
|
||||
|
||||
// Called when the checked state changes. First argument will be the new state.
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ToggleSwitch;
|
|
@ -2,7 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 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.
|
||||
|
@ -18,67 +18,60 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import React, {Component, CSSProperties} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import dis from '../../../dispatcher';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const MIN_TOOLTIP_HEIGHT = 25;
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'Tooltip',
|
||||
|
||||
propTypes: {
|
||||
interface IProps {
|
||||
// Class applied to the element used to position the tooltip
|
||||
className: PropTypes.string,
|
||||
className?: string;
|
||||
// Class applied to the tooltip itself
|
||||
tooltipClassName: PropTypes.string,
|
||||
tooltipClassName?: string;
|
||||
// Whether the tooltip is visible or hidden.
|
||||
// The hidden state allows animating the tooltip away via CSS.
|
||||
// Defaults to visible if unset.
|
||||
visible: PropTypes.bool,
|
||||
visible?: boolean;
|
||||
// the react element to put into the tooltip
|
||||
label: PropTypes.node,
|
||||
},
|
||||
label: React.ReactNode;
|
||||
forceOnRight?: boolean;
|
||||
}
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
visible: true,
|
||||
};
|
||||
},
|
||||
export default class Tooltip extends React.Component<IProps> {
|
||||
private tooltipContainer: HTMLElement;
|
||||
private tooltip: void | Element | Component<Element, any, any>;
|
||||
private parent: Element;
|
||||
|
||||
|
||||
public static readonly defaultProps = {
|
||||
visible: true,
|
||||
};
|
||||
|
||||
// Create a wrapper for the tooltip outside the parent and attach it to the body element
|
||||
componentDidMount: function() {
|
||||
public componentDidMount() {
|
||||
this.tooltipContainer = document.createElement("div");
|
||||
this.tooltipContainer.className = "mx_Tooltip_wrapper";
|
||||
document.body.appendChild(this.tooltipContainer);
|
||||
window.addEventListener('scroll', this._renderTooltip, true);
|
||||
window.addEventListener('scroll', this.renderTooltip, true);
|
||||
|
||||
this.parent = ReactDOM.findDOMNode(this).parentNode;
|
||||
this.parent = ReactDOM.findDOMNode(this).parentNode as Element;
|
||||
|
||||
this._renderTooltip();
|
||||
},
|
||||
this.renderTooltip();
|
||||
}
|
||||
|
||||
componentDidUpdate: function() {
|
||||
this._renderTooltip();
|
||||
},
|
||||
public componentDidUpdate() {
|
||||
this.renderTooltip();
|
||||
}
|
||||
|
||||
// Remove the wrapper element, as the tooltip has finished using it
|
||||
componentWillUnmount: function() {
|
||||
dis.dispatch({
|
||||
action: 'view_tooltip',
|
||||
tooltip: null,
|
||||
parent: null,
|
||||
});
|
||||
|
||||
public componentWillUnmount() {
|
||||
ReactDOM.unmountComponentAtNode(this.tooltipContainer);
|
||||
document.body.removeChild(this.tooltipContainer);
|
||||
window.removeEventListener('scroll', this._renderTooltip, true);
|
||||
},
|
||||
window.removeEventListener('scroll', this.renderTooltip, true);
|
||||
}
|
||||
|
||||
_updatePosition(style) {
|
||||
private updatePosition(style: CSSProperties) {
|
||||
const parentBox = this.parent.getBoundingClientRect();
|
||||
let offset = 0;
|
||||
if (parentBox.height > MIN_TOOLTIP_HEIGHT) {
|
||||
|
@ -88,19 +81,23 @@ export default createReactClass({
|
|||
// we need so that we're still centered.
|
||||
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
|
||||
}
|
||||
style.top = (parentBox.top - 2) + window.pageYOffset + offset;
|
||||
style.left = 6 + parentBox.right + window.pageXOffset;
|
||||
return style;
|
||||
},
|
||||
|
||||
_renderTooltip: function() {
|
||||
style.top = (parentBox.top - 2) + window.pageYOffset + offset;
|
||||
if (!this.props.forceOnRight && parentBox.right > window.innerWidth / 2) {
|
||||
style.right = window.innerWidth - parentBox.right - window.pageXOffset - 8;
|
||||
} else {
|
||||
style.left = parentBox.right + window.pageXOffset + 6;
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
private renderTooltip = () => {
|
||||
// Add the parent's position to the tooltips, so it's correctly
|
||||
// positioned, also taking into account any window zoom
|
||||
// NOTE: The additional 6 pixels for the left position, is to take account of the
|
||||
// tooltips chevron
|
||||
const parent = ReactDOM.findDOMNode(this).parentNode;
|
||||
let style = {};
|
||||
style = this._updatePosition(style);
|
||||
const style = this.updatePosition({});
|
||||
// Hide the entire container when not visible. This prevents flashing of the tooltip
|
||||
// if it is not meant to be visible on first mount.
|
||||
style.display = this.props.visible ? "block" : "none";
|
||||
|
@ -118,21 +115,14 @@ export default createReactClass({
|
|||
);
|
||||
|
||||
// Render the tooltip manually, as we wish it not to be rendered within the parent
|
||||
this.tooltip = ReactDOM.render(tooltip, this.tooltipContainer);
|
||||
this.tooltip = ReactDOM.render<Element>(tooltip, this.tooltipContainer);
|
||||
};
|
||||
|
||||
// Tell the roomlist about us so it can manipulate us if it wishes
|
||||
dis.dispatch({
|
||||
action: 'view_tooltip',
|
||||
tooltip: this.tooltip,
|
||||
parent: parent,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
public render() {
|
||||
// Render a placeholder
|
||||
return (
|
||||
<div className={this.props.className} >
|
||||
<div className={this.props.className}>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -34,7 +34,7 @@ export default createReactClass({
|
|||
});
|
||||
},
|
||||
|
||||
onMouseOut: function() {
|
||||
onMouseLeave: function() {
|
||||
this.setState({
|
||||
hover: false,
|
||||
});
|
||||
|
@ -48,7 +48,7 @@ export default createReactClass({
|
|||
label={this.props.helpText}
|
||||
/> : <div />;
|
||||
return (
|
||||
<div className="mx_TooltipButton" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} >
|
||||
<div className="mx_TooltipButton" onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave}>
|
||||
?
|
||||
{ tip }
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
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.
|
||||
|
@ -15,11 +16,39 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
/* eslint-disable babel/no-invalid-this */
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import classNames from 'classnames';
|
||||
type Data = Pick<IFieldState, "value" | "allowEmpty">;
|
||||
|
||||
interface IRule<T> {
|
||||
key: string;
|
||||
final?: boolean;
|
||||
skip?(this: T, data: Data): boolean;
|
||||
test(this: T, data: Data): boolean | Promise<boolean>;
|
||||
valid?(this: T): string;
|
||||
invalid?(this: T): string;
|
||||
}
|
||||
|
||||
interface IArgs<T> {
|
||||
rules: IRule<T>[];
|
||||
description(this: T): React.ReactChild;
|
||||
}
|
||||
|
||||
export interface IFieldState {
|
||||
value: string;
|
||||
focused: boolean;
|
||||
allowEmpty: boolean;
|
||||
}
|
||||
|
||||
export interface IValidationResult {
|
||||
valid?: boolean;
|
||||
feedback?: React.ReactChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a validation function from a set of rules describing what to validate.
|
||||
* Generic T is the "this" type passed to the rule methods
|
||||
*
|
||||
* @param {Function} description
|
||||
* Function that returns a string summary of the kind of value that will
|
||||
|
@ -37,8 +66,8 @@ import classNames from 'classnames';
|
|||
* A validation function that takes in the current input value and returns
|
||||
* the overall validity and a feedback UI that can be rendered for more detail.
|
||||
*/
|
||||
export default function withValidation({ description, rules }) {
|
||||
return async function onValidate({ value, focused, allowEmpty = true }) {
|
||||
export default function withValidation<T = undefined>({ description, rules }: IArgs<T>) {
|
||||
return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise<IValidationResult> {
|
||||
if (!value && allowEmpty) {
|
||||
return {
|
||||
valid: null,
|
|
@ -17,8 +17,7 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
||||
import Spinner from "../Spinner";
|
||||
import * as QRCode from "qrcode";
|
||||
import QRCode from "../QRCode";
|
||||
|
||||
@replaceableComponent("views.elements.crypto.VerificationQRCode")
|
||||
export default class VerificationQRCode extends React.PureComponent {
|
||||
|
@ -26,33 +25,12 @@ export default class VerificationQRCode extends React.PureComponent {
|
|||
qrCodeData: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
dataUri: null,
|
||||
};
|
||||
this.generateQrCode();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps): void {
|
||||
if (JSON.stringify(this.props) === JSON.stringify(prevProps)) return; // No prop change
|
||||
|
||||
this.generateQRCode();
|
||||
}
|
||||
|
||||
async generateQrCode() {
|
||||
// Now actually assemble the QR code's data URI
|
||||
const uri = await QRCode.toDataURL([{data: this.props.qrCodeData.buffer, mode: 'byte'}], {
|
||||
errorCorrectionLevel: 'L', // we want it as trivial-looking as possible
|
||||
});
|
||||
this.setState({dataUri: uri});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.dataUri) {
|
||||
return <div className='mx_VerificationQRCode'><Spinner /></div>;
|
||||
}
|
||||
|
||||
return <img src={this.state.dataUri} className='mx_VerificationQRCode' />;
|
||||
return (
|
||||
<QRCode
|
||||
data={[{data: this.props.qrCodeData.buffer, mode: 'byte'}]}
|
||||
className="mx_VerificationQRCode"
|
||||
width={196} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue