Don't pass chatview object to converse-chat-content

and any child components.

This makes it easier to use these components independently of one
another and the overarching view.
This commit is contained in:
JC Brand 2021-02-09 11:14:06 +01:00
parent f81292e955
commit 95c14e5a26
15 changed files with 269 additions and 256 deletions

View File

@ -146,6 +146,9 @@ describe("A Chat Message", function () {
keyCode: 13 // Enter
});
await u.waitUntil(() => textarea.value === '');
await u.waitUntil(() => Array.from(view.querySelectorAll('.chat-msg__text')).filter(
m => m.textContent === 'It is the east, and Juliet is the sun.').length);
const messages = view.querySelectorAll('.chat-msg');
expect(messages.length).toBe(3);
expect(messages[0].querySelector('.chat-msg__text').textContent)
@ -244,9 +247,9 @@ describe("A Chat Message", function () {
action = view.querySelector('.chat-msg .chat-msg__action');
action.style.opacity = 1;
action.click();
expect(textarea.value).toBe('');
expect(view.model.messages.at(0).get('correcting')).toBe(false);
expect(view.querySelectorAll('.chat-msg').length).toBe(1);
expect(textarea.value).toBe('');
await u.waitUntil(() => (u.hasClass('correcting', view.querySelector('.chat-msg')) === false), 500);
// Test that messages from other users don't have the pencil icon

View File

@ -1,41 +1,56 @@
import "./message-history";
import xss from "xss/dist/xss";
import { CustomElement } from './element.js';
import { _converse, api } from "@converse/headless/core";
import { html } from 'lit-element';
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
import { api } from "@converse/headless/core";
export default class ChatContent extends CustomElement {
static get properties () {
return {
chatview: { type: Object}
jid: { type: String }
}
}
connectedCallback () {
super.connectedCallback();
const model = this.chatview.model;
this.listenTo(model.messages, 'add', this.requestUpdate);
this.listenTo(model.messages, 'change', this.requestUpdate);
this.listenTo(model.messages, 'remove', this.requestUpdate);
this.listenTo(model.messages, 'reset', this.requestUpdate);
this.listenTo(model.notifications, 'change', this.requestUpdate);
if (model.occupants) {
this.listenTo(model.occupants, 'change', this.requestUpdate);
this.model = _converse.chatboxes.get(this.jid);
this.listenTo(this.model.messages, 'add', this.requestUpdate);
this.listenTo(this.model.messages, 'change', this.requestUpdate);
this.listenTo(this.model.messages, 'remove', this.requestUpdate);
this.listenTo(this.model.messages, 'reset', this.requestUpdate);
this.listenTo(this.model.notifications, 'change', this.requestUpdate);
if (this.model.occupants) {
this.listenTo(this.model.occupants, 'change', this.requestUpdate);
}
// We jot down whether we were scrolled down before rendering, because when an
// image loads, it triggers 'scroll' and the chat will be marked as scrolled,
// which is technically true, but not what we want because the user
// didn't initiate the scrolling.
this.was_scrolled_up = this.model.get('scrolled');
this.addEventListener('imageLoaded', () => {
!this.was_scrolled_up && this.scrollDown();
});
}
render () {
const notifications = xss.filterXSS(this.chatview.getNotifications(), {'whiteList': {}});
return html`
<converse-message-history
.chatview=${this.chatview}
.messages=${[...this.chatview.model.messages.models]}>
.model=${this.model}
.messages=${[...this.model.messages.models]}>
</converse-message-history>
<div class="chat-content__notifications">${unsafeHTML(notifications)}</div>
<div class="chat-content__notifications">${this.model.getNotificationsText()}</div>
`;
}
scrollDown () {
if (this.scrollTo) {
const behavior = this.scrollTop ? 'smooth' : 'auto';
this.scrollTo({ 'top': this.scrollHeight, behavior });
} else {
this.scrollTop = this.scrollHeight;
}
}
}
api.elements.define('converse-chat-content', ChatContent);

View File

@ -1,15 +1,17 @@
import log from '@converse/headless/log';
import { CustomElement } from './element.js';
import { __ } from '../i18n';
import { api } from "@converse/headless/core";
import { _converse, api, converse } from "@converse/headless/core";
import { html } from 'lit-element';
import { until } from 'lit-html/directives/until.js';
const { Strophe, u } = converse.env;
class MessageActions extends CustomElement {
static get properties () {
return {
chatview: { type: Object },
model: { type: Object },
editable: { type: Boolean },
correcting: { type: Boolean },
@ -22,6 +24,16 @@ class MessageActions extends CustomElement {
return html`${ until(this.renderActions(), '') }`;
}
async renderActions () {
const buttons = await this.getActionButtons();
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 '';
}
}
static getActionsDropdownItem (o) {
return html`
<button class="chat-msg__action ${o.button_class}" @click=${o.handler}>
@ -36,12 +48,112 @@ class MessageActions extends CustomElement {
onMessageEditButtonClicked (ev) {
ev.preventDefault();
this.chatview.onMessageEditButtonClicked(this.model);
const currently_correcting = this.model.collection.findWhere('correcting');
// TODO: Use state intead of DOM querying
// Then this code can also be put on the model
const unsent_text = u.ancestor(this, '.chatbox')?.querySelector('.chat-textarea')?.value;
if (unsent_text && (!currently_correcting || currently_correcting.get('message') !== unsent_text)) {
if (!confirm(__('You have an unsent message which will be lost if you continue. Are you sure?'))) {
return;
}
}
if (currently_correcting !== this.model) {
currently_correcting?.save('correcting', false);
this.model.save('correcting', true);
} else {
this.model.save('correcting', false);
}
}
async onDirectMessageRetractButtonClicked () {
if (this.model.get('sender') !== 'me') {
return log.error("onMessageRetractButtonClicked called for someone else's this.model!");
}
const retraction_warning = __(
'Be aware that other XMPP/Jabber clients (and servers) may ' +
'not yet support retractions and that this this.model may not ' +
'be removed everywhere.'
);
const messages = [__('Are you sure you want to retract this this.model?')];
if (api.settings.get('show_retraction_warning')) {
messages[1] = retraction_warning;
}
const result = await api.confirm(__('Confirm'), messages);
if (result) {
const chatbox = this.model.collection.chatbox;
chatbox.retractOwnMessage(this.model);
}
}
/**
* Retract someone else's message in this groupchat.
* @private
* @param { _converse.Message } message - The message which we're retracting.
* @param { string } [reason] - The reason for retracting the message.
*/
async retractOtherMessage (reason) {
const chatbox = this.model.collection.chatbox;
const result = await chatbox.retractOtherMessage(this.model, reason);
if (result === null) {
const err_msg = __(`A timeout occurred while trying to retract the message`);
api.alert('error', __('Error'), err_msg);
log(err_msg, Strophe.LogLevel.WARN);
} else if (u.isErrorStanza(result)) {
const err_msg = __(`Sorry, you're not allowed to retract this message.`);
api.alert('error', __('Error'), err_msg);
log(err_msg, Strophe.LogLevel.WARN);
log(result, Strophe.LogLevel.WARN);
}
}
async onMUCMessageRetractButtonClicked () {
const retraction_warning = __(
'Be aware that other XMPP/Jabber clients (and servers) may ' +
'not yet support retractions and that this this.model may not ' +
'be removed everywhere.'
);
if (this.model.mayBeRetracted()) {
const messages = [__('Are you sure you want to retract this this.model?')];
if (api.settings.get('show_retraction_warning')) {
messages[1] = retraction_warning;
}
if (await api.confirm(__('Confirm'), messages)) {
const chatbox = this.model.collection.chatbox;
chatbox.retractOwnMessage(this.model);
}
} else if (await this.model.mayBeModerated()) {
if (this.model.get('sender') === 'me') {
let messages = [__('Are you sure you want to retract this this.model?')];
if (api.settings.get('show_retraction_warning')) {
messages = [messages[0], retraction_warning, messages[1]];
}
!!(await api.confirm(__('Confirm'), messages)) && this.retractOtherMessage();
} else {
let messages = [
__('You are about to retract this this.model.'),
__('You may optionally include a this.model, explaining the reason for the retraction.')
];
if (api.settings.get('show_retraction_warning')) {
messages = [messages[0], retraction_warning, messages[1]];
}
const reason = await api.prompt(__('this.model Retraction'), messages, __('Optional reason'));
reason !== false && this.retractOtherMessage(reason);
}
} else {
const err_msg = __(`Sorry, you're not allowed to retract this this.model`);
api.alert('error', __('Error'), err_msg);
}
}
onMessageRetractButtonClicked (ev) {
ev.preventDefault();
this.chatview.onMessageRetractButtonClicked(this.model);
const chatbox = this.model.collection.chatbox;
if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
this.onMUCMessageRetractButtonClicked();
} else {
this.onDirectMessageRetractButtonClicked();
}
}
async getActionButtons () {
@ -83,16 +195,6 @@ class MessageActions extends CustomElement {
*/
return api.hook('getMessageActionButtons', this, buttons);
}
async renderActions () {
const buttons = await this.getActionButtons();
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 '';
}
}
}
api.elements.define('converse-message-actions', MessageActions);

View File

@ -11,7 +11,6 @@ const i18n_no_history = __('No message history available.');
const tpl_message = (o) => html`
<converse-chat-message
.chatview=${o.chatview}
.hats=${o.hats}
.model=${o.model}
?correcting=${o.correcting}
@ -108,7 +107,7 @@ export default class MessageHistory extends CustomElement {
static get properties () {
return {
chatview: { type: Object},
model: { type: Object},
messages: { type: Array}
}
}
@ -129,8 +128,8 @@ export default class MessageHistory extends CustomElement {
const message = tpl_message(
Object.assign(
model.toJSON(),
getDerivedMessageProps(this.chatview.model, model),
{ 'chatview': this.chatview, model }
getDerivedMessageProps(this.model, model),
{ model }
)
);
return [...templates, message];

View File

@ -2,8 +2,9 @@ import '../shared/registry';
import './dropdown.js';
import './message-actions.js';
import './message-body.js';
import { getDerivedMessageProps } from './message-history';
import MessageVersionsModal from '../modals/message-versions.js';
import OccupantModal from 'modals/occupant.js';
import UserDetailsModal from 'modals/user-details.js';
import dayjs from 'dayjs';
import filesize from 'filesize';
import tpl_chat_message from '../templates/chat_message.js';
@ -11,6 +12,7 @@ import tpl_spinner from '../templates/spinner.js';
import { CustomElement } from './element.js';
import { __ } from '../i18n';
import { _converse, api, converse } from '@converse/headless/core';
import { getDerivedMessageProps } from './message-history';
import { html } from 'lit-element';
import { renderAvatar } from 'templates/directives/avatar';
@ -22,7 +24,6 @@ export default class Message extends CustomElement {
static get properties () {
return {
chatview: { type: Object},
correcting: { type: Boolean },
editable: { type: Boolean },
edited: { type: String },
@ -259,9 +260,12 @@ export default class Message extends CustomElement {
if (this.model.get('sender') === 'me') {
_converse.xmppstatusview.showProfileModal(ev);
} else if (this.message_type === 'groupchat') {
this.chatview.showOccupantDetailsModal(ev, this.model);
ev.preventDefault();
api.modal.show(OccupantModal, { 'model': this.model.occupant }, ev);
} else {
this.chatview.showUserDetailsModal(ev, this.model);
ev.preventDefault();
const chatbox = this.model.collection.chatbox;
api.modal.show(UserDetailsModal, { model: chatbox }, ev);
}
}

View File

@ -103,6 +103,19 @@ const ChatBox = ModelWithContact.extend({
this.notifications = new Model();
},
getNotificationsText () {
const { __ } = _converse;
if (this.notifications?.get('chat_state') === _converse.COMPOSING) {
return __('%1$s is typing', this.getDisplayName());
} else if (this.notifications?.get('chat_state') === _converse.PAUSED) {
return __('%1$s has stopped typing', this.getDisplayName());
} else if (this.notifications?.get('chat_state') === _converse.GONE) {
return __('%1$s has gone away', this.getDisplayName());
} else {
return '';
}
},
afterMessagesFetched (messages) {
this.most_recent_cached_message = messages ? this.getMostRecentMessage(messages) : null;
/**

View File

@ -1855,6 +1855,81 @@ const ChatRoomMixin = {
return false;
},
getNotificationsText () {
const { __ } = _converse;
const actors_per_state = this.notifications.toJSON();
const role_changes = api.settings
.get('muc_show_info_messages')
.filter(role_change => converse.MUC_ROLE_CHANGES_LIST.includes(role_change));
const join_leave_events = api.settings
.get('muc_show_info_messages')
.filter(join_leave_event => converse.MUC_TRAFFIC_STATES_LIST.includes(join_leave_event));
const states = [...converse.CHAT_STATES, ...join_leave_events, ...role_changes];
return states.reduce((result, state) => {
const existing_actors = actors_per_state[state];
if (!existing_actors?.length) {
return result;
}
const actors = existing_actors.map(a => this.getOccupant(a)?.getDisplayName() || a);
if (actors.length === 1) {
if (state === 'composing') {
return `${result}${__('%1$s is typing', actors[0])}\n`;
} else if (state === 'paused') {
return `${result}${__('%1$s has stopped typing', actors[0])}\n`;
} else if (state === _converse.GONE) {
return `${result}${__('%1$s has gone away', actors[0])}\n`;
} else if (state === 'entered') {
return `${result}${__('%1$s has entered the groupchat', actors[0])}\n`;
} else if (state === 'exited') {
return `${result}${__('%1$s has left the groupchat', actors[0])}\n`;
} else if (state === 'op') {
return `${result}${__('%1$s is now a moderator', actors[0])}\n`;
} else if (state === 'deop') {
return `${result}${__('%1$s is no longer a moderator', actors[0])}\n`;
} else if (state === 'voice') {
return `${result}${__('%1$s has been given a voice', actors[0])}\n`;
} else if (state === 'mute') {
return `${result}${__('%1$s has been muted', actors[0])}\n`;
}
} else if (actors.length > 1) {
let actors_str;
if (actors.length > 3) {
actors_str = `${Array.from(actors)
.slice(0, 2)
.join(', ')} and others`;
} else {
const last_actor = actors.pop();
actors_str = __('%1$s and %2$s', actors.join(', '), last_actor);
}
if (state === 'composing') {
return `${result}${__('%1$s are typing', actors_str)}\n`;
} else if (state === 'paused') {
return `${result}${__('%1$s have stopped typing', actors_str)}\n`;
} else if (state === _converse.GONE) {
return `${result}${__('%1$s have gone away', actors_str)}\n`;
} else if (state === 'entered') {
return `${result}${__('%1$s have entered the groupchat', actors_str)}\n`;
} else if (state === 'exited') {
return `${result}${__('%1$s have left the groupchat', actors_str)}\n`;
} else if (state === 'op') {
return `${result}${__('%1$s are now moderators', actors[0])}\n`;
} else if (state === 'deop') {
return `${result}${__('%1$s are no longer moderators', actors[0])}\n`;
} else if (state === 'voice') {
return `${result}${__('%1$s have been given voices', actors[0])}\n`;
} else if (state === 'mute') {
return `${result}${__('%1$s have been muted', actors[0])}\n`;
}
}
return result;
}, '');
},
/**
* @param {String} actor - The nickname of the actor that caused the notification
* @param {String|Array<String>} states - The state or states representing the type of notificcation

View File

@ -1,6 +1,5 @@
import BaseChatView from 'shared/chatview.js';
import UserDetailsModal from 'modals/user-details.js';
import log from '@converse/headless/log';
import tpl_chatbox from 'templates/chatbox.js';
import tpl_chatbox_head from 'templates/chatbox_head.js';
import { __ } from 'i18n';
@ -45,6 +44,7 @@ export default class ChatView extends BaseChatView {
this.listenTo(this.model, 'change:hidden', () => !this.model.get('hidden') && this.afterShown());
this.listenTo(this.model, 'change:status', this.onStatusMessageChanged);
this.listenTo(this.model, 'vcard:change', this.renderHeading);
this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting);
if (this.model.contact) {
this.listenTo(this.model.contact, 'destroy', this.renderHeading);
@ -77,10 +77,7 @@ export default class ChatView extends BaseChatView {
render () {
const result = tpl_chatbox(Object.assign(
this.model.toJSON(), {
'markScrolled': ev => this.markScrolled(ev),
'chatview': this
})
this.model.toJSON(), { 'markScrolled': ev => this.markScrolled(ev) })
);
render(result, this);
this.content = this.querySelector('.chat-content');
@ -90,18 +87,6 @@ export default class ChatView extends BaseChatView {
return this;
}
getNotifications () {
if (this.model.notifications.get('chat_state') === _converse.COMPOSING) {
return __('%1$s is typing', this.model.getDisplayName());
} else if (this.model.notifications.get('chat_state') === _converse.PAUSED) {
return __('%1$s has stopped typing', this.model.getDisplayName());
} else if (this.model.notifications.get('chat_state') === _converse.GONE) {
return __('%1$s has gone away', this.model.getDisplayName());
} else {
return '';
}
}
getHelpMessages () { // eslint-disable-line class-methods-use-this
return [
`<strong>/clear</strong>: ${__('Remove messages')}`,
@ -385,26 +370,6 @@ export default class ChatView extends BaseChatView {
this.insertIntoTextArea('', true, false);
}
async onMessageRetractButtonClicked (message) {
if (message.get('sender') !== 'me') {
return log.error("onMessageRetractButtonClicked called for someone else's message!");
}
const retraction_warning = __(
'Be aware that other XMPP/Jabber clients (and servers) may ' +
'not yet support retractions and that this message may not ' +
'be removed everywhere.'
);
const messages = [__('Are you sure you want to retract this message?')];
if (api.settings.get('show_retraction_warning')) {
messages[1] = retraction_warning;
}
const result = await api.confirm(__('Confirm'), messages);
if (result) {
this.model.retractOwnMessage(message);
}
}
inputChanged (ev) { // eslint-disable-line class-methods-use-this
const height = ev.target.scrollHeight + 'px';
if (ev.target.style.height != height) {

View File

@ -51,7 +51,6 @@ class HeadlinesView extends BaseChatView {
this.setAttribute('id', this.model.get('box_id'));
const result = tpl_chatbox(
Object.assign(this.model.toJSON(), {
chatview: this,
show_send_button: false,
show_toolbar: false,
})

View File

@ -4,7 +4,6 @@ import 'shared/autocomplete/index.js';
import BaseChatView from 'shared/chatview.js';
import MUCInviteModal from 'modals/muc-invite.js';
import ModeratorToolsModal from 'modals/moderator-tools.js';
import OccupantModal from 'modals/occupant.js';
import RoomDetailsModal from 'modals/muc-details.js';
import log from '@converse/headless/log';
import tpl_chatroom from 'templates/chatroom.js';
@ -97,6 +96,7 @@ export default class MUCView extends BaseChatView {
this.listenTo(this.model.features, 'change:moderated', this.renderBottomPanel);
this.listenTo(this.model.features, 'change:open', this.renderHeading);
this.listenTo(this.model.messages, 'rendered', this.maybeScrollDown);
this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting);
this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged);
// Bind so that we can pass it to addEventListener and removeEventListener
@ -137,7 +137,6 @@ export default class MUCView extends BaseChatView {
render(
tpl_chatroom({
sidebar_hidden,
'chatview': this,
'model': this.model,
'occupants': this.model.occupants,
'show_sidebar':
@ -168,80 +167,6 @@ export default class MUCView extends BaseChatView {
!this.model.get('hidden') && this.show();
}
getNotifications () {
const actors_per_state = this.model.notifications.toJSON();
const role_changes = api.settings
.get('muc_show_info_messages')
.filter(role_change => converse.MUC_ROLE_CHANGES_LIST.includes(role_change));
const join_leave_events = api.settings
.get('muc_show_info_messages')
.filter(join_leave_event => converse.MUC_TRAFFIC_STATES_LIST.includes(join_leave_event));
const states = [...converse.CHAT_STATES, ...join_leave_events, ...role_changes];
return states.reduce((result, state) => {
const existing_actors = actors_per_state[state];
if (!existing_actors?.length) {
return result;
}
const actors = existing_actors.map(a => this.model.getOccupant(a)?.getDisplayName() || a);
if (actors.length === 1) {
if (state === 'composing') {
return `${result}${__('%1$s is typing', actors[0])}\n`;
} else if (state === 'paused') {
return `${result}${__('%1$s has stopped typing', actors[0])}\n`;
} else if (state === _converse.GONE) {
return `${result}${__('%1$s has gone away', actors[0])}\n`;
} else if (state === 'entered') {
return `${result}${__('%1$s has entered the groupchat', actors[0])}\n`;
} else if (state === 'exited') {
return `${result}${__('%1$s has left the groupchat', actors[0])}\n`;
} else if (state === 'op') {
return `${result}${__('%1$s is now a moderator', actors[0])}\n`;
} else if (state === 'deop') {
return `${result}${__('%1$s is no longer a moderator', actors[0])}\n`;
} else if (state === 'voice') {
return `${result}${__('%1$s has been given a voice', actors[0])}\n`;
} else if (state === 'mute') {
return `${result}${__('%1$s has been muted', actors[0])}\n`;
}
} else if (actors.length > 1) {
let actors_str;
if (actors.length > 3) {
actors_str = `${Array.from(actors)
.slice(0, 2)
.join(', ')} and others`;
} else {
const last_actor = actors.pop();
actors_str = __('%1$s and %2$s', actors.join(', '), last_actor);
}
if (state === 'composing') {
return `${result}${__('%1$s are typing', actors_str)}\n`;
} else if (state === 'paused') {
return `${result}${__('%1$s have stopped typing', actors_str)}\n`;
} else if (state === _converse.GONE) {
return `${result}${__('%1$s have gone away', actors_str)}\n`;
} else if (state === 'entered') {
return `${result}${__('%1$s have entered the groupchat', actors_str)}\n`;
} else if (state === 'exited') {
return `${result}${__('%1$s have left the groupchat', actors_str)}\n`;
} else if (state === 'op') {
return `${result}${__('%1$s are now moderators', actors[0])}\n`;
} else if (state === 'deop') {
return `${result}${__('%1$s are no longer moderators', actors[0])}\n`;
} else if (state === 'voice') {
return `${result}${__('%1$s have been given voices', actors[0])}\n`;
} else if (state === 'mute') {
return `${result}${__('%1$s have been muted', actors[0])}\n`;
}
}
return result;
}, '');
}
getHelpMessages () {
const setting = api.settings.get('muc_disable_slash_commands');
const disabled_commands = Array.isArray(setting) ? setting : [];
@ -445,64 +370,6 @@ export default class MUCView extends BaseChatView {
return _converse.ChatBoxView.prototype.onKeyUp.call(this, ev);
}
async onMessageRetractButtonClicked (message) {
const retraction_warning = __(
'Be aware that other XMPP/Jabber clients (and servers) may ' +
'not yet support retractions and that this message may not ' +
'be removed everywhere.'
);
if (message.mayBeRetracted()) {
const messages = [__('Are you sure you want to retract this message?')];
if (api.settings.get('show_retraction_warning')) {
messages[1] = retraction_warning;
}
!!(await api.confirm(__('Confirm'), messages)) && this.model.retractOwnMessage(message);
} else if (await message.mayBeModerated()) {
if (message.get('sender') === 'me') {
let messages = [__('Are you sure you want to retract this message?')];
if (api.settings.get('show_retraction_warning')) {
messages = [messages[0], retraction_warning, messages[1]];
}
!!(await api.confirm(__('Confirm'), messages)) && this.retractOtherMessage(message);
} else {
let messages = [
__('You are about to retract this message.'),
__('You may optionally include a message, explaining the reason for the retraction.')
];
if (api.settings.get('show_retraction_warning')) {
messages = [messages[0], retraction_warning, messages[1]];
}
const reason = await api.prompt(__('Message Retraction'), messages, __('Optional reason'));
reason !== false && this.retractOtherMessage(message, reason);
}
} else {
const err_msg = __(`Sorry, you're not allowed to retract this message`);
api.alert('error', __('Error'), err_msg);
}
}
/**
* Retract someone else's message in this groupchat.
* @private
* @method _converse.ChatRoomView#retractOtherMessage
* @param { _converse.Message } message - The message which we're retracting.
* @param { string } [reason] - The reason for retracting the message.
*/
async retractOtherMessage (message, reason) {
const result = await this.model.retractOtherMessage(message, reason);
if (result === null) {
const err_msg = __(`A timeout occurred while trying to retract the message`);
api.alert('error', __('Error'), err_msg);
log(err_msg, Strophe.LogLevel.WARN);
} else if (u.isErrorStanza(result)) {
const err_msg = __(`Sorry, you're not allowed to retract this message.`);
api.alert('error', __('Error'), err_msg);
log(err_msg, Strophe.LogLevel.WARN);
log(result, Strophe.LogLevel.WARN);
}
}
showModeratorToolsModal (affiliation) {
if (!this.verifyRoles(['moderator'])) {
return;
@ -522,11 +389,6 @@ export default class MUCView extends BaseChatView {
api.modal.show(RoomDetailsModal, { 'model': this.model }, ev);
}
showOccupantDetailsModal (ev, message) { // eslint-disable-line class-methods-use-this
ev.preventDefault();
api.modal.show(OccupantModal, { 'model': message.occupant }, ev);
}
showChatStateNotification (message) {
if (message.get('sender') === 'me') {
return;

View File

@ -234,7 +234,6 @@ export default class BaseChatView extends ElementView {
.reverse()
.find(m => m.get('editable'));
if (message) {
this.insertIntoTextArea(u.prefixMentions(message), true, true);
message.save('correcting', true);
}
}
@ -309,21 +308,16 @@ export default class BaseChatView extends ElementView {
this.insertIntoTextArea(emoji, autocompleting, false, ac_position);
}
onMessageEditButtonClicked (message) {
const currently_correcting = this.model.messages.findWhere('correcting');
const unsent_text = this.querySelector('.chat-textarea')?.value;
if (unsent_text && (!currently_correcting || currently_correcting.get('message') !== unsent_text)) {
if (!confirm(__('You have an unsent message which will be lost if you continue. Are you sure?'))) {
return;
}
}
if (currently_correcting !== message) {
currently_correcting?.save('correcting', false);
message.save('correcting', true);
onMessageCorrecting (message) {
if (message.get('correcting')) {
this.insertIntoTextArea(u.prefixMentions(message), true, true);
} else {
message.save('correcting', false);
this.insertIntoTextArea('', true, false);
const currently_correcting = this.model.messages.findWhere('correcting');
if (currently_correcting && currently_correcting !== message) {
this.insertIntoTextArea(u.prefixMentions(message), true, true);
} else {
this.insertIntoTextArea('', true, false);
}
}
}
@ -415,13 +409,7 @@ export default class BaseChatView extends ElementView {
'scrollTop': null
});
}
const msgs_container = this.querySelector('.chat-content__messages');
if (msgs_container.scrollTo) {
const behavior = msgs_container.scrollTop ? 'smooth' : 'auto';
msgs_container.scrollTo({ 'top': msgs_container.scrollHeight, behavior });
} else {
msgs_container.scrollTop = msgs_container.scrollHeight;
}
this.querySelector('.chat-content__messages').scrollDown();
this.onScrolledDown();
}

View File

@ -33,7 +33,6 @@ export default (o) => {
${ o.is_retracted ? o.renderRetraction() : o.renderMessageText() }
</div>
<converse-message-actions
.chatview=${o.chatview}
.model=${o.model}
?correcting="${o.correcting}"
?editable="${o.editable}"

View File

@ -8,7 +8,7 @@ export default (o) => html`
<div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
<converse-chat-content
class="chat-content__messages"
.chatview=${o.chatview}
jid=${o.jid}
@scroll=${o.markScrolled}></converse-chat-content>
<div class="chat-content__help"></div>

View File

@ -9,7 +9,7 @@ export default (o) => html`
<div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
<converse-chat-content
class="chat-content__messages"
.chatview=${o.chatview}
jid=${o.model.get('jid')}
@scroll=${o.markScrolled}></converse-chat-content>
<div class="chat-content__help"></div>

View File

@ -1,30 +1,19 @@
import { MessageText } from '../../shared/message/text.js';
import { api, converse } from "@converse/headless/core";
import { api } from "@converse/headless/core";
import { directive, html } from "lit-html";
import { until } from 'lit-html/directives/until.js';
const u = converse.env.utils;
class MessageBodyRenderer {
constructor (component) {
this.model = component.model;
this.component = component;
this.chatview = u.ancestor(this.component, 'converse-chat-message')?.chatview;
// We jot down whether we were scrolled down before rendering, because when an
// image loads, it triggers 'scroll' and the chat will be marked as scrolled,
// which is technically true, but not what we want because the user
// didn't initiate the scrolling.
this.was_scrolled_up = this.chatview.model.get('scrolled');
this.text = this.component.model.getMessageText();
this.text = this.model.getMessageText();
}
scrollDownOnImageLoad () {
if (!this.was_scrolled_up) {
this.chatview.scrollDown();
}
onImageLoaded () {
this.component.dispatchEvent(new CustomEvent('imageLoaded', { detail: this.component }));
}
async transform () {
@ -35,7 +24,7 @@ class MessageBodyRenderer {
this.model,
offset,
show_images,
() => this.scrollDownOnImageLoad(),
() => this.onImageLoaded(),
ev => this.component.showImageModal(ev)
);
await text.addTemplates();