Merge branch 'develop' into travis/stickerpicker/remount

This commit is contained in:
Travis Ralston 2019-04-03 17:00:19 -06:00
commit ad777782b8
18 changed files with 521 additions and 236 deletions

View file

@ -56,6 +56,7 @@ export default class ContextualMenu extends React.Component {
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.
@ -215,16 +216,22 @@ export default class ContextualMenu extends React.Component {
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}>
return <div className={className} style={{...position, ...wrapperStyle}}>
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect}>
{ chevron }
<ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} />
</div>
{ props.hasBackground && <div className="mx_ContextualMenu_background"
{ props.hasBackground && <div className="mx_ContextualMenu_background" style={wrapperStyle}
onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
<style>{ chevronCSS }</style>
</div>;

View file

@ -19,29 +19,28 @@ limitations under the License.
// TODO: This component is enormous! There's several things which could stand-alone:
// - Search results component
// - Drag and drop
// - File uploading - uploadFile()
import shouldHideEvent from "../../shouldHideEvent";
import shouldHideEvent from '../../shouldHideEvent';
const React = require("react");
const ReactDOM = require("react-dom");
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import Promise from 'bluebird';
import filesize from 'filesize';
const classNames = require("classnames");
import classNames from 'classnames';
import { _t } from '../../languageHandler';
import {RoomPermalinkCreator} from "../../matrix-to";
import {RoomPermalinkCreator} from '../../matrix-to';
const MatrixClientPeg = require("../../MatrixClientPeg");
const ContentMessages = require("../../ContentMessages");
const Modal = require("../../Modal");
const sdk = require('../../index');
const CallHandler = require('../../CallHandler');
const dis = require("../../dispatcher");
const Tinter = require("../../Tinter");
const rate_limited_func = require('../../ratelimitedfunc');
const ObjectUtils = require('../../ObjectUtils');
const Rooms = require('../../Rooms');
import MatrixClientPeg from '../../MatrixClientPeg';
import ContentMessages from '../../ContentMessages';
import Modal from '../../Modal';
import sdk from '../../index';
import CallHandler from '../../CallHandler';
import dis from '../../dispatcher';
import Tinter from '../../Tinter';
import rate_limited_func from '../../ratelimitedfunc';
import ObjectUtils from '../../ObjectUtils';
import Rooms from '../../Rooms';
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
@ -170,7 +169,6 @@ module.exports = React.createClass({
MatrixClientPeg.get().on("accountData", this.onAccountData);
MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus);
MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged);
this._fetchMediaConfig();
// Start listening for RoomViewStore updates
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._onRoomViewStoreUpdate(true);
@ -178,27 +176,6 @@ module.exports = React.createClass({
WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
},
_fetchMediaConfig: function(invalidateCache: boolean = false) {
/// NOTE: Using global here so we don't make repeated requests for the
/// config every time we swap room.
if(global.mediaConfig !== undefined && !invalidateCache) {
this.setState({mediaConfig: global.mediaConfig});
return;
}
console.log("[Media Config] Fetching");
MatrixClientPeg.get().getMediaConfig().then((config) => {
console.log("[Media Config] Fetched config:", config);
return config;
}).catch(() => {
// Media repo can't or won't report limits, so provide an empty object (no limits).
console.log("[Media Config] Could not fetch config, so not limiting uploads.");
return {};
}).then((config) => {
global.mediaConfig = config;
this.setState({mediaConfig: config});
});
},
_onRoomViewStoreUpdate: function(initial) {
if (this.unmounted) {
return;
@ -510,7 +487,7 @@ module.exports = React.createClass({
},
onPageUnload(event) {
if (ContentMessages.getCurrentUploads().length > 0) {
if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) {
return event.returnValue =
_t("You seem to be uploading files, are you sure you want to quit?");
} else if (this._getCallForRoom() && this.state.callState !== 'ended') {
@ -561,11 +538,6 @@ module.exports = React.createClass({
case 'picture_snapshot':
this.uploadFile(payload.file);
break;
case 'upload_failed':
// 413: File was too big or upset the server in some way.
if (payload.error && payload.error.http_status === 413) {
this._fetchMediaConfig(true);
}
case 'notifier_enabled':
case 'upload_started':
case 'upload_finished':
@ -1015,9 +987,11 @@ module.exports = React.createClass({
onDrop: function(ev) {
ev.stopPropagation();
ev.preventDefault();
ContentMessages.sharedInstance().sendContentListToRoom(
ev.dataTransfer.files, this.state.room.roomId, MatrixClientPeg.get(),
);
this.setState({ draggingFile: false });
const files = [...ev.dataTransfer.files];
files.forEach(this.uploadFile);
dis.dispatch({action: 'focus_composer'});
},
onDragLeaveOrEnd: function(ev) {
@ -1026,55 +1000,13 @@ module.exports = React.createClass({
this.setState({ draggingFile: false });
},
isFileUploadAllowed(file) {
if (this.state.mediaConfig !== undefined &&
this.state.mediaConfig["m.upload.size"] !== undefined &&
file.size > this.state.mediaConfig["m.upload.size"]) {
return _t("File is too big. Maximum file size is %(fileSize)s", {fileSize: filesize(this.state.mediaConfig["m.upload.size"])});
}
return true;
},
uploadFile: async function(file) {
dis.dispatch({action: 'focus_composer'});
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'require_registration'});
return;
}
try {
await ContentMessages.sendContentToRoom(file, this.state.room.roomId, MatrixClientPeg.get());
} catch (error) {
if (error.name === "UnknownDeviceError") {
// Let the status bar handle this
return;
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to upload file " + file + " " + error);
Modal.createTrackedDialog('Failed to upload file', '', ErrorDialog, {
title: _t('Failed to upload file'),
description: ((error && error.message)
? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
});
// bail early to avoid calling the dispatch below
return;
}
// Send message_sent callback, for things like _checkIfAlone because after all a file is still a message.
dis.dispatch({
action: 'message_sent',
});
},
injectSticker: function(url, info, text) {
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'require_registration'});
return;
}
ContentMessages.sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get())
ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get())
.done(undefined, (error) => {
if (error.name === "UnknownDeviceError") {
// Let the staus bar handle this
@ -1666,7 +1598,7 @@ module.exports = React.createClass({
let statusBar;
let isStatusAreaExpanded = true;
if (ContentMessages.getCurrentUploads().length > 0) {
if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) {
const UploadBar = sdk.getComponent('structures.UploadBar');
statusBar = <UploadBar room={this.state.room} />;
} else if (!this.state.searchResults) {
@ -1774,11 +1706,9 @@ module.exports = React.createClass({
messageComposer =
<MessageComposer
room={this.state.room}
uploadFile={this.uploadFile}
callState={this.state.callState}
disabled={this.props.disabled}
showApps={this.state.showApps}
uploadAllowed={this.isFileUploadAllowed}
e2eStatus={this.state.e2eStatus}
permalinkCreator={this.state.permalinkCreator}
/>;

View file

@ -29,8 +29,11 @@ const UNPAGINATION_PADDING = 6000;
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
// many scroll events causing many unfilling requests.
const UNFILL_REQUEST_DEBOUNCE_MS = 200;
const PAGE_SIZE = 200;
// _updateHeight makes the height a ceiled multiple of this so we
// don't have to update the height too often. It also allows the user
// to scroll past the pagination spinner a bit so they don't feel blocked so
// much while the content loads.
const PAGE_SIZE = 400;
let debuglog;
if (DEBUG_SCROLL) {
@ -222,10 +225,12 @@ module.exports = React.createClass({
// whether it will stay that way when the children update.
isAtBottom: function() {
const sn = this._getScrollNode();
// fractional values for scrollTop happen on certain browsers/platforms
// fractional values (both too big and too small)
// for scrollTop happen on certain browsers/platforms
// when scrolled all the way down. E.g. Chrome 72 on debian.
// so ceil everything upwards to make sure it aligns.
return Math.ceil(sn.scrollTop) === Math.ceil(sn.scrollHeight - sn.clientHeight);
// so check difference <= 1;
return Math.abs(sn.scrollHeight - (sn.scrollTop + sn.clientHeight)) <= 1;
},
// returns the vertical height in the given direction that can be removed from

View file

@ -47,7 +47,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
},
render: function() {
const uploads = ContentMessages.getCurrentUploads();
const uploads = ContentMessages.sharedInstance().getCurrentUploads();
// for testing UI... - also fix up the ContentMessages.getCurrentUploads().length
// check in RoomView
@ -93,7 +93,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
</div>
<img className="mx_UploadBar_uploadIcon mx_filterFlipColor" src={require("../../../res/img/fileicon.png")} width="17" height="22" />
<img className="mx_UploadBar_uploadCancel mx_filterFlipColor" src={require("../../../res/img/cancel.svg")} width="18" height="18"
onClick={function() { ContentMessages.cancelUpload(upload.promise); }}
onClick={function() { ContentMessages.sharedInstance().cancelUpload(upload.promise); }}
/>
<div className="mx_UploadBar_uploadBytes">
{ uploadedSize } / { totalSize }

View file

@ -0,0 +1,106 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default class UploadConfirmDialog extends React.Component {
static propTypes = {
file: PropTypes.object.isRequired,
currentIndex: PropTypes.number,
totalFiles: PropTypes.number,
onFinished: PropTypes.func.isRequired,
}
static defaultProps = {
totalFiles: 1,
}
constructor(props) {
super(props);
this._objectUrl = URL.createObjectURL(props.file);
}
componentWillUnmount() {
if (this._objectUrl) URL.revokeObjectURL(this._objectUrl);
}
_onCancelClick = () => {
this.props.onFinished(false);
}
_onUploadClick = () => {
this.props.onFinished(true);
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let title;
if (this.props.totalFiles > 1 && this.props.currentIndex !== undefined) {
title = _t(
"Upload files (%(current)s of %(total)s)",
{
current: this.props.currentIndex + 1,
total: this.props.totalFiles,
},
);
} else {
title = _t('Upload files');
}
let preview;
if (this.props.file.type.startsWith('image/')) {
preview = <div className="mx_UploadConfirmDialog_previewOuter">
<div className="mx_UploadConfirmDialog_previewInner">
<div><img className="mx_UploadConfirmDialog_imagePreview" src={this._objectUrl} /></div>
<div>{this.props.file.name}</div>
</div>
</div>;
} else {
preview = <div>
<div>
<img className="mx_UploadConfirmDialog_fileIcon"
src={require("../../../../res/img/files.png")}
/>
{this.props.file.name}
</div>
</div>;
}
return (
<BaseDialog className='mx_UploadConfirmDialog'
onFinished={this._onCancelClick}
title={title}
contentId='mx_Dialog_content'
>
<div id='mx_Dialog_content'>
{preview}
</div>
<DialogButtons primaryButton={_t('Upload')}
hasCancel={false}
onPrimaryButtonClick={this._onUploadClick}
focus={true}
/>
</BaseDialog>
);
}
}

View file

@ -0,0 +1,120 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import filesize from 'filesize';
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import ContentMessages from '../../../ContentMessages';
/*
* Tells the user about files we know cannot be uploaded before we even try uploading
* them. This is named fairly generically but the only thing we check right now is
* the size of the file.
*/
export default class UploadFailureDialog extends React.Component {
static propTypes = {
badFiles: PropTypes.arrayOf(PropTypes.object).isRequired,
totalFiles: PropTypes.number.isRequired,
contentMessages: PropTypes.instanceOf(ContentMessages).isRequired,
onFinished: PropTypes.func.isRequired,
}
_onCancelClick = () => {
this.props.onFinished(false);
}
_onUploadClick = () => {
this.props.onFinished(true);
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let message;
let preview;
let buttons;
if (this.props.totalFiles === 1 && this.props.badFiles.length === 1) {
message = _t(
"This file is <b>too large</b> to upload. " +
"The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.",
{
limit: filesize(this.props.contentMessages.getUploadLimit()),
sizeOfThisFile: filesize(this.props.badFiles[0].size),
}, {
b: sub => <b>{sub}</b>,
},
);
buttons = <DialogButtons primaryButton={_t('OK')}
hasCancel={false}
onPrimaryButtonClick={this._onCancelClick}
focus={true}
/>;
} else if (this.props.totalFiles === this.props.badFiles.length) {
message = _t(
"These files are <b>too large</b> to upload. " +
"The file size limit is %(limit)s.",
{
limit: filesize(this.props.contentMessages.getUploadLimit()),
}, {
b: sub => <b>{sub}</b>,
},
);
buttons = <DialogButtons primaryButton={_t('OK')}
hasCancel={false}
onPrimaryButtonClick={this._onCancelClick}
focus={true}
/>;
} else {
message = _t(
"Some files are <b>too large</b> to be uploaded. " +
"The file size limit is %(limit)s.",
{
limit: filesize(this.props.contentMessages.getUploadLimit()),
}, {
b: sub => <b>{sub}</b>,
},
);
const howManyOthers = this.props.totalFiles - this.props.badFiles.length;
buttons = <DialogButtons
primaryButton={_t('Upload %(count)s other files', { count: howManyOthers })}
onPrimaryButtonClick={this._onUploadClick}
hasCancel={true}
cancelButton={_t("Cancel All")}
onCancel={this._onCancelClick}
focus={true}
/>;
}
return (
<BaseDialog className='mx_UploadFailureDialog'
onFinished={this._onCancelClick}
title={_t("Upload Error")}
contentId='mx_Dialog_content'
>
<div id='mx_Dialog_content'>
{message}
{preview}
</div>
{buttons}
</BaseDialog>
);
}
}

View file

@ -26,6 +26,7 @@ import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import Stickerpicker from './Stickerpicker';
import { makeRoomPermalink } from '../../../matrix-to';
import ContentMessages from '../../../ContentMessages';
import classNames from 'classnames';
import E2EIcon from './E2EIcon';
@ -47,8 +48,7 @@ export default class MessageComposer extends React.Component {
this.onCallClick = this.onCallClick.bind(this);
this.onHangupClick = this.onHangupClick.bind(this);
this.onUploadClick = this.onUploadClick.bind(this);
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
this.uploadFiles = this.uploadFiles.bind(this);
this._onUploadFileInputChange = this._onUploadFileInputChange.bind(this);
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this);
@ -145,89 +145,25 @@ export default class MessageComposer extends React.Component {
this.refs.uploadInput.click();
}
onUploadFileSelected(files) {
const tfiles = files.target.files;
this.uploadFiles(tfiles);
}
_onUploadFileInputChange(ev) {
if (ev.target.files.length === 0) return;
uploadFiles(files) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const fileList = [];
const acceptedFiles = [];
const failedFiles = [];
for (let i=0; i<files.length; i++) {
const fileAcceptedOrError = this.props.uploadAllowed(files[i]);
if (fileAcceptedOrError === true) {
acceptedFiles.push(<li key={i}>
<TintableSvg key={i} src={require("../../../../res/img/files.svg")} width="16" height="16" /> { files[i].name || _t('Attachment') }
</li>);
fileList.push(files[i]);
} else {
failedFiles.push(<li key={i}>
<TintableSvg key={i} src={require("../../../../res/img/files.svg")} width="16" height="16" /> { files[i].name || _t('Attachment') } <p>{ _t('Reason') + ": " + fileAcceptedOrError}</p>
</li>);
}
// take a copy so we can safely reset the value of the form control
// (Note it is a FileList: we can't use slice or sesnible iteration).
const tfiles = [];
for (let i = 0; i < ev.target.files.length; ++i) {
tfiles.push(ev.target.files[i]);
}
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
let replyToWarning = null;
if (isQuoting) {
replyToWarning = <p>{
_t('At this time it is not possible to reply with a file so this will be sent without being a reply.')
}</p>;
}
const acceptedFilesPart = acceptedFiles.length === 0 ? null : (
<div>
<p>{ _t('Are you sure you want to upload the following files?') }</p>
<ul style={{listStyle: 'none', textAlign: 'left'}}>
{ acceptedFiles }
</ul>
</div>
ContentMessages.sharedInstance().sendContentListToRoom(
tfiles, this.props.room.roomId, MatrixClientPeg.get(),
);
const failedFilesPart = failedFiles.length === 0 ? null : (
<div>
<p>{ _t('The following files cannot be uploaded:') }</p>
<ul style={{listStyle: 'none', textAlign: 'left'}}>
{ failedFiles }
</ul>
</div>
);
let buttonText;
if (acceptedFiles.length > 0 && failedFiles.length > 0) {
buttonText = "Upload selected"
} else if (failedFiles.length > 0) {
buttonText = "Close"
}
Modal.createTrackedDialog('Upload Files confirmation', '', QuestionDialog, {
title: _t('Upload Files'),
description: (
<div>
{ acceptedFilesPart }
{ failedFilesPart }
{ replyToWarning }
</div>
),
hasCancelButton: acceptedFiles.length > 0,
button: buttonText,
onFinished: (shouldUpload) => {
if (shouldUpload) {
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
if (fileList) {
for (let i=0; i<fileList.length; i++) {
this.props.uploadFile(fileList[i]);
}
}
}
this.refs.uploadInput.value = null;
},
});
// This is the onChange handler for a file form control, but we're
// not keeping any state, so reset the value of the form control
// to empty.
// NB. we need to set 'value': the 'files' property is immutable.
ev.target.value = '';
}
onHangupClick() {
@ -376,7 +312,7 @@ export default class MessageComposer extends React.Component {
<input ref="uploadInput" type="file"
style={uploadInputStyle}
multiple
onChange={this.onUploadFileSelected} />
onChange={this._onUploadFileInputChange} />
</AccessibleButton>
);
@ -414,7 +350,6 @@ export default class MessageComposer extends React.Component {
key="controls_input"
room={this.props.room}
placeholder={placeholderText}
onFilesPasted={this.uploadFiles}
onInputStateChanged={this.onInputStateChanged}
permalinkCreator={this.props.permalinkCreator} />,
formattingButton,
@ -510,12 +445,6 @@ MessageComposer.propTypes = {
// string representing the current voip call state
callState: PropTypes.string,
// callback when a file to upload is chosen
uploadFile: PropTypes.func.isRequired,
// function to test whether a file should be allowed to be uploaded.
uploadAllowed: PropTypes.func.isRequired,
// string representing the current room app drawer state
showApps: PropTypes.bool
};

View file

@ -47,6 +47,7 @@ import {Completion} from "../../../autocomplete/Autocompleter";
import Markdown from '../../../Markdown';
import ComposerHistoryManager from '../../../ComposerHistoryManager';
import MessageComposerStore from '../../../stores/MessageComposerStore';
import ContentMessage from '../../../ContentMessages';
import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix';
@ -1009,7 +1010,13 @@ export default class MessageComposerInput extends React.Component {
switch (transfer.type) {
case 'files':
return this.props.onFilesPasted(transfer.files);
// This actually not so much for 'files' as such (at time of writing
// neither chrome nor firefox let you paste a plain file copied
// from Finder) but more images copied from a different website
// / word processor etc.
return ContentMessage.sharedInstance().sendContentListToRoom(
transfer.files, this.props.room.roomId, this.client,
);
case 'html': {
if (this.state.isRichTextEnabled) {
// FIXME: https://github.com/ianstormtaylor/slate/issues/1497 means

View file

@ -52,10 +52,15 @@ export default class RoomBreadcrumbs extends React.Component {
console.error("Failed to parse breadcrumbs:", e);
}
}
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
}
componentWillUnmount() {
dis.unregister(this._dispatcherRef);
const client = MatrixClientPeg.get();
if (client) client.removeListener("Room.myMembership", this.onMyMembership);
}
componentDidUpdate() {
@ -81,6 +86,17 @@ export default class RoomBreadcrumbs extends React.Component {
}
}
onMyMembership = (room, membership) => {
if (membership === "leave" || membership === "ban") {
const rooms = this.state.rooms.slice();
const roomState = rooms.find((r) => r.room.roomId === room.roomId);
if (roomState) {
roomState.left = true;
this.setState({rooms});
}
}
};
_appendRoomId(roomId) {
const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) {
@ -130,23 +146,24 @@ export default class RoomBreadcrumbs extends React.Component {
return null;
}
const rooms = this.state.rooms;
const avatars = rooms.map(({room, animated, hover}, i) => {
const avatars = rooms.map((r, i) => {
const isFirst = i === 0;
const classes = classNames({
"mx_RoomBreadcrumbs_crumb": true,
"mx_RoomBreadcrumbs_preAnimate": isFirst && !animated,
"mx_RoomBreadcrumbs_preAnimate": isFirst && !r.animated,
"mx_RoomBreadcrumbs_animate": isFirst,
"mx_RoomBreadcrumbs_left": r.left,
});
let tooltip = null;
if (hover) {
tooltip = <Tooltip label={room.name} />;
if (r.hover) {
tooltip = <Tooltip label={r.room.name} />;
}
return (
<AccessibleButton className={classes} key={room.roomId} onClick={() => this._viewRoom(room)}
onMouseEnter={() => this._onMouseEnter(room)} onMouseLeave={() => this._onMouseLeave(room)}>
<RoomAvatar room={room} width={32} height={32} />
<AccessibleButton className={classes} key={r.room.roomId} onClick={() => this._viewRoom(r.room)}
onMouseEnter={() => this._onMouseEnter(r.room)} onMouseLeave={() => this._onMouseLeave(r.room)}>
<RoomAvatar room={r.room} width={32} height={32} />
{tooltip}
</AccessibleButton>
);

View file

@ -29,9 +29,9 @@ import PersistedElement from "../elements/PersistedElement";
const widgetType = 'm.stickerpicker';
// We sit in a context menu, so the persisted element container needs to float
// above it, so it needs a greater z-index than the ContextMenu
const STICKERPICKER_Z_INDEX = 5000;
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
// We sit in a context menu, so this should be given to the context menu.
const STICKERPICKER_Z_INDEX = 3500;
// Key to store the widget's AppTile under in PersistedElement
const PERSISTED_ELEMENT_KEY = "stickerPicker";
@ -373,6 +373,7 @@ export default class Stickerpicker extends React.Component {
menuPaddingTop={0}
menuPaddingLeft={0}
menuPaddingRight={0}
zIndex={STICKERPICKER_Z_INDEX}
/>;
if (this.state.showStickers) {