You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
315 lines
10 KiB
JavaScript
315 lines
10 KiB
JavaScript
'use strict';
|
|
|
|
|
|
define('forum/chats/messages', [
|
|
'components', 'hooks', 'bootbox', 'alerts',
|
|
'messages', 'api', 'forum/topic/images',
|
|
], function (
|
|
components, hooks, bootbox, alerts, messagesModule, api, images
|
|
) {
|
|
const messages = {};
|
|
|
|
messages.sendMessage = async function (roomId, inputEl) {
|
|
let message = inputEl.val();
|
|
if (!message.trim().length) {
|
|
return;
|
|
}
|
|
const chatContent = inputEl.parents(`[component="chat/messages"][data-roomid="${roomId}"]`);
|
|
inputEl.val('').trigger('input');
|
|
|
|
messages.updateRemainingLength(inputEl.parent());
|
|
messages.updateTextAreaHeight(chatContent);
|
|
const payload = { roomId, message };
|
|
({ roomId, message } = await hooks.fire('filter:chat.send', payload));
|
|
const replyToEl = inputEl.parents('[component="chat/composer"]')
|
|
.find('[component="chat/composer/replying-to"]');
|
|
const toMid = replyToEl.attr('data-tomid');
|
|
api.post(`/chats/${roomId}`, { message, toMid: toMid }).then(() => {
|
|
hooks.fire('action:chat.sent', { roomId, message });
|
|
replyToEl.addClass('hidden');
|
|
replyToEl.attr('data-tomid', '');
|
|
}).catch((err) => {
|
|
inputEl.val(message).trigger('input');
|
|
messages.updateRemainingLength(inputEl.parent());
|
|
messages.updateTextAreaHeight(chatContent);
|
|
if (err.message === '[[error:email-not-confirmed-chat]]') {
|
|
return messagesModule.showEmailConfirmWarning(err.message);
|
|
}
|
|
|
|
alerts.alert({
|
|
alert_id: 'chat_spam_error',
|
|
title: '[[global:alert.error]]',
|
|
message: err.message,
|
|
type: 'danger',
|
|
timeout: 10000,
|
|
});
|
|
});
|
|
};
|
|
|
|
messages.updateRemainingLength = function (parent) {
|
|
const element = parent.find('[component="chat/input"]');
|
|
parent.find('[component="chat/message/length"]').text(element.val().length);
|
|
parent.find('[component="chat/message/remaining"]').text(config.maximumChatMessageLength - element.val().length);
|
|
hooks.fire('action:chat.updateRemainingLength', {
|
|
parent: parent,
|
|
});
|
|
};
|
|
|
|
messages.updateTextAreaHeight = function (chatContentEl) {
|
|
const textarea = chatContentEl.find('[component="chat/input"]');
|
|
textarea.css({ height: messages.calcAutoTextAreaHeight(textarea) + 'px' });
|
|
};
|
|
|
|
messages.calcAutoTextAreaHeight = function (textarea) {
|
|
const scrollHeight = textarea.prop('scrollHeight');
|
|
const borderTopWidth = parseFloat(textarea.css('border-top-width'), 10) || 0;
|
|
const borderBottomWidth = parseFloat(textarea.css('border-bottom-width'), 10) || 0;
|
|
return scrollHeight + borderTopWidth + borderBottomWidth;
|
|
};
|
|
|
|
function autoresizeTextArea(textarea) {
|
|
textarea.css({ height: messages.calcAutoTextAreaHeight(textarea) + 'px' });
|
|
textarea.on('input', function () {
|
|
textarea.css({ height: 0 });
|
|
textarea.css({ height: messages.calcAutoTextAreaHeight(textarea) + 'px' });
|
|
});
|
|
}
|
|
|
|
messages.appendChatMessage = function (chatContentEl, data) {
|
|
const lastMsgEl = chatContentEl.find('.chat-message').last();
|
|
const lastSpeaker = parseInt(lastMsgEl.attr('data-uid'), 10);
|
|
const lasttimestamp = parseInt(lastMsgEl.attr('data-timestamp'), 10);
|
|
if (!Array.isArray(data)) {
|
|
data.newSet = data.toMid || lastSpeaker !== parseInt(data.fromuid, 10) ||
|
|
parseInt(data.timestamp, 10) > parseInt(lasttimestamp, 10) + (1000 * 60 * 3);
|
|
}
|
|
|
|
messages.parseMessage(data, function (html) {
|
|
onMessagesParsed(chatContentEl, html);
|
|
});
|
|
};
|
|
|
|
function onMessagesParsed(chatContentEl, html) {
|
|
const newMessage = $(html);
|
|
const isAtBottom = messages.isAtBottom(chatContentEl);
|
|
newMessage.appendTo(chatContentEl);
|
|
messages.onMessagesAddedToDom(newMessage);
|
|
if (isAtBottom) {
|
|
messages.scrollToBottom(chatContentEl);
|
|
// remove some message elements if there are too many
|
|
const chatMsgEls = chatContentEl.find('[data-mid]');
|
|
if (chatMsgEls.length > 150) {
|
|
const removeCount = chatMsgEls.length - 150;
|
|
chatMsgEls.slice(0, removeCount).remove();
|
|
}
|
|
}
|
|
|
|
hooks.fire('action:chat.received', {
|
|
messageEl: newMessage,
|
|
});
|
|
}
|
|
|
|
messages.onMessagesAddedToDom = function (messageEls) {
|
|
messageEls.find('.timeago').timeago();
|
|
messageEls.find('img:not(.not-responsive)').addClass('img-fluid');
|
|
messageEls.find('img:not(.emoji)').each(function () {
|
|
images.wrapImageInLink($(this));
|
|
});
|
|
};
|
|
|
|
messages.parseMessage = function (data, callback) {
|
|
const tplData = {
|
|
messages: data,
|
|
isAdminOrGlobalMod: app.user.isAdmin || app.user.isGlobalMod,
|
|
};
|
|
if (Array.isArray(data)) {
|
|
app.parseAndTranslate('partials/chats/messages', tplData).then(callback);
|
|
} else {
|
|
app.parseAndTranslate('partials/chats/' + (data.system ? 'system-message' : 'message'), tplData).then(callback);
|
|
}
|
|
};
|
|
|
|
messages.isAtBottom = function (containerEl, threshold) {
|
|
if (containerEl.length) {
|
|
const distanceToBottom = containerEl[0].scrollHeight - (
|
|
containerEl.outerHeight() + containerEl.scrollTop()
|
|
);
|
|
return distanceToBottom < (threshold || 100);
|
|
}
|
|
};
|
|
|
|
messages.scrollToBottom = function (containerEl) {
|
|
if (containerEl && containerEl.length) {
|
|
containerEl.scrollTop(containerEl[0].scrollHeight - containerEl.height());
|
|
containerEl.parent()
|
|
.find('[component="chat/messages/scroll-up-alert"]')
|
|
.addClass('hidden');
|
|
}
|
|
};
|
|
|
|
messages.wrapImagesInLinks = function (containerEl) {
|
|
containerEl.find('[component="chat/message/body"] img:not(.emoji)').each(function () {
|
|
images.wrapImageInLink($(this));
|
|
});
|
|
};
|
|
|
|
messages.toggleScrollUpAlert = function (containerEl) {
|
|
const isAtBottom = messages.isAtBottom(containerEl, 300);
|
|
containerEl.parent()
|
|
.find('[component="chat/messages/scroll-up-alert"]')
|
|
.toggleClass('hidden', isAtBottom);
|
|
};
|
|
|
|
messages.prepReplyTo = async function (msgEl, roomId) {
|
|
const chatMessages = msgEl.parents(`[component="chat/messages"][data-roomid="${roomId}"]`);
|
|
const chatContent = chatMessages.find('[component="chat/message/content"]');
|
|
const composerEl = chatMessages.find('[component="chat/composer"]');
|
|
const mid = msgEl.attr('data-mid');
|
|
const replyToEl = composerEl.find('[component="chat/composer/replying-to"]');
|
|
replyToEl.attr('data-tomid', mid)
|
|
.find('[component="chat/composer/replying-to-text"]')
|
|
.translateText(`[[modules:chat.replying-to, ${msgEl.attr('data-username')}]]`);
|
|
replyToEl.removeClass('hidden');
|
|
replyToEl.find('[component="chat/composer/replying-to-cancel"]').off('click')
|
|
.on('click', () => {
|
|
replyToEl.attr('data-tomid', '');
|
|
replyToEl.addClass('hidden');
|
|
});
|
|
|
|
if (chatContent.length && messages.isAtBottom(chatContent)) {
|
|
messages.scrollToBottom(chatContent);
|
|
}
|
|
composerEl.find('[component="chat/input"]').trigger('focus');
|
|
};
|
|
|
|
messages.prepEdit = async function (msgEl, mid, roomId) {
|
|
const raw = await socket.emit('modules.chats.getRaw', { mid: mid, roomId: roomId });
|
|
const editEl = await app.parseAndTranslate('partials/chats/edit-message', {
|
|
rawContent: raw,
|
|
});
|
|
const messageBody = msgEl.find(`[component="chat/message/body"]`);
|
|
const messageControls = msgEl.find(`[component="chat/message/controls"]`);
|
|
const chatContent = messageBody.parents('[component="chat/message/content"]');
|
|
|
|
messageBody.addClass('hidden');
|
|
messageControls.addClass('hidden');
|
|
editEl.insertAfter(messageBody);
|
|
|
|
const textarea = editEl.find('textarea');
|
|
|
|
textarea.focus().putCursorAtEnd();
|
|
autoresizeTextArea(textarea);
|
|
|
|
if (chatContent.length && messages.isAtBottom(chatContent)) {
|
|
messages.scrollToBottom(chatContent);
|
|
}
|
|
|
|
const chats = await app.require('forum/chats');
|
|
const autoCompleteEl = chats.createAutoComplete(0, textarea, {
|
|
placement: 'bottom',
|
|
});
|
|
|
|
function finishEdit() {
|
|
messageBody.removeClass('hidden');
|
|
messageControls.removeClass('hidden');
|
|
editEl.remove();
|
|
if (autoCompleteEl) {
|
|
autoCompleteEl.destroy();
|
|
}
|
|
}
|
|
textarea.on('keyup', (e) => {
|
|
if (e.key === 'Escape') {
|
|
finishEdit();
|
|
}
|
|
});
|
|
editEl.find('[data-action="cancel"]').on('click', finishEdit);
|
|
|
|
editEl.find('[data-action="save"]').on('click', function () {
|
|
const message = textarea.val();
|
|
if (!message.trim().length) {
|
|
return;
|
|
}
|
|
api.put(`/chats/${roomId}/messages/${mid}`, { message }).then(() => {
|
|
finishEdit();
|
|
hooks.fire('action:chat.edited', { roomId, message, mid });
|
|
}).catch((err) => {
|
|
textarea.val(message).trigger('input');
|
|
alerts.error(err);
|
|
});
|
|
});
|
|
|
|
hooks.fire('action:chat.prepEdit', {
|
|
msgEl: msgEl,
|
|
messageId: mid,
|
|
roomId: roomId,
|
|
editEl: editEl,
|
|
messageBody: messageBody,
|
|
});
|
|
};
|
|
|
|
messages.addSocketListeners = function () {
|
|
socket.removeListener('event:chats.edit', onChatMessageEdited);
|
|
socket.on('event:chats.edit', onChatMessageEdited);
|
|
|
|
socket.removeListener('event:chats.delete', onChatMessageDeleted);
|
|
socket.on('event:chats.delete', onChatMessageDeleted);
|
|
|
|
socket.removeListener('event:chats.restore', onChatMessageRestored);
|
|
socket.on('event:chats.restore', onChatMessageRestored);
|
|
};
|
|
|
|
function onChatMessageEdited(data) {
|
|
data.messages.forEach(function (message) {
|
|
const self = parseInt(message.fromuid, 10) === parseInt(app.user.uid, 10);
|
|
message.self = self ? 1 : 0;
|
|
messages.parseMessage(message, function (html) {
|
|
const msgEl = components.get('chat/message', message.mid);
|
|
if (msgEl.length) {
|
|
msgEl.replaceWith(html);
|
|
messages.onMessagesAddedToDom(components.get('chat/message', message.mid));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function onChatMessageDeleted(messageId) {
|
|
const msgEl = components.get('chat/message', messageId);
|
|
const isSelf = parseInt(msgEl.attr('data-uid'), 10) === app.user.uid;
|
|
msgEl.toggleClass('deleted', true);
|
|
if (!isSelf) {
|
|
msgEl.find('[component="chat/message/body"]')
|
|
.translateHtml('<p>[[modules:chat.message-deleted]]</p>');
|
|
}
|
|
}
|
|
|
|
function onChatMessageRestored(message) {
|
|
const msgEl = components.get('chat/message', message.messageId);
|
|
const isSelf = parseInt(msgEl.attr('data-uid'), 10) === app.user.uid;
|
|
msgEl.toggleClass('deleted', false);
|
|
if (!isSelf) {
|
|
msgEl.find('[component="chat/message/body"]')
|
|
.translateHtml(message.content);
|
|
}
|
|
}
|
|
|
|
messages.delete = function (messageId, roomId) {
|
|
bootbox.confirm('[[modules:chat.delete_message_confirm]]', function (ok) {
|
|
if (!ok) {
|
|
return;
|
|
}
|
|
|
|
api.del(`/chats/${roomId}/messages/${messageId}`, {}).then(() => {
|
|
components.get('chat/message', messageId).toggleClass('deleted', true);
|
|
}).catch(alerts.error);
|
|
});
|
|
};
|
|
|
|
messages.restore = function (messageId, roomId) {
|
|
api.post(`/chats/${roomId}/messages/${messageId}`, {}).then(() => {
|
|
components.get('chat/message', messageId).toggleClass('deleted', false);
|
|
}).catch(alerts.error);
|
|
};
|
|
|
|
return messages;
|
|
});
|