import {bindFileInputEvents, eraseFileInput, fileStructuredClone} from './chat-file-uploader' import './chat-search' import { clearPeriodicUpdateChatPeopleList, initPeriodicUpdateChatPeopleList, isEmptyValue, updateChatPeopleList } from "./common"; export default class Chat { static INCOMING_TEMPLATE = "incoming-template"; constructor(chatId) { this.chatId = chatId; this.filesToSend = ""; this.messageToSend = ""; } init() { this.cacheDOM(); this.bindEvents(); this.renderChatHistory(); } cacheDOM() { this.$chatHeader = $(".chat-header"); this.$button = $("#send-btn-message"); this.$chatHistory = $(".chat-history"); this.$textarea = $("#message-to-send"); this.$chatSelectBtn = $(".chat-select-btn"); this.$chatPeopleList = $(".people-list"); this.$fileUploader = $(".chat-file-uploader"); this.$chatHistoryList = this.$chatHistory.find("ul"); } bindEvents() { this.$button.off(); this.$button.on("click", this.addMessage.bind(this)); this.$textarea.off(); this.$textarea.on("keyup", this.addMessageByEnterBtn.bind(this)); } /** * Функция включения анимации загрузки * * @returns {void} */ startLoading() { let emptyChat = $(".empty-chat-history"); if (emptyChat.length < 1) { this.$chatHistoryList?.empty(); this.$chatHistoryList?.append("
"); emptyChat = $(".empty-chat-history"); } emptyChat.empty(); emptyChat.addClass("loading"); this.renderChatHeader(); } /** * Отрисовка заголовка чата * * @param {object|null} response - ответ сервера в формате JSON * * @returns {void} */ renderChatHeader(response = null) { this.$chatHeader?.empty(); if (response) { this.$chatHeader?.append(response); } } templateCompile(template, data) { const pattern = /{{\s*(\w+?)\s*}}/g; // {{property}} return template.replace(pattern, (_, token) => data[token] || ""); } renderChatHistory(senderType) { if ( (this.messageToSend.trim() !== "" || this.filesToSend.length > 0) && !isEmptyValue(senderType) ) { let messageContext = {}; if (this.messageToSend.trim() !== "") { messageContext = this.renderChatHistoryMessage(senderType); } let fileContexts = []; if (this.filesToSend.length > 0) { fileContexts = this.renderChatHistoryFile(senderType); } this.scrollToBottom(); if (senderType === Chat.INCOMING_TEMPLATE) { eraseFileInput(); this.$textarea.val(""); this.sendMessage(messageContext, fileContexts); } } } renderChatHistoryMessage(senderType) { let template = $(`#${senderType}-message`).html(); let context = { messageOutput: this.messageToSend.trim(), time: this.getCurrentDateTime(), messageUid: this.getRandomUid(), }; this.$chatHistoryList.append(this.templateCompile(template, context)); return context; } renderChatHistoryFile(senderType) { let contexts = []; if (this.filesToSend.length > 0) { let template = $(`#${senderType}-file`).html(); for (let I = 0; I < this.filesToSend.length; I++) { const file = this.filesToSend[I]; const context = { time: this.getCurrentDateTime(), fileUid: this.getRandomUid(), fileName: file.name, }; this.$chatHistoryList.append(this.templateCompile(template, context)); contexts.push(context); } } return contexts; } /** * Функция генерации случайного идентификатора * * @returns {string} */ getRandomUid() { return `${Math.random().toString(36).substring(2, 15)}-${Date.now()}-${Math.random() .toString(36) .substring(2, 15)}`; } /** * Инкрементами счётчика сообщений * * @param {int} delta * * @returns {void} */ incrementMessageCounter(delta = 1) { let counter = $(".chat-num-messages span"); if (isEmptyValue(counter?.text())) { counter?.text("1"); } counter?.html(parseInt(counter?.text()) + delta); } /** * Функция отправки сообщения * * @param {object} messageContext - контекст сообщения * @param {array} fileContexts - контексты файлов * * @returns {void} */ sendMessage(messageContext, fileContexts) { let chat = this; let formData = new FormData(); if (chat.filesToSend.length > 0) { for (let I = 0; I < chat.filesToSend.length; I++) { const file = chat.filesToSend[I]; formData.append(`file[${I}]`, file, file.name); } } formData.append("chatId", this.chatId); formData.append("usersIds", JSON.stringify(this.getUsersIds(), null, 2)); formData.append("fileContexts", JSON.stringify(fileContexts, null, 2)); formData.append("messageContext", JSON.stringify(messageContext, null, 2)); let totalSendMessageCount = chat.filesToSend.length; if (!isEmptyValue(messageContext?.messageOutput)) { totalSendMessageCount++; } $.ajax({ url: sendMessageUrl, type: "POST", data: formData, processData: false, contentType: false, beforeSend: function () { clearPeriodicUpdateChatPeopleList(); }, }) .done(function (response) { chat.chatId = response.chatId; if (response.blobsUid.length > 0) { chat.changeBlobStatus(response.blobsUid, "sending", "fa fa-check success"); } bindFileInputEvents(); chat.incrementMessageCounter(totalSendMessageCount); updateChatPeopleList(); }) .fail(function (response) { // TODO: добавить эвент превращающий клик в повторную отправку if (response.responseJSON.blobsUid.length > 0) { chat.changeBlobStatus( response?.responseJSON?.blobsUid, "sending", "fa fa-exclamation-triangle error" ); } }) .always(function () { initPeriodicUpdateChatPeopleList(); }); } changeBlobStatus(blobsUid, removeClass, addClass) { for (let I = 0; I < blobsUid.length; I++) { const blobUid = blobsUid[I]; let blobStatus = $(`#${blobUid}`)?.find(".message-status"); if (blobStatus) { blobStatus.removeClass(removeClass); blobStatus.addClass(addClass); } } } /** * Функция получения идентификаторов пользователей * * @returns {array} - массив идентификаторов пользователей */ getUsersIds() { let destinationUserId = this.$chatHeader?.find(".chat-about")?.data("destination_user_id"); return [destinationUserId, currentUserId]; } addMessage() { this.filesToSend = ""; const filesTemp = $(".chat-file-uploader #file-upload")[0]?.files; if (!isEmptyValue(filesTemp)) { this.filesToSend = filesTemp; fileStructuredClone(); } this.messageToSend = this.$textarea.val(); this.renderChatHistory(Chat.INCOMING_TEMPLATE); } addMessageByEnterBtn(event) { // enter was pressed if (event.keyCode === 13) { this.addMessage(); } } scrollToBottom() { this.$chatHistory.scrollTop(this.$chatHistory[0]?.scrollHeight); } getCurrentTime() { return new Date().toLocaleTimeString().replace(/([\d]+:[\d]{2})(:[\d]{2})(.*)/, "$1$3"); } getCurrentDateTime() { let time = this.getCurrentTime(); let date = this.formatDate(new Date()); return `${date} ${time}`; } getRandomItem(arr) { return arr[Math.floor(Math.random() * arr.length)]; } /** * Функция перерисовки истории чата * * @param {string} refreshedHistory * * @returns {void} */ refreshChatHistory(refreshedHistory) { this.$chatHistoryList.empty(); this.$chatHistoryList.html(refreshedHistory); } /** * Дополняет входящий номер до 2х символов * заполняя пустоту нулями (`0`) * * @param {string} num * * @returns {string} */ padTo2Digits(num) { return this.padStart(num.toString(), 2, "0"); } /** * В IE11 нет функции `String.prototype.padStart` * Придётся заменять её в ручную * * @param {string} originString * @param {int} targetLength * @param {string} padString * * @returns {string} */ padStart(originString, targetLength, padString) { targetLength = targetLength >> 0; padString = String(typeof padString !== "undefined" ? padString : " "); if (originString.length > targetLength) { return String(originString); } else { targetLength = targetLength - originString.length; if (targetLength > padString.length) { padString += padString.repeat(targetLength / padString.length); } return padString.slice(0, targetLength) + String(originString); } } /** * Генерирует строку даты формата 'd.m.Y' * * @param {Date} date * * @returns {string} */ formatDate(date) { return [ this.padTo2Digits(date.getDate()), this.padTo2Digits(date.getMonth() + 1), date.getFullYear(), ].join("."); } }