Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into soru/spoilers

This commit is contained in:
Sorunome 2019-08-28 20:23:24 +02:00
commit fe9ae46ffb
No known key found for this signature in database
GPG key ID: 63E31F7B5993A9C4
389 changed files with 23061 additions and 5544 deletions

View file

@ -0,0 +1,63 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import AccessibleButton from "./AccessibleButton";
import sdk from "../../../index";
export default class AccessibleTooltipButton extends React.PureComponent {
static propTypes = {
...AccessibleButton.propTypes,
// The tooltip to render on hover
title: PropTypes.string.isRequired,
};
state = {
hover: false,
};
onMouseOver = () => {
this.setState({
hover: true,
});
};
onMouseOut = () => {
this.setState({
hover: false,
});
};
render() {
const Tooltip = sdk.getComponent("elements.Tooltip");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const {title, ...props} = this.props;
const tip = this.state.hover ? <Tooltip
className="mx_AccessibleTooltipButton_container"
tooltipClassName="mx_AccessibleTooltipButton_tooltip"
label={title}
/> : <div />;
return (
<AccessibleButton {...props} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
{ tip }
</AccessibleButton>
);
}
}

View file

@ -1,3 +1,21 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector 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 url from 'url';
@ -50,7 +68,10 @@ export default class AppPermission extends React.Component {
<img src={require("../../../../res/img/feather-customised/warning-triangle.svg")} alt={_t('Warning!')} />
</div>
<div className='mx_AppPermissionWarningText'>
<span className='mx_AppPermissionWarningTextLabel'>{ _t('Do you want to load widget from URL:') }</span> <span className='mx_AppPermissionWarningTextURL'>{ this.state.curlBase }</span>
<span className='mx_AppPermissionWarningTextLabel'>{_t('Do you want to load widget from URL:')}</span>
<span className='mx_AppPermissionWarningTextURL'
title={this.state.curlBase}
>{this.state.curlBase}</span>
{ e2eWarningText }
{ cookieWarning }
</div>

View file

@ -1,6 +1,7 @@
/**
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,14 +16,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import url from 'url';
import qs from 'querystring';
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import ScalarAuthClient from '../../../ScalarAuthClient';
import WidgetMessaging from '../../../WidgetMessaging';
import AccessibleButton from './AccessibleButton';
import Modal from '../../../Modal';
@ -35,6 +33,8 @@ import WidgetUtils from '../../../utils/WidgetUtils';
import dis from '../../../dispatcher';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import classNames from 'classnames';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;
@ -139,7 +139,10 @@ export default class AppTile extends React.Component {
}
componentWillMount() {
this.setScalarToken();
// Only fetch IM token on mount if we're showing and have permission to load
if (this.props.show && this.state.hasPermissionToLoad) {
this.setScalarToken();
}
}
componentDidMount() {
@ -153,7 +156,7 @@ export default class AppTile extends React.Component {
// if it's not remaining on screen, get rid of the PersistedElement container
if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) {
ActiveWidgetStore.destroyPersistentWidget();
ActiveWidgetStore.destroyPersistentWidget(this.props.id);
const PersistedElement = sdk.getComponent("elements.PersistedElement");
PersistedElement.destroyElement(this._persistKey);
}
@ -164,8 +167,6 @@ export default class AppTile extends React.Component {
* Component initialisation is only complete when this function has resolved
*/
setScalarToken() {
this.setState({initialising: true});
if (!WidgetUtils.isScalarUrl(this.props.url)) {
console.warn('Non-scalar widget, not setting scalar token!', url);
this.setState({
@ -176,9 +177,22 @@ export default class AppTile extends React.Component {
return;
}
const managers = IntegrationManagers.sharedInstance();
if (!managers.hasManager()) {
console.warn("No integration manager - not setting scalar token", url);
this.setState({
error: null,
widgetUrl: this._addWurlParams(this.props.url),
initialising: false,
});
return;
}
// TODO: Pick the right manager for the widget
// Fetch the token before loading the iframe as we need it to mangle the URL
if (!this._scalarClient) {
this._scalarClient = new ScalarAuthClient();
this._scalarClient = managers.getPrimaryManager().getScalarClient();
}
this._scalarClient.getScalarToken().done((token) => {
// Append scalar_token as a query param if not already present
@ -187,7 +201,7 @@ export default class AppTile extends React.Component {
const params = qs.parse(u.query);
if (!params.scalar_token) {
params.scalar_token = encodeURIComponent(token);
// u.search must be set to undefined, so that u.format() uses query paramerters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
// u.search must be set to undefined, so that u.format() uses query parameters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
u.search = undefined;
u.query = params;
}
@ -214,11 +228,20 @@ export default class AppTile extends React.Component {
componentWillReceiveProps(nextProps) {
if (nextProps.url !== this.props.url) {
this._getNewState(nextProps);
this.setScalarToken();
} else if (nextProps.show && !this.props.show && this.props.waitForIframeLoad) {
this.setState({
loading: true,
});
// Fetch IM token for new URL if we're showing and have permission to load
if (this.props.show && this.state.hasPermissionToLoad) {
this.setScalarToken();
}
} else if (nextProps.show && !this.props.show) {
if (this.props.waitForIframeLoad) {
this.setState({
loading: true,
});
}
// Fetch IM token now that we're showing if we already have permission to load
if (this.state.hasPermissionToLoad) {
this.setScalarToken();
}
} else if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) {
this.setState({
widgetPageTitle: nextProps.widgetPageTitle,
@ -240,19 +263,20 @@ export default class AppTile extends React.Component {
if (this.props.onEditClick) {
this.props.onEditClick();
} else {
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
this._scalarClient.connect().done(() => {
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
this.props.room, 'type_' + this.props.type, this.props.id);
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
src: src,
}, "mx_IntegrationsManager");
}, (err) => {
this.setState({
error: err.message,
});
console.error('Error ensuring a valid scalar_token exists', err);
});
// TODO: Open the right manager for the widget
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll(
this.props.room,
'type_' + this.props.type,
this.props.id,
);
} else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(
this.props.room,
'type_' + this.props.type,
this.props.id,
);
}
}
}
@ -416,6 +440,8 @@ export default class AppTile extends React.Component {
console.warn('Granting permission to load widget - ', this.state.widgetUrl);
localStorage.setItem(this.state.widgetPermissionId, true);
this.setState({hasPermissionToLoad: true});
// Now that we have permission, fetch the IM token
this.setScalarToken();
}
_revokeWidgetPermission() {
@ -424,7 +450,7 @@ export default class AppTile extends React.Component {
this.setState({hasPermissionToLoad: false});
// Force the widget to be non-persistent
ActiveWidgetStore.destroyPersistentWidget();
ActiveWidgetStore.destroyPersistentWidget(this.props.id);
const PersistedElement = sdk.getComponent("elements.PersistedElement");
PersistedElement.destroyElement(this._persistKey);
}
@ -531,13 +557,24 @@ export default class AppTile extends React.Component {
<MessageSpinner msg='Loading...' />
</div>
);
if (this.state.initialising) {
if (!this.state.hasPermissionToLoad) {
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
appTileBody = (
<div className={appTileBodyClass}>
<AppPermission
isRoomEncrypted={isRoomEncrypted}
url={this.state.widgetUrl}
onPermissionGranted={this._grantWidgetPermission}
/>
</div>
);
} else if (this.state.initialising) {
appTileBody = (
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
{ loadingElement }
</div>
);
} else if (this.state.hasPermissionToLoad == true) {
} else {
if (this.isMixedContent()) {
appTileBody = (
<div className={appTileBodyClass}>
@ -559,11 +596,10 @@ export default class AppTile extends React.Component {
src={this._getSafeUrl()}
allowFullScreen="true"
sandbox={sandboxFlags}
onLoad={this._onLoaded}
></iframe>
onLoad={this._onLoaded} />
</div>
);
// if the widget would be allowed to remian on screen, we must put it in
// 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')) {
@ -577,17 +613,6 @@ export default class AppTile extends React.Component {
</div>;
}
}
} else {
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
appTileBody = (
<div className={appTileBodyClass}>
<AppPermission
isRoomEncrypted={isRoomEncrypted}
url={this.state.widgetUrl}
onPermissionGranted={this._grantWidgetPermission}
/>
</div>
);
}
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -48,7 +49,7 @@ class MenuOption extends React.Component {
});
return <div className={optClasses}
onClick={this._onClick} onKeyPress={this._onKeyPress}
onClick={this._onClick}
onMouseEnter={this._onMouseEnter}
>
{ this.props.children }
@ -58,7 +59,7 @@ class MenuOption extends React.Component {
MenuOption.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(React.PropTypes.node),
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
highlighted: PropTypes.bool,

View file

@ -88,6 +88,7 @@ export class EditableItem extends React.Component {
export default class EditableItemList extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.string).isRequired,
itemsLabel: PropTypes.string,
noItemsLabel: PropTypes.string,
@ -121,10 +122,8 @@ export default class EditableItemList extends React.Component {
return (
<form onSubmit={this._onItemAdded} autoComplete={false}
noValidate={true} className="mx_EditableItemList_newItem">
<Field id="newEmailAddress" label={this.props.placeholder}
type="text" autoComplete="off" value={this.props.newItem}
onChange={this._onNewItemChanged}
/>
<Field id={`mx_EditableItemList_new_${this.props.id}`} label={this.props.placeholder} type="text"
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged} />
<AccessibleButton onClick={this._onItemAdded} kind="primary">
{_t("Add")}
</AccessibleButton>
@ -135,11 +134,11 @@ export default class EditableItemList extends React.Component {
render() {
const editableItems = this.props.items.map((item, index) => {
if (!this.props.canRemove) {
return <li>{item}</li>;
return <li key={item}>{item}</li>;
}
return <EditableItem
key={index}
key={item}
index={index}
value={item}
onRemove={this._onItemRemoved}

View file

@ -18,7 +18,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import sdk from '../../../index';
import { throttle } from 'lodash';
import { debounce } from 'lodash';
// Invoke validation from user input (when typing, etc.) at most once every N ms.
const VALIDATION_THROTTLE_MS = 200;
@ -46,6 +46,14 @@ export default class Field extends React.PureComponent {
// 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,
// All other props pass through to the <input>.
};
@ -118,14 +126,25 @@ export default class Field extends React.PureComponent {
}
}
validateOnChange = throttle(() => {
/*
* 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() {
const { element, prefix, onValidate, children, ...inputProps } = this.props;
const {
element, prefix, onValidate, children, tooltipContent, flagInvalid,
tooltipClassName, ...inputProps} = this.props;
const inputElement = element || "input";
@ -145,23 +164,27 @@ export default class Field extends React.PureComponent {
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
}
const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined;
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, {
// 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_invalid: onValidate && this.state.valid === false,
mx_Field_invalid: hasValidationFlag
? flagInvalid
: onValidate && this.state.valid === false,
});
// Handle displaying feedback on validity
const Tooltip = sdk.getComponent("elements.Tooltip");
let tooltip;
if (this.state.feedback) {
tooltip = <Tooltip
tooltipClassName="mx_Field_tooltip"
let fieldTooltip;
if (tooltipContent || this.state.feedback) {
const addlClassName = tooltipClassName ? tooltipClassName : '';
fieldTooltip = <Tooltip
tooltipClassName={`mx_Field_tooltip ${addlClassName}`}
visible={this.state.feedbackVisible}
label={this.state.feedback}
label={tooltipContent || this.state.feedback}
/>;
}
@ -169,7 +192,7 @@ export default class Field extends React.PureComponent {
{prefixContainer}
{fieldInput}
<label htmlFor={this.props.id}>{this.props.label}</label>
{tooltip}
{fieldTooltip}
</div>;
}
}

View file

@ -134,9 +134,6 @@ Flair.propTypes = {
groups: PropTypes.arrayOf(PropTypes.string),
};
// TODO: We've decided that all components should follow this pattern, which means removing withMatrixClient and using
// this.context.matrixClient everywhere instead of this.props.matrixClient.
// See https://github.com/vector-im/riot-web/issues/4951.
Flair.contextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
};

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,7 +17,8 @@ limitations under the License.
'use strict';
const React = require('react');
import React from 'react';
import PropTypes from 'prop-types';
const MatrixClientPeg = require('../../../MatrixClientPeg');
@ -29,19 +31,19 @@ import { _t } from '../../../languageHandler';
export default class ImageView extends React.Component {
static propTypes = {
src: React.PropTypes.string.isRequired, // the source of the image being displayed
name: React.PropTypes.string, // the main title ('name') for the image
link: React.PropTypes.string, // the link (if any) applied to the name of the image
width: React.PropTypes.number, // width of the image src in pixels
height: React.PropTypes.number, // height of the image src in pixels
fileSize: React.PropTypes.number, // size of the image src in bytes
onFinished: React.PropTypes.func.isRequired, // callback when the lightbox is dismissed
src: PropTypes.string.isRequired, // the source of the image being displayed
name: PropTypes.string, // the main title ('name') for the image
link: PropTypes.string, // the link (if any) applied to the name of the image
width: PropTypes.number, // width of the image src in pixels
height: PropTypes.number, // height of the image src in pixels
fileSize: PropTypes.number, // size of the image src in bytes
onFinished: PropTypes.func.isRequired, // callback when the lightbox is dismissed
// the event (if any) that the Image is displaying. Used for event-specific stuff like
// redactions, senders, timestamps etc. Other descriptors are taken from the explicit
// properties above, which let us use lightboxes to display images which aren't associated
// with events.
mxEvent: React.PropTypes.object,
mxEvent: PropTypes.object,
};
constructor(props) {
@ -60,7 +62,7 @@ export default class ImageView extends React.Component {
}
onKeyDown = (ev) => {
if (ev.keyCode == 27) { // escape
if (ev.keyCode === 27) { // escape
ev.stopPropagation();
ev.preventDefault();
this.props.onFinished();
@ -72,7 +74,6 @@ export default class ImageView extends React.Component {
Modal.createTrackedDialog('Confirm Redact Dialog', 'Image View', ConfirmRedactDialog, {
onFinished: (proceed) => {
if (!proceed) return;
const self = this;
MatrixClientPeg.get().redactEvent(
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId(),
).catch(function(e) {
@ -153,32 +154,38 @@ export default class ImageView extends React.Component {
size = filesize(this.props.fileSize);
}
let size_res;
let sizeRes;
if (size && res) {
size_res = size + ", " + res;
sizeRes = size + ", " + res;
} else {
size_res = size || res;
sizeRes = size || res;
}
let mayRedact = false;
const showEventMeta = !!this.props.mxEvent;
let eventMeta;
if (showEventMeta) {
// Figure out the sender, defaulting to mxid
let sender = this.props.mxEvent.getSender();
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId());
if (room) {
mayRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId);
const member = room.getMember(sender);
if (member) sender = member.name;
}
eventMeta = (<div className="mx_ImageView_metadata">
{ _t('Uploaded on %(date)s by %(user)s', {date: formatDate(new Date(this.props.mxEvent.getTs())), user: sender}) }
{ _t('Uploaded on %(date)s by %(user)s', {
date: formatDate(new Date(this.props.mxEvent.getTs())),
user: sender,
}) }
</div>);
}
let eventRedact;
if (showEventMeta) {
if (mayRedact) {
eventRedact = (<div className="mx_ImageView_button" onClick={this.onRedactClick}>
{ _t('Remove') }
</div>);
@ -213,7 +220,7 @@ export default class ImageView extends React.Component {
<a className="mx_ImageView_link" href={ this.props.src } download={ this.props.name } target="_blank" rel="noopener">
<div className="mx_ImageView_download">
{ _t('Download this file') }<br />
<span className="mx_ImageView_size">{ size_res }</span>
<span className="mx_ImageView_size">{ sizeRes }</span>
</div>
</a>
{ eventRedact }

View file

@ -0,0 +1,334 @@
/*
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 {
propTypes: {
// Content to show in the tooltip
content: PropTypes.node.isRequired,
// Function to call when visibility of the tooltip changes
onVisibilityChange: PropTypes.func,
};
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 (!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 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,
});
}
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 New Vector 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.
@ -17,95 +18,40 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import classNames from 'classnames';
import SdkConfig from '../../../SdkConfig';
import ScalarAuthClient from '../../../ScalarAuthClient';
import ScalarMessaging from '../../../ScalarMessaging';
import Modal from "../../../Modal";
import { _t } from '../../../languageHandler';
import AccessibleButton from './AccessibleButton';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
export default class ManageIntegsButton extends React.Component {
constructor(props) {
super(props);
this.state = {
scalarError: null,
};
this.onManageIntegrations = this.onManageIntegrations.bind(this);
}
componentWillMount() {
ScalarMessaging.startListening();
this.scalarClient = null;
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
this.scalarClient = new ScalarAuthClient();
this.scalarClient.connect().done(() => {
this.forceUpdate();
}, (err) => {
this.setState({scalarError: err});
console.error('Error whilst initialising scalarClient for ManageIntegsButton', err);
});
}
}
componentWillUnmount() {
ScalarMessaging.stopListening();
}
onManageIntegrations(ev) {
onManageIntegrations = (ev) => {
ev.preventDefault();
if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
return;
const managers = IntegrationManagers.sharedInstance();
if (!managers.hasManager()) {
managers.openNoManagerDialog();
} else {
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
managers.openAll(this.props.room);
} else {
managers.getPrimaryManager().open(this.props.room);
}
}
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
this.scalarClient.connect().done(() => {
Modal.createDialog(IntegrationsManager, {
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room) :
null,
}, "mx_IntegrationsManager");
}, (err) => {
this.setState({scalarError: err});
console.error('Error ensuring a valid scalar_token exists', err);
});
}
};
render() {
let integrationsButton = <div />;
let integrationsWarningTriangle = <div />;
let integrationsErrorPopup = <div />;
if (this.scalarClient !== null) {
const integrationsButtonClasses = classNames({
mx_RoomHeader_button: true,
mx_RoomHeader_manageIntegsButton: true,
mx_ManageIntegsButton_error: !!this.state.scalarError,
});
if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
integrationsWarningTriangle = <img
src={require("../../../../res/img/warning.svg")}
title={_t('Integrations Error')}
width="17"
/>;
// Popup shown when hovering over integrationsButton_error (via CSS)
integrationsErrorPopup = (
<span className="mx_ManageIntegsButton_errorPopup">
{ _t('Could not connect to the integration server') }
</span>
);
}
if (IntegrationManagers.sharedInstance().hasManager()) {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
integrationsButton = (
<AccessibleButton className={integrationsButtonClasses}
<AccessibleButton
className='mx_RoomHeader_button mx_RoomHeader_manageIntegsButton'
title={_t("Manage Integrations")}
onClick={this.onManageIntegrations}
title={_t('Manage Integrations')}
>
{ integrationsWarningTriangle }
{ integrationsErrorPopup }
</AccessibleButton>
/>
);
}

View file

@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -17,7 +18,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
@ -277,6 +277,11 @@ module.exports = React.createClass({
? _t("%(severalUsers)schanged their avatar %(count)s times", { severalUsers: "", count: repeats })
: _t("%(oneUser)schanged their avatar %(count)s times", { oneUser: "", count: repeats });
break;
case "no_change":
res = (userCount > 1)
? _t("%(severalUsers)smade no changes %(count)s times", { severalUsers: "", count: repeats })
: _t("%(oneUser)smade no changes %(count)s times", { oneUser: "", count: repeats });
break;
}
return res;
@ -308,6 +313,11 @@ module.exports = React.createClass({
* if a transition is not recognised.
*/
_getTransition: function(e) {
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
// Handle 3pid invites the same as invites so they get bundled together
return 'invited';
}
switch (e.mxEvent.getContent().membership) {
case 'invite': return 'invited';
case 'ban': return 'banned';
@ -321,7 +331,7 @@ module.exports = React.createClass({
return 'changed_avatar';
}
// console.log("MELS ignoring duplicate membership join event");
return null;
return 'no_change';
} else {
return 'joined';
}
@ -335,8 +345,8 @@ module.exports = React.createClass({
switch (e.mxEvent.getPrevContent().membership) {
case 'invite': return 'invite_withdrawal';
case 'ban': return 'unbanned';
case 'join': return 'kicked';
default: return 'left';
// sender is not target and made the target leave, if not from invite/ban then this is a kick
default: return 'kicked';
}
default: return null;
}
@ -422,9 +432,17 @@ module.exports = React.createClass({
userEvents[userId] = [];
if (e.target) avatarMembers.push(e.target);
}
let displayName = userId;
if (e.getType() === 'm.room.third_party_invite') {
displayName = e.getContent().display_name;
} else if (e.target) {
displayName = e.target.name;
}
userEvents[userId].push({
mxEvent: e,
displayName: (e.target ? e.target.name : null) || userId,
displayName,
index: index,
});
});

View file

@ -1,198 +0,0 @@
/*
Copyright 2019 New Vector 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 sdk from '../../../index';
import {_t} from '../../../languageHandler';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher';
import EditorModel from '../../../editor/model';
import {setCaretPosition} from '../../../editor/caret';
import {getCaretOffsetAndText} from '../../../editor/dom';
import {htmlSerialize, textSerialize, requiresHtml} from '../../../editor/serialize';
import {parseEvent} from '../../../editor/deserialize';
import Autocomplete from '../rooms/Autocomplete';
import {PartCreator} from '../../../editor/parts';
import {renderModel} from '../../../editor/render';
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
import classNames from 'classnames';
export default class MessageEditor extends React.Component {
static propTypes = {
// the message event being edited
event: PropTypes.instanceOf(MatrixEvent).isRequired,
};
static contextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
};
constructor(props, context) {
super(props, context);
const room = this.context.matrixClient.getRoom(this.props.event.getRoomId());
const partCreator = new PartCreator(
() => this._autocompleteRef,
query => this.setState({query}),
room,
);
this.model = new EditorModel(
parseEvent(this.props.event, room),
partCreator,
this._updateEditorState,
);
this.state = {
autoComplete: null,
room,
};
this._editorRef = null;
this._autocompleteRef = null;
}
_updateEditorState = (caret) => {
renderModel(this._editorRef, this.model);
if (caret) {
try {
setCaretPosition(this._editorRef, this.model, caret);
} catch (err) {
console.error(err);
}
}
this.setState({autoComplete: this.model.autoComplete});
}
_onInput = (event) => {
const sel = document.getSelection();
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
this.model.update(text, event.inputType, caret);
}
_onKeyDown = (event) => {
// insert newline on Shift+Enter
if (event.shiftKey && event.key === "Enter") {
event.preventDefault(); // just in case the browser does support this
document.execCommand("insertHTML", undefined, "\n");
return;
}
// autocomplete or enter to send below shouldn't have any modifier keys pressed.
if (event.metaKey || event.altKey || event.shiftKey) {
return;
}
if (this.model.autoComplete) {
const autoComplete = this.model.autoComplete;
switch (event.key) {
case "Enter":
autoComplete.onEnter(event); break;
case "ArrowUp":
autoComplete.onUpArrow(event); break;
case "ArrowDown":
autoComplete.onDownArrow(event); break;
case "Tab":
autoComplete.onTab(event); break;
case "Escape":
autoComplete.onEscape(event); break;
default:
return; // don't preventDefault on anything else
}
event.preventDefault();
} else if (event.key === "Enter") {
this._sendEdit();
event.preventDefault();
} else if (event.key === "Escape") {
this._cancelEdit();
}
}
_cancelEdit = () => {
dis.dispatch({action: "edit_event", event: null});
}
_sendEdit = () => {
const newContent = {
"msgtype": "m.text",
"body": textSerialize(this.model),
};
const contentBody = {
msgtype: newContent.msgtype,
body: ` * ${newContent.body}`,
};
if (requiresHtml(this.model)) {
newContent.format = "org.matrix.custom.html";
newContent.formatted_body = htmlSerialize(this.model);
contentBody.format = newContent.format;
contentBody.formatted_body = ` * ${newContent.formatted_body}`;
}
const content = Object.assign({
"m.new_content": newContent,
"m.relates_to": {
"rel_type": "m.replace",
"event_id": this.props.event.getId(),
},
}, contentBody);
const roomId = this.props.event.getRoomId();
this.context.matrixClient.sendMessage(roomId, content);
dis.dispatch({action: "edit_event", event: null});
}
_onAutoCompleteConfirm = (completion) => {
this.model.autoComplete.onComponentConfirm(completion);
}
_onAutoCompleteSelectionChange = (completion) => {
this.model.autoComplete.onComponentSelectionChange(completion);
}
componentDidMount() {
this._updateEditorState();
setCaretPosition(this._editorRef, this.model, this.model.getPositionAtEnd());
this._editorRef.focus();
}
render() {
let autoComplete;
if (this.state.autoComplete) {
const query = this.state.query;
const queryLen = query.length;
autoComplete = <div className="mx_MessageEditor_AutoCompleteWrapper">
<Autocomplete
ref={ref => this._autocompleteRef = ref}
query={query}
onConfirm={this._onAutoCompleteConfirm}
onSelectionChange={this._onAutoCompleteSelectionChange}
selection={{beginning: true, end: queryLen, start: queryLen}}
room={this.state.room}
/>
</div>;
}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <div className={classNames("mx_MessageEditor", this.props.className)}>
{ autoComplete }
<div
className="mx_MessageEditor_editor"
contentEditable="true"
tabIndex="1"
onInput={this._onInput}
onKeyDown={this._onKeyDown}
ref={ref => this._editorRef = ref}
></div>
<div className="mx_MessageEditor_buttons">
<AccessibleButton kind="secondary" onClick={this._cancelEdit}>{_t("Cancel")}</AccessibleButton>
<AccessibleButton kind="primary" onClick={this._sendEdit}>{_t("Save")}</AccessibleButton>
</div>
</div>;
}
}

View file

@ -113,7 +113,7 @@ module.exports = React.createClass({
this.props.onChange(parseInt(this.state.customValue), this.props.powerLevelKey);
},
onCustomKeyPress: function(event) {
onCustomKeyDown: function(event) {
if (event.key === "Enter") {
event.preventDefault();
event.stopPropagation();
@ -133,7 +133,7 @@ module.exports = React.createClass({
picker = (
<Field id={`powerSelector_custom_${this.props.powerLevelKey}`} type="number"
label={this.props.label || _t("Power level")} max={this.props.maxValue}
onBlur={this.onCustomBlur} onKeyPress={this.onCustomKeyPress} onChange={this.onCustomChange}
onBlur={this.onCustomBlur} onKeyDown={this.onCustomKeyDown} onChange={this.onCustomChange}
value={String(this.state.customValue)} disabled={this.props.disabled} />
);
} else {

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -176,6 +177,9 @@ export default class ReplyThread extends React.Component {
componentWillMount() {
this.unmounted = false;
this.room = this.context.matrixClient.getRoom(this.props.parentEv.getRoomId());
this.room.on("Room.redaction", this.onRoomRedaction);
// same event handler as Room.redaction as for both we just do forceUpdate
this.room.on("Room.redactionCancelled", this.onRoomRedaction);
this.initialize();
}
@ -185,8 +189,21 @@ export default class ReplyThread extends React.Component {
componentWillUnmount() {
this.unmounted = true;
if (this.room) {
this.room.removeListener("Room.redaction", this.onRoomRedaction);
this.room.removeListener("Room.redactionCancelled", this.onRoomRedaction);
}
}
onRoomRedaction = (ev, room) => {
if (this.unmounted) return;
// If one of the events we are rendering gets redacted, force a re-render
if (this.state.events.some(event => event.getId() === ev.getId())) {
this.forceUpdate();
}
};
async initialize() {
const {parentEv} = this.props;
// at time of making this component we checked that props.parentEv has a parentEventId
@ -298,11 +315,13 @@ export default class ReplyThread extends React.Component {
return <blockquote className="mx_ReplyThread" key={ev.getId()}>
{ dateSep }
<EventTile mxEvent={ev}
tileShape="reply"
onHeightChanged={this.props.onHeightChanged}
permalinkCreator={this.props.permalinkCreator}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
<EventTile
mxEvent={ev}
tileShape="reply"
onHeightChanged={this.props.onHeightChanged}
permalinkCreator={this.props.permalinkCreator}
isRedacted={ev.isRedacted()}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
</blockquote>;
});

View file

@ -0,0 +1,56 @@
/*
Copyright 2019 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 PropTypes from 'prop-types';
import sdk from '../../../index';
export default class TextWithTooltip extends React.Component {
static propTypes = {
class: PropTypes.string,
tooltip: PropTypes.string.isRequired,
};
constructor() {
super();
this.state = {
hover: false,
};
}
onMouseOver = () => {
this.setState({hover: true});
};
onMouseOut = () => {
this.setState({hover: false});
};
render() {
const Tooltip = sdk.getComponent("elements.Tooltip");
return (
<span onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} className={this.props.class}>
{this.props.children}
<Tooltip
label={this.props.tooltip}
visible={this.state.hover}
className={"mx_TextWithTooltip_tooltip"} />
</span>
);
}
}

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,6 +19,7 @@ limitations under the License.
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher';
import classNames from 'classnames';
@ -28,15 +30,15 @@ module.exports = React.createClass({
propTypes: {
// Class applied to the element used to position the tooltip
className: React.PropTypes.string,
className: PropTypes.string,
// Class applied to the tooltip itself
tooltipClassName: React.PropTypes.string,
tooltipClassName: PropTypes.string,
// Whether the tooltip is visible or hidden.
// The hidden state allows animating the tooltip away via CSS.
// Defaults to visible if unset.
visible: React.PropTypes.bool,
visible: PropTypes.bool,
// the react element to put into the tooltip
label: React.PropTypes.node,
label: PropTypes.node,
},
getDefaultProps() {
@ -79,6 +81,10 @@ module.exports = React.createClass({
let offset = 0;
if (parentBox.height > MIN_TOOLTIP_HEIGHT) {
offset = Math.floor((parentBox.height - MIN_TOOLTIP_HEIGHT) / 2);
} else {
// The tooltip is larger than the parent height: figure out what offset
// 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;

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 New Vector 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.
@ -18,7 +19,7 @@ import React from 'react';
import sdk from '../../../index';
module.exports = React.createClass({
displayName: 'ToolTipButton',
displayName: 'TooltipButton',
getInitialState: function() {
return {
@ -41,12 +42,12 @@ module.exports = React.createClass({
render: function() {
const Tooltip = sdk.getComponent("elements.Tooltip");
const tip = this.state.hover ? <Tooltip
className="mx_ToolTipButton_container"
tooltipClassName="mx_ToolTipButton_helpText"
className="mx_TooltipButton_container"
tooltipClassName="mx_TooltipButton_helpText"
label={this.props.helpText}
/> : <div />;
return (
<div className="mx_ToolTipButton" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} >
<div className="mx_TooltipButton" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} >
?
{ tip }
</div>

View file

@ -25,7 +25,7 @@ module.exports = React.createClass({
propTypes: {
onChange: PropTypes.func,
selected_users: PropTypes.arrayOf(React.PropTypes.string),
selected_users: PropTypes.arrayOf(PropTypes.string),
},
getDefaultProps: function() {