Commit c2c1db58 authored by JC Brand's avatar JC Brand

Create a component which renders message actions in a dropdown

parent 6ad76c14
...@@ -90,7 +90,6 @@ ...@@ -90,7 +90,6 @@
display: inline-flex; display: inline-flex;
width: 100%; width: 100%;
flex-direction: row; flex-direction: row;
overflow: auto; // Ensures that content stays inside
padding: 0.125rem 1rem; padding: 0.125rem 1rem;
&.onload { &.onload {
...@@ -98,12 +97,7 @@ ...@@ -98,12 +97,7 @@
-webkit-animation: colorchange-chatmessage 1s; -webkit-animation: colorchange-chatmessage 1s;
} }
&:hover { &:hover {
background-color: rgba(0, 0, 0, 0.035); background-color: var(--list-item-hover-color);
.chat-msg__actions {
.chat-msg__action {
opacity: 1;
}
}
} }
&.correcting { &.correcting {
&.groupchat { &.groupchat {
...@@ -162,9 +156,15 @@ ...@@ -162,9 +156,15 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
&:hover {
.btn--standalone {
opacity: 1;
}
}
} }
.chat-msg__message { .chat-msg__message {
line-height: 1.5em;
display: inline-flex; display: inline-flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
...@@ -235,20 +235,36 @@ ...@@ -235,20 +235,36 @@
} }
} }
converse-message-actions {
margin-left: 0.5em;
}
.chat-msg__actions { .chat-msg__actions {
display: flex; .dropdown-menu {
flex-wrap: nowrap; min-width: 5rem;
.chat-msg__action { }
height: var(--message-font-size); i {
font-size: var(--message-font-size); color: var(--text-color-lighten-15-percent);
padding: 0; font-size: 70%;
margin-left: 0.75em; }
button {
border: none; border: none;
opacity: 0;
background: transparent; background: transparent;
cursor: pointer; color: var(--text-color-lighten-15-percent);
&:focus { padding: 0 0.25em;
opacity: 1; }
.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);
} }
} }
} }
......
...@@ -178,6 +178,7 @@ $mobile_portrait_length: 480px !default; ...@@ -178,6 +178,7 @@ $mobile_portrait_length: 480px !default;
--list-toggle-color: #818479; // $gray-color --list-toggle-color: #818479; // $gray-color
--list-toggle-hover-color: #585B51; // $dark-gray-color --list-toggle-hover-color: #585B51; // $dark-gray-color
--list-toggle-font-weight: normal; --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-action-color: #e3eef3; // lighten($lightest-blue, 25%)
--list-item-link-color: inherit; --list-item-link-color: inherit;
--list-item-link-hover-color: var(--dark-link-color); --list-item-link-hover-color: var(--dark-link-color);
......
...@@ -107,7 +107,7 @@ describe("Chatboxes", function () { ...@@ -107,7 +107,7 @@ describe("Chatboxes", function () {
// get the 'chat-msg--followup' class. // get the 'chat-msg--followup' class.
message = 'This a normal message'; message = 'This a normal message';
await mock.sendMessage(view, 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); 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'); let el = view.el.querySelector('converse-chat-message:last-child .chat-msg__body');
expect(u.hasClass('chat-msg--followup', el)).toBeFalsy(); expect(u.hasClass('chat-msg--followup', el)).toBeFalsy();
......
...@@ -106,7 +106,7 @@ describe("A Chat Message", function () { ...@@ -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?'}); 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); expect(view.el.querySelectorAll('.chat-msg .chat-msg__action').length).toBe(2);
let action = view.el.querySelector('.chat-msg .chat-msg__action'); 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.style.opacity = 1;
action.click(); action.click();
......
...@@ -1215,7 +1215,7 @@ describe("A Groupchat Message", function () { ...@@ -1215,7 +1215,7 @@ describe("A Groupchat Message", function () {
`<origin-id id="${msg.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+ `<origin-id id="${msg.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
`</message>`); `</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.style.opacity = 1;
action.click(); action.click();
......
...@@ -437,7 +437,8 @@ describe("The OMEMO module", function() { ...@@ -437,7 +437,8 @@ describe("The OMEMO module", function() {
_converse.connection._dataRecv(mock.createRequest(carbon)); _converse.connection._dataRecv(mock.createRequest(carbon));
await new Promise(resolve => view.model.messages.once('rendered', resolve)); await new Promise(resolve => view.model.messages.once('rendered', resolve));
expect(view.model.messages.length).toBe(1); 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'); .toBe('This is an encrypted carbon message from another device of mine');
expect(devicelist.devices.length).toBe(2); expect(devicelist.devices.length).toBe(2);
......
...@@ -119,8 +119,7 @@ describe("Message Retractions", function () { ...@@ -119,8 +119,7 @@ describe("Message Retractions", function () {
`); `);
_converse.connection._dataRecv(mock.createRequest(received_stanza)); _converse.connection._dataRecv(mock.createRequest(received_stanza));
await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2); await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2);
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1, 1000);
expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
expect(view.model.messages.length).toBe(1); expect(view.model.messages.length).toBe(1);
const message = view.model.messages.at(0) const message = view.model.messages.at(0)
......
...@@ -52,15 +52,17 @@ export class DropdownList extends BaseDropdown { ...@@ -52,15 +52,17 @@ export class DropdownList extends BaseDropdown {
static get properties () { static get properties () {
return { return {
'icon_classes': { type: String },
'items': { type: Array } 'items': { type: Array }
} }
} }
render () { render () {
const icon_classes = this.icon_classes || "fa fa-bars";
return html` return html`
<div class="dropleft"> <div class="dropleft">
<button type="button" class="btn btn--transparent btn--standalone" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <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> </button>
<div class="dropdown-menu"> <div class="dropdown-menu">
${ this.items.map(b => until(b, '')) } ${ this.items.map(b => until(b, '')) }
......
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);
import "./message-body.js"; import "./message-body.js";
import './dropdown.js';
import './message-actions.js';
import MessageVersionsModal from '../modals/message-versions.js'; import MessageVersionsModal from '../modals/message-versions.js';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import filesize from "filesize"; import filesize from "filesize";
...@@ -8,12 +10,10 @@ import { __ } from '@converse/headless/i18n'; ...@@ -8,12 +10,10 @@ import { __ } from '@converse/headless/i18n';
import { _converse, api, converse } from "@converse/headless/converse-core"; import { _converse, api, converse } from "@converse/headless/converse-core";
import { html } from 'lit-element'; import { html } from 'lit-element';
import { renderAvatar } from './../templates/directives/avatar'; import { renderAvatar } from './../templates/directives/avatar';
import { renderRetractionLink } from './../templates/directives/retraction';
const { Strophe } = converse.env; const { Strophe } = converse.env;
const u = converse.env.utils; const u = converse.env.utils;
const i18n_edit_message = __('Edit this message');
const i18n_edited = __('This message has been edited'); const i18n_edited = __('This message has been edited');
const i18n_show = __('Show more'); const i18n_show = __('Show more');
const i18n_show_less = __('Show less'); const i18n_show_less = __('Show less');
...@@ -150,17 +150,13 @@ class Message extends CustomElement { ...@@ -150,17 +150,13 @@ class Message extends CustomElement {
</div> </div>
${ (this.received && !this.is_me_message && !is_groupchat_message) ? html`<span class="fa fa-check chat-msg__receipt"></span>` : '' } ${ (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>` : '' } ${ (this.edited) ? html`<i title="${ i18n_edited }" class="fa fa-edit chat-msg__edit-modal" @click=${this.showMessageVersionsModal}></i>` : '' }
<div class="chat-msg__actions"> <converse-message-actions
${ this.editable ? .chatview=${this.chatview}
html`<button .model=${this.model}
class="chat-msg__action chat-msg__action-edit" ?correcting="${this.correcting}"
title="${i18n_edit_message}" ?editable="${this.editable}"
@click=${this.onMessageEditButtonClicked} ?is_retracted="${this.is_retracted}"
> message_type="${this.message_type}"></converse-message-actions>
<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>
</div> </div>
</div> </div>
</div>`; </div>`;
...@@ -189,16 +185,6 @@ class Message extends CustomElement { ...@@ -189,16 +185,6 @@ class Message extends CustomElement {
this.parentElement.removeChild(this); this.parentElement.removeChild(this);
} }
onMessageRetractButtonClicked (ev) {
ev.preventDefault();
this.chatview.onMessageRetractButtonClicked(this.model);
}
onMessageEditButtonClicked (ev) {
ev.preventDefault();
this.chatview.onMessageEditButtonClicked(this.model);
}
isFollowup () { isFollowup () {
const messages = this.model.collection.models; const messages = this.model.collection.models;
const idx = messages.indexOf(this.model); const idx = messages.indexOf(this.model);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment