Merge branch 'develop' into feature_confetti#14676

This commit is contained in:
Steffen Kolmer 2020-11-26 18:21:28 +01:00 committed by GitHub
commit 27312c3553
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
236 changed files with 13547 additions and 3882 deletions

View file

@ -39,7 +39,7 @@ interface IProps extends React.InputHTMLAttributes<Element> {
tabIndex?: number;
disabled?: boolean;
className?: string;
onClick?(e?: ButtonEvent): void;
onClick(e?: ButtonEvent): void;
}
interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> {

View file

@ -46,7 +46,7 @@ export default class AddressSelector extends React.Component {
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(props) {
UNSAFE_componentWillReceiveProps(props) { // eslint-disable-line camelcase
// Make sure the selected item isn't outside the list bounds
const selected = this.state.selected;
const maxSelected = this._maxSelected(props.addressList);

View file

@ -23,7 +23,6 @@ import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import AccessibleButton from './AccessibleButton';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import AppPermission from './AppPermission';
import AppWarning from './AppWarning';
import Spinner from './Spinner';
@ -61,6 +60,7 @@ export default class AppTile extends React.Component {
// This is a function to make the impact of calling SettingsStore slightly less
hasPermissionToLoad = (props) => {
if (this._usingLocalWidget()) return true;
if (!props.room) return true; // user widgets always have permissions
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
if (currentlyAllowedWidgets[props.app.eventId] === undefined) {
@ -335,6 +335,7 @@ export default class AppTile extends React.Component {
</div>
);
if (!this.state.hasPermissionToLoad) {
// only possible for room widgets, can assert this.props.room here
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
appTileBody = (
<div className={appTileBodyClass}>
@ -373,19 +374,18 @@ export default class AppTile extends React.Component {
/>
</div>
);
// if the widget would be allowed to remain on screen, we must put it in
// a PersistedElement from the get-go, otherwise the iframe will be
// re-mounted later when we do.
if (this.props.whitelistCapabilities.includes('m.always_on_screen')) {
const PersistedElement = sdk.getComponent("elements.PersistedElement");
// Also wrap the PersistedElement in a div to fix the height, otherwise
// AppTile's border is in the wrong place
appTileBody = <div className="mx_AppTile_persistedWrapper">
<PersistedElement persistKey={this._persistKey}>
{appTileBody}
</PersistedElement>
</div>;
}
// all widgets can theoretically be allowed to remain on screen, so we wrap
// them all in a PersistedElement from the get-go. If we wait, the iframe will
// be re-mounted later, which means the widget has to start over, which is bad.
// Also wrap the PersistedElement in a div to fix the height, otherwise
// AppTile's border is in the wrong place
appTileBody = <div className="mx_AppTile_persistedWrapper">
<PersistedElement persistKey={this._persistKey}>
{appTileBody}
</PersistedElement>
</div>;
}
}
@ -446,7 +446,9 @@ AppTile.displayName = 'AppTile';
AppTile.propTypes = {
app: PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
// 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: PropTypes.object,
// 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: PropTypes.bool,
@ -470,10 +472,6 @@ AppTile.propTypes = {
handleMinimisePointerEvents: PropTypes.bool,
// Optionally hide the popout widget icon
showPopout: PropTypes.bool,
// Widget capabilities to allow by default (without user confirmation)
// NOTE -- Use with caution. This is intended to aid better integration / UX
// basic widget capabilities, e.g. injecting sticker message events.
whitelistCapabilities: PropTypes.array,
// Is this an instance of a user widget
userWidget: PropTypes.bool,
};
@ -484,7 +482,6 @@ AppTile.defaultProps = {
showTitle: true,
showPopout: true,
handleMinimisePointerEvents: false,
whitelistCapabilities: [],
userWidget: false,
miniMode: false,
};

View file

@ -54,6 +54,9 @@ export default class DialogButtons extends React.Component {
// disables only the primary button
primaryDisabled: PropTypes.bool,
// something to stick next to the buttons, optionally
additive: PropTypes.element,
};
static defaultProps = {
@ -85,8 +88,14 @@ export default class DialogButtons extends React.Component {
</button>;
}
let additive = null;
if (this.props.additive) {
additive = <div className="mx_Dialog_buttons_additive">{this.props.additive}</div>;
}
return (
<div className="mx_Dialog_buttons">
{ additive }
{ cancelButton }
{ this.props.children }
<button type={this.props.primaryIsSubmit ? 'submit' : 'button'}

View file

@ -16,13 +16,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default class DirectorySearchBox extends React.Component {
constructor() {
super();
constructor(props) {
super(props);
this._collectInput = this._collectInput.bind(this);
this._onClearClick = this._onClearClick.bind(this);
this._onChange = this._onChange.bind(this);
@ -32,7 +31,7 @@ export default class DirectorySearchBox extends React.Component {
this.input = null;
this.state = {
value: '',
value: this.props.initialText || '',
};
}
@ -78,28 +77,33 @@ export default class DirectorySearchBox extends React.Component {
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const searchbox_classes = {
const searchboxClasses = {
mx_DirectorySearchBox: true,
};
searchbox_classes[this.props.className] = true;
searchboxClasses[this.props.className] = true;
let join_button;
let joinButton;
if (this.props.showJoinButton) {
join_button = <AccessibleButton className="mx_DirectorySearchBox_joinButton"
joinButton = <AccessibleButton className="mx_DirectorySearchBox_joinButton"
onClick={this._onJoinButtonClick}
>{_t("Join")}</AccessibleButton>;
}
return <div className={`mx_DirectorySearchBox ${this.props.className} mx_textinput`}>
<input type="text" name="dirsearch" value={this.state.value}
className="mx_textinput_icon mx_textinput_search"
ref={this._collectInput}
onChange={this._onChange} onKeyUp={this._onKeyUp}
placeholder={this.props.placeholder} autoFocus
/>
{ join_button }
<AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this._onClearClick}></AccessibleButton>
</div>;
<input
type="text"
name="dirsearch"
value={this.state.value}
className="mx_textinput_icon mx_textinput_search"
ref={this._collectInput}
onChange={this._onChange}
onKeyUp={this._onKeyUp}
placeholder={this.props.placeholder}
autoFocus
/>
{ joinButton }
<AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} />
</div>;
}
}
@ -110,4 +114,5 @@ DirectorySearchBox.propTypes = {
onJoinClick: PropTypes.func,
placeholder: PropTypes.string,
showJoinButton: PropTypes.bool,
initialText: PropTypes.string,
};

View file

@ -64,7 +64,7 @@ interface IProps {
// All other props pass through to the <input>.
}
interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
export 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.

View file

@ -1,34 +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 PropTypes from 'prop-types';
import AccessibleButton from "./AccessibleButton";
export default function IconButton(props) {
const {icon, className, ...restProps} = props;
let newClassName = (className || "") + " mx_IconButton";
newClassName = newClassName + " mx_IconButton_icon_" + icon;
const allProps = Object.assign({}, restProps, {className: newClassName});
return React.createElement(AccessibleButton, allProps);
}
IconButton.propTypes = Object.assign({
icon: PropTypes.string,
}, AccessibleButton.propTypes);

View file

@ -0,0 +1,94 @@
/*
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, {useContext, useRef, useState} from 'react';
import classNames from 'classnames';
import AccessibleButton from "./AccessibleButton";
import Tooltip from './Tooltip';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useTimeout} from "../../../hooks/useTimeout";
import Analytics from "../../../Analytics";
import CountlyAnalytics from '../../../CountlyAnalytics';
export const AVATAR_SIZE = 52;
interface IProps {
hasAvatar: boolean;
noAvatarLabel?: string;
hasAvatarLabel?: string;
setAvatarUrl(url: string): Promise<void>;
}
const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, children }) => {
const cli = useContext(MatrixClientContext);
const [busy, setBusy] = useState(false);
const [hover, setHover] = useState(false);
const [show, setShow] = useState(false);
useTimeout(() => {
setShow(true);
}, 3000); // show after 3 seconds
useTimeout(() => {
setShow(false);
}, 13000); // hide after being shown for 10 seconds
const uploadRef = useRef<HTMLInputElement>();
const label = (hasAvatar || busy) ? hasAvatarLabel : noAvatarLabel;
return <React.Fragment>
<input
type="file"
ref={uploadRef}
className="mx_MiniAvatarUploader_input"
onChange={async (ev) => {
if (!ev.target.files?.length) return;
setBusy(true);
Analytics.trackEvent("mini_avatar", "upload");
CountlyAnalytics.instance.track("mini_avatar_upload");
const file = ev.target.files[0];
const uri = await cli.uploadContent(file);
await setAvatarUrl(uri);
setBusy(false);
}}
accept="image/*"
/>
<AccessibleButton
className={classNames("mx_MiniAvatarUploader", {
mx_MiniAvatarUploader_busy: busy,
mx_MiniAvatarUploader_hasAvatar: hasAvatar,
})}
disabled={busy}
onClick={() => {
uploadRef.current.click();
}}
onMouseOver={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
{ children }
<Tooltip
label={label}
visible={!!label && (hover || show)}
forceOnRight
/>
</AccessibleButton>
</React.Fragment>;
};
export default MiniAvatarUploader;

View file

@ -21,6 +21,8 @@ import {throttle} from "lodash";
import ResizeObserver from 'resize-observer-polyfill';
import dis from '../../../dispatcher/dispatcher';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
@ -144,9 +146,11 @@ export default class PersistedElement extends React.Component {
}
renderApp() {
const content = <div ref={this.collectChild} style={this.props.style}>
{this.props.children}
</div>;
const content = <MatrixClientContext.Provider value={MatrixClientPeg.get()}>
<div ref={this.collectChild} style={this.props.style}>
{this.props.children}
</div>
</MatrixClientContext.Provider>;
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
}

View file

@ -71,7 +71,6 @@ export default class PersistentApp extends React.Component {
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
persistentWidgetInRoomId, appEvent.getId(),
);
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId);
const AppTile = sdk.getComponent('elements.AppTile');
return <AppTile
key={app.id}
@ -82,7 +81,6 @@ export default class PersistentApp extends React.Component {
creatorUserId={app.creatorUserId}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
whitelistCapabilities={capWhitelist}
miniMode={true}
showMenubar={false}
/>;

View file

@ -32,7 +32,8 @@ 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): React.ReactChild;
hideDescriptionIfValid?: boolean;
deriveData?(data: Data): Promise<D>;
}
@ -54,6 +55,8 @@ export interface IValidationResult {
* @param {Function} description
* Function that returns a string summary of the kind of value that will
* meet the validation rules. Shown at the top of the validation feedback.
* @param {Boolean} hideDescriptionIfValid
* If true, don't show the description if the validation passes validation.
* @param {Function} deriveData
* Optional function that returns a Promise to an object of generic type D.
* The result of this Promise is passed to rule methods `skip`, `test`, `valid`, and `invalid`.
@ -71,7 +74,9 @@ export interface IValidationResult {
* 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<T = undefined, D = void>({ description, deriveData, rules }: IArgs<T, D>) {
export default function withValidation<T = undefined, D = void>({
description, hideDescriptionIfValid, deriveData, rules,
}: IArgs<T, D>) {
return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise<IValidationResult> {
if (!value && allowEmpty) {
return {
@ -156,7 +161,7 @@ export default function withValidation<T = undefined, D = void>({ description, d
}
let summary;
if (description) {
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);