From 19df5c09d20391247af90bf6c31476321ff44e0a Mon Sep 17 00:00:00 2001
From: NeonDaniel <34697904+NeonDaniel@users.noreply.github.com>
Date: Thu, 13 Feb 2025 18:20:36 +0000
Subject: [PATCH] Updated Version and Dynamic Dependencies
---
chat_client/static/js/klatchatNano.js | 5607 ++++++++++++-------------
version.py | 4 +-
2 files changed, 2784 insertions(+), 2827 deletions(-)
diff --git a/chat_client/static/js/klatchatNano.js b/chat_client/static/js/klatchatNano.js
index 12bcb66b..459844b8 100644
--- a/chat_client/static/js/klatchatNano.js
+++ b/chat_client/static/js/klatchatNano.js
@@ -1,145 +1,221 @@
-let socket;
-
-const sioTriggeringEvents = ['configLoaded', 'configNanoLoaded'];
-
-sioTriggeringEvents.forEach(event => {
- document.addEventListener(event, _ => {
- socket = initSIO();
- });
-});
-
/**
- * Inits socket io client listener by attaching relevant listeners on message channels
- * @return {Socket} Socket IO client instance
+ * Downloads desired content
+ * @param content: content to download
+ * @param filename: name of the file to download
+ * @param contentType: type of the content
*/
-function initSIO() {
-
- const sioServerURL = configData['CHAT_SERVER_URL_BASE'];
+function download(content, filename, contentType = 'application/octet-stream') {
+ if (content) {
+ const a = document.createElement('a');
+ const blob = new Blob([content], {
+ 'type': contentType
+ });
+ a.href = window.URL.createObjectURL(blob);
+ a.target = 'blank';
+ a.download = filename;
+ a.click();
+ window.URL.revokeObjectURL(content);
+ } else {
+ console.warn('Skipping downloading as content is invalid')
+ }
+}
- const socket = io(
- sioServerURL, {
- extraHeaders: {
- "session": getSessionToken()
- }
+/**
+ * Handles error while loading the image data
+ * @param image: target image Node
+ */
+function handleImgError(image) {
+ image.parentElement.insertAdjacentHTML('afterbegin', `
${image.getAttribute('alt')}
`);
+ image.parentElement.removeChild(image);
+}
+/**
+ * Resolves user reply on message
+ * @param replyID: id of user reply
+ * @param repliedID id of replied message
+ */
+function resolveUserReply(replyID, repliedID) {
+ if (repliedID) {
+ const repliedElem = document.getElementById(repliedID);
+ if (repliedElem) {
+ let repliedText = repliedElem.getElementsByClassName('message-text')[0].innerText;
+ repliedText = shrinkToFit(repliedText, 15);
+ const replyHTML = `
+${repliedText}
+`;
+ const replyPlaceholder = document.getElementById(replyID).getElementsByClassName('reply-placeholder')[0];
+ replyPlaceholder.insertAdjacentHTML('afterbegin', replyHTML);
+ attachReplyHighlighting(replyPlaceholder.getElementsByClassName('reply-text')[0]);
}
- );
-
- socket.__proto__.emitAuthorized = (event, data) => {
- socket.io.opts.extraHeaders.session = getSessionToken();
- return socket.emit(event, data);
}
+}
- socket.on('auth_expired', () => {
- if (currentUser && Object.keys(currentUser).length > 0) {
- console.log('Authorization Token expired, refreshing...')
- location.reload();
- }
+/**
+ * Attaches reply highlighting for reply item
+ * @param replyItem reply item element
+ */
+function attachReplyHighlighting(replyItem) {
+ replyItem.addEventListener('click', (e) => {
+ const repliedItem = document.getElementById(replyItem.getAttribute('data-replied-id'));
+ const backgroundParent = repliedItem.parentElement.parentElement;
+ repliedItem.scrollIntoView();
+ backgroundParent.classList.remove('message-selected');
+ setTimeout(() => backgroundParent.classList.add('message-selected'), 500);
});
+}
- socket.on('connect', () => {
- console.info(`Socket IO Connected to Server: ${sioServerURL}`)
- });
+/**
+ * Attaches message replies to initialized conversation
+ * @param conversationData: conversation data object
+ */
+function attachReplies(conversationData) {
+ if (conversationData.hasOwnProperty('chat_flow')) {
+ getUserMessages(conversationData).forEach(message => {
+ resolveUserReply(message['message_id'], message?.replied_message);
+ });
+ Array.from(document.getElementsByClassName('reply-text')).forEach(replyItem => {
+ attachReplyHighlighting(replyItem);
+ });
+ }
+}
+let userSettingsModal;
+let applyUserSettings;
+let minifyMessagesCheck;
+let settingsLink;
- socket.on("connect_error", (err) => {
- console.log(`connect_error due to ${err.message}`);
+/**
+ * Displays relevant user settings section based on provided name
+ * @param name: name of the section to display
+ */
+const displaySection = (name) => {
+ Array.from(document.getElementsByClassName('user-settings-section')).forEach(elem => {
+ elem.hidden = true;
});
+ const elem = document.getElementById(`user-settings-${name}-section`);
+ elem.hidden = false;
+}
- socket.on('new_prompt_created', async (prompt) => {
- const messageContainer = getMessageListContainer(prompt['cid']);
- const promptID = prompt['_id'];
- if (await getCurrentSkin(prompt['cid']) === CONVERSATION_SKINS.PROMPTS) {
- if (!document.getElementById(promptID)) {
- const messageHTML = await buildPromptHTML(prompt);
- messageContainer.insertAdjacentHTML('beforeend', messageHTML);
- }
- }
- });
+/**
+ * Displays user settings based on received preferences
+ * @param preferences
+ */
+const displayUserSettings = (preferences) => {
+ if (preferences) {
+ minifyMessagesCheck.checked = preferences?.minify_messages === '1'
+ }
+}
- socket.on('new_message', async (data) => {
- if (await getCurrentSkin(data.cid) === CONVERSATION_SKINS.PROMPTS && data?.prompt_id) {
- console.debug('Skipping prompt-related message')
- return
- }
- // console.debug('received new_message -> ', data)
- const preferredLang = getPreferredLanguage(data['cid']);
- if (data?.lang !== preferredLang) {
- requestTranslation(data['cid'], data['messageID']).catch(err => console.error(`Failed to request translation of cid=${data['cid']} messageID=${data['messageID']}: ${err}`));
- }
- addNewMessage(data['cid'], data['userID'], data['messageID'], data['messageText'], data['timeCreated'], data['repliedMessage'], data['attachments'], data?.isAudio, data?.isAnnouncement)
- .then(_ => addMessageTransformCallback(data['cid'], data['messageID'], data?.isAudio))
- .catch(err => console.error('Error occurred while adding new message: ', err));
- });
+/**
+ * Initialises section of settings based on provided name
+ * @param sectionName: name of the section provided
+ */
+const initSettingsSection = async (sectionName) => {
+ await refreshCurrentUser(false)
+ .then(userData => displayUserSettings(userData?.preferences))
+ .then(_ => displaySection(sectionName));
+}
- socket.on('new_prompt_message', async (message) => {
- await addPromptMessage(message['cid'], message['userID'], message['messageText'], message['promptID'], message['promptState'])
- .catch(err => console.error('Error occurred while adding new prompt data: ', err));
+/**
+ * Initialises User Settings Modal
+ */
+const initSettingsModal = async () => {
+ Array.from(document.getElementsByClassName('nav-user-settings')).forEach(navItem => {
+ navItem.addEventListener('click', async (e) => {
+ await initSettingsSection(navItem.getAttribute('data-section-name'));
+ });
});
+}
- socket.on('set_prompt_completed', async (data) => {
- const promptID = data['prompt_id'];
- const promptElem = document.getElementById(promptID);
- console.info(`setting prompt_id=${promptID} as completed`);
- if (promptElem) {
- const promptWinner = document.getElementById(`${promptID}_winner`);
- promptWinner.innerHTML = getPromptWinnerText(data['winner']);
+/**
+ * Applies new settings to current user
+ */
+const applyNewSettings = async () => {
+ const newUserSettings = {
+ 'minify_messages': minifyMessagesCheck.checked ? '1' : '0'
+ };
+ const query_url = 'preferences/update'
+ await fetchServer(query_url, REQUEST_METHODS.POST, newUserSettings, true).then(async response => {
+ const responseJson = await response.json();
+ if (response.ok) {
+ location.reload();
} else {
- console.warn(`Failed to get HTML element from prompt_id=${promptID}`);
+ displayAlert(document.getElementById(`userSettingsModalBody`),
+ `${responseJson['msg']}`,
+ 'danger');
}
});
+}
- socket.on('translation_response', async (data) => {
- console.debug('translation_response: ', data)
- await applyTranslations(data);
- });
-
- socket.on('subminds_state', async (data) => {
- console.debug('subminds_state: ', data)
- parseSubmindsState(data);
- });
-
- socket.on('incoming_tts', (data) => {
- console.debug('received incoming stt audio');
- playTTS(data['cid'], data['lang'], data['audio_data']);
- });
-
- socket.on('incoming_stt', (data) => {
- console.debug('received incoming stt response');
- showSTT(data['message_id'], data['lang'], data['message_text']);
+function initSettings(elem) {
+ elem.addEventListener('click', async (e) => {
+ await initSettingsModal();
+ userSettingsModal.modal('show');
});
-
- // socket.on('updated_shouts', async (data) =>{
- // const inputType = data['input_type'];
- // for (const [cid, shouts] of Object.entries(data['translations'])){
- // if (await getCurrentSkin(cid) === CONVERSATION_SKINS.BASE){
- // await requestTranslation(cid, shouts, null, inputType);
- // }
- // }
- // });
-
- return socket;
}
-const myAccountLink = document.getElementById('myAccountLink');
/**
- * Shows modal associated with profile
- * @param nick: nickname to fetch
- * @param edit: to open modal in edit mode
- *
- * @return true if modal shown successfully, false otherwise
+ * Initialise user settings links based on the current client
*/
-async function showProfileModal(userID = null, edit = '0') {
- let fetchURL = `${configData['currentURLBase']}/components/profile?`
- let modalId;
- let avatarId;
- if (edit === '1') {
- modalId = `${currentUser['_id']}EditModal`;
- // avatarId = `${currentUser['nickname']}EditAvatar`;
- fetchURL += `edit=1`;
+const initSettingsLinks = () => {
+ if (configData.client === CLIENTS.NANO) {
+ console.log('initialising settings link for ', Array.from(document.getElementsByClassName('settings-link')).length, ' elements')
+ Array.from(document.getElementsByClassName('settings-link')).forEach(elem => {
+ initSettings(elem);
+ });
} else {
- modalId = `${userID}Modal`;
- // avatarId = `${nick}Avatar`;
- fetchURL += `user_id=${userID}`;
+ initSettings(document.getElementById('settingsLink'));
+ }
+}
+
+document.addEventListener('DOMContentLoaded', (_) => {
+ if (configData.client === CLIENTS.MAIN) {
+ userSettingsModal = $('#userSettingsModal');
+ applyUserSettings = document.getElementById('applyUserSettings');
+ minifyMessagesCheck = document.getElementById('minifyMessages');
+ applyUserSettings.addEventListener('click', async (e) => await applyNewSettings());
+ settingsLink = document.getElementById('settingsLink');
+ settingsLink.addEventListener('click', async (e) => {
+ e.preventDefault();
+ await initSettingsModal();
+ userSettingsModal.modal('show');
+ });
+ } else {
+ document.addEventListener('modalsLoaded', (e) => {
+ userSettingsModal = $('#userSettingsModal');
+ applyUserSettings = document.getElementById('applyUserSettings');
+ minifyMessagesCheck = document.getElementById('minifyMessages');
+ applyUserSettings.addEventListener('click', async (e) => await applyNewSettings());
+ if (configData.client === CLIENTS.MAIN) {
+ initSettingsLinks();
+ }
+ });
+
+ document.addEventListener('nanoChatsLoaded', (e) => {
+ setTimeout(() => initSettingsLinks(), 1000);
+ })
+ }
+});
+const myAccountLink = document.getElementById('myAccountLink');
+
+/**
+ * Shows modal associated with profile
+ * @param nick: nickname to fetch
+ * @param edit: to open modal in edit mode
+ *
+ * @return true if modal shown successfully, false otherwise
+ */
+async function showProfileModal(userID = null, edit = '0') {
+ let fetchURL = `${configData['currentURLBase']}/components/profile?`
+ let modalId;
+ let avatarId;
+ if (edit === '1') {
+ modalId = `${currentUser['_id']}EditModal`;
+ // avatarId = `${currentUser['nickname']}EditAvatar`;
+ fetchURL += `edit=1`;
+ } else {
+ modalId = `${userID}Modal`;
+ // avatarId = `${nick}Avatar`;
+ fetchURL += `user_id=${userID}`;
}
const profileModalHTML = await fetch(fetchURL, {
headers: new Headers({
@@ -275,459 +351,681 @@ document.addEventListener('DOMContentLoaded', (e) => {
attachEditModalInvoker(myAccountLink);
}
});
-/**
- * Returns DOM container for message elements under specific conversation id
- * @param cid: conversation id to consider
- * @return {Element} DOM container for message elements of considered conversation
- */
-const getMessageListContainer = (cid) => {
- const cidElem = document.getElementById(cid);
- if (cidElem) {
- return cidElem.getElementsByClassName('card-body')[0].getElementsByClassName('chat-list')[0]
- }
+let currentUserNavDisplay = document.getElementById('currentUserNavDisplay');
+/* Login items */
+let loginModal;
+let loginButton;
+let loginUsername;
+let loginPassword;
+let toggleSignup;
+/* Logout Items */
+let logoutModal;
+let logoutConfirm;
+/* Signup items */
+let signupModal;
+let signupButton;
+let signupUsername;
+let signupFirstName;
+let signupLastName;
+let signupPassword;
+let repeatSignupPassword;
+let toggleLogin;
+
+let currentUser = null;
+
+
+function initModalElements() {
+ currentUserNavDisplay = document.getElementById('currentUserNavDisplay');
+ logoutModal = $('#logoutModal');
+ logoutConfirm = document.getElementById('logoutConfirm');
+ loginModal = $('#loginModal');
+ loginButton = document.getElementById('loginButton');
+ loginUsername = document.getElementById('loginUsername');
+ loginPassword = document.getElementById('loginPassword');
+ toggleSignup = document.getElementById('toggleSignup');
+ signupModal = $('#signupModal');
+ signupButton = document.getElementById('signupButton');
+ signupUsername = document.getElementById('signupUsername');
+ signupFirstName = document.getElementById('signupFirstName');
+ signupLastName = document.getElementById('signupLastName');
+ signupPassword = document.getElementById('signupPassword');
+ repeatSignupPassword = document.getElementById('repeatSignupPassword');
+ toggleLogin = document.getElementById('toggleLogin');
+}
+
+
+const MODAL_NAMES = {
+ LOGIN: 'login',
+ LOGOUT: 'logout',
+ SIGN_UP: 'signup',
+ USER_SETTINGS: 'user_settings'
}
+
/**
- * Gets message node from the message container
- * @param messageContainer: DOM Message Container element to consider
- * @param validateType: type of message to validate
- * @return {HTMLElement} ID of the message
+ * Adds new modal under specific conversation id
+ * @param name: name of the modal from MODAL_NAMES to add
*/
-const getMessageNode = (messageContainer, validateType = null) => {
- let detectedType;
- let node
- if (messageContainer.getElementsByTagName('table').length > 0) {
- detectedType = 'prompt';
- node = messageContainer.getElementsByTagName('table')[0];
- } else {
- detectedType = 'plain'
- node = messageContainer.getElementsByClassName('chat-body')[0].getElementsByClassName('chat-message')[0];
- }
- if (validateType && validateType !== detectedType) {
- return null;
+async function addModal(name) {
+ if (Object.values(MODAL_NAMES).includes(name)) {
+ return await buildHTMLFromTemplate(`modals.${name}`)
} else {
- return node;
+ console.warn(`Unresolved modal name - ${name}`)
}
}
/**
- * Adds new message to desired conversation id
- * @param cid: desired conversation id
- * @param userID: message sender id
- * @param messageID: id of sent message (gets generated if null)
- * @param messageText: text of the message
- * @param timeCreated: timestamp for message creation
- * @param repliedMessageID: id of the replied message (optional)
- * @param attachments: array of attachments to add (optional)
- * @param isAudio: is audio message (defaults to '0')
- * @param isAnnouncement: is message an announcement (defaults to "0")
- * @returns {Promise}: promise resolving id of added message, -1 if failed to resolve message id creation
+ * Initializes modals per target conversation id (if not provided - for main client)
+ * @param parentID: id of the parent to attach element to
*/
-async function addNewMessage(cid, userID = null, messageID = null, messageText, timeCreated, repliedMessageID = null, attachments = [], isAudio = '0', isAnnouncement = '0') {
- const messageList = getMessageListContainer(cid);
- if (messageList) {
- let userData;
- const isMine = userID === currentUser['_id'];
- if (isMine) {
- userData = currentUser;
- } else {
- userData = await getUserData(userID);
- }
- if (!messageID) {
- messageID = generateUUID();
+async function initModals(parentID = null) {
+ if (parentID) {
+ const parentElem = document.getElementById(parentID);
+ if (!parentElem) {
+ console.warn('No element detected with provided parentID=', parentID)
+ return -1;
}
- let messageHTML = await buildUserMessageHTML(userData, cid, messageID, messageText, timeCreated, isMine, isAudio, isAnnouncement);
- const blankChat = messageList.getElementsByClassName('blank_chat');
- if (blankChat.length > 0) {
- messageList.removeChild(blankChat[0]);
+ for (const modalName of [
+ MODAL_NAMES.LOGIN,
+ MODAL_NAMES.LOGOUT,
+ MODAL_NAMES.SIGN_UP,
+ MODAL_NAMES.USER_SETTINGS
+ ]) {
+ const modalHTML = await addModal(modalName);
+ parentElem.insertAdjacentHTML('beforeend', modalHTML);
}
- messageList.insertAdjacentHTML('beforeend', messageHTML);
- resolveMessageAttachments(cid, messageID, attachments);
- resolveUserReply(messageID, repliedMessageID);
- addProfileDisplay(userID, cid, messageID, 'plain');
- scrollOnNewMessage(messageList);
- return messageID;
}
+ initModalElements();
+ logoutConfirm.addEventListener('click', (e) => {
+ e.preventDefault();
+ logoutUser().catch(err => console.error('Error while logging out user: ', err));
+ });
+ toggleLogin.addEventListener('click', (e) => {
+ e.preventDefault();
+ signupModal.modal('hide');
+ loginModal.modal('show');
+ });
+ loginButton.addEventListener('click', (e) => {
+ e.preventDefault();
+ loginUser().catch(err => console.error('Error while logging in user: ', err));
+ });
+ toggleSignup.addEventListener('click', (e) => {
+ e.preventDefault();
+ loginModal.modal('hide');
+ signupModal.modal('show');
+ });
+ signupButton.addEventListener('click', (e) => {
+ e.preventDefault();
+ createUser().catch(err => console.error('Error while creating a user: ', err));
+ });
+ const modalsLoaded = new CustomEvent('modalsLoaded');
+ document.dispatchEvent(modalsLoaded);
}
-const PROMPT_STATES = {
- 1: 'RESP',
- 2: 'DISC',
- 3: 'VOTE'
-}
-/**
- * Returns HTML Element representing user row in prompt
- * @param promptID: target prompt id
- * @param userID: target user id
- * @return {HTMLElement}: HTML Element containing user prompt data
- */
-const getUserPromptTR = (promptID, userID) => {
- return document.getElementById(`${promptID}_${userID}_prompt_row`);
-}
+const USER_DATA_CACHE = {}
+const USER_DATA_CACHE_EXPIRY_SECONDS = 3600;
/**
- * Adds prompt message of specified user id
- * @param cid: target conversation id
- * @param userID: target submind user id
- * @param messageText: message of submind
- * @param promptId: target prompt id
- * @param promptState: prompt state to consider
+ * Gets user data from local cache
+ * @param userID - id of the user to look-up (lookups authorized user if null)
+ * @returns {Promise<{}>} promise resolving obtaining of user data
*/
-async function addPromptMessage(cid, userID, messageText, promptId, promptState) {
- const tableBody = document.getElementById(`${promptId}_tbody`);
- if (await getCurrentSkin(cid) === CONVERSATION_SKINS.PROMPTS) {
- try {
- promptState = PROMPT_STATES[promptState].toLowerCase();
- if (!getUserPromptTR(promptId, userID)) {
- const userData = await getUserData(userID);
- const newUserRow = await buildSubmindHTML(promptId, userID, userData, '', '', '');
- tableBody.insertAdjacentHTML('beforeend', newUserRow);
- }
- try {
- const messageElem = document.getElementById(`${promptId}_${userID}_${promptState}`);
- messageElem.innerText = messageText;
- } catch (e) {
- console.warn(`Failed to add prompt message (${cid},${userID}, ${messageText}, ${promptId}, ${promptState}) - ${e}`)
- }
- } catch (e) {
- console.info(`Skipping message of invalid prompt state - ${promptState}`);
+const getUserDataFromCache = (userID) => {
+ if (USER_DATA_CACHE?.[userID]?.data) {
+ if (getCurrentTimestamp() - USER_DATA_CACHE[userID].ts < USER_DATA_CACHE_EXPIRY_SECONDS) {
+ return USER_DATA_CACHE[userID].data;
}
}
}
-
/**
- * Returns first message id based on given element
- * @param firstChild: DOM element of first message child
+ * Gets user data from chat client URL
+ * @param userID - id of the user to look-up (lookups authorized user if null)
+ * @returns {Promise<{}>} promise resolving obtaining of user data
*/
-function getFirstMessageFromCID(firstChild) {
- if (firstChild.classList.contains('prompt-item')) {
- const promptTable = firstChild.getElementsByTagName('table')[0];
- const promptID = promptTable.id;
- const promptTBody = promptTable.getElementsByTagName('tbody')[0];
- let currentRecentMessage = null;
- let currentOldestTS = null;
- Array.from(promptTBody.getElementsByTagName('tr')).forEach(tr => {
- const submindID = tr.getAttribute('data-submind-id');
- ['resp', 'opinion', 'vote'].forEach(phase => {
- const phaseElem = document.getElementById(`${promptID}_${submindID}_${phase}`);
- if (phaseElem) {
- let createdOn = phaseElem.getAttribute(`data-created-on`);
- const messageID = phaseElem.getAttribute(`data-message-id`)
- if (createdOn && messageID) {
- createdOn = parseInt(createdOn);
- if (!currentOldestTS || createdOn < currentOldestTS) {
- currentOldestTS = createdOn;
- currentRecentMessage = messageID;
- }
- }
- }
- });
- });
- return currentRecentMessage;
- } else {
- return getMessageNode(firstChild, 'plain')?.id;
+async function getUserData(userID = null) {
+ let userData = {}
+ let query_url = `users_api/`;
+ if (userID) {
+ const cachedUserData = getUserDataFromCache(userID);
+ if (cachedUserData) {
+ return cachedUserData;
+ }
+ query_url += '?user_id=' + userID;
}
+ await fetchServer(query_url)
+ .then(response => response.ok ? response.json() : {
+ 'data': {}
+ })
+ .then(data => {
+ userData = data['data'];
+ const oldToken = getSessionToken();
+ if (data['token'] !== oldToken && !userID) {
+ setSessionToken(data['token']);
+ }
+ USER_DATA_CACHE[userID] = {
+ data: userData,
+ ts: getCurrentTimestamp()
+ }
+ });
+ return userData;
}
/**
- * Gets list of the next n-older messages
- * @param cid: target conversation id
- * @param skin: target conversation skin
+ * Method that handles fetching provided user data with valid login credentials
+ * @returns {Promise} promise resolving validity of user-entered data
*/
-async function addOldMessages(cid, skin = CONVERSATION_SKINS.BASE) {
- const messageContainer = getMessageListContainer(cid);
- if (messageContainer.children.length > 0) {
- for (let i = 0; i < messageContainer.children.length; i++) {
- const firstMessageItem = messageContainer.children[i];
- const oldestMessageTS = await DBGateway.getInstance(DB_TABLES.CHAT_MESSAGES_PAGINATION).getItem(cid).then(res => res?.oldest_created_on || null);
- if (oldestMessageTS) {
- const numMessages = await getCurrentSkin(cid) === CONVERSATION_SKINS.PROMPTS ? 30 : 10;
- await getConversationDataByInput(cid, skin, oldestMessageTS, numMessages).then(async conversationData => {
- if (messageContainer) {
- const userMessageList = getUserMessages(conversationData, null);
- userMessageList.sort((a, b) => {
- a['created_on'] - b['created_on'];
- }).reverse();
- for (const message of userMessageList) {
- message['cid'] = cid;
- if (!isDisplayed(getMessageID(message))) {
- const messageHTML = await messageHTMLFromData(message, skin);
- messageContainer.insertAdjacentHTML('afterbegin', messageHTML);
- } else {
- console.debug(`!!message_id=${message["message_id"]} is already displayed`)
- }
- }
- await initMessages(conversationData, skin);
- }
- }).then(_ => {
- firstMessageItem.scrollIntoView({
- behavior: "smooth"
- });
- });
- break;
- } else {
- console.warn(`NONE first message id detected for cid=${cid}`)
- }
- }
+async function loginUser() {
+ const loginModalBody = document.getElementById('loginModalBody');
+ const query_url = `auth/login/`;
+ const formData = new FormData();
+ const inputValues = [loginUsername.value, loginPassword.value];
+ if (inputValues.includes("") || inputValues.includes(null)) {
+ displayAlert(loginModalBody, 'Required fields are blank', 'danger');
+ } else {
+ formData.append('username', loginUsername.value);
+ formData.append('password', loginPassword.value);
+ await fetchServer(query_url, REQUEST_METHODS.POST, formData)
+ .then(async response => {
+ return {
+ 'ok': response.ok,
+ 'data': await response.json()
+ };
+ })
+ .then(async responseData => {
+ if (responseData['ok']) {
+ setSessionToken(responseData['data']['token']);
+ } else {
+ displayAlert(loginModalBody, responseData['data']['msg'], 'danger', 'login-failed-alert');
+ loginPassword.value = "";
+ }
+ }).catch(ex => {
+ console.warn(`Exception during loginUser -> ${ex}`);
+ displayAlert(loginModalBody);
+ });
}
}
-
/**
- * Returns message id based on message type
- * @param message: message object to check
- * @returns {null|*} message id extracted if valid message type detected
+ * Method that handles logging user out
+ * @returns {Promise} promise resolving user logout
*/
-const getMessageID = (message) => {
- switch (message['message_type']) {
- case 'plain':
- return message['message_id'];
- case 'prompt':
- return message['_id'];
- default:
- console.warn(`Invalid message structure received - ${message}`);
- return null;
- }
+async function logoutUser() {
+ const query_url = `auth/logout/`;
+ await fetchServer(query_url).then(async response => {
+ if (response.ok) {
+ const responseJson = await response.json();
+ setSessionToken(responseJson['token']);
+ }
+ });
}
/**
- * Array of user messages in given conversation
- * @param conversationData: Conversation Data object to fetch
- * @param forceType: to force particular type of messages among the chat flow
+ * Method that handles fetching provided user data with valid sign up credentials
+ * @returns {Promise} promise resolving validity of new user creation
*/
-const getUserMessages = (conversationData, forceType = 'plain') => {
- try {
- let messages = Array.from(conversationData['chat_flow']);
- if (forceType) {
- messages = messages.filter(message => message['message_type'] === forceType);
- }
- return messages;
- } catch {
- return [];
+async function createUser() {
+ const signupModalBody = document.getElementById('signupModalBody');
+ const query_url = `auth/signup/`;
+ const formData = new FormData();
+ const inputValues = [signupUsername.value, signupFirstName.value, signupLastName.value, signupPassword.value, repeatSignupPassword.value];
+ if (inputValues.includes("") || inputValues.includes(null)) {
+ displayAlert(signupModalBody, 'Required fields are blank', 'danger');
+ } else if (signupPassword.value !== repeatSignupPassword.value) {
+ displayAlert(signupModalBody, 'Passwords do not match', 'danger');
+ } else {
+ formData.append('nickname', signupUsername.value);
+ formData.append('first_name', signupFirstName.value);
+ formData.append('last_name', signupLastName.value);
+ formData.append('password', signupPassword.value);
+ await fetchServer(query_url, REQUEST_METHODS.POST, formData)
+ .then(async response => {
+ return {
+ 'ok': response.ok,
+ 'data': await response.json()
+ }
+ })
+ .then(async data => {
+ if (data['ok']) {
+ setSessionToken(data['data']['token']);
+ } else {
+ let errorMessage = 'Failed to create an account';
+ if (data['data'].hasOwnProperty('msg')) {
+ errorMessage = data['data']['msg'];
+ }
+ displayAlert(signupModalBody, errorMessage, 'danger');
+ }
+ });
}
}
/**
- * Initializes listener for loading old message on scrolling conversation box
- * @param conversationData: Conversation Data object to fetch
- * @param skin: conversation skin to apply
+ * Helper method for updating navbar based on current user property
+ * @param forceUpdate to force updating of navbar (defaults to false)
*/
-function initLoadOldMessages(conversationData, skin) {
- const cid = conversationData['_id'];
- const messageList = getMessageListContainer(cid);
- const messageListParent = messageList.parentElement;
- setDefault(setDefault(conversationState, cid, {}), 'lastScrollY', 0);
- messageListParent.addEventListener("scroll", async (e) => {
- const oldScrollPosition = conversationState[cid]['scrollY'];
- conversationState[cid]['scrollY'] = e.target.scrollTop;
- if (oldScrollPosition > conversationState[cid]['scrollY'] &&
- !conversationState[cid]['all_messages_displayed'] &&
- conversationState[cid]['scrollY'] === 0) {
- setChatState(cid, 'updating', 'Loading messages...')
- await addOldMessages(cid, skin);
- for (const inputType of ['incoming', 'outcoming']) {
- await requestTranslation(cid, null, null, inputType);
+function updateNavbar(forceUpdate = false) {
+ if (currentUser || forceUpdate) {
+ let innerText = shrinkToFit(currentUser['nickname'], 10);
+ let targetElems = [currentUserNavDisplay];
+ if (configData.client === CLIENTS.MAIN) {
+ if (currentUser['is_tmp']) {
+ // Leaving only "guest" without suffix
+ innerText = innerText.split('_')[0]
+ innerText += ', Login';
+ } else {
+ innerText += ', Logout';
}
- setTimeout(() => {
- setChatState(cid, 'active');
- }, 700);
+ } else if (configData.client === CLIENTS.NANO) {
+ if (currentUser['is_tmp']) {
+ // Leaving only "guest" without suffix
+ innerText = innerText.split('_')[0]
+ innerText += ' ';
+ } else {
+ innerText += ' ';
+ }
+ targetElems = Array.from(document.getElementsByClassName('account-link'))
}
- });
+ if (targetElems.length > 0 && targetElems[0]) {
+ targetElems.forEach(elem => {
+ elem.innerHTML = `
+${innerText}
+`;
+ });
+ }
+ }
}
+
/**
- * Attaches event listener to display element's target user profile
- * @param userID target user id
- * @param elem target DOM element
+ * Refreshes HTML components appearance based on the current user
+ * NOTE: this must have only visual impact, the actual validation is done on the backend
*/
-function attachTargetProfileDisplay(userID, elem) {
- if (elem) {
- elem.addEventListener('click', async (_) => {
- if (userID) await showProfileModal(userID)
- });
+const refreshComponentsAppearance = () => {
+ const currentUserRoles = currentUser?.roles ?? [];
+ const isAdmin = currentUserRoles.includes("admin");
+
+ const createLiveConversationWrapper = document.getElementById("createLiveConversationWrapper");
+
+ if (isAdmin) {
+ createLiveConversationWrapper.style.display = "";
+ } else {
+ createLiveConversationWrapper.style.display = "none";
}
}
/**
- * Adds callback for showing profile information on profile avatar click
- * @param userID target user id
- * @param cid target conversation id
- * @param messageId target message id
- * @param messageType type of message to display
+ * Custom Event fired on current user loaded
+ * @type {CustomEvent}
*/
-function addProfileDisplay(userID, cid, messageId, messageType = 'plain') {
- if (messageType === 'plain') {
- attachTargetProfileDisplay(userID, document.getElementById(`${messageId}_avatar`))
- } else if (messageType === 'prompt') {
- const promptTBody = document.getElementById(`${messageId}_tbody`);
- const rows = promptTBody.getElementsByTagName('tr');
- Array.from(rows).forEach(row => {
- attachTargetProfileDisplay(userID, Array.from(row.getElementsByTagName('td'))[0].getElementsByClassName('chat-img')[0]);
- })
- }
-}
-
+const currentUserLoaded = new CustomEvent("currentUserLoaded", {
+ "detail": "Event that is fired when current user is loaded"
+});
/**
- * Inits addProfileDisplay() on each message of provided conversation
- * @param conversationData - target conversation data
+ * Convenience method encapsulating refreshing page view based on current user
+ * @param refreshChats: to refresh the chats (defaults to false)
+ * @param conversationContainer: DOM Element representing conversation container
*/
-function initProfileDisplay(conversationData) {
- getUserMessages(conversationData, null).forEach(message => {
- addProfileDisplay(message['user_id'], conversationData['_id'], getMessageID(message), message['message_type']);
+async function refreshCurrentUser(refreshChats = false, conversationContainer = null) {
+ await getUserData().then(data => {
+ currentUser = data;
+ console.log(`Loaded current user = ${JSON.stringify(currentUser)}`);
+ setTimeout(() => updateNavbar(), 500);
+ if (refreshChats) {
+ refreshChatView(conversationContainer);
+ }
+ refreshComponentsAppearance()
+ console.log('current user loaded');
+ document.dispatchEvent(currentUserLoaded);
+ return data;
});
}
+
+document.addEventListener('DOMContentLoaded', async (e) => {
+ if (configData['client'] === CLIENTS.MAIN) {
+ await initModals();
+ currentUserNavDisplay.addEventListener('click', (e) => {
+ e.preventDefault();
+ currentUser['is_tmp'] ? loginModal.modal('show') : logoutModal.modal('show');
+ });
+ }
+});
/**
- * Inits pagination based on the oldest message creation timestamp
- * @param conversationData - target conversation data
+ * Gets time object from provided UNIX timestamp
+ * @param timestampCreated: UNIX timestamp (in seconds)
+ * @returns {string} string time (hours:minutes)
*/
-async function initPagination(conversationData) {
- const userMessages = getUserMessages(conversationData, null);
- if (userMessages.length > 0) {
- const oldestMessage = Math.min(...userMessages.map(msg => parseInt(msg.created_on)));
- await DBGateway
- .getInstance(DB_TABLES.CHAT_MESSAGES_PAGINATION)
- .putItem({
- cid: conversationData['_id'],
- oldest_created_on: oldestMessage
- })
+function getTimeFromTimestamp(timestampCreated = 0) {
+ if (!timestampCreated) {
+ return ''
}
-}
-
+ let date = new Date(timestampCreated * 1000);
+ let year = date.getFullYear().toString();
+ let month = date.getMonth() + 1;
+ month = month >= 10 ? month.toString() : '0' + month.toString();
+ let day = date.getDate();
-/**
- * Initializes messages based on provided conversation aata
- * @param conversationData - JS Object containing conversation data of type:
- * {
- * '_id': 'id of conversation',
- * 'conversation_name': 'title of the conversation',
- * 'chat_flow': [{
- * 'user_nickname': 'nickname of sender',
- * 'user_avatar': 'avatar of sender',
- * 'message_id': 'id of the message',
- * 'message_text': 'text of the message',
- * 'is_audio': true if message is an audio message
- * 'is_announcement': true if message is considered to be an announcement
- * 'created_on': 'creation time of the message'
- * }, ... (num of user messages returned)]
- * }
- * @param skin - target conversation skin to consider
- */
-async function initMessages(conversationData, skin) {
- initProfileDisplay(conversationData);
- attachReplies(conversationData);
- addAttachments(conversationData);
- addCommunicationChannelTransformCallback(conversationData);
- initLoadOldMessages(conversationData, skin);
- await initPagination(conversationData);
+ day = day >= 10 ? day.toString() : '0' + day.toString();
+ const hours = date.getHours().toString();
+ let minutes = date.getMinutes();
+ minutes = minutes >= 10 ? minutes.toString() : '0' + minutes.toString();
+ return strFmtDate(year, month, day, hours, minutes, null);
}
/**
- * Emits user message to Socket IO Server
- * @param textInputElem: DOM Element with input text (audio object if isAudio=true)
- * @param cid: Conversation ID
- * @param repliedMessageID: ID of replied message
- * @param attachments: list of attachments file names
- * @param isAudio: is audio message being emitted (defaults to '0')
- * @param isAnnouncement: is message an announcement (defaults to '0')
+ * Composes date based on input params
+ * @param year: desired year
+ * @param month: desired month
+ * @param day: desired day
+ * @param hours: num of hours
+ * @param minutes: minutes
+ * @param seconds: seconds
+ * @return date string
*/
-function emitUserMessage(textInputElem, cid, repliedMessageID = null, attachments = [], isAudio = '0', isAnnouncement = '0') {
- if (isAudio === '1' || textInputElem && textInputElem.value) {
- const timeCreated = getCurrentTimestamp();
- let messageText;
- if (isAudio === '1') {
- messageText = textInputElem;
- } else {
- messageText = textInputElem.value;
- }
- addNewMessage(cid, currentUser['_id'], null, messageText, timeCreated, repliedMessageID, attachments, isAudio, isAnnouncement).then(async messageID => {
- const preferredShoutLang = getPreferredLanguage(cid, 'outcoming');
- socket.emitAuthorized('user_message', {
- 'cid': cid,
- 'userID': currentUser['_id'],
- 'messageText': messageText,
- 'messageID': messageID,
- 'lang': preferredShoutLang,
- 'attachments': attachments,
- 'isAudio': isAudio,
- 'isAnnouncement': isAnnouncement,
- 'timeCreated': timeCreated
- });
- if (preferredShoutLang !== 'en') {
- await requestTranslation(cid, messageID, 'en', 'outcoming', true);
- }
- addMessageTransformCallback(cid, messageID, isAudio);
- });
- if (isAudio === '0') {
- textInputElem.value = "";
+function strFmtDate(year, month, day, hours, minutes, seconds) {
+ let finalDate = "";
+ if (year && month && day) {
+ finalDate += `${year}-${month}-${day}`
+ }
+ if (hours && minutes) {
+ finalDate += ` ${hours}:${minutes}`
+ if (seconds) {
+ finalDate += `:${seconds}`
}
}
+ return finalDate;
}
/**
- * Enum of possible Alert Behaviours:
- * - DEFAULT: static alert message appeared with no expiration time
- * - AUTO_EXPIRE: alert message will be expired after some amount of time (defaults to 3 seconds)
+ * Adds speaking callback for the message
+ * @param cid: id of the conversation
+ * @param messageID: id of the message
*/
-const alertBehaviors = {
- STATIC: 'static',
- AUTO_EXPIRE: 'auto_expire'
+function addTTSCallback(cid, messageID) {
+ const speakingButton = document.getElementById(`${messageID}_speak`);
+ if (speakingButton) {
+ speakingButton.addEventListener('click', (e) => {
+ e.preventDefault();
+ getTTS(cid, messageID, getPreferredLanguage(cid));
+ setChatState(cid, 'updating', `Fetching TTS...`)
+ });
+ }
}
/**
- * Adds Bootstrap alert HTML to specified element's id
- * @param parentElem: DOM Element in which to display alert
- * @param text: Text of alert (defaults 'Error Occurred')
- * @param alertType: Type of alert from bootstrap-supported alert types (defaults to 'danger')
- * @param alertID: Id of alert to display (defaults to 'alert')
- * @param alertBehaviorProperties: optional properties associated with alert message behavior
+ * Adds speaking callback for the message
+ * @param cid: id of the conversation
+ * @param messageID: id of the message
*/
-function displayAlert(parentElem, text = 'Error Occurred', alertType = 'danger', alertID = 'alert',
- alertBehaviorProperties = null) {
- if (!parentElem) {
- console.warn('Alert is not displayed as parentElem is not defined');
- return
- }
- if (typeof parentElem === 'string') {
- parentElem = document.getElementById(parentElem);
- }
- if (!['info', 'success', 'warning', 'danger', 'primary', 'secondary', 'dark'].includes(alertType)) {
- alertType = 'danger'; //default
- }
- let alert = document.getElementById(alertID);
- if (alert) {
- alert.remove();
- }
-
- if (!alertBehaviorProperties) {
- alertBehaviorProperties = {
- 'type': alertBehaviors.AUTO_EXPIRE,
- }
- }
-
- if (text) {
- parentElem.insertAdjacentHTML('afterbegin',
- `
-${text}
-
-
`);
- if (alertBehaviorProperties) {
- setDefault(alertBehaviorProperties, 'type', alertBehaviors.STATIC);
- if (alertBehaviorProperties['type'] === alertBehaviors.AUTO_EXPIRE) {
- const expirationTime = setDefault(alertBehaviorProperties, 'expiration', 3000);
- const slideLength = setDefault(alertBehaviorProperties, 'fadeLength', 500);
- setTimeout(function() {
- $(`#${alertID}`).slideUp(slideLength, () => {
- $(this).remove();
- });
- }, expirationTime);
+function addSTTCallback(cid, messageID) {
+ const sttButton = document.getElementById(`${messageID}_text`);
+ if (sttButton) {
+ sttButton.addEventListener('click', (e) => {
+ e.preventDefault();
+ const sttContent = document.getElementById(`${messageID}-stt`);
+ if (sttContent) {
+ sttContent.innerHTML = `
+Waiting for STT...
+Loading...
+
+
`;
+ sttContent.style.setProperty('display', 'block', 'important');
+ getSTT(cid, messageID, getPreferredLanguage(cid));
}
- }
+ });
+ }
+}
+
+/**
+ * Attaches STT capabilities for audio messages and TTS capabilities for text messages
+ * @param cid: parent conversation id
+ * @param messageID: target message id
+ * @param isAudio: if its an audio message (defaults to '0')
+ */
+function addMessageTransformCallback(cid, messageID, isAudio = '0') {
+ if (isAudio === '1') {
+ addSTTCallback(cid, messageID);
+ } else {
+ addTTSCallback(cid, messageID);
+ }
+}
+
+
+/**
+ * Attaches STT capabilities for audio messages and TTS capabilities for text messages
+ * @param conversationData: conversation data object
+ */
+function addCommunicationChannelTransformCallback(conversationData) {
+ if (conversationData.hasOwnProperty('chat_flow')) {
+ getUserMessages(conversationData).forEach(message => {
+ addMessageTransformCallback(conversationData['_id'], message['message_id'], message?.is_audio);
+ });
+ }
+}
+/**
+ * Collection of supported clients, current client is matched based on client configuration
+ * @type {{NANO: string, MAIN: string}}
+ */
+const CLIENTS = {
+ MAIN: 'main',
+ NANO: 'nano',
+ UNDEFINED: undefined
+}
+
+/**
+ * JS Object containing frontend configuration data
+ * @type {{staticFolder: string, currentURLBase: string, currentURLFull: (string|string|string|SVGAnimatedString|*), client: string}}
+ */
+
+let configData = {
+ 'staticFolder': "../../static",
+ 'currentURLBase': extractURLBase(),
+ 'currentURLFull': window.location.href,
+ 'client': typeof metaConfig !== 'undefined' ? metaConfig?.client : CLIENTS.UNDEFINED,
+ "MAX_CONVERSATIONS_PER_PAGE": 4,
+};
+
+/**
+ * Default key for storing data in local storage
+ * @type {string}
+ */
+const conversationAlignmentKey = 'conversationAlignment';
+
+/**
+ * Custom Event fired on configs ended up loading
+ * @type {CustomEvent}
+ */
+const configFullLoadedEvent = new CustomEvent("configLoaded", {
+ "detail": "Event that is fired when configs are loaded"
+});
+
+/**
+ * Convenience method for getting URL base for current page
+ * @returns {string} constructed URL base
+ */
+function extractURLBase() {
+ return window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
+}
+
+/**
+ * Extracts json data from provided URL path
+ * @param urlPath - file path string
+ * @param onError - callback on extraction failure
+ * @returns {Promise<* | {}>} promise that resolves data obtained from file path
+ */
+async function extractJsonData(urlPath = "",
+ onError = (e) => console.error(`failed to extractJsonData - ${e}`)) {
+ return fetch(urlPath).then(response => {
+ if (response.ok) {
+ return response.json();
+ }
+ return {};
+ }).catch(onError);
+}
+
+
+document.addEventListener('DOMContentLoaded', async (e) => {
+ if (configData['client'] === CLIENTS.MAIN) {
+ configData = Object.assign(configData, await extractJsonData(`${configData['currentURLBase']}/base/runtime_config`), (e) => location.reload());
+ document.dispatchEvent(configFullLoadedEvent);
+ }
+});
+/**
+ * Returns current UNIX timestamp in seconds
+ * @return {number}: current unix timestamp
+ */
+const getCurrentTimestamp = () => {
+ return Math.floor(Date.now() / 1000);
+};
+
+// Client's timer
+// TODO consider refactoring to "timer per component" if needed
+let __timer = 0;
+
+
+/**
+ * Sets timer to current timestamp
+ */
+const startTimer = () => {
+ __timer = Date.now();
+};
+
+/**
+ * Resets times and returns time elapsed since invocation of startTimer()
+ * @return {number} Number of seconds elapsed
+ */
+const stopTimer = () => {
+ const timeDue = Date.now() - __timer;
+ __timer = 0;
+ return timeDue;
+};
+const REQUEST_METHODS = {
+ GET: 'GET',
+ PUT: 'PUT',
+ DELETE: 'DELETE',
+ POST: 'POST'
+}
+
+const controllers = new Set();
+
+
+const getSessionToken = () => {
+ return localStorage.getItem('session') || '';
+}
+
+const setSessionToken = (val) => {
+ const currentValue = getSessionToken();
+ localStorage.setItem('session', val);
+ if (currentValue && currentValue !== val) {
+ location.reload();
+ }
+}
+
+const fetchServer = async (urlSuffix, method = REQUEST_METHODS.GET, body = null, json = false) => {
+ const controller = new AbortController();
+ controllers.add(controller);
+ const signal = controller.signal;
+
+ const options = {
+ method: method,
+ headers: new Headers({
+ 'Authorization': getSessionToken()
+ }),
+ signal,
+ }
+ if (body) {
+ options['body'] = body;
+ }
+ // TODO: there is an issue validating FormData on backend, so JSON property should eventually become true
+ if (json) {
+ options['headers'].append('Content-Type', 'application/json');
+ if (options['body']) {
+ options['body'] &&= JSON.stringify(options['body'])
+ }
+ }
+ return fetch(`${configData["CHAT_SERVER_URL_BASE"]}/${urlSuffix}`, options).then(async response => {
+ if (response.status === 401) {
+ const responseJson = await response.json();
+ if (responseJson['msg'] === 'Session token is invalid or expired') {
+ localStorage.removeItem('session');
+ location.reload();
+ }
+ }
+ return response;
+ }).finally(() => {
+ controllers.delete(controller);
+ });
+}
+
+
+document.addEventListener('beforeunload', () => {
+ for (const controller of controllers) {
+ controller.abort();
+ }
+});
+/**
+ * Enum of possible Alert Behaviours:
+ * - DEFAULT: static alert message appeared with no expiration time
+ * - AUTO_EXPIRE: alert message will be expired after some amount of time (defaults to 3 seconds)
+ */
+const alertBehaviors = {
+ STATIC: 'static',
+ AUTO_EXPIRE: 'auto_expire'
+}
+
+/**
+ * Adds Bootstrap alert HTML to specified element's id
+ * @param parentElem: DOM Element in which to display alert
+ * @param text: Text of alert (defaults 'Error Occurred')
+ * @param alertType: Type of alert from bootstrap-supported alert types (defaults to 'danger')
+ * @param alertID: Id of alert to display (defaults to 'alert')
+ * @param alertBehaviorProperties: optional properties associated with alert message behavior
+ */
+function displayAlert(parentElem, text = 'Error Occurred', alertType = 'danger', alertID = 'alert',
+ alertBehaviorProperties = null) {
+ if (!parentElem) {
+ console.warn('Alert is not displayed as parentElem is not defined');
+ return
+ }
+ if (typeof parentElem === 'string') {
+ parentElem = document.getElementById(parentElem);
+ }
+ if (!['info', 'success', 'warning', 'danger', 'primary', 'secondary', 'dark'].includes(alertType)) {
+ alertType = 'danger'; //default
+ }
+ let alert = document.getElementById(alertID);
+ if (alert) {
+ alert.remove();
+ }
+
+ if (!alertBehaviorProperties) {
+ alertBehaviorProperties = {
+ 'type': alertBehaviors.AUTO_EXPIRE,
+ }
+ }
+
+ if (text) {
+ parentElem.insertAdjacentHTML('afterbegin',
+ `
+${text}
+
+
`);
+ if (alertBehaviorProperties) {
+ setDefault(alertBehaviorProperties, 'type', alertBehaviors.STATIC);
+ if (alertBehaviorProperties['type'] === alertBehaviors.AUTO_EXPIRE) {
+ const expirationTime = setDefault(alertBehaviorProperties, 'expiration', 3000);
+ const slideLength = setDefault(alertBehaviorProperties, 'fadeLength', 500);
+ setTimeout(function() {
+ $(`#${alertID}`).slideUp(slideLength, () => {
+ $(this).remove();
+ });
+ }, expirationTime);
+ }
+ }
}
}
@@ -883,788 +1181,397 @@ const MIMES = [
];
const IMAGE_EXTENSIONS = MIMES.filter(item => item[1].startsWith('image/')).map(item => item[0]);
-let submindsState;
-
-function renderActiveSubminds(cid) {
- if (!submindsState) {
- console.log(`Subminds for CID ${cid} not yet loaded.`);
- return;
- }
- const loadingSpinner = document.getElementById(`${cid}-subminds-state-loading`);
- if (loadingSpinner) {
- loadingSpinner.classList.remove('d-flex');
- loadingSpinner.style.display = 'none';
- }
-
- const dropdownMenu = document.getElementById(`bot-list-${cid}`);
- dropdownMenu.addEventListener('click', (event) => {
- event.stopPropagation();
- });
-
- const table = document.getElementById(`${cid}-subminds-state-table`);
- const entriesContainer = document.getElementById(`${cid}-subminds-state-entries`);
- const buttonsContainer = document.getElementById(`${cid}-subminds-buttons`);
- buttonsContainer.style.display = 'none';
- const cancelButton = document.getElementById(`${cid}-reset-button`);
- const submitButton = document.getElementById(`${cid}-submit-button`);
-
- const {
- subminds_per_cid: submindsPerCID,
- connected_subminds: connectedSubminds
- } = submindsState;
-
- const activeSubminds = submindsPerCID?.[cid]?.filter(submind => submind.status === 'active') || [];
- const activeSubmindServices = new Set(activeSubminds.map(submind => submind.submind_id.slice(0, submind.submind_id.lastIndexOf('-'))))
-
- const banned_subminds = submindsPerCID?.[cid]?.filter(submind => submind.status === 'banned') || [];
- const bannedSubmindIds = new Set(banned_subminds.map(submind => submind.submind_id));
-
- const initialSubmindsState = [];
- const processedServiceNames = [];
- for (let [submindID, submindData] of Object.entries(connectedSubminds || {})) {
- const serviceName = submindData.service_name;
- const botType = submindData.bot_type;
- if (botType === "submind" && !bannedSubmindIds.has(submindID) && !processedServiceNames.includes(serviceName)) {
- processedServiceNames.push(serviceName)
- initialSubmindsState.push({
- service_name: serviceName,
- is_active: activeSubmindServices.has(serviceName)
- })
- }
- }
- initialSubmindsState.sort((a, b) => {
- return b.is_active - a.is_active;
- })
-
- let currentState = structuredClone(initialSubmindsState);
-
- const updateButtonVisibility = () => {
- const hasChanges = initialSubmindsState.some((submind, index) => submind.is_active !== currentState[index].is_active);
- buttonsContainer.style.display = hasChanges ? 'block' : 'none';
- };
-
- table.style.display = '';
- entriesContainer.innerHTML = '';
-
- initialSubmindsState.forEach((submind, index) => {
- const row = document.createElement('tr');
- row.innerHTML = `
-${submind.service_name} |
-
-
-
-
-
- |
-`;
-
- const checkbox = row.querySelector(`#toggle-${cid}-${submind.service_name}`);
- checkbox.addEventListener('change', () => {
- currentState[index].is_active = checkbox.checked;
- updateButtonVisibility();
- });
- entriesContainer.appendChild(row);
- });
-
- cancelButton.onclick = () => {
- currentState = structuredClone(initialSubmindsState);
- currentState.forEach((submind, index) => {
- const checkbox = document.getElementById(`toggle-${cid}-${submind.service_name}`);
- checkbox.checked = (submind.is_active) ? "checked" : '';
- });
- updateButtonVisibility();
- };
-
- submitButton.onclick = () => {
- const modifiedSubminds = currentState.filter((current, index) => {
- return current.is_active !== initialSubmindsState[index].is_active;
- });
-
- let subminds_to_remove = modifiedSubminds.filter(submind => !submind.is_active).map(submind => submind.service_name);
- let subminds_to_add = modifiedSubminds.filter(submind => submind.is_active).map(submind => submind.service_name);
-
- if (subminds_to_add.length !== 0 || subminds_to_remove.length !== 0) {
- socket.emit('broadcast', {
- msg_type: "update_participating_subminds",
- "cid": cid,
- "subminds_to_invite": subminds_to_add,
- "subminds_to_kick": subminds_to_remove,
- });
- }
-
- const dropdownToggle = document.getElementById(`dropdownToggle-${cid}`);
- if (dropdownToggle) dropdownToggle.click();
-
- buttonsContainer.style.display = 'none';
- };
-}
-
-
-function parseSubmindsState(data) {
- submindsState = data;
-
- const cids = Object.keys(submindsState["subminds_per_cid"])
- if (cids.length === 0) {
- setAllCountersToZero();
- } else {
- for (const cid of cids) {
- refreshSubmindsCount(cid);
- }
- }
-}
-let currentUserNavDisplay = document.getElementById('currentUserNavDisplay');
-/* Login items */
-let loginModal;
-let loginButton;
-let loginUsername;
-let loginPassword;
-let toggleSignup;
-/* Logout Items */
-let logoutModal;
-let logoutConfirm;
-/* Signup items */
-let signupModal;
-let signupButton;
-let signupUsername;
-let signupFirstName;
-let signupLastName;
-let signupPassword;
-let repeatSignupPassword;
-let toggleLogin;
-
-let currentUser = null;
-
-
-function initModalElements() {
- currentUserNavDisplay = document.getElementById('currentUserNavDisplay');
- logoutModal = $('#logoutModal');
- logoutConfirm = document.getElementById('logoutConfirm');
- loginModal = $('#loginModal');
- loginButton = document.getElementById('loginButton');
- loginUsername = document.getElementById('loginUsername');
- loginPassword = document.getElementById('loginPassword');
- toggleSignup = document.getElementById('toggleSignup');
- signupModal = $('#signupModal');
- signupButton = document.getElementById('signupButton');
- signupUsername = document.getElementById('signupUsername');
- signupFirstName = document.getElementById('signupFirstName');
- signupLastName = document.getElementById('signupLastName');
- signupPassword = document.getElementById('signupPassword');
- repeatSignupPassword = document.getElementById('repeatSignupPassword');
- toggleLogin = document.getElementById('toggleLogin');
-}
-
-
-const MODAL_NAMES = {
- LOGIN: 'login',
- LOGOUT: 'logout',
- SIGN_UP: 'signup',
- USER_SETTINGS: 'user_settings'
-}
-
-
-/**
- * Adds new modal under specific conversation id
- * @param name: name of the modal from MODAL_NAMES to add
- */
-async function addModal(name) {
- if (Object.values(MODAL_NAMES).includes(name)) {
- return await buildHTMLFromTemplate(`modals.${name}`)
- } else {
- console.warn(`Unresolved modal name - ${name}`)
- }
-}
-
-/**
- * Initializes modals per target conversation id (if not provided - for main client)
- * @param parentID: id of the parent to attach element to
- */
-async function initModals(parentID = null) {
- if (parentID) {
- const parentElem = document.getElementById(parentID);
- if (!parentElem) {
- console.warn('No element detected with provided parentID=', parentID)
- return -1;
- }
- for (const modalName of [
- MODAL_NAMES.LOGIN,
- MODAL_NAMES.LOGOUT,
- MODAL_NAMES.SIGN_UP,
- MODAL_NAMES.USER_SETTINGS
- ]) {
- const modalHTML = await addModal(modalName);
- parentElem.insertAdjacentHTML('beforeend', modalHTML);
- }
- }
- initModalElements();
- logoutConfirm.addEventListener('click', (e) => {
- e.preventDefault();
- logoutUser().catch(err => console.error('Error while logging out user: ', err));
- });
- toggleLogin.addEventListener('click', (e) => {
- e.preventDefault();
- signupModal.modal('hide');
- loginModal.modal('show');
- });
- loginButton.addEventListener('click', (e) => {
- e.preventDefault();
- loginUser().catch(err => console.error('Error while logging in user: ', err));
- });
- toggleSignup.addEventListener('click', (e) => {
- e.preventDefault();
- loginModal.modal('hide');
- signupModal.modal('show');
- });
- signupButton.addEventListener('click', (e) => {
- e.preventDefault();
- createUser().catch(err => console.error('Error while creating a user: ', err));
- });
- const modalsLoaded = new CustomEvent('modalsLoaded');
- document.dispatchEvent(modalsLoaded);
-}
-
-
-const USER_DATA_CACHE = {}
-const USER_DATA_CACHE_EXPIRY_SECONDS = 3600;
-
-/**
- * Gets user data from local cache
- * @param userID - id of the user to look-up (lookups authorized user if null)
- * @returns {Promise<{}>} promise resolving obtaining of user data
- */
-const getUserDataFromCache = (userID) => {
- if (USER_DATA_CACHE?.[userID]?.data) {
- if (getCurrentTimestamp() - USER_DATA_CACHE[userID].ts < USER_DATA_CACHE_EXPIRY_SECONDS) {
- return USER_DATA_CACHE[userID].data;
- }
- }
-}
-
-/**
- * Gets user data from chat client URL
- * @param userID - id of the user to look-up (lookups authorized user if null)
- * @returns {Promise<{}>} promise resolving obtaining of user data
- */
-async function getUserData(userID = null) {
- let userData = {}
- let query_url = `users_api/`;
- if (userID) {
- const cachedUserData = getUserDataFromCache(userID);
- if (cachedUserData) {
- return cachedUserData;
- }
- query_url += '?user_id=' + userID;
- }
- await fetchServer(query_url)
- .then(response => response.ok ? response.json() : {
- 'data': {}
- })
- .then(data => {
- userData = data['data'];
- const oldToken = getSessionToken();
- if (data['token'] !== oldToken && !userID) {
- setSessionToken(data['token']);
- }
- USER_DATA_CACHE[userID] = {
- data: userData,
- ts: getCurrentTimestamp()
- }
- });
- return userData;
-}
-
-/**
- * Method that handles fetching provided user data with valid login credentials
- * @returns {Promise} promise resolving validity of user-entered data
- */
-async function loginUser() {
- const loginModalBody = document.getElementById('loginModalBody');
- const query_url = `auth/login/`;
- const formData = new FormData();
- const inputValues = [loginUsername.value, loginPassword.value];
- if (inputValues.includes("") || inputValues.includes(null)) {
- displayAlert(loginModalBody, 'Required fields are blank', 'danger');
- } else {
- formData.append('username', loginUsername.value);
- formData.append('password', loginPassword.value);
- await fetchServer(query_url, REQUEST_METHODS.POST, formData)
- .then(async response => {
- return {
- 'ok': response.ok,
- 'data': await response.json()
- };
- })
- .then(async responseData => {
- if (responseData['ok']) {
- setSessionToken(responseData['data']['token']);
- } else {
- displayAlert(loginModalBody, responseData['data']['msg'], 'danger', 'login-failed-alert');
- loginPassword.value = "";
- }
- }).catch(ex => {
- console.warn(`Exception during loginUser -> ${ex}`);
- displayAlert(loginModalBody);
- });
- }
-}
-
-/**
- * Method that handles logging user out
- * @returns {Promise} promise resolving user logout
- */
-async function logoutUser() {
- const query_url = `auth/logout/`;
- await fetchServer(query_url).then(async response => {
- if (response.ok) {
- const responseJson = await response.json();
- setSessionToken(responseJson['token']);
- }
- });
-}
-
-/**
- * Method that handles fetching provided user data with valid sign up credentials
- * @returns {Promise} promise resolving validity of new user creation
- */
-async function createUser() {
- const signupModalBody = document.getElementById('signupModalBody');
- const query_url = `auth/signup/`;
- const formData = new FormData();
- const inputValues = [signupUsername.value, signupFirstName.value, signupLastName.value, signupPassword.value, repeatSignupPassword.value];
- if (inputValues.includes("") || inputValues.includes(null)) {
- displayAlert(signupModalBody, 'Required fields are blank', 'danger');
- } else if (signupPassword.value !== repeatSignupPassword.value) {
- displayAlert(signupModalBody, 'Passwords do not match', 'danger');
- } else {
- formData.append('nickname', signupUsername.value);
- formData.append('first_name', signupFirstName.value);
- formData.append('last_name', signupLastName.value);
- formData.append('password', signupPassword.value);
- await fetchServer(query_url, REQUEST_METHODS.POST, formData)
- .then(async response => {
- return {
- 'ok': response.ok,
- 'data': await response.json()
- }
- })
- .then(async data => {
- if (data['ok']) {
- setSessionToken(data['data']['token']);
- } else {
- let errorMessage = 'Failed to create an account';
- if (data['data'].hasOwnProperty('msg')) {
- errorMessage = data['data']['msg'];
- }
- displayAlert(signupModalBody, errorMessage, 'danger');
- }
- });
- }
-}
-
-/**
- * Helper method for updating navbar based on current user property
- * @param forceUpdate to force updating of navbar (defaults to false)
- */
-function updateNavbar(forceUpdate = false) {
- if (currentUser || forceUpdate) {
- let innerText = shrinkToFit(currentUser['nickname'], 10);
- let targetElems = [currentUserNavDisplay];
- if (configData.client === CLIENTS.MAIN) {
- if (currentUser['is_tmp']) {
- // Leaving only "guest" without suffix
- innerText = innerText.split('_')[0]
- innerText += ', Login';
- } else {
- innerText += ', Logout';
- }
- } else if (configData.client === CLIENTS.NANO) {
- if (currentUser['is_tmp']) {
- // Leaving only "guest" without suffix
- innerText = innerText.split('_')[0]
- innerText += ' ';
- } else {
- innerText += ' ';
- }
- targetElems = Array.from(document.getElementsByClassName('account-link'))
- }
- if (targetElems.length > 0 && targetElems[0]) {
- targetElems.forEach(elem => {
- elem.innerHTML = `
-${innerText}
-`;
- });
- }
- }
-}
-
-
/**
- * Refreshes HTML components appearance based on the current user
- * NOTE: this must have only visual impact, the actual validation is done on the backend
- */
-const refreshComponentsAppearance = () => {
- const currentUserRoles = currentUser?.roles ?? [];
- const isAdmin = currentUserRoles.includes("admin");
-
- const createLiveConversationWrapper = document.getElementById("createLiveConversationWrapper");
-
- if (isAdmin) {
- createLiveConversationWrapper.style.display = "";
- } else {
- createLiveConversationWrapper.style.display = "none";
- }
-}
-
-/**
- * Custom Event fired on current user loaded
- * @type {CustomEvent}
- */
-const currentUserLoaded = new CustomEvent("currentUserLoaded", {
- "detail": "Event that is fired when current user is loaded"
-});
-
-/**
- * Convenience method encapsulating refreshing page view based on current user
- * @param refreshChats: to refresh the chats (defaults to false)
- * @param conversationContainer: DOM Element representing conversation container
+ * Returns DOM container for message elements under specific conversation id
+ * @param cid: conversation id to consider
+ * @return {Element} DOM container for message elements of considered conversation
*/
-async function refreshCurrentUser(refreshChats = false, conversationContainer = null) {
- await getUserData().then(data => {
- currentUser = data;
- console.log(`Loaded current user = ${JSON.stringify(currentUser)}`);
- setTimeout(() => updateNavbar(), 500);
- if (refreshChats) {
- refreshChatView(conversationContainer);
- }
- refreshComponentsAppearance()
- console.log('current user loaded');
- document.dispatchEvent(currentUserLoaded);
- return data;
- });
+const getMessageListContainer = (cid) => {
+ const cidElem = document.getElementById(cid);
+ if (cidElem) {
+ return cidElem.getElementsByClassName('card-body')[0].getElementsByClassName('chat-list')[0]
+ }
}
-
-
-document.addEventListener('DOMContentLoaded', async (e) => {
- if (configData['client'] === CLIENTS.MAIN) {
- await initModals();
- currentUserNavDisplay.addEventListener('click', (e) => {
- e.preventDefault();
- currentUser['is_tmp'] ? loginModal.modal('show') : logoutModal.modal('show');
- });
- }
-});
/**
- * Returns preferred language specified in provided cid
- * @param cid: provided conversation id
- * @param inputType: type of the language preference to fetch:
- * "incoming" - for external shouts, "outcoming" - for emitted shouts
- *
- * @return preferred lang by cid or "en"
+ * Gets message node from the message container
+ * @param messageContainer: DOM Message Container element to consider
+ * @param validateType: type of message to validate
+ * @return {HTMLElement} ID of the message
*/
-function getPreferredLanguage(cid, inputType = 'incoming') {
- let preferredLang = 'en';
- try {
- preferredLang = getChatLanguageMapping(cid, inputType);
- } catch (e) {
- console.warn(`Failed to getChatLanguageMapping - ${e}`)
+const getMessageNode = (messageContainer, validateType = null) => {
+ let detectedType;
+ let node
+ if (messageContainer.getElementsByTagName('table').length > 0) {
+ detectedType = 'prompt';
+ node = messageContainer.getElementsByTagName('table')[0];
+ } else {
+ detectedType = 'plain'
+ node = messageContainer.getElementsByClassName('chat-body')[0].getElementsByClassName('chat-message')[0];
+ }
+ if (validateType && validateType !== detectedType) {
+ return null;
+ } else {
+ return node;
}
- return preferredLang;
}
/**
- * Returns preferred language specified in provided cid
- * @param cid: provided conversation id
- * @param lang: new preferred language to set
- * @param inputType: type of the language preference to fetch:
- * @param updateDB: to update user preferences in database
- * @param updateDBOnly: to update user preferences in database only (without translation request)
- * "incoming" - for external shouts, "outcoming" - for emitted shouts
+ * Adds new message to desired conversation id
+ * @param cid: desired conversation id
+ * @param userID: message sender id
+ * @param messageID: id of sent message (gets generated if null)
+ * @param messageText: text of the message
+ * @param timeCreated: timestamp for message creation
+ * @param repliedMessageID: id of the replied message (optional)
+ * @param attachments: array of attachments to add (optional)
+ * @param isAudio: is audio message (defaults to '0')
+ * @param isAnnouncement: is message an announcement (defaults to "0")
+ * @returns {Promise}: promise resolving id of added message, -1 if failed to resolve message id creation
*/
-async function setPreferredLanguage(cid, lang, inputType = 'incoming', updateDB = true, updateDBOnly = false) {
- let isOk = false;
- if (updateDB) {
- const formData = new FormData();
- formData.append('lang', lang);
- isOk = await fetchServer(`preferences/update_language/${cid}/${inputType}`, REQUEST_METHODS.POST, formData)
- .then(res => {
- return res.ok;
- });
- }
- if ((isOk || !updateDB) && !updateDBOnly) {
- updateChatLanguageMapping(cid, inputType, lang);
- const shoutIds = getMessagesOfCID(cid, MESSAGE_REFER_TYPE.ALL, 'plain', true);
- await requestTranslation(cid, shoutIds, lang, inputType);
+async function addNewMessage(cid, userID = null, messageID = null, messageText, timeCreated, repliedMessageID = null, attachments = [], isAudio = '0', isAnnouncement = '0') {
+ const messageList = getMessageListContainer(cid);
+ if (messageList) {
+ let userData;
+ const isMine = userID === currentUser['_id'];
+ if (isMine) {
+ userData = currentUser;
+ } else {
+ userData = await getUserData(userID);
+ }
+ if (!messageID) {
+ messageID = generateUUID();
+ }
+ let messageHTML = await buildUserMessageHTML(userData, cid, messageID, messageText, timeCreated, isMine, isAudio, isAnnouncement);
+ const blankChat = messageList.getElementsByClassName('blank_chat');
+ if (blankChat.length > 0) {
+ messageList.removeChild(blankChat[0]);
+ }
+ messageList.insertAdjacentHTML('beforeend', messageHTML);
+ resolveMessageAttachments(cid, messageID, attachments);
+ resolveUserReply(messageID, repliedMessageID);
+ addProfileDisplay(userID, cid, messageID, 'plain');
+ return messageID;
}
}
+const PROMPT_STATES = {
+ 1: 'RESP',
+ 2: 'DISC',
+ 3: 'VOTE'
+}
+
/**
- * Fetches supported languages
+ * Returns HTML Element representing user row in prompt
+ * @param promptID: target prompt id
+ * @param userID: target user id
+ * @return {HTMLElement}: HTML Element containing user prompt data
*/
-async function fetchSupportedLanguages() {
- const query_url = `language_api/settings`;
- return await fetchServer(query_url)
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- console.log(`failed to fetch supported languages - ${response.statusText}`)
- throw response.statusText;
- }
- })
- .then(data => {
- configData['supportedLanguages'] = data['supported_languages'];
- console.info(`supported languages updated - ${JSON.stringify(configData['supportedLanguages'])}`)
- }).catch(err => console.warn('Failed to fulfill request due to error:', err));
+const getUserPromptTR = (promptID, userID) => {
+ return document.getElementById(`${promptID}_${userID}_prompt_row`);
}
/**
- * Sends request for updating target conversation(s) content to the desired language
- * @param cid: conversation id to bound request to
- * @param shouts: list of shout ids to bound request to
- * @param lang: language to apply (defaults to preferred language of each fetched conversation)
- * @param inputType: type of the language input to apply (incoming or outcoming)
- * @param translateToBaseLang: to translate provided items to the system base lang (based on preferred)
+ * Adds prompt message of specified user id
+ * @param cid: target conversation id
+ * @param userID: target submind user id
+ * @param messageText: message of submind
+ * @param promptId: target prompt id
+ * @param promptState: prompt state to consider
*/
-async function requestTranslation(cid = null, shouts = null, lang = null, inputType = 'incoming', translateToBaseLang = false) {
- let requestBody = {
- chat_mapping: {}
- };
- if (cid && isDisplayed(cid)) {
- lang = lang || getPreferredLanguage(cid, inputType);
- if (lang !== 'en' && getMessagesOfCID(cid, MESSAGE_REFER_TYPE.ALL, 'plain').length > 0) {
- setChatState(cid, 'updating', 'Applying New Language...');
- }
- if (shouts && !Array.isArray(shouts)) {
- shouts = [shouts];
- }
- if (!shouts && inputType) {
- shouts = getMessagesOfCID(cid, getMessageReferType(inputType), 'plain', true);
- if (shouts.length === 0) {
- console.log(`${cid} yet has no shouts matching type=${inputType}`);
- setChatState(cid, 'active');
- return
+async function addPromptMessage(cid, userID, messageText, promptId, promptState) {
+ const tableBody = document.getElementById(`${promptId}_tbody`);
+ if (await getCurrentSkin(cid) === CONVERSATION_SKINS.PROMPTS) {
+ try {
+ promptState = PROMPT_STATES[promptState].toLowerCase();
+ if (!getUserPromptTR(promptId, userID)) {
+ const userData = await getUserData(userID);
+ const newUserRow = await buildSubmindHTML(promptId, userID, userData, '', '', '');
+ tableBody.insertAdjacentHTML('beforeend', newUserRow);
}
- }
- setDefault(requestBody.chat_mapping, cid, {});
- requestBody.chat_mapping[cid] = {
- 'lang': lang,
- 'shouts': shouts || []
- }
- if (translateToBaseLang) {
- requestBody.chat_mapping[cid]['source_lang'] = getPreferredLanguage(cid);
- }
- } else {
- requestBody.chat_mapping = getChatLanguageMapping();
- if (!requestBody.chat_mapping) {
- console.log('Chat mapping is undefined - returning');
- return
+ try {
+ const messageElem = document.getElementById(`${promptId}_${userID}_${promptState}`);
+ messageElem.innerText = messageText;
+ } catch (e) {
+ console.warn(`Failed to add prompt message (${cid},${userID}, ${messageText}, ${promptId}, ${promptState}) - ${e}`)
+ }
+ } catch (e) {
+ console.info(`Skipping message of invalid prompt state - ${promptState}`);
}
}
- requestBody['user'] = currentUser['_id'];
- requestBody['inputType'] = inputType;
- console.debug(`requestBody = ${JSON.stringify(requestBody)}`);
- socket.emitAuthorized('request_translate', requestBody);
}
+
/**
- * Sets selected language to the target language selector
- * @param clickedItem: Language selector element clicked
- * @param cid: target conversation id
- * @param inputType: type of the language input to apply (incoming or outcoming)
+ * Returns first message id based on given element
+ * @param firstChild: DOM element of first message child
*/
-async function setSelectedLang(clickedItem, cid, inputType = "incoming") {
- const selectedLangNode = document.getElementById(`language-selected-${cid}-${inputType}`);
- const selectedLangList = document.getElementById(`language-list-${cid}-${inputType}`);
-
- // console.log('emitted lang update')
- const preferredLang = getPreferredLanguage(cid, inputType);
- const preferredLangProps = configData['supportedLanguages'][preferredLang];
- const newKey = clickedItem.getAttribute('data-lang');
- const newPreferredLangProps = configData['supportedLanguages'][newKey];
-
- const direction = inputType === 'incoming' ? 'down' : 'up';
- selectedLangNode.innerHTML = await buildHTMLFromTemplate('selected_lang', {
- 'key': newKey,
- 'name': newPreferredLangProps['name'],
- 'icon': newPreferredLangProps['icon'],
- 'direction': direction
- })
- if (preferredLangProps) {
- selectedLangList.getElementsByClassName('lang-container')[0].insertAdjacentHTML('beforeend', await buildLangOptionHTML(cid, preferredLang, preferredLangProps['name'], preferredLangProps['icon'], inputType));
- } else {
- console.warn(`"${preferredLang}" is set to be preferred but currently not supported`)
- }
- if (clickedItem.parentNode) {
- clickedItem.parentNode.removeChild(clickedItem);
- }
- console.log(`cid=${cid};new preferredLang=${newKey}, inputType=${inputType}`);
- await setPreferredLanguage(cid, newKey, inputType, true);
- const insertedNode = document.getElementById(getLangOptionID(cid, preferredLang, inputType));
- insertedNode.addEventListener('click', async (e) => {
- e.preventDefault();
- await setSelectedLang(insertedNode, cid, inputType);
- });
+function getFirstMessageFromCID(firstChild) {
+ if (firstChild.classList.contains('prompt-item')) {
+ const promptTable = firstChild.getElementsByTagName('table')[0];
+ const promptID = promptTable.id;
+ const promptTBody = promptTable.getElementsByTagName('tbody')[0];
+ let currentRecentMessage = null;
+ let currentOldestTS = null;
+ Array.from(promptTBody.getElementsByTagName('tr')).forEach(tr => {
+ const submindID = tr.getAttribute('data-submind-id');
+ ['resp', 'opinion', 'vote'].forEach(phase => {
+ const phaseElem = document.getElementById(`${promptID}_${submindID}_${phase}`);
+ if (phaseElem) {
+ let createdOn = phaseElem.getAttribute(`data-created-on`);
+ const messageID = phaseElem.getAttribute(`data-message-id`)
+ if (createdOn && messageID) {
+ createdOn = parseInt(createdOn);
+ if (!currentOldestTS || createdOn < currentOldestTS) {
+ currentOldestTS = createdOn;
+ currentRecentMessage = messageID;
+ }
+ }
+ }
+ });
+ });
+ return currentRecentMessage;
+ } else {
+ return getMessageNode(firstChild, 'plain')?.id;
+ }
}
/**
- * Initialize language selector for conversation
+ * Gets list of the next n-older messages
* @param cid: target conversation id
- * @param inputType: type of the language input to apply (incoming or outcoming)
+ * @param skin: target conversation skin
*/
-async function initLanguageSelector(cid, inputType = "incoming") {
- let preferredLang = getPreferredLanguage(cid, inputType);
- const supportedLanguages = configData['supportedLanguages'];
- if (!supportedLanguages.hasOwnProperty(preferredLang)) {
- preferredLang = 'en';
- }
- const selectedLangNode = document.getElementById(`language-selected-${cid}-${inputType}`);
- const langList = document.getElementById(`language-list-${cid}-${inputType}`);
- if (langList) {
- const langListContainer = langList.getElementsByClassName('lang-container')[0]
-
- if (langListContainer) {
- langListContainer.innerHTML = "";
- }
-
- // selectedLangNode.innerHTML = "";
- for (const [key, value] of Object.entries(supportedLanguages)) {
-
- if (key === preferredLang) {
- const direction = inputType === 'incoming' ? 'down' : 'up';
- selectedLangNode.innerHTML = await buildHTMLFromTemplate('selected_lang', {
- 'key': key,
- 'name': value['name'],
- 'icon': value['icon'],
- 'direction': direction
- })
- } else {
- langListContainer.insertAdjacentHTML('beforeend', await buildLangOptionHTML(cid, key, value['name'], value['icon'], inputType));
- const itemNode = document.getElementById(getLangOptionID(cid, key, inputType));
- itemNode.addEventListener('click', async (e) => {
- e.preventDefault();
- await setSelectedLang(itemNode, cid, inputType)
+async function addOldMessages(cid, skin = CONVERSATION_SKINS.BASE) {
+ const messageContainer = getMessageListContainer(cid);
+ if (messageContainer.children.length > 0) {
+ for (let i = 0; i < messageContainer.children.length; i++) {
+ const firstMessageItem = messageContainer.children[i];
+ const oldestMessageTS = await DBGateway.getInstance(DB_TABLES.CHAT_MESSAGES_PAGINATION).getItem(cid).then(res => res?.oldest_created_on || null);
+ if (oldestMessageTS) {
+ const numMessages = await getCurrentSkin(cid) === CONVERSATION_SKINS.PROMPTS ? 30 : 10;
+ await getConversationDataByInput(cid, skin, oldestMessageTS, numMessages).then(async conversationData => {
+ if (messageContainer) {
+ const userMessageList = getUserMessages(conversationData, null);
+ userMessageList.sort((a, b) => {
+ a['created_on'] - b['created_on'];
+ }).reverse();
+ for (const message of userMessageList) {
+ message['cid'] = cid;
+ if (!isDisplayed(getMessageID(message))) {
+ const messageHTML = await messageHTMLFromData(message, skin);
+ messageContainer.insertAdjacentHTML('afterbegin', messageHTML);
+ } else {
+ console.debug(`!!message_id=${message["message_id"]} is already displayed`)
+ }
+ }
+ await initMessages(conversationData, skin);
+ }
+ }).then(_ => {
+ firstMessageItem.scrollIntoView({
+ behavior: "smooth"
+ });
});
+ break;
+ } else {
+ console.warn(`NONE first message id detected for cid=${cid}`)
}
}
}
}
+
/**
- * Inits both incoming and outcoming language selectors
- * @param cid: target conversation id
+ * Returns message id based on message type
+ * @param message: message object to check
+ * @returns {null|*} message id extracted if valid message type detected
*/
-const initLanguageSelectors = async (cid) => {
- for (const inputType of ['incoming', 'outcoming']) {
- await initLanguageSelector(cid, inputType);
+const getMessageID = (message) => {
+ switch (message['message_type']) {
+ case 'plain':
+ return message['message_id'];
+ case 'prompt':
+ return message['_id'];
+ default:
+ console.warn(`Invalid message structure received - ${message}`);
+ return null;
}
}
-
-function getMessageReferType(inputType) {
- return inputType === 'incoming' ? MESSAGE_REFER_TYPE.OTHERS : MESSAGE_REFER_TYPE.MINE;
+/**
+ * Array of user messages in given conversation
+ * @param conversationData: Conversation Data object to fetch
+ * @param forceType: to force particular type of messages among the chat flow
+ */
+const getUserMessages = (conversationData, forceType = 'plain') => {
+ try {
+ let messages = Array.from(conversationData['chat_flow']);
+ if (forceType) {
+ messages = messages.filter(message => message['message_type'] === forceType);
+ }
+ return messages;
+ } catch {
+ return [];
+ }
}
-
/**
- * Sends request to server for chat language refreshing
+ * Initializes listener for loading old message on scrolling conversation box
+ * @param conversationData: Conversation Data object to fetch
+ * @param skin: conversation skin to apply
*/
-async function requestChatsLanguageRefresh() {
- const languageMapping = currentUser?.preferences?.chat_language_mapping || {};
- console.log(`languageMapping=${JSON.stringify(languageMapping)}`)
- for (const [cid, value] of Object.entries(languageMapping)) {
- if (isDisplayed(cid)) {
+function initLoadOldMessages(conversationData, skin) {
+ const cid = conversationData['_id'];
+ const messageList = getMessageListContainer(cid);
+ const messageListParent = messageList.parentElement;
+ setDefault(setDefault(conversationState, cid, {}), 'lastScrollY', 0);
+ messageListParent.addEventListener("scroll", async (e) => {
+ const oldScrollPosition = conversationState[cid]['scrollY'];
+ conversationState[cid]['scrollY'] = e.target.scrollTop;
+ if (oldScrollPosition > conversationState[cid]['scrollY'] &&
+ !conversationState[cid]['all_messages_displayed'] &&
+ conversationState[cid]['scrollY'] === 0) {
+ setChatState(cid, 'updating', 'Loading messages...')
+ await addOldMessages(cid, skin);
for (const inputType of ['incoming', 'outcoming']) {
- const lang = value[inputType] || 'en';
- if (lang !== 'en') {
- await setPreferredLanguage(cid, lang, inputType, false);
- }
+ await requestTranslation(cid, null, null, inputType);
}
+ setTimeout(() => {
+ setChatState(cid, 'active');
+ }, 700);
}
- }
- console.log(`chatLanguageMapping=${JSON.stringify(getChatLanguageMapping())}`)
+ });
}
/**
- * Applies translation based on received data
- * @param data: translation object received
- * Note: data should be of format:
- * {
- * 'cid': {'message1':'translation of message 1',
- * 'message2':'translation of message 2'}
- * }
+ * Attaches event listener to display element's target user profile
+ * @param userID target user id
+ * @param elem target DOM element
*/
-async function applyTranslations(data) {
- const inputType = setDefault(data, 'input_type', 'incoming');
- for (const [cid, messageTranslations] of Object.entries(data['translations'])) {
-
- if (!isDisplayed(cid)) {
- console.log(`cid=${cid} is not displayed, skipping translations population`)
- continue;
- }
+function attachTargetProfileDisplay(userID, elem) {
+ if (elem) {
+ elem.addEventListener('click', async (_) => {
+ if (userID) await showProfileModal(userID)
+ });
+ }
+}
- setChatState(cid, 'active');
+/**
+ * Adds callback for showing profile information on profile avatar click
+ * @param userID target user id
+ * @param cid target conversation id
+ * @param messageId target message id
+ * @param messageType type of message to display
+ */
+function addProfileDisplay(userID, cid, messageId, messageType = 'plain') {
+ if (messageType === 'plain') {
+ attachTargetProfileDisplay(userID, document.getElementById(`${messageId}_avatar`))
+ } else if (messageType === 'prompt') {
+ const promptTBody = document.getElementById(`${messageId}_tbody`);
+ const rows = promptTBody.getElementsByTagName('tr');
+ Array.from(rows).forEach(row => {
+ attachTargetProfileDisplay(userID, Array.from(row.getElementsByTagName('td'))[0].getElementsByClassName('chat-img')[0]);
+ })
+ }
+}
- console.debug(`Fetching translation of ${cid}`);
- // console.debug(`translations=${JSON.stringify(messageTranslations)}`)
- const messageTranslationsShouts = messageTranslations['shouts'];
- if (messageTranslationsShouts) {
- const messageReferType = getMessageReferType(inputType);
- const messages = getMessagesOfCID(cid, messageReferType, 'plain');
- Array.from(messages).forEach(message => {
- const messageID = message.id;
- let repliedMessage = null;
- let repliedMessageID = null;
- try {
- repliedMessage = message.getElementsByClassName('reply-placeholder')[0].getElementsByClassName('reply-text')[0];
- repliedMessageID = repliedMessage.getAttribute('data-replied-id')
- // console.debug(`repliedMessageID=${repliedMessageID}`)
- } catch (e) {
- // console.debug(`replied message not found for ${messageID}`);
- }
- if (messageID in messageTranslationsShouts) {
- message.getElementsByClassName('message-text')[0].innerHTML = messageTranslationsShouts[messageID];
- }
- if (repliedMessageID && repliedMessageID in messageTranslationsShouts) {
- repliedMessage.innerHTML = messageTranslationsShouts[repliedMessageID];
- }
- });
- await initLanguageSelector(cid, inputType);
- }
- }
+/**
+ * Inits addProfileDisplay() on each message of provided conversation
+ * @param conversationData - target conversation data
+ */
+function initProfileDisplay(conversationData) {
+ getUserMessages(conversationData, null).forEach(message => {
+ addProfileDisplay(message['user_id'], conversationData['_id'], getMessageID(message), message['message_type']);
+ });
}
-const getChatLanguageMapping = (cid = null, inputType = null) => {
- let res = setDefault(setDefault(currentUser, 'preferences', {}), 'chat_language_mapping', {});
- if (cid) {
- res = setDefault(res, cid, {});
- }
- if (inputType) {
- res = setDefault(res, inputType, 'en');
+/**
+ * Inits pagination based on the oldest message creation timestamp
+ * @param conversationData - target conversation data
+ */
+async function initPagination(conversationData) {
+ const userMessages = getUserMessages(conversationData, null);
+ if (userMessages.length > 0) {
+ const oldestMessage = Math.min(...userMessages.map(msg => parseInt(msg.created_on)));
+ await DBGateway
+ .getInstance(DB_TABLES.CHAT_MESSAGES_PAGINATION)
+ .putItem({
+ cid: conversationData['_id'],
+ oldest_created_on: oldestMessage
+ })
}
- return res;
}
-const updateChatLanguageMapping = (cid, inputType, lang) => {
- setDefault(currentUser.preferences.chat_language_mapping, cid, {})[inputType] = lang;
- console.log(`cid=${cid},inputType=${inputType} updated to lang=${lang}`);
+
+/**
+ * Initializes messages based on provided conversation aata
+ * @param conversationData - JS Object containing conversation data of type:
+ * {
+ * '_id': 'id of conversation',
+ * 'conversation_name': 'title of the conversation',
+ * 'chat_flow': [{
+ * 'user_nickname': 'nickname of sender',
+ * 'user_avatar': 'avatar of sender',
+ * 'message_id': 'id of the message',
+ * 'message_text': 'text of the message',
+ * 'is_audio': true if message is an audio message
+ * 'is_announcement': true if message is considered to be an announcement
+ * 'created_on': 'creation time of the message'
+ * }, ... (num of user messages returned)]
+ * }
+ * @param skin - target conversation skin to consider
+ */
+async function initMessages(conversationData, skin) {
+ initProfileDisplay(conversationData);
+ attachReplies(conversationData);
+ addAttachments(conversationData);
+ addCommunicationChannelTransformCallback(conversationData);
+ initLoadOldMessages(conversationData, skin);
+ await initPagination(conversationData);
}
/**
- * Custom Event fired on supported languages init
- * @type {CustomEvent}
+ * Emits user message to Socket IO Server
+ * @param textInputElem: DOM Element with input text (audio object if isAudio=true)
+ * @param cid: Conversation ID
+ * @param repliedMessageID: ID of replied message
+ * @param attachments: list of attachments file names
+ * @param isAudio: is audio message being emitted (defaults to '0')
+ * @param isAnnouncement: is message an announcement (defaults to '0')
*/
-const supportedLanguagesLoadedEvent = new CustomEvent("supportedLanguagesLoaded", {
- "detail": "Event that is fired when system supported languages are loaded"
-});
-
-document.addEventListener('DOMContentLoaded', (_) => {
- document.addEventListener('configLoaded', async (_) => {
- await fetchSupportedLanguages().then(_ => document.dispatchEvent(supportedLanguagesLoadedEvent));
- });
-});
+function emitUserMessage(textInputElem, cid, repliedMessageID = null, attachments = [], isAudio = '0', isAnnouncement = '0') {
+ if (isAudio === '1' || textInputElem && textInputElem.value) {
+ const timeCreated = getCurrentTimestamp();
+ let messageText;
+ if (isAudio === '1') {
+ messageText = textInputElem;
+ } else {
+ messageText = textInputElem.value;
+ }
+ addNewMessage(cid, currentUser['_id'], null, messageText, timeCreated, repliedMessageID, attachments, isAudio, isAnnouncement).then(async messageID => {
+ const preferredShoutLang = getPreferredLanguage(cid, 'outcoming');
+ socket.emitAuthorized('user_message', {
+ 'cid': cid,
+ 'userID': currentUser['_id'],
+ 'messageText': messageText,
+ 'messageID': messageID,
+ 'lang': preferredShoutLang,
+ 'attachments': attachments,
+ 'isAudio': isAudio,
+ 'isAnnouncement': isAnnouncement,
+ 'timeCreated': timeCreated
+ });
+ if (preferredShoutLang !== 'en') {
+ await requestTranslation(cid, messageID, 'en', 'outcoming', true);
+ }
+ addMessageTransformCallback(cid, messageID, isAudio);
+ });
+ if (isAudio === '0') {
+ textInputElem.value = "";
+ }
+ }
+}
/**
* Generic function to play base64 audio file (currently only .wav format is supported)
* @param audio_data: base64 encoded audio data
@@ -1832,81 +1739,6 @@ async function addRecorder(conversationData) {
};
}
}
-/**
- * Gets time object from provided UNIX timestamp
- * @param timestampCreated: UNIX timestamp (in seconds)
- * @returns {string} string time (hours:minutes)
- */
-function getTimeFromTimestamp(timestampCreated = 0) {
- if (!timestampCreated) {
- return ''
- }
- let date = new Date(timestampCreated * 1000);
- let year = date.getFullYear().toString();
- let month = date.getMonth() + 1;
- month = month >= 10 ? month.toString() : '0' + month.toString();
- let day = date.getDate();
-
- day = day >= 10 ? day.toString() : '0' + day.toString();
- const hours = date.getHours().toString();
- let minutes = date.getMinutes();
- minutes = minutes >= 10 ? minutes.toString() : '0' + minutes.toString();
- return strFmtDate(year, month, day, hours, minutes, null);
-}
-
-/**
- * Composes date based on input params
- * @param year: desired year
- * @param month: desired month
- * @param day: desired day
- * @param hours: num of hours
- * @param minutes: minutes
- * @param seconds: seconds
- * @return date string
- */
-function strFmtDate(year, month, day, hours, minutes, seconds) {
- let finalDate = "";
- if (year && month && day) {
- finalDate += `${year}-${month}-${day}`
- }
- if (hours && minutes) {
- finalDate += ` ${hours}:${minutes}`
- if (seconds) {
- finalDate += `:${seconds}`
- }
- }
- return finalDate;
-}
-/**
- * Downloads desired content
- * @param content: content to download
- * @param filename: name of the file to download
- * @param contentType: type of the content
- */
-function download(content, filename, contentType = 'application/octet-stream') {
- if (content) {
- const a = document.createElement('a');
- const blob = new Blob([content], {
- 'type': contentType
- });
- a.href = window.URL.createObjectURL(blob);
- a.target = 'blank';
- a.download = filename;
- a.click();
- window.URL.revokeObjectURL(content);
- } else {
- console.warn('Skipping downloading as content is invalid')
- }
-}
-
-/**
- * Handles error while loading the image data
- * @param image: target image Node
- */
-function handleImgError(image) {
- image.parentElement.insertAdjacentHTML('afterbegin', `${image.getAttribute('alt')}
`);
- image.parentElement.removeChild(image);
-}
/**
* Renders suggestions HTML
*/
@@ -1938,1901 +1770,2026 @@ async function renderSuggestions() {
importConversationModalSuggestions.style.setProperty('display', 'inherit', 'important');
});
}
-/**
- * Object representing loaded HTML components mapping:
- * - key: component name,
- * - value: HTML template that should be populated with actual data)
- * @type Object
- */
-let loadedComponents = {}
+let submindsState;
-/**
- * Fetches template context into provided html template
- * @param html: HTML template
- * @param templateContext: object containing context to fetch
- * @return {string} HTML with fetched context
- */
-function fetchTemplateContext(html, templateContext) {
- for (const [key, value] of Object.entries(templateContext)) {
- html = html.replaceAll('{' + key + '}', value);
+function renderActiveSubminds(cid) {
+ if (!submindsState) {
+ console.log(`Subminds for CID ${cid} not yet loaded.`);
+ return;
}
- return html;
-}
+ const loadingSpinner = document.getElementById(`${cid}-subminds-state-loading`);
+ if (loadingSpinner) {
+ loadingSpinner.classList.remove('d-flex');
+ loadingSpinner.style.display = 'none';
+ }
+
+ const dropdownMenu = document.getElementById(`bot-list-${cid}`);
+ dropdownMenu.addEventListener('click', (event) => {
+ event.stopPropagation();
+ });
+
+ const table = document.getElementById(`${cid}-subminds-state-table`);
+ const entriesContainer = document.getElementById(`${cid}-subminds-state-entries`);
+ const buttonsContainer = document.getElementById(`${cid}-subminds-buttons`);
+ buttonsContainer.style.display = 'none';
+ const cancelButton = document.getElementById(`${cid}-reset-button`);
+ const submitButton = document.getElementById(`${cid}-submit-button`);
+
+ const {
+ subminds_per_cid: submindsPerCID,
+ connected_subminds: connectedSubminds
+ } = submindsState;
+
+ const activeSubminds = submindsPerCID?.[cid]?.filter(submind => submind.status === 'active') || [];
+ const activeSubmindServices = new Set(activeSubminds.map(submind => submind.submind_id.slice(0, submind.submind_id.lastIndexOf('-'))))
+
+ const banned_subminds = submindsPerCID?.[cid]?.filter(submind => submind.status === 'banned') || [];
+ const bannedSubmindIds = new Set(banned_subminds.map(submind => submind.submind_id));
+
+ const initialSubmindsState = [];
+ const processedServiceNames = [];
+ for (let [submindID, submindData] of Object.entries(connectedSubminds || {})) {
+ const serviceName = submindData.service_name;
+ const botType = submindData.bot_type;
+ if (botType === "submind" && !bannedSubmindIds.has(submindID) && !processedServiceNames.includes(serviceName)) {
+ processedServiceNames.push(serviceName)
+ initialSubmindsState.push({
+ service_name: serviceName,
+ is_active: activeSubmindServices.has(serviceName)
+ })
+ }
+ }
+ initialSubmindsState.sort((a, b) => {
+ return b.is_active - a.is_active;
+ })
+
+ let currentState = structuredClone(initialSubmindsState);
+
+ const updateButtonVisibility = () => {
+ const hasChanges = initialSubmindsState.some((submind, index) => submind.is_active !== currentState[index].is_active);
+ buttonsContainer.style.display = hasChanges ? 'block' : 'none';
+ };
+
+ table.style.display = '';
+ entriesContainer.innerHTML = '';
+
+ initialSubmindsState.forEach((submind, index) => {
+ const row = document.createElement('tr');
+ row.innerHTML = `
+${submind.service_name} |
+
+
+
+
+
+ |
+`;
+
+ const checkbox = row.querySelector(`#toggle-${cid}-${submind.service_name}`);
+ checkbox.addEventListener('change', () => {
+ currentState[index].is_active = checkbox.checked;
+ updateButtonVisibility();
+ });
+ entriesContainer.appendChild(row);
+ });
+
+ cancelButton.onclick = () => {
+ currentState = structuredClone(initialSubmindsState);
+ currentState.forEach((submind, index) => {
+ const checkbox = document.getElementById(`toggle-${cid}-${submind.service_name}`);
+ checkbox.checked = (submind.is_active) ? "checked" : '';
+ });
+ updateButtonVisibility();
+ };
+
+ submitButton.onclick = () => {
+ const modifiedSubminds = currentState.filter((current, index) => {
+ return current.is_active !== initialSubmindsState[index].is_active;
+ });
+
+ let subminds_to_remove = modifiedSubminds.filter(submind => !submind.is_active).map(submind => submind.service_name);
+ let subminds_to_add = modifiedSubminds.filter(submind => submind.is_active).map(submind => submind.service_name);
-/**
- * Builds HTML from passed params and template name
- * @param templateName: name of the template to fetch
- * @param templateContext: properties from template to fetch
- * @param requestArgs: request string arguments (optional)
- * @returns built template string
- */
-async function buildHTMLFromTemplate(templateName, templateContext = {}, requestArgs = '') {
- if (!configData['DISABLE_CACHING'] && loadedComponents.hasOwnProperty(templateName) && !requestArgs) {
- const html = loadedComponents[templateName];
- return fetchTemplateContext(html, templateContext);
- } else {
- return await fetch(`${configData['currentURLBase']}/components/${templateName}?${requestArgs}`)
- .then((response) => {
- if (response.ok) {
- return response.text();
- }
- throw `template unreachable (HTTP STATUS:${response.status}: ${response.statusText})`
- })
- .then((html) => {
- if (!(configData['DISABLE_CACHING'] || loadedComponents.hasOwnProperty(templateName) || requestArgs)) {
- loadedComponents[templateName] = html;
- }
- return fetchTemplateContext(html, templateContext);
- }).catch(err => console.warn(`Failed to fetch template for ${templateName}: ${err}`));
- }
-}
+ if (subminds_to_add.length !== 0 || subminds_to_remove.length !== 0) {
+ socket.emit('broadcast', {
+ msg_type: "update_participating_subminds",
+ "cid": cid,
+ "subminds_to_invite": subminds_to_add,
+ "subminds_to_kick": subminds_to_remove,
+ });
+ }
+ const dropdownToggle = document.getElementById(`dropdownToggle-${cid}`);
+ if (dropdownToggle) dropdownToggle.click();
-/**
- * Get Node id based on language key
- * @param cid: desired conversation id
- * @param key: language key (e.g. 'en')
- * @param inputType: type of the language input to apply (incoming or outcoming)
- * @return {string} ID of Node
- */
-function getLangOptionID(cid, key, inputType = 'incoming') {
- return `language-option-${cid}-${inputType}-${key}`;
+ buttonsContainer.style.display = 'none';
+ };
}
-/**
- * Build language selection HTML based on provided params
- * @param cid: desired conversation id
- * @param key: language key (e.g 'en')
- * @param name: name of the language (e.g. English)
- * @param icon: language icon (refers to flag-icon specs)
- * @param inputType: type of the language input to apply (incoming or outcoming)
- * @return {string} formatted langSelectPattern
- */
-async function buildLangOptionHTML(cid, key, name, icon, inputType) {
- return await buildHTMLFromTemplate('lang_option', {
- 'itemId': getLangOptionID(cid, key, inputType),
- 'key': key,
- 'name': name,
- 'icon': icon
- })
-}
-/**
- * Builds user message HTML
- * @param userData: data of message sender
- * @param cid: conversation id of target message
- * @param messageID: id of user message
- * @param messageText: text of user message
- * @param timeCreated: date of creation
- * @param isMine: if message was emitted by current user
- * @param isAudio: if message is audio message (defaults to '0')
- * @param isAnnouncement: is message if announcement (defaults to '0')
- * @returns {string}: constructed HTML out of input params
- */
-async function buildUserMessageHTML(userData, cid, messageID, messageText, timeCreated, isMine, isAudio = '0', isAnnouncement = '0') {
- const messageTime = getTimeFromTimestamp(timeCreated);
- let shortedNick = `${userData['nickname'][0]}${userData['nickname'][userData['nickname'].length - 1]}`;
- let imageComponent = `${shortedNick}
`;
- // if (userData.hasOwnProperty('avatar') && userData['avatar']){
- // imageComponent = `
`
- // }
- const messageClass = isAnnouncement === '1' ? 'announcement' : isMine ? 'in' : 'out';
- const messageOrientation = isMine ? 'right' : 'left';
- let minificationEnabled = currentUser?.preferences?.minify_messages === '1' || await getCurrentSkin(cid) === CONVERSATION_SKINS.PROMPTS;
- let templateSuffix = minificationEnabled ? '_minified' : '';
- const templateName = isAudio === '1' ? `user_message_audio${templateSuffix}` : `user_message${templateSuffix}`;
- if (isAudio === '0') {
- messageText = messageText.replaceAll('\n', '
');
- }
- let statusIconHTML = '';
- let userTooltip = userData['nickname'];
- if (userData?.is_bot === '1') {
- statusIconHTML = ' '
- userTooltip = `bot ${userTooltip}`
- }
- return await buildHTMLFromTemplate(templateName, {
- 'message_class': messageClass,
- 'is_announcement': isAnnouncement,
- 'image_component': imageComponent,
- 'message_id': messageID,
- 'user_tooltip': userTooltip,
- 'nickname': userData['nickname'],
- 'nickname_shrunk': shrinkToFit(userData['nickname'], 15, '..'),
- 'status_icon': statusIconHTML,
- 'message_text': messageText,
- 'message_orientation': messageOrientation,
- 'audio_url': `${configData["CHAT_SERVER_URL_BASE"]}/files/audio/${messageID}`,
- 'message_time': messageTime
- });
-}
+function parseSubmindsState(data) {
+ submindsState = data;
-/**
- *
- * @param nick: nickname to shorten
- * @return {string} - shortened nickname
- */
-const shrinkNickname = (nick) => {
- return `${nick[0]}${nick[nick.length - 1]}`;
+ const cids = Object.keys(submindsState["subminds_per_cid"])
+ if (cids.length === 0) {
+ setAllCountersToZero();
+ } else {
+ for (const cid of cids) {
+ refreshSubmindsCount(cid);
+ }
+ }
}
-
+let __inputFileList = {};
/**
- * Builds Prompt Skin HTML for submind responses
- * @param promptID: target prompt id
- * @param submindID: user id of submind
- * @param submindUserData: user data of submind
- * @param submindResponse: Responding data of submind to incoming prompt
- * @param submindOpinion: Discussion data of submind to incoming prompt
- * @param submindVote: Vote data of submind in prompt
- * @return {Promise} - Submind Data HTML populated with provided data
+ * Gets uploaded files from specified conversation id
+ * @param cid specified conversation id
+ * @return {*} list of files from specified cid if any
*/
-async function buildSubmindHTML(promptID, submindID, submindUserData, submindResponse, submindOpinion, submindVote) {
- const userNickname = shrinkNickname(submindUserData['nickname']);
- let tooltip = submindUserData['nickname'];
- if (submindUserData['is_bot']) {
- tooltip = `bot ${tooltip}`;
- }
- const phaseDataObjectMapping = {
- 'response': submindResponse,
- 'opinion': submindOpinion,
- 'vote': submindVote
- }
- let templateData = {
- 'prompt_id': promptID,
- 'user_id': submindID,
- 'user_first_name': submindUserData['first_name'],
- 'user_last_name': submindUserData['last_name'],
- 'user_nickname': submindUserData['nickname'],
- 'user_nickname_shrunk': userNickname,
- // 'user_avatar': `${configData["CHAT_SERVER_URL_BASE"]}/files/avatar/${submindID}`,
- 'tooltip': tooltip
- }
- const submindPromptData = {}
- for (const [k, v] of Object.entries(phaseDataObjectMapping)) {
- submindPromptData[k] = v.message_text
- submindPromptData[`${k}_message_id`] = v?.message_id
- const dateCreated = getTimeFromTimestamp(v?.created_on);
- submindPromptData[`${k}_created_on`] = v?.created_on;
- submindPromptData[`${k}_created_on_tooltip`] = dateCreated ? `shouted on: ${dateCreated}` : `no ${k} from ${userNickname} in this prompt`;
+function getUploadedFiles(cid) {
+ if (__inputFileList.hasOwnProperty(cid)) {
+ return __inputFileList[cid];
}
- return await buildHTMLFromTemplate("prompt_participant", Object.assign(templateData, submindPromptData));
+ return [];
}
-
/**
- * Gets winner text based on the provided winner data
- * @param winner: provided winner
- * @return {string} generated winner text
+ * Cleans uploaded files per conversation
*/
-const getPromptWinnerText = (winner) => {
- let res;
- if (winner) {
- res = `Selected winner "${winner}"`;
- } else {
- res = 'Consensus not reached';
+function cleanUploadedFiles(cid) {
+ if (__inputFileList.hasOwnProperty(cid)) {
+ delete __inputFileList[cid];
}
- return res;
+ const attachmentsButton = document.getElementById('file-input-' + cid);
+ attachmentsButton.value = "";
+ const fileContainer = document.getElementById('filename-container-' + cid);
+ fileContainer.innerHTML = "";
}
-
/**
- * Builds prompt HTML from received prompt data
- * @param prompt: prompt object
- * @return Prompt HTML
- */
-async function buildPromptHTML(prompt) {
- let submindsHTML = "";
- const promptData = prompt['data'];
- if (prompt['is_completed'] === '0') {
- promptData['winner'] = `Prompt in progress
-
-Loading...
-
`
- } else {
- promptData['winner'] = getPromptWinnerText(promptData['winner']);
- }
- const emptyAnswer = `-
`;
- for (const submindID of Array.from(setDefault(promptData, 'participating_subminds', []))) {
- let submindUserData;
- try {
- const searchedKeys = ['proposed_responses', 'submind_opinions', 'votes'];
- let isLegacy = false;
- try {
- submindUserData = prompt['user_mapping'][submindID][0];
- } catch (e) {
- console.warn('Detected legacy prompt structure');
- submindUserData = {
- 'nickname': submindID,
- 'first_name': 'Klat',
- 'last_name': 'User',
- 'is_bot': '0'
- }
- isLegacy = true
- }
- const data = {}
- searchedKeys.forEach(key => {
- try {
- const messageId = promptData[key][submindID];
- let value = null;
- if (!isLegacy) {
- value = prompt['message_mapping'][messageId][0];
- value['message_id'] = messageId;
- }
- if (!value) {
- value = {
- 'message_text': emptyAnswer
- }
- }
- data[key] = value;
- } catch (e) {
- data[key] = {
- 'message_text': emptyAnswer
- };
- }
- });
- submindsHTML += await buildSubmindHTML(prompt['_id'], submindID, submindUserData,
- data.proposed_responses, data.submind_opinions, data.votes);
- } catch (e) {
- console.log(`Malformed data for ${submindID} (prompt_id=${prompt['_id']}) ex=${e}`);
- }
+ * Adds File upload to specified cid
+ * @param cid: mentioned cid
+ * @param file: File object
+ */
+function addUpload(cid, file) {
+ if (!__inputFileList.hasOwnProperty(cid)) {
+ __inputFileList[cid] = [];
}
- return await buildHTMLFromTemplate("prompt_table", {
- 'prompt_text': promptData['prompt_text'],
- 'selected_winner': promptData['winner'],
- 'prompt_participants_data': submindsHTML,
- 'prompt_id': prompt['_id'],
- 'cid': prompt['cid'],
- 'message_time': prompt['created_on']
- });
+ __inputFileList[cid].push(file);
}
/**
- * Gets user message HTML from received message data object
- * @param message: Message Object received
- * @param skin: conversation skin
- * @return {Promise} HTML by the provided message data
+ * Adds download request on attachment item click
+ * @param attachmentItem: desired attachment item
+ * @param cid: current conversation id
+ * @param messageID: current message id
*/
-async function messageHTMLFromData(message, skin = CONVERSATION_SKINS.BASE) {
- if (skin === CONVERSATION_SKINS.PROMPTS && message['message_type'] === 'prompt') {
- return buildPromptHTML(message);
- } else {
- const isMine = currentUser && message['user_nickname'] === currentUser['nickname'];
- return buildUserMessageHTML({
- 'avatar': message['user_avatar'],
- 'nickname': message['user_nickname'],
- 'is_bot': message['user_is_bot'],
- '_id': message['user_id']
- },
- message['cid'],
- message['message_id'],
- message['message_text'],
- message['created_on'],
- isMine,
- message?.is_audio,
- message?.is_announcement);
+async function downloadAttachment(attachmentItem, cid, messageID) {
+ if (attachmentItem) {
+ const fileName = attachmentItem.getAttribute('data-file-name');
+ const mime = attachmentItem.getAttribute('data-mime');
+ const getFileURL = `files/${messageID}/get_attachment/${fileName}`;
+ await fetchServer(getFileURL).then(async response => {
+ response.ok ?
+ download(await response.blob(), fileName, mime) :
+ console.error(`No file data received for path,
+cid=${cid};\n
+message_id=${messageID};\n
+file_name=${fileName}`)
+ }).catch(err => console.error(`Failed to fetch: ${getFileURL}: ${err}`));
}
}
/**
- * Builds HTML for received conversation data
- * @param conversationData: JS Object containing conversation data of type:
- * {
- * '_id': 'id of conversation',
- * 'conversation_name': 'title of the conversation',
- * 'chat_flow': [{
- * 'user_nickname': 'nickname of sender',
- * 'user_avatar': 'avatar of sender',
- * 'message_id': 'id of the message',
- * 'message_text': 'text of the message',
- * 'created_on': 'creation time of the message'
- * }, ... (num of user messages returned)]
- * }
- * @param skin: conversation skin to build
- * @returns {string} conversation HTML based on provided data
+ * Attaches message replies to initialized conversation
+ * @param conversationData: conversation data object
*/
-async function buildConversationHTML(conversationData = {}, skin = CONVERSATION_SKINS.BASE) {
- const cid = conversationData['_id'];
- const conversation_name = conversationData['conversation_name'];
- let chatFlowHTML = "";
+function addAttachments(conversationData) {
if (conversationData.hasOwnProperty('chat_flow')) {
- for (const message of Array.from(conversationData['chat_flow'])) {
- message['cid'] = cid;
- chatFlowHTML += await messageHTMLFromData(message, skin);
- // if (skin === CONVERSATION_SKINS.BASE) {
- // }
- }
- } else {
- chatFlowHTML += `No messages in this chat yet...
`;
+ getUserMessages(conversationData).forEach(message => {
+ resolveMessageAttachments(conversationData['_id'], message['message_id'], message?.attachments);
+ });
}
- const conversationNameShrunk = shrinkToFit(conversation_name, 6);
- let nanoHeaderHTML = '';
- if (configData.client === CLIENTS.NANO) {
- nanoHeaderHTML = await buildHTMLFromTemplate('nano_header', {
- 'cid': cid
- })
+}
+
+/**
+ * Activates attachments event listeners for message attachments in specified conversation
+ * @param cid: desired conversation id
+ * @param elem: parent element for attachment (defaults to document)
+ */
+function activateAttachments(cid, elem = null) {
+ if (!elem) {
+ elem = document;
}
- return await buildHTMLFromTemplate('conversation', {
- 'cid': cid,
- 'nano_header': nanoHeaderHTML,
- 'conversation_name': conversation_name,
- 'conversation_name_shrunk': conversationNameShrunk,
- 'chat_flow': chatFlowHTML
- }, `skin=${skin}`);
+ Array.from(elem.getElementsByClassName('attachment-item')).forEach(attachmentItem => {
+ attachmentItem.addEventListener('click', async (e) => {
+ e.preventDefault();
+ const attachmentName = attachmentItem.getAttribute('data-file-name');
+ try {
+ setChatState(cid, 'updating', `Downloading attachment file`);
+ await downloadAttachment(attachmentItem, cid, attachmentItem.parentNode.parentNode.id);
+ } catch (e) {
+ console.warn(`Failed to download attachment file - ${attachmentName} (${e})`)
+ } finally {
+ setChatState(cid, 'active');
+ }
+ });
+ });
}
+
/**
- * Builds suggestion HTML
- * @param cid: target conversation id
- * @param name: target conversation name
- * @return {Promise} HTML with fetched data
+ * Returns DOM element to include as file resolver based on its name
+ * @param filename: name of file to fetch
+ * @return {string}: resulting DOM element
*/
-const buildSuggestionHTML = async (cid, name) => {
- return await buildHTMLFromTemplate('suggestion', {
- 'cid': cid,
- 'conversation_name': name
- })
-};
-document.addEventListener('configLoaded', async (_) => {
+function attachmentHTMLBasedOnFilename(filename) {
- const buildVersion = configData?.["BUILD_VERSION"];
- const buildTS = configData?.["BUILD_TS"];
- if (buildVersion && buildTS) {
- document.getElementById("app-version").innerText = `v${buildVersion} (${getTimeFromTimestamp(buildTS)})`;
+ let fSplitted = filename.split('.');
+ if (fSplitted.length > 1) {
+ const extension = fSplitted.pop();
+ const shrinkedName = shrinkToFit(filename, 12, `...${extension}`);
+ if (IMAGE_EXTENSIONS.includes(extension)) {
+ return ` ${shrinkedName}`;
+ } else {
+ return shrinkedName;
+ }
}
-});
+ return shrinkToFit(filename, 12);
+}
+
/**
- * Displays modal bounded to the provided conversation id
- * @param modalElem: modal to display
- * @param cid: conversation id to consider
+ * Resolves attachments to the message
+ * @param cid: id of conversation
+ * @param messageID: id of user message
+ * @param attachments list of attachments received
*/
-function displayModalInCID(modalElem, cid) {
- modalElem.modal('hide');
- $('.modal-backdrop').appendTo(`#${cid}`);
- modalElem.modal('show');
-}
-const DATABASES = {
- CHATS: 'chats'
-}
-const DB_TABLES = {
- CHAT_ALIGNMENT: 'chat_alignment',
- MINIFY_SETTINGS: 'minify_settings',
- CHAT_MESSAGES_PAGINATION: 'chat_messages_pagination'
-}
-const __db_instances = {}
-const __db_definitions = {
- [DATABASES.CHATS]: {
- [DB_TABLES.CHAT_ALIGNMENT]: `cid, added_on, skin`,
- [DB_TABLES.CHAT_MESSAGES_PAGINATION]: `cid, oldest_created_on`
+function resolveMessageAttachments(cid, messageID, attachments = []) {
+ if (messageID) {
+ const messageElem = document.getElementById(messageID);
+ if (messageElem) {
+ const attachmentToggle = messageElem.getElementsByClassName('attachment-toggle')[0];
+ if (attachments.length > 0) {
+ if (messageElem) {
+ const attachmentPlaceholder = messageElem.getElementsByClassName('attachments-placeholder')[0];
+ attachments.forEach(attachment => {
+ const attachmentHTML = `
+${attachmentHTMLBasedOnFilename(attachment['name'])}
+
`;
+ attachmentPlaceholder.insertAdjacentHTML('afterbegin', attachmentHTML);
+ });
+ attachmentToggle.addEventListener('click', (e) => {
+ attachmentPlaceholder.style.display = attachmentPlaceholder.style.display === "none" ? "" : "none";
+ });
+ activateAttachments(cid, attachmentPlaceholder);
+ attachmentToggle.style.display = "";
+ // attachmentPlaceholder.style.display = "";
+ }
+ } else {
+ attachmentToggle.style.display = "none";
+ }
+ }
}
}
+let socket;
+
+const sioTriggeringEvents = ['configLoaded', 'configNanoLoaded'];
+
+sioTriggeringEvents.forEach(event => {
+ document.addEventListener(event, _ => {
+ socket = initSIO();
+ });
+});
/**
- * Gets database and table from name
- * @param db: database name to get
- * @param table: table name to get
- * @return {Table} Dexie database object under specified table
+ * Inits socket io client listener by attaching relevant listeners on message channels
+ * @return {Socket} Socket IO client instance
*/
-const getDb = (db, table) => {
- let _instance;
- if (!Object.keys(__db_instances).includes(db)) {
- _instance = new Dexie(name);
- if (Object.keys(__db_definitions).includes(db)) {
- _instance.version(1).stores(__db_definitions[db]);
+function initSIO() {
+
+ const sioServerURL = configData['CHAT_SERVER_URL_BASE'];
+
+ const socket = io(
+ sioServerURL, {
+ extraHeaders: {
+ "session": getSessionToken()
+ }
}
- __db_instances[db] = _instance;
- } else {
- _instance = __db_instances[db];
+ );
+
+ socket.__proto__.emitAuthorized = (event, data) => {
+ socket.io.opts.extraHeaders.session = getSessionToken();
+ return socket.emit(event, data);
}
- return _instance[table];
-}
+ socket.on('auth_expired', () => {
+ if (currentUser && Object.keys(currentUser).length > 0) {
+ console.log('Authorization Token expired, refreshing...')
+ location.reload();
+ }
+ });
+
+ socket.on('connect', () => {
+ console.info(`Socket IO Connected to Server: ${sioServerURL}`)
+ });
+
+ socket.on("connect_error", (err) => {
+ console.log(`connect_error due to ${err.message}`);
+ });
-class DBGateway {
- constructor(db, table) {
- this.db = db;
- this.table = table;
+ socket.on('new_prompt_created', async (prompt) => {
+ const messageContainer = getMessageListContainer(prompt['cid']);
+ const promptID = prompt['_id'];
+ if (await getCurrentSkin(prompt['cid']) === CONVERSATION_SKINS.PROMPTS) {
+ if (!document.getElementById(promptID)) {
+ const messageHTML = await buildPromptHTML(prompt);
+ messageContainer.insertAdjacentHTML('beforeend', messageHTML);
+ }
+ }
+ });
- this._db_instance = getDb(this.db, this.table);
- this._db_columns_definitions = __db_definitions[this.db][this.table]
- this._db_key = this._db_columns_definitions.split(',')[0]
- }
+ socket.on('new_message', async (data) => {
+ if (await getCurrentSkin(data.cid) === CONVERSATION_SKINS.PROMPTS && data?.prompt_id) {
+ console.debug('Skipping prompt-related message')
+ return
+ }
+ // console.debug('received new_message -> ', data)
+ const preferredLang = getPreferredLanguage(data['cid']);
+ if (data?.lang !== preferredLang) {
+ requestTranslation(data['cid'], data['messageID']).catch(err => console.error(`Failed to request translation of cid=${data['cid']} messageID=${data['messageID']}: ${err}`));
+ }
+ addNewMessage(data['cid'], data['userID'], data['messageID'], data['messageText'], data['timeCreated'], data['repliedMessage'], data['attachments'], data?.isAudio, data?.isAnnouncement)
+ .then(_ => addMessageTransformCallback(data['cid'], data['messageID'], data?.isAudio))
+ .catch(err => console.error('Error occurred while adding new message: ', err));
+ });
- async getItem(key = "") {
- return await this._db_instance.where({
- [this._db_key]: key
- }).first();
- }
+ socket.on('new_prompt_message', async (message) => {
+ await addPromptMessage(message['cid'], message['userID'], message['messageText'], message['promptID'], message['promptState'])
+ .catch(err => console.error('Error occurred while adding new prompt data: ', err));
+ });
- async listItems(orderBy = "") {
- let expression = this._db_instance;
- if (orderBy !== "") {
- expression = expression.orderBy(orderBy)
+ socket.on('set_prompt_completed', async (data) => {
+ const promptID = data['prompt_id'];
+ const promptElem = document.getElementById(promptID);
+ console.info(`setting prompt_id=${promptID} as completed`);
+ if (promptElem) {
+ const promptWinner = document.getElementById(`${promptID}_winner`);
+ promptWinner.innerHTML = getPromptWinnerText(data['winner']);
+ } else {
+ console.warn(`Failed to get HTML element from prompt_id=${promptID}`);
}
- return await expression.toArray();
- }
+ });
- async putItem(data = {}) {
- return await this._db_instance.put(data, [data[this._db_key]])
- }
+ socket.on('translation_response', async (data) => {
+ console.debug('translation_response: ', data)
+ await applyTranslations(data);
+ });
- updateItem(data = {}) {
- const key = data[this._db_key]
- delete data[this._db_key]
- return this._db_instance.update(key, data);
- }
+ socket.on('subminds_state', async (data) => {
+ console.debug('subminds_state: ', data)
+ parseSubmindsState(data);
+ });
- async deleteItem(key = "") {
- return await this._db_instance.where({
- [this._db_key]: key
- }).delete();
- }
+ socket.on('incoming_tts', (data) => {
+ console.debug('received incoming stt audio');
+ playTTS(data['cid'], data['lang'], data['audio_data']);
+ });
- static getInstance(table) {
- return new DBGateway(DATABASES.CHATS, table);
- }
+ socket.on('incoming_stt', (data) => {
+ console.debug('received incoming stt response');
+ showSTT(data['message_id'], data['lang'], data['message_text']);
+ });
+
+ // socket.on('updated_shouts', async (data) =>{
+ // const inputType = data['input_type'];
+ // for (const [cid, shouts] of Object.entries(data['translations'])){
+ // if (await getCurrentSkin(cid) === CONVERSATION_SKINS.BASE){
+ // await requestTranslation(cid, shouts, null, inputType);
+ // }
+ // }
+ // });
+
+ return socket;
}
/**
- * Adds speaking callback for the message
- * @param cid: id of the conversation
- * @param messageID: id of the message
+ * Object representing loaded HTML components mapping:
+ * - key: component name,
+ * - value: HTML template that should be populated with actual data)
+ * @type Object
*/
-function addTTSCallback(cid, messageID) {
- const speakingButton = document.getElementById(`${messageID}_speak`);
- if (speakingButton) {
- speakingButton.addEventListener('click', (e) => {
- e.preventDefault();
- getTTS(cid, messageID, getPreferredLanguage(cid));
- setChatState(cid, 'updating', `Fetching TTS...`)
- });
- }
-}
+let loadedComponents = {}
/**
- * Adds speaking callback for the message
- * @param cid: id of the conversation
- * @param messageID: id of the message
+ * Fetches template context into provided html template
+ * @param html: HTML template
+ * @param templateContext: object containing context to fetch
+ * @return {string} HTML with fetched context
*/
-function addSTTCallback(cid, messageID) {
- const sttButton = document.getElementById(`${messageID}_text`);
- if (sttButton) {
- sttButton.addEventListener('click', (e) => {
- e.preventDefault();
- const sttContent = document.getElementById(`${messageID}-stt`);
- if (sttContent) {
- sttContent.innerHTML = `
-Waiting for STT...
-Loading...
-
-
`;
- sttContent.style.setProperty('display', 'block', 'important');
- getSTT(cid, messageID, getPreferredLanguage(cid));
- }
- });
+function fetchTemplateContext(html, templateContext) {
+ for (const [key, value] of Object.entries(templateContext)) {
+ html = html.replaceAll('{' + key + '}', value);
}
+ return html;
}
/**
- * Attaches STT capabilities for audio messages and TTS capabilities for text messages
- * @param cid: parent conversation id
- * @param messageID: target message id
- * @param isAudio: if its an audio message (defaults to '0')
+ * Builds HTML from passed params and template name
+ * @param templateName: name of the template to fetch
+ * @param templateContext: properties from template to fetch
+ * @param requestArgs: request string arguments (optional)
+ * @returns built template string
*/
-function addMessageTransformCallback(cid, messageID, isAudio = '0') {
- if (isAudio === '1') {
- addSTTCallback(cid, messageID);
+async function buildHTMLFromTemplate(templateName, templateContext = {}, requestArgs = '') {
+ if (!configData['DISABLE_CACHING'] && loadedComponents.hasOwnProperty(templateName) && !requestArgs) {
+ const html = loadedComponents[templateName];
+ return fetchTemplateContext(html, templateContext);
} else {
- addTTSCallback(cid, messageID);
+ return await fetch(`${configData['currentURLBase']}/components/${templateName}?${requestArgs}`)
+ .then((response) => {
+ if (response.ok) {
+ return response.text();
+ }
+ throw `template unreachable (HTTP STATUS:${response.status}: ${response.statusText})`
+ })
+ .then((html) => {
+ if (!(configData['DISABLE_CACHING'] || loadedComponents.hasOwnProperty(templateName) || requestArgs)) {
+ loadedComponents[templateName] = html;
+ }
+ return fetchTemplateContext(html, templateContext);
+ }).catch(err => console.warn(`Failed to fetch template for ${templateName}: ${err}`));
}
}
/**
- * Attaches STT capabilities for audio messages and TTS capabilities for text messages
- * @param conversationData: conversation data object
+ * Get Node id based on language key
+ * @param cid: desired conversation id
+ * @param key: language key (e.g. 'en')
+ * @param inputType: type of the language input to apply (incoming or outcoming)
+ * @return {string} ID of Node
*/
-function addCommunicationChannelTransformCallback(conversationData) {
- if (conversationData.hasOwnProperty('chat_flow')) {
- getUserMessages(conversationData).forEach(message => {
- addMessageTransformCallback(conversationData['_id'], message['message_id'], message?.is_audio);
- });
- }
+function getLangOptionID(cid, key, inputType = 'incoming') {
+ return `language-option-${cid}-${inputType}-${key}`;
}
+
/**
- * Resolves user reply on message
- * @param replyID: id of user reply
- * @param repliedID id of replied message
+ * Build language selection HTML based on provided params
+ * @param cid: desired conversation id
+ * @param key: language key (e.g 'en')
+ * @param name: name of the language (e.g. English)
+ * @param icon: language icon (refers to flag-icon specs)
+ * @param inputType: type of the language input to apply (incoming or outcoming)
+ * @return {string} formatted langSelectPattern
*/
-function resolveUserReply(replyID, repliedID) {
- if (repliedID) {
- const repliedElem = document.getElementById(repliedID);
- if (repliedElem) {
- let repliedText = repliedElem.getElementsByClassName('message-text')[0].innerText;
- repliedText = shrinkToFit(repliedText, 15);
- const replyHTML = `
-${repliedText}
-`;
- const replyPlaceholder = document.getElementById(replyID).getElementsByClassName('reply-placeholder')[0];
- replyPlaceholder.insertAdjacentHTML('afterbegin', replyHTML);
- attachReplyHighlighting(replyPlaceholder.getElementsByClassName('reply-text')[0]);
- }
+async function buildLangOptionHTML(cid, key, name, icon, inputType) {
+ return await buildHTMLFromTemplate('lang_option', {
+ 'itemId': getLangOptionID(cid, key, inputType),
+ 'key': key,
+ 'name': name,
+ 'icon': icon
+ })
+}
+
+/**
+ * Builds user message HTML
+ * @param userData: data of message sender
+ * @param cid: conversation id of target message
+ * @param messageID: id of user message
+ * @param messageText: text of user message
+ * @param timeCreated: date of creation
+ * @param isMine: if message was emitted by current user
+ * @param isAudio: if message is audio message (defaults to '0')
+ * @param isAnnouncement: is message if announcement (defaults to '0')
+ * @returns {string}: constructed HTML out of input params
+ */
+async function buildUserMessageHTML(userData, cid, messageID, messageText, timeCreated, isMine, isAudio = '0', isAnnouncement = '0') {
+ const messageTime = getTimeFromTimestamp(timeCreated);
+ let shortedNick = `${userData['nickname'][0]}${userData['nickname'][userData['nickname'].length - 1]}`;
+ let imageComponent = `${shortedNick}
`;
+ // if (userData.hasOwnProperty('avatar') && userData['avatar']){
+ // imageComponent = `
`
+ // }
+ const messageClass = isAnnouncement === '1' ? 'announcement' : isMine ? 'in' : 'out';
+ const messageOrientation = isMine ? 'right' : 'left';
+ let minificationEnabled = currentUser?.preferences?.minify_messages === '1' || await getCurrentSkin(cid) === CONVERSATION_SKINS.PROMPTS;
+ let templateSuffix = minificationEnabled ? '_minified' : '';
+ const templateName = isAudio === '1' ? `user_message_audio${templateSuffix}` : `user_message${templateSuffix}`;
+ if (isAudio === '0') {
+ messageText = messageText.replaceAll('\n', '
');
+ }
+ let statusIconHTML = '';
+ let userTooltip = userData['nickname'];
+ if (userData?.is_bot === '1') {
+ statusIconHTML = ' '
+ userTooltip = `bot ${userTooltip}`
}
+ return await buildHTMLFromTemplate(templateName, {
+ 'message_class': messageClass,
+ 'is_announcement': isAnnouncement,
+ 'image_component': imageComponent,
+ 'message_id': messageID,
+ 'user_tooltip': userTooltip,
+ 'nickname': userData['nickname'],
+ 'nickname_shrunk': shrinkToFit(userData['nickname'], 15, '..'),
+ 'status_icon': statusIconHTML,
+ 'message_text': messageText,
+ 'message_orientation': messageOrientation,
+ 'audio_url': `${configData["CHAT_SERVER_URL_BASE"]}/files/audio/${messageID}`,
+ 'message_time': messageTime
+ });
}
/**
- * Attaches reply highlighting for reply item
- * @param replyItem reply item element
+ *
+ * @param nick: nickname to shorten
+ * @return {string} - shortened nickname
*/
-function attachReplyHighlighting(replyItem) {
- replyItem.addEventListener('click', (e) => {
- const repliedItem = document.getElementById(replyItem.getAttribute('data-replied-id'));
- const backgroundParent = repliedItem.parentElement.parentElement;
- repliedItem.scrollIntoView();
- backgroundParent.classList.remove('message-selected');
- setTimeout(() => backgroundParent.classList.add('message-selected'), 500);
- });
+const shrinkNickname = (nick) => {
+ return `${nick[0]}${nick[nick.length - 1]}`;
}
+
/**
- * Attaches message replies to initialized conversation
- * @param conversationData: conversation data object
+ * Builds Prompt Skin HTML for submind responses
+ * @param promptID: target prompt id
+ * @param submindID: user id of submind
+ * @param submindUserData: user data of submind
+ * @param submindResponse: Responding data of submind to incoming prompt
+ * @param submindOpinion: Discussion data of submind to incoming prompt
+ * @param submindVote: Vote data of submind in prompt
+ * @return {Promise} - Submind Data HTML populated with provided data
*/
-function attachReplies(conversationData) {
- if (conversationData.hasOwnProperty('chat_flow')) {
- getUserMessages(conversationData).forEach(message => {
- resolveUserReply(message['message_id'], message?.replied_message);
- });
- Array.from(document.getElementsByClassName('reply-text')).forEach(replyItem => {
- attachReplyHighlighting(replyItem);
- });
+async function buildSubmindHTML(promptID, submindID, submindUserData, submindResponse, submindOpinion, submindVote) {
+ const userNickname = shrinkNickname(submindUserData['nickname']);
+ let tooltip = submindUserData['nickname'];
+ if (submindUserData['is_bot']) {
+ tooltip = `bot ${tooltip}`;
+ }
+ const phaseDataObjectMapping = {
+ 'response': submindResponse,
+ 'opinion': submindOpinion,
+ 'vote': submindVote
+ }
+ let templateData = {
+ 'prompt_id': promptID,
+ 'user_id': submindID,
+ 'user_first_name': submindUserData['first_name'],
+ 'user_last_name': submindUserData['last_name'],
+ 'user_nickname': submindUserData['nickname'],
+ 'user_nickname_shrunk': userNickname,
+ // 'user_avatar': `${configData["CHAT_SERVER_URL_BASE"]}/files/avatar/${submindID}`,
+ 'tooltip': tooltip
+ }
+ const submindPromptData = {}
+ for (const [k, v] of Object.entries(phaseDataObjectMapping)) {
+ submindPromptData[k] = v.message_text
+ submindPromptData[`${k}_message_id`] = v?.message_id
+ const dateCreated = getTimeFromTimestamp(v?.created_on);
+ submindPromptData[`${k}_created_on`] = v?.created_on;
+ submindPromptData[`${k}_created_on_tooltip`] = dateCreated ? `shouted on: ${dateCreated}` : `no ${k} from ${userNickname} in this prompt`;
}
+ return await buildHTMLFromTemplate("prompt_participant", Object.assign(templateData, submindPromptData));
}
-let userSettingsModal;
-let applyUserSettings;
-let minifyMessagesCheck;
-let settingsLink;
+
/**
- * Displays relevant user settings section based on provided name
- * @param name: name of the section to display
+ * Gets winner text based on the provided winner data
+ * @param winner: provided winner
+ * @return {string} generated winner text
*/
-const displaySection = (name) => {
- Array.from(document.getElementsByClassName('user-settings-section')).forEach(elem => {
- elem.hidden = true;
- });
- const elem = document.getElementById(`user-settings-${name}-section`);
- elem.hidden = false;
+const getPromptWinnerText = (winner) => {
+ let res;
+ if (winner) {
+ res = `Selected winner "${winner}"`;
+ } else {
+ res = 'Consensus not reached';
+ }
+ return res;
}
+
/**
- * Displays user settings based on received preferences
- * @param preferences
+ * Builds prompt HTML from received prompt data
+ * @param prompt: prompt object
+ * @return Prompt HTML
*/
-const displayUserSettings = (preferences) => {
- if (preferences) {
- minifyMessagesCheck.checked = preferences?.minify_messages === '1'
+async function buildPromptHTML(prompt) {
+ let submindsHTML = "";
+ const promptData = prompt['data'];
+ if (prompt['is_completed'] === '0') {
+ promptData['winner'] = `Prompt in progress
+
+Loading...
+
`
+ } else {
+ promptData['winner'] = getPromptWinnerText(promptData['winner']);
+ }
+ const emptyAnswer = `-
`;
+ for (const submindID of Array.from(setDefault(promptData, 'participating_subminds', []))) {
+ let submindUserData;
+ try {
+ const searchedKeys = ['proposed_responses', 'submind_opinions', 'votes'];
+ let isLegacy = false;
+ try {
+ submindUserData = prompt['user_mapping'][submindID][0];
+ } catch (e) {
+ console.warn('Detected legacy prompt structure');
+ submindUserData = {
+ 'nickname': submindID,
+ 'first_name': 'Klat',
+ 'last_name': 'User',
+ 'is_bot': '0'
+ }
+ isLegacy = true
+ }
+ const data = {}
+ searchedKeys.forEach(key => {
+ try {
+ const messageId = promptData[key][submindID];
+ let value = null;
+ if (!isLegacy) {
+ value = prompt['message_mapping'][messageId][0];
+ value['message_id'] = messageId;
+ }
+ if (!value) {
+ value = {
+ 'message_text': emptyAnswer
+ }
+ }
+ data[key] = value;
+ } catch (e) {
+ data[key] = {
+ 'message_text': emptyAnswer
+ };
+ }
+ });
+ submindsHTML += await buildSubmindHTML(prompt['_id'], submindID, submindUserData,
+ data.proposed_responses, data.submind_opinions, data.votes);
+ } catch (e) {
+ console.log(`Malformed data for ${submindID} (prompt_id=${prompt['_id']}) ex=${e}`);
+ }
}
+ return await buildHTMLFromTemplate("prompt_table", {
+ 'prompt_text': promptData['prompt_text'],
+ 'selected_winner': promptData['winner'],
+ 'prompt_participants_data': submindsHTML,
+ 'prompt_id': prompt['_id'],
+ 'cid': prompt['cid'],
+ 'message_time': prompt['created_on']
+ });
}
/**
- * Initialises section of settings based on provided name
- * @param sectionName: name of the section provided
+ * Gets user message HTML from received message data object
+ * @param message: Message Object received
+ * @param skin: conversation skin
+ * @return {Promise} HTML by the provided message data
*/
-const initSettingsSection = async (sectionName) => {
- await refreshCurrentUser(false)
- .then(userData => displayUserSettings(userData?.preferences))
- .then(_ => displaySection(sectionName));
+async function messageHTMLFromData(message, skin = CONVERSATION_SKINS.BASE) {
+ if (skin === CONVERSATION_SKINS.PROMPTS && message['message_type'] === 'prompt') {
+ return buildPromptHTML(message);
+ } else {
+ const isMine = currentUser && message['user_nickname'] === currentUser['nickname'];
+ return buildUserMessageHTML({
+ 'avatar': message['user_avatar'],
+ 'nickname': message['user_nickname'],
+ 'is_bot': message['user_is_bot'],
+ '_id': message['user_id']
+ },
+ message['cid'],
+ message['message_id'],
+ message['message_text'],
+ message['created_on'],
+ isMine,
+ message?.is_audio,
+ message?.is_announcement);
+ }
}
/**
- * Initialises User Settings Modal
+ * Builds HTML for received conversation data
+ * @param conversationData: JS Object containing conversation data of type:
+ * {
+ * '_id': 'id of conversation',
+ * 'conversation_name': 'title of the conversation',
+ * 'chat_flow': [{
+ * 'user_nickname': 'nickname of sender',
+ * 'user_avatar': 'avatar of sender',
+ * 'message_id': 'id of the message',
+ * 'message_text': 'text of the message',
+ * 'created_on': 'creation time of the message'
+ * }, ... (num of user messages returned)]
+ * }
+ * @param skin: conversation skin to build
+ * @returns {string} conversation HTML based on provided data
*/
-const initSettingsModal = async () => {
- Array.from(document.getElementsByClassName('nav-user-settings')).forEach(navItem => {
- navItem.addEventListener('click', async (e) => {
- await initSettingsSection(navItem.getAttribute('data-section-name'));
- });
- });
+async function buildConversationHTML(conversationData = {}, skin = CONVERSATION_SKINS.BASE) {
+ const cid = conversationData['_id'];
+ const conversation_name = conversationData['conversation_name'];
+ let chatFlowHTML = "";
+ if (conversationData.hasOwnProperty('chat_flow')) {
+ for (const message of Array.from(conversationData['chat_flow'])) {
+ message['cid'] = cid;
+ chatFlowHTML += await messageHTMLFromData(message, skin);
+ // if (skin === CONVERSATION_SKINS.BASE) {
+ // }
+ }
+ } else {
+ chatFlowHTML += `No messages in this chat yet...
`;
+ }
+ const conversationNameShrunk = shrinkToFit(conversation_name, 6);
+ let nanoHeaderHTML = '';
+ if (configData.client === CLIENTS.NANO) {
+ nanoHeaderHTML = await buildHTMLFromTemplate('nano_header', {
+ 'cid': cid
+ })
+ }
+ return await buildHTMLFromTemplate('conversation', {
+ 'cid': cid,
+ 'nano_header': nanoHeaderHTML,
+ 'conversation_name': conversation_name,
+ 'conversation_name_shrunk': conversationNameShrunk,
+ 'chat_flow': chatFlowHTML
+ }, `skin=${skin}`);
}
/**
- * Applies new settings to current user
+ * Builds suggestion HTML
+ * @param cid: target conversation id
+ * @param name: target conversation name
+ * @return {Promise} HTML with fetched data
*/
-const applyNewSettings = async () => {
- const newUserSettings = {
- 'minify_messages': minifyMessagesCheck.checked ? '1' : '0'
- };
- const query_url = 'preferences/update'
- await fetchServer(query_url, REQUEST_METHODS.POST, newUserSettings, true).then(async response => {
- const responseJson = await response.json();
- if (response.ok) {
- location.reload();
- } else {
- displayAlert(document.getElementById(`userSettingsModalBody`),
- `${responseJson['msg']}`,
- 'danger');
- }
- });
-}
-
-function initSettings(elem) {
- elem.addEventListener('click', async (e) => {
- await initSettingsModal();
- userSettingsModal.modal('show');
- });
+const buildSuggestionHTML = async (cid, name) => {
+ return await buildHTMLFromTemplate('suggestion', {
+ 'cid': cid,
+ 'conversation_name': name
+ })
+};
+/**
+ * Returns preferred language specified in provided cid
+ * @param cid: provided conversation id
+ * @param inputType: type of the language preference to fetch:
+ * "incoming" - for external shouts, "outcoming" - for emitted shouts
+ *
+ * @return preferred lang by cid or "en"
+ */
+function getPreferredLanguage(cid, inputType = 'incoming') {
+ let preferredLang = 'en';
+ try {
+ preferredLang = getChatLanguageMapping(cid, inputType);
+ } catch (e) {
+ console.warn(`Failed to getChatLanguageMapping - ${e}`)
+ }
+ return preferredLang;
}
/**
- * Initialise user settings links based on the current client
+ * Returns preferred language specified in provided cid
+ * @param cid: provided conversation id
+ * @param lang: new preferred language to set
+ * @param inputType: type of the language preference to fetch:
+ * @param updateDB: to update user preferences in database
+ * @param updateDBOnly: to update user preferences in database only (without translation request)
+ * "incoming" - for external shouts, "outcoming" - for emitted shouts
*/
-const initSettingsLinks = () => {
- if (configData.client === CLIENTS.NANO) {
- console.log('initialising settings link for ', Array.from(document.getElementsByClassName('settings-link')).length, ' elements')
- Array.from(document.getElementsByClassName('settings-link')).forEach(elem => {
- initSettings(elem);
- });
- } else {
- initSettings(document.getElementById('settingsLink'));
+async function setPreferredLanguage(cid, lang, inputType = 'incoming', updateDB = true, updateDBOnly = false) {
+ let isOk = false;
+ if (updateDB) {
+ const formData = new FormData();
+ formData.append('lang', lang);
+ isOk = await fetchServer(`preferences/update_language/${cid}/${inputType}`, REQUEST_METHODS.POST, formData)
+ .then(res => {
+ return res.ok;
+ });
+ }
+ if ((isOk || !updateDB) && !updateDBOnly) {
+ updateChatLanguageMapping(cid, inputType, lang);
+ const shoutIds = getMessagesOfCID(cid, MESSAGE_REFER_TYPE.ALL, 'plain', true);
+ await requestTranslation(cid, shoutIds, lang, inputType);
}
}
-document.addEventListener('DOMContentLoaded', (_) => {
- if (configData.client === CLIENTS.MAIN) {
- userSettingsModal = $('#userSettingsModal');
- applyUserSettings = document.getElementById('applyUserSettings');
- minifyMessagesCheck = document.getElementById('minifyMessages');
- applyUserSettings.addEventListener('click', async (e) => await applyNewSettings());
- settingsLink = document.getElementById('settingsLink');
- settingsLink.addEventListener('click', async (e) => {
- e.preventDefault();
- await initSettingsModal();
- userSettingsModal.modal('show');
- });
- } else {
- document.addEventListener('modalsLoaded', (e) => {
- userSettingsModal = $('#userSettingsModal');
- applyUserSettings = document.getElementById('applyUserSettings');
- minifyMessagesCheck = document.getElementById('minifyMessages');
- applyUserSettings.addEventListener('click', async (e) => await applyNewSettings());
- if (configData.client === CLIENTS.MAIN) {
- initSettingsLinks();
+/**
+ * Fetches supported languages
+ */
+async function fetchSupportedLanguages() {
+ const query_url = `language_api/settings`;
+ return await fetchServer(query_url)
+ .then(response => {
+ if (response.ok) {
+ return response.json();
+ } else {
+ console.log(`failed to fetch supported languages - ${response.statusText}`)
+ throw response.statusText;
}
- });
-
- document.addEventListener('nanoChatsLoaded', (e) => {
- setTimeout(() => initSettingsLinks(), 1000);
})
- }
-});
-const MessageScrollPosition = {
- START: 'START',
- END: 'END',
- MIDDLE: 'MIDDLE',
-};
+ .then(data => {
+ configData['supportedLanguages'] = data['supported_languages'];
+ console.info(`supported languages updated - ${JSON.stringify(configData['supportedLanguages'])}`)
+ }).catch(err => console.warn('Failed to fulfill request due to error:', err));
+}
/**
- * Gets current message list scroller position based on first and last n-items visibility
- * @param messageList: Container of messages
- * @param numElements: number of first and last elements to check for visibility
- * @param assertOnly: check only for one of the scroll position (preventing ambiguity if its a start or the end)
- * @return {string} MessageScrollPosition from Enum
+ * Sends request for updating target conversation(s) content to the desired language
+ * @param cid: conversation id to bound request to
+ * @param shouts: list of shout ids to bound request to
+ * @param lang: language to apply (defaults to preferred language of each fetched conversation)
+ * @param inputType: type of the language input to apply (incoming or outcoming)
+ * @param translateToBaseLang: to translate provided items to the system base lang (based on preferred)
*/
-function getMessageScrollPosition(messageList, numElements = 3, assertOnly = null) {
- numElements = Math.min(messageList.children.length, numElements);
- if (numElements > 0) {
- for (let i = 1; i <= numElements; i++) {
- if (!(assertOnly === MessageScrollPosition.START) &&
- isInViewport(messageList.children[messageList.children.length - i])) {
- return MessageScrollPosition.END;
- }
- if (!(assertOnly === MessageScrollPosition.END) && isInViewport(messageList.children[i - 1])) {
- return MessageScrollPosition.START;
+async function requestTranslation(cid = null, shouts = null, lang = null, inputType = 'incoming', translateToBaseLang = false) {
+ let requestBody = {
+ chat_mapping: {}
+ };
+ if (cid && isDisplayed(cid)) {
+ lang = lang || getPreferredLanguage(cid, inputType);
+ if (lang !== 'en' && getMessagesOfCID(cid, MESSAGE_REFER_TYPE.ALL, 'plain').length > 0) {
+ setChatState(cid, 'updating', 'Applying New Language...');
+ }
+ if (shouts && !Array.isArray(shouts)) {
+ shouts = [shouts];
+ }
+ if (!shouts && inputType) {
+ shouts = getMessagesOfCID(cid, getMessageReferType(inputType), 'plain', true);
+ if (shouts.length === 0) {
+ console.log(`${cid} yet has no shouts matching type=${inputType}`);
+ setChatState(cid, 'active');
+ return
}
}
+ setDefault(requestBody.chat_mapping, cid, {});
+ requestBody.chat_mapping[cid] = {
+ 'lang': lang,
+ 'shouts': shouts || []
+ }
+ if (translateToBaseLang) {
+ requestBody.chat_mapping[cid]['source_lang'] = getPreferredLanguage(cid);
+ }
+ } else {
+ requestBody.chat_mapping = getChatLanguageMapping();
+ if (!requestBody.chat_mapping) {
+ console.log('Chat mapping is undefined - returning');
+ return
+ }
}
- return MessageScrollPosition.MIDDLE;
+ requestBody['user'] = currentUser['_id'];
+ requestBody['inputType'] = inputType;
+ console.debug(`requestBody = ${JSON.stringify(requestBody)}`);
+ socket.emitAuthorized('request_translate', requestBody);
}
/**
- * Decides whether scrolling on new message is required based on the current viewport
- * @param messageList: message list DOM element
- * @param lastNElements: number of last elements to consider a live following
+ * Sets selected language to the target language selector
+ * @param clickedItem: Language selector element clicked
+ * @param cid: target conversation id
+ * @param inputType: type of the language input to apply (incoming or outcoming)
*/
-function scrollOnNewMessage(messageList, lastNElements = 3) {
- // If we see last element of the chat - we are following it
- if (getMessageScrollPosition(messageList, lastNElements, MessageScrollPosition.END) === MessageScrollPosition.END) {
- messageList.lastChild.scrollIntoView();
- }
-}
-const importConversationModal = $('#importConversationModal');
-const importConversationOpener = document.getElementById('importConversationOpener');
-const conversationSearchInput = document.getElementById('conversationSearchInput');
-const importConversationModalSuggestions = document.getElementById('importConversationModalSuggestions');
-
-const addBySearch = document.getElementById('addBySearch');
-
-const newConversationModal = $('#newConversationModal');
-const bindServiceSelect = document.getElementById('bind-service-select')
-const addNewConversation = document.getElementById('addNewConversation');
-
-const conversationBody = document.getElementById('conversationsBody');
+async function setSelectedLang(clickedItem, cid, inputType = "incoming") {
+ const selectedLangNode = document.getElementById(`language-selected-${cid}-${inputType}`);
+ const selectedLangList = document.getElementById(`language-list-${cid}-${inputType}`);
-let conversationState = {};
+ // console.log('emitted lang update')
+ const preferredLang = getPreferredLanguage(cid, inputType);
+ const preferredLangProps = configData['supportedLanguages'][preferredLang];
+ const newKey = clickedItem.getAttribute('data-lang');
+ const newPreferredLangProps = configData['supportedLanguages'][newKey];
-/**
- * Clears conversation state cache
- * @param cid - Conversation ID to clear
- */
-const clearStateCache = (cid) => {
- delete conversationState[cid];
-}
-/**
- * Sets all participants counters to zero
- */
-const setAllCountersToZero = () => {
- const countNodes = document.querySelectorAll('[id^="participants-count-"]');
- countNodes.forEach(node => node.innerText = 0);
+ const direction = inputType === 'incoming' ? 'down' : 'up';
+ selectedLangNode.innerHTML = await buildHTMLFromTemplate('selected_lang', {
+ 'key': newKey,
+ 'name': newPreferredLangProps['name'],
+ 'icon': newPreferredLangProps['icon'],
+ 'direction': direction
+ })
+ if (preferredLangProps) {
+ selectedLangList.getElementsByClassName('lang-container')[0].insertAdjacentHTML('beforeend', await buildLangOptionHTML(cid, preferredLang, preferredLangProps['name'], preferredLangProps['icon'], inputType));
+ } else {
+ console.warn(`"${preferredLang}" is set to be preferred but currently not supported`)
+ }
+ if (clickedItem.parentNode) {
+ clickedItem.parentNode.removeChild(clickedItem);
+ }
+ console.log(`cid=${cid};new preferredLang=${newKey}, inputType=${inputType}`);
+ await setPreferredLanguage(cid, newKey, inputType, true);
+ const insertedNode = document.getElementById(getLangOptionID(cid, preferredLang, inputType));
+ insertedNode.addEventListener('click', async (e) => {
+ e.preventDefault();
+ await setSelectedLang(insertedNode, cid, inputType);
+ });
}
-
/**
- * Sets participants count for conversation view
- * @param cid - desired conversation id
+ * Initialize language selector for conversation
+ * @param cid: target conversation id
+ * @param inputType: type of the language input to apply (incoming or outcoming)
*/
-const refreshSubmindsCount = (cid) => {
- const participantsCountNode = document.getElementById(`participants-count-${cid}`);
- if (participantsCountNode) {
- let submindsCount = 0
- if (!isEmpty(submindsState)) {
- submindsCount = submindsState["subminds_per_cid"][cid].filter(submind => {
- const connectedSubmind = submindsState.connected_subminds[submind.submind_id];
- return connectedSubmind && connectedSubmind.bot_type === "submind" && submind.status === "active";
- }).length;
+async function initLanguageSelector(cid, inputType = "incoming") {
+ let preferredLang = getPreferredLanguage(cid, inputType);
+ const supportedLanguages = configData['supportedLanguages'];
+ if (!supportedLanguages.hasOwnProperty(preferredLang)) {
+ preferredLang = 'en';
+ }
+ const selectedLangNode = document.getElementById(`language-selected-${cid}-${inputType}`);
+ const langList = document.getElementById(`language-list-${cid}-${inputType}`);
+ if (langList) {
+ const langListContainer = langList.getElementsByClassName('lang-container')[0]
+
+ if (langListContainer) {
+ langListContainer.innerHTML = "";
}
- participantsCountNode.innerText = submindsCount;
- }
-}
+ // selectedLangNode.innerHTML = "";
+ for (const [key, value] of Object.entries(supportedLanguages)) {
-/**
- * Saves attached files to the server
- * @param cid - target conversation id
- * @return attachments array or `-1` if something went wrong
- */
-const saveAttachedFiles = async (cid) => {
- const filesArr = getUploadedFiles(cid);
- const attachments = [];
- if (filesArr.length > 0) {
- setChatState(cid, 'updating', 'Saving attachments...');
- let errorOccurred = null;
- const formData = new FormData();
- const attachmentProperties = {}
- filesArr.forEach(file => {
- const generatedFileName = `${generateUUID(10,'00041000')}.${file.name.split('.').pop()}`;
- attachmentProperties[generatedFileName] = {
- 'size': file.size,
- 'type': file.type
+ if (key === preferredLang) {
+ const direction = inputType === 'incoming' ? 'down' : 'up';
+ selectedLangNode.innerHTML = await buildHTMLFromTemplate('selected_lang', {
+ 'key': key,
+ 'name': value['name'],
+ 'icon': value['icon'],
+ 'direction': direction
+ })
+ } else {
+ langListContainer.insertAdjacentHTML('beforeend', await buildLangOptionHTML(cid, key, value['name'], value['icon'], inputType));
+ const itemNode = document.getElementById(getLangOptionID(cid, key, inputType));
+ itemNode.addEventListener('click', async (e) => {
+ e.preventDefault();
+ await setSelectedLang(itemNode, cid, inputType)
+ });
}
- const renamedFile = new File([file], generatedFileName, {
- type: file.type
- });
- formData.append('files', renamedFile);
- });
- cleanUploadedFiles(cid);
-
- await fetchServer(`files/attachments`, REQUEST_METHODS.POST, formData)
- .then(async response => {
- const responseJson = await response.json();
- if (response.ok) {
- for (const [fileName, savedName] of Object.entries(responseJson['location_mapping'])) {
- attachments.push({
- 'name': savedName,
- 'size': attachmentProperties[fileName].size,
- 'mime': attachmentProperties[fileName].type
- })
- }
- } else {
- throw `Failed to save attachments status=${response.status}, msg=${responseJson}`;
- }
- }).catch(err => {
- errorOccurred = err;
- });
- setChatState(cid, 'active')
- if (errorOccurred) {
- console.error(`Error during attachments preparation: ${errorOccurred}, skipping message sending`);
- return -1
- } else {
- console.log('Received attachments array: ', attachments);
}
}
- return attachments;
}
/**
- * Supported conversation skins
- * @type Object
+ * Inits both incoming and outcoming language selectors
+ * @param cid: target conversation id
*/
-const CONVERSATION_SKINS = {
- BASE: 'base',
- PROMPTS: 'prompts'
+const initLanguageSelectors = async (cid) => {
+ for (const inputType of ['incoming', 'outcoming']) {
+ await initLanguageSelector(cid, inputType);
+ }
}
-/**
- * Initiates selection of the table rows.
- * @param table - target table to select
- * @param exportToExcelBtn - DOM element of `Export to Excel` button
- */
-const startSelection = (table, exportToExcelBtn) => {
- table.classList.remove('selected');
- const container = table.parentElement.parentElement;
- if (Array.from(container.getElementsByClassName('selected')).length === 0) {
- exportToExcelBtn.disabled = true;
- }
- startTimer();
+
+function getMessageReferType(inputType) {
+ return inputType === 'incoming' ? MESSAGE_REFER_TYPE.OTHERS : MESSAGE_REFER_TYPE.MINE;
}
/**
- * Marks target table as selected
- * @param table - HTMLTable element
- * @param exportToExcelBtn - export to excel button (optional)
+ * Sends request to server for chat language refreshing
*/
-const selectTable = (table, exportToExcelBtn = null) => {
- const timePassed = stopTimer();
- if (timePassed >= 300) {
- if (exportToExcelBtn)
- exportToExcelBtn.disabled = false;
- table.classList.add('selected');
+async function requestChatsLanguageRefresh() {
+ const languageMapping = currentUser?.preferences?.chat_language_mapping || {};
+ console.log(`languageMapping=${JSON.stringify(languageMapping)}`)
+ for (const [cid, value] of Object.entries(languageMapping)) {
+ if (isDisplayed(cid)) {
+ for (const inputType of ['incoming', 'outcoming']) {
+ const lang = value[inputType] || 'en';
+ if (lang !== 'en') {
+ await setPreferredLanguage(cid, lang, inputType, false);
+ }
+ }
+ }
}
+ console.log(`chatLanguageMapping=${JSON.stringify(getChatLanguageMapping())}`)
}
/**
- * Wraps the provided array of HTMLTable elements into XLSX file and exports it to the invoked user
- * @param tables - array of HTMLTable elements to export
- * @param filePrefix - prefix of the file name to be imported
- * @param sheetPrefix - prefix to apply for each sheet generated per HTMLTable
- * @param appname - name of the application to export (defaults to Excel)
+ * Applies translation based on received data
+ * @param data: translation object received
+ * Note: data should be of format:
+ * {
+ * 'cid': {'message1':'translation of message 1',
+ * 'message2':'translation of message 2'}
+ * }
*/
-const exportTablesToExcel = (function() {
- const uri = 'data:application/vnd.ms-excel;base64,';
- const tmplWorkbookXML = `
-
-
-
-
-PyKlatchat Generator
-{created}
-
-
-'
-'
-
-{worksheets}
-
-`
- const tmplWorksheetXML = ''
- const tmplCellXML = '{data} | '
- const base64 = function(s) {
- return window.btoa(unescape(encodeURIComponent(s)))
- }
- const format = function(s, c) {
- return s.replace(/{(\w+)}/g, function(m, p) {
- return c[p];
- })
- }
- return function(tables, filePrefix, sheetPrefix = '', appname = 'Excel') {
- let ctx = "";
- let workbookXML = "";
- let worksheetsXML = "";
- let rowsXML = "";
+async function applyTranslations(data) {
+ const inputType = setDefault(data, 'input_type', 'incoming');
+ for (const [cid, messageTranslations] of Object.entries(data['translations'])) {
- for (let i = 0; i < tables.length; i++) {
- if (!tables[i].nodeType) tables[i] = document.getElementById(tables[i]);
- for (let j = 0; j < tables[i].rows.length; j++) {
- rowsXML += ''
- for (let k = 0; k < tables[i].rows[j].cells.length; k++) {
- let data = tables[i].rows[j].cells[k].innerHTML
- if (k === 0) {
- const chatImgElem = tables[i].rows[j].cells[k].getElementsByClassName("chat-img")[0]
- if (chatImgElem) {
- data = chatImgElem.getAttribute("title");
- }
- }
- ctx = {
- data: data,
- };
- rowsXML += format(tmplCellXML, ctx);
- }
- rowsXML += '
'
- }
- const sheetName = sheetPrefix.replaceAll("{id}", tables[i].id);
- ctx = {
- rows: rowsXML,
- nameWS: sheetName || 'Sheet' + i
- };
- worksheetsXML += format(tmplWorksheetXML, ctx);
- rowsXML = "";
+ if (!isDisplayed(cid)) {
+ console.log(`cid=${cid} is not displayed, skipping translations population`)
+ continue;
}
- ctx = {
- created: getCurrentTimestamp() * 1000,
- worksheets: worksheetsXML
- };
- workbookXML = format(tmplWorkbookXML, ctx);
+ setChatState(cid, 'active');
- let link = document.createElement("A");
- link.href = uri + base64(workbookXML);
- const fileName = `${filePrefix}_${getCurrentTimestamp()}`;
- link.download = `${fileName}.xls`;
- link.target = '_blank';
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
+ console.debug(`Fetching translation of ${cid}`);
+ // console.debug(`translations=${JSON.stringify(messageTranslations)}`)
+
+ const messageTranslationsShouts = messageTranslations['shouts'];
+ if (messageTranslationsShouts) {
+ const messageReferType = getMessageReferType(inputType);
+ const messages = getMessagesOfCID(cid, messageReferType, 'plain');
+ Array.from(messages).forEach(message => {
+ const messageID = message.id;
+ let repliedMessage = null;
+ let repliedMessageID = null;
+ try {
+ repliedMessage = message.getElementsByClassName('reply-placeholder')[0].getElementsByClassName('reply-text')[0];
+ repliedMessageID = repliedMessage.getAttribute('data-replied-id')
+ // console.debug(`repliedMessageID=${repliedMessageID}`)
+ } catch (e) {
+ // console.debug(`replied message not found for ${messageID}`);
+ }
+ if (messageID in messageTranslationsShouts) {
+ message.getElementsByClassName('message-text')[0].innerHTML = messageTranslationsShouts[messageID];
+ }
+ if (repliedMessageID && repliedMessageID in messageTranslationsShouts) {
+ repliedMessage.innerHTML = messageTranslationsShouts[repliedMessageID];
+ }
+ });
+ await initLanguageSelector(cid, inputType);
+ }
}
-})();
+}
-/**
- * Sends the message based on input
- * @param inputElem - input DOM element
- * @param cid - target conversation id
- * @param repliedMessageId - replied message id (optional)
- * @param isAudio - `1` if the message is audio-message (defaults to `0`)
- * @param isAnnouncement - `1` if the message is an announcement (defaults to `0`)
- */
-const sendMessage = async (inputElem, cid, repliedMessageId = null, isAudio = '0', isAnnouncement = '0') => {
- const attachments = await saveAttachedFiles(cid);
- if (Array.isArray(attachments)) {
- emitUserMessage(inputElem, cid, repliedMessageId, attachments, isAudio, isAnnouncement);
+const getChatLanguageMapping = (cid = null, inputType = null) => {
+ let res = setDefault(setDefault(currentUser, 'preferences', {}), 'chat_language_mapping', {});
+ if (cid) {
+ res = setDefault(res, cid, {});
+ }
+ if (inputType) {
+ res = setDefault(res, inputType, 'en');
}
- inputElem.value = "";
+ return res;
+}
+
+const updateChatLanguageMapping = (cid, inputType, lang) => {
+ setDefault(currentUser.preferences.chat_language_mapping, cid, {})[inputType] = lang;
+ console.log(`cid=${cid},inputType=${inputType} updated to lang=${lang}`);
}
/**
- * Gets all opened chat ids
- * @return {[]} list of displayed chat ids
+ * Custom Event fired on supported languages init
+ * @type {CustomEvent}
*/
-function getOpenedChatIds() {
- let cids = [];
- Array.from(conversationBody.getElementsByClassName('conversationContainer')).forEach(conversationContainer => {
- cids.push(conversationContainer.getElementsByClassName('card')[0].id);
+const supportedLanguagesLoadedEvent = new CustomEvent("supportedLanguagesLoaded", {
+ "detail": "Event that is fired when system supported languages are loaded"
+});
+
+document.addEventListener('DOMContentLoaded', (_) => {
+ document.addEventListener('configLoaded', async (_) => {
+ await fetchSupportedLanguages().then(_ => document.dispatchEvent(supportedLanguagesLoadedEvent));
});
- return cids;
-}
+});
+document.addEventListener('configLoaded', async (_) => {
-const resizeConversationContainers = () => {
- const openedChatIds = getOpenedChatIds();
- const newWidth = `${100/openedChatIds.length}vw`;
- openedChatIds.forEach(cid => {
- document.getElementById(cid).style.width = newWidth;
- })
+ const buildVersion = configData?.["BUILD_VERSION"];
+ const buildTS = configData?.["BUILD_TS"];
+ if (buildVersion && buildTS) {
+ document.getElementById("app-version").innerText = `v${buildVersion} (${getTimeFromTimestamp(buildTS)})`;
+ }
+});
+const DATABASES = {
+ CHATS: 'chats'
+}
+const DB_TABLES = {
+ CHAT_ALIGNMENT: 'chat_alignment',
+ MINIFY_SETTINGS: 'minify_settings',
+ CHAT_MESSAGES_PAGINATION: 'chat_messages_pagination'
+}
+const __db_instances = {}
+const __db_definitions = {
+ [DATABASES.CHATS]: {
+ [DB_TABLES.CHAT_ALIGNMENT]: `cid, added_on, skin`,
+ [DB_TABLES.CHAT_MESSAGES_PAGINATION]: `cid, oldest_created_on`
+ }
}
/**
- * Builds new conversation HTML from provided data and attaches it to the list of displayed conversations
- * @param conversationData - JS Object containing conversation data of type:
- * {
- * '_id': 'id of conversation',
- * 'conversation_name': 'title of the conversation',
- * 'chat_flow': [{
- * 'user_nickname': 'nickname of sender',
- * 'user_avatar': 'avatar of sender',
- * 'message_id': 'id of the message',
- * 'message_text': 'text of the message',
- * 'is_audio': true if message is an audio message
- * 'is_announcement': true if message is considered to be an announcement
- * 'created_on': 'creation time of the message'
- * }, ... (num of user messages returned)]
- * }
- * @param skin - Conversation skin to build
- * @param remember - to store this conversation into localStorage (defaults to true)*
- * @param conversationParentID - ID of conversation parent
- * @return id of the built conversation
+ * Gets database and table from name
+ * @param db: database name to get
+ * @param table: table name to get
+ * @return {Table} Dexie database object under specified table
*/
-async function buildConversation(conversationData, skin, remember = true, conversationParentID = 'conversationsBody') {
- const idField = '_id';
- const cid = conversationData[idField];
- if (!cid) {
- console.error(`Failed to extract id field="${idField}" from conversation data - ${conversationData}`);
- return -1;
- }
- if (remember) {
- await addNewCID(cid, skin);
+const getDb = (db, table) => {
+ let _instance;
+ if (!Object.keys(__db_instances).includes(db)) {
+ _instance = new Dexie(name);
+ if (Object.keys(__db_definitions).includes(db)) {
+ _instance.version(1).stores(__db_definitions[db]);
+ }
+ __db_instances[db] = _instance;
+ } else {
+ _instance = __db_instances[db];
}
- const newConversationHTML = await buildConversationHTML(conversationData, skin);
- const conversationsBody = document.getElementById(conversationParentID);
- conversationsBody.insertAdjacentHTML('afterbegin', newConversationHTML);
+ return _instance[table];
+}
- resizeConversationContainers()
- setChatState(cid, CHAT_STATES.UPDATING, "Loading messages...")
- initMessages(conversationData, skin).then(_ => setChatState(cid, CHAT_STATES.ACTIVE));
+class DBGateway {
+ constructor(db, table) {
+ this.db = db;
+ this.table = table;
- const messageListContainer = getMessageListContainer(cid);
- const currentConversation = document.getElementById(cid);
- const conversationParent = currentConversation.parentElement;
- const conversationHolder = conversationParent.parentElement;
+ this._db_instance = getDb(this.db, this.table);
+ this._db_columns_definitions = __db_definitions[this.db][this.table]
+ this._db_key = this._db_columns_definitions.split(',')[0]
+ }
- let chatCloseButton = document.getElementById(`close-${cid}`);
- const chatInputButton = document.getElementById(conversationData['_id'] + '-send');
- const filenamesContainer = document.getElementById(`filename-container-${conversationData['_id']}`)
- const attachmentsButton = document.getElementById('file-input-' + conversationData['_id']);
- const textInputElem = document.getElementById(conversationData['_id'] + '-input');
- if (chatInputButton.hasAttribute('data-target-cid')) {
- textInputElem.addEventListener('keyup', async (e) => {
- if (e.shiftKey && e.key === 'Enter') {
- await sendMessage(textInputElem, conversationData['_id']);
- }
- });
- chatInputButton.addEventListener('click', async (e) => {
- await sendMessage(textInputElem, conversationData['_id']);
- });
+ async getItem(key = "") {
+ return await this._db_instance.where({
+ [this._db_key]: key
+ }).first();
}
- attachmentsButton.addEventListener('change', (e) => {
- e.preventDefault();
- const fileName = getFilenameFromPath(e.currentTarget.value);
- const lastFile = attachmentsButton.files[attachmentsButton.files.length - 1]
- if (lastFile.size > configData['maxUploadSize']) {
- console.warn(`Uploaded file is too big`);
- } else {
- addUpload(attachmentsButton.parentNode.parentNode.id, lastFile);
- filenamesContainer.insertAdjacentHTML('afterbegin',
- `${fileName}`);
- filenamesContainer.style.display = "";
- if (filenamesContainer.children.length === configData['maxNumAttachments']) {
- attachmentsButton.disabled = true;
- }
+ async listItems(orderBy = "") {
+ let expression = this._db_instance;
+ if (orderBy !== "") {
+ expression = expression.orderBy(orderBy)
}
- });
- await addRecorder(conversationData);
- await initLanguageSelectors(conversationData['_id']);
-
- if (skin === CONVERSATION_SKINS.BASE) {
- const promptModeButton = document.getElementById(`prompt-mode-${conversationData['_id']}`);
-
- promptModeButton.addEventListener('click', async (e) => {
- e.preventDefault();
- chatCloseButton.click();
- await displayConversation(conversationData['_id'], CONVERSATION_SKINS.PROMPTS, null, conversationParentID);
- });
- } else if (skin === CONVERSATION_SKINS.PROMPTS) {
- chatCloseButton = document.getElementById(`close-prompts-${cid}`);
- const baseModeButton = document.getElementById(`base-mode-${cid}`);
- const exportToExcelBtn = document.getElementById(`${cid}-export-to-excel`)
-
- // TODO: fix here to use prompt- prefix
- baseModeButton.addEventListener('click', async (e) => {
- e.preventDefault();
- chatCloseButton.click();
- await displayConversation(cid, CONVERSATION_SKINS.BASE, null, conversationParentID);
- });
-
- // TODO: make an array of prompt tables only in dedicated conversation
- Array.from(getMessagesOfCID(cid, MESSAGE_REFER_TYPE.ALL, 'prompt', false)).forEach(table => {
-
- table.addEventListener('mousedown', (_) => startSelection(table, exportToExcelBtn));
- table.addEventListener('touchstart', (_) => startSelection(table, exportToExcelBtn));
- table.addEventListener('mouseup', (_) => selectTable(table, exportToExcelBtn));
- table.addEventListener("touchend", (_) => selectTable(table, exportToExcelBtn));
-
- });
- exportToExcelBtn.addEventListener('click', (e) => {
- const selectedTables = messageListContainer.getElementsByClassName('selected');
- exportTablesToExcel(selectedTables, `prompts_of_${cid}`, 'prompt_{id}');
- Array.from(selectedTables).forEach(selectedTable => {
- selectedTable.classList.remove('selected');
- });
- });
+ return await expression.toArray();
}
- if (chatCloseButton.hasAttribute('data-target-cid')) {
- chatCloseButton.addEventListener('click', async (_) => {
- conversationHolder.removeChild(conversationParent);
- await removeConversation(cid);
- clearStateCache(cid);
- resizeConversationContainers()
- });
+ async putItem(data = {}) {
+ return await this._db_instance.put(data, [data[this._db_key]])
}
- // Hide close button for Nano Frames
- if (configData.client === CLIENTS.NANO) {
- chatCloseButton.hidden = true;
+
+ updateItem(data = {}) {
+ const key = data[this._db_key]
+ delete data[this._db_key]
+ return this._db_instance.update(key, data);
}
- setTimeout(() => getMessageListContainer(conversationData['_id']).lastElementChild?.scrollIntoView(true), 0);
- setTimeout(() => document.getElementById('klatchatHeader').scrollIntoView(true), 0);
- // $('#copyrightContainer').css('position', 'inherit');
- return cid;
-}
-/**
- * Gets conversation data based on input string
- * @param input - input string text
- * @param oldestMessageTS - creation timestamp of the oldest displayed message
- * @param skin - resolves by server for which data to return
- * @param maxResults - max number of messages to fetch
- * @returns {Promise<{}>} promise resolving conversation data returned
- */
-async function getConversationDataByInput(input, skin, oldestMessageTS = null, maxResults = 10) {
- let conversationData = {};
- if (input) {
- let query_url = `chat_api/search/${input.toString()}?limit_chat_history=${maxResults}&skin=${skin}`;
- if (oldestMessageTS) {
- query_url += `&creation_time_from=${oldestMessageTS}`;
- }
- await fetchServer(query_url)
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw response.statusText;
- }
- })
- .then(data => {
- if (getUserMessages(data, null).length === 0) {
- console.log('All of the messages are already displayed');
- setDefault(setDefault(conversationState, data['_id'], {}), 'all_messages_displayed', true);
- }
- conversationData = data;
- }).catch(async err => {
- console.warn('Failed to fulfill request due to error:', err);
- });
+ async deleteItem(key = "") {
+ return await this._db_instance.where({
+ [this._db_key]: key
+ }).delete();
}
- return conversationData;
-}
-/**
- * Returns table representing chat alignment
- * @return {Table}
- */
-const getChatAlignmentTable = () => {
- return getDb(DATABASES.CHATS, DB_TABLES.CHAT_ALIGNMENT);
+ static getInstance(table) {
+ return new DBGateway(DATABASES.CHATS, table);
+ }
}
-
/**
- * Retrieves conversation layout from local storage
- * @returns {Array} collection of database-stored elements
+ * Displays modal bounded to the provided conversation id
+ * @param modalElem: modal to display
+ * @param cid: conversation id to consider
*/
-async function retrieveItemsLayout(idOnly = false) {
- let layout = await getChatAlignmentTable().orderBy("added_on").toArray();
- if (idOnly) {
- layout = layout.map(a => a.cid);
- }
- return layout;
+function displayModalInCID(modalElem, cid) {
+ modalElem.modal('hide');
+ $('.modal-backdrop').appendTo(`#${cid}`);
+ modalElem.modal('show');
}
+const importConversationModal = $('#importConversationModal');
+const importConversationOpener = document.getElementById('importConversationOpener');
+const conversationSearchInput = document.getElementById('conversationSearchInput');
+const importConversationModalSuggestions = document.getElementById('importConversationModalSuggestions');
+
+const addBySearch = document.getElementById('addBySearch');
+
+const newConversationModal = $('#newConversationModal');
+const bindServiceSelect = document.getElementById('bind-service-select')
+const addNewConversation = document.getElementById('addNewConversation');
+
+const conversationBody = document.getElementById('conversationsBody');
+let conversationState = {};
/**
- * Adds new conversation id to local storage
- * @param cid - conversation id to add
- * @param skin - conversation skin to add
+ * Clears conversation state cache
+ * @param cid - Conversation ID to clear
*/
-async function addNewCID(cid, skin) {
- return await getChatAlignmentTable().put({
- 'cid': cid,
- 'skin': skin,
- 'added_on': getCurrentTimestamp()
- }, [cid]);
+const clearStateCache = (cid) => {
+ delete conversationState[cid];
}
-
/**
- * Removed conversation id from local storage
- * @param cid - conversation id to remove
+ * Sets all participants counters to zero
*/
-async function removeConversation(cid) {
- return await Promise.all([DBGateway.getInstance(DB_TABLES.CHAT_ALIGNMENT).deleteItem(cid),
- DBGateway.getInstance(DB_TABLES.CHAT_MESSAGES_PAGINATION).deleteItem(cid)
- ]);
+const setAllCountersToZero = () => {
+ const countNodes = document.querySelectorAll('[id^="participants-count-"]');
+ countNodes.forEach(node => node.innerText = 0);
}
+
/**
- * Checks if conversation is displayed
- * @param cid - target conversation id
- *
- * @return true if cid is stored in client db, false otherwise
+ * Sets participants count for conversation view
+ * @param cid - desired conversation id
*/
-function isDisplayed(cid) {
- return document.getElementById(cid) !== null;
+const refreshSubmindsCount = (cid) => {
+ const participantsCountNode = document.getElementById(`participants-count-${cid}`);
+ if (participantsCountNode) {
+ let submindsCount = 0
+ if (!isEmpty(submindsState)) {
+ submindsCount = submindsState["subminds_per_cid"][cid].filter(submind => {
+ const connectedSubmind = submindsState.connected_subminds[submind.submind_id];
+ return connectedSubmind && connectedSubmind.bot_type === "submind" && submind.status === "active";
+ }).length;
+ }
+ participantsCountNode.innerText = submindsCount;
+ }
}
/**
- * Gets value of desired property in stored conversation
+ * Saves attached files to the server
* @param cid - target conversation id
- *
- * @return true if cid is displayed, false otherwise
+ * @return attachments array or `-1` if something went wrong
*/
-async function getStoredConversationData(cid) {
- return await getChatAlignmentTable().where({
- cid: cid
- }).first();
+const saveAttachedFiles = async (cid) => {
+ const filesArr = getUploadedFiles(cid);
+ const attachments = [];
+ if (filesArr.length > 0) {
+ setChatState(cid, 'updating', 'Saving attachments...');
+ let errorOccurred = null;
+ const formData = new FormData();
+ const attachmentProperties = {}
+ filesArr.forEach(file => {
+ const generatedFileName = `${generateUUID(10,'00041000')}.${file.name.split('.').pop()}`;
+ attachmentProperties[generatedFileName] = {
+ 'size': file.size,
+ 'type': file.type
+ }
+ const renamedFile = new File([file], generatedFileName, {
+ type: file.type
+ });
+ formData.append('files', renamedFile);
+ });
+ cleanUploadedFiles(cid);
+
+ await fetchServer(`files/attachments`, REQUEST_METHODS.POST, formData)
+ .then(async response => {
+ const responseJson = await response.json();
+ if (response.ok) {
+ for (const [fileName, savedName] of Object.entries(responseJson['location_mapping'])) {
+ attachments.push({
+ 'name': savedName,
+ 'size': attachmentProperties[fileName].size,
+ 'mime': attachmentProperties[fileName].type
+ })
+ }
+ } else {
+ throw `Failed to save attachments status=${response.status}, msg=${responseJson}`;
+ }
+ }).catch(err => {
+ errorOccurred = err;
+ });
+ setChatState(cid, 'active')
+ if (errorOccurred) {
+ console.error(`Error during attachments preparation: ${errorOccurred}, skipping message sending`);
+ return -1
+ } else {
+ console.log('Received attachments array: ', attachments);
+ }
+ }
+ return attachments;
}
/**
- * Returns current skin of provided conversation id
- * @param cid - target conversation id
- *
- * @return {string} skin from CONVERSATION_SKINS
+ * Supported conversation skins
+ * @type Object
*/
-async function getCurrentSkin(cid) {
- const storedCID = await getStoredConversationData(cid);
- if (storedCID) {
- return storedCID['skin'];
- }
- return null;
+const CONVERSATION_SKINS = {
+ BASE: 'base',
+ PROMPTS: 'prompts'
}
/**
- * Boolean function that checks whether live chats must be displayed based on page meta properties
- * @returns {boolean} true if live chat should be displayed, false otherwise
+ * Initiates selection of the table rows.
+ * @param table - target table to select
+ * @param exportToExcelBtn - DOM element of `Export to Excel` button
*/
-const shouldDisplayLiveChat = () => {
- const liveMetaElem = document.querySelector("meta[name='live']");
- if (liveMetaElem) {
- return liveMetaElem.getAttribute("content") === "1"
+const startSelection = (table, exportToExcelBtn) => {
+ table.classList.remove('selected');
+ const container = table.parentElement.parentElement;
+ if (Array.from(container.getElementsByClassName('selected')).length === 0) {
+ exportToExcelBtn.disabled = true;
}
- return false
+ startTimer();
}
+
/**
- * Fetches latest live conversation from the klat server API and builds its HTML
- * @returns {Promise<*>} fetched conversation data
+ * Marks target table as selected
+ * @param table - HTMLTable element
+ * @param exportToExcelBtn - export to excel button (optional)
*/
-const displayLiveChat = async () => {
- return await fetchServer('chat_api/live')
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw response.statusText;
- }
- })
- .then(data => {
- if (getUserMessages(data, null).length === 0) {
- console.debug('All of the messages are already displayed');
- setDefault(setDefault(conversationState, data['_id'], {}), 'all_messages_displayed', true);
- }
- return data;
- })
- .then(
- async data => {
- await buildConversation(data, CONVERSATION_SKINS.PROMPTS, true);
- return data;
- }
- )
- .catch(async err => {
- console.warn('Failed to display live chat:', err);
- });
+const selectTable = (table, exportToExcelBtn = null) => {
+ const timePassed = stopTimer();
+ if (timePassed >= 300) {
+ if (exportToExcelBtn)
+ exportToExcelBtn.disabled = false;
+ table.classList.add('selected');
+ }
}
/**
- * Restores chat alignment based on the page cache
+ * Wraps the provided array of HTMLTable elements into XLSX file and exports it to the invoked user
+ * @param tables - array of HTMLTable elements to export
+ * @param filePrefix - prefix of the file name to be imported
+ * @param sheetPrefix - prefix to apply for each sheet generated per HTMLTable
+ * @param appname - name of the application to export (defaults to Excel)
*/
-const restoreChatAlignmentFromCache = async () => {
- let cachedItems = await retrieveItemsLayout();
- if (cachedItems.length === 0) {
- await displayLiveChat();
+const exportTablesToExcel = (function() {
+ const uri = 'data:application/vnd.ms-excel;base64,';
+ const tmplWorkbookXML = `
+
+
+
+
+PyKlatchat Generator
+{created}
+
+
+'
+'
+
+{worksheets}
+
+`
+ const tmplWorksheetXML = ''
+ const tmplCellXML = '{data} | '
+ const base64 = function(s) {
+ return window.btoa(unescape(encodeURIComponent(s)))
}
- for (const item of cachedItems) {
- await getConversationDataByInput(item.cid, item.skin).then(async conversationData => {
- if (conversationData && Object.keys(conversationData).length > 0) {
- await buildConversation(conversationData, item.skin, false);
- } else {
- if (item.cid !== '1') {
- displayAlert(document.getElementById('conversationsBody'), 'No matching conversation found', 'danger', 'noRestoreConversationAlert', {
- 'type': alertBehaviors.AUTO_EXPIRE
- });
+ const format = function(s, c) {
+ return s.replace(/{(\w+)}/g, function(m, p) {
+ return c[p];
+ })
+ }
+ return function(tables, filePrefix, sheetPrefix = '', appname = 'Excel') {
+ let ctx = "";
+ let workbookXML = "";
+ let worksheetsXML = "";
+ let rowsXML = "";
+
+ for (let i = 0; i < tables.length; i++) {
+ if (!tables[i].nodeType) tables[i] = document.getElementById(tables[i]);
+ for (let j = 0; j < tables[i].rows.length; j++) {
+ rowsXML += ''
+ for (let k = 0; k < tables[i].rows[j].cells.length; k++) {
+ let data = tables[i].rows[j].cells[k].innerHTML
+ if (k === 0) {
+ const chatImgElem = tables[i].rows[j].cells[k].getElementsByClassName("chat-img")[0]
+ if (chatImgElem) {
+ data = chatImgElem.getAttribute("title");
+ }
+ }
+ ctx = {
+ data: data,
+ };
+ rowsXML += format(tmplCellXML, ctx);
}
- await removeConversation(item.cid);
+ rowsXML += '
'
}
- });
- }
-}
+ const sheetName = sheetPrefix.replaceAll("{id}", tables[i].id);
+ ctx = {
+ rows: rowsXML,
+ nameWS: sheetName || 'Sheet' + i
+ };
+ worksheetsXML += format(tmplWorksheetXML, ctx);
+ rowsXML = "";
+ }
-/**
- * Custom Event fired on supported languages init
- * @type {CustomEvent}
- */
-const chatAlignmentRestoredEvent = new CustomEvent("chatAlignmentRestored", {
- "detail": "Event that is fired when chat alignment is restored"
-});
+ ctx = {
+ created: getCurrentTimestamp() * 1000,
+ worksheets: worksheetsXML
+ };
+ workbookXML = format(tmplWorkbookXML, ctx);
-/**
- * Restores chats alignment from the local storage
- **/
-async function restoreChatAlignment() {
- if (shouldDisplayLiveChat()) {
- await displayLiveChat();
- } else {
- await restoreChatAlignmentFromCache();
+ let link = document.createElement("A");
+ link.href = uri + base64(workbookXML);
+ const fileName = `${filePrefix}_${getCurrentTimestamp()}`;
+ link.download = `${fileName}.xls`;
+ link.target = '_blank';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
}
- console.debug('Chat Alignment Restored');
- document.dispatchEvent(chatAlignmentRestoredEvent);
-}
-
+})();
-/**
- * Helper struct to decide on which kind of messages to refer
- * "all" - all the messages
- * "mine" - only the messages emitted by current user
- * "others" - all the messages except "mine"
- */
-const MESSAGE_REFER_TYPE = {
- ALL: 'all',
- MINE: 'mine',
- OTHERS: 'other'
-}
/**
- * Gets array of messages for provided conversation id
+ * Sends the message based on input
+ * @param inputElem - input DOM element
* @param cid - target conversation id
- * @param messageReferType - message refer type to consider from `MESSAGE_REFER_TYPE`
- * @param idOnly - to return id only (defaults to false)
- * @param forceType - to get only the certain type of messages (optional)
- *
- * @return array of message DOM objects under given conversation
+ * @param repliedMessageId - replied message id (optional)
+ * @param isAudio - `1` if the message is audio-message (defaults to `0`)
+ * @param isAnnouncement - `1` if the message is an announcement (defaults to `0`)
*/
-function getMessagesOfCID(cid, messageReferType = MESSAGE_REFER_TYPE.ALL, forceType = null, idOnly = false) {
- let messages = []
- const messageContainer = getMessageListContainer(cid);
- if (messageContainer) {
- const listItems = messageContainer.getElementsByTagName('li');
- Array.from(listItems).forEach(li => {
- try {
- const messageNode = getMessageNode(li, forceType);
- if (messageNode) {
- if (messageReferType === MESSAGE_REFER_TYPE.ALL ||
- (messageReferType === MESSAGE_REFER_TYPE.MINE && messageNode.getAttribute('data-sender') === currentUser['nickname']) ||
- (messageReferType === MESSAGE_REFER_TYPE.OTHERS && messageNode.getAttribute('data-sender') !== currentUser['nickname'])) {
- if (idOnly) {
- messages.push(messageNode.id);
- } else {
- messages.push(messageNode);
- }
- }
- }
- } catch (e) {
- console.warn(`Failed to get message under node: ${li} - ${e}`);
- }
- });
+const sendMessage = async (inputElem, cid, repliedMessageId = null, isAudio = '0', isAnnouncement = '0') => {
+ const attachments = await saveAttachedFiles(cid);
+ if (Array.isArray(attachments)) {
+ emitUserMessage(inputElem, cid, repliedMessageId, attachments, isAudio, isAnnouncement);
}
- return messages;
+ inputElem.value = "";
}
/**
- * Refreshes chat view (for instance when user session gets updated)
+ * Gets all opened chat ids
+ * @return {[]} list of displayed chat ids
*/
-function refreshChatView(conversationContainer = null) {
- if (!conversationContainer) {
- conversationContainer = conversationBody;
- }
- Array.from(conversationContainer.getElementsByClassName('conversationContainer')).forEach(async conversation => {
- const cid = conversation.getElementsByClassName('card')[0].id;
- const skin = await getCurrentSkin(cid);
- if (skin === CONVERSATION_SKINS.BASE) {
- const messages = getMessagesOfCID(cid, MESSAGE_REFER_TYPE.ALL, 'plain');
- Array.from(messages).forEach(message => {
- if (message.hasAttribute('data-sender')) {
- const messageSenderNickname = message.getAttribute('data-sender');
- if (message.parentElement.parentElement.className !== 'announcement')
- message.parentElement.parentElement.className = (currentUser && messageSenderNickname === currentUser['nickname']) ? 'in' : 'out';
- }
- });
- }
- await initLanguageSelectors(cid);
+function getOpenedChatIds() {
+ let cids = [];
+ Array.from(conversationBody.getElementsByClassName('conversationContainer')).forEach(conversationContainer => {
+ cids.push(conversationContainer.getElementsByClassName('card')[0].id);
});
+ return cids;
}
-/**
- * Enum of possible displayed chat states
- * "active" - ready to be used by user
- * "updating" - in processes of applying changes, temporary unavailable
- */
-const CHAT_STATES = {
- ACTIVE: 'active',
- UPDATING: 'updating',
+const resizeConversationContainers = () => {
+ const openedChatIds = getOpenedChatIds();
+ const newWidth = `${100/openedChatIds.length}vw`;
+ openedChatIds.forEach(cid => {
+ document.getElementById(cid).style.width = newWidth;
+ })
}
/**
- * Sets state to the desired cid
- * @param cid - target conversation id
- * @param state - the new chat state from `CHAT_STATES`
- * @param state_msg - reason for state transitioning (optional)
+ * Builds new conversation HTML from provided data and attaches it to the list of displayed conversations
+ * @param conversationData - JS Object containing conversation data of type:
+ * {
+ * '_id': 'id of conversation',
+ * 'conversation_name': 'title of the conversation',
+ * 'chat_flow': [{
+ * 'user_nickname': 'nickname of sender',
+ * 'user_avatar': 'avatar of sender',
+ * 'message_id': 'id of the message',
+ * 'message_text': 'text of the message',
+ * 'is_audio': true if message is an audio message
+ * 'is_announcement': true if message is considered to be an announcement
+ * 'created_on': 'creation time of the message'
+ * }, ... (num of user messages returned)]
+ * }
+ * @param skin - Conversation skin to build
+ * @param remember - to store this conversation into localStorage (defaults to true)*
+ * @param conversationParentID - ID of conversation parent
+ * @return id of the built conversation
*/
-function setChatState(cid, state = CHAT_STATES.ACTIVE, state_msg = '') {
- // TODO: refactor this method to handle when there are multiple messages on a stack
- // console.log(`cid=${cid}, state=${state}, state_msg=${state_msg}`)
- const cidNode = document.getElementById(cid);
- if (cidNode) {
- setDefault(setDefault(conversationState, cid, {}))
- const spinner = document.getElementById(`${cid}-spinner`);
- const spinnerUpdateMsg = document.getElementById(`${cid}-update-msg`);
- if (state === 'updating') {
- cidNode.classList.add('chat-loading');
- spinner.style.setProperty('display', 'flex', 'important');
- spinnerUpdateMsg.innerHTML = state_msg;
- } else if (state === 'active') {
- cidNode.classList.remove('chat-loading');
- spinner.style.setProperty('display', 'none', 'important');
- spinnerUpdateMsg.innerHTML = '';
- }
- conversationState[cid]['state'] = state;
- conversationState[cid]['state_message'] = state_msg;
+async function buildConversation(conversationData, skin, remember = true, conversationParentID = 'conversationsBody') {
+ const idField = '_id';
+ const cid = conversationData[idField];
+ if (!cid) {
+ console.error(`Failed to extract id field="${idField}" from conversation data - ${conversationData}`);
+ return -1;
}
-}
+ if (remember) {
+ await addNewCID(cid, skin);
+ }
+ const newConversationHTML = await buildConversationHTML(conversationData, skin);
+ const conversationsBody = document.getElementById(conversationParentID);
+ conversationsBody.insertAdjacentHTML('afterbegin', newConversationHTML);
-/**
- * Displays first conversation matching search string
- * @param searchStr - Search string to find matching conversation
- * @param skin - target conversation skin to display
- * @param alertParentID - id of the element to display alert in
- * @param conversationParentID - parent Node ID of the conversation
- */
-async function displayConversation(searchStr, skin = CONVERSATION_SKINS.PROMPTS, alertParentID = null, conversationParentID = 'conversationsBody') {
- if (getOpenedChatIds().length === configData.MAX_CONVERSATIONS_PER_PAGE) {
- alert(`Up to ${configData.MAX_CONVERSATIONS_PER_PAGE} allowed per page`)
- } else if (searchStr !== "") {
- const alertParent = document.getElementById(alertParentID || conversationParentID);
- await getConversationDataByInput(searchStr, skin, null, 10).then(async conversationData => {
- let responseOk = false;
- if (!conversationData || Object.keys(conversationData).length === 0) {
- displayAlert(
- alertParent,
- 'Cannot find conversation matching your search',
- 'danger',
- 'noSuchConversationAlert', {
- 'type': alertBehaviors.AUTO_EXPIRE
- }
- );
- } else if (isDisplayed(conversationData['_id'])) {
- displayAlert(alertParent, 'Chat is already displayed', 'danger');
- } else {
- await buildConversation(conversationData, skin, true, conversationParentID);
- if (skin === CONVERSATION_SKINS.BASE) {
- for (const inputType of ['incoming', 'outcoming']) {
- await requestTranslation(conversationData['_id'], null, null, inputType);
- }
- }
- responseOk = true;
- if (configData.client === CLIENTS.NANO) {
- attachEditModalInvoker(document.getElementById(`${conversationData['_id']}-account-link`));
- updateNavbar();
- initSettings(document.getElementById(`${conversationData['_id']}-settings-link`));
- }
+ resizeConversationContainers()
+
+ setChatState(cid, CHAT_STATES.UPDATING, "Loading messages...")
+ initMessages(conversationData, skin).then(_ => setChatState(cid, CHAT_STATES.ACTIVE));
+
+ const messageListContainer = getMessageListContainer(cid);
+ const currentConversation = document.getElementById(cid);
+ const conversationParent = currentConversation.parentElement;
+ const conversationHolder = conversationParent.parentElement;
+
+ let chatCloseButton = document.getElementById(`close-${cid}`);
+ const chatInputButton = document.getElementById(conversationData['_id'] + '-send');
+ const filenamesContainer = document.getElementById(`filename-container-${conversationData['_id']}`)
+ const attachmentsButton = document.getElementById('file-input-' + conversationData['_id']);
+ const textInputElem = document.getElementById(conversationData['_id'] + '-input');
+ if (chatInputButton.hasAttribute('data-target-cid')) {
+ textInputElem.addEventListener('keyup', async (e) => {
+ if (e.shiftKey && e.key === 'Enter') {
+ await sendMessage(textInputElem, conversationData['_id']);
}
- return responseOk;
+ });
+ chatInputButton.addEventListener('click', async (e) => {
+ await sendMessage(textInputElem, conversationData['_id']);
});
}
-}
-
-/**
- * Handles requests on creation new conversation by the user
- * @param conversationName - New Conversation Name
- * @param isPrivate - if conversation should be private (defaults to false)
- * @param boundServiceID - id of the service to bind to conversation (optional)
- * @param createLiveConversation - if conversation should be treated as live conversation (defaults to false)
- */
-async function createNewConversation(conversationName, isPrivate = false, boundServiceID = null, createLiveConversation = false) {
-
- let formData = new FormData();
-
- formData.append('conversation_name', conversationName);
- formData.append('is_private', isPrivate ? '1' : '0')
- formData.append('bound_service', boundServiceID ? boundServiceID : '');
- formData.append('is_live_conversation', createLiveConversation ? '1' : '0')
- await fetchServer(`chat_api/new`, REQUEST_METHODS.POST, formData).then(async response => {
- const responseJson = await response.json();
- let responseOk = false;
- if (response.ok) {
- await buildConversation(responseJson, CONVERSATION_SKINS.PROMPTS);
- responseOk = true;
+ attachmentsButton.addEventListener('change', (e) => {
+ e.preventDefault();
+ const fileName = getFilenameFromPath(e.currentTarget.value);
+ const lastFile = attachmentsButton.files[attachmentsButton.files.length - 1]
+ if (lastFile.size > configData['maxUploadSize']) {
+ console.warn(`Uploaded file is too big`);
} else {
- displayAlert('newConversationModalBody',
- `${responseJson['msg']}`,
- 'danger');
+ addUpload(attachmentsButton.parentNode.parentNode.id, lastFile);
+ filenamesContainer.insertAdjacentHTML('afterbegin',
+ `${fileName}`);
+ filenamesContainer.style.display = "";
+ if (filenamesContainer.children.length === configData['maxNumAttachments']) {
+ attachmentsButton.disabled = true;
+ }
}
- return responseOk;
});
-}
+ await addRecorder(conversationData);
+ await initLanguageSelectors(conversationData['_id']);
-document.addEventListener('DOMContentLoaded', (_) => {
+ if (skin === CONVERSATION_SKINS.BASE) {
+ const promptModeButton = document.getElementById(`prompt-mode-${conversationData['_id']}`);
- if (configData['client'] === CLIENTS.MAIN) {
- document.addEventListener('supportedLanguagesLoaded', async (_) => {
- await refreshCurrentUser(false)
- .then(async _ => await restoreChatAlignment())
- .then(async _ => await refreshCurrentUser(true))
- .then(async _ => await requestChatsLanguageRefresh());
- });
- addBySearch.addEventListener('click', async (e) => {
+ promptModeButton.addEventListener('click', async (e) => {
e.preventDefault();
- displayConversation(conversationSearchInput.value, CONVERSATION_SKINS.PROMPTS, 'importConversationModalBody').then(responseOk => {
- conversationSearchInput.value = "";
- if (responseOk) {
- importConversationModal.modal('hide');
- }
- });
- });
- conversationSearchInput.addEventListener('input', async (_) => {
- await renderSuggestions();
+ chatCloseButton.click();
+ await displayConversation(conversationData['_id'], CONVERSATION_SKINS.PROMPTS, null, conversationParentID);
});
- addNewConversation.addEventListener('click', async (e) => {
+ } else if (skin === CONVERSATION_SKINS.PROMPTS) {
+ chatCloseButton = document.getElementById(`close-prompts-${cid}`);
+ const baseModeButton = document.getElementById(`base-mode-${cid}`);
+ const exportToExcelBtn = document.getElementById(`${cid}-export-to-excel`)
+
+ // TODO: fix here to use prompt- prefix
+ baseModeButton.addEventListener('click', async (e) => {
e.preventDefault();
- const newConversationName = document.getElementById('conversationName');
- const isPrivate = document.getElementById('isPrivate');
- const createLiveConversation = document.getElementById("createLiveConversation");
- let boundServiceID = bindServiceSelect.value;
+ chatCloseButton.click();
+ await displayConversation(cid, CONVERSATION_SKINS.BASE, null, conversationParentID);
+ });
- if (boundServiceID) {
- const targetItem = document.getElementById(boundServiceID);
- if (targetItem.value) {
- if (targetItem.nodeName === 'SELECT') {
- boundServiceID = targetItem.value;
- } else {
- boundServiceID = targetItem.getAttribute('data-value') + '.' + targetItem.value
- }
- } else {
- displayAlert('newConversationModalBody', 'Missing bound service name');
- return -1;
- }
- }
+ // TODO: make an array of prompt tables only in dedicated conversation
+ Array.from(getMessagesOfCID(cid, MESSAGE_REFER_TYPE.ALL, 'prompt', false)).forEach(table => {
+
+ table.addEventListener('mousedown', (_) => startSelection(table, exportToExcelBtn));
+ table.addEventListener('touchstart', (_) => startSelection(table, exportToExcelBtn));
+ table.addEventListener('mouseup', (_) => selectTable(table, exportToExcelBtn));
+ table.addEventListener("touchend", (_) => selectTable(table, exportToExcelBtn));
- createNewConversation(newConversationName.value, isPrivate.checked, boundServiceID, createLiveConversation.checked).then(responseOk => {
- newConversationName.value = "";
- isPrivate.checked = false;
- if (responseOk) {
- newConversationModal.modal('hide');
- }
- });
- });
- importConversationOpener.addEventListener('click', async (e) => {
- e.preventDefault();
- conversationSearchInput.value = "";
- await renderSuggestions();
});
- bindServiceSelect.addEventListener("change", function() {
- Array.from(document.getElementsByClassName('create-conversation-bind-group')).forEach(x => {
- x.hidden = true;
+ exportToExcelBtn.addEventListener('click', (e) => {
+ const selectedTables = messageListContainer.getElementsByClassName('selected');
+ exportTablesToExcel(selectedTables, `prompts_of_${cid}`, 'prompt_{id}');
+ Array.from(selectedTables).forEach(selectedTable => {
+ selectedTable.classList.remove('selected');
});
- if (bindServiceSelect.value) {
- const targetItem = document.getElementById(bindServiceSelect.value);
- targetItem.hidden = false;
- }
});
}
-});
-/**
- * Collection of supported clients, current client is matched based on client configuration
- * @type {{NANO: string, MAIN: string}}
- */
-const CLIENTS = {
- MAIN: 'main',
- NANO: 'nano',
- UNDEFINED: undefined
-}
-
-/**
- * JS Object containing frontend configuration data
- * @type {{staticFolder: string, currentURLBase: string, currentURLFull: (string|string|string|SVGAnimatedString|*), client: string}}
- */
-
-let configData = {
- 'staticFolder': "../../static",
- 'currentURLBase': extractURLBase(),
- 'currentURLFull': window.location.href,
- 'client': typeof metaConfig !== 'undefined' ? metaConfig?.client : CLIENTS.UNDEFINED,
- "MAX_CONVERSATIONS_PER_PAGE": 4,
-};
-/**
- * Default key for storing data in local storage
- * @type {string}
- */
-const conversationAlignmentKey = 'conversationAlignment';
+ if (chatCloseButton.hasAttribute('data-target-cid')) {
+ chatCloseButton.addEventListener('click', async (_) => {
+ conversationHolder.removeChild(conversationParent);
+ await removeConversation(cid);
+ clearStateCache(cid);
+ resizeConversationContainers()
+ });
+ }
+ // Hide close button for Nano Frames
+ if (configData.client === CLIENTS.NANO) {
+ chatCloseButton.hidden = true;
+ }
+ document.getElementById('klatchatHeader').scrollIntoView(true);
+ return cid;
+}
/**
- * Custom Event fired on configs ended up loading
- * @type {CustomEvent}
+ * Gets conversation data based on input string
+ * @param input - input string text
+ * @param oldestMessageTS - creation timestamp of the oldest displayed message
+ * @param skin - resolves by server for which data to return
+ * @param maxResults - max number of messages to fetch
+ * @returns {Promise<{}>} promise resolving conversation data returned
*/
-const configFullLoadedEvent = new CustomEvent("configLoaded", {
- "detail": "Event that is fired when configs are loaded"
-});
+async function getConversationDataByInput(input, skin, oldestMessageTS = null, maxResults = 10) {
+ let conversationData = {};
+ if (input) {
+ let query_url = `chat_api/search/${input.toString()}?limit_chat_history=${maxResults}&skin=${skin}`;
+ if (oldestMessageTS) {
+ query_url += `&creation_time_from=${oldestMessageTS}`;
+ }
+ await fetchServer(query_url)
+ .then(response => {
+ if (response.ok) {
+ return response.json();
+ } else {
+ throw response.statusText;
+ }
+ })
+ .then(data => {
+ if (getUserMessages(data, null).length === 0) {
+ console.log('All of the messages are already displayed');
+ setDefault(setDefault(conversationState, data['_id'], {}), 'all_messages_displayed', true);
+ }
+ conversationData = data;
+ }).catch(async err => {
+ console.warn('Failed to fulfill request due to error:', err);
+ });
+ }
+ return conversationData;
+}
/**
- * Convenience method for getting URL base for current page
- * @returns {string} constructed URL base
+ * Returns table representing chat alignment
+ * @return {Table}
*/
-function extractURLBase() {
- return window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
+const getChatAlignmentTable = () => {
+ return getDb(DATABASES.CHATS, DB_TABLES.CHAT_ALIGNMENT);
}
/**
- * Extracts json data from provided URL path
- * @param urlPath - file path string
- * @param onError - callback on extraction failure
- * @returns {Promise<* | {}>} promise that resolves data obtained from file path
+ * Retrieves conversation layout from local storage
+ * @returns {Array} collection of database-stored elements
*/
-async function extractJsonData(urlPath = "",
- onError = (e) => console.error(`failed to extractJsonData - ${e}`)) {
- return fetch(urlPath).then(response => {
- if (response.ok) {
- return response.json();
- }
- return {};
- }).catch(onError);
+async function retrieveItemsLayout(idOnly = false) {
+ let layout = await getChatAlignmentTable().orderBy("added_on").toArray();
+ if (idOnly) {
+ layout = layout.map(a => a.cid);
+ }
+ return layout;
}
-document.addEventListener('DOMContentLoaded', async (e) => {
- if (configData['client'] === CLIENTS.MAIN) {
- configData = Object.assign(configData, await extractJsonData(`${configData['currentURLBase']}/base/runtime_config`), (e) => location.reload());
- document.dispatchEvent(configFullLoadedEvent);
- }
-});
-const REQUEST_METHODS = {
- GET: 'GET',
- PUT: 'PUT',
- DELETE: 'DELETE',
- POST: 'POST'
+/**
+ * Adds new conversation id to local storage
+ * @param cid - conversation id to add
+ * @param skin - conversation skin to add
+ */
+async function addNewCID(cid, skin) {
+ return await getChatAlignmentTable().put({
+ 'cid': cid,
+ 'skin': skin,
+ 'added_on': getCurrentTimestamp()
+ }, [cid]);
}
-const controllers = new Set();
-
-
-const getSessionToken = () => {
- return localStorage.getItem('session') || '';
+/**
+ * Removed conversation id from local storage
+ * @param cid - conversation id to remove
+ */
+async function removeConversation(cid) {
+ return await Promise.all([DBGateway.getInstance(DB_TABLES.CHAT_ALIGNMENT).deleteItem(cid),
+ DBGateway.getInstance(DB_TABLES.CHAT_MESSAGES_PAGINATION).deleteItem(cid)
+ ]);
}
-const setSessionToken = (val) => {
- const currentValue = getSessionToken();
- localStorage.setItem('session', val);
- if (currentValue && currentValue !== val) {
- location.reload();
- }
+/**
+ * Checks if conversation is displayed
+ * @param cid - target conversation id
+ *
+ * @return true if cid is stored in client db, false otherwise
+ */
+function isDisplayed(cid) {
+ return document.getElementById(cid) !== null;
}
-const fetchServer = async (urlSuffix, method = REQUEST_METHODS.GET, body = null, json = false) => {
- const controller = new AbortController();
- controllers.add(controller);
- const signal = controller.signal;
- const options = {
- method: method,
- headers: new Headers({
- 'Authorization': getSessionToken()
- }),
- signal,
- }
- if (body) {
- options['body'] = body;
- }
- // TODO: there is an issue validating FormData on backend, so JSON property should eventually become true
- if (json) {
- options['headers'].append('Content-Type', 'application/json');
- if (options['body']) {
- options['body'] &&= JSON.stringify(options['body'])
- }
- }
- return fetch(`${configData["CHAT_SERVER_URL_BASE"]}/${urlSuffix}`, options).then(async response => {
- if (response.status === 401) {
- const responseJson = await response.json();
- if (responseJson['msg'] === 'Session token is invalid or expired') {
- localStorage.removeItem('session');
- location.reload();
- }
- }
- return response;
- }).finally(() => {
- controllers.delete(controller);
- });
+/**
+ * Gets value of desired property in stored conversation
+ * @param cid - target conversation id
+ *
+ * @return true if cid is displayed, false otherwise
+ */
+async function getStoredConversationData(cid) {
+ return await getChatAlignmentTable().where({
+ cid: cid
+ }).first();
}
-
-document.addEventListener('beforeunload', () => {
- for (const controller of controllers) {
- controller.abort();
+/**
+ * Returns current skin of provided conversation id
+ * @param cid - target conversation id
+ *
+ * @return {string} skin from CONVERSATION_SKINS
+ */
+async function getCurrentSkin(cid) {
+ const storedCID = await getStoredConversationData(cid);
+ if (storedCID) {
+ return storedCID['skin'];
}
-});
-let __inputFileList = {};
+ return null;
+}
/**
- * Gets uploaded files from specified conversation id
- * @param cid specified conversation id
- * @return {*} list of files from specified cid if any
+ * Boolean function that checks whether live chats must be displayed based on page meta properties
+ * @returns {boolean} true if live chat should be displayed, false otherwise
*/
-function getUploadedFiles(cid) {
- if (__inputFileList.hasOwnProperty(cid)) {
- return __inputFileList[cid];
+const shouldDisplayLiveChat = () => {
+ const liveMetaElem = document.querySelector("meta[name='live']");
+ if (liveMetaElem) {
+ return liveMetaElem.getAttribute("content") === "1"
}
- return [];
+ return false
}
/**
- * Cleans uploaded files per conversation
+ * Fetches latest live conversation from the klat server API and builds its HTML
+ * @returns {Promise<*>} fetched conversation data
*/
-function cleanUploadedFiles(cid) {
- if (__inputFileList.hasOwnProperty(cid)) {
- delete __inputFileList[cid];
- }
- const attachmentsButton = document.getElementById('file-input-' + cid);
- attachmentsButton.value = "";
- const fileContainer = document.getElementById('filename-container-' + cid);
- fileContainer.innerHTML = "";
+const displayLiveChat = async () => {
+ return await fetchServer('chat_api/live')
+ .then(response => {
+ if (response.ok) {
+ return response.json();
+ } else {
+ throw response.statusText;
+ }
+ })
+ .then(data => {
+ if (getUserMessages(data, null).length === 0) {
+ console.debug('All of the messages are already displayed');
+ setDefault(setDefault(conversationState, data['_id'], {}), 'all_messages_displayed', true);
+ }
+ return data;
+ })
+ .then(
+ async data => {
+ await buildConversation(data, CONVERSATION_SKINS.PROMPTS, true);
+ return data;
+ }
+ )
+ .catch(async err => {
+ console.warn('Failed to display live chat:', err);
+ });
}
/**
- * Adds File upload to specified cid
- * @param cid: mentioned cid
- * @param file: File object
+ * Restores chat alignment based on the page cache
*/
-function addUpload(cid, file) {
- if (!__inputFileList.hasOwnProperty(cid)) {
- __inputFileList[cid] = [];
+const restoreChatAlignmentFromCache = async () => {
+ let cachedItems = await retrieveItemsLayout();
+ if (cachedItems.length === 0) {
+ await displayLiveChat();
+ }
+ for (const item of cachedItems) {
+ await getConversationDataByInput(item.cid, item.skin).then(async conversationData => {
+ if (conversationData && Object.keys(conversationData).length > 0) {
+ await buildConversation(conversationData, item.skin, false);
+ } else {
+ if (item.cid !== '1') {
+ displayAlert(document.getElementById('conversationsBody'), 'No matching conversation found', 'danger', 'noRestoreConversationAlert', {
+ 'type': alertBehaviors.AUTO_EXPIRE
+ });
+ }
+ await removeConversation(item.cid);
+ }
+ });
}
- __inputFileList[cid].push(file);
}
/**
- * Adds download request on attachment item click
- * @param attachmentItem: desired attachment item
- * @param cid: current conversation id
- * @param messageID: current message id
+ * Custom Event fired on supported languages init
+ * @type {CustomEvent}
*/
-async function downloadAttachment(attachmentItem, cid, messageID) {
- if (attachmentItem) {
- const fileName = attachmentItem.getAttribute('data-file-name');
- const mime = attachmentItem.getAttribute('data-mime');
- const getFileURL = `files/${messageID}/get_attachment/${fileName}`;
- await fetchServer(getFileURL).then(async response => {
- response.ok ?
- download(await response.blob(), fileName, mime) :
- console.error(`No file data received for path,
-cid=${cid};\n
-message_id=${messageID};\n
-file_name=${fileName}`)
- }).catch(err => console.error(`Failed to fetch: ${getFileURL}: ${err}`));
+const chatAlignmentRestoredEvent = new CustomEvent("chatAlignmentRestored", {
+ "detail": "Event that is fired when chat alignment is restored"
+});
+
+/**
+ * Restores chats alignment from the local storage
+ **/
+async function restoreChatAlignment() {
+ if (shouldDisplayLiveChat()) {
+ await displayLiveChat();
+ } else {
+ await restoreChatAlignmentFromCache();
}
+ console.debug('Chat Alignment Restored');
+ document.dispatchEvent(chatAlignmentRestoredEvent);
}
+
/**
- * Attaches message replies to initialized conversation
- * @param conversationData: conversation data object
+ * Helper struct to decide on which kind of messages to refer
+ * "all" - all the messages
+ * "mine" - only the messages emitted by current user
+ * "others" - all the messages except "mine"
*/
-function addAttachments(conversationData) {
- if (conversationData.hasOwnProperty('chat_flow')) {
- getUserMessages(conversationData).forEach(message => {
- resolveMessageAttachments(conversationData['_id'], message['message_id'], message?.attachments);
- });
- }
+const MESSAGE_REFER_TYPE = {
+ ALL: 'all',
+ MINE: 'mine',
+ OTHERS: 'other'
}
/**
- * Activates attachments event listeners for message attachments in specified conversation
- * @param cid: desired conversation id
- * @param elem: parent element for attachment (defaults to document)
+ * Gets array of messages for provided conversation id
+ * @param cid - target conversation id
+ * @param messageReferType - message refer type to consider from `MESSAGE_REFER_TYPE`
+ * @param idOnly - to return id only (defaults to false)
+ * @param forceType - to get only the certain type of messages (optional)
+ *
+ * @return array of message DOM objects under given conversation
*/
-function activateAttachments(cid, elem = null) {
- if (!elem) {
- elem = document;
- }
- Array.from(elem.getElementsByClassName('attachment-item')).forEach(attachmentItem => {
- attachmentItem.addEventListener('click', async (e) => {
- e.preventDefault();
- const attachmentName = attachmentItem.getAttribute('data-file-name');
+function getMessagesOfCID(cid, messageReferType = MESSAGE_REFER_TYPE.ALL, forceType = null, idOnly = false) {
+ let messages = []
+ const messageContainer = getMessageListContainer(cid);
+ if (messageContainer) {
+ const listItems = messageContainer.getElementsByTagName('li');
+ Array.from(listItems).forEach(li => {
try {
- setChatState(cid, 'updating', `Downloading attachment file`);
- await downloadAttachment(attachmentItem, cid, attachmentItem.parentNode.parentNode.id);
+ const messageNode = getMessageNode(li, forceType);
+ if (messageNode) {
+ if (messageReferType === MESSAGE_REFER_TYPE.ALL ||
+ (messageReferType === MESSAGE_REFER_TYPE.MINE && messageNode.getAttribute('data-sender') === currentUser['nickname']) ||
+ (messageReferType === MESSAGE_REFER_TYPE.OTHERS && messageNode.getAttribute('data-sender') !== currentUser['nickname'])) {
+ if (idOnly) {
+ messages.push(messageNode.id);
+ } else {
+ messages.push(messageNode);
+ }
+ }
+ }
} catch (e) {
- console.warn(`Failed to download attachment file - ${attachmentName} (${e})`)
- } finally {
- setChatState(cid, 'active');
+ console.warn(`Failed to get message under node: ${li} - ${e}`);
}
});
- });
+ }
+ return messages;
}
+/**
+ * Refreshes chat view (for instance when user session gets updated)
+ */
+function refreshChatView(conversationContainer = null) {
+ if (!conversationContainer) {
+ conversationContainer = conversationBody;
+ }
+ Array.from(conversationContainer.getElementsByClassName('conversationContainer')).forEach(async conversation => {
+ const cid = conversation.getElementsByClassName('card')[0].id;
+ const skin = await getCurrentSkin(cid);
+ if (skin === CONVERSATION_SKINS.BASE) {
+ const messages = getMessagesOfCID(cid, MESSAGE_REFER_TYPE.ALL, 'plain');
+ Array.from(messages).forEach(message => {
+ if (message.hasAttribute('data-sender')) {
+ const messageSenderNickname = message.getAttribute('data-sender');
+ if (message.parentElement.parentElement.className !== 'announcement')
+ message.parentElement.parentElement.className = (currentUser && messageSenderNickname === currentUser['nickname']) ? 'in' : 'out';
+ }
+ });
+ }
+ await initLanguageSelectors(cid);
+ });
+}
/**
- * Returns DOM element to include as file resolver based on its name
- * @param filename: name of file to fetch
- * @return {string}: resulting DOM element
+ * Enum of possible displayed chat states
+ * "active" - ready to be used by user
+ * "updating" - in processes of applying changes, temporary unavailable
*/
-function attachmentHTMLBasedOnFilename(filename) {
+const CHAT_STATES = {
+ ACTIVE: 'active',
+ UPDATING: 'updating',
+}
- let fSplitted = filename.split('.');
- if (fSplitted.length > 1) {
- const extension = fSplitted.pop();
- const shrinkedName = shrinkToFit(filename, 12, `...${extension}`);
- if (IMAGE_EXTENSIONS.includes(extension)) {
- return ` ${shrinkedName}`;
- } else {
- return shrinkedName;
+/**
+ * Sets state to the desired cid
+ * @param cid - target conversation id
+ * @param state - the new chat state from `CHAT_STATES`
+ * @param state_msg - reason for state transitioning (optional)
+ */
+function setChatState(cid, state = CHAT_STATES.ACTIVE, state_msg = '') {
+ // TODO: refactor this method to handle when there are multiple messages on a stack
+ // console.log(`cid=${cid}, state=${state}, state_msg=${state_msg}`)
+ const cidNode = document.getElementById(cid);
+ if (cidNode) {
+ setDefault(setDefault(conversationState, cid, {}))
+ const spinner = document.getElementById(`${cid}-spinner`);
+ const spinnerUpdateMsg = document.getElementById(`${cid}-update-msg`);
+ if (state === 'updating') {
+ cidNode.classList.add('chat-loading');
+ spinner.style.setProperty('display', 'flex', 'important');
+ spinnerUpdateMsg.innerHTML = state_msg;
+ } else if (state === 'active') {
+ cidNode.classList.remove('chat-loading');
+ spinner.style.setProperty('display', 'none', 'important');
+ spinnerUpdateMsg.innerHTML = '';
}
+ conversationState[cid]['state'] = state;
+ conversationState[cid]['state_message'] = state_msg;
}
- return shrinkToFit(filename, 12);
}
/**
- * Resolves attachments to the message
- * @param cid: id of conversation
- * @param messageID: id of user message
- * @param attachments list of attachments received
+ * Displays first conversation matching search string
+ * @param searchStr - Search string to find matching conversation
+ * @param skin - target conversation skin to display
+ * @param alertParentID - id of the element to display alert in
+ * @param conversationParentID - parent Node ID of the conversation
*/
-function resolveMessageAttachments(cid, messageID, attachments = []) {
- if (messageID) {
- const messageElem = document.getElementById(messageID);
- if (messageElem) {
- const attachmentToggle = messageElem.getElementsByClassName('attachment-toggle')[0];
- if (attachments.length > 0) {
- if (messageElem) {
- const attachmentPlaceholder = messageElem.getElementsByClassName('attachments-placeholder')[0];
- attachments.forEach(attachment => {
- const attachmentHTML = `
-${attachmentHTMLBasedOnFilename(attachment['name'])}
-
`;
- attachmentPlaceholder.insertAdjacentHTML('afterbegin', attachmentHTML);
- });
- attachmentToggle.addEventListener('click', (e) => {
- attachmentPlaceholder.style.display = attachmentPlaceholder.style.display === "none" ? "" : "none";
- });
- activateAttachments(cid, attachmentPlaceholder);
- attachmentToggle.style.display = "";
- // attachmentPlaceholder.style.display = "";
- }
+async function displayConversation(searchStr, skin = CONVERSATION_SKINS.PROMPTS, alertParentID = null, conversationParentID = 'conversationsBody') {
+ if (getOpenedChatIds().length === configData.MAX_CONVERSATIONS_PER_PAGE) {
+ alert(`Up to ${configData.MAX_CONVERSATIONS_PER_PAGE} allowed per page`)
+ } else if (searchStr !== "") {
+ const alertParent = document.getElementById(alertParentID || conversationParentID);
+ await getConversationDataByInput(searchStr, skin, null, 10).then(async conversationData => {
+ let responseOk = false;
+ if (!conversationData || Object.keys(conversationData).length === 0) {
+ displayAlert(
+ alertParent,
+ 'Cannot find conversation matching your search',
+ 'danger',
+ 'noSuchConversationAlert', {
+ 'type': alertBehaviors.AUTO_EXPIRE
+ }
+ );
+ } else if (isDisplayed(conversationData['_id'])) {
+ displayAlert(alertParent, 'Chat is already displayed', 'danger');
} else {
- attachmentToggle.style.display = "none";
+ await buildConversation(conversationData, skin, true, conversationParentID);
+ if (skin === CONVERSATION_SKINS.BASE) {
+ for (const inputType of ['incoming', 'outcoming']) {
+ await requestTranslation(conversationData['_id'], null, null, inputType);
+ }
+ }
+ responseOk = true;
+ if (configData.client === CLIENTS.NANO) {
+ attachEditModalInvoker(document.getElementById(`${conversationData['_id']}-account-link`));
+ updateNavbar();
+ initSettings(document.getElementById(`${conversationData['_id']}-settings-link`));
+ }
}
- }
+ return responseOk;
+ });
}
}
+
/**
- * Returns current UNIX timestamp in seconds
- * @return {number}: current unix timestamp
+ * Handles requests on creation new conversation by the user
+ * @param conversationName - New Conversation Name
+ * @param isPrivate - if conversation should be private (defaults to false)
+ * @param boundServiceID - id of the service to bind to conversation (optional)
+ * @param createLiveConversation - if conversation should be treated as live conversation (defaults to false)
*/
-const getCurrentTimestamp = () => {
- return Math.floor(Date.now() / 1000);
-};
+async function createNewConversation(conversationName, isPrivate = false, boundServiceID = null, createLiveConversation = false) {
-// Client's timer
-// TODO consider refactoring to "timer per component" if needed
-let __timer = 0;
+ let formData = new FormData();
+
+ formData.append('conversation_name', conversationName);
+ formData.append('is_private', isPrivate ? '1' : '0')
+ formData.append('bound_service', boundServiceID ? boundServiceID : '');
+ formData.append('is_live_conversation', createLiveConversation ? '1' : '0')
+ await fetchServer(`chat_api/new`, REQUEST_METHODS.POST, formData).then(async response => {
+ const responseJson = await response.json();
+ let responseOk = false;
+ if (response.ok) {
+ await buildConversation(responseJson, CONVERSATION_SKINS.PROMPTS);
+ responseOk = true;
+ } else {
+ displayAlert('newConversationModalBody',
+ `${responseJson['msg']}`,
+ 'danger');
+ }
+ return responseOk;
+ });
+}
-/**
- * Sets timer to current timestamp
- */
-const startTimer = () => {
- __timer = Date.now();
-};
+document.addEventListener('DOMContentLoaded', (_) => {
-/**
- * Resets times and returns time elapsed since invocation of startTimer()
- * @return {number} Number of seconds elapsed
- */
-const stopTimer = () => {
- const timeDue = Date.now() - __timer;
- __timer = 0;
- return timeDue;
-};
+ if (configData['client'] === CLIENTS.MAIN) {
+ document.addEventListener('supportedLanguagesLoaded', async (_) => {
+ await refreshCurrentUser(false)
+ .then(async _ => await restoreChatAlignment())
+ .then(async _ => await refreshCurrentUser(true))
+ .then(async _ => await requestChatsLanguageRefresh());
+ });
+ addBySearch.addEventListener('click', async (e) => {
+ e.preventDefault();
+ displayConversation(conversationSearchInput.value, CONVERSATION_SKINS.PROMPTS, 'importConversationModalBody').then(responseOk => {
+ conversationSearchInput.value = "";
+ if (responseOk) {
+ importConversationModal.modal('hide');
+ }
+ });
+ });
+ conversationSearchInput.addEventListener('input', async (_) => {
+ await renderSuggestions();
+ });
+ addNewConversation.addEventListener('click', async (e) => {
+ e.preventDefault();
+ const newConversationName = document.getElementById('conversationName');
+ const isPrivate = document.getElementById('isPrivate');
+ const createLiveConversation = document.getElementById("createLiveConversation");
+ let boundServiceID = bindServiceSelect.value;
+
+ if (boundServiceID) {
+ const targetItem = document.getElementById(boundServiceID);
+ if (targetItem.value) {
+ if (targetItem.nodeName === 'SELECT') {
+ boundServiceID = targetItem.value;
+ } else {
+ boundServiceID = targetItem.getAttribute('data-value') + '.' + targetItem.value
+ }
+ } else {
+ displayAlert('newConversationModalBody', 'Missing bound service name');
+ return -1;
+ }
+ }
+
+ createNewConversation(newConversationName.value, isPrivate.checked, boundServiceID, createLiveConversation.checked).then(responseOk => {
+ newConversationName.value = "";
+ isPrivate.checked = false;
+ if (responseOk) {
+ newConversationModal.modal('hide');
+ }
+ });
+ });
+ importConversationOpener.addEventListener('click', async (e) => {
+ e.preventDefault();
+ conversationSearchInput.value = "";
+ await renderSuggestions();
+ });
+ bindServiceSelect.addEventListener("change", function() {
+ Array.from(document.getElementsByClassName('create-conversation-bind-group')).forEach(x => {
+ x.hidden = true;
+ });
+ if (bindServiceSelect.value) {
+ const targetItem = document.getElementById(bindServiceSelect.value);
+ targetItem.hidden = false;
+ }
+ });
+ }
+});
const configNanoLoadedEvent = new CustomEvent("configNanoLoaded", {
"detail": "Event that is fired when nano configs are loaded"
});
diff --git a/version.py b/version.py
index d1270ea5..75590e28 100644
--- a/version.py
+++ b/version.py
@@ -26,5 +26,5 @@
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-__version__ = "0.4.12a1"
-__version_ts__ = 1738931283
+__version__ = "0.4.12a2"
+__version_ts__ = 1739470832