Extract message options button to action bar
This adds a new action bar component to hold multiple per-message actions. This existing options button has moved to this new component, and is currently the only action.
This commit is contained in:
parent
530c92e03d
commit
ed8bbc7082
6 changed files with 148 additions and 61 deletions
|
@ -113,6 +113,7 @@
|
||||||
@import "./views/messages/_MNoticeBody.scss";
|
@import "./views/messages/_MNoticeBody.scss";
|
||||||
@import "./views/messages/_MStickerBody.scss";
|
@import "./views/messages/_MStickerBody.scss";
|
||||||
@import "./views/messages/_MTextBody.scss";
|
@import "./views/messages/_MTextBody.scss";
|
||||||
|
@import "./views/messages/_MessageActionBar.scss";
|
||||||
@import "./views/messages/_MessageTimestamp.scss";
|
@import "./views/messages/_MessageTimestamp.scss";
|
||||||
@import "./views/messages/_RoomAvatarEvent.scss";
|
@import "./views/messages/_RoomAvatarEvent.scss";
|
||||||
@import "./views/messages/_SenderProfile.scss";
|
@import "./views/messages/_SenderProfile.scss";
|
||||||
|
|
37
res/css/views/messages/_MessageActionBar.scss
Normal file
37
res/css/views/messages/_MessageActionBar.scss
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_MessageActionBar {
|
||||||
|
position: absolute;
|
||||||
|
visibility: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 6px;
|
||||||
|
right: 6px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageActionBar_optionsButton {
|
||||||
|
display: inline-block;
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
background-image: url($edit-button-url);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MatrixChat_useCompactLayout {
|
||||||
|
.mx_MessageActionBar {
|
||||||
|
top: 3px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -121,7 +121,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile:hover .mx_EventTile_line,
|
.mx_EventTile:hover .mx_EventTile_line,
|
||||||
.mx_EventTile.menu .mx_EventTile_line
|
.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line
|
||||||
{
|
{
|
||||||
background-color: $event-selected-color;
|
background-color: $event-selected-color;
|
||||||
}
|
}
|
||||||
|
@ -206,7 +206,7 @@ limitations under the License.
|
||||||
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
||||||
.mx_EventTile_last > div > a > .mx_MessageTimestamp,
|
.mx_EventTile_last > div > a > .mx_MessageTimestamp,
|
||||||
.mx_EventTile:hover > div > a > .mx_MessageTimestamp,
|
.mx_EventTile:hover > div > a > .mx_MessageTimestamp,
|
||||||
.mx_EventTile.menu > div > a > .mx_MessageTimestamp {
|
.mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,21 +219,8 @@ limitations under the License.
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_optionsButton {
|
.mx_EventTile:hover .mx_MessageActionBar,
|
||||||
position: absolute;
|
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar {
|
||||||
display: inline-block;
|
|
||||||
visibility: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
top: 6px;
|
|
||||||
right: 6px;
|
|
||||||
width: 19px;
|
|
||||||
height: 19px;
|
|
||||||
background-image: url($edit-button-url);
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile:hover .mx_EventTile_optionsButton,
|
|
||||||
.mx_EventTile.menu .mx_EventTile_optionsButton {
|
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -551,10 +538,6 @@ limitations under the License.
|
||||||
top: 3px;
|
top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_optionsButton {
|
|
||||||
top: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_readAvatars {
|
.mx_EventTile_readAvatars {
|
||||||
top: 27px;
|
top: 27px;
|
||||||
}
|
}
|
||||||
|
|
87
src/components/views/messages/MessageActionBar.js
Normal file
87
src/components/views/messages/MessageActionBar.js
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
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 { _t } from '../../../languageHandler';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import Modal from '../../../Modal';
|
||||||
|
import { createMenu } from '../../structures/ContextualMenu';
|
||||||
|
|
||||||
|
export default class MessageActionBar extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
mxEvent: PropTypes.object.isRequired,
|
||||||
|
permalinkCreator: PropTypes.object,
|
||||||
|
tile: PropTypes.element,
|
||||||
|
replyThread: PropTypes.element,
|
||||||
|
onFocusChange: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
onFocusChange = (focused) => {
|
||||||
|
if (!this.props.onFocusChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.props.onFocusChange(focused);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCryptoClicked = () => {
|
||||||
|
const event = this.props.mxEvent;
|
||||||
|
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
||||||
|
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
|
||||||
|
{event},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onOptionsClicked = (ev) => {
|
||||||
|
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
|
||||||
|
const buttonRect = ev.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 {tile, replyThread} = this.props;
|
||||||
|
|
||||||
|
let e2eInfoCallback = null;
|
||||||
|
if (this.props.mxEvent.isEncrypted()) {
|
||||||
|
e2eInfoCallback = () => this.onCryptoClicked();
|
||||||
|
}
|
||||||
|
|
||||||
|
createMenu(MessageContextMenu, {
|
||||||
|
chevronOffset: 10,
|
||||||
|
mxEvent: this.props.mxEvent,
|
||||||
|
left: x,
|
||||||
|
top: y,
|
||||||
|
permalinkCreator: this.props.permalinkCreator,
|
||||||
|
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
|
||||||
|
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
|
||||||
|
e2eInfoCallback: e2eInfoCallback,
|
||||||
|
onFinished: () => {
|
||||||
|
this.onFocusChange(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.onFocusChange(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// TODO: Move the bar to a separate element once there are several buttons
|
||||||
|
return <span className="mx_MessageActionBar mx_MessageActionBar_optionsButton"
|
||||||
|
title={_t("Options")}
|
||||||
|
onClick={this.onOptionsClicked}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
@ -30,7 +29,6 @@ const sdk = require('../../../index');
|
||||||
const TextForEvent = require('../../../TextForEvent');
|
const TextForEvent = require('../../../TextForEvent');
|
||||||
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||||
|
|
||||||
const ContextualMenu = require('../../structures/ContextualMenu');
|
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {EventStatus} from 'matrix-js-sdk';
|
import {EventStatus} from 'matrix-js-sdk';
|
||||||
|
@ -172,8 +170,8 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
// Whether the context menu is being displayed.
|
// Whether the action bar is focused.
|
||||||
menu: false,
|
actionBarFocused: false,
|
||||||
// Whether all read receipts are being displayed. If not, only display
|
// Whether all read receipts are being displayed. If not, only display
|
||||||
// a truncation of them.
|
// a truncation of them.
|
||||||
allReadAvatars: false,
|
allReadAvatars: false,
|
||||||
|
@ -309,36 +307,6 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
return actions.tweaks.highlight;
|
return actions.tweaks.highlight;
|
||||||
},
|
},
|
||||||
|
|
||||||
onOptionsClicked: function(e) {
|
|
||||||
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
|
|
||||||
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 self = this;
|
|
||||||
|
|
||||||
const {tile, replyThread} = this.refs;
|
|
||||||
|
|
||||||
let e2eInfoCallback = null;
|
|
||||||
if (this.props.mxEvent.isEncrypted()) e2eInfoCallback = () => this.onCryptoClicked();
|
|
||||||
|
|
||||||
ContextualMenu.createMenu(MessageContextMenu, {
|
|
||||||
chevronOffset: 10,
|
|
||||||
mxEvent: this.props.mxEvent,
|
|
||||||
left: x,
|
|
||||||
top: y,
|
|
||||||
permalinkCreator: this.props.permalinkCreator,
|
|
||||||
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
|
|
||||||
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
|
|
||||||
e2eInfoCallback: e2eInfoCallback,
|
|
||||||
onFinished: function() {
|
|
||||||
self.setState({menu: false});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.setState({menu: true});
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleAllReadAvatars: function() {
|
toggleAllReadAvatars: function() {
|
||||||
this.setState({
|
this.setState({
|
||||||
allReadAvatars: !this.state.allReadAvatars,
|
allReadAvatars: !this.state.allReadAvatars,
|
||||||
|
@ -490,6 +458,12 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onActionBarFocusChange(focused) {
|
||||||
|
this.setState({
|
||||||
|
actionBarFocused: focused,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
|
const MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
|
||||||
const SenderProfile = sdk.getComponent('messages.SenderProfile');
|
const SenderProfile = sdk.getComponent('messages.SenderProfile');
|
||||||
|
@ -536,7 +510,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation,
|
mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation,
|
||||||
mx_EventTile_last: this.props.last,
|
mx_EventTile_last: this.props.last,
|
||||||
mx_EventTile_contextual: this.props.contextual,
|
mx_EventTile_contextual: this.props.contextual,
|
||||||
menu: this.state.menu,
|
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
|
||||||
mx_EventTile_verified: this.state.verified === true,
|
mx_EventTile_verified: this.state.verified === true,
|
||||||
mx_EventTile_unverified: this.state.verified === false,
|
mx_EventTile_unverified: this.state.verified === false,
|
||||||
mx_EventTile_bad: isEncryptionFailure,
|
mx_EventTile_bad: isEncryptionFailure,
|
||||||
|
@ -602,9 +576,14 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionsButton = (
|
const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
|
||||||
<span className="mx_EventTile_optionsButton" title={_t("Options")} onClick={this.onOptionsClicked} />
|
const actionBar = <MessageActionBar
|
||||||
);
|
mxEvent={this.props.mxEvent}
|
||||||
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
|
tile={this.refs.tile}
|
||||||
|
replyThread={this.refs.replyThread}
|
||||||
|
onFocusChange={this.onActionBarFocusChange}
|
||||||
|
/>;
|
||||||
|
|
||||||
const timestamp = this.props.mxEvent.getTs() ?
|
const timestamp = this.props.mxEvent.getTs() ?
|
||||||
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
||||||
|
@ -755,7 +734,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
onHeightChanged={this.props.onHeightChanged} />
|
onHeightChanged={this.props.onHeightChanged} />
|
||||||
{ keyRequestInfo }
|
{ keyRequestInfo }
|
||||||
{ optionsButton }
|
{ actionBar }
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
// The avatar goes after the event tile as it's absolutly positioned to be over the
|
// The avatar goes after the event tile as it's absolutly positioned to be over the
|
||||||
|
|
|
@ -671,7 +671,6 @@
|
||||||
"%(senderName)s sent an image": "%(senderName)s sent an image",
|
"%(senderName)s sent an image": "%(senderName)s sent an image",
|
||||||
"%(senderName)s sent a video": "%(senderName)s sent a video",
|
"%(senderName)s sent a video": "%(senderName)s sent a video",
|
||||||
"%(senderName)s uploaded a file": "%(senderName)s uploaded a file",
|
"%(senderName)s uploaded a file": "%(senderName)s uploaded a file",
|
||||||
"Options": "Options",
|
|
||||||
"Your key share request has been sent - please check your other devices for key share requests.": "Your key share request has been sent - please check your other devices for key share requests.",
|
"Your key share request has been sent - please check your other devices for key share requests.": "Your key share request has been sent - please check your other devices for key share requests.",
|
||||||
"Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.": "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.",
|
"Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.": "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.",
|
||||||
"If your other devices do not have the key for this message you will not be able to decrypt them.": "If your other devices do not have the key for this message you will not be able to decrypt them.",
|
"If your other devices do not have the key for this message you will not be able to decrypt them.": "If your other devices do not have the key for this message you will not be able to decrypt them.",
|
||||||
|
@ -889,6 +888,7 @@
|
||||||
"Today": "Today",
|
"Today": "Today",
|
||||||
"Yesterday": "Yesterday",
|
"Yesterday": "Yesterday",
|
||||||
"Error decrypting audio": "Error decrypting audio",
|
"Error decrypting audio": "Error decrypting audio",
|
||||||
|
"Options": "Options",
|
||||||
"Attachment": "Attachment",
|
"Attachment": "Attachment",
|
||||||
"Error decrypting attachment": "Error decrypting attachment",
|
"Error decrypting attachment": "Error decrypting attachment",
|
||||||
"Decrypt %(text)s": "Decrypt %(text)s",
|
"Decrypt %(text)s": "Decrypt %(text)s",
|
||||||
|
|
Loading…
Reference in a new issue