Create a component which renders message actions in a dropdown

This commit is contained in:
JC Brand 2020-06-04 17:26:18 +02:00
parent 6ad76c14ef
commit c2c1db587f
10 changed files with 127 additions and 48 deletions

View File

@ -90,7 +90,6 @@
display: inline-flex;
width: 100%;
flex-direction: row;
overflow: auto; // Ensures that content stays inside
padding: 0.125rem 1rem;
&.onload {
@ -98,12 +97,7 @@
-webkit-animation: colorchange-chatmessage 1s;
}
&:hover {
background-color: rgba(0, 0, 0, 0.035);
.chat-msg__actions {
.chat-msg__action {
opacity: 1;
}
}
background-color: var(--list-item-hover-color);
}
&.correcting {
&.groupchat {
@ -162,9 +156,15 @@
display: flex;
flex-direction: row;
justify-content: space-between;
&:hover {
.btn--standalone {
opacity: 1;
}
}
}
.chat-msg__message {
line-height: 1.5em;
display: inline-flex;
flex-direction: column;
width: 100%;
@ -235,20 +235,36 @@
}
}
converse-message-actions {
margin-left: 0.5em;
}
.chat-msg__actions {
display: flex;
flex-wrap: nowrap;
.chat-msg__action {
height: var(--message-font-size);
font-size: var(--message-font-size);
padding: 0;
margin-left: 0.75em;
.dropdown-menu {
min-width: 5rem;
}
i {
color: var(--text-color-lighten-15-percent);
font-size: 70%;
}
button {
border: none;
opacity: 0;
background: transparent;
cursor: pointer;
&:focus {
opacity: 1;
color: var(--text-color-lighten-15-percent);
padding: 0 0.25em;
}
.btn--standalone {
opacity: 0;
margin-top: -0.2em;
}
.chat-msg__action {
width: 100%;
padding: 0.5em 1em;
text-align: left;
white-space: nowrap;
&:hover {
color: var(--text-color);
background-color: var(--list-item-hover-color);
}
}
}

View File

@ -178,6 +178,7 @@ $mobile_portrait_length: 480px !default;
--list-toggle-color: #818479; // $gray-color
--list-toggle-hover-color: #585B51; // $dark-gray-color
--list-toggle-font-weight: normal;
--list-item-hover-color: rgba(0, 0, 0, 0.035);
--list-item-action-color: #e3eef3; // lighten($lightest-blue, 25%)
--list-item-link-color: inherit;
--list-item-link-hover-color: var(--dark-link-color);

View File

@ -107,7 +107,7 @@ describe("Chatboxes", function () {
// get the 'chat-msg--followup' class.
message = 'This a normal message';
await mock.sendMessage(view, message);
const msg_txt_sel = 'converse-chat-message:last-child .chat-msg__body';
const msg_txt_sel = 'converse-chat-message:last-child .chat-msg__text';
await u.waitUntil(() => view.el.querySelector(msg_txt_sel).textContent.trim() === message);
let el = view.el.querySelector('converse-chat-message:last-child .chat-msg__body');
expect(u.hasClass('chat-msg--followup', el)).toBeFalsy();

View File

@ -106,7 +106,7 @@ describe("A Chat Message", function () {
const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'});
expect(view.el.querySelectorAll('.chat-msg .chat-msg__action').length).toBe(2);
let action = view.el.querySelector('.chat-msg .chat-msg__action');
expect(action.getAttribute('title')).toBe('Edit this message');
expect(action.textContent.trim()).toBe('Edit');
action.style.opacity = 1;
action.click();

View File

@ -1215,7 +1215,7 @@ describe("A Groupchat Message", function () {
`<origin-id id="${msg.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
`</message>`);
const action = view.el.querySelector('.chat-msg .chat-msg__action');
const action = await u.waitUntil(() => view.el.querySelector('.chat-msg .chat-msg__action'));
action.style.opacity = 1;
action.click();

View File

@ -437,7 +437,8 @@ describe("The OMEMO module", function() {
_converse.connection._dataRecv(mock.createRequest(carbon));
await new Promise(resolve => view.model.messages.once('rendered', resolve));
expect(view.model.messages.length).toBe(1);
expect(view.el.querySelector('.chat-msg__body').textContent.trim())
expect(view.el.querySelector('.chat-msg__text').textContent.trim())
.toBe('This is an encrypted carbon message from another device of mine');
expect(devicelist.devices.length).toBe(2);

View File

@ -119,8 +119,7 @@ describe("Message Retractions", function () {
`);
_converse.connection._dataRecv(mock.createRequest(received_stanza));
await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2);
expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1, 1000);
expect(view.model.messages.length).toBe(1);
const message = view.model.messages.at(0)

View File

@ -52,15 +52,17 @@ export class DropdownList extends BaseDropdown {
static get properties () {
return {
'icon_classes': { type: String },
'items': { type: Array }
}
}
render () {
const icon_classes = this.icon_classes || "fa fa-bars";
return html`
<div class="dropleft">
<button type="button" class="btn btn--transparent btn--standalone" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-bars only-icon"></i>
<i class="${icon_classes} only-icon"></i>
</button>
<div class="dropdown-menu">
${ this.items.map(b => until(b, '')) }

View File

@ -0,0 +1,74 @@
import { CustomElement } from './element.js';
import { __ } from '@converse/headless/i18n';
import { html } from 'lit-element';
import { until } from 'lit-html/directives/until.js';
class MessageActions extends CustomElement {
static get properties () {
return {
chatview: { type: Object },
model: { type: Object },
editable: { type: Boolean },
correcting: { type: Boolean },
message_type: { type: String },
is_retracted: { type: Boolean },
}
}
render () {
return html`${ until(this.renderActions(), '') }`;
}
static getActionsDropdownItem (o) {
return html`
<button class="chat-msg__action ${o.button_class}" @click=${o.handler}>
<fa-icon class="${o.icon_class}" path-prefix="/dist" color="var(--text-color-lighten-15-percent)" size="1em"></fa-icon>
${o.i18n_text}
</button>
`;
}
onMessageEditButtonClicked (ev) {
ev.preventDefault();
this.chatview.onMessageEditButtonClicked(this.model);
}
onMessageRetractButtonClicked (ev) {
ev.preventDefault();
this.chatview.onMessageRetractButtonClicked(this.model);
}
async renderActions () {
const buttons = [];
if (this.editable) {
buttons.push({
'i18n_text': this.correcting ? __('Cancel Editing') : __('Edit'),
'handler': ev => this.onMessageEditButtonClicked(ev),
'button_class': 'chat-msg__action-edit',
'icon_class': 'fa fa-pencil-alt',
'name': 'edit'
});
}
const may_be_moderated = this.model.get('type') === 'groupchat' && await this.model.mayBeModerated();
const retractable = !this.is_retracted && (this.model.mayBeRetracted() || may_be_moderated);
if (retractable) {
buttons.push({
'i18n_text': __('Retract'),
'handler': ev => this.onMessageRetractButtonClicked(ev),
'button_class': 'chat-msg__action-retract',
'icon_class': 'fas fa-trash-alt',
'name': 'retract'
});
}
const items = buttons.map(b => MessageActions.getActionsDropdownItem(b));
if (items.length) {
return html`<converse-dropdown class="chat-msg__actions" .items=${ items }></converse-dropdown>`;
} else {
return '';
}
}
}
customElements.define('converse-message-actions', MessageActions);

View File

@ -1,4 +1,6 @@
import "./message-body.js";
import './dropdown.js';
import './message-actions.js';
import MessageVersionsModal from '../modals/message-versions.js';
import dayjs from 'dayjs';
import filesize from "filesize";
@ -8,12 +10,10 @@ import { __ } from '@converse/headless/i18n';
import { _converse, api, converse } from "@converse/headless/converse-core";
import { html } from 'lit-element';
import { renderAvatar } from './../templates/directives/avatar';
import { renderRetractionLink } from './../templates/directives/retraction';
const { Strophe } = converse.env;
const u = converse.env.utils;
const i18n_edit_message = __('Edit this message');
const i18n_edited = __('This message has been edited');
const i18n_show = __('Show more');
const i18n_show_less = __('Show less');
@ -150,17 +150,13 @@ class Message extends CustomElement {
</div>
${ (this.received && !this.is_me_message && !is_groupchat_message) ? html`<span class="fa fa-check chat-msg__receipt"></span>` : '' }
${ (this.edited) ? html`<i title="${ i18n_edited }" class="fa fa-edit chat-msg__edit-modal" @click=${this.showMessageVersionsModal}></i>` : '' }
<div class="chat-msg__actions">
${ this.editable ?
html`<button
class="chat-msg__action chat-msg__action-edit"
title="${i18n_edit_message}"
@click=${this.onMessageEditButtonClicked}
>
<fa-icon class="fas fa-pencil-alt" path-prefix="dist" color="var(--text-color-lighten-15-percent)" size="1em"></fa-icon>
</button>` : '' }
${ renderRetractionLink(this) }
</div>
<converse-message-actions
.chatview=${this.chatview}
.model=${this.model}
?correcting="${this.correcting}"
?editable="${this.editable}"
?is_retracted="${this.is_retracted}"
message_type="${this.message_type}"></converse-message-actions>
</div>
</div>
</div>`;
@ -189,16 +185,6 @@ class Message extends CustomElement {
this.parentElement.removeChild(this);
}
onMessageRetractButtonClicked (ev) {
ev.preventDefault();
this.chatview.onMessageRetractButtonClicked(this.model);
}
onMessageEditButtonClicked (ev) {
ev.preventDefault();
this.chatview.onMessageEditButtonClicked(this.model);
}
isFollowup () {
const messages = this.model.collection.models;
const idx = messages.indexOf(this.model);