Merge pull request #2935 from jryans/message-action-bar

Add a message action bar
pull/21833/head
J. Ryan Stinnett 2019-04-29 18:01:15 +01:00 committed by GitHub
commit 0bc54ba904
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 232 additions and 95 deletions

View File

@ -113,6 +113,7 @@
@import "./views/messages/_MNoticeBody.scss";
@import "./views/messages/_MStickerBody.scss";
@import "./views/messages/_MTextBody.scss";
@import "./views/messages/_MessageActionBar.scss";
@import "./views/messages/_MessageTimestamp.scss";
@import "./views/messages/_RoomAvatarEvent.scss";
@import "./views/messages/_SenderProfile.scss";

View File

@ -0,0 +1,73 @@
/*
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;
display: flex;
height: 24px;
border-radius: 4px;
background: $primary-bg-color;
top: -13px;
right: 8px;
user-select: none;
> * {
display: inline-block;
position: relative;
width: 27px;
border: 1px solid $message-action-bar-border-color;
margin-left: -1px;
&:hover {
border-color: $message-action-bar-hover-border-color;
z-index: 1;
}
&:first-child {
border-radius: 3px 0 0 3px;
}
&:last-child {
border-radius: 0 3px 3px 0;
}
&:only-child {
border-radius: 3px;
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
mask-repeat: no-repeat;
mask-position: center;
background-color: $primary-fg-color;
}
}
}
.mx_MessageActionBar_replyButton::after {
mask-image: url('$(res)/img/reply.svg');
}
.mx_MessageActionBar_optionsButton::after {
mask-image: url('$(res)/img/icon_context.svg');
}

View File

@ -121,7 +121,7 @@ limitations under the License.
}
.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;
}
@ -206,7 +206,7 @@ limitations under the License.
// 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:hover > div > a > .mx_MessageTimestamp,
.mx_EventTile.menu > div > a > .mx_MessageTimestamp {
.mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp {
visibility: visible;
}
@ -219,21 +219,8 @@ limitations under the License.
width: auto;
}
.mx_EventTile_editButton {
position: absolute;
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_editButton,
.mx_EventTile.menu .mx_EventTile_editButton {
.mx_EventTile:hover .mx_MessageActionBar,
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar {
visibility: visible;
}
@ -551,10 +538,6 @@ limitations under the License.
top: 3px;
}
.mx_EventTile_editButton {
top: 3px;
}
.mx_EventTile_readAvatars {
top: 27px;
}

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="19px" height="19px" viewBox="0 0 19 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 39.1 (31720) - http://www.bohemiancoding.com/sketch -->
<title>ED5D3E59-2561-4AC1-9B43-82FBC51767FC</title>
<desc>Created with sketchtool.</desc>
<defs></defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon_context">
<g>
<path d="M9.5,19 C14.7467051,19 19,14.7467051 19,9.5 C19,4.25329488 14.7467051,0 9.5,0 C4.25329488,0 0,4.25329488 0,9.5 C0,14.7467051 4.25329488,19 9.5,19 Z" id="Oval-69" fill="#ECECEC"></path>
<path d="M4.5,9.50063771 C4.5,9.13148623 4.59887838,8.85242947 4.7966381,8.66345907 C4.99439782,8.47448867 5.28224377,8.38000488 5.66018457,8.38000488 C6.0249414,8.38000488 6.3072941,8.47668596 6.50725115,8.67005103 C6.70720821,8.86341609 6.80718523,9.14027555 6.80718523,9.50063771 C6.80718523,9.84781589 6.70610956,10.1213794 6.50395517,10.3213365 C6.30180079,10.5212935 6.02054674,10.6212705 5.66018457,10.6212705 C5.29103309,10.6212705 5.00538444,10.5234908 4.80323006,10.3279284 C4.60107568,10.132366 4.5,9.85660521 4.5,9.50063771 L4.5,9.50063771 Z M8.3431114,9.50063771 C8.3431114,9.13148623 8.44198978,8.85242947 8.63974951,8.66345907 C8.83750923,8.47448867 9.12755247,8.38000488 9.50988794,8.38000488 C9.87464476,8.38000488 10.1569975,8.47668596 10.3569545,8.67005103 C10.5569116,8.86341609 10.6568886,9.14027555 10.6568886,9.50063771 C10.6568886,9.84781589 10.5558129,10.1213794 10.3536585,10.3213365 C10.1515042,10.5212935 9.8702501,10.6212705 9.50988794,10.6212705 C9.13634179,10.6212705 8.84849585,10.5234908 8.64634146,10.3279284 C8.44418708,10.132366 8.3431114,9.85660521 8.3431114,9.50063771 L8.3431114,9.50063771 Z M12.1928148,9.50063771 C12.1928148,9.13148623 12.2916931,8.85242947 12.4894529,8.66345907 C12.6872126,8.47448867 12.9750585,8.38000488 13.3529993,8.38000488 C13.7177562,8.38000488 14.0001089,8.47668596 14.2000659,8.67005103 C14.400023,8.86341609 14.5,9.14027555 14.5,9.50063771 C14.5,9.84781589 14.3989243,10.1213794 14.1967699,10.3213365 C13.9946156,10.5212935 13.7133615,10.6212705 13.3529993,10.6212705 C12.9838479,10.6212705 12.6981992,10.5234908 12.4960448,10.3279284 C12.2938904,10.132366 12.1928148,9.85660521 12.1928148,9.50063771 L12.1928148,9.50063771 Z" id="…" fill="#9B9B9B"></path>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

6
res/img/reply.svg Normal file
View File

@ -0,0 +1,6 @@
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg">
<g stroke="#2E2F32" stroke-width=".75" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<path d="M8.75 4.75L12.5 8.5l-3.75 3.75"/>
<path d="M.5.25V5.5a3 3 0 0 0 3 3h9"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 289 B

View File

@ -146,6 +146,9 @@ $room-warning-bg-color: $header-panel-bg-color;
$dark-panel-bg-color: $header-panel-bg-color;
$panel-gradient: rgba(34, 38, 46, 0), rgba(34, 38, 46, 1);
$message-action-bar-border-color: $input-darker-bg-color;
$message-action-bar-hover-border-color: $text-secondary-color;
// ***** Mixins! *****
@define-mixin mx_DialogButton {

View File

@ -73,7 +73,7 @@ $primary-hairline-color: #e5e5e5;
// used for the border of input text fields
$input-border-color: #e7e7e7;
$input-darker-bg-color: rgba(193, 201, 214, 0.29);
$input-darker-bg-color: #e3e8f0;
$input-darker-fg-color: #9fa9ba;
$input-lighter-bg-color: #f2f5f8;
$input-lighter-fg-color: $input-darker-fg-color;
@ -153,7 +153,7 @@ $roomheader-button-color: #91A1C0;
$groupheader-button-color: #91A1C0;
$rightpanel-button-color: #91A1C0;
$composer-button-color: #91A1C0;
$roomtopic-color: #9fa9ba;
$roomtopic-color: #9e9e9e;
$eventtile-meta-color: $roomtopic-color;
$composer-e2e-icon-color: #c9ced6;
@ -203,7 +203,6 @@ $event-redacted-border-color: #cccccc;
// event timestamp
$event-timestamp-color: #acacac;
$edit-button-url: "$(res)/img/icon_context_message.svg";
$copy-button-url: "$(res)/img/icon_copy_message.svg";
// e2e
@ -255,6 +254,9 @@ $authpage-secondary-color: #61708b;
$dark-panel-bg-color: $secondary-accent-color;
$panel-gradient: rgba(242, 245, 248, 0), rgba(242, 245, 248, 1);
$message-action-bar-border-color: $input-darker-bg-color;
$message-action-bar-hover-border-color: $roomtopic-color;
// ***** Mixins! *****
@define-mixin mx_DialogButton {

View File

@ -201,14 +201,6 @@ module.exports = React.createClass({
this.closeMenu();
},
onReplyClick: function() {
dis.dispatch({
action: 'reply_to_event',
event: this.props.mxEvent,
});
this.closeMenu();
},
onCollapseReplyThreadClick: function() {
this.props.collapseReplyThread();
this.closeMenu();
@ -226,7 +218,6 @@ module.exports = React.createClass({
let unhidePreviewButton;
let externalURLButton;
let quoteButton;
let replyButton;
let collapseReplyThread;
// status is SENT before remote-echo, null after
@ -265,12 +256,6 @@ module.exports = React.createClass({
</div>
);
replyButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onReplyClick}>
{ _t('Reply') }
</div>
);
if (this.state.canPin) {
pinButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
@ -368,7 +353,6 @@ module.exports = React.createClass({
{ unhidePreviewButton }
{ permalinkButton }
{ quoteButton }
{ replyButton }
{ externalURLButton }
{ collapseReplyThread }
{ e2eInfo }

View File

@ -0,0 +1,121 @@
/*
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 {EventStatus} from 'matrix-js-sdk';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import dis from '../../../dispatcher';
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},
);
}
onReplyClick = (ev) => {
dis.dispatch({
action: 'reply_to_event',
event: this.props.mxEvent,
});
}
onOptionsClick = (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() {
const { mxEvent } = this.props;
const { status: eventStatus } = mxEvent;
// status is SENT before remote-echo, null after
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
let replyButton;
if (isSent && mxEvent.getType() === 'm.room.message') {
const content = mxEvent.getContent();
if (
content.msgtype &&
content.msgtype !== 'm.bad.encrypted' &&
content.hasOwnProperty('body')
) {
replyButton = <span className="mx_MessageActionBar_replyButton"
title={_t("Reply")}
onClick={this.onReplyClick}
/>;
}
}
return <div className="mx_MessageActionBar">
{replyButton}
<span className="mx_MessageActionBar_optionsButton"
title={_t("Options")}
onClick={this.onOptionsClick}
/>
</div>;
}
}

View File

@ -17,7 +17,6 @@ limitations under the License.
'use strict';
import ReplyThread from "../elements/ReplyThread";
const React = require('react');
@ -30,7 +29,6 @@ const sdk = require('../../../index');
const TextForEvent = require('../../../TextForEvent');
import withMatrixClient from '../../../wrappers/withMatrixClient';
const ContextualMenu = require('../../structures/ContextualMenu');
import dis from '../../../dispatcher';
import SettingsStore from "../../../settings/SettingsStore";
import {EventStatus} from 'matrix-js-sdk';
@ -172,8 +170,8 @@ module.exports = withMatrixClient(React.createClass({
getInitialState: function() {
return {
// Whether the context menu is being displayed.
menu: false,
// Whether the action bar is focused.
actionBarFocused: false,
// Whether all read receipts are being displayed. If not, only display
// a truncation of them.
allReadAvatars: false,
@ -309,36 +307,6 @@ module.exports = withMatrixClient(React.createClass({
return actions.tweaks.highlight;
},
onEditClicked: 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() {
this.setState({
allReadAvatars: !this.state.allReadAvatars,
@ -490,6 +458,12 @@ module.exports = withMatrixClient(React.createClass({
return null;
},
onActionBarFocusChange(focused) {
this.setState({
actionBarFocused: focused,
});
},
render: function() {
const MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
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_last: this.props.last,
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_unverified: this.state.verified === false,
mx_EventTile_bad: isEncryptionFailure,
@ -602,9 +576,14 @@ module.exports = withMatrixClient(React.createClass({
}
}
const editButton = (
<span className="mx_EventTile_editButton" title={_t("Options")} onClick={this.onEditClicked} />
);
const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
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() ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
@ -755,7 +734,7 @@ module.exports = withMatrixClient(React.createClass({
showUrlPreview={this.props.showUrlPreview}
onHeightChanged={this.props.onHeightChanged} />
{ keyRequestInfo }
{ editButton }
{ actionBar }
</div>
{
// The avatar goes after the event tile as it's absolutly positioned to be over the

View File

@ -671,7 +671,6 @@
"%(senderName)s sent an image": "%(senderName)s sent an image",
"%(senderName)s sent a video": "%(senderName)s sent a video",
"%(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.",
"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.",
@ -889,6 +888,7 @@
"Today": "Today",
"Yesterday": "Yesterday",
"Error decrypting audio": "Error decrypting audio",
"Options": "Options",
"Attachment": "Attachment",
"Error decrypting attachment": "Error decrypting attachment",
"Decrypt %(text)s": "Decrypt %(text)s",