Merge branch 'develop' into sort-imports
Signed-off-by: Aaron Raimist <aaron@raim.ist>
This commit is contained in:
commit
7b94e13a84
642 changed files with 30052 additions and 8035 deletions
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import Tooltip, { Alignment } from './Tooltip';
|
||||
|
@ -70,13 +69,12 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, ...props } = this.props;
|
||||
|
||||
const tip = this.state.hover ? <Tooltip
|
||||
className="mx_AccessibleTooltipButton_container"
|
||||
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
|
||||
const tip = this.state.hover && <Tooltip
|
||||
tooltipClassName={tooltipClassName}
|
||||
label={tooltip || title}
|
||||
yOffset={yOffset}
|
||||
alignment={alignment}
|
||||
/> : null;
|
||||
/>;
|
||||
return (
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
|
|
|
@ -19,11 +19,6 @@ limitations under the License.
|
|||
|
||||
import url from 'url';
|
||||
import React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { MatrixCapabilities } from "matrix-widget-api";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -32,22 +27,27 @@ import AppWarning from './AppWarning';
|
|||
import Spinner from './Spinner';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
import classNames from 'classnames';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { aboveLeftOf, ContextMenuButton } from "../../structures/ContextMenu";
|
||||
import PersistedElement, { getPersistKey } from "./PersistedElement";
|
||||
import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import { StopGapWidget } from "../../../stores/widgets/StopGapWidget";
|
||||
import { ElementWidgetActions } from "../../../stores/widgets/ElementWidgetActions";
|
||||
import { MatrixCapabilities } from "matrix-widget-api";
|
||||
import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { IApp } from "../../../stores/WidgetStore";
|
||||
|
||||
import { WidgetLayoutStore, Container } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
interface IProps {
|
||||
app: IApp;
|
||||
// If room is not specified then it is an account level widget
|
||||
// which bypasses permission prompts as it was added explicitly by that user
|
||||
room: Room;
|
||||
threadId?: string | null;
|
||||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||
fullWidth?: boolean;
|
||||
|
@ -89,6 +89,8 @@ interface IState {
|
|||
requiresClient: boolean;
|
||||
}
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
@replaceableComponent("views.elements.AppTile")
|
||||
export default class AppTile extends React.Component<IProps, IState> {
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
|
@ -99,6 +101,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
handleMinimisePointerEvents: false,
|
||||
userWidget: false,
|
||||
miniMode: false,
|
||||
threadId: null,
|
||||
};
|
||||
|
||||
private contextMenuButton = createRef<any>();
|
||||
|
@ -227,7 +230,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
this.sgWidget.on("ready", this.onWidgetReady);
|
||||
this.startWidget();
|
||||
} catch (e) {
|
||||
logger.log("Failed to construct widget", e);
|
||||
logger.error("Failed to construct widget", e);
|
||||
this.sgWidget = null;
|
||||
}
|
||||
}
|
||||
|
@ -241,7 +244,13 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
private iframeRefChange = (ref: HTMLIFrameElement): void => {
|
||||
this.iframe = ref;
|
||||
if (ref) {
|
||||
if (this.sgWidget) this.sgWidget.start(ref);
|
||||
try {
|
||||
if (this.sgWidget) {
|
||||
this.sgWidget.start(ref);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Failed to start widget", e);
|
||||
}
|
||||
} else {
|
||||
this.resetWidget(this.props);
|
||||
}
|
||||
|
@ -284,7 +293,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
dis.dispatch({ action: 'hangup_conference' });
|
||||
CallHandler.instance.hangupCallApp(this.props.room.roomId);
|
||||
}
|
||||
|
||||
// Delete the widget from the persisted store for good measure.
|
||||
|
@ -315,7 +324,13 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
switch (payload.action) {
|
||||
case 'm.sticker':
|
||||
if (this.sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
|
||||
dis.dispatch({ action: 'post_sticker_message', data: payload.data });
|
||||
dis.dispatch({
|
||||
action: 'post_sticker_message',
|
||||
data: {
|
||||
...payload.data,
|
||||
threadId: this.props.threadId,
|
||||
},
|
||||
});
|
||||
dis.dispatch({ action: 'stickerpicker_close' });
|
||||
} else {
|
||||
logger.warn('Ignoring sticker message. Invalid capability');
|
||||
|
@ -394,6 +409,14 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
{ target: '_blank', href: this.sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click();
|
||||
};
|
||||
|
||||
private onMaxMinWidgetClick = (): void => {
|
||||
const targetContainer =
|
||||
WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, Container.Center)
|
||||
? Container.Right
|
||||
: Container.Center;
|
||||
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, targetContainer);
|
||||
};
|
||||
|
||||
private onContextMenuClick = (): void => {
|
||||
this.setState({ menuDisplayed: true });
|
||||
};
|
||||
|
@ -420,7 +443,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' ');
|
||||
const appTileBodyStyles = {};
|
||||
if (this.props.pointerEvents) {
|
||||
appTileBodyStyles['pointer-events'] = this.props.pointerEvents;
|
||||
appTileBodyStyles['pointerEvents'] = this.props.pointerEvents;
|
||||
}
|
||||
|
||||
const loadingElement = (
|
||||
|
@ -516,6 +539,23 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
/>
|
||||
);
|
||||
}
|
||||
let maxMinButton;
|
||||
if (SettingsStore.getValue("feature_maximised_widgets")) {
|
||||
const widgetIsMaximised = WidgetLayoutStore.instance.
|
||||
isInContainer(this.props.room, this.props.app, Container.Center);
|
||||
maxMinButton = <AccessibleButton
|
||||
className={
|
||||
"mx_AppTileMenuBar_iconButton"
|
||||
+ (widgetIsMaximised
|
||||
? " mx_AppTileMenuBar_iconButton_minWidget"
|
||||
: " mx_AppTileMenuBar_iconButton_maxWidget")
|
||||
}
|
||||
title={
|
||||
widgetIsMaximised ? _t('Close'): _t('Maximise widget')
|
||||
}
|
||||
onClick={this.onMaxMinWidgetClick}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<div className={appTileClasses} id={this.props.app.id}>
|
||||
|
@ -525,6 +565,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
{ this.props.showTitle && this.getTileTitle() }
|
||||
</span>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{ maxMinButton }
|
||||
{ (this.props.showPopout && !this.state.requiresClient) && <AccessibleButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
|
||||
title={_t('Popout widget')}
|
||||
|
|
|
@ -15,10 +15,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import TagTile from './TagTile';
|
||||
import { ContextMenu, toRightOf, useContextMenu } from "../../structures/ContextMenu";
|
||||
|
||||
import React from 'react';
|
||||
import ContextMenu, { toRightOf, useContextMenu } from "../../structures/ContextMenu";
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
export default function DNDTagTile(props) {
|
||||
|
|
|
@ -178,26 +178,20 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
|||
this.ignoreEvent = ev;
|
||||
};
|
||||
|
||||
private onChevronClick = (ev: React.MouseEvent) => {
|
||||
if (this.state.expanded) {
|
||||
this.setState({ expanded: false });
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
private onAccessibleButtonClick = (ev: ButtonEvent) => {
|
||||
if (this.props.disabled) return;
|
||||
|
||||
if (!this.state.expanded) {
|
||||
this.setState({
|
||||
expanded: true,
|
||||
});
|
||||
this.setState({ expanded: true });
|
||||
ev.preventDefault();
|
||||
} else if ((ev as React.KeyboardEvent).key === Key.ENTER) {
|
||||
// the accessible button consumes enter onKeyDown for firing onClick, so handle it here
|
||||
this.props.onOptionChange(this.state.highlightedOption);
|
||||
this.close();
|
||||
} else if (!(ev as React.KeyboardEvent).key) {
|
||||
// collapse on other non-keyboard event activations
|
||||
this.setState({ expanded: false });
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -228,14 +222,22 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
|||
this.close();
|
||||
break;
|
||||
case Key.ARROW_DOWN:
|
||||
this.setState({
|
||||
highlightedOption: this.nextOption(this.state.highlightedOption),
|
||||
});
|
||||
if (this.state.expanded) {
|
||||
this.setState({
|
||||
highlightedOption: this.nextOption(this.state.highlightedOption),
|
||||
});
|
||||
} else {
|
||||
this.setState({ expanded: true });
|
||||
}
|
||||
break;
|
||||
case Key.ARROW_UP:
|
||||
this.setState({
|
||||
highlightedOption: this.prevOption(this.state.highlightedOption),
|
||||
});
|
||||
if (this.state.expanded) {
|
||||
this.setState({
|
||||
highlightedOption: this.prevOption(this.state.highlightedOption),
|
||||
});
|
||||
} else {
|
||||
this.setState({ expanded: true });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
|
@ -383,7 +385,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
|
|||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
{ currentValue }
|
||||
<span onClick={this.onChevronClick} className="mx_Dropdown_arrow" />
|
||||
<span className="mx_Dropdown_arrow" />
|
||||
{ menu }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
|
|
|
@ -105,13 +105,19 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> {
|
|||
</React.Fragment>;
|
||||
}
|
||||
|
||||
let clearCacheButton: JSX.Element;
|
||||
// we only show this button if there is an initialised MatrixClient otherwise we can't clear the cache
|
||||
if (MatrixClientPeg.get()) {
|
||||
clearCacheButton = <AccessibleButton onClick={this.onClearCacheAndReload} kind='danger'>
|
||||
{ _t("Clear cache and reload") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return <div className="mx_ErrorBoundary">
|
||||
<div className="mx_ErrorBoundary_body">
|
||||
<h1>{ _t("Something went wrong!") }</h1>
|
||||
{ bugReportSection }
|
||||
<AccessibleButton onClick={this.onClearCacheAndReload} kind='danger'>
|
||||
{ _t("Clear cache and reload") }
|
||||
</AccessibleButton>
|
||||
{ clearCacheButton }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
import React, { ReactNode, useEffect } from "react";
|
||||
import { uniqBy } from "lodash";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
|
@ -22,7 +23,7 @@ import MemberAvatar from '../avatars/MemberAvatar';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import { useStateToggle } from "../../../hooks/useStateToggle";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import { Layout } from '../../../settings/Layout';
|
||||
import { Layout } from '../../../settings/enums/Layout';
|
||||
|
||||
interface IProps {
|
||||
// An array of member events to summarise
|
||||
|
@ -80,7 +81,8 @@ const EventListSummary: React.FC<IProps> = ({
|
|||
{ children }
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
const avatars = summaryMembers.map((m) => <MemberAvatar key={m.userId} member={m} width={14} height={14} />);
|
||||
const uniqueMembers = uniqBy(summaryMembers, member => member.getMxcAvatarUrl());
|
||||
const avatars = uniqueMembers.map((m) => <MemberAvatar key={m.userId} member={m} width={14} height={14} />);
|
||||
body = (
|
||||
<div className="mx_EventTile_line">
|
||||
<div className="mx_EventTile_info">
|
||||
|
|
|
@ -22,7 +22,7 @@ import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
|||
import * as Avatar from '../../../Avatar';
|
||||
import EventTile from '../rooms/EventTile';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { Layout } from "../../../settings/Layout";
|
||||
import { Layout } from "../../../settings/enums/Layout";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Spinner from './Spinner';
|
||||
|
|
|
@ -46,6 +46,9 @@ interface IProps {
|
|||
label?: string;
|
||||
// The field's placeholder string. Defaults to the label.
|
||||
placeholder?: string;
|
||||
// When true (default false), the placeholder will be shown instead of the label when
|
||||
// the component is unfocused & empty.
|
||||
usePlaceholderAsHint?: boolean;
|
||||
// Optional component to include inside the field before the input.
|
||||
prefixComponent?: React.ReactNode;
|
||||
// Optional component to include inside the field after the input.
|
||||
|
@ -227,6 +230,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
||||
const { element, prefixComponent, postfixComponent, className, onValidate, children,
|
||||
tooltipContent, forceValidity, tooltipClassName, list, validateOnBlur, validateOnChange, validateOnFocus,
|
||||
usePlaceholderAsHint,
|
||||
...inputProps } = this.props;
|
||||
|
||||
// Set some defaults for the <input> element
|
||||
|
@ -257,7 +261,8 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
// 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: prefixComponent,
|
||||
mx_Field_labelAlwaysTopLeft: prefixComponent || usePlaceholderAsHint,
|
||||
mx_Field_placeholderIsHint: usePlaceholderAsHint,
|
||||
mx_Field_valid: hasValidationFlag ? forceValidity : onValidate && this.state.valid === true,
|
||||
mx_Field_invalid: hasValidationFlag
|
||||
? !forceValidity
|
||||
|
|
|
@ -31,6 +31,7 @@ import MessageTimestamp from "../messages/MessageTimestamp";
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { formatFullDate } from "../../../DateUtils";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import { normalizeWheelEvent } from "../../../utils/Mouse";
|
||||
|
@ -333,7 +334,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
// matrix.to, but also for it to enable routing within Element when clicked.
|
||||
ev.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
action: Action.ViewRoom,
|
||||
event_id: this.props.mxEvent.getId(),
|
||||
highlighted: true,
|
||||
room_id: this.props.mxEvent.getRoomId(),
|
||||
|
|
496
src/components/views/elements/InteractiveTooltip.tsx
Normal file
496
src/components/views/elements/InteractiveTooltip.tsx
Normal file
|
@ -0,0 +1,496 @@
|
|||
/*
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { CSSProperties, MouseEventHandler, ReactNode, RefCallback } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import classNames from "classnames";
|
||||
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { ChevronFace } from "../../structures/ContextMenu";
|
||||
|
||||
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(): HTMLElement {
|
||||
let container = document.getElementById(InteractiveTooltipContainerId);
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = InteractiveTooltipContainerId;
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
interface IRect {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
}
|
||||
|
||||
function isInRect(x: number, y: number, rect: IRect): boolean {
|
||||
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 {number}
|
||||
*/
|
||||
function getDiagonalSlope(rect: IRect): number {
|
||||
const { top, right, bottom, left } = rect;
|
||||
return (bottom - top) / (right - left);
|
||||
}
|
||||
|
||||
function isInUpperLeftHalf(x: number, y: number, rect: IRect): boolean {
|
||||
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: number, y: number, rect: IRect): boolean {
|
||||
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: number, y: number, rect: IRect): boolean {
|
||||
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: number, y: number, rect: IRect): boolean {
|
||||
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));
|
||||
}
|
||||
|
||||
export enum Direction {
|
||||
Top,
|
||||
Left,
|
||||
Bottom,
|
||||
Right,
|
||||
}
|
||||
|
||||
// exported for tests
|
||||
export function mouseWithinRegion(
|
||||
x: number,
|
||||
y: number,
|
||||
direction: Direction,
|
||||
targetRect: DOMRect,
|
||||
contentRect: DOMRect,
|
||||
): boolean {
|
||||
// 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 true;
|
||||
}
|
||||
|
||||
switch (direction) {
|
||||
case Direction.Left: {
|
||||
const contentRectWithBuffer = {
|
||||
top: contentRect.top - buffer,
|
||||
right: contentRect.right,
|
||||
bottom: contentRect.bottom + buffer,
|
||||
left: contentRect.left - buffer,
|
||||
};
|
||||
const trapezoidTop = {
|
||||
top: contentRect.top - buffer,
|
||||
right: targetRect.right,
|
||||
bottom: targetRect.top,
|
||||
left: contentRect.right,
|
||||
};
|
||||
const trapezoidCenter = {
|
||||
top: targetRect.top,
|
||||
right: targetRect.left,
|
||||
bottom: targetRect.bottom,
|
||||
left: contentRect.right,
|
||||
};
|
||||
const trapezoidBottom = {
|
||||
top: targetRect.bottom,
|
||||
right: targetRect.right,
|
||||
bottom: contentRect.bottom + buffer,
|
||||
left: contentRect.right,
|
||||
};
|
||||
|
||||
if (
|
||||
isInRect(x, y, contentRectWithBuffer) ||
|
||||
isInLowerLeftHalf(x, y, trapezoidTop) ||
|
||||
isInRect(x, y, trapezoidCenter) ||
|
||||
isInUpperLeftHalf(x, y, trapezoidBottom)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case Direction.Right: {
|
||||
const contentRectWithBuffer = {
|
||||
top: contentRect.top - buffer,
|
||||
right: contentRect.right + buffer,
|
||||
bottom: contentRect.bottom + buffer,
|
||||
left: contentRect.left,
|
||||
};
|
||||
const trapezoidTop = {
|
||||
top: contentRect.top - buffer,
|
||||
right: contentRect.left,
|
||||
bottom: targetRect.top,
|
||||
left: targetRect.left,
|
||||
};
|
||||
const trapezoidCenter = {
|
||||
top: targetRect.top,
|
||||
right: contentRect.left,
|
||||
bottom: targetRect.bottom,
|
||||
left: targetRect.right,
|
||||
};
|
||||
const trapezoidBottom = {
|
||||
top: targetRect.bottom,
|
||||
right: contentRect.left,
|
||||
bottom: contentRect.bottom + buffer,
|
||||
left: targetRect.left,
|
||||
};
|
||||
|
||||
if (
|
||||
isInRect(x, y, contentRectWithBuffer) ||
|
||||
isInLowerRightHalf(x, y, trapezoidTop) ||
|
||||
isInRect(x, y, trapezoidCenter) ||
|
||||
isInUpperRightHalf(x, y, trapezoidBottom)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case Direction.Top: {
|
||||
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.top,
|
||||
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 true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case Direction.Bottom: {
|
||||
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.bottom,
|
||||
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 true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
children(props: {
|
||||
ref: RefCallback<HTMLElement>;
|
||||
onMouseOver: MouseEventHandler;
|
||||
}): ReactNode;
|
||||
// Content to show in the tooltip
|
||||
content: ReactNode;
|
||||
direction?: Direction;
|
||||
// Function to call when visibility of the tooltip changes
|
||||
onVisibilityChange?(visible: boolean): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
contentRect: DOMRect;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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<IProps, IState> {
|
||||
private target: HTMLElement;
|
||||
|
||||
public static defaultProps = {
|
||||
side: Direction.Top,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private collectContentRect = (element: HTMLElement): void => {
|
||||
// We don't need to clean up when unmounting, so ignore
|
||||
if (!element) return;
|
||||
|
||||
this.setState({
|
||||
contentRect: element.getBoundingClientRect(),
|
||||
});
|
||||
};
|
||||
|
||||
private collectTarget = (element: HTMLElement) => {
|
||||
this.target = element;
|
||||
};
|
||||
|
||||
private onLeftOfTarget(): boolean {
|
||||
const { contentRect } = this.state;
|
||||
const targetRect = this.target.getBoundingClientRect();
|
||||
|
||||
if (this.props.direction === Direction.Left) {
|
||||
const targetLeft = targetRect.left + window.pageXOffset;
|
||||
return !contentRect || (targetLeft - contentRect.width > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
|
||||
} else {
|
||||
const targetRight = targetRect.right + window.pageXOffset;
|
||||
const spaceOnRight = UIStore.instance.windowWidth - targetRight;
|
||||
return !contentRect || (spaceOnRight - contentRect.width < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
|
||||
}
|
||||
}
|
||||
|
||||
private aboveTarget(): boolean {
|
||||
const { contentRect } = this.state;
|
||||
const targetRect = this.target.getBoundingClientRect();
|
||||
|
||||
if (this.props.direction === Direction.Top) {
|
||||
const targetTop = targetRect.top + window.pageYOffset;
|
||||
return !contentRect || (targetTop - contentRect.height > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
|
||||
} else {
|
||||
const targetBottom = targetRect.bottom + window.pageYOffset;
|
||||
const spaceBelow = UIStore.instance.windowHeight - targetBottom;
|
||||
return !contentRect || (spaceBelow - contentRect.height < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
|
||||
}
|
||||
}
|
||||
|
||||
private get isOnTheSide(): boolean {
|
||||
return this.props.direction === Direction.Left || this.props.direction === Direction.Right;
|
||||
}
|
||||
|
||||
private onMouseMove = (ev: MouseEvent) => {
|
||||
const { clientX: x, clientY: y } = ev;
|
||||
const { contentRect } = this.state;
|
||||
const targetRect = this.target.getBoundingClientRect();
|
||||
|
||||
let direction: Direction;
|
||||
if (this.isOnTheSide) {
|
||||
direction = this.onLeftOfTarget() ? Direction.Left : Direction.Right;
|
||||
} else {
|
||||
direction = this.aboveTarget() ? Direction.Top : Direction.Bottom;
|
||||
}
|
||||
|
||||
if (!mouseWithinRegion(x, y, direction, targetRect, contentRect)) {
|
||||
this.hideTooltip();
|
||||
}
|
||||
};
|
||||
|
||||
private onTargetMouseOver = (): void => {
|
||||
this.showTooltip();
|
||||
};
|
||||
|
||||
private showTooltip(): void {
|
||||
// Don't enter visible state if we haven't collected the target yet
|
||||
if (!this.target) return;
|
||||
|
||||
this.setState({
|
||||
visible: true,
|
||||
});
|
||||
this.props.onVisibilityChange?.(true);
|
||||
document.addEventListener("mousemove", this.onMouseMove);
|
||||
}
|
||||
|
||||
public hideTooltip() {
|
||||
this.setState({
|
||||
visible: false,
|
||||
});
|
||||
this.props.onVisibilityChange?.(false);
|
||||
document.removeEventListener("mousemove", this.onMouseMove);
|
||||
}
|
||||
|
||||
private renderTooltip() {
|
||||
const { contentRect, visible } = this.state;
|
||||
if (!visible) {
|
||||
ReactDOM.unmountComponentAtNode(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 targetRight = targetRect.right + 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: Partial<IRect> = {};
|
||||
let chevronFace: ChevronFace = null;
|
||||
if (this.isOnTheSide) {
|
||||
if (this.onLeftOfTarget()) {
|
||||
position.left = targetLeft;
|
||||
chevronFace = ChevronFace.Right;
|
||||
} else {
|
||||
position.left = targetRight;
|
||||
chevronFace = ChevronFace.Left;
|
||||
}
|
||||
|
||||
position.top = targetTop;
|
||||
} else {
|
||||
if (this.aboveTarget()) {
|
||||
position.bottom = UIStore.instance.windowHeight - targetTop;
|
||||
chevronFace = ChevronFace.Bottom;
|
||||
} else {
|
||||
position.top = targetBottom;
|
||||
chevronFace = 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 === ChevronFace.Top,
|
||||
'mx_InteractiveTooltip_withChevron_left': chevronFace === ChevronFace.Left,
|
||||
'mx_InteractiveTooltip_withChevron_right': chevronFace === ChevronFace.Right,
|
||||
'mx_InteractiveTooltip_withChevron_bottom': chevronFace === ChevronFace.Bottom,
|
||||
});
|
||||
|
||||
const menuStyle: CSSProperties = {};
|
||||
if (contentRect && !this.isOnTheSide) {
|
||||
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() {
|
||||
return this.props.children({
|
||||
ref: this.collectTarget,
|
||||
onMouseOver: this.onTargetMouseOver,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -19,7 +19,6 @@ limitations under the License.
|
|||
import React, { ComponentProps } from 'react';
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
|
@ -31,7 +30,8 @@ import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
|
|||
import { Action } from '../../../dispatcher/actions';
|
||||
import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
|
||||
import { jsxJoin } from '../../../utils/ReactUtils';
|
||||
import { Layout } from '../../../settings/Layout';
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
import { Layout } from '../../../settings/enums/Layout';
|
||||
|
||||
const onPinnedMessagesClick = (): void => {
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
|
|
180
src/components/views/elements/PollCreateDialog.tsx
Normal file
180
src/components/views/elements/PollCreateDialog.tsx
Normal file
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import ScrollableBaseModal, { IScrollableBaseState } from "../dialogs/ScrollableBaseModal";
|
||||
import { IDialogProps } from "../dialogs/IDialogProps";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import React, { ChangeEvent, createRef } from "react";
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { arrayFastClone, arraySeed } from "../../../utils/arrays";
|
||||
import Field from "./Field";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import { makePollContent, POLL_KIND_DISCLOSED, POLL_START_EVENT_TYPE } from "../../../polls/consts";
|
||||
import Spinner from "./Spinner";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
room: Room;
|
||||
}
|
||||
|
||||
interface IState extends IScrollableBaseState {
|
||||
question: string;
|
||||
options: string[];
|
||||
busy: boolean;
|
||||
}
|
||||
|
||||
const MIN_OPTIONS = 2;
|
||||
const MAX_OPTIONS = 20;
|
||||
const DEFAULT_NUM_OPTIONS = 2;
|
||||
const MAX_QUESTION_LENGTH = 340;
|
||||
const MAX_OPTION_LENGTH = 340;
|
||||
|
||||
export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState> {
|
||||
private addOptionRef = createRef<HTMLDivElement>();
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
title: _t("Create poll"),
|
||||
actionLabel: _t("Create Poll"),
|
||||
canSubmit: false, // need to add a question and at least one option first
|
||||
|
||||
question: "",
|
||||
options: arraySeed("", DEFAULT_NUM_OPTIONS),
|
||||
busy: false,
|
||||
};
|
||||
}
|
||||
|
||||
private checkCanSubmit() {
|
||||
this.setState({
|
||||
canSubmit:
|
||||
!this.state.busy &&
|
||||
this.state.question.trim().length > 0 &&
|
||||
this.state.options.filter(op => op.trim().length > 0).length >= MIN_OPTIONS,
|
||||
});
|
||||
}
|
||||
|
||||
private onQuestionChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ question: e.target.value }, () => this.checkCanSubmit());
|
||||
};
|
||||
|
||||
private onOptionChange = (i: number, e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newOptions = arrayFastClone(this.state.options);
|
||||
newOptions[i] = e.target.value;
|
||||
this.setState({ options: newOptions }, () => this.checkCanSubmit());
|
||||
};
|
||||
|
||||
private onOptionRemove = (i: number) => {
|
||||
const newOptions = arrayFastClone(this.state.options);
|
||||
newOptions.splice(i, 1);
|
||||
this.setState({ options: newOptions }, () => this.checkCanSubmit());
|
||||
};
|
||||
|
||||
private onOptionAdd = () => {
|
||||
const newOptions = arrayFastClone(this.state.options);
|
||||
newOptions.push("");
|
||||
this.setState({ options: newOptions }, () => {
|
||||
// Scroll the button into view after the state update to ensure we don't experience
|
||||
// a pop-in effect, and to avoid the button getting cut off due to a mid-scroll render.
|
||||
this.addOptionRef.current?.scrollIntoView?.();
|
||||
});
|
||||
};
|
||||
|
||||
protected submit(): void {
|
||||
this.setState({ busy: true, canSubmit: false });
|
||||
this.matrixClient.sendEvent(
|
||||
this.props.room.roomId,
|
||||
POLL_START_EVENT_TYPE.name,
|
||||
makePollContent(
|
||||
this.state.question, this.state.options, POLL_KIND_DISCLOSED.name,
|
||||
),
|
||||
).then(
|
||||
() => this.props.onFinished(true),
|
||||
).catch(e => {
|
||||
console.error("Failed to post poll:", e);
|
||||
Modal.createTrackedDialog(
|
||||
'Failed to post poll',
|
||||
'',
|
||||
QuestionDialog,
|
||||
{
|
||||
title: _t("Failed to post poll"),
|
||||
description: _t(
|
||||
"Sorry, the poll you tried to create was not posted."),
|
||||
button: _t('Try again'),
|
||||
cancelButton: _t('Cancel'),
|
||||
onFinished: (tryAgain: boolean) => {
|
||||
if (!tryAgain) {
|
||||
this.cancel();
|
||||
} else {
|
||||
this.setState({ busy: false, canSubmit: true });
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
protected cancel(): void {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
protected renderContent(): React.ReactNode {
|
||||
return <div className="mx_PollCreateDialog">
|
||||
<h2>{ _t("What is your poll question or topic?") }</h2>
|
||||
<Field
|
||||
value={this.state.question}
|
||||
maxLength={MAX_QUESTION_LENGTH}
|
||||
label={_t("Question or topic")}
|
||||
placeholder={_t("Write something...")}
|
||||
onChange={this.onQuestionChange}
|
||||
usePlaceholderAsHint={true}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
<h2>{ _t("Create options") }</h2>
|
||||
{
|
||||
this.state.options.map((op, i) => <div key={`option_${i}`} className="mx_PollCreateDialog_option">
|
||||
<Field
|
||||
value={op}
|
||||
maxLength={MAX_OPTION_LENGTH}
|
||||
label={_t("Option %(number)s", { number: i + 1 })}
|
||||
placeholder={_t("Write an option")}
|
||||
onChange={e => this.onOptionChange(i, e)}
|
||||
usePlaceholderAsHint={true}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
<AccessibleButton
|
||||
onClick={() => this.onOptionRemove(i)}
|
||||
className="mx_PollCreateDialog_removeOption"
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</div>)
|
||||
}
|
||||
<AccessibleButton
|
||||
onClick={this.onOptionAdd}
|
||||
disabled={this.state.busy || this.state.options.length >= MAX_OPTIONS}
|
||||
kind="secondary"
|
||||
className="mx_PollCreateDialog_addOption"
|
||||
inputRef={this.addOptionRef}
|
||||
>{ _t("Add option") }</AccessibleButton>
|
||||
{
|
||||
this.state.busy &&
|
||||
<div className="mx_PollCreateDialog_busy"><Spinner /></div>
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -17,25 +17,25 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import escapeHtml from "escape-html";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { RelationType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { Layout } from "../../../settings/Layout";
|
||||
import { Layout } from "../../../settings/enums/Layout";
|
||||
import escapeHtml from "escape-html";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { PERMITTED_URL_SCHEMES } from "../../../HtmlUtils";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Spinner from './Spinner';
|
||||
import ReplyTile from "../rooms/ReplyTile";
|
||||
import Pill from './Pill';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { RelationType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
||||
/**
|
||||
* This number is based on the previous behavior - if we have message of height
|
||||
|
@ -110,6 +110,8 @@ export default class ReplyChain extends React.Component<IProps, IState> {
|
|||
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
|
||||
const mInReplyTo = mRelatesTo['m.in_reply_to'];
|
||||
if (mInReplyTo && mInReplyTo['event_id']) return mInReplyTo['event_id'];
|
||||
} else if (!SettingsStore.getValue("feature_thread") && ev.isThreadRelation) {
|
||||
return ev.threadRootId;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -53,8 +53,10 @@ const onHelpClick = () => {
|
|||
};
|
||||
|
||||
const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }: IProps) => {
|
||||
const disableCustomUrls = SdkConfig.get()["disable_custom_urls"];
|
||||
|
||||
let editBtn;
|
||||
if (!SdkConfig.get()["disable_custom_urls"] && onServerConfigChange) {
|
||||
if (!disableCustomUrls && onServerConfigChange) {
|
||||
const onClick = () => {
|
||||
showPickerDialog(dialogTitle, serverConfig, (config?: ValidatedServerConfig) => {
|
||||
if (config) {
|
||||
|
@ -83,7 +85,7 @@ const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }
|
|||
|
||||
return <div className="mx_ServerPicker">
|
||||
<h3>{ title || _t("Homeserver") }</h3>
|
||||
<AccessibleButton className="mx_ServerPicker_help" onClick={onHelpClick} />
|
||||
{ !disableCustomUrls ? <AccessibleButton className="mx_ServerPicker_help" onClick={onHelpClick} /> : null }
|
||||
<span className="mx_ServerPicker_server">{ serverName }</span>
|
||||
{ editBtn }
|
||||
{ desc }
|
||||
|
|
|
@ -18,8 +18,15 @@ import React from "react";
|
|||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import classnames from 'classnames';
|
||||
|
||||
export enum CheckboxStyle {
|
||||
Solid = "solid",
|
||||
Outline = "outline",
|
||||
}
|
||||
|
||||
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
kind?: CheckboxStyle;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -41,13 +48,21 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
|
|||
|
||||
public render() {
|
||||
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
||||
const { children, className, ...otherProps } = this.props;
|
||||
return <span className={"mx_Checkbox " + className}>
|
||||
const { children, className, kind = CheckboxStyle.Solid, ...otherProps } = this.props;
|
||||
const newClassName = classnames(
|
||||
"mx_Checkbox",
|
||||
className,
|
||||
{
|
||||
"mx_Checkbox_hasKind": kind,
|
||||
[`mx_Checkbox_kind_${kind}`]: kind,
|
||||
},
|
||||
);
|
||||
return <span className={newClassName}>
|
||||
<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={require("../../../../res/img/feather-customised/check.svg")} />
|
||||
<div className="mx_Checkbox_checkmark" />
|
||||
</div>
|
||||
<div>
|
||||
{ this.props.children }
|
||||
|
|
|
@ -40,13 +40,13 @@ export default class StyledRadioButton extends React.PureComponent<IProps, IStat
|
|||
public render() {
|
||||
const { children, className, disabled, outlined, childrenInLabel, ...otherProps } = this.props;
|
||||
const _className = classnames(
|
||||
'mx_RadioButton',
|
||||
'mx_StyledRadioButton',
|
||||
className,
|
||||
{
|
||||
"mx_RadioButton_disabled": disabled,
|
||||
"mx_RadioButton_enabled": !disabled,
|
||||
"mx_RadioButton_checked": this.props.checked,
|
||||
"mx_RadioButton_outlined": outlined,
|
||||
"mx_StyledRadioButton_disabled": disabled,
|
||||
"mx_StyledRadioButton_enabled": !disabled,
|
||||
"mx_StyledRadioButton_checked": this.props.checked,
|
||||
"mx_StyledRadioButton_outlined": outlined,
|
||||
});
|
||||
|
||||
const radioButton = <React.Fragment>
|
||||
|
@ -58,16 +58,16 @@ export default class StyledRadioButton extends React.PureComponent<IProps, IStat
|
|||
if (childrenInLabel) {
|
||||
return <label className={_className}>
|
||||
{ radioButton }
|
||||
<div className="mx_RadioButton_content">{ children }</div>
|
||||
<div className="mx_RadioButton_spacer" />
|
||||
<div className="mx_StyledRadioButton_content">{ children }</div>
|
||||
<div className="mx_StyledRadioButton_spacer" />
|
||||
</label>;
|
||||
} else {
|
||||
return <div className={_className}>
|
||||
<label className="mx_RadioButton_innerLabel">
|
||||
<label className="mx_StyledRadioButton_innerLabel">
|
||||
{ radioButton }
|
||||
</label>
|
||||
<div className="mx_RadioButton_content">{ children }</div>
|
||||
<div className="mx_RadioButton_spacer" />
|
||||
<div className="mx_StyledRadioButton_content">{ children }</div>
|
||||
<div className="mx_StyledRadioButton_spacer" />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,12 @@ import classNames from "classnames";
|
|||
|
||||
type Data = Pick<IFieldState, "value" | "allowEmpty">;
|
||||
|
||||
interface IResult {
|
||||
key: string;
|
||||
valid: boolean;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface IRule<T, D = void> {
|
||||
key: string;
|
||||
final?: boolean;
|
||||
|
@ -32,7 +38,7 @@ interface IRule<T, D = void> {
|
|||
|
||||
interface IArgs<T, D = void> {
|
||||
rules: IRule<T, D>[];
|
||||
description?(this: T, derivedData: D): React.ReactChild;
|
||||
description?(this: T, derivedData: D, results: IResult[]): React.ReactChild;
|
||||
hideDescriptionIfValid?: boolean;
|
||||
deriveData?(data: Data): Promise<D>;
|
||||
}
|
||||
|
@ -88,7 +94,7 @@ export default function withValidation<T = undefined, D = void>({
|
|||
const data = { value, allowEmpty };
|
||||
const derivedData = deriveData ? await deriveData(data) : undefined;
|
||||
|
||||
const results = [];
|
||||
const results: IResult[] = [];
|
||||
let valid = true;
|
||||
if (rules && rules.length) {
|
||||
for (const rule of rules) {
|
||||
|
@ -164,8 +170,8 @@ export default function withValidation<T = undefined, D = void>({
|
|||
if (description && (details || !hideDescriptionIfValid)) {
|
||||
// We're setting `this` to whichever component holds the validation
|
||||
// function. That allows rules to access the state of the component.
|
||||
const content = description.call(this, derivedData);
|
||||
summary = <div className="mx_Validation_description">{ content }</div>;
|
||||
const content = description.call(this, derivedData, results);
|
||||
summary = content ? <div className="mx_Validation_description">{ content }</div> : undefined;
|
||||
}
|
||||
|
||||
let feedback;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue