Merge remote-tracking branch 'origin/develop' into jryans/4s-new-key-backup
This commit is contained in:
commit
fae819dfe5
149 changed files with 3358 additions and 12421 deletions
484
src/components/structures/ContextMenu.js
Normal file
484
src/components/structures/ContextMenu.js
Normal file
|
@ -0,0 +1,484 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 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, {useRef, useState} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import {Key} from "../../Keyboard";
|
||||
import sdk from "../../index";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
// pass in a custom control as the actual body.
|
||||
|
||||
const ContextualMenuContainerId = "mx_ContextualMenu_Container";
|
||||
|
||||
function getOrCreateContainer() {
|
||||
let container = document.getElementById(ContextualMenuContainerId);
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = ContextualMenuContainerId;
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
|
||||
// Generic ContextMenu Portal wrapper
|
||||
// all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1}
|
||||
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
|
||||
export class ContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
top: PropTypes.number,
|
||||
bottom: PropTypes.number,
|
||||
left: PropTypes.number,
|
||||
right: PropTypes.number,
|
||||
menuWidth: PropTypes.number,
|
||||
menuHeight: PropTypes.number,
|
||||
chevronOffset: PropTypes.number,
|
||||
chevronFace: PropTypes.string, // top, bottom, left, right or none
|
||||
// Function to be called on menu close
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
menuPaddingTop: PropTypes.number,
|
||||
menuPaddingRight: PropTypes.number,
|
||||
menuPaddingBottom: PropTypes.number,
|
||||
menuPaddingLeft: PropTypes.number,
|
||||
zIndex: PropTypes.number,
|
||||
|
||||
// If true, insert an invisible screen-sized element behind the
|
||||
// menu that when clicked will close it.
|
||||
hasBackground: PropTypes.bool,
|
||||
|
||||
// on resize callback
|
||||
windowResize: PropTypes.func,
|
||||
|
||||
catchTab: PropTypes.bool, // whether to close the ContextMenu on TAB (default=true)
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
hasBackground: true,
|
||||
catchTab: true,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
contextMenuElem: null,
|
||||
};
|
||||
|
||||
// persist what had focus when we got initialized so we can return it after
|
||||
this.initialFocus = document.activeElement;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// return focus to the thing which had it before us
|
||||
this.initialFocus.focus();
|
||||
}
|
||||
|
||||
collectContextMenuRect = (element) => {
|
||||
// We don't need to clean up when unmounting, so ignore
|
||||
if (!element) return;
|
||||
|
||||
let first = element.querySelector('[role^="menuitem"]');
|
||||
if (!first) {
|
||||
first = element.querySelector('[tab-index]');
|
||||
}
|
||||
if (first) {
|
||||
first.focus();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
contextMenuElem: element,
|
||||
});
|
||||
};
|
||||
|
||||
onContextMenu = (e) => {
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
|
||||
e.preventDefault();
|
||||
const x = e.clientX;
|
||||
const y = e.clientY;
|
||||
|
||||
// XXX: This isn't pretty but the only way to allow opening a different context menu on right click whilst
|
||||
// a context menu and its click-guard are up without completely rewriting how the context menus work.
|
||||
setImmediate(() => {
|
||||
const clickEvent = document.createEvent('MouseEvents');
|
||||
clickEvent.initMouseEvent(
|
||||
'contextmenu', true, true, window, 0,
|
||||
0, 0, x, y, false, false,
|
||||
false, false, 0, null,
|
||||
);
|
||||
document.elementFromPoint(x, y).dispatchEvent(clickEvent);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onMoveFocus = (element, up) => {
|
||||
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||
|
||||
do {
|
||||
const child = up ? element.lastElementChild : element.firstElementChild;
|
||||
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
|
||||
|
||||
if (descending) {
|
||||
if (child) {
|
||||
element = child;
|
||||
} else if (sibling) {
|
||||
element = sibling;
|
||||
} else {
|
||||
descending = false;
|
||||
element = element.parentElement;
|
||||
}
|
||||
} else {
|
||||
if (sibling) {
|
||||
element = sibling;
|
||||
descending = true;
|
||||
} else {
|
||||
element = element.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
if (element) {
|
||||
if (element.classList.contains("mx_ContextualMenu")) { // we hit the top
|
||||
element = up ? element.lastElementChild : element.firstElementChild;
|
||||
descending = true;
|
||||
}
|
||||
}
|
||||
} while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role")));
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
};
|
||||
|
||||
_onMoveFocusHomeEnd = (element, up) => {
|
||||
let results = element.querySelectorAll('[role^="menuitem"]');
|
||||
if (!results) {
|
||||
results = element.querySelectorAll('[tab-index]');
|
||||
}
|
||||
if (results && results.length) {
|
||||
if (up) {
|
||||
results[0].focus();
|
||||
} else {
|
||||
results[results.length - 1].focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onKeyDown = (ev) => {
|
||||
let handled = true;
|
||||
|
||||
switch (ev.key) {
|
||||
case Key.TAB:
|
||||
if (!this.props.catchTab) {
|
||||
handled = false;
|
||||
break;
|
||||
}
|
||||
// fallthrough
|
||||
case Key.ESCAPE:
|
||||
this.props.onFinished();
|
||||
break;
|
||||
case Key.ARROW_UP:
|
||||
this._onMoveFocus(ev.target, true);
|
||||
break;
|
||||
case Key.ARROW_DOWN:
|
||||
this._onMoveFocus(ev.target, false);
|
||||
break;
|
||||
case Key.HOME:
|
||||
this._onMoveFocusHomeEnd(this.state.contextMenuElem, true);
|
||||
break;
|
||||
case Key.END:
|
||||
this._onMoveFocusHomeEnd(this.state.contextMenuElem, false);
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
// consume all other keys in context menu
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
renderMenu(hasBackground=this.props.hasBackground) {
|
||||
const position = {};
|
||||
let chevronFace = null;
|
||||
const props = this.props;
|
||||
|
||||
if (props.top) {
|
||||
position.top = props.top;
|
||||
} else {
|
||||
position.bottom = props.bottom;
|
||||
}
|
||||
|
||||
if (props.left) {
|
||||
position.left = props.left;
|
||||
chevronFace = 'left';
|
||||
} else {
|
||||
position.right = props.right;
|
||||
chevronFace = 'right';
|
||||
}
|
||||
|
||||
const contextMenuRect = this.state.contextMenuElem ? this.state.contextMenuElem.getBoundingClientRect() : null;
|
||||
const padding = 10;
|
||||
|
||||
const chevronOffset = {};
|
||||
if (props.chevronFace) {
|
||||
chevronFace = props.chevronFace;
|
||||
}
|
||||
const hasChevron = chevronFace && chevronFace !== "none";
|
||||
|
||||
if (chevronFace === 'top' || chevronFace === 'bottom') {
|
||||
chevronOffset.left = props.chevronOffset;
|
||||
} else {
|
||||
const target = position.top;
|
||||
|
||||
// By default, no adjustment is made
|
||||
let adjusted = target;
|
||||
|
||||
// If we know the dimensions of the context menu, adjust its position
|
||||
// such that it does not leave the (padded) window.
|
||||
if (contextMenuRect) {
|
||||
adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding);
|
||||
}
|
||||
|
||||
position.top = adjusted;
|
||||
chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted);
|
||||
}
|
||||
|
||||
let chevron;
|
||||
if (hasChevron) {
|
||||
chevron = <div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace} />;
|
||||
}
|
||||
|
||||
const menuClasses = classNames({
|
||||
'mx_ContextualMenu': true,
|
||||
'mx_ContextualMenu_left': !hasChevron && position.left,
|
||||
'mx_ContextualMenu_right': !hasChevron && position.right,
|
||||
'mx_ContextualMenu_top': !hasChevron && position.top,
|
||||
'mx_ContextualMenu_bottom': !hasChevron && position.bottom,
|
||||
'mx_ContextualMenu_withChevron_left': chevronFace === 'left',
|
||||
'mx_ContextualMenu_withChevron_right': chevronFace === 'right',
|
||||
'mx_ContextualMenu_withChevron_top': chevronFace === 'top',
|
||||
'mx_ContextualMenu_withChevron_bottom': chevronFace === 'bottom',
|
||||
});
|
||||
|
||||
const menuStyle = {};
|
||||
if (props.menuWidth) {
|
||||
menuStyle.width = props.menuWidth;
|
||||
}
|
||||
|
||||
if (props.menuHeight) {
|
||||
menuStyle.height = props.menuHeight;
|
||||
}
|
||||
|
||||
if (!isNaN(Number(props.menuPaddingTop))) {
|
||||
menuStyle["paddingTop"] = props.menuPaddingTop;
|
||||
}
|
||||
if (!isNaN(Number(props.menuPaddingLeft))) {
|
||||
menuStyle["paddingLeft"] = props.menuPaddingLeft;
|
||||
}
|
||||
if (!isNaN(Number(props.menuPaddingBottom))) {
|
||||
menuStyle["paddingBottom"] = props.menuPaddingBottom;
|
||||
}
|
||||
if (!isNaN(Number(props.menuPaddingRight))) {
|
||||
menuStyle["paddingRight"] = props.menuPaddingRight;
|
||||
}
|
||||
|
||||
const wrapperStyle = {};
|
||||
if (!isNaN(Number(props.zIndex))) {
|
||||
menuStyle["zIndex"] = props.zIndex + 1;
|
||||
wrapperStyle["zIndex"] = props.zIndex;
|
||||
}
|
||||
|
||||
let background;
|
||||
if (hasBackground) {
|
||||
background = (
|
||||
<div className="mx_ContextualMenu_background" style={wrapperStyle} onClick={props.onFinished} onContextMenu={this.onContextMenu} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_ContextualMenu_wrapper" style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown}>
|
||||
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} role="menu">
|
||||
{ chevron }
|
||||
{ props.children }
|
||||
</div>
|
||||
{ background }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer());
|
||||
}
|
||||
}
|
||||
|
||||
// Semantic component for representing the AccessibleButton which launches a <ContextMenu />
|
||||
export const ContextMenuButton = ({ label, isExpanded, children, ...props }) => {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<AccessibleButton {...props} title={label} aria-label={label} aria-haspopup={true} aria-expanded={isExpanded}>
|
||||
{ children }
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
ContextMenuButton.propTypes = {
|
||||
...AccessibleButton.propTypes,
|
||||
label: PropTypes.string.isRequired,
|
||||
isExpanded: PropTypes.bool.isRequired, // whether or not the context menu is currently open
|
||||
};
|
||||
|
||||
// Semantic component for representing a role=menuitem
|
||||
export const MenuItem = ({children, label, ...props}) => {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={label}>
|
||||
{ children }
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
MenuItem.propTypes = {
|
||||
...AccessibleButton.propTypes,
|
||||
label: PropTypes.string, // optional
|
||||
className: PropTypes.string, // optional
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
// Semantic component for representing a role=group for grouping menu radios/checkboxes
|
||||
export const MenuGroup = ({children, label, ...props}) => {
|
||||
return <div {...props} role="group" aria-label={label}>
|
||||
{ children }
|
||||
</div>;
|
||||
};
|
||||
MenuGroup.propTypes = {
|
||||
...AccessibleButton.propTypes,
|
||||
label: PropTypes.string.isRequired,
|
||||
className: PropTypes.string, // optional
|
||||
};
|
||||
|
||||
// Semantic component for representing a role=menuitemcheckbox
|
||||
export const MenuItemCheckbox = ({children, label, active=false, disabled=false, ...props}) => {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<AccessibleButton {...props} role="menuitemcheckbox" aria-checked={active} aria-disabled={disabled} tabIndex={-1} aria-label={label}>
|
||||
{ children }
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
MenuItemCheckbox.propTypes = {
|
||||
...AccessibleButton.propTypes,
|
||||
label: PropTypes.string, // optional
|
||||
active: PropTypes.bool.isRequired,
|
||||
disabled: PropTypes.bool, // optional
|
||||
className: PropTypes.string, // optional
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
// Semantic component for representing a role=menuitemradio
|
||||
export const MenuItemRadio = ({children, label, active=false, disabled=false, ...props}) => {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<AccessibleButton {...props} role="menuitemradio" aria-checked={active} aria-disabled={disabled} tabIndex={-1} aria-label={label}>
|
||||
{ children }
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
MenuItemRadio.propTypes = {
|
||||
...AccessibleButton.propTypes,
|
||||
label: PropTypes.string, // optional
|
||||
active: PropTypes.bool.isRequired,
|
||||
disabled: PropTypes.bool, // optional
|
||||
className: PropTypes.string, // optional
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
|
||||
export const toRightOf = (elementRect, chevronOffset=12) => {
|
||||
const left = elementRect.right + window.pageXOffset + 3;
|
||||
let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
|
||||
top -= chevronOffset + 8; // where 8 is half the height of the chevron
|
||||
return {left, top, chevronOffset};
|
||||
};
|
||||
|
||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
|
||||
export const aboveLeftOf = (elementRect, chevronFace="none") => {
|
||||
const menuOptions = { chevronFace };
|
||||
|
||||
const buttonRight = elementRect.right + window.pageXOffset;
|
||||
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
||||
const buttonTop = elementRect.top + window.pageYOffset;
|
||||
// Align the right edge of the menu to the right edge of the button
|
||||
menuOptions.right = window.innerWidth - buttonRight;
|
||||
// Align the menu vertically on whichever side of the button has more space available.
|
||||
if (buttonBottom < window.innerHeight / 2) {
|
||||
menuOptions.top = buttonBottom;
|
||||
} else {
|
||||
menuOptions.bottom = window.innerHeight - buttonTop;
|
||||
}
|
||||
|
||||
return menuOptions;
|
||||
};
|
||||
|
||||
export const useContextMenu = () => {
|
||||
const button = useRef(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const open = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
const close = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return [isOpen, button, open, close, setIsOpen];
|
||||
};
|
||||
|
||||
export default class LegacyContextMenu extends ContextMenu {
|
||||
render() {
|
||||
return this.renderMenu(false);
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Deprecated, used only for dynamic Tooltips. Avoid using at all costs.
|
||||
export function createMenu(ElementClass, props) {
|
||||
const onFinished = function(...args) {
|
||||
ReactDOM.unmountComponentAtNode(getOrCreateContainer());
|
||||
|
||||
if (props && props.onFinished) {
|
||||
props.onFinished.apply(null, args);
|
||||
}
|
||||
};
|
||||
|
||||
const menu = <LegacyContextMenu
|
||||
{...props}
|
||||
onFinished={onFinished} // eslint-disable-line react/jsx-no-bind
|
||||
windowResize={onFinished} // eslint-disable-line react/jsx-no-bind
|
||||
>
|
||||
<ElementClass {...props} onFinished={onFinished} />
|
||||
</LegacyContextMenu>;
|
||||
|
||||
ReactDOM.render(menu, getOrCreateContainer());
|
||||
|
||||
return {close: onFinished};
|
||||
}
|
|
@ -1,253 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 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 ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import {focusCapturedRef} from "../../utils/Accessibility";
|
||||
import {KeyCode} from "../../Keyboard";
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
// pass in a custom control as the actual body.
|
||||
|
||||
const ContextualMenuContainerId = "mx_ContextualMenu_Container";
|
||||
|
||||
function getOrCreateContainer() {
|
||||
let container = document.getElementById(ContextualMenuContainerId);
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = ContextualMenuContainerId;
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
export default class ContextualMenu extends React.Component {
|
||||
propTypes: {
|
||||
top: PropTypes.number,
|
||||
bottom: PropTypes.number,
|
||||
left: PropTypes.number,
|
||||
right: PropTypes.number,
|
||||
menuWidth: PropTypes.number,
|
||||
menuHeight: PropTypes.number,
|
||||
chevronOffset: PropTypes.number,
|
||||
chevronFace: PropTypes.string, // top, bottom, left, right or none
|
||||
// Function to be called on menu close
|
||||
onFinished: PropTypes.func,
|
||||
menuPaddingTop: PropTypes.number,
|
||||
menuPaddingRight: PropTypes.number,
|
||||
menuPaddingBottom: PropTypes.number,
|
||||
menuPaddingLeft: PropTypes.number,
|
||||
zIndex: PropTypes.number,
|
||||
|
||||
// If true, insert an invisible screen-sized element behind the
|
||||
// menu that when clicked will close it.
|
||||
hasBackground: PropTypes.bool,
|
||||
|
||||
// The component to render as the context menu
|
||||
elementClass: PropTypes.element.isRequired,
|
||||
// on resize callback
|
||||
windowResize: PropTypes.func,
|
||||
// method to close menu
|
||||
closeMenu: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
contextMenuRect: null,
|
||||
};
|
||||
|
||||
this.onContextMenu = this.onContextMenu.bind(this);
|
||||
this.collectContextMenuRect = this.collectContextMenuRect.bind(this);
|
||||
}
|
||||
|
||||
collectContextMenuRect(element) {
|
||||
// We don't need to clean up when unmounting, so ignore
|
||||
if (!element) return;
|
||||
|
||||
// For screen readers to find the thing
|
||||
focusCapturedRef(element);
|
||||
|
||||
this.setState({
|
||||
contextMenuRect: element.getBoundingClientRect(),
|
||||
});
|
||||
}
|
||||
|
||||
onContextMenu(e) {
|
||||
if (this.props.closeMenu) {
|
||||
this.props.closeMenu();
|
||||
|
||||
e.preventDefault();
|
||||
const x = e.clientX;
|
||||
const y = e.clientY;
|
||||
|
||||
// XXX: This isn't pretty but the only way to allow opening a different context menu on right click whilst
|
||||
// a context menu and its click-guard are up without completely rewriting how the context menus work.
|
||||
setImmediate(() => {
|
||||
const clickEvent = document.createEvent('MouseEvents');
|
||||
clickEvent.initMouseEvent(
|
||||
'contextmenu', true, true, window, 0,
|
||||
0, 0, x, y, false, false,
|
||||
false, false, 0, null,
|
||||
);
|
||||
document.elementFromPoint(x, y).dispatchEvent(clickEvent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyDown = (ev) => {
|
||||
if (ev.keyCode === KeyCode.ESCAPE) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.props.closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const position = {};
|
||||
let chevronFace = null;
|
||||
const props = this.props;
|
||||
|
||||
if (props.top) {
|
||||
position.top = props.top;
|
||||
} else {
|
||||
position.bottom = props.bottom;
|
||||
}
|
||||
|
||||
if (props.left) {
|
||||
position.left = props.left;
|
||||
chevronFace = 'left';
|
||||
} else {
|
||||
position.right = props.right;
|
||||
chevronFace = 'right';
|
||||
}
|
||||
|
||||
const contextMenuRect = this.state.contextMenuRect || null;
|
||||
const padding = 10;
|
||||
|
||||
const chevronOffset = {};
|
||||
if (props.chevronFace) {
|
||||
chevronFace = props.chevronFace;
|
||||
}
|
||||
const hasChevron = chevronFace && chevronFace !== "none";
|
||||
|
||||
if (chevronFace === 'top' || chevronFace === 'bottom') {
|
||||
chevronOffset.left = props.chevronOffset;
|
||||
} else {
|
||||
const target = position.top;
|
||||
|
||||
// By default, no adjustment is made
|
||||
let adjusted = target;
|
||||
|
||||
// If we know the dimensions of the context menu, adjust its position
|
||||
// such that it does not leave the (padded) window.
|
||||
if (contextMenuRect) {
|
||||
adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding);
|
||||
}
|
||||
|
||||
position.top = adjusted;
|
||||
chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted);
|
||||
}
|
||||
|
||||
const chevron = hasChevron ?
|
||||
<div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace} /> :
|
||||
undefined;
|
||||
const className = 'mx_ContextualMenu_wrapper';
|
||||
|
||||
const menuClasses = classNames({
|
||||
'mx_ContextualMenu': true,
|
||||
'mx_ContextualMenu_left': !hasChevron && position.left,
|
||||
'mx_ContextualMenu_right': !hasChevron && position.right,
|
||||
'mx_ContextualMenu_top': !hasChevron && position.top,
|
||||
'mx_ContextualMenu_bottom': !hasChevron && position.bottom,
|
||||
'mx_ContextualMenu_withChevron_left': chevronFace === 'left',
|
||||
'mx_ContextualMenu_withChevron_right': chevronFace === 'right',
|
||||
'mx_ContextualMenu_withChevron_top': chevronFace === 'top',
|
||||
'mx_ContextualMenu_withChevron_bottom': chevronFace === 'bottom',
|
||||
});
|
||||
|
||||
const menuStyle = {};
|
||||
if (props.menuWidth) {
|
||||
menuStyle.width = props.menuWidth;
|
||||
}
|
||||
|
||||
if (props.menuHeight) {
|
||||
menuStyle.height = props.menuHeight;
|
||||
}
|
||||
|
||||
if (!isNaN(Number(props.menuPaddingTop))) {
|
||||
menuStyle["paddingTop"] = props.menuPaddingTop;
|
||||
}
|
||||
if (!isNaN(Number(props.menuPaddingLeft))) {
|
||||
menuStyle["paddingLeft"] = props.menuPaddingLeft;
|
||||
}
|
||||
if (!isNaN(Number(props.menuPaddingBottom))) {
|
||||
menuStyle["paddingBottom"] = props.menuPaddingBottom;
|
||||
}
|
||||
if (!isNaN(Number(props.menuPaddingRight))) {
|
||||
menuStyle["paddingRight"] = props.menuPaddingRight;
|
||||
}
|
||||
|
||||
const wrapperStyle = {};
|
||||
if (!isNaN(Number(props.zIndex))) {
|
||||
menuStyle["zIndex"] = props.zIndex + 1;
|
||||
wrapperStyle["zIndex"] = props.zIndex;
|
||||
}
|
||||
|
||||
const ElementClass = props.elementClass;
|
||||
|
||||
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
|
||||
// property set here so you can't close the menu from a button click!
|
||||
return <div className={className} style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown}>
|
||||
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} tabIndex={0}>
|
||||
{ chevron }
|
||||
<ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} />
|
||||
</div>
|
||||
{ props.hasBackground && <div className="mx_ContextualMenu_background" style={wrapperStyle}
|
||||
onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export function createMenu(ElementClass, props, hasBackground=true) {
|
||||
const closeMenu = function(...args) {
|
||||
ReactDOM.unmountComponentAtNode(getOrCreateContainer());
|
||||
|
||||
if (props && props.onFinished) {
|
||||
props.onFinished.apply(null, args);
|
||||
}
|
||||
};
|
||||
|
||||
// We only reference closeMenu once per call to createMenu
|
||||
const menu = <ContextualMenu
|
||||
hasBackground={hasBackground}
|
||||
{...props}
|
||||
elementClass={ElementClass}
|
||||
closeMenu={closeMenu} // eslint-disable-line react/jsx-no-bind
|
||||
windowResize={closeMenu} // eslint-disable-line react/jsx-no-bind
|
||||
/>;
|
||||
|
||||
ReactDOM.render(menu, getOrCreateContainer());
|
||||
|
||||
return {close: closeMenu};
|
||||
}
|
|
@ -1214,25 +1214,25 @@ export default createReactClass({
|
|||
|
||||
const EditableText = sdk.getComponent("elements.EditableText");
|
||||
|
||||
nameNode = <EditableText ref="nameEditor"
|
||||
className="mx_GroupView_editable"
|
||||
placeholderClassName="mx_GroupView_placeholder"
|
||||
placeholder={_t('Community Name')}
|
||||
blurToCancel={false}
|
||||
initialValue={this.state.profileForm.name}
|
||||
onValueChanged={this._onNameChange}
|
||||
tabIndex="0"
|
||||
dir="auto" />;
|
||||
nameNode = <EditableText
|
||||
className="mx_GroupView_editable"
|
||||
placeholderClassName="mx_GroupView_placeholder"
|
||||
placeholder={_t('Community Name')}
|
||||
blurToCancel={false}
|
||||
initialValue={this.state.profileForm.name}
|
||||
onValueChanged={this._onNameChange}
|
||||
tabIndex="0"
|
||||
dir="auto" />;
|
||||
|
||||
shortDescNode = <EditableText ref="descriptionEditor"
|
||||
className="mx_GroupView_editable"
|
||||
placeholderClassName="mx_GroupView_placeholder"
|
||||
placeholder={_t("Description")}
|
||||
blurToCancel={false}
|
||||
initialValue={this.state.profileForm.short_description}
|
||||
onValueChanged={this._onShortDescChange}
|
||||
tabIndex="0"
|
||||
dir="auto" />;
|
||||
shortDescNode = <EditableText
|
||||
className="mx_GroupView_editable"
|
||||
placeholderClassName="mx_GroupView_placeholder"
|
||||
placeholder={_t("Description")}
|
||||
blurToCancel={false}
|
||||
initialValue={this.state.profileForm.short_description}
|
||||
onValueChanged={this._onShortDescChange}
|
||||
tabIndex="0"
|
||||
dir="auto" />;
|
||||
} else {
|
||||
const onGroupHeaderItemClick = this.state.isUserMember ? this._onEditClick : null;
|
||||
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
|
||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
import Matrix from 'matrix-js-sdk';
|
||||
const InteractiveAuth = Matrix.InteractiveAuth;
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
@ -129,6 +129,8 @@ export default createReactClass({
|
|||
this._authLogic.poll();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
this._stageComponent = createRef();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -153,8 +155,8 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
tryContinue: function() {
|
||||
if (this.refs.stageComponent && this.refs.stageComponent.tryContinue) {
|
||||
this.refs.stageComponent.tryContinue();
|
||||
if (this._stageComponent.current && this._stageComponent.current.tryContinue) {
|
||||
this._stageComponent.current.tryContinue();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -192,8 +194,8 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
_setFocus: function() {
|
||||
if (this.refs.stageComponent && this.refs.stageComponent.focus) {
|
||||
this.refs.stageComponent.focus();
|
||||
if (this._stageComponent.current && this._stageComponent.current.focus) {
|
||||
this._stageComponent.current.focus();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -214,7 +216,8 @@ export default createReactClass({
|
|||
|
||||
const StageComponent = getEntryComponentForLoginType(stage);
|
||||
return (
|
||||
<StageComponent ref="stageComponent"
|
||||
<StageComponent
|
||||
ref={this._stageComponent}
|
||||
loginType={stage}
|
||||
matrixClient={this.props.matrixClient}
|
||||
authSessionId={this._authLogic.getSessionId()}
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
|
@ -129,6 +129,8 @@ const LoggedInView = createReactClass({
|
|||
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
||||
|
||||
fixupColorFonts();
|
||||
|
||||
this._roomView = createRef();
|
||||
},
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
@ -165,10 +167,10 @@ const LoggedInView = createReactClass({
|
|||
},
|
||||
|
||||
canResetTimelineInRoom: function(roomId) {
|
||||
if (!this.refs.roomView) {
|
||||
if (!this._roomView.current) {
|
||||
return true;
|
||||
}
|
||||
return this.refs.roomView.canResetTimeline();
|
||||
return this._roomView.current.canResetTimeline();
|
||||
},
|
||||
|
||||
_setStateFromSessionStore() {
|
||||
|
@ -401,6 +403,11 @@ const LoggedInView = createReactClass({
|
|||
const isClickShortcut = ev.target !== document.body &&
|
||||
(ev.key === Key.SPACE || ev.key === Key.ENTER);
|
||||
|
||||
// Do not capture the context menu key to improve keyboard accessibility
|
||||
if (ev.key === Key.CONTEXT_MENU) {
|
||||
return;
|
||||
}
|
||||
|
||||
// XXX: Remove after CIDER replaces Slate completely: https://github.com/vector-im/riot-web/issues/11036
|
||||
// If using Slate, consume the Backspace without first focusing as it causes an implosion
|
||||
if (ev.key === Key.BACKSPACE && !SettingsStore.getValue("useCiderComposer")) {
|
||||
|
@ -423,8 +430,8 @@ const LoggedInView = createReactClass({
|
|||
* @param {Object} ev The key event
|
||||
*/
|
||||
_onScrollKeyPressed: function(ev) {
|
||||
if (this.refs.roomView) {
|
||||
this.refs.roomView.handleScrollKey(ev);
|
||||
if (this._roomView.current) {
|
||||
this._roomView.current.handleScrollKey(ev);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -538,7 +545,7 @@ const LoggedInView = createReactClass({
|
|||
switch (this.props.page_type) {
|
||||
case PageTypes.RoomView:
|
||||
pageElement = <RoomView
|
||||
ref='roomView'
|
||||
ref={this._roomView}
|
||||
autoJoin={this.props.autoJoin}
|
||||
onRegistered={this.props.onRegistered}
|
||||
thirdPartyInvite={this.props.thirdPartyInvite}
|
||||
|
|
|
@ -24,6 +24,8 @@ import Matrix from "matrix-js-sdk";
|
|||
|
||||
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
|
||||
import 'focus-visible';
|
||||
// what-input helps improve keyboard accessibility
|
||||
import 'what-input';
|
||||
|
||||
import Analytics from "../../Analytics";
|
||||
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
||||
|
|
|
@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
@ -159,6 +159,10 @@ export default class MessagePanel extends React.Component {
|
|||
SettingsStore.getValue("showHiddenEventsInTimeline");
|
||||
|
||||
this._isMounted = false;
|
||||
|
||||
this._readMarkerNode = createRef();
|
||||
this._whoIsTyping = createRef();
|
||||
this._scrollPanel = createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -191,8 +195,7 @@ export default class MessagePanel extends React.Component {
|
|||
/* return true if the content is fully scrolled down right now; else false.
|
||||
*/
|
||||
isAtBottom() {
|
||||
return this.refs.scrollPanel
|
||||
&& this.refs.scrollPanel.isAtBottom();
|
||||
return this._scrollPanel.current && this._scrollPanel.current.isAtBottom();
|
||||
}
|
||||
|
||||
/* get the current scroll state. See ScrollPanel.getScrollState for
|
||||
|
@ -201,8 +204,7 @@ export default class MessagePanel extends React.Component {
|
|||
* returns null if we are not mounted.
|
||||
*/
|
||||
getScrollState() {
|
||||
if (!this.refs.scrollPanel) { return null; }
|
||||
return this.refs.scrollPanel.getScrollState();
|
||||
return this._scrollPanel.current ? this._scrollPanel.current.getScrollState() : null;
|
||||
}
|
||||
|
||||
// returns one of:
|
||||
|
@ -212,8 +214,8 @@ export default class MessagePanel extends React.Component {
|
|||
// 0: read marker is within the window
|
||||
// +1: read marker is below the window
|
||||
getReadMarkerPosition() {
|
||||
const readMarker = this.refs.readMarkerNode;
|
||||
const messageWrapper = this.refs.scrollPanel;
|
||||
const readMarker = this._readMarkerNode.current;
|
||||
const messageWrapper = this._scrollPanel.current;
|
||||
|
||||
if (!readMarker || !messageWrapper) {
|
||||
return null;
|
||||
|
@ -236,16 +238,16 @@ export default class MessagePanel extends React.Component {
|
|||
/* jump to the top of the content.
|
||||
*/
|
||||
scrollToTop() {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.scrollToTop();
|
||||
if (this._scrollPanel.current) {
|
||||
this._scrollPanel.current.scrollToTop();
|
||||
}
|
||||
}
|
||||
|
||||
/* jump to the bottom of the content.
|
||||
*/
|
||||
scrollToBottom() {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.scrollToBottom();
|
||||
if (this._scrollPanel.current) {
|
||||
this._scrollPanel.current.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -255,8 +257,8 @@ export default class MessagePanel extends React.Component {
|
|||
* @param {number} mult: -1 to page up, +1 to page down
|
||||
*/
|
||||
scrollRelative(mult) {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.scrollRelative(mult);
|
||||
if (this._scrollPanel.current) {
|
||||
this._scrollPanel.current.scrollRelative(mult);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -266,8 +268,8 @@ export default class MessagePanel extends React.Component {
|
|||
* @param {KeyboardEvent} ev: the keyboard event to handle
|
||||
*/
|
||||
handleScrollKey(ev) {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.handleScrollKey(ev);
|
||||
if (this._scrollPanel.current) {
|
||||
this._scrollPanel.current.handleScrollKey(ev);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,8 +284,8 @@ export default class MessagePanel extends React.Component {
|
|||
* defaults to 0.
|
||||
*/
|
||||
scrollToEvent(eventId, pixelOffset, offsetBase) {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.scrollToToken(eventId, pixelOffset, offsetBase);
|
||||
if (this._scrollPanel.current) {
|
||||
this._scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -297,8 +299,8 @@ export default class MessagePanel extends React.Component {
|
|||
/* check the scroll state and send out pagination requests if necessary.
|
||||
*/
|
||||
checkFillState() {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.checkFillState();
|
||||
if (this._scrollPanel.current) {
|
||||
this._scrollPanel.current.checkFillState();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -345,7 +347,7 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<li key={"readMarker_"+eventId} ref="readMarkerNode"
|
||||
<li key={"readMarker_"+eventId} ref={this._readMarkerNode}
|
||||
className="mx_RoomView_myReadMarker_container">
|
||||
{ hr }
|
||||
</li>
|
||||
|
@ -693,6 +695,10 @@ export default class MessagePanel extends React.Component {
|
|||
|
||||
const readReceipts = this._readReceiptsByEvent[eventId];
|
||||
|
||||
// Dev note: `this._isUnmounting.bind(this)` is important - it ensures that
|
||||
// the function is run in the context of this class and not EventTile, therefore
|
||||
// ensuring the right `this._mounted` variable is used by read receipts (which
|
||||
// don't update their position if we, the MessagePanel, is unmounting).
|
||||
ret.push(
|
||||
<li key={eventId}
|
||||
ref={this._collectEventNode.bind(this, eventId)}
|
||||
|
@ -707,7 +713,7 @@ export default class MessagePanel extends React.Component {
|
|||
readReceipts={readReceipts}
|
||||
readReceiptMap={this._readReceiptMap}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
checkUnmounting={this._isUnmounting}
|
||||
checkUnmounting={this._isUnmounting.bind(this)}
|
||||
eventSendStatus={mxEv.getAssociatedStatus()}
|
||||
tileShape={this.props.tileShape}
|
||||
isTwelveHour={this.props.isTwelveHour}
|
||||
|
@ -825,14 +831,14 @@ export default class MessagePanel extends React.Component {
|
|||
// once dynamic content in the events load, make the scrollPanel check the
|
||||
// scroll offsets.
|
||||
_onHeightChanged = () => {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
const scrollPanel = this._scrollPanel.current;
|
||||
if (scrollPanel) {
|
||||
scrollPanel.checkScroll();
|
||||
}
|
||||
};
|
||||
|
||||
_onTypingShown = () => {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
const scrollPanel = this._scrollPanel.current;
|
||||
// this will make the timeline grow, so checkScroll
|
||||
scrollPanel.checkScroll();
|
||||
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
|
||||
|
@ -841,7 +847,7 @@ export default class MessagePanel extends React.Component {
|
|||
};
|
||||
|
||||
_onTypingHidden = () => {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
const scrollPanel = this._scrollPanel.current;
|
||||
if (scrollPanel) {
|
||||
// as hiding the typing notifications doesn't
|
||||
// update the scrollPanel, we tell it to apply
|
||||
|
@ -854,11 +860,11 @@ export default class MessagePanel extends React.Component {
|
|||
};
|
||||
|
||||
updateTimelineMinHeight() {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
const scrollPanel = this._scrollPanel.current;
|
||||
|
||||
if (scrollPanel) {
|
||||
const isAtBottom = scrollPanel.isAtBottom();
|
||||
const whoIsTyping = this.refs.whoIsTyping;
|
||||
const whoIsTyping = this._whoIsTyping.current;
|
||||
const isTypingVisible = whoIsTyping && whoIsTyping.isVisible();
|
||||
// when messages get added to the timeline,
|
||||
// but somebody else is still typing,
|
||||
|
@ -871,7 +877,7 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
|
||||
onTimelineReset() {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
const scrollPanel = this._scrollPanel.current;
|
||||
if (scrollPanel) {
|
||||
scrollPanel.clearPreventShrinking();
|
||||
}
|
||||
|
@ -905,19 +911,22 @@ export default class MessagePanel extends React.Component {
|
|||
room={this.props.room}
|
||||
onShown={this._onTypingShown}
|
||||
onHidden={this._onTypingHidden}
|
||||
ref="whoIsTyping" />
|
||||
ref={this._whoIsTyping} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollPanel ref="scrollPanel" className={className}
|
||||
onScroll={this.props.onScroll}
|
||||
onResize={this.onResize}
|
||||
onFillRequest={this.props.onFillRequest}
|
||||
onUnfillRequest={this.props.onUnfillRequest}
|
||||
style={style}
|
||||
stickyBottom={this.props.stickyBottom}
|
||||
resizeNotifier={this.props.resizeNotifier}>
|
||||
<ScrollPanel
|
||||
ref={this._scrollPanel}
|
||||
className={className}
|
||||
onScroll={this.props.onScroll}
|
||||
onResize={this.onResize}
|
||||
onFillRequest={this.props.onFillRequest}
|
||||
onUnfillRequest={this.props.onUnfillRequest}
|
||||
style={style}
|
||||
stickyBottom={this.props.stickyBottom}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
{ topSpinner }
|
||||
{ this._getEventTiles() }
|
||||
{ whoIsTyping }
|
||||
|
|
|
@ -572,7 +572,7 @@ module.exports = createReactClass({
|
|||
if (rows.length === 0 && !this.state.loading) {
|
||||
scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
|
||||
} else {
|
||||
scrollpanel_content = <table ref="directory_table" className="mx_RoomDirectory_table">
|
||||
scrollpanel_content = <table className="mx_RoomDirectory_table">
|
||||
<tbody>
|
||||
{ rows }
|
||||
</tbody>
|
||||
|
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
|
@ -36,12 +35,11 @@ import {_t} from "../../languageHandler";
|
|||
// turn this on for drop & drag console debugging galore
|
||||
const debug = false;
|
||||
|
||||
const RoomSubList = createReactClass({
|
||||
displayName: 'RoomSubList',
|
||||
export default class RoomSubList extends React.PureComponent {
|
||||
static displayName = 'RoomSubList';
|
||||
static debug = debug;
|
||||
|
||||
debug: debug,
|
||||
|
||||
propTypes: {
|
||||
static propTypes = {
|
||||
list: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
tagName: PropTypes.string,
|
||||
|
@ -59,10 +57,26 @@ const RoomSubList = createReactClass({
|
|||
incomingCall: PropTypes.object,
|
||||
extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
|
||||
forceExpand: PropTypes.bool,
|
||||
},
|
||||
};
|
||||
|
||||
getInitialState: function() {
|
||||
static defaultProps = {
|
||||
onHeaderClick: function() {
|
||||
}, // NOP
|
||||
extraTiles: [],
|
||||
isInvite: false,
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
return {
|
||||
listLength: props.list.length,
|
||||
scrollTop: props.list.length === state.listLength ? state.scrollTop : 0,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hidden: this.props.startAsHidden || false,
|
||||
// some values to get LazyRenderList starting
|
||||
scrollerHeight: 800,
|
||||
|
@ -71,47 +85,33 @@ const RoomSubList = createReactClass({
|
|||
// we have to store the length of the list here so we can see if it's changed or not...
|
||||
listLength: null,
|
||||
};
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onHeaderClick: function() {
|
||||
}, // NOP
|
||||
extraTiles: [],
|
||||
isInvite: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._header = createRef();
|
||||
this._subList = createRef();
|
||||
this._scroller = createRef();
|
||||
this._headerButton = createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
},
|
||||
}
|
||||
|
||||
statics: {
|
||||
getDerivedStateFromProps: function(props, state) {
|
||||
return {
|
||||
listLength: props.list.length,
|
||||
scrollTop: props.list.length === state.listLength ? state.scrollTop : 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
}
|
||||
|
||||
// The header is collapsible if it is hidden or not stuck
|
||||
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
|
||||
isCollapsibleOnClick: function() {
|
||||
const stuck = this.refs.header.dataset.stuck;
|
||||
isCollapsibleOnClick() {
|
||||
const stuck = this._header.current.dataset.stuck;
|
||||
if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onAction: function(payload) {
|
||||
onAction = (payload) => {
|
||||
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
|
||||
// but this is no longer true, so we must do it here (and can apply the small
|
||||
// optimisation of checking that we care about the room being read).
|
||||
|
@ -124,9 +124,9 @@ const RoomSubList = createReactClass({
|
|||
) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
onClick: function(ev) {
|
||||
onClick = (ev) => {
|
||||
if (this.isCollapsibleOnClick()) {
|
||||
// The header isCollapsible, so the click is to be interpreted as collapse and truncation logic
|
||||
const isHidden = !this.state.hidden;
|
||||
|
@ -135,11 +135,11 @@ const RoomSubList = createReactClass({
|
|||
});
|
||||
} else {
|
||||
// The header is stuck, so the click is to be interpreted as a scroll to the header
|
||||
this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition);
|
||||
this.props.onHeaderClick(this.state.hidden, this._header.current.dataset.originalPosition);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
onHeaderKeyDown: function(ev) {
|
||||
onHeaderKeyDown = (ev) => {
|
||||
switch (ev.key) {
|
||||
case Key.TAB:
|
||||
// Prevent LeftPanel handling Tab if focus is on the sublist header itself
|
||||
|
@ -159,7 +159,7 @@ const RoomSubList = createReactClass({
|
|||
this.onClick();
|
||||
} else if (!this.props.forceExpand) {
|
||||
// sublist is expanded, go to first room
|
||||
const element = this.refs.subList && this.refs.subList.querySelector(".mx_RoomTile");
|
||||
const element = this._subList.current && this._subList.current.querySelector(".mx_RoomTile");
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
|
@ -167,9 +167,9 @@ const RoomSubList = createReactClass({
|
|||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
onKeyDown: function(ev) {
|
||||
onKeyDown = (ev) => {
|
||||
switch (ev.key) {
|
||||
// On ARROW_LEFT go to the sublist header
|
||||
case Key.ARROW_LEFT:
|
||||
|
@ -180,24 +180,24 @@ const RoomSubList = createReactClass({
|
|||
case Key.ARROW_RIGHT:
|
||||
ev.stopPropagation();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
onRoomTileClick(roomId, ev) {
|
||||
onRoomTileClick = (roomId, ev) => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
clear_search: (ev && (ev.keyCode === KeyCode.ENTER || ev.keyCode === KeyCode.SPACE)),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
_updateSubListCount: function() {
|
||||
_updateSubListCount = () => {
|
||||
// Force an update by setting the state to the current state
|
||||
// Doing it this way rather than using forceUpdate(), so that the shouldComponentUpdate()
|
||||
// method is honoured
|
||||
this.setState(this.state);
|
||||
},
|
||||
};
|
||||
|
||||
makeRoomTile: function(room) {
|
||||
makeRoomTile = (room) => {
|
||||
return <RoomTile
|
||||
room={room}
|
||||
roomSubList={this}
|
||||
|
@ -212,9 +212,9 @@ const RoomSubList = createReactClass({
|
|||
incomingCall={null}
|
||||
onClick={this.onRoomTileClick}
|
||||
/>;
|
||||
},
|
||||
};
|
||||
|
||||
_onNotifBadgeClick: function(e) {
|
||||
_onNotifBadgeClick = (e) => {
|
||||
// prevent the roomsublist collapsing
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -225,9 +225,9 @@ const RoomSubList = createReactClass({
|
|||
room_id: room.roomId,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
_onInviteBadgeClick: function(e) {
|
||||
_onInviteBadgeClick = (e) => {
|
||||
// prevent the roomsublist collapsing
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -247,14 +247,14 @@ const RoomSubList = createReactClass({
|
|||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
onAddRoom: function(e) {
|
||||
onAddRoom = (e) => {
|
||||
e.stopPropagation();
|
||||
if (this.props.onAddRoom) this.props.onAddRoom();
|
||||
},
|
||||
};
|
||||
|
||||
_getHeaderJsx: function(isCollapsed) {
|
||||
_getHeaderJsx(isCollapsed) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton');
|
||||
const subListNotifications = !this.props.isInvite ?
|
||||
|
@ -328,7 +328,7 @@ const RoomSubList = createReactClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSubList_labelContainer" title={title} ref="header" onKeyDown={this.onHeaderKeyDown}>
|
||||
<div className="mx_RoomSubList_labelContainer" title={title} ref={this._header} onKeyDown={this.onHeaderKeyDown}>
|
||||
<AccessibleButton
|
||||
onClick={this.onClick}
|
||||
className="mx_RoomSubList_label"
|
||||
|
@ -346,36 +346,36 @@ const RoomSubList = createReactClass({
|
|||
{ addRoomButton }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
checkOverflow: function() {
|
||||
if (this.refs.scroller) {
|
||||
this.refs.scroller.checkOverflow();
|
||||
checkOverflow = () => {
|
||||
if (this._scroller.current) {
|
||||
this._scroller.current.checkOverflow();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
setHeight: function(height) {
|
||||
if (this.refs.subList) {
|
||||
this.refs.subList.style.height = `${height}px`;
|
||||
setHeight = (height) => {
|
||||
if (this._subList.current) {
|
||||
this._subList.current.style.height = `${height}px`;
|
||||
}
|
||||
this._updateLazyRenderHeight(height);
|
||||
},
|
||||
};
|
||||
|
||||
_updateLazyRenderHeight: function(height) {
|
||||
_updateLazyRenderHeight(height) {
|
||||
this.setState({scrollerHeight: height});
|
||||
},
|
||||
}
|
||||
|
||||
_onScroll: function() {
|
||||
this.setState({scrollTop: this.refs.scroller.getScrollTop()});
|
||||
},
|
||||
_onScroll = () => {
|
||||
this.setState({scrollTop: this._scroller.current.getScrollTop()});
|
||||
};
|
||||
|
||||
_canUseLazyListRendering() {
|
||||
// for now disable lazy rendering as they are already rendered tiles
|
||||
// not rooms like props.list we pass to LazyRenderList
|
||||
return !this.props.extraTiles || !this.props.extraTiles.length;
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
const len = this.props.list.length + this.props.extraTiles.length;
|
||||
const isCollapsed = this.state.hidden && !this.props.forceExpand;
|
||||
|
||||
|
@ -391,7 +391,7 @@ const RoomSubList = createReactClass({
|
|||
// no body
|
||||
} else if (this._canUseLazyListRendering()) {
|
||||
content = (
|
||||
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
|
||||
<IndicatorScrollbar ref={this._scroller} className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
|
||||
<LazyRenderList
|
||||
scrollTop={this.state.scrollTop }
|
||||
height={ this.state.scrollerHeight }
|
||||
|
@ -404,7 +404,7 @@ const RoomSubList = createReactClass({
|
|||
const roomTiles = this.props.list.map(r => this.makeRoomTile(r));
|
||||
const tiles = roomTiles.concat(this.props.extraTiles);
|
||||
content = (
|
||||
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
|
||||
<IndicatorScrollbar ref={this._scroller} className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
|
||||
{ tiles }
|
||||
</IndicatorScrollbar>
|
||||
);
|
||||
|
@ -418,7 +418,7 @@ const RoomSubList = createReactClass({
|
|||
|
||||
return (
|
||||
<div
|
||||
ref="subList"
|
||||
ref={this._subList}
|
||||
className={subListClasses}
|
||||
role="group"
|
||||
aria-label={this.props.label}
|
||||
|
@ -428,7 +428,5 @@ const RoomSubList = createReactClass({
|
|||
{ content }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = RoomSubList;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ limitations under the License.
|
|||
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -207,6 +207,9 @@ module.exports = createReactClass({
|
|||
this._onCiderUpdated();
|
||||
this._ciderWatcherRef = SettingsStore.watchSetting(
|
||||
"useCiderComposer", null, this._onCiderUpdated);
|
||||
|
||||
this._roomView = createRef();
|
||||
this._searchResultsPanel = createRef();
|
||||
},
|
||||
|
||||
_onCiderUpdated: function() {
|
||||
|
@ -459,8 +462,8 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
if (this.refs.roomView) {
|
||||
const roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
||||
if (this._roomView.current) {
|
||||
const roomView = ReactDOM.findDOMNode(this._roomView.current);
|
||||
if (!roomView.ondrop) {
|
||||
roomView.addEventListener('drop', this.onDrop);
|
||||
roomView.addEventListener('dragover', this.onDragOver);
|
||||
|
@ -474,10 +477,10 @@ module.exports = createReactClass({
|
|||
// in render() prevents the ref from being set on first mount, so we try and
|
||||
// catch the messagePanel when it does mount. Because we only want the ref once,
|
||||
// we use a boolean flag to avoid duplicate work.
|
||||
if (this.refs.messagePanel && !this.state.atEndOfLiveTimelineInit) {
|
||||
if (this._messagePanel && !this.state.atEndOfLiveTimelineInit) {
|
||||
this.setState({
|
||||
atEndOfLiveTimelineInit: true,
|
||||
atEndOfLiveTimeline: this.refs.messagePanel.isAtEndOfLiveTimeline(),
|
||||
atEndOfLiveTimeline: this._messagePanel.isAtEndOfLiveTimeline(),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -499,12 +502,12 @@ module.exports = createReactClass({
|
|||
// stop tracking room changes to format permalinks
|
||||
this._stopAllPermalinkCreators();
|
||||
|
||||
if (this.refs.roomView) {
|
||||
if (this._roomView.current) {
|
||||
// disconnect the D&D event listeners from the room view. This
|
||||
// is really just for hygiene - we're going to be
|
||||
// deleted anyway, so it doesn't matter if the event listeners
|
||||
// don't get cleaned up.
|
||||
const roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
||||
const roomView = ReactDOM.findDOMNode(this._roomView.current);
|
||||
roomView.removeEventListener('drop', this.onDrop);
|
||||
roomView.removeEventListener('dragover', this.onDragOver);
|
||||
roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||
|
@ -701,10 +704,10 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
canResetTimeline: function() {
|
||||
if (!this.refs.messagePanel) {
|
||||
if (!this._messagePanel) {
|
||||
return true;
|
||||
}
|
||||
return this.refs.messagePanel.canResetTimeline();
|
||||
return this._messagePanel.canResetTimeline();
|
||||
},
|
||||
|
||||
// called when state.room is first initialised (either at initial load,
|
||||
|
@ -1046,7 +1049,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
onMessageListScroll: function(ev) {
|
||||
if (this.refs.messagePanel.isAtEndOfLiveTimeline()) {
|
||||
if (this._messagePanel.isAtEndOfLiveTimeline()) {
|
||||
this.setState({
|
||||
numUnreadMessages: 0,
|
||||
atEndOfLiveTimeline: true,
|
||||
|
@ -1119,8 +1122,8 @@ module.exports = createReactClass({
|
|||
|
||||
// if we already have a search panel, we need to tell it to forget
|
||||
// about its scroll state.
|
||||
if (this.refs.searchResultsPanel) {
|
||||
this.refs.searchResultsPanel.resetScrollState();
|
||||
if (this._searchResultsPanel.current) {
|
||||
this._searchResultsPanel.current.resetScrollState();
|
||||
}
|
||||
|
||||
// make sure that we don't end up showing results from
|
||||
|
@ -1225,7 +1228,7 @@ module.exports = createReactClass({
|
|||
// once dynamic content in the search results load, make the scrollPanel check
|
||||
// the scroll offsets.
|
||||
const onHeightChanged = () => {
|
||||
const scrollPanel = this.refs.searchResultsPanel;
|
||||
const scrollPanel = this._searchResultsPanel.current;
|
||||
if (scrollPanel) {
|
||||
scrollPanel.checkScroll();
|
||||
}
|
||||
|
@ -1370,28 +1373,28 @@ module.exports = createReactClass({
|
|||
|
||||
// jump down to the bottom of this room, where new events are arriving
|
||||
jumpToLiveTimeline: function() {
|
||||
this.refs.messagePanel.jumpToLiveTimeline();
|
||||
this._messagePanel.jumpToLiveTimeline();
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
// jump up to wherever our read marker is
|
||||
jumpToReadMarker: function() {
|
||||
this.refs.messagePanel.jumpToReadMarker();
|
||||
this._messagePanel.jumpToReadMarker();
|
||||
},
|
||||
|
||||
// update the read marker to match the read-receipt
|
||||
forgetReadMarker: function(ev) {
|
||||
ev.stopPropagation();
|
||||
this.refs.messagePanel.forgetReadMarker();
|
||||
this._messagePanel.forgetReadMarker();
|
||||
},
|
||||
|
||||
// decide whether or not the top 'unread messages' bar should be shown
|
||||
_updateTopUnreadMessagesBar: function() {
|
||||
if (!this.refs.messagePanel) {
|
||||
if (!this._messagePanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const showBar = this.refs.messagePanel.canJumpToReadMarker();
|
||||
const showBar = this._messagePanel.canJumpToReadMarker();
|
||||
if (this.state.showTopUnreadMessagesBar != showBar) {
|
||||
this.setState({showTopUnreadMessagesBar: showBar});
|
||||
}
|
||||
|
@ -1401,7 +1404,7 @@ module.exports = createReactClass({
|
|||
// restored when we switch back to it.
|
||||
//
|
||||
_getScrollState: function() {
|
||||
const messagePanel = this.refs.messagePanel;
|
||||
const messagePanel = this._messagePanel;
|
||||
if (!messagePanel) return null;
|
||||
|
||||
// if we're following the live timeline, we want to return null; that
|
||||
|
@ -1506,10 +1509,10 @@ module.exports = createReactClass({
|
|||
*/
|
||||
handleScrollKey: function(ev) {
|
||||
let panel;
|
||||
if (this.refs.searchResultsPanel) {
|
||||
panel = this.refs.searchResultsPanel;
|
||||
} else if (this.refs.messagePanel) {
|
||||
panel = this.refs.messagePanel;
|
||||
if (this._searchResultsPanel.current) {
|
||||
panel = this._searchResultsPanel.current;
|
||||
} else if (this._messagePanel) {
|
||||
panel = this._messagePanel;
|
||||
}
|
||||
|
||||
if (panel) {
|
||||
|
@ -1530,7 +1533,7 @@ module.exports = createReactClass({
|
|||
// this has to be a proper method rather than an unnamed function,
|
||||
// otherwise react calls it with null on each update.
|
||||
_gatherTimelinePanelRef: function(r) {
|
||||
this.refs.messagePanel = r;
|
||||
this._messagePanel = r;
|
||||
if (r) {
|
||||
console.log("updateTint from RoomView._gatherTimelinePanelRef");
|
||||
this.updateTint();
|
||||
|
@ -1719,7 +1722,7 @@ module.exports = createReactClass({
|
|||
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
|
||||
} else if (this.state.searching) {
|
||||
hideCancel = true; // has own cancel
|
||||
aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress} onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch} />;
|
||||
aux = <SearchBar searchInProgress={this.state.searchInProgress} onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch} />;
|
||||
} else if (showRoomUpgradeBar) {
|
||||
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
||||
hideCancel = true;
|
||||
|
@ -1775,7 +1778,7 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
const auxPanel = (
|
||||
<AuxPanel ref="auxPanel" room={this.state.room}
|
||||
<AuxPanel room={this.state.room}
|
||||
fullHeight={false}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
conferenceHandler={this.props.ConferenceHandler}
|
||||
|
@ -1875,7 +1878,7 @@ module.exports = createReactClass({
|
|||
searchResultsPanel = (<div className="mx_RoomView_messagePanel mx_RoomView_messagePanelSearchSpinner" />);
|
||||
} else {
|
||||
searchResultsPanel = (
|
||||
<ScrollPanel ref="searchResultsPanel"
|
||||
<ScrollPanel ref={this._searchResultsPanel}
|
||||
className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel"
|
||||
onFillRequest={this.onSearchResultsFillRequest}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
|
@ -1898,7 +1901,8 @@ module.exports = createReactClass({
|
|||
|
||||
// console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
|
||||
const messagePanel = (
|
||||
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
||||
<TimelinePanel
|
||||
ref={this._gatherTimelinePanelRef}
|
||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||
showReadReceipts={SettingsStore.getValue('showReadReceipts')}
|
||||
manageReadReceipts={!this.state.isPeeking}
|
||||
|
@ -1952,9 +1956,11 @@ module.exports = createReactClass({
|
|||
const collapsedRhs = hideRightPanel || this.props.collapsedRhs;
|
||||
|
||||
return (
|
||||
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
|
||||
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref={this._roomView}>
|
||||
<ErrorBoundary>
|
||||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
||||
<RoomHeader
|
||||
room={this.state.room}
|
||||
searchInfo={searchInfo}
|
||||
oobData={this.props.oobData}
|
||||
inRoom={myMembership === 'join'}
|
||||
collapsedRhs={collapsedRhs}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, {createRef} from "react";
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
|
@ -166,6 +166,8 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
this.resetScrollState();
|
||||
|
||||
this._itemlist = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -328,7 +330,7 @@ module.exports = createReactClass({
|
|||
this._isFilling = true;
|
||||
}
|
||||
|
||||
const itemlist = this.refs.itemlist;
|
||||
const itemlist = this._itemlist.current;
|
||||
const firstTile = itemlist && itemlist.firstElementChild;
|
||||
const contentTop = firstTile && firstTile.offsetTop;
|
||||
const fillPromises = [];
|
||||
|
@ -373,7 +375,7 @@ module.exports = createReactClass({
|
|||
|
||||
const origExcessHeight = excessHeight;
|
||||
|
||||
const tiles = this.refs.itemlist.children;
|
||||
const tiles = this._itemlist.current.children;
|
||||
|
||||
// The scroll token of the first/last tile to be unpaginated
|
||||
let markerScrollToken = null;
|
||||
|
@ -602,7 +604,7 @@ module.exports = createReactClass({
|
|||
const scrollNode = this._getScrollNode();
|
||||
const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight);
|
||||
|
||||
const itemlist = this.refs.itemlist;
|
||||
const itemlist = this._itemlist.current;
|
||||
const messages = itemlist.children;
|
||||
let node = null;
|
||||
|
||||
|
@ -644,7 +646,7 @@ module.exports = createReactClass({
|
|||
const sn = this._getScrollNode();
|
||||
sn.scrollTop = sn.scrollHeight;
|
||||
} else if (scrollState.trackedScrollToken) {
|
||||
const itemlist = this.refs.itemlist;
|
||||
const itemlist = this._itemlist.current;
|
||||
const trackedNode = this._getTrackedNode();
|
||||
if (trackedNode) {
|
||||
const newBottomOffset = this._topFromBottom(trackedNode);
|
||||
|
@ -682,7 +684,7 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
const sn = this._getScrollNode();
|
||||
const itemlist = this.refs.itemlist;
|
||||
const itemlist = this._itemlist.current;
|
||||
const contentHeight = this._getMessagesHeight();
|
||||
const minHeight = sn.clientHeight;
|
||||
const height = Math.max(minHeight, contentHeight);
|
||||
|
@ -724,7 +726,7 @@ module.exports = createReactClass({
|
|||
|
||||
if (!trackedNode || !trackedNode.parentElement) {
|
||||
let node;
|
||||
const messages = this.refs.itemlist.children;
|
||||
const messages = this._itemlist.current.children;
|
||||
const scrollToken = scrollState.trackedScrollToken;
|
||||
|
||||
for (let i = messages.length-1; i >= 0; --i) {
|
||||
|
@ -756,7 +758,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
_getMessagesHeight() {
|
||||
const itemlist = this.refs.itemlist;
|
||||
const itemlist = this._itemlist.current;
|
||||
const lastNode = itemlist.lastElementChild;
|
||||
const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0;
|
||||
const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0;
|
||||
|
@ -765,7 +767,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
_topFromBottom(node) {
|
||||
return this.refs.itemlist.clientHeight - node.offsetTop;
|
||||
return this._itemlist.current.clientHeight - node.offsetTop;
|
||||
},
|
||||
|
||||
/* get the DOM node which has the scrollTop property we care about for our
|
||||
|
@ -797,7 +799,7 @@ module.exports = createReactClass({
|
|||
the same minimum bottom offset, effectively preventing the timeline to shrink.
|
||||
*/
|
||||
preventShrinking: function() {
|
||||
const messageList = this.refs.itemlist;
|
||||
const messageList = this._itemlist.current;
|
||||
const tiles = messageList && messageList.children;
|
||||
if (!messageList) {
|
||||
return;
|
||||
|
@ -824,7 +826,7 @@ module.exports = createReactClass({
|
|||
|
||||
/** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */
|
||||
clearPreventShrinking: function() {
|
||||
const messageList = this.refs.itemlist;
|
||||
const messageList = this._itemlist.current;
|
||||
const balanceElement = messageList && messageList.parentElement;
|
||||
if (balanceElement) balanceElement.style.paddingBottom = null;
|
||||
this.preventShrinkingState = null;
|
||||
|
@ -843,7 +845,7 @@ module.exports = createReactClass({
|
|||
if (this.preventShrinkingState) {
|
||||
const sn = this._getScrollNode();
|
||||
const scrollState = this.scrollState;
|
||||
const messageList = this.refs.itemlist;
|
||||
const messageList = this._itemlist.current;
|
||||
const {offsetNode, offsetFromBottom} = this.preventShrinkingState;
|
||||
// element used to set paddingBottom to balance the typing notifs disappearing
|
||||
const balanceElement = messageList.parentElement;
|
||||
|
@ -879,7 +881,7 @@ module.exports = createReactClass({
|
|||
onScroll={this.onScroll}
|
||||
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
|
||||
<div className="mx_RoomView_messageListWrapper">
|
||||
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
|
||||
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite">
|
||||
{ this.props.children }
|
||||
</ol>
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
|
@ -53,6 +53,10 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._search = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
},
|
||||
|
@ -66,26 +70,26 @@ module.exports = createReactClass({
|
|||
|
||||
switch (payload.action) {
|
||||
case 'view_room':
|
||||
if (this.refs.search && payload.clear_search) {
|
||||
if (this._search.current && payload.clear_search) {
|
||||
this._clearSearch();
|
||||
}
|
||||
break;
|
||||
case 'focus_room_filter':
|
||||
if (this.refs.search) {
|
||||
this.refs.search.focus();
|
||||
if (this._search.current) {
|
||||
this._search.current.focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onChange: function() {
|
||||
if (!this.refs.search) return;
|
||||
this.setState({ searchTerm: this.refs.search.value });
|
||||
if (!this._search.current) return;
|
||||
this.setState({ searchTerm: this._search.current.value });
|
||||
this.onSearch();
|
||||
},
|
||||
|
||||
onSearch: throttle(function() {
|
||||
this.props.onSearch(this.refs.search.value);
|
||||
this.props.onSearch(this._search.current.value);
|
||||
}, 200, {trailing: true, leading: true}),
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
|
@ -113,7 +117,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
_clearSearch: function(source) {
|
||||
this.refs.search.value = "";
|
||||
this._search.current.value = "";
|
||||
this.onChange();
|
||||
if (this.props.onCleared) {
|
||||
this.props.onCleared(source);
|
||||
|
@ -146,7 +150,7 @@ module.exports = createReactClass({
|
|||
<input
|
||||
key="searchfield"
|
||||
type="text"
|
||||
ref="search"
|
||||
ref={this._search}
|
||||
className={"mx_textinput_icon mx_textinput_search " + className}
|
||||
value={ this.state.searchTerm }
|
||||
onFocus={ this._onFocus }
|
||||
|
|
|
@ -19,7 +19,7 @@ limitations under the License.
|
|||
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import ReactDOM from "react-dom";
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -203,6 +203,8 @@ const TimelinePanel = createReactClass({
|
|||
this.lastRRSentEventId = undefined;
|
||||
this.lastRMSentEventId = undefined;
|
||||
|
||||
this._messagePanel = createRef();
|
||||
|
||||
if (this.props.manageReadReceipts) {
|
||||
this.updateReadReceiptOnUserActivity();
|
||||
}
|
||||
|
@ -425,8 +427,8 @@ const TimelinePanel = createReactClass({
|
|||
if (payload.action === "edit_event") {
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({editState}, () => {
|
||||
if (payload.event && this.refs.messagePanel) {
|
||||
this.refs.messagePanel.scrollToEventIfNeeded(
|
||||
if (payload.event && this._messagePanel.current) {
|
||||
this._messagePanel.current.scrollToEventIfNeeded(
|
||||
payload.event.getId(),
|
||||
);
|
||||
}
|
||||
|
@ -442,9 +444,9 @@ const TimelinePanel = createReactClass({
|
|||
// updates from pagination will happen when the paginate completes.
|
||||
if (toStartOfTimeline || !data || !data.liveEvent) return;
|
||||
|
||||
if (!this.refs.messagePanel) return;
|
||||
if (!this._messagePanel.current) return;
|
||||
|
||||
if (!this.refs.messagePanel.getScrollState().stuckAtBottom) {
|
||||
if (!this._messagePanel.current.getScrollState().stuckAtBottom) {
|
||||
// we won't load this event now, because we don't want to push any
|
||||
// events off the other end of the timeline. But we need to note
|
||||
// that we can now paginate.
|
||||
|
@ -499,7 +501,7 @@ const TimelinePanel = createReactClass({
|
|||
}
|
||||
|
||||
this.setState(updatedState, () => {
|
||||
this.refs.messagePanel.updateTimelineMinHeight();
|
||||
this._messagePanel.current.updateTimelineMinHeight();
|
||||
if (callRMUpdated) {
|
||||
this.props.onReadMarkerUpdated();
|
||||
}
|
||||
|
@ -510,13 +512,13 @@ const TimelinePanel = createReactClass({
|
|||
onRoomTimelineReset: function(room, timelineSet) {
|
||||
if (timelineSet !== this.props.timelineSet) return;
|
||||
|
||||
if (this.refs.messagePanel && this.refs.messagePanel.isAtBottom()) {
|
||||
if (this._messagePanel.current && this._messagePanel.current.isAtBottom()) {
|
||||
this._loadTimeline();
|
||||
}
|
||||
},
|
||||
|
||||
canResetTimeline: function() {
|
||||
return this.refs.messagePanel && this.refs.messagePanel.isAtBottom();
|
||||
return this._messagePanel.current && this._messagePanel.current.isAtBottom();
|
||||
},
|
||||
|
||||
onRoomRedaction: function(ev, room) {
|
||||
|
@ -629,7 +631,7 @@ const TimelinePanel = createReactClass({
|
|||
sendReadReceipt: function() {
|
||||
if (SettingsStore.getValue("lowBandwidth")) return;
|
||||
|
||||
if (!this.refs.messagePanel) return;
|
||||
if (!this._messagePanel.current) return;
|
||||
if (!this.props.manageReadReceipts) return;
|
||||
// This happens on user_activity_end which is delayed, and it's
|
||||
// very possible have logged out within that timeframe, so check
|
||||
|
@ -815,8 +817,8 @@ const TimelinePanel = createReactClass({
|
|||
if (this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||
this._loadTimeline();
|
||||
} else {
|
||||
if (this.refs.messagePanel) {
|
||||
this.refs.messagePanel.scrollToBottom();
|
||||
if (this._messagePanel.current) {
|
||||
this._messagePanel.current.scrollToBottom();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -826,7 +828,7 @@ const TimelinePanel = createReactClass({
|
|||
*/
|
||||
jumpToReadMarker: function() {
|
||||
if (!this.props.manageReadMarkers) return;
|
||||
if (!this.refs.messagePanel) return;
|
||||
if (!this._messagePanel.current) return;
|
||||
if (!this.state.readMarkerEventId) return;
|
||||
|
||||
// we may not have loaded the event corresponding to the read-marker
|
||||
|
@ -835,11 +837,11 @@ const TimelinePanel = createReactClass({
|
|||
//
|
||||
// a quick way to figure out if we've loaded the relevant event is
|
||||
// simply to check if the messagepanel knows where the read-marker is.
|
||||
const ret = this.refs.messagePanel.getReadMarkerPosition();
|
||||
const ret = this._messagePanel.current.getReadMarkerPosition();
|
||||
if (ret !== null) {
|
||||
// The messagepanel knows where the RM is, so we must have loaded
|
||||
// the relevant event.
|
||||
this.refs.messagePanel.scrollToEvent(this.state.readMarkerEventId,
|
||||
this._messagePanel.current.scrollToEvent(this.state.readMarkerEventId,
|
||||
0, 1/3);
|
||||
return;
|
||||
}
|
||||
|
@ -874,8 +876,8 @@ const TimelinePanel = createReactClass({
|
|||
* at the end of the live timeline.
|
||||
*/
|
||||
isAtEndOfLiveTimeline: function() {
|
||||
return this.refs.messagePanel
|
||||
&& this.refs.messagePanel.isAtBottom()
|
||||
return this._messagePanel.current
|
||||
&& this._messagePanel.current.isAtBottom()
|
||||
&& this._timelineWindow
|
||||
&& !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
||||
},
|
||||
|
@ -887,8 +889,8 @@ const TimelinePanel = createReactClass({
|
|||
* returns null if we are not mounted.
|
||||
*/
|
||||
getScrollState: function() {
|
||||
if (!this.refs.messagePanel) { return null; }
|
||||
return this.refs.messagePanel.getScrollState();
|
||||
if (!this._messagePanel.current) { return null; }
|
||||
return this._messagePanel.current.getScrollState();
|
||||
},
|
||||
|
||||
// returns one of:
|
||||
|
@ -899,9 +901,9 @@ const TimelinePanel = createReactClass({
|
|||
// +1: read marker is below the window
|
||||
getReadMarkerPosition: function() {
|
||||
if (!this.props.manageReadMarkers) return null;
|
||||
if (!this.refs.messagePanel) return null;
|
||||
if (!this._messagePanel.current) return null;
|
||||
|
||||
const ret = this.refs.messagePanel.getReadMarkerPosition();
|
||||
const ret = this._messagePanel.current.getReadMarkerPosition();
|
||||
if (ret !== null) {
|
||||
return ret;
|
||||
}
|
||||
|
@ -936,7 +938,7 @@ const TimelinePanel = createReactClass({
|
|||
* We pass it down to the scroll panel.
|
||||
*/
|
||||
handleScrollKey: function(ev) {
|
||||
if (!this.refs.messagePanel) { return; }
|
||||
if (!this._messagePanel.current) { return; }
|
||||
|
||||
// jump to the live timeline on ctrl-end, rather than the end of the
|
||||
// timeline window.
|
||||
|
@ -944,7 +946,7 @@ const TimelinePanel = createReactClass({
|
|||
ev.keyCode == KeyCode.END) {
|
||||
this.jumpToLiveTimeline();
|
||||
} else {
|
||||
this.refs.messagePanel.handleScrollKey(ev);
|
||||
this._messagePanel.current.handleScrollKey(ev);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -986,8 +988,8 @@ const TimelinePanel = createReactClass({
|
|||
const onLoaded = () => {
|
||||
// clear the timeline min-height when
|
||||
// (re)loading the timeline
|
||||
if (this.refs.messagePanel) {
|
||||
this.refs.messagePanel.onTimelineReset();
|
||||
if (this._messagePanel.current) {
|
||||
this._messagePanel.current.onTimelineReset();
|
||||
}
|
||||
this._reloadEvents();
|
||||
|
||||
|
@ -1002,7 +1004,7 @@ const TimelinePanel = createReactClass({
|
|||
timelineLoading: false,
|
||||
}, () => {
|
||||
// initialise the scroll state of the message panel
|
||||
if (!this.refs.messagePanel) {
|
||||
if (!this._messagePanel.current) {
|
||||
// this shouldn't happen - we know we're mounted because
|
||||
// we're in a setState callback, and we know
|
||||
// timelineLoading is now false, so render() should have
|
||||
|
@ -1012,10 +1014,10 @@ const TimelinePanel = createReactClass({
|
|||
return;
|
||||
}
|
||||
if (eventId) {
|
||||
this.refs.messagePanel.scrollToEvent(eventId, pixelOffset,
|
||||
this._messagePanel.current.scrollToEvent(eventId, pixelOffset,
|
||||
offsetBase);
|
||||
} else {
|
||||
this.refs.messagePanel.scrollToBottom();
|
||||
this._messagePanel.current.scrollToBottom();
|
||||
}
|
||||
|
||||
this.sendReadReceipt();
|
||||
|
@ -1134,7 +1136,7 @@ const TimelinePanel = createReactClass({
|
|||
const ignoreOwn = opts.ignoreOwn || false;
|
||||
const allowPartial = opts.allowPartial || false;
|
||||
|
||||
const messagePanel = this.refs.messagePanel;
|
||||
const messagePanel = this._messagePanel.current;
|
||||
if (messagePanel === undefined) return null;
|
||||
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
|
@ -1313,7 +1315,8 @@ const TimelinePanel = createReactClass({
|
|||
['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState)
|
||||
);
|
||||
return (
|
||||
<MessagePanel ref="messagePanel"
|
||||
<MessagePanel
|
||||
ref={this._messagePanel}
|
||||
room={this.props.timelineSet.room}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
hidden={this.props.hidden}
|
||||
|
|
|
@ -17,15 +17,13 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as ContextualMenu from './ContextualMenu';
|
||||
import {TopLeftMenu} from '../views/context_menus/TopLeftMenu';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
import BaseAvatar from '../views/avatars/BaseAvatar';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import Avatar from '../../Avatar';
|
||||
import { _t } from '../../languageHandler';
|
||||
import dis from "../../dispatcher";
|
||||
import {focusCapturedRef} from "../../utils/Accessibility";
|
||||
import {ContextMenu, ContextMenuButton} from "./ContextMenu";
|
||||
|
||||
const AVATAR_SIZE = 28;
|
||||
|
||||
|
@ -40,11 +38,8 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
super();
|
||||
this.state = {
|
||||
menuDisplayed: false,
|
||||
menuFunctions: null, // should be { close: fn }
|
||||
profileInfo: null,
|
||||
};
|
||||
|
||||
this.onToggleMenu = this.onToggleMenu.bind(this);
|
||||
}
|
||||
|
||||
async _getProfileInfo() {
|
||||
|
@ -95,7 +90,21 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
openMenu = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.setState({ menuDisplayed: true });
|
||||
};
|
||||
|
||||
closeMenu = () => {
|
||||
this.setState({
|
||||
menuDisplayed: false,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const cli = MatrixClientPeg.get().getUserId();
|
||||
|
||||
const name = this._getDisplayName();
|
||||
let nameElement;
|
||||
let chevronElement;
|
||||
|
@ -106,14 +115,29 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
chevronElement = <span className="mx_TopLeftMenuButton_chevron" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton
|
||||
let contextMenu;
|
||||
if (this.state.menuDisplayed) {
|
||||
const elementRect = this._buttonRef.getBoundingClientRect();
|
||||
|
||||
contextMenu = (
|
||||
<ContextMenu
|
||||
chevronFace="none"
|
||||
left={elementRect.left}
|
||||
top={elementRect.top + elementRect.height}
|
||||
onFinished={this.closeMenu}
|
||||
>
|
||||
<TopLeftMenu displayName={name} userId={cli} onFinished={this.closeMenu} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<ContextMenuButton
|
||||
className="mx_TopLeftMenuButton"
|
||||
onClick={this.onToggleMenu}
|
||||
onClick={this.openMenu}
|
||||
inputRef={(r) => this._buttonRef = r}
|
||||
aria-label={_t("Your profile")}
|
||||
aria-haspopup={true}
|
||||
aria-expanded={this.state.menuDisplayed}
|
||||
label={_t("Your profile")}
|
||||
isExpanded={this.state.menuDisplayed}
|
||||
>
|
||||
<BaseAvatar
|
||||
idName={MatrixClientPeg.get().getUserId()}
|
||||
|
@ -125,34 +149,9 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
/>
|
||||
{ nameElement }
|
||||
{ chevronElement }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
</ContextMenuButton>
|
||||
|
||||
onToggleMenu(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (this.state.menuDisplayed && this.state.menuFunctions) {
|
||||
this.state.menuFunctions.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const elementRect = e.currentTarget.getBoundingClientRect();
|
||||
const x = elementRect.left;
|
||||
const y = elementRect.top + elementRect.height;
|
||||
|
||||
const menuFunctions = ContextualMenu.createMenu(TopLeftMenu, {
|
||||
chevronFace: "none",
|
||||
left: x,
|
||||
top: y,
|
||||
userId: MatrixClientPeg.get().getUserId(),
|
||||
displayName: this._getDisplayName(),
|
||||
containerRef: focusCapturedRef, // Focus the TopLeftMenu on first render
|
||||
onFinished: () => {
|
||||
this.setState({ menuDisplayed: false, menuFunctions: null });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true, menuFunctions });
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -48,6 +48,8 @@ module.exports = createReactClass({
|
|||
|
||||
componentWillMount: function() {
|
||||
this._captchaWidgetId = null;
|
||||
|
||||
this._recaptchaContainer = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -67,7 +69,7 @@ module.exports = createReactClass({
|
|||
scriptTag.setAttribute(
|
||||
'src', `${protocol}//www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
|
||||
);
|
||||
this.refs.recaptchaContainer.appendChild(scriptTag);
|
||||
this._recaptchaContainer.current.appendChild(scriptTag);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -124,11 +126,11 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div ref="recaptchaContainer">
|
||||
<div ref={this._recaptchaContainer}>
|
||||
<p>{_t(
|
||||
"This homeserver would like to make sure you are not a robot.",
|
||||
)}</p>
|
||||
<div id={DIV_ID}></div>
|
||||
<div id={DIV_ID} />
|
||||
{ error }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import url from 'url';
|
||||
|
@ -581,6 +581,8 @@ export const FallbackAuthEntry = createReactClass({
|
|||
// the popup if we open it immediately.
|
||||
this._popupWindow = null;
|
||||
window.addEventListener("message", this._onReceiveMessage);
|
||||
|
||||
this._fallbackButton = createRef();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -591,8 +593,8 @@ export const FallbackAuthEntry = createReactClass({
|
|||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.refs.fallbackButton) {
|
||||
this.refs.fallbackButton.focus();
|
||||
if (this._fallbackButton.current) {
|
||||
this._fallbackButton.current.focus();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -624,7 +626,7 @@ export const FallbackAuthEntry = createReactClass({
|
|||
}
|
||||
return (
|
||||
<div>
|
||||
<a ref="fallbackButton" onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
|
||||
<a ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
|
||||
{errorSection}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
import classNames from 'classnames';
|
||||
import * as ContextualMenu from "../../structures/ContextualMenu";
|
||||
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
||||
|
||||
export default class MemberStatusMessageAvatar extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -43,7 +43,10 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
|||
|
||||
this.state = {
|
||||
hasStatus: this.hasStatus,
|
||||
menuDisplayed: false,
|
||||
};
|
||||
|
||||
this._button = createRef();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
|
@ -86,25 +89,12 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onClick = (e) => {
|
||||
e.stopPropagation();
|
||||
openMenu = () => {
|
||||
this.setState({ menuDisplayed: true });
|
||||
};
|
||||
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
const x = (elementRect.left + window.pageXOffset);
|
||||
const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom
|
||||
const chevronOffset = (elementRect.width - chevronWidth) / 2;
|
||||
const chevronMargin = 1; // Add some spacing away from target
|
||||
const y = elementRect.top + window.pageYOffset - chevronMargin;
|
||||
|
||||
ContextualMenu.createMenu(StatusMessageContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
chevronFace: 'bottom',
|
||||
left: x,
|
||||
top: y,
|
||||
menuWidth: 226,
|
||||
user: this.props.member.user,
|
||||
});
|
||||
closeMenu = () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -124,10 +114,39 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
|||
"mx_MemberStatusMessageAvatar_hasStatus": this.state.hasStatus,
|
||||
});
|
||||
|
||||
return <AccessibleButton className={classes}
|
||||
element="div" onClick={this._onClick}
|
||||
>
|
||||
{avatar}
|
||||
</AccessibleButton>;
|
||||
let contextMenu;
|
||||
if (this.state.menuDisplayed) {
|
||||
const elementRect = this._button.current.getBoundingClientRect();
|
||||
|
||||
const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom
|
||||
const chevronMargin = 1; // Add some spacing away from target
|
||||
|
||||
contextMenu = (
|
||||
<ContextMenu
|
||||
chevronOffset={(elementRect.width - chevronWidth) / 2}
|
||||
chevronFace="bottom"
|
||||
left={elementRect.left + window.pageXOffset}
|
||||
top={elementRect.top + window.pageYOffset - chevronMargin}
|
||||
menuWidth={226}
|
||||
onFinished={this.closeMenu}
|
||||
>
|
||||
<StatusMessageContextMenu user={this.props.member.user} onFinished={this.closeMenu} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<ContextMenuButton
|
||||
className={classes}
|
||||
inputRef={this._button}
|
||||
onClick={this.openMenu}
|
||||
isExpanded={this.state.menuDisplayed}
|
||||
label={_t("User Status")}
|
||||
>
|
||||
{avatar}
|
||||
</ContextMenuButton>
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import { _t } from '../../../languageHandler';
|
|||
import Modal from '../../../Modal';
|
||||
import {Group} from 'matrix-js-sdk';
|
||||
import GroupStore from "../../../stores/GroupStore";
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
|
||||
export default class GroupInviteTileContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -36,7 +37,7 @@ export default class GroupInviteTileContextMenu extends React.Component {
|
|||
this._onClickReject = this._onClickReject.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this._unmounted = false;
|
||||
}
|
||||
|
||||
|
@ -78,12 +79,11 @@ export default class GroupInviteTileContextMenu extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return <div>
|
||||
<AccessibleButton className="mx_RoomTileContextMenu_leave" onClick={this._onClickReject} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" />
|
||||
<MenuItem className="mx_RoomTileContextMenu_leave" onClick={this._onClickReject}>
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" alt="" />
|
||||
{ _t('Reject') }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import Resend from '../../../Resend';
|
|||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
|
||||
function canCancel(eventStatus) {
|
||||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||
|
@ -289,8 +290,6 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const me = cli.getUserId();
|
||||
const mxEvent = this.props.mxEvent;
|
||||
|
@ -322,89 +321,89 @@ module.exports = createReactClass({
|
|||
if (!mxEvent.isRedacted()) {
|
||||
if (eventStatus === EventStatus.NOT_SENT) {
|
||||
resendButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
|
||||
{ _t('Resend') }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
if (editStatus === EventStatus.NOT_SENT) {
|
||||
resendEditButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendEditClick}>
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendEditClick}>
|
||||
{ _t('Resend edit') }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
if (unsentReactionsCount !== 0) {
|
||||
resendReactionsButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}>
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}>
|
||||
{ _t('Resend %(unsentCount)s reaction(s)', {unsentCount: unsentReactionsCount}) }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (redactStatus === EventStatus.NOT_SENT) {
|
||||
resendRedactionButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendRedactionClick}>
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendRedactionClick}>
|
||||
{ _t('Resend removal') }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSent && this.state.canRedact) {
|
||||
redactButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
|
||||
{ _t('Remove') }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
if (allowCancel) {
|
||||
cancelButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
|
||||
{ _t('Cancel Sending') }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
if (isContentActionable(mxEvent)) {
|
||||
forwardButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
|
||||
{ _t('Forward Message') }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
if (this.state.canPin) {
|
||||
pinButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
|
||||
{ this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const viewSourceButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
|
||||
{ _t('View Source') }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
if (mxEvent.getType() !== mxEvent.getWireType()) {
|
||||
viewClearSourceButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}>
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}>
|
||||
{ _t('View Decrypted Source') }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.eventTileOps) {
|
||||
if (this.props.eventTileOps.isWidgetHidden()) {
|
||||
unhidePreviewButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
|
||||
{ _t('Unhide Preview') }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -415,19 +414,19 @@ module.exports = createReactClass({
|
|||
}
|
||||
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
|
||||
const permalinkButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field">
|
||||
<MenuItem className="mx_MessageContextMenu_field">
|
||||
<a href={permalink} target="_blank" rel="noopener" onClick={this.onPermalinkClick} tabIndex={-1}>
|
||||
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
|
||||
? _t('Share Permalink') : _t('Share Message') }
|
||||
</a>
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
if (this.props.eventTileOps && this.props.eventTileOps.getInnerText) {
|
||||
quoteButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
|
||||
{ _t('Quote') }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -437,7 +436,7 @@ module.exports = createReactClass({
|
|||
isUrlPermitted(mxEvent.event.content.external_url)
|
||||
) {
|
||||
externalURLButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field">
|
||||
<MenuItem className="mx_MessageContextMenu_field">
|
||||
<a
|
||||
href={mxEvent.event.content.external_url}
|
||||
target="_blank"
|
||||
|
@ -447,33 +446,33 @@ module.exports = createReactClass({
|
|||
>
|
||||
{ _t('Source URL') }
|
||||
</a>
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.collapseReplyThread) {
|
||||
collapseReplyThread = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
|
||||
{ _t('Collapse Reply Thread') }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
let e2eInfo;
|
||||
if (this.props.e2eInfoCallback) {
|
||||
e2eInfo = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
|
||||
{ _t('End-to-end encryption information') }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
let reportEventButton;
|
||||
if (mxEvent.getSender() !== me) {
|
||||
reportEventButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onReportEventClick}>
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onReportEventClick}>
|
||||
{ _t('Report Content') }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,36 @@ import Modal from '../../../Modal';
|
|||
import RoomListActions from '../../../actions/RoomListActions';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import {sleep} from "../../../utils/promise";
|
||||
import {MenuItem, MenuItemCheckbox, MenuItemRadio} from "../../structures/ContextMenu";
|
||||
|
||||
const RoomTagOption = ({active, onClick, src, srcSet, label}) => {
|
||||
const classes = classNames('mx_RoomTileContextMenu_tag_field', {
|
||||
'mx_RoomTileContextMenu_tag_fieldSet': active,
|
||||
'mx_RoomTileContextMenu_tag_fieldDisabled': false,
|
||||
});
|
||||
|
||||
return (
|
||||
<MenuItemCheckbox className={classes} onClick={onClick} active={active} label={label}>
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={src} width="15" height="15" alt="" />
|
||||
<img className="mx_RoomTileContextMenu_tag_icon_set" src={srcSet} width="15" height="15" alt="" />
|
||||
{ label }
|
||||
</MenuItemCheckbox>
|
||||
);
|
||||
};
|
||||
|
||||
const NotifOption = ({active, onClick, src, label}) => {
|
||||
const classes = classNames('mx_RoomTileContextMenu_notif_field', {
|
||||
'mx_RoomTileContextMenu_notif_fieldSet': active,
|
||||
});
|
||||
|
||||
return (
|
||||
<MenuItemRadio className={classes} onClick={onClick} active={active} label={label}>
|
||||
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" alt="" />
|
||||
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={src} width="16" height="12" alt="" />
|
||||
{ label }
|
||||
</MenuItemRadio>
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = createReactClass({
|
||||
displayName: 'RoomTileContextMenu',
|
||||
|
@ -228,53 +258,36 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
_renderNotifMenu: function() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const alertMeClasses = classNames({
|
||||
'mx_RoomTileContextMenu_notif_field': true,
|
||||
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES_LOUD,
|
||||
});
|
||||
|
||||
const allNotifsClasses = classNames({
|
||||
'mx_RoomTileContextMenu_notif_field': true,
|
||||
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES,
|
||||
});
|
||||
|
||||
const mentionsClasses = classNames({
|
||||
'mx_RoomTileContextMenu_notif_field': true,
|
||||
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.MENTIONS_ONLY,
|
||||
});
|
||||
|
||||
const muteNotifsClasses = classNames({
|
||||
'mx_RoomTileContextMenu_notif_field': true,
|
||||
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.MUTE,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx_RoomTileContextMenu">
|
||||
<div className="mx_RoomTileContextMenu_notif_picker">
|
||||
<img src={require("../../../../res/img/notif-slider.svg")} width="20" height="107" />
|
||||
<div className="mx_RoomTileContextMenu" role="group" aria-label={_t("Notification settings")}>
|
||||
<div className="mx_RoomTileContextMenu_notif_picker" role="presentation">
|
||||
<img src={require("../../../../res/img/notif-slider.svg")} width="20" height="107" alt="" />
|
||||
</div>
|
||||
<AccessibleButton className={alertMeClasses} onClick={this._onClickAlertMe}>
|
||||
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
|
||||
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-off-copy.svg")} width="16" height="12" />
|
||||
{ _t('All messages (noisy)') }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className={allNotifsClasses} onClick={this._onClickAllNotifs}>
|
||||
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
|
||||
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-off.svg")} width="16" height="12" />
|
||||
{ _t('All messages') }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className={mentionsClasses} onClick={this._onClickMentions}>
|
||||
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
|
||||
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-mentions.svg")} width="16" height="12" />
|
||||
{ _t('Mentions only') }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className={muteNotifsClasses} onClick={this._onClickMute}>
|
||||
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
|
||||
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute.svg")} width="16" height="12" />
|
||||
{ _t('Mute') }
|
||||
</AccessibleButton>
|
||||
|
||||
<NotifOption
|
||||
active={this.state.roomNotifState === RoomNotifs.ALL_MESSAGES_LOUD}
|
||||
label={_t('All messages (noisy)')}
|
||||
onClick={this._onClickAlertMe}
|
||||
src={require("../../../../res/img/icon-context-mute-off-copy.svg")}
|
||||
/>
|
||||
<NotifOption
|
||||
active={this.state.roomNotifState === RoomNotifs.ALL_MESSAGES}
|
||||
label={_t('All messages')}
|
||||
onClick={this._onClickAllNotifs}
|
||||
src={require("../../../../res/img/icon-context-mute-off.svg")}
|
||||
/>
|
||||
<NotifOption
|
||||
active={this.state.roomNotifState === RoomNotifs.MENTIONS_ONLY}
|
||||
label={_t('Mentions only')}
|
||||
onClick={this._onClickMentions}
|
||||
src={require("../../../../res/img/icon-context-mute-mentions.svg")}
|
||||
/>
|
||||
<NotifOption
|
||||
active={this.state.roomNotifState === RoomNotifs.MUTE}
|
||||
label={_t('Mute')}
|
||||
onClick={this._onClickMute}
|
||||
src={require("../../../../res/img/icon-context-mute.svg")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -290,13 +303,12 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
_renderSettingsMenu: function() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<div>
|
||||
<AccessibleButton className="mx_RoomTileContextMenu_tag_field" onClick={this._onClickSettings} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icons-settings-room.svg")} width="15" height="15" />
|
||||
<MenuItem className="mx_RoomTileContextMenu_tag_field" onClick={this._onClickSettings}>
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icons-settings-room.svg")} width="15" height="15" alt="" />
|
||||
{ _t('Settings') }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -306,8 +318,6 @@ module.exports = createReactClass({
|
|||
return null;
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let leaveClickHandler = null;
|
||||
let leaveText = null;
|
||||
|
||||
|
@ -329,52 +339,38 @@ module.exports = createReactClass({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<AccessibleButton className="mx_RoomTileContextMenu_leave" onClick={leaveClickHandler} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" />
|
||||
<MenuItem className="mx_RoomTileContextMenu_leave" onClick={leaveClickHandler}>
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" alt="" />
|
||||
{ leaveText }
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_renderRoomTagMenu: function() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const favouriteClasses = classNames({
|
||||
'mx_RoomTileContextMenu_tag_field': true,
|
||||
'mx_RoomTileContextMenu_tag_fieldSet': this.state.isFavourite,
|
||||
'mx_RoomTileContextMenu_tag_fieldDisabled': false,
|
||||
});
|
||||
|
||||
const lowPriorityClasses = classNames({
|
||||
'mx_RoomTileContextMenu_tag_field': true,
|
||||
'mx_RoomTileContextMenu_tag_fieldSet': this.state.isLowPriority,
|
||||
'mx_RoomTileContextMenu_tag_fieldDisabled': false,
|
||||
});
|
||||
|
||||
const dmClasses = classNames({
|
||||
'mx_RoomTileContextMenu_tag_field': true,
|
||||
'mx_RoomTileContextMenu_tag_fieldSet': this.state.isDirectMessage,
|
||||
'mx_RoomTileContextMenu_tag_fieldDisabled': false,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AccessibleButton className={favouriteClasses} onClick={this._onClickFavourite} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_fave.svg")} width="15" height="15" />
|
||||
<img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_fave_on.svg")} width="15" height="15" />
|
||||
{ _t('Favourite') }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className={lowPriorityClasses} onClick={this._onClickLowPriority} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_low.svg")} width="15" height="15" />
|
||||
<img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_low_on.svg")} width="15" height="15" />
|
||||
{ _t('Low Priority') }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className={dmClasses} onClick={this._onClickDM} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_person.svg")} width="15" height="15" />
|
||||
<img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_person_on.svg")} width="15" height="15" />
|
||||
{ _t('Direct Chat') }
|
||||
</AccessibleButton>
|
||||
<RoomTagOption
|
||||
active={this.state.isFavourite}
|
||||
label={_t('Favourite')}
|
||||
onClick={this._onClickFavourite}
|
||||
src={require("../../../../res/img/icon_context_fave.svg")}
|
||||
srcSet={require("../../../../res/img/icon_context_fave_on.svg")}
|
||||
/>
|
||||
<RoomTagOption
|
||||
active={this.state.isLowPriority}
|
||||
label={_t('Low Priority')}
|
||||
onClick={this._onClickLowPriority}
|
||||
src={require("../../../../res/img/icon_context_low.svg")}
|
||||
srcSet={require("../../../../res/img/icon_context_low_on.svg")}
|
||||
/>
|
||||
<RoomTagOption
|
||||
active={this.state.isDirectMessage}
|
||||
label={_t('Direct Chat')}
|
||||
onClick={this._onClickDM}
|
||||
src={require("../../../../res/img/icon_context_person.svg")}
|
||||
srcSet={require("../../../../res/img/icon_context_person_on.svg")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -386,11 +382,11 @@ module.exports = createReactClass({
|
|||
case 'join':
|
||||
return <div>
|
||||
{ this._renderNotifMenu() }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
|
||||
{ this._renderLeaveMenu(myMembership) }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
|
||||
{ this._renderRoomTagMenu() }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
|
||||
{ this._renderSettingsMenu() }
|
||||
</div>;
|
||||
case 'invite':
|
||||
|
@ -400,7 +396,7 @@ module.exports = createReactClass({
|
|||
default:
|
||||
return <div>
|
||||
{ this._renderLeaveMenu(myMembership) }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
|
||||
{ this._renderSettingsMenu() }
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 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.
|
||||
|
@ -16,11 +17,12 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher';
|
||||
import TagOrderActions from '../../../actions/TagOrderActions';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
|
||||
export default class TagTileContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -29,6 +31,10 @@ export default class TagTileContextMenu extends React.Component {
|
|||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
|
@ -45,18 +51,15 @@ export default class TagTileContextMenu extends React.Component {
|
|||
}
|
||||
|
||||
_onRemoveClick() {
|
||||
dis.dispatch(TagOrderActions.removeTag(
|
||||
// XXX: Context menus don't have a MatrixClient context
|
||||
MatrixClientPeg.get(),
|
||||
this.props.tag,
|
||||
));
|
||||
dis.dispatch(TagOrderActions.removeTag(this.context.matrixClient, this.props.tag));
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
render() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
return <div>
|
||||
<div className="mx_TagTileContextMenu_item" onClick={this._onViewCommunityClick} >
|
||||
<MenuItem className="mx_TagTileContextMenu_item" onClick={this._onViewCommunityClick}>
|
||||
<TintableSvg
|
||||
className="mx_TagTileContextMenu_item_icon"
|
||||
src={require("../../../../res/img/icons-groups.svg")}
|
||||
|
@ -64,12 +67,12 @@ export default class TagTileContextMenu extends React.Component {
|
|||
height="15"
|
||||
/>
|
||||
{ _t('View Community') }
|
||||
</div>
|
||||
<hr className="mx_TagTileContextMenu_separator" />
|
||||
<div className="mx_TagTileContextMenu_item" onClick={this._onRemoveClick} >
|
||||
<img className="mx_TagTileContextMenu_item_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" />
|
||||
</MenuItem>
|
||||
<hr className="mx_TagTileContextMenu_separator" role="separator" />
|
||||
<MenuItem className="mx_TagTileContextMenu_item" onClick={this._onRemoveClick}>
|
||||
<img className="mx_TagTileContextMenu_item_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" alt="" />
|
||||
{ _t('Hide') }
|
||||
</div>
|
||||
</MenuItem>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ import Modal from "../../../Modal";
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import { getHostingLink } from '../../../utils/HostingLink';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from "../../../index";
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
|
||||
export class TopLeftMenu extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -58,8 +58,6 @@ export class TopLeftMenu extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const isGuest = MatrixClientPeg.get().isGuest();
|
||||
|
||||
const hostingSignupLink = getHostingLink('user-context-menu');
|
||||
|
@ -69,10 +67,10 @@ export class TopLeftMenu extends React.Component {
|
|||
{_t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
{
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener" tabIndex="0">{sub}</a>,
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener" tabIndex={-1}>{sub}</a>,
|
||||
},
|
||||
)}
|
||||
<a href={hostingSignupLink} target="_blank" rel="noopener" aria-hidden={true}>
|
||||
<a href={hostingSignupLink} target="_blank" rel="noopener" role="presentation" aria-hidden={true} tabIndex={-1}>
|
||||
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
||||
</a>
|
||||
</div>;
|
||||
|
@ -81,40 +79,40 @@ export class TopLeftMenu extends React.Component {
|
|||
let homePageItem = null;
|
||||
if (this.hasHomePage()) {
|
||||
homePageItem = (
|
||||
<AccessibleButton element="li" className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>
|
||||
<MenuItem className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>
|
||||
{_t("Home")}
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
let signInOutItem;
|
||||
if (isGuest) {
|
||||
signInOutItem = (
|
||||
<AccessibleButton element="li" className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>
|
||||
<MenuItem className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>
|
||||
{_t("Sign in")}
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
} else {
|
||||
signInOutItem = (
|
||||
<AccessibleButton element="li" className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>
|
||||
<MenuItem className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>
|
||||
{_t("Sign out")}
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
const settingsItem = (
|
||||
<AccessibleButton element="li" className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>
|
||||
<MenuItem className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>
|
||||
{_t("Settings")}
|
||||
</AccessibleButton>
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
return <div className="mx_TopLeftMenu" ref={this.props.containerRef}>
|
||||
<div className="mx_TopLeftMenu_section_noIcon" aria-readonly={true}>
|
||||
return <div className="mx_TopLeftMenu" ref={this.props.containerRef} role="menu">
|
||||
<div className="mx_TopLeftMenu_section_noIcon" aria-readonly={true} tabIndex={-1}>
|
||||
<div>{this.props.displayName}</div>
|
||||
<div className="mx_TopLeftMenu_greyedText" aria-hidden={true}>{this.props.userId}</div>
|
||||
{hostingSignup}
|
||||
</div>
|
||||
<ul className="mx_TopLeftMenu_section_withIcon">
|
||||
<ul className="mx_TopLeftMenu_section_withIcon" role="none">
|
||||
{homePageItem}
|
||||
{settingsItem}
|
||||
{signInOutItem}
|
||||
|
|
|
@ -16,8 +16,8 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
|
||||
export default class WidgetContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -71,50 +71,45 @@ export default class WidgetContextMenu extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
|
||||
|
||||
const options = [];
|
||||
|
||||
if (this.props.onEditClicked) {
|
||||
options.push(
|
||||
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onEditClicked} key='edit'>
|
||||
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onEditClicked} key='edit'>
|
||||
{_t("Edit")}
|
||||
</AccessibleButton>,
|
||||
</MenuItem>,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.onReloadClicked) {
|
||||
options.push(
|
||||
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onReloadClicked}
|
||||
key='reload'>
|
||||
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onReloadClicked} key='reload'>
|
||||
{_t("Reload")}
|
||||
</AccessibleButton>,
|
||||
</MenuItem>,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.onSnapshotClicked) {
|
||||
options.push(
|
||||
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onSnapshotClicked}
|
||||
key='snap'>
|
||||
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onSnapshotClicked} key='snap'>
|
||||
{_t("Take picture")}
|
||||
</AccessibleButton>,
|
||||
</MenuItem>,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.onDeleteClicked) {
|
||||
options.push(
|
||||
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onDeleteClicked}
|
||||
key='delete'>
|
||||
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onDeleteClicked} key='delete'>
|
||||
{_t("Remove for everyone")}
|
||||
</AccessibleButton>,
|
||||
</MenuItem>,
|
||||
);
|
||||
}
|
||||
|
||||
// Push this last so it appears last. It's always present.
|
||||
options.push(
|
||||
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onRevokeClicked} key='revoke'>
|
||||
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onRevokeClicked} key='revoke'>
|
||||
{_t("Remove for me")}
|
||||
</AccessibleButton>,
|
||||
</MenuItem>,
|
||||
);
|
||||
|
||||
// Put separators between the options
|
||||
|
|
|
@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
||||
|
@ -106,10 +106,14 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._textinput = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.props.focus) {
|
||||
// Set the cursor at the end of the text input
|
||||
this.refs.textinput.value = this.props.value;
|
||||
this._textinput.current.value = this.props.value;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -126,8 +130,8 @@ module.exports = createReactClass({
|
|||
let selectedList = this.state.selectedList.slice();
|
||||
// Check the text input field to see if user has an unconverted address
|
||||
// If there is and it's valid add it to the local selectedList
|
||||
if (this.refs.textinput.value !== '') {
|
||||
selectedList = this._addAddressesToList([this.refs.textinput.value]);
|
||||
if (this._textinput.current.value !== '') {
|
||||
selectedList = this._addAddressesToList([this._textinput.current.value]);
|
||||
if (selectedList === null) return;
|
||||
}
|
||||
this.props.onFinished(true, selectedList);
|
||||
|
@ -154,23 +158,23 @@ module.exports = createReactClass({
|
|||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.addressSelector) this.addressSelector.chooseSelection();
|
||||
} else if (this.refs.textinput.value.length === 0 && this.state.selectedList.length && e.keyCode === 8) { // backspace
|
||||
} else if (this._textinput.current.value.length === 0 && this.state.selectedList.length && e.keyCode === 8) { // backspace
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.onDismissed(this.state.selectedList.length - 1)();
|
||||
} else if (e.keyCode === 13) { // enter
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.refs.textinput.value === '') {
|
||||
if (this._textinput.current.value === '') {
|
||||
// if there's nothing in the input box, submit the form
|
||||
this.onButtonClick();
|
||||
} else {
|
||||
this._addAddressesToList([this.refs.textinput.value]);
|
||||
this._addAddressesToList([this._textinput.current.value]);
|
||||
}
|
||||
} else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this._addAddressesToList([this.refs.textinput.value]);
|
||||
this._addAddressesToList([this._textinput.current.value]);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -647,7 +651,7 @@ module.exports = createReactClass({
|
|||
onPaste={this._onPaste}
|
||||
rows="1"
|
||||
id="textinput"
|
||||
ref="textinput"
|
||||
ref={this._textinput}
|
||||
className="mx_AddressPickerDialog_input"
|
||||
onChange={this.onQueryChanged}
|
||||
placeholder={this.getPlaceholder()}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -21,15 +22,12 @@ import sdk from '../../../index';
|
|||
import Analytics from '../../../Analytics';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import * as Lifecycle from '../../../Lifecycle';
|
||||
import Velocity from 'velocity-animate';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default class DeactivateAccountDialog extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._passwordField = null;
|
||||
|
||||
this._onOk = this._onOk.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
|
||||
|
@ -78,7 +76,6 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
// https://matrix.org/jira/browse/SYN-744
|
||||
if (err.httpStatus === 401 || err.httpStatus === 403) {
|
||||
errStr = _t('Incorrect password');
|
||||
Velocity(this._passwordField, "callout.shake", 300);
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
|
@ -181,7 +178,6 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
label={_t('Password')}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
value={this.state.password}
|
||||
ref={(e) => {this._passwordField = e;}}
|
||||
className={passwordBoxClass}
|
||||
/>
|
||||
</div>
|
||||
|
|
128
src/components/views/dialogs/RoomUpgradeWarningDialog.js
Normal file
128
src/components/views/dialogs/RoomUpgradeWarningDialog.js
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
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 {_t} from "../../../languageHandler";
|
||||
import sdk from "../../../index";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import Modal from "../../../Modal";
|
||||
|
||||
export default class RoomUpgradeWarningDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
roomId: PropTypes.string.isRequired,
|
||||
targetVersion: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
const joinRules = room ? room.currentState.getStateEvents("m.room.join_rules", "") : null;
|
||||
const isPrivate = joinRules ? joinRules.getContent()['join_rule'] !== 'public' : true;
|
||||
this.state = {
|
||||
currentVersion: room ? room.getVersion() : "1",
|
||||
isPrivate,
|
||||
inviteUsersToNewRoom: true,
|
||||
};
|
||||
}
|
||||
|
||||
_onContinue = () => {
|
||||
this.props.onFinished({continue: true, invite: this.state.isPrivate && this.state.inviteUsersToNewRoom});
|
||||
};
|
||||
|
||||
_onCancel = () => {
|
||||
this.props.onFinished({continue: false, invite: false});
|
||||
};
|
||||
|
||||
_onInviteUsersToggle = (newVal) => {
|
||||
this.setState({inviteUsersToNewRoom: newVal});
|
||||
};
|
||||
|
||||
_openBugReportDialog = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
|
||||
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
let inviteToggle = null;
|
||||
if (this.state.isPrivate) {
|
||||
inviteToggle = (
|
||||
<LabelledToggleSwitch
|
||||
value={this.state.inviteUsersToNewRoom}
|
||||
onChange={this._onInviteUsersToggle}
|
||||
label={_t("Automatically invite users")} />
|
||||
);
|
||||
}
|
||||
|
||||
const title = this.state.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room");
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_RoomUpgradeWarningDialog'
|
||||
hasCancel={true}
|
||||
fixedWidth={false}
|
||||
onFinished={this.props.onFinished}
|
||||
title={title}
|
||||
>
|
||||
<div>
|
||||
<p>
|
||||
{_t(
|
||||
"Upgrading a room is an advanced action and is usually recommended when a room " +
|
||||
"is unstable due to bugs, missing features or security vulnerabilities.",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{_t(
|
||||
"This usually only affects how the room is processed on the server. If you're " +
|
||||
"having problems with your Riot, please <a>report a bug</a>.",
|
||||
{}, {
|
||||
"a": (sub) => {
|
||||
return <a href='#' onClick={this._openBugReportDialog}>{sub}</a>;
|
||||
},
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{_t(
|
||||
"You'll upgrade this room from <oldVersion /> to <newVersion />.",
|
||||
{},
|
||||
{
|
||||
oldVersion: () => <code>{this.state.currentVersion}</code>,
|
||||
newVersion: () => <code>{this.props.targetVersion}</code>,
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
{inviteToggle}
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Upgrade")}
|
||||
onPrimaryButtonClick={this._onContinue}
|
||||
cancelButton={_t("Cancel")}
|
||||
onCancel={this._onCancel}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
|
@ -62,8 +62,13 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._input_value = createRef();
|
||||
this._uiAuth = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.refs.input_value.select();
|
||||
this._input_value.current.select();
|
||||
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
},
|
||||
|
@ -102,8 +107,8 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
onSubmit: function(ev) {
|
||||
if (this.refs.uiAuth) {
|
||||
this.refs.uiAuth.tryContinue();
|
||||
if (this._uiAuth.current) {
|
||||
this._uiAuth.current.tryContinue();
|
||||
}
|
||||
this.setState({
|
||||
doingUIAuth: true,
|
||||
|
@ -215,7 +220,7 @@ export default createReactClass({
|
|||
onAuthFinished={this._onUIAuthFinished}
|
||||
inputs={{}}
|
||||
poll={true}
|
||||
ref="uiAuth"
|
||||
ref={this._uiAuth}
|
||||
continueIsManaged={true}
|
||||
/>;
|
||||
}
|
||||
|
@ -257,7 +262,7 @@ export default createReactClass({
|
|||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<div className="mx_SetMxIdDialog_input_group">
|
||||
<input type="text" ref="input_value" value={this.state.username}
|
||||
<input type="text" ref={this._input_value} value={this.state.username}
|
||||
autoFocus={true}
|
||||
onChange={this.onValueChange}
|
||||
onKeyUp={this.onKeyUp}
|
||||
|
|
|
@ -14,14 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import QRCode from 'qrcode-react';
|
||||
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import * as ContextualMenu from "../../structures/ContextualMenu";
|
||||
import * as ContextMenu from "../../structures/ContextMenu";
|
||||
import {toRightOf} from "../../structures/ContextMenu";
|
||||
|
||||
const socials = [
|
||||
{
|
||||
|
@ -73,6 +74,8 @@ export default class ShareDialog extends React.Component {
|
|||
// MatrixEvent defaults to share linkSpecificEvent
|
||||
linkSpecificEvent: this.props.target instanceof MatrixEvent,
|
||||
};
|
||||
|
||||
this._link = createRef();
|
||||
}
|
||||
|
||||
static _selectText(target) {
|
||||
|
@ -93,7 +96,7 @@ export default class ShareDialog extends React.Component {
|
|||
onCopyClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
ShareDialog._selectText(this.refs.link);
|
||||
ShareDialog._selectText(this._link.current);
|
||||
|
||||
let successful;
|
||||
try {
|
||||
|
@ -102,18 +105,12 @@ export default class ShareDialog extends React.Component {
|
|||
console.error('Failed to copy: ', err);
|
||||
}
|
||||
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const buttonRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = buttonRect.right + window.pageXOffset;
|
||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||
const {close} = ContextualMenu.createMenu(GenericTextContextMenu, {
|
||||
chevronOffset: 10,
|
||||
left: x,
|
||||
top: y,
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
}, false);
|
||||
});
|
||||
// Drop a reference to this close handler for componentWillUnmount
|
||||
this.closeCopiedTooltip = e.target.onmouseleave = close;
|
||||
}
|
||||
|
@ -200,7 +197,7 @@ export default class ShareDialog extends React.Component {
|
|||
>
|
||||
<div className="mx_ShareDialog_content">
|
||||
<div className="mx_ShareDialog_matrixto">
|
||||
<a ref="link"
|
||||
<a ref={this._link}
|
||||
href={matrixToUrl}
|
||||
onClick={ShareDialog.onLinkClick}
|
||||
className="mx_ShareDialog_matrixto_link"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
|
@ -42,15 +42,19 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._textinput = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.props.focus) {
|
||||
// Set the cursor at the end of the text input
|
||||
this.refs.textinput.value = this.props.value;
|
||||
this._textinput.current.value = this.props.value;
|
||||
}
|
||||
},
|
||||
|
||||
onOk: function() {
|
||||
this.props.onFinished(true, this.refs.textinput.value);
|
||||
this.props.onFinished(true, this._textinput.current.value);
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
|
@ -70,7 +74,13 @@ export default createReactClass({
|
|||
<label htmlFor="textinput"> { this.props.description } </label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
|
||||
<input
|
||||
id="textinput"
|
||||
ref={this._textinput}
|
||||
className="mx_TextInputDialog_input"
|
||||
defaultValue={this.props.value}
|
||||
autoFocus={this.props.focus}
|
||||
size="64" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
|
||||
import url from 'url';
|
||||
import qs from 'querystring';
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import WidgetMessaging from '../../../WidgetMessaging';
|
||||
|
@ -35,7 +35,7 @@ import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
|||
import classNames from 'classnames';
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import {createMenu} from "../../structures/ContextualMenu";
|
||||
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
||||
import PersistedElement from "./PersistedElement";
|
||||
|
||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||
|
@ -62,6 +62,10 @@ export default class AppTile extends React.Component {
|
|||
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
|
||||
this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this);
|
||||
this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
|
||||
|
||||
this._contextMenuButton = createRef();
|
||||
this._appFrame = createRef();
|
||||
this._menu_bar = createRef();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -89,6 +93,7 @@ export default class AppTile extends React.Component {
|
|||
error: null,
|
||||
deleting: false,
|
||||
widgetPageTitle: newProps.widgetPageTitle,
|
||||
menuDisplayed: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -334,14 +339,14 @@ export default class AppTile extends React.Component {
|
|||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
||||
// its hold on the webcam. Without this, the widget holds a media
|
||||
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
|
||||
if (this.refs.appFrame) {
|
||||
if (this._appFrame.current) {
|
||||
// In practice we could just do `+= ''` to trick the browser
|
||||
// into thinking the URL changed, however I can foresee this
|
||||
// being optimized out by a browser. Instead, we'll just point
|
||||
// the iframe at a page that is reasonably safe to use in the
|
||||
// event the iframe doesn't wink away.
|
||||
// This is relative to where the Riot instance is located.
|
||||
this.refs.appFrame.src = 'about:blank';
|
||||
this._appFrame.current.src = 'about:blank';
|
||||
}
|
||||
|
||||
WidgetUtils.setRoomWidget(
|
||||
|
@ -386,7 +391,7 @@ export default class AppTile extends React.Component {
|
|||
// FIXME: There's probably no reason to do this here: it should probably be done entirely
|
||||
// in ActiveWidgetStore.
|
||||
const widgetMessaging = new WidgetMessaging(
|
||||
this.props.id, this.props.url, this.props.userWidget, this.refs.appFrame.contentWindow);
|
||||
this.props.id, this.props.url, this.props.userWidget, this._appFrame.current.contentWindow);
|
||||
ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging);
|
||||
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
||||
console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities);
|
||||
|
@ -493,7 +498,7 @@ export default class AppTile extends React.Component {
|
|||
ev.preventDefault();
|
||||
|
||||
// Ignore clicks on menu bar children
|
||||
if (ev.target !== this.refs.menu_bar) {
|
||||
if (ev.target !== this._menu_bar.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -552,48 +557,15 @@ export default class AppTile extends React.Component {
|
|||
|
||||
_onReloadWidgetClick() {
|
||||
// Reload iframe in this way to avoid cross-origin restrictions
|
||||
this.refs.appFrame.src = this.refs.appFrame.src;
|
||||
this._appFrame.current.src = this._appFrame.current.src;
|
||||
}
|
||||
|
||||
_getMenuOptions(ev) {
|
||||
// TODO: This block of code gets copy/pasted a lot. We should make that happen less.
|
||||
const menuOptions = {};
|
||||
const buttonRect = ev.target.getBoundingClientRect();
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const buttonLeft = buttonRect.left + window.pageXOffset;
|
||||
const buttonTop = buttonRect.top + window.pageYOffset;
|
||||
// Align the right edge of the menu to the left edge of the button
|
||||
menuOptions.right = window.innerWidth - buttonLeft;
|
||||
// Align the menu vertically on whichever side of the button has more
|
||||
// space available.
|
||||
if (buttonTop < window.innerHeight / 2) {
|
||||
menuOptions.top = buttonTop;
|
||||
} else {
|
||||
menuOptions.bottom = window.innerHeight - buttonTop;
|
||||
}
|
||||
return menuOptions;
|
||||
}
|
||||
_onContextMenuClick = () => {
|
||||
this.setState({ menuDisplayed: true });
|
||||
};
|
||||
|
||||
_onContextMenuClick = (ev) => {
|
||||
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
|
||||
const menuOptions = {
|
||||
...this._getMenuOptions(ev),
|
||||
|
||||
// A revoke handler is always required
|
||||
onRevokeClicked: this._onRevokeClicked,
|
||||
};
|
||||
|
||||
const canUserModify = this._canUserModify();
|
||||
const showEditButton = Boolean(this._scalarClient && canUserModify);
|
||||
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
|
||||
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
|
||||
|
||||
if (showEditButton) menuOptions.onEditClicked = this._onEditClick;
|
||||
if (showDeleteButton) menuOptions.onDeleteClicked = this._onDeleteClick;
|
||||
if (showPictureSnapshotButton) menuOptions.onSnapshotClicked = this._onSnapshotClick;
|
||||
if (this.props.showReload) menuOptions.onReloadClicked = this._onReloadWidgetClick;
|
||||
|
||||
createMenu(WidgetContextMenu, menuOptions);
|
||||
_closeContextMenu = () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -601,7 +573,7 @@ export default class AppTile extends React.Component {
|
|||
|
||||
// Don't render widget if it is in the process of being deleted
|
||||
if (this.state.deleting) {
|
||||
return <div></div>;
|
||||
return <div />;
|
||||
}
|
||||
|
||||
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
|
||||
|
@ -656,7 +628,7 @@ export default class AppTile extends React.Component {
|
|||
{ this.state.loading && loadingElement }
|
||||
<iframe
|
||||
allow={iframeFeatures}
|
||||
ref="appFrame"
|
||||
ref={this._appFrame}
|
||||
src={this._getSafeUrl()}
|
||||
allowFullScreen={true}
|
||||
sandbox={sandboxFlags}
|
||||
|
@ -697,10 +669,34 @@ export default class AppTile extends React.Component {
|
|||
mx_AppTileMenuBar_expanded: this.props.show,
|
||||
});
|
||||
|
||||
return (
|
||||
let contextMenu;
|
||||
if (this.state.menuDisplayed) {
|
||||
const elementRect = this._contextMenuButton.current.getBoundingClientRect();
|
||||
|
||||
const canUserModify = this._canUserModify();
|
||||
const showEditButton = Boolean(this._scalarClient && canUserModify);
|
||||
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
|
||||
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
|
||||
|
||||
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
|
||||
contextMenu = (
|
||||
<ContextMenu {...aboveLeftOf(elementRect, null)} onFinished={this._closeContextMenu}>
|
||||
<WidgetContextMenu
|
||||
onRevokeClicked={this._onRevokeClicked}
|
||||
onEditClicked={showEditButton ? this._onEditClick : undefined}
|
||||
onDeleteClicked={showDeleteButton ? this._onDeleteClick : undefined}
|
||||
onSnapshotClicked={showPictureSnapshotButton ? this._onSnapshotClick : undefined}
|
||||
onReloadClicked={this.props.showReload ? this._onReloadWidgetClick : undefined}
|
||||
onFinished={this._closeContextMenu}
|
||||
/>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<div className={appTileClass} id={this.props.id}>
|
||||
{ this.props.showMenubar &&
|
||||
<div ref="menu_bar" className={menuBarClasses} onClick={this.onClickMenuBar}>
|
||||
<div ref={this._menu_bar} className={menuBarClasses} onClick={this.onClickMenuBar}>
|
||||
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
||||
{ /* Minimise widget */ }
|
||||
{ showMinimiseButton && <AccessibleButton
|
||||
|
@ -725,20 +721,24 @@ export default class AppTile extends React.Component {
|
|||
onClick={this._onPopoutWidgetClick}
|
||||
/> }
|
||||
{ /* Context menu */ }
|
||||
{ <AccessibleButton
|
||||
{ <ContextMenuButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
|
||||
title={_t('More options')}
|
||||
label={_t('More options')}
|
||||
isExpanded={this.state.menuDisplayed}
|
||||
inputRef={this._contextMenuButton}
|
||||
onClick={this._onContextMenuClick}
|
||||
/> }
|
||||
</span>
|
||||
</div> }
|
||||
{ appTileBody }
|
||||
</div>
|
||||
);
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
}
|
||||
}
|
||||
|
||||
AppTile.displayName ='AppTile';
|
||||
AppTile.displayName = 'AppTile';
|
||||
|
||||
AppTile.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import {Key} from "../../../Keyboard";
|
||||
|
@ -65,7 +65,7 @@ module.exports = createReactClass({
|
|||
componentWillReceiveProps: function(nextProps) {
|
||||
if (nextProps.initialValue !== this.props.initialValue) {
|
||||
this.value = nextProps.initialValue;
|
||||
if (this.refs.editable_div) {
|
||||
if (this._editable_div.current) {
|
||||
this.showPlaceholder(!this.value);
|
||||
}
|
||||
}
|
||||
|
@ -76,24 +76,27 @@ module.exports = createReactClass({
|
|||
// as React doesn't play nice with contentEditable.
|
||||
this.value = '';
|
||||
this.placeholder = false;
|
||||
|
||||
this._editable_div = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.value = this.props.initialValue;
|
||||
if (this.refs.editable_div) {
|
||||
if (this._editable_div.current) {
|
||||
this.showPlaceholder(!this.value);
|
||||
}
|
||||
},
|
||||
|
||||
showPlaceholder: function(show) {
|
||||
if (show) {
|
||||
this.refs.editable_div.textContent = this.props.placeholder;
|
||||
this.refs.editable_div.setAttribute("class", this.props.className + " " + this.props.placeholderClassName);
|
||||
this._editable_div.current.textContent = this.props.placeholder;
|
||||
this._editable_div.current.setAttribute("class", this.props.className
|
||||
+ " " + this.props.placeholderClassName);
|
||||
this.placeholder = true;
|
||||
this.value = '';
|
||||
} else {
|
||||
this.refs.editable_div.textContent = this.value;
|
||||
this.refs.editable_div.setAttribute("class", this.props.className);
|
||||
this._editable_div.current.textContent = this.value;
|
||||
this._editable_div.current.setAttribute("class", this.props.className);
|
||||
this.placeholder = false;
|
||||
}
|
||||
},
|
||||
|
@ -120,7 +123,7 @@ module.exports = createReactClass({
|
|||
this.value = this.props.initialValue;
|
||||
this.showPlaceholder(!this.value);
|
||||
this.onValueChanged(false);
|
||||
this.refs.editable_div.blur();
|
||||
this._editable_div.current.blur();
|
||||
},
|
||||
|
||||
onValueChanged: function(shouldSubmit) {
|
||||
|
@ -219,7 +222,7 @@ module.exports = createReactClass({
|
|||
</div>;
|
||||
} else {
|
||||
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
||||
editableEl = <div ref="editable_div"
|
||||
editableEl = <div ref={this._editable_div}
|
||||
contentEditable={true}
|
||||
className={className}
|
||||
onKeyDown={this.onKeyDown}
|
||||
|
|
|
@ -90,7 +90,7 @@ function isInLowerLeftHalf(x, y, rect) {
|
|||
* tooltip along one edge of the target.
|
||||
*/
|
||||
export default class InteractiveTooltip extends React.Component {
|
||||
propTypes: {
|
||||
static propTypes = {
|
||||
// Content to show in the tooltip
|
||||
content: PropTypes.node.isRequired,
|
||||
// Function to call when visibility of the tooltip changes
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd.
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
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.
|
||||
|
@ -15,20 +16,21 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import classNames from 'classnames';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
|
||||
import * as ContextualMenu from '../../structures/ContextualMenu';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import TagOrderStore from '../../../stores/TagOrderStore';
|
||||
import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu";
|
||||
|
||||
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
|
||||
// a thing to click on for the user to filter the visible rooms in the RoomList to:
|
||||
|
@ -58,6 +60,8 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
componentDidMount() {
|
||||
this._contextMenuButton = createRef();
|
||||
|
||||
this.unmounted = false;
|
||||
if (this.props.tag[0] === '+') {
|
||||
FlairStore.addListener('updateGroupProfile', this._onFlairStoreUpdated);
|
||||
|
@ -107,56 +111,35 @@ export default createReactClass({
|
|||
}
|
||||
},
|
||||
|
||||
_openContextMenu: function(x, y, chevronOffset) {
|
||||
// Hide the (...) immediately
|
||||
this.setState({ hover: false });
|
||||
|
||||
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||
ContextualMenu.createMenu(TagTileContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
tag: this.props.tag,
|
||||
onFinished: () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
},
|
||||
|
||||
onContextButtonClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = elementRect.right + window.pageXOffset + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||
|
||||
this._openContextMenu(x, y, chevronOffset);
|
||||
},
|
||||
|
||||
onContextMenu: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const chevronOffset = 12;
|
||||
this._openContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
|
||||
},
|
||||
|
||||
onMouseOver: function() {
|
||||
console.log("DEBUG onMouseOver");
|
||||
this.setState({hover: true});
|
||||
},
|
||||
|
||||
onMouseOut: function() {
|
||||
console.log("DEBUG onMouseOut");
|
||||
this.setState({hover: false});
|
||||
},
|
||||
|
||||
openMenu: function(e) {
|
||||
// Prevent the TagTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({
|
||||
menuDisplayed: true,
|
||||
hover: false,
|
||||
});
|
||||
},
|
||||
|
||||
closeMenu: function() {
|
||||
this.setState({
|
||||
menuDisplayed: false,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const Tooltip = sdk.getComponent('elements.Tooltip');
|
||||
const profile = this.state.profile || {};
|
||||
const name = profile.name || this.props.tag;
|
||||
|
@ -184,23 +167,46 @@ export default createReactClass({
|
|||
const tip = this.state.hover ?
|
||||
<Tooltip className="mx_TagTile_tooltip" label={name} /> :
|
||||
<div />;
|
||||
// FIXME: this ought to use AccessibleButton for a11y but that causes onMouseOut/onMouseOver to fire too much
|
||||
const contextButton = this.state.hover || this.state.menuDisplayed ?
|
||||
<div className="mx_TagTile_context_button" onClick={this.onContextButtonClick}>
|
||||
<div className="mx_TagTile_context_button" onClick={this.openMenu} ref={this._contextMenuButton}>
|
||||
{ "\u00B7\u00B7\u00B7" }
|
||||
</div> : <div />;
|
||||
return <AccessibleButton className={className} onClick={this.onClick} onContextMenu={this.onContextMenu}>
|
||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
idName={this.props.tag}
|
||||
url={httpUrl}
|
||||
width={avatarHeight}
|
||||
height={avatarHeight}
|
||||
/>
|
||||
{ tip }
|
||||
{ contextButton }
|
||||
{ badgeElement }
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
</div> : <div ref={this._contextMenuButton} />;
|
||||
|
||||
let contextMenu;
|
||||
if (this.state.menuDisplayed) {
|
||||
const elementRect = this._contextMenuButton.current.getBoundingClientRect();
|
||||
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||
contextMenu = (
|
||||
<ContextMenu {...toRightOf(elementRect)} onFinished={this.closeMenu}>
|
||||
<TagTileContextMenu tag={this.props.tag} onFinished={this.closeMenu} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<ContextMenuButton
|
||||
className={className}
|
||||
onClick={this.onClick}
|
||||
onContextMenu={this.openMenu}
|
||||
label={_t("Options")}
|
||||
isExpanded={this.state.menuDisplayed}
|
||||
>
|
||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
idName={this.props.tag}
|
||||
url={httpUrl}
|
||||
width={avatarHeight}
|
||||
height={avatarHeight}
|
||||
/>
|
||||
{ tip }
|
||||
{ contextButton }
|
||||
{ badgeElement }
|
||||
</div>
|
||||
</ContextMenuButton>
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -34,6 +34,10 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._user_id_input = createRef();
|
||||
},
|
||||
|
||||
addUser: function(user_id) {
|
||||
if (this.props.selected_users.indexOf(user_id == -1)) {
|
||||
this.props.onChange(this.props.selected_users.concat([user_id]));
|
||||
|
@ -47,20 +51,20 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
onAddUserId: function() {
|
||||
this.addUser(this.refs.user_id_input.value);
|
||||
this.refs.user_id_input.value = "";
|
||||
this.addUser(this._user_id_input.current.value);
|
||||
this._user_id_input.current.value = "";
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const self = this;
|
||||
return (
|
||||
<div>
|
||||
<ul className="mx_UserSelector_UserIdList" ref="list">
|
||||
<ul className="mx_UserSelector_UserIdList">
|
||||
{ this.props.selected_users.map(function(user_id, i) {
|
||||
return <li key={user_id}>{ user_id } - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>;
|
||||
}) }
|
||||
</ul>
|
||||
<input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder={_t("ex. @bob:example.com")} />
|
||||
<input type="text" ref={this._user_id_input} defaultValue="" className="mx_UserSelector_userIdInput" placeholder={_t("ex. @bob:example.com")} />
|
||||
<button onClick={this.onAddUserId} className="mx_UserSelector_AddUserId">
|
||||
{ _t("Add User") }
|
||||
</button>
|
||||
|
|
|
@ -62,7 +62,7 @@ EMOJIBASE.forEach(emoji => {
|
|||
DATA_BY_CATEGORY[categoryId].push(emoji);
|
||||
}
|
||||
// This is used as the string to match the query against when filtering emojis.
|
||||
emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`;
|
||||
emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`.toLowerCase();
|
||||
});
|
||||
|
||||
export const CATEGORY_HEADER_HEIGHT = 22;
|
||||
|
@ -201,6 +201,7 @@ class EmojiPicker extends React.Component {
|
|||
}
|
||||
|
||||
onChangeFilter(filter) {
|
||||
filter = filter.toLowerCase(); // filter is case insensitive stored lower-case
|
||||
for (const cat of this.categories) {
|
||||
let emojis;
|
||||
// If the new filter string includes the old filter string, we don't have to re-filter the whole dataset.
|
||||
|
|
|
@ -23,7 +23,6 @@ class ReactionPicker extends React.Component {
|
|||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
closeMenu: PropTypes.func.isRequired,
|
||||
reactions: PropTypes.object,
|
||||
};
|
||||
|
||||
|
@ -89,7 +88,6 @@ class ReactionPicker extends React.Component {
|
|||
|
||||
onChoose(reaction) {
|
||||
this.componentWillUnmount();
|
||||
this.props.closeMenu();
|
||||
this.props.onFinished();
|
||||
const myReactions = this.getReactions();
|
||||
if (myReactions.hasOwnProperty(reaction)) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
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.
|
||||
|
@ -21,11 +22,12 @@ import createReactClass from 'create-react-class';
|
|||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import classNames from 'classnames';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import {createMenu} from "../../structures/ContextualMenu";
|
||||
import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu";
|
||||
|
||||
// XXX this class copies a lot from RoomTile.js
|
||||
export default createReactClass({
|
||||
displayName: 'GroupInviteTile',
|
||||
|
||||
|
@ -69,54 +71,49 @@ export default createReactClass({
|
|||
});
|
||||
},
|
||||
|
||||
_showContextMenu: function(x, y, chevronOffset) {
|
||||
const GroupInviteTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu');
|
||||
|
||||
createMenu(GroupInviteTileContextMenu, {
|
||||
chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
group: this.props.group,
|
||||
onFinished: () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
},
|
||||
|
||||
onContextMenu: function(e) {
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.preventDefault();
|
||||
_showContextMenu: function(boundingClientRect) {
|
||||
// Only allow non-guests to access the context menu
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
const chevronOffset = 12;
|
||||
this._showContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
|
||||
},
|
||||
|
||||
onBadgeClicked: function(e) {
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
// Only allow non-guests to access the context menu
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
const state = {
|
||||
contextMenuPosition: boundingClientRect,
|
||||
};
|
||||
|
||||
// If the badge is clicked, then no longer show tooltip
|
||||
if (this.props.collapsed) {
|
||||
this.setState({ hover: false });
|
||||
state.hover = false;
|
||||
}
|
||||
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = elementRect.right + window.pageXOffset + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||
onContextMenuButtonClick: function(e) {
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this._showContextMenu(x, y, chevronOffset);
|
||||
this._showContextMenu(e.target.getBoundingClientRect());
|
||||
},
|
||||
|
||||
onContextMenu: function(e) {
|
||||
// Prevent the native context menu
|
||||
e.preventDefault();
|
||||
|
||||
this._showContextMenu({
|
||||
right: e.clientX,
|
||||
top: e.clientY,
|
||||
height: 0,
|
||||
});
|
||||
},
|
||||
|
||||
closeMenu: function() {
|
||||
this.setState({
|
||||
contextMenuPosition: null,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
|
||||
const groupName = this.props.group.name || this.props.group.groupId;
|
||||
|
@ -125,21 +122,31 @@ export default createReactClass({
|
|||
|
||||
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;
|
||||
|
||||
const isMenuDisplayed = Boolean(this.state.contextMenuPosition);
|
||||
const nameClasses = classNames('mx_RoomTile_name mx_RoomTile_invite mx_RoomTile_badgeShown', {
|
||||
'mx_RoomTile_badgeShown': this.state.badgeHover || this.state.menuDisplayed,
|
||||
'mx_RoomTile_badgeShown': this.state.badgeHover || isMenuDisplayed,
|
||||
});
|
||||
|
||||
const label = <div title={this.props.group.groupId} className={nameClasses} dir="auto">
|
||||
{ groupName }
|
||||
</div>;
|
||||
|
||||
const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed;
|
||||
const badgeEllipsis = this.state.badgeHover || isMenuDisplayed;
|
||||
const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', {
|
||||
'mx_RoomTile_badgeButton': badgeEllipsis,
|
||||
});
|
||||
|
||||
const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!';
|
||||
const badge = <div className={badgeClasses} onClick={this.onBadgeClicked}>{ badgeContent }</div>;
|
||||
const badge = (
|
||||
<ContextMenuButton
|
||||
className={badgeClasses}
|
||||
onClick={this.onContextMenuButtonClick}
|
||||
label={_t("Options")}
|
||||
isExpanded={isMenuDisplayed}
|
||||
>
|
||||
{ badgeContent }
|
||||
</ContextMenuButton>
|
||||
);
|
||||
|
||||
let tooltip;
|
||||
if (this.props.collapsed && this.state.hover) {
|
||||
|
@ -148,17 +155,28 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
const classes = classNames('mx_RoomTile mx_RoomTile_highlight', {
|
||||
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
|
||||
'mx_RoomTile_menuDisplayed': isMenuDisplayed,
|
||||
'mx_RoomTile_selected': this.state.selected,
|
||||
'mx_GroupInviteTile': true,
|
||||
});
|
||||
|
||||
return (
|
||||
<AccessibleButton className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
let contextMenu;
|
||||
if (isMenuDisplayed) {
|
||||
const GroupInviteTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu');
|
||||
contextMenu = (
|
||||
<ContextMenu {...toRightOf(this.state.contextMenuPosition)} onFinished={this.closeMenu}>
|
||||
<GroupInviteTileContextMenu group={this.props.group} onFinished={this.closeMenu} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<AccessibleButton
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
>
|
||||
<div className="mx_RoomTile_avatar">
|
||||
{ av }
|
||||
|
@ -169,6 +187,8 @@ export default createReactClass({
|
|||
</div>
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
|
||||
|
@ -51,6 +51,8 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
}
|
||||
const canRedact = room.currentState.maySendRedactionForEvent(event, userId);
|
||||
this.state = {canRedact, sendStatus: event.getAssociatedStatus()};
|
||||
|
||||
this._content = createRef();
|
||||
}
|
||||
|
||||
_onAssociatedStatusChanged = () => {
|
||||
|
@ -78,8 +80,8 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
|
||||
pillifyLinks() {
|
||||
// not present for redacted events
|
||||
if (this.refs.content) {
|
||||
pillifyLinks(this.refs.content.children, this.props.mxEvent);
|
||||
if (this._content.current) {
|
||||
pillifyLinks(this._content.current.children, this.props.mxEvent);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,9 +104,9 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
// hide the button when already redacted
|
||||
let redactButton;
|
||||
if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent) {
|
||||
if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent && this.state.canRedact) {
|
||||
redactButton = (
|
||||
<AccessibleButton onClick={this._onRedactClick} disabled={!this.state.canRedact}>
|
||||
<AccessibleButton onClick={this._onRedactClick}>
|
||||
{_t("Remove")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
@ -140,13 +142,13 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
if (mxEvent.getContent().msgtype === "m.emote") {
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
contentContainer = (
|
||||
<div className="mx_EventTile_content" ref="content">*
|
||||
<div className="mx_EventTile_content" ref={this._content}>*
|
||||
<span className="mx_MEmoteBody_sender">{ name }</span>
|
||||
{contentElements}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
contentContainer = <div className="mx_EventTile_content" ref="content">{contentElements}</div>;
|
||||
contentContainer = <div className="mx_EventTile_content" ref={this._content}>{contentElements}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ export default class MAudioBody extends React.Component {
|
|||
|
||||
if (this.state.error !== null) {
|
||||
return (
|
||||
<span className="mx_MAudioBody" ref="body">
|
||||
<span className="mx_MAudioBody">
|
||||
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
|
||||
{ _t("Error decrypting audio") }
|
||||
</span>
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import filesize from 'filesize';
|
||||
|
@ -251,6 +251,12 @@ module.exports = createReactClass({
|
|||
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._iframe = createRef();
|
||||
this._dummyLink = createRef();
|
||||
this._downloadImage = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
// Add this to the list of mounted components to receive notifications
|
||||
// when the tint changes.
|
||||
|
@ -272,17 +278,17 @@ module.exports = createReactClass({
|
|||
|
||||
tint: function() {
|
||||
// Update our tinted copy of require("../../../../res/img/download.svg")
|
||||
if (this.refs.downloadImage) {
|
||||
this.refs.downloadImage.src = tintedDownloadImageURL;
|
||||
if (this._downloadImage.current) {
|
||||
this._downloadImage.current.src = tintedDownloadImageURL;
|
||||
}
|
||||
if (this.refs.iframe) {
|
||||
if (this._iframe.current) {
|
||||
// If the attachment is encrypted then the download image
|
||||
// will be inside the iframe so we wont be able to update
|
||||
// it directly.
|
||||
this.refs.iframe.contentWindow.postMessage({
|
||||
this._iframe.current.contentWindow.postMessage({
|
||||
code: remoteSetTint.toString(),
|
||||
imgSrc: tintedDownloadImageURL,
|
||||
style: computedStyle(this.refs.dummyLink),
|
||||
style: computedStyle(this._dummyLink.current),
|
||||
}, "*");
|
||||
}
|
||||
},
|
||||
|
@ -325,7 +331,7 @@ module.exports = createReactClass({
|
|||
};
|
||||
|
||||
return (
|
||||
<span className="mx_MFileBody" ref="body">
|
||||
<span className="mx_MFileBody">
|
||||
<div className="mx_MFileBody_download">
|
||||
<a href="javascript:void(0)" onClick={decrypt}>
|
||||
{ _t("Decrypt %(text)s", { text: text }) }
|
||||
|
@ -340,7 +346,7 @@ module.exports = createReactClass({
|
|||
ev.target.contentWindow.postMessage({
|
||||
code: remoteRender.toString(),
|
||||
imgSrc: tintedDownloadImageURL,
|
||||
style: computedStyle(this.refs.dummyLink),
|
||||
style: computedStyle(this._dummyLink.current),
|
||||
blob: this.state.decryptedBlob,
|
||||
// Set a download attribute for encrypted files so that the file
|
||||
// will have the correct name when the user tries to download it.
|
||||
|
@ -367,9 +373,9 @@ module.exports = createReactClass({
|
|||
* We'll use it to learn how the download link
|
||||
* would have been styled if it was rendered inline.
|
||||
*/ }
|
||||
<a ref="dummyLink" />
|
||||
<a ref={this._dummyLink} />
|
||||
</div>
|
||||
<iframe src={renderer_url} onLoad={onIframeLoad} ref="iframe" />
|
||||
<iframe src={renderer_url} onLoad={onIframeLoad} ref={this._iframe} />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
|
@ -439,7 +445,7 @@ module.exports = createReactClass({
|
|||
<span className="mx_MFileBody">
|
||||
<div className="mx_MFileBody_download">
|
||||
<a {...downloadProps}>
|
||||
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage" />
|
||||
<img src={tintedDownloadImageURL} width="12" height="14" ref={this._downloadImage} />
|
||||
{ _t("Download %(text)s", { text: text }) }
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
|
@ -65,6 +65,8 @@ export default class MImageBody extends React.Component {
|
|||
hover: false,
|
||||
showImage: SettingsStore.getValue("showImages"),
|
||||
};
|
||||
|
||||
this._image = createRef();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
|
@ -158,8 +160,8 @@ export default class MImageBody extends React.Component {
|
|||
|
||||
let loadedImageDimensions;
|
||||
|
||||
if (this.refs.image) {
|
||||
const { naturalWidth, naturalHeight } = this.refs.image;
|
||||
if (this._image.current) {
|
||||
const { naturalWidth, naturalHeight } = this._image.current;
|
||||
// this is only used as a fallback in case content.info.w/h is missing
|
||||
loadedImageDimensions = { naturalWidth, naturalHeight };
|
||||
}
|
||||
|
@ -342,7 +344,7 @@ export default class MImageBody extends React.Component {
|
|||
imageElement = <HiddenImagePlaceholder />;
|
||||
} else {
|
||||
imageElement = (
|
||||
<img style={{display: 'none'}} src={thumbUrl} ref="image"
|
||||
<img style={{display: 'none'}} src={thumbUrl} ref={this._image}
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
|
@ -385,7 +387,7 @@ export default class MImageBody extends React.Component {
|
|||
// which has the same width as the timeline
|
||||
// mx_MImageBody_thumbnail resizes img to exactly container size
|
||||
img = (
|
||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref={this._image}
|
||||
style={{ maxWidth: maxWidth + "px" }}
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
|
@ -459,7 +461,7 @@ export default class MImageBody extends React.Component {
|
|||
|
||||
if (this.state.error !== null) {
|
||||
return (
|
||||
<span className="mx_MImageBody" ref="body">
|
||||
<span className="mx_MImageBody">
|
||||
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
|
||||
{ _t("Error decrypting image") }
|
||||
</span>
|
||||
|
@ -477,7 +479,7 @@ export default class MImageBody extends React.Component {
|
|||
const thumbnail = this._messageContent(contentUrl, thumbUrl, content);
|
||||
const fileBody = this.getFileBody();
|
||||
|
||||
return <span className="mx_MImageBody" ref="body">
|
||||
return <span className="mx_MImageBody">
|
||||
{ thumbnail }
|
||||
{ fileBody }
|
||||
</span>;
|
||||
|
|
|
@ -132,7 +132,7 @@ module.exports = createReactClass({
|
|||
|
||||
if (this.state.error !== null) {
|
||||
return (
|
||||
<span className="mx_MVideoBody" ref="body">
|
||||
<span className="mx_MVideoBody">
|
||||
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
|
||||
{ _t("Error decrypting video") }
|
||||
</span>
|
||||
|
@ -144,8 +144,8 @@ module.exports = createReactClass({
|
|||
// The attachment is decrypted in componentDidMount.
|
||||
// For now add an img tag with a spinner.
|
||||
return (
|
||||
<span className="mx_MVideoBody" ref="body">
|
||||
<div className="mx_MImageBody_thumbnail mx_MImageBody_thumbnail_spinner" ref="image">
|
||||
<span className="mx_MVideoBody">
|
||||
<div className="mx_MImageBody_thumbnail mx_MImageBody_thumbnail_spinner">
|
||||
<img src={require("../../../../res/img/spinner.gif")} alt={content.body} width="16" height="16" />
|
||||
</div>
|
||||
</span>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
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.
|
||||
|
@ -15,17 +16,96 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import { createMenu } from '../../structures/ContextualMenu';
|
||||
import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu';
|
||||
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
|
||||
import {RoomContext} from "../../structures/RoomView";
|
||||
|
||||
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
useEffect(() => {
|
||||
onFocusChange(menuDisplayed);
|
||||
}, [onFocusChange, menuDisplayed]);
|
||||
|
||||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
|
||||
|
||||
const tile = getTile && getTile();
|
||||
const replyThread = getReplyThread && getReplyThread();
|
||||
|
||||
const onCryptoClick = () => {
|
||||
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
||||
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
|
||||
{event: mxEvent},
|
||||
);
|
||||
};
|
||||
|
||||
let e2eInfoCallback = null;
|
||||
if (mxEvent.isEncrypted()) {
|
||||
e2eInfoCallback = onCryptoClick;
|
||||
}
|
||||
|
||||
const buttonRect = button.current.getBoundingClientRect();
|
||||
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
|
||||
<MessageContextMenu
|
||||
mxEvent={mxEvent}
|
||||
permalinkCreator={permalinkCreator}
|
||||
eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
|
||||
collapseReplyThread={replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined}
|
||||
e2eInfoCallback={e2eInfoCallback}
|
||||
onFinished={closeMenu}
|
||||
/>
|
||||
</ContextMenu>;
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<ContextMenuButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
|
||||
label={_t("Options")}
|
||||
onClick={openMenu}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={button}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
};
|
||||
|
||||
const ReactButton = ({mxEvent, reactions, onFocusChange}) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
useEffect(() => {
|
||||
onFocusChange(menuDisplayed);
|
||||
}, [onFocusChange, menuDisplayed]);
|
||||
|
||||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
const buttonRect = button.current.getBoundingClientRect();
|
||||
const ReactionPicker = sdk.getComponent('emojipicker.ReactionPicker');
|
||||
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} catchTab={false}>
|
||||
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
|
||||
</ContextMenu>;
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<ContextMenuButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton"
|
||||
label={_t("React")}
|
||||
onClick={openMenu}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={button}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
};
|
||||
|
||||
export default class MessageActionBar extends React.PureComponent {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
|
@ -62,14 +142,6 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
this.props.onFocusChange(focused);
|
||||
};
|
||||
|
||||
onCryptoClick = () => {
|
||||
const event = this.props.mxEvent;
|
||||
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
||||
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
|
||||
{event},
|
||||
);
|
||||
};
|
||||
|
||||
onReplyClick = (ev) => {
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
|
@ -84,71 +156,6 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
});
|
||||
};
|
||||
|
||||
getMenuOptions = (ev) => {
|
||||
const menuOptions = {};
|
||||
const buttonRect = ev.target.getBoundingClientRect();
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const buttonRight = buttonRect.right + window.pageXOffset;
|
||||
const buttonBottom = buttonRect.bottom + window.pageYOffset;
|
||||
const buttonTop = buttonRect.top + window.pageYOffset;
|
||||
// Align the right edge of the menu to the right edge of the button
|
||||
menuOptions.right = window.innerWidth - buttonRight;
|
||||
// Align the menu vertically on whichever side of the button has more
|
||||
// space available.
|
||||
if (buttonBottom < window.innerHeight / 2) {
|
||||
menuOptions.top = buttonBottom;
|
||||
} else {
|
||||
menuOptions.bottom = window.innerHeight - buttonTop;
|
||||
}
|
||||
return menuOptions;
|
||||
};
|
||||
|
||||
onReactClick = (ev) => {
|
||||
const ReactionPicker = sdk.getComponent('emojipicker.ReactionPicker');
|
||||
|
||||
const menuOptions = {
|
||||
...this.getMenuOptions(ev),
|
||||
mxEvent: this.props.mxEvent,
|
||||
reactions: this.props.reactions,
|
||||
chevronFace: "none",
|
||||
onFinished: () => this.onFocusChange(false),
|
||||
};
|
||||
|
||||
createMenu(ReactionPicker, menuOptions);
|
||||
|
||||
this.onFocusChange(true);
|
||||
};
|
||||
|
||||
onOptionsClick = (ev) => {
|
||||
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
|
||||
|
||||
const { getTile, getReplyThread } = this.props;
|
||||
const tile = getTile && getTile();
|
||||
const replyThread = getReplyThread && getReplyThread();
|
||||
|
||||
let e2eInfoCallback = null;
|
||||
if (this.props.mxEvent.isEncrypted()) {
|
||||
e2eInfoCallback = () => this.onCryptoClick();
|
||||
}
|
||||
|
||||
const menuOptions = {
|
||||
...this.getMenuOptions(ev),
|
||||
mxEvent: this.props.mxEvent,
|
||||
chevronFace: "none",
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
|
||||
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
|
||||
e2eInfoCallback: e2eInfoCallback,
|
||||
onFinished: () => {
|
||||
this.onFocusChange(false);
|
||||
},
|
||||
};
|
||||
|
||||
createMenu(MessageContextMenu, menuOptions);
|
||||
|
||||
this.onFocusChange(true);
|
||||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
|
@ -158,11 +165,9 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
|
||||
if (isContentActionable(this.props.mxEvent)) {
|
||||
if (this.context.room.canReact) {
|
||||
reactButton = <AccessibleButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton"
|
||||
title={_t("React")}
|
||||
onClick={this.onReactClick}
|
||||
/>;
|
||||
reactButton = (
|
||||
<ReactButton mxEvent={this.props.mxEvent} reactions={this.props.reactions} onFocusChange={this.onFocusChange} />
|
||||
);
|
||||
}
|
||||
if (this.context.room.canReply) {
|
||||
replyButton = <AccessibleButton
|
||||
|
@ -185,11 +190,12 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
{reactButton}
|
||||
{replyButton}
|
||||
{editButton}
|
||||
<AccessibleButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
|
||||
title={_t("Options")}
|
||||
onClick={this.onOptionsClick}
|
||||
aria-haspopup={true}
|
||||
<OptionsButton
|
||||
mxEvent={this.props.mxEvent}
|
||||
getReplyThread={this.props.getReplyThread}
|
||||
getTile={this.props.getTile}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
onFocusChange={this.onFocusChange}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import sdk from '../../../index';
|
||||
|
@ -47,8 +47,12 @@ module.exports = createReactClass({
|
|||
maxImageHeight: PropTypes.number,
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._body = createRef();
|
||||
},
|
||||
|
||||
getEventTileOps: function() {
|
||||
return this.refs.body && this.refs.body.getEventTileOps ? this.refs.body.getEventTileOps() : null;
|
||||
return this._body.current && this._body.current.getEventTileOps ? this._body.current.getEventTileOps() : null;
|
||||
},
|
||||
|
||||
onTileUpdate: function() {
|
||||
|
@ -103,7 +107,8 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
return <BodyType
|
||||
ref="body" mxEvent={this.props.mxEvent}
|
||||
ref={this._body}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
|
|
|
@ -149,7 +149,11 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
</a>;
|
||||
}
|
||||
|
||||
return <div className="mx_ReactionsRow">
|
||||
return <div
|
||||
className="mx_ReactionsRow"
|
||||
role="toolbar"
|
||||
aria-label={_t("Reactions")}
|
||||
>
|
||||
{items}
|
||||
{showAllButton}
|
||||
</div>;
|
||||
|
|
|
@ -20,6 +20,8 @@ import classNames from 'classnames';
|
|||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
|
||||
export default class ReactionsRowButton extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
@ -79,7 +81,7 @@ export default class ReactionsRowButton extends React.PureComponent {
|
|||
render() {
|
||||
const ReactionsRowButtonTooltip =
|
||||
sdk.getComponent('messages.ReactionsRowButtonTooltip');
|
||||
const { content, count, reactionEvents, myReactionEvent } = this.props;
|
||||
const { mxEvent, content, count, reactionEvents, myReactionEvent } = this.props;
|
||||
|
||||
const classes = classNames({
|
||||
mx_ReactionsRowButton: true,
|
||||
|
@ -96,18 +98,48 @@ export default class ReactionsRowButton extends React.PureComponent {
|
|||
/>;
|
||||
}
|
||||
|
||||
return <span className={classes}
|
||||
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId());
|
||||
let label;
|
||||
if (room) {
|
||||
const senders = [];
|
||||
for (const reactionEvent of reactionEvents) {
|
||||
const member = room.getMember(reactionEvent.getSender());
|
||||
const name = member ? member.name : reactionEvent.getSender();
|
||||
senders.push(name);
|
||||
}
|
||||
label = _t(
|
||||
"<reactors/><reactedWith> reacted with %(content)s</reactedWith>",
|
||||
{
|
||||
content,
|
||||
},
|
||||
{
|
||||
reactors: () => {
|
||||
return formatCommaSeparatedList(senders, 6);
|
||||
},
|
||||
reactedWith: (sub) => {
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
return sub;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return <AccessibleButton className={classes}
|
||||
aria-label={label}
|
||||
onClick={this.onClick}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseOut={this.onMouseOut}
|
||||
>
|
||||
<span className="mx_ReactionsRowButton_content">
|
||||
<span className="mx_ReactionsRowButton_content" aria-hidden="true">
|
||||
{content}
|
||||
</span>
|
||||
<span className="mx_ReactionsRowButton_count">
|
||||
<span className="mx_ReactionsRowButton_count" aria-hidden="true">
|
||||
{count}
|
||||
</span>
|
||||
{tooltip}
|
||||
</span>;
|
||||
</AccessibleButton>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
@ -27,12 +27,13 @@ import sdk from '../../../index';
|
|||
import Modal from '../../../Modal';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as ContextualMenu from '../../structures/ContextualMenu';
|
||||
import * as ContextMenu from '../../structures/ContextMenu';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import {pillifyLinks} from '../../../utils/pillify';
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
|
||||
import {toRightOf} from "../../structures/ContextMenu";
|
||||
|
||||
module.exports = createReactClass({
|
||||
displayName: 'TextualBody',
|
||||
|
@ -85,6 +86,10 @@ module.exports = createReactClass({
|
|||
return successful;
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._content = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
if (!this.props.editState) {
|
||||
|
@ -93,13 +98,13 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
_applyFormatting() {
|
||||
this.activateSpoilers(this.refs.content.children);
|
||||
this.activateSpoilers(this._content.current.children);
|
||||
|
||||
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
||||
// are still sent as plaintext URLs. If these are ever pillified in the composer,
|
||||
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
|
||||
pillifyLinks(this.refs.content.children, this.props.mxEvent);
|
||||
HtmlUtils.linkifyElement(this.refs.content);
|
||||
pillifyLinks(this._content.current.children, this.props.mxEvent);
|
||||
HtmlUtils.linkifyElement(this._content.current);
|
||||
this.calculateUrlPreview();
|
||||
|
||||
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
|
||||
|
@ -162,7 +167,7 @@ module.exports = createReactClass({
|
|||
//console.info("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||
|
||||
if (this.props.showUrlPreview) {
|
||||
let links = this.findLinks(this.refs.content.children);
|
||||
let links = this.findLinks(this._content.current.children);
|
||||
if (links.length) {
|
||||
// de-dup the links (but preserve ordering)
|
||||
const seen = new Set();
|
||||
|
@ -272,18 +277,12 @@ module.exports = createReactClass({
|
|||
const copyCode = button.parentNode.getElementsByTagName("code")[0];
|
||||
const successful = this.copyToClipboard(copyCode.textContent);
|
||||
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const buttonRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = buttonRect.right + window.pageXOffset;
|
||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||
const {close} = ContextualMenu.createMenu(GenericTextContextMenu, {
|
||||
chevronOffset: 10,
|
||||
left: x,
|
||||
top: y,
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
}, false);
|
||||
});
|
||||
e.target.onmouseleave = close;
|
||||
};
|
||||
|
||||
|
@ -332,7 +331,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
getInnerText: () => {
|
||||
return this.refs.content.innerText;
|
||||
return this._content.current.innerText;
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -406,13 +405,18 @@ module.exports = createReactClass({
|
|||
label={_t("Edited at %(date)s. Click to view edits.", {date: dateString})}
|
||||
/>;
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<div
|
||||
key="editedMarker" className="mx_EventTile_edited"
|
||||
<AccessibleButton
|
||||
key="editedMarker"
|
||||
className="mx_EventTile_edited"
|
||||
onClick={this._openHistoryDialog}
|
||||
onMouseEnter={this._onMouseEnterEditedMarker}
|
||||
onMouseLeave={this._onMouseLeaveEditedMarker}
|
||||
>{editedTooltip}<span>{`(${_t("edited")})`}</span></div>
|
||||
>
|
||||
{ editedTooltip }<span>{`(${_t("edited")})`}</span>
|
||||
</AccessibleButton>
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -457,7 +461,7 @@ module.exports = createReactClass({
|
|||
case "m.emote":
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
return (
|
||||
<span ref="content" className="mx_MEmoteBody mx_EventTile_content">
|
||||
<span ref={this._content} className="mx_MEmoteBody mx_EventTile_content">
|
||||
*
|
||||
<span
|
||||
className="mx_MEmoteBody_sender"
|
||||
|
@ -472,14 +476,14 @@ module.exports = createReactClass({
|
|||
);
|
||||
case "m.notice":
|
||||
return (
|
||||
<span ref="content" className="mx_MNoticeBody mx_EventTile_content">
|
||||
<span ref={this._content} className="mx_MNoticeBody mx_EventTile_content">
|
||||
{ body }
|
||||
{ widgets }
|
||||
</span>
|
||||
);
|
||||
default: // including "m.text"
|
||||
return (
|
||||
<span ref="content" className="mx_MTextBody mx_EventTile_content">
|
||||
<span ref={this._content} className="mx_MTextBody mx_EventTile_content">
|
||||
{ body }
|
||||
{ widgets }
|
||||
</span>
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
|
@ -58,13 +58,15 @@ export default class RoomProfileSettings extends React.Component {
|
|||
canSetTopic: room.currentState.maySendStateEvent('m.room.topic', client.getUserId()),
|
||||
canSetAvatar: room.currentState.maySendStateEvent('m.room.avatar', client.getUserId()),
|
||||
};
|
||||
|
||||
this._avatarUpload = createRef();
|
||||
}
|
||||
|
||||
_uploadAvatar = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.refs.avatarUpload.click();
|
||||
this._avatarUpload.current.click();
|
||||
};
|
||||
|
||||
_saveProfile = async (e) => {
|
||||
|
@ -178,7 +180,7 @@ export default class RoomProfileSettings extends React.Component {
|
|||
|
||||
return (
|
||||
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
|
||||
<input type="file" ref="avatarUpload" className="mx_ProfileSettings_avatarUpload"
|
||||
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
|
||||
onChange={this._onAvatarChanged} accept="image/*" />
|
||||
<div className="mx_ProfileSettings_profile">
|
||||
<div className="mx_ProfileSettings_controls">
|
||||
|
|
|
@ -188,14 +188,15 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
const callView = (
|
||||
<CallView ref="callView" room={this.props.room}
|
||||
<CallView
|
||||
room={this.props.room}
|
||||
ConferenceHandler={this.props.conferenceHandler}
|
||||
onResize={this.props.onResize}
|
||||
maxVideoHeight={this.props.maxHeight}
|
||||
/>
|
||||
);
|
||||
|
||||
const appsDrawer = <AppsDrawer ref="appsDrawer"
|
||||
const appsDrawer = <AppsDrawer
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
maxHeight={this.props.maxHeight}
|
||||
|
|
|
@ -19,7 +19,7 @@ limitations under the License.
|
|||
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
const classNames = require("classnames");
|
||||
|
@ -224,6 +224,9 @@ module.exports = createReactClass({
|
|||
// don't do RR animations until we are mounted
|
||||
this._suppressReadReceiptAnimation = true;
|
||||
this._verifyEvent(this.props.mxEvent);
|
||||
|
||||
this._tile = createRef();
|
||||
this._replyThread = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -494,6 +497,9 @@ module.exports = createReactClass({
|
|||
if (ev.status === EventStatus.NOT_SENT) {
|
||||
return;
|
||||
}
|
||||
if (ev.isState()) {
|
||||
return; // we expect this to be unencrypted
|
||||
}
|
||||
// if the event is not encrypted, but it's an e2e room, show the open padlock
|
||||
return <E2ePadlockUnencrypted {...props} />;
|
||||
}
|
||||
|
@ -509,11 +515,11 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
getTile() {
|
||||
return this.refs.tile;
|
||||
return this._tile.current;
|
||||
},
|
||||
|
||||
getReplyThread() {
|
||||
return this.refs.replyThread;
|
||||
return this._replyThread.current;
|
||||
},
|
||||
|
||||
getReactions() {
|
||||
|
@ -745,7 +751,7 @@ module.exports = createReactClass({
|
|||
</a>
|
||||
</div>
|
||||
<div className="mx_EventTile_line">
|
||||
<EventTileType ref="tile"
|
||||
<EventTileType ref={this._tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
|
@ -759,7 +765,7 @@ module.exports = createReactClass({
|
|||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_line">
|
||||
<EventTileType ref="tile"
|
||||
<EventTileType ref={this._tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
|
@ -789,7 +795,7 @@ module.exports = createReactClass({
|
|||
this.props.mxEvent,
|
||||
this.props.onHeightChanged,
|
||||
this.props.permalinkCreator,
|
||||
'replyThread',
|
||||
this._replyThread,
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
@ -802,7 +808,7 @@ module.exports = createReactClass({
|
|||
</a>
|
||||
{ !isBubbleMessage && this._renderE2EPadlock() }
|
||||
{ thread }
|
||||
<EventTileType ref="tile"
|
||||
<EventTileType ref={this._tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
|
@ -817,7 +823,7 @@ module.exports = createReactClass({
|
|||
this.props.mxEvent,
|
||||
this.props.onHeightChanged,
|
||||
this.props.permalinkCreator,
|
||||
'replyThread',
|
||||
this._replyThread,
|
||||
);
|
||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||
return (
|
||||
|
@ -836,7 +842,7 @@ module.exports = createReactClass({
|
|||
</a>
|
||||
{ !isBubbleMessage && this._renderE2EPadlock() }
|
||||
{ thread }
|
||||
<EventTileType ref="tile"
|
||||
<EventTileType ref={this._tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
editState={this.props.editState}
|
||||
|
@ -887,7 +893,7 @@ module.exports.haveTileForEvent = function(e) {
|
|||
|
||||
function E2ePadlockUndecryptable(props) {
|
||||
return (
|
||||
<E2ePadlock title={_t("Undecryptable")} icon="undecryptable" {...props} />
|
||||
<E2ePadlock title={_t("This message cannot be decrypted")} icon="undecryptable" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -899,19 +905,57 @@ function E2ePadlockUnverified(props) {
|
|||
|
||||
function E2ePadlockUnencrypted(props) {
|
||||
return (
|
||||
<E2ePadlock title={_t("Unencrypted message")} icon="unencrypted" {...props} />
|
||||
<E2ePadlock title={_t("Unencrypted")} icon="unencrypted" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function E2ePadlock(props) {
|
||||
if (SettingsStore.getValue("alwaysShowEncryptionIcons")) {
|
||||
return (<div
|
||||
className={`mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${props.icon}`}
|
||||
title={props.title} onClick={props.onClick} />);
|
||||
} else {
|
||||
return (<div
|
||||
className={`mx_EventTile_e2eIcon mx_EventTile_e2eIcon_hidden mx_EventTile_e2eIcon_${props.icon}`}
|
||||
onClick={props.onClick} />);
|
||||
class E2ePadlock extends React.Component {
|
||||
static propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
hover: false,
|
||||
};
|
||||
}
|
||||
|
||||
onClick = (e) => {
|
||||
if (this.props.onClick) this.props.onClick(e);
|
||||
};
|
||||
|
||||
onHoverStart = () => {
|
||||
this.setState({hover: true});
|
||||
};
|
||||
|
||||
onHoverEnd = () => {
|
||||
this.setState({hover: false});
|
||||
};
|
||||
|
||||
render() {
|
||||
let tooltip = null;
|
||||
if (this.state.hover) {
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
tooltip = <Tooltip className="mx_EventTile_e2eIcon_tooltip" label={this.props.title} dir="auto" />;
|
||||
}
|
||||
|
||||
let classes = `mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${this.props.icon}`;
|
||||
if (!SettingsStore.getValue("alwaysShowEncryptionIcons")) {
|
||||
classes += ' mx_EventTile_e2eIcon_hidden';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onHoverStart}
|
||||
onMouseLeave={this.onHoverEnd}
|
||||
>{tooltip}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { linkifyElement } from '../../../HtmlUtils';
|
||||
|
@ -54,17 +54,19 @@ module.exports = createReactClass({
|
|||
}, (error)=>{
|
||||
console.error("Failed to get URL preview: " + error);
|
||||
});
|
||||
|
||||
this._description = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.refs.description) {
|
||||
linkifyElement(this.refs.description);
|
||||
if (this._description.current) {
|
||||
linkifyElement(this._description.current);
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
if (this.refs.description) {
|
||||
linkifyElement(this.refs.description);
|
||||
if (this._description.current) {
|
||||
linkifyElement(this._description.current);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -129,7 +131,7 @@ module.exports = createReactClass({
|
|||
<div className="mx_LinkPreviewWidget_caption">
|
||||
<div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noopener">{ p["og:title"] }</a></div>
|
||||
<div className="mx_LinkPreviewWidget_siteName">{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }</div>
|
||||
<div className="mx_LinkPreviewWidget_description" ref="description">
|
||||
<div className="mx_LinkPreviewWidget_description" ref={this._description}>
|
||||
{ p["og:description"] }
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@ 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 React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
|
@ -111,6 +111,8 @@ class UploadButton extends React.Component {
|
|||
super(props, context);
|
||||
this.onUploadClick = this.onUploadClick.bind(this);
|
||||
this.onUploadFileInputChange = this.onUploadFileInputChange.bind(this);
|
||||
|
||||
this._uploadInput = createRef();
|
||||
}
|
||||
|
||||
onUploadClick(ev) {
|
||||
|
@ -118,7 +120,7 @@ class UploadButton extends React.Component {
|
|||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
this.refs.uploadInput.click();
|
||||
this._uploadInput.current.click();
|
||||
}
|
||||
|
||||
onUploadFileInputChange(ev) {
|
||||
|
@ -150,7 +152,9 @@ class UploadButton extends React.Component {
|
|||
onClick={this.onUploadClick}
|
||||
title={_t('Upload file')}
|
||||
>
|
||||
<input ref="uploadInput" type="file"
|
||||
<input
|
||||
ref={this._uploadInput}
|
||||
type="file"
|
||||
style={uploadInputStyle}
|
||||
multiple
|
||||
onChange={this.onUploadFileInputChange}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, {createRef} from "react";
|
||||
import dis from "../../../dispatcher";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
|
@ -45,6 +45,8 @@ export default class RoomBreadcrumbs extends React.Component {
|
|||
// The room IDs we're waiting to come down the Room handler and when we
|
||||
// started waiting for them. Used to track a room over an upgrade/autojoin.
|
||||
this._waitingRoomQueue = [/* { roomId, addedTs } */];
|
||||
|
||||
this._scroller = createRef();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
|
@ -284,8 +286,8 @@ export default class RoomBreadcrumbs extends React.Component {
|
|||
}
|
||||
this.setState({rooms});
|
||||
|
||||
if (this.refs.scroller) {
|
||||
this.refs.scroller.moveToOrigin();
|
||||
if (this._scroller.current) {
|
||||
this._scroller.current.moveToOrigin();
|
||||
}
|
||||
|
||||
// We don't track room aesthetics (badges, membership, etc) over the wire so we
|
||||
|
@ -390,7 +392,7 @@ export default class RoomBreadcrumbs extends React.Component {
|
|||
return (
|
||||
<div role="toolbar" aria-label={_t("Recent rooms")}>
|
||||
<IndicatorScrollbar
|
||||
ref="scroller"
|
||||
ref={this._scroller}
|
||||
className="mx_RoomBreadcrumbs"
|
||||
trackHorizontalOverflow={true}
|
||||
verticalScrollsHorizontally={true}
|
||||
|
|
|
@ -55,7 +55,7 @@ export default createReactClass({
|
|||
if (rows.length === 0) {
|
||||
rooms = <i>{ _t('No rooms to show') }</i>;
|
||||
} else {
|
||||
rooms = <table ref="directory_table" className="mx_RoomDirectory_table">
|
||||
rooms = <table className="mx_RoomDirectory_table">
|
||||
<tbody>
|
||||
{ this.getRows() }
|
||||
</tbody>
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import sdk from '../../../index';
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { linkifyElement } from '../../../HtmlUtils';
|
||||
import { ContentRepo } from 'matrix-js-sdk';
|
||||
|
@ -49,11 +49,15 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
_linkifyTopic: function() {
|
||||
if (this.refs.topic) {
|
||||
linkifyElement(this.refs.topic);
|
||||
if (this._topic.current) {
|
||||
linkifyElement(this._topic.current);
|
||||
}
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._topic = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._linkifyTopic();
|
||||
},
|
||||
|
@ -104,7 +108,7 @@ export default createReactClass({
|
|||
<td className="mx_RoomDirectory_roomDescription">
|
||||
<div className="mx_RoomDirectory_name">{ name }</div>
|
||||
{ perms }
|
||||
<div className="mx_RoomDirectory_topic" ref="topic" onClick={this.onTopicClick}>
|
||||
<div className="mx_RoomDirectory_topic" ref={this._topic} onClick={this.onTopicClick}>
|
||||
{ room.topic }
|
||||
</div>
|
||||
<div className="mx_RoomDirectory_alias">{ getDisplayAliasForRoom(room) }</div>
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import classNames from 'classnames';
|
||||
|
@ -56,6 +56,10 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._topic = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.events", this._onRoomStateEvents);
|
||||
|
@ -70,8 +74,8 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
if (this.refs.topic) {
|
||||
linkifyElement(this.refs.topic);
|
||||
if (this._topic.current) {
|
||||
linkifyElement(this._topic.current);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -204,7 +208,7 @@ module.exports = createReactClass({
|
|||
}
|
||||
}
|
||||
const topicElement =
|
||||
<div className="mx_RoomHeader_topic" ref="topic" title={topic} dir="auto">{ topic }</div>;
|
||||
<div className="mx_RoomHeader_topic" ref={this._topic} title={topic} dir="auto">{ topic }</div>;
|
||||
const avatarSize = 28;
|
||||
let roomAvatar;
|
||||
if (this.props.room) {
|
||||
|
|
|
@ -65,14 +65,14 @@ module.exports = createReactClass({
|
|||
|
||||
return (
|
||||
<div className="mx_RoomHeader_name">
|
||||
<EditableText ref="editor"
|
||||
className="mx_RoomHeader_nametext mx_RoomHeader_editable"
|
||||
placeholderClassName="mx_RoomHeader_placeholder"
|
||||
placeholder={this._placeholderName}
|
||||
blurToCancel={false}
|
||||
initialValue={this.state.name}
|
||||
onValueChanged={this._onValueChanged}
|
||||
dir="auto" />
|
||||
<EditableText
|
||||
className="mx_RoomHeader_nametext mx_RoomHeader_editable"
|
||||
placeholderClassName="mx_RoomHeader_placeholder"
|
||||
placeholder={this._placeholderName}
|
||||
blurToCancel={false}
|
||||
initialValue={this.state.name}
|
||||
onValueChanged={this._onValueChanged}
|
||||
dir="auto" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -451,16 +451,21 @@ module.exports = createReactClass({
|
|||
if (isDM) {
|
||||
title = _t("Do you want to chat with %(user)s?",
|
||||
{ user: inviteMember.name });
|
||||
subTitle = [
|
||||
avatar,
|
||||
_t("<userName/> wants to chat", {}, {userName: () => inviterElement}),
|
||||
];
|
||||
primaryActionLabel = _t("Start chatting");
|
||||
} else {
|
||||
title = _t("Do you want to join %(roomName)s?",
|
||||
{ roomName: this._roomName() });
|
||||
subTitle = [
|
||||
avatar,
|
||||
_t("<userName/> invited you", {}, {userName: () => inviterElement}),
|
||||
];
|
||||
primaryActionLabel = _t("Accept");
|
||||
}
|
||||
subTitle = [
|
||||
avatar,
|
||||
_t("<userName/> invited you", {}, {userName: () => inviterElement}),
|
||||
];
|
||||
|
||||
primaryActionLabel = _t("Accept");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
secondaryActionLabel = _t("Reject");
|
||||
secondaryActionHandler = this.props.onRejectClick;
|
||||
|
|
|
@ -17,7 +17,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
@ -26,10 +25,9 @@ import dis from '../../../dispatcher';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import sdk from '../../../index';
|
||||
import {createMenu} from '../../structures/ContextualMenu';
|
||||
import {ContextMenu, ContextMenuButton, toRightOf} from '../../structures/ContextMenu';
|
||||
import * as RoomNotifs from '../../../RoomNotifs';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import ActiveRoomObserver from '../../../ActiveRoomObserver';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
@ -61,7 +59,7 @@ module.exports = createReactClass({
|
|||
return ({
|
||||
hover: false,
|
||||
badgeHover: false,
|
||||
menuDisplayed: false,
|
||||
contextMenuPosition: null, // DOM bounding box, null if non-shown
|
||||
roomName: this.props.room.name,
|
||||
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
||||
notificationCount: this.props.room.getUnreadNotificationCount(),
|
||||
|
@ -229,32 +227,6 @@ module.exports = createReactClass({
|
|||
this.badgeOnMouseLeave();
|
||||
},
|
||||
|
||||
_showContextMenu: function(x, y, chevronOffset) {
|
||||
const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
|
||||
|
||||
createMenu(RoomTileContextMenu, {
|
||||
chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
room: this.props.room,
|
||||
onFinished: () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
this.props.refreshSubList();
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
},
|
||||
|
||||
onContextMenu: function(e) {
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.preventDefault();
|
||||
// Only allow non-guests to access the context menu
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
const chevronOffset = 12;
|
||||
this._showContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
|
||||
},
|
||||
|
||||
badgeOnMouseEnter: function() {
|
||||
// Only allow non-guests to access the context menu
|
||||
// and only change it if it needs to change
|
||||
|
@ -267,26 +239,46 @@ module.exports = createReactClass({
|
|||
this.setState( { badgeHover: false } );
|
||||
},
|
||||
|
||||
onOpenMenu: function(e) {
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
_showContextMenu: function(boundingClientRect) {
|
||||
// Only allow non-guests to access the context menu
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
const state = {
|
||||
contextMenuPosition: boundingClientRect,
|
||||
};
|
||||
|
||||
// If the badge is clicked, then no longer show tooltip
|
||||
if (this.props.collapsed) {
|
||||
this.setState({ hover: false });
|
||||
state.hover = false;
|
||||
}
|
||||
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = elementRect.right + window.pageXOffset + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||
onContextMenuButtonClick: function(e) {
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this._showContextMenu(x, y, chevronOffset);
|
||||
this._showContextMenu(e.target.getBoundingClientRect());
|
||||
},
|
||||
|
||||
onContextMenu: function(e) {
|
||||
// Prevent the native context menu
|
||||
e.preventDefault();
|
||||
|
||||
this._showContextMenu({
|
||||
right: e.clientX,
|
||||
top: e.clientY,
|
||||
height: 0,
|
||||
});
|
||||
},
|
||||
|
||||
closeMenu: function() {
|
||||
this.setState({
|
||||
contextMenuPosition: null,
|
||||
});
|
||||
this.props.refreshSubList();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -303,6 +295,8 @@ module.exports = createReactClass({
|
|||
subtext = this.state.statusMessage;
|
||||
}
|
||||
|
||||
const isMenuDisplayed = Boolean(this.state.contextMenuPosition);
|
||||
|
||||
const classes = classNames({
|
||||
'mx_RoomTile': true,
|
||||
'mx_RoomTile_selected': this.state.selected,
|
||||
|
@ -310,7 +304,7 @@ module.exports = createReactClass({
|
|||
'mx_RoomTile_unreadNotify': notifBadges,
|
||||
'mx_RoomTile_highlight': mentionBadges,
|
||||
'mx_RoomTile_invited': isInvite,
|
||||
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
|
||||
'mx_RoomTile_menuDisplayed': isMenuDisplayed,
|
||||
'mx_RoomTile_noBadges': !badges,
|
||||
'mx_RoomTile_transparent': this.props.transparent,
|
||||
'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
|
||||
|
@ -322,7 +316,7 @@ module.exports = createReactClass({
|
|||
|
||||
const badgeClasses = classNames({
|
||||
'mx_RoomTile_badge': true,
|
||||
'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.menuDisplayed,
|
||||
'mx_RoomTile_badgeButton': this.state.badgeHover || isMenuDisplayed,
|
||||
});
|
||||
|
||||
let name = this.state.roomName;
|
||||
|
@ -344,7 +338,7 @@ module.exports = createReactClass({
|
|||
const nameClasses = classNames({
|
||||
'mx_RoomTile_name': true,
|
||||
'mx_RoomTile_invite': this.props.isInvite,
|
||||
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
|
||||
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || isMenuDisplayed,
|
||||
});
|
||||
|
||||
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
|
||||
|
@ -360,9 +354,17 @@ module.exports = createReactClass({
|
|||
// incomingCallBox = <IncomingCallBox incomingCall={ this.props.incomingCall }/>;
|
||||
//}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let contextMenuButton;
|
||||
if (!MatrixClientPeg.get().isGuest()) {
|
||||
contextMenuButton = <AccessibleButton className="mx_RoomTile_menuButton" onClick={this.onOpenMenu} />;
|
||||
contextMenuButton = (
|
||||
<ContextMenuButton
|
||||
className="mx_RoomTile_menuButton"
|
||||
label={_t("Options")}
|
||||
isExpanded={isMenuDisplayed}
|
||||
onClick={this.onContextMenuButtonClick} />
|
||||
);
|
||||
}
|
||||
|
||||
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||
|
@ -393,32 +395,47 @@ module.exports = createReactClass({
|
|||
ariaLabel += " " + _t("Unread messages.");
|
||||
}
|
||||
|
||||
return <AccessibleButton tabIndex="0"
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
aria-label={ariaLabel}
|
||||
aria-selected={this.state.selected}
|
||||
role="treeitem"
|
||||
>
|
||||
<div className={avatarClasses}>
|
||||
<div className="mx_RoomTile_avatar_container">
|
||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||
{ dmIndicator }
|
||||
let contextMenu;
|
||||
if (isMenuDisplayed) {
|
||||
const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
|
||||
contextMenu = (
|
||||
<ContextMenu {...toRightOf(this.state.contextMenuPosition)} onFinished={this.closeMenu}>
|
||||
<RoomTileContextMenu room={this.props.room} onFinished={this.closeMenu} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<AccessibleButton
|
||||
tabIndex="0"
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
aria-label={ariaLabel}
|
||||
aria-selected={this.state.selected}
|
||||
role="treeitem"
|
||||
>
|
||||
<div className={avatarClasses}>
|
||||
<div className="mx_RoomTile_avatar_container">
|
||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||
{ dmIndicator }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
<div className="mx_RoomTile_labelContainer">
|
||||
{ label }
|
||||
{ subtextLabel }
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
<div className="mx_RoomTile_labelContainer">
|
||||
{ label }
|
||||
{ subtextLabel }
|
||||
</div>
|
||||
{ contextMenuButton }
|
||||
{ badge }
|
||||
</div>
|
||||
{ contextMenuButton }
|
||||
{ badge }
|
||||
</div>
|
||||
{ /* { incomingCallBox } */ }
|
||||
{ tooltip }
|
||||
</AccessibleButton>;
|
||||
{ /* { incomingCallBox } */ }
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
const classNames = require('classnames');
|
||||
const AccessibleButton = require('../../../components/views/elements/AccessibleButton');
|
||||
|
@ -29,6 +29,10 @@ module.exports = createReactClass({
|
|||
});
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._search_term = createRef();
|
||||
},
|
||||
|
||||
onThisRoomClick: function() {
|
||||
this.setState({ scope: 'Room' }, () => this._searchIfQuery());
|
||||
},
|
||||
|
@ -47,29 +51,41 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
_searchIfQuery: function() {
|
||||
if (this.refs.search_term.value) {
|
||||
if (this._search_term.current.value) {
|
||||
this.onSearch();
|
||||
}
|
||||
},
|
||||
|
||||
onSearch: function() {
|
||||
this.props.onSearch(this.refs.search_term.value, this.state.scope);
|
||||
this.props.onSearch(this._search_term.current.value, this.state.scope);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const searchButtonClasses = classNames({ mx_SearchBar_searchButton: true, mx_SearchBar_searching: this.props.searchInProgress });
|
||||
const thisRoomClasses = classNames({ mx_SearchBar_button: true, mx_SearchBar_unselected: this.state.scope !== 'Room' });
|
||||
const allRoomsClasses = classNames({ mx_SearchBar_button: true, mx_SearchBar_unselected: this.state.scope !== 'All' });
|
||||
const searchButtonClasses = classNames("mx_SearchBar_searchButton", {
|
||||
mx_SearchBar_searching: this.props.searchInProgress,
|
||||
});
|
||||
const thisRoomClasses = classNames("mx_SearchBar_button", {
|
||||
mx_SearchBar_unselected: this.state.scope !== 'Room',
|
||||
});
|
||||
const allRoomsClasses = classNames("mx_SearchBar_button", {
|
||||
mx_SearchBar_unselected: this.state.scope !== 'All',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx_SearchBar">
|
||||
<AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick}>{_t("This Room")}</AccessibleButton>
|
||||
<AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick}>{_t("All Rooms")}</AccessibleButton>
|
||||
<div className="mx_SearchBar_input mx_textinput">
|
||||
<input ref="search_term" type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange} />
|
||||
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch}></AccessibleButton>
|
||||
<div className="mx_SearchBar_buttons" role="radiogroup">
|
||||
<AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick} aria-checked={this.state.scope === 'Room'} role="radio">
|
||||
{_t("This Room")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick} aria-checked={this.state.scope === 'All'} role="radio">
|
||||
{_t("All Rooms")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick}></AccessibleButton>
|
||||
<div className="mx_SearchBar_input mx_textinput">
|
||||
<input ref={this._search_term} type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange} />
|
||||
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch} />
|
||||
</div>
|
||||
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -14,12 +14,11 @@ 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 React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
|
@ -27,7 +26,6 @@ import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
|||
import Stickerpicker from './Stickerpicker';
|
||||
import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import E2EIcon from './E2EIcon';
|
||||
|
||||
|
@ -143,6 +141,8 @@ class UploadButton extends React.Component {
|
|||
super(props, context);
|
||||
this.onUploadClick = this.onUploadClick.bind(this);
|
||||
this.onUploadFileInputChange = this.onUploadFileInputChange.bind(this);
|
||||
|
||||
this._uploadInput = createRef();
|
||||
}
|
||||
|
||||
onUploadClick(ev) {
|
||||
|
@ -150,7 +150,7 @@ class UploadButton extends React.Component {
|
|||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
this.refs.uploadInput.click();
|
||||
this._uploadInput.current.click();
|
||||
}
|
||||
|
||||
onUploadFileInputChange(ev) {
|
||||
|
@ -182,7 +182,7 @@ class UploadButton extends React.Component {
|
|||
onClick={this.onUploadClick}
|
||||
title={_t('Upload file')}
|
||||
>
|
||||
<input ref="uploadInput" type="file"
|
||||
<input ref={this._uploadInput} type="file"
|
||||
style={uploadInputStyle}
|
||||
multiple
|
||||
onChange={this.onUploadFileInputChange}
|
||||
|
|
|
@ -25,6 +25,7 @@ import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
|||
import PersistedElement from "../elements/PersistedElement";
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {ContextMenu} from "../../structures/ContextMenu";
|
||||
|
||||
const widgetType = 'm.stickerpicker';
|
||||
|
||||
|
@ -371,26 +372,8 @@ export default class Stickerpicker extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const ContextualMenu = sdk.getComponent('structures.ContextualMenu');
|
||||
const GenericElementContextMenu = sdk.getComponent('context_menus.GenericElementContextMenu');
|
||||
let stickerPicker;
|
||||
let stickersButton;
|
||||
|
||||
const stickerPicker = <ContextualMenu
|
||||
elementClass={GenericElementContextMenu}
|
||||
chevronOffset={this.state.stickerPickerChevronOffset}
|
||||
chevronFace={'bottom'}
|
||||
left={this.state.stickerPickerX}
|
||||
top={this.state.stickerPickerY}
|
||||
menuWidth={this.popoverWidth}
|
||||
menuHeight={this.popoverHeight}
|
||||
element={this._getStickerpickerContent()}
|
||||
onFinished={this._onFinished}
|
||||
menuPaddingTop={0}
|
||||
menuPaddingLeft={0}
|
||||
menuPaddingRight={0}
|
||||
zIndex={STICKERPICKER_Z_INDEX}
|
||||
/>;
|
||||
|
||||
if (this.state.showStickers) {
|
||||
// Show hide-stickers button
|
||||
stickersButton =
|
||||
|
@ -402,6 +385,23 @@ export default class Stickerpicker extends React.Component {
|
|||
title={_t("Hide Stickers")}
|
||||
>
|
||||
</AccessibleButton>;
|
||||
|
||||
const GenericElementContextMenu = sdk.getComponent('context_menus.GenericElementContextMenu');
|
||||
stickerPicker = <ContextMenu
|
||||
chevronOffset={this.state.stickerPickerChevronOffset}
|
||||
chevronFace="bottom"
|
||||
left={this.state.stickerPickerX}
|
||||
top={this.state.stickerPickerY}
|
||||
menuWidth={this.popoverWidth}
|
||||
menuHeight={this.popoverHeight}
|
||||
onFinished={this._onFinished}
|
||||
menuPaddingTop={0}
|
||||
menuPaddingLeft={0}
|
||||
menuPaddingRight={0}
|
||||
zIndex={STICKERPICKER_Z_INDEX}
|
||||
>
|
||||
<GenericElementContextMenu element={this._getStickerpickerContent()} onResize={this._onFinished} />
|
||||
</ContextMenu>;
|
||||
} else {
|
||||
// Show show-stickers button
|
||||
stickersButton =
|
||||
|
@ -415,8 +415,8 @@ export default class Stickerpicker extends React.Component {
|
|||
</AccessibleButton>;
|
||||
}
|
||||
return <React.Fragment>
|
||||
{stickersButton}
|
||||
{this.state.showStickers && stickerPicker}
|
||||
{ stickersButton }
|
||||
{ stickerPicker }
|
||||
</React.Fragment>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import Field from "../elements/Field";
|
||||
|
@ -48,13 +48,15 @@ export default class ProfileSettings extends React.Component {
|
|||
avatarFile: null,
|
||||
enableProfileSave: false,
|
||||
};
|
||||
|
||||
this._avatarUpload = createRef();
|
||||
}
|
||||
|
||||
_uploadAvatar = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.refs.avatarUpload.click();
|
||||
this._avatarUpload.current.click();
|
||||
};
|
||||
|
||||
_saveProfile = async (e) => {
|
||||
|
@ -156,7 +158,7 @@ export default class ProfileSettings extends React.Component {
|
|||
|
||||
return (
|
||||
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
|
||||
<input type="file" ref="avatarUpload" className="mx_ProfileSettings_avatarUpload"
|
||||
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
|
||||
onChange={this._onAvatarChanged} accept="image/*" />
|
||||
<div className="mx_ProfileSettings_profile">
|
||||
<div className="mx_ProfileSettings_controls">
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import MatrixClientPeg from "../../../../../MatrixClientPeg";
|
||||
|
@ -44,13 +44,15 @@ export default class NotificationsSettingsTab extends React.Component {
|
|||
}
|
||||
this.setState({currentSound: soundData.name || soundData.url});
|
||||
});
|
||||
|
||||
this._soundUpload = createRef();
|
||||
}
|
||||
|
||||
async _triggerUploader(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.refs.soundUpload.click();
|
||||
this._soundUpload.current.click();
|
||||
}
|
||||
|
||||
async _onSoundUploadChanged(e) {
|
||||
|
@ -157,7 +159,7 @@ export default class NotificationsSettingsTab extends React.Component {
|
|||
<div>
|
||||
<h3>{_t("Set a new custom sound")}</h3>
|
||||
<form autoComplete="off" noValidate={true}>
|
||||
<input ref="soundUpload" className="mx_NotificationSound_soundUpload" type="file" onChange={this._onSoundUploadChanged.bind(this)} accept="audio/*" />
|
||||
<input ref={this._soundUpload} className="mx_NotificationSound_soundUpload" type="file" onChange={this._onSoundUploadChanged.bind(this)} accept="audio/*" />
|
||||
</form>
|
||||
|
||||
{currentUploadedFile}
|
||||
|
|
|
@ -49,8 +49,6 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
|
||||
this.state = {
|
||||
language: languageHandler.getCurrentLanguage(),
|
||||
theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"),
|
||||
useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"),
|
||||
haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
|
||||
serverSupportsSeparateAddAndBind: null,
|
||||
idServerHasUnsignedTerms: false,
|
||||
|
@ -62,6 +60,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
},
|
||||
emails: [],
|
||||
msisdns: [],
|
||||
...this._calculateThemeState(),
|
||||
};
|
||||
|
||||
this.dispatcherRef = dis.register(this._onAction);
|
||||
|
@ -80,6 +79,39 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
_calculateThemeState() {
|
||||
// We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
|
||||
// show the right values for things.
|
||||
|
||||
const themeChoice = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme");
|
||||
const systemThemeExplicit = SettingsStore.getValueAt(
|
||||
SettingLevel.DEVICE, "use_system_theme", null, false, true);
|
||||
const themeExplicit = SettingsStore.getValueAt(
|
||||
SettingLevel.DEVICE, "theme", null, false, true);
|
||||
|
||||
// If the user has enabled system theme matching, use that.
|
||||
if (systemThemeExplicit) {
|
||||
return {
|
||||
theme: themeChoice,
|
||||
useSystemTheme: true,
|
||||
};
|
||||
}
|
||||
|
||||
// If the user has set a theme explicitly, use that (no system theme matching)
|
||||
if (themeExplicit) {
|
||||
return {
|
||||
theme: themeChoice,
|
||||
useSystemTheme: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise assume the defaults for the settings
|
||||
return {
|
||||
theme: themeChoice,
|
||||
useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"),
|
||||
};
|
||||
}
|
||||
|
||||
_onAction = (payload) => {
|
||||
if (payload.action === 'id_server_changed') {
|
||||
this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())});
|
||||
|
@ -89,11 +121,11 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
|
||||
_onEmailsChange = (emails) => {
|
||||
this.setState({ emails });
|
||||
}
|
||||
};
|
||||
|
||||
_onMsisdnsChange = (msisdns) => {
|
||||
this.setState({ msisdns });
|
||||
}
|
||||
};
|
||||
|
||||
async _getThreepidState() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -193,9 +225,9 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
|
||||
_onUseSystemThemeChanged = (checked) => {
|
||||
this.setState({useSystemTheme: checked});
|
||||
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked);
|
||||
dis.dispatch({action: 'recheck_theme'});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
_onPasswordChangeError = (err) => {
|
||||
// TODO: Figure out a design that doesn't involve replacing the current dialog
|
||||
|
@ -307,12 +339,15 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
|
||||
_renderThemeSection() {
|
||||
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
||||
const LabelledToggleSwitch = sdk.getComponent("views.elements.LabelledToggleSwitch");
|
||||
|
||||
const themeWatcher = new ThemeWatcher();
|
||||
let systemThemeSection;
|
||||
if (themeWatcher.isSystemThemeSupported()) {
|
||||
systemThemeSection = <div>
|
||||
<SettingsFlag name="use_system_theme" level={SettingLevel.DEVICE}
|
||||
<LabelledToggleSwitch
|
||||
value={this.state.useSystemTheme}
|
||||
label={SettingsStore.getDisplayName("use_system_theme")}
|
||||
onChange={this._onUseSystemThemeChanged}
|
||||
/>
|
||||
</div>;
|
||||
|
|
|
@ -13,7 +13,7 @@ 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 React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import dis from '../../../dispatcher';
|
||||
|
@ -56,6 +56,10 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._video = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.showCall();
|
||||
|
@ -128,7 +132,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
getVideoView: function() {
|
||||
return this.refs.video;
|
||||
return this._video.current;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -147,7 +151,9 @@ module.exports = createReactClass({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<VideoView ref="video" onClick={this.props.onClick}
|
||||
<VideoView
|
||||
ref={this._video}
|
||||
onClick={this.props.onClick}
|
||||
onResize={this.props.onResize}
|
||||
maxHeight={this.props.maxVideoHeight}
|
||||
/>
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
||||
|
@ -30,12 +30,16 @@ module.exports = createReactClass({
|
|||
onResize: PropTypes.func,
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this._vid = createRef();
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
this.refs.vid.addEventListener('resize', this.onResize);
|
||||
this._vid.current.addEventListener('resize', this.onResize);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.refs.vid.removeEventListener('resize', this.onResize);
|
||||
this._vid.current.removeEventListener('resize', this.onResize);
|
||||
},
|
||||
|
||||
onResize: function(e) {
|
||||
|
@ -46,7 +50,7 @@ module.exports = createReactClass({
|
|||
|
||||
render: function() {
|
||||
return (
|
||||
<video ref="vid" style={{maxHeight: this.props.maxHeight}}>
|
||||
<video ref={this._vid} style={{maxHeight: this.props.maxHeight}}>
|
||||
</video>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
@ -49,6 +49,11 @@ module.exports = createReactClass({
|
|||
onResize: PropTypes.func,
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._local = createRef();
|
||||
this._remote = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
},
|
||||
|
@ -58,7 +63,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
getRemoteVideoElement: function() {
|
||||
return ReactDOM.findDOMNode(this.refs.remote);
|
||||
return ReactDOM.findDOMNode(this._remote.current);
|
||||
},
|
||||
|
||||
getRemoteAudioElement: function() {
|
||||
|
@ -74,7 +79,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
getLocalVideoElement: function() {
|
||||
return ReactDOM.findDOMNode(this.refs.local);
|
||||
return ReactDOM.findDOMNode(this._local.current);
|
||||
},
|
||||
|
||||
setContainer: function(c) {
|
||||
|
@ -125,11 +130,11 @@ module.exports = createReactClass({
|
|||
return (
|
||||
<div className="mx_VideoView" ref={this.setContainer} onClick={this.props.onClick}>
|
||||
<div className="mx_VideoView_remoteVideoFeed">
|
||||
<VideoFeed ref="remote" onResize={this.props.onResize}
|
||||
<VideoFeed ref={this._remote} onResize={this.props.onResize}
|
||||
maxHeight={maxVideoHeight} />
|
||||
</div>
|
||||
<div className={localVideoFeedClasses}>
|
||||
<VideoFeed ref="local" />
|
||||
<VideoFeed ref={this._local} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue