/* the bar on the right with the chats (Channel & Client) */ import {settings, Settings} from "tc-shared/settings"; import {LogCategory} from "tc-shared/log"; import {format, helpers} from "tc-shared/ui/frames/side/chat_helper"; import {bbcode_chat} from "tc-shared/ui/frames/chat"; import {Frame} from "tc-shared/ui/frames/chat_frame"; import {ChatBox} from "tc-shared/ui/frames/side/chat_box"; import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; import * as log from "tc-shared/log"; import * as htmltags from "tc-shared/ui/htmltags"; declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; export type PrivateConversationViewEntry = { html_tag: JQuery; } export type PrivateConversationMessageData = { message_id: string; message: string; sender: "self" | "partner"; sender_name: string; sender_unique_id: string; sender_client_id: number; timestamp: number; }; export type PrivateConversationViewMessage = PrivateConversationMessageData & PrivateConversationViewEntry & { time_update_id: number; }; export type PrivateConversationViewSpacer = PrivateConversationViewEntry; export enum PrivateConversationState { OPEN, CLOSED, DISCONNECTED, DISCONNECTED_SELF, } export type DisplayedMessage = { timestamp: number; message: PrivateConversationViewMessage | PrivateConversationViewEntry; message_type: "spacer" | "message"; /* structure as following 1. time pointer 2. unread 3. message */ tag_message: JQuery; tag_unread: PrivateConversationViewSpacer | undefined; tag_timepointer: PrivateConversationViewSpacer | undefined; } export class PrivateConveration { readonly handle: PrivateConverations; private _html_entry_tag: JQuery; private _message_history: PrivateConversationMessageData[] = []; private _callback_message: (text: string) => any; private _state: PrivateConversationState; private _last_message_updater_id: number; private _last_typing: number = 0; private _typing_timeout: number = 4000; private _typing_timeout_task: number; _scroll_position: number | undefined; /* undefined to follow bottom | position for special stuff */ _html_message_container: JQuery; /* only set when this chat is selected! */ client_unique_id: string; client_id: number; client_name: string; private _displayed_messages: DisplayedMessage[] = []; private _displayed_messages_length: number = 500; private _spacer_unread_message: DisplayedMessage; constructor(handle: PrivateConverations, client_unique_id: string, client_name: string, client_id: number) { this.handle = handle; this.client_name = client_name; this.client_unique_id = client_unique_id; this.client_id = client_id; this._state = PrivateConversationState.OPEN; this._build_entry_tag(); this.set_unread_flag(false); this.load_history(); } private history_key() { return this.handle.handle.handle.channelTree.server.properties.virtualserver_unique_identifier + "_" + this.client_unique_id; } private load_history() { helpers.history.load_history(this.history_key()).then((data: PrivateConversationMessageData[]) => { if(!data) return; const flag_unread = !!this._spacer_unread_message; for(const message of data.slice(data.length > this._displayed_messages_length ? data.length - this._displayed_messages_length : 0)) { this.append_message(message.message, { type: message.sender, name: message.sender_name, unique_id: message.sender_unique_id, client_id: message.sender_client_id }, new Date(message.timestamp), false); } if(!flag_unread) this.set_unread_flag(false); this.fix_scroll(false); this.save_history(); }).catch(error => { log.warn(LogCategory.CHAT, tr("Failed to load private conversation history for user %s on server %s: %o"), this.client_unique_id, this.handle.handle.handle.channelTree.server.properties.virtualserver_unique_identifier, error); }) } private save_history() { helpers.history.save_history(this.history_key(), this._message_history).catch(error => { log.warn(LogCategory.CHAT, tr("Failed to save private conversation history for user %s on server %s: %o"), this.client_unique_id, this.handle.handle.handle.channelTree.server.properties.virtualserver_unique_identifier, error); }); } entry_tag() : JQuery { return this._html_entry_tag; } destroy() { this._html_message_container = undefined; /* we do not own this container */ this.clear_messages(false); this._html_entry_tag && this._html_entry_tag.remove(); this._html_entry_tag = undefined; this._message_history = undefined; if(this._typing_timeout_task) clearTimeout(this._typing_timeout_task); } private _2d_flat(array: T[][]) : T[] { const result = []; for(const a of array) result.push(...a.filter(e => typeof(e) !== "undefined")); return result; } messages_tags() : JQuery[] { return this._2d_flat(this._displayed_messages.slice().reverse().map(e => [ e.tag_timepointer ? e.tag_timepointer.html_tag : undefined, e.tag_unread ? e.tag_unread.html_tag : undefined, e.tag_message ])); } append_message(message: string, sender: { type: "self" | "partner"; name: string; unique_id: string; client_id: number; }, timestamp?: Date, save_history?: boolean) { const message_date = timestamp || new Date(); const message_timestamp = message_date.getTime(); const packed_message = { message: message, sender: sender.type, sender_name: sender.name, sender_client_id: sender.client_id, sender_unique_id: sender.unique_id, timestamp: message_date.getTime(), message_id: 'undefined' }; /* first of all register message in message history */ { let index = 0; for(;index < this._message_history.length; index++) { if(this._message_history[index].timestamp > message_timestamp) continue; this._message_history.splice(index, 0, packed_message); break; } if(index > 100) return; /* message is too old to be displayed */ if(index >= this._message_history.length) this._message_history.push(packed_message); while(this._message_history.length > 100) this._message_history.pop(); } if(sender.type === "partner") { clearTimeout(this._typing_timeout_task); this._typing_timeout_task = 0; if(this.typing_active()) { this._last_typing = 0; this.typing_expired(); } else { this._update_message_timestamp(); } } else { this._update_message_timestamp(); } if(typeof(save_history) !== "boolean" || save_history) this.save_history(); /* insert in view */ { const basic_view_entry = this._build_message(packed_message); this._register_displayed_message({ timestamp: basic_view_entry.timestamp, message: basic_view_entry, message_type: "message", tag_message: basic_view_entry.html_tag, tag_timepointer: undefined, tag_unread: undefined }, true); } } private _displayed_message_first_tag(message: DisplayedMessage) { const tp = message.tag_timepointer ? message.tag_timepointer.html_tag : undefined; const tu = message.tag_unread ? message.tag_unread.html_tag : undefined; return tp || tu || message.tag_message; } private _destroy_displayed_message(message: DisplayedMessage, update_pointers: boolean) { if(update_pointers) { const index = this._displayed_messages.indexOf(message); if(index != -1 && index > 0) { const next = this._displayed_messages[index - 1]; if(!next.tag_timepointer && message.tag_timepointer) { next.tag_timepointer = message.tag_timepointer; message.tag_timepointer = undefined; } if(!next.tag_unread && message.tag_unread) { this._spacer_unread_message = next; next.tag_unread = message.tag_unread; message.tag_unread = undefined; } } if(message == this._spacer_unread_message) this._spacer_unread_message = undefined; } this._displayed_messages.remove(message); if(message.tag_timepointer) this._destroy_view_entry(message.tag_timepointer); if(message.tag_unread) this._destroy_view_entry(message.tag_unread); this._destroy_view_entry(message.message); } clear_messages(save?: boolean) { this._message_history = []; while(this._displayed_messages.length > 0) { this._destroy_displayed_message(this._displayed_messages[0], false); } this._spacer_unread_message = undefined; this._update_message_timestamp(); if(save) this.save_history(); } fix_scroll(animate: boolean) { if(!this._html_message_container) return; let offset; if(this._spacer_unread_message) { offset = this._displayed_message_first_tag(this._spacer_unread_message)[0].offsetTop; } else if(typeof(this._scroll_position) !== "undefined") { offset = this._scroll_position; } else { offset = this._html_message_container[0].scrollHeight; } if(animate) { this._html_message_container.stop(true).animate({ scrollTop: offset }, 'slow'); } else { this._html_message_container.stop(true).scrollTop(offset); } } private _update_message_timestamp() { if(this._last_message_updater_id) clearTimeout(this._last_message_updater_id); if(!this._html_entry_tag) return; /* we got deleted, not need for updates */ if(this.typing_active()) { this._html_entry_tag.find(".last-message").text(tr("currently typing...")); return; } const last_message = this._message_history[0]; if(!last_message) { this._html_entry_tag.find(".last-message").text(tr("no history")); return; } const timestamp = new Date(last_message.timestamp); let time = format.date.format_chat_time(timestamp); this._html_entry_tag.find(".last-message").text(time.result); if(time.next_update > 0) { this._last_message_updater_id = setTimeout(() => this._update_message_timestamp(), time.next_update); } else { this._last_message_updater_id = 0; } } private _destroy_message(message: PrivateConversationViewMessage) { if(message.time_update_id) clearTimeout(message.time_update_id); } private _build_message(message: PrivateConversationMessageData) : PrivateConversationViewMessage { const result = message as PrivateConversationViewMessage; if(result.html_tag) return result; const timestamp = new Date(message.timestamp); let time = format.date.format_chat_time(timestamp); result.html_tag = $("#tmpl_frame_chat_private_message").renderTag({ timestamp: time.result, message_id: message.message_id, client_name: htmltags.generate_client_object({ add_braces: false, client_name: message.sender_name, client_unique_id: message.sender_unique_id, client_id: message.sender_client_id }), message: bbcode_chat(message.message), avatar: this.handle.handle.handle.fileManager.avatars.generate_chat_tag({id: message.sender_client_id}, message.sender_unique_id) }); if(time.next_update > 0) { const _updater = () => { time = format.date.format_chat_time(timestamp); result.html_tag.find(".info .timestamp").text(time.result); if(time.next_update > 0) result.time_update_id = setTimeout(_updater, time.next_update); else result.time_update_id = 0; }; result.time_update_id = setTimeout(_updater, time.next_update); } else { result.time_update_id = 0; } return result; } private _build_spacer(message: string, type: "date" | "new" | "disconnect" | "disconnect_self" | "reconnect" | "closed" | "error") : PrivateConversationViewSpacer { const tag = $("#tmpl_frame_chat_private_spacer").renderTag({ message: message }).addClass("type-" + type); return { html_tag: tag } } private _register_displayed_message(message: DisplayedMessage, update_new: boolean) { const message_date = new Date(message.timestamp); /* before := older message; after := newer message */ let entry_before: DisplayedMessage, entry_after: DisplayedMessage; let index = 0; for(;index < this._displayed_messages.length; index++) { if(this._displayed_messages[index].timestamp > message.timestamp) continue; entry_after = index > 0 ? this._displayed_messages[index - 1] : undefined; entry_before = this._displayed_messages[index]; this._displayed_messages.splice(index, 0, message); break; } if(index >= this._displayed_messages_length) { return; /* message is out of view region */ } if(index >= this._displayed_messages.length) { entry_before = undefined; entry_after = this._displayed_messages.last(); this._displayed_messages.push(message); } while(this._displayed_messages.length > this._displayed_messages_length) this._destroy_displayed_message(this._displayed_messages.last(), true); const flag_new_message = update_new && index == 0 && (message.message_type === "spacer" || (message.message).sender === "partner"); /* Timeline for before - now */ { let append_pointer = false; if(entry_before) { if(!helpers.date.same_day(message.timestamp, entry_before.timestamp)) { append_pointer = true; } } else { append_pointer = true; } if(append_pointer) { const diff = format.date.date_format(message_date, new Date()); if(diff == format.date.ColloquialFormat.YESTERDAY) message.tag_timepointer = this._build_spacer(tr("Yesterday"), "date"); else if(diff == format.date.ColloquialFormat.TODAY) message.tag_timepointer = this._build_spacer(tr("Today"), "date"); else if(diff == format.date.ColloquialFormat.GENERAL) message.tag_timepointer = this._build_spacer(format.date.format_date_general(message_date, false), "date"); } } /* Timeline not and after */ { if(entry_after) { if(helpers.date.same_day(message_date, entry_after.timestamp)) { if(entry_after.tag_timepointer) { this._destroy_view_entry(entry_after.tag_timepointer); entry_after.tag_timepointer = undefined; } } else if(!entry_after.tag_timepointer) { const diff = format.date.date_format(new Date(entry_after.timestamp), new Date()); if(diff == format.date.ColloquialFormat.YESTERDAY) entry_after.tag_timepointer = this._build_spacer(tr("Yesterday"), "date"); else if(diff == format.date.ColloquialFormat.TODAY) entry_after.tag_timepointer = this._build_spacer(tr("Today"), "date"); else if(diff == format.date.ColloquialFormat.GENERAL) entry_after.tag_timepointer = this._build_spacer(format.date.format_date_general(message_date, false), "date"); entry_after.tag_timepointer.html_tag.insertBefore(entry_after.tag_message); } } } /* new message flag */ if(flag_new_message) { if(!this._spacer_unread_message) { this._spacer_unread_message = message; message.tag_unread = this._build_spacer(tr("Unread messages"), "new"); this.set_unread_flag(true); } } if(this._html_message_container) { if(entry_before) { message.tag_message.insertAfter(entry_before.tag_message); } else if(entry_after) { message.tag_message.insertBefore(this._displayed_message_first_tag(entry_after)); } else { this._html_message_container.append(message.tag_message); } /* first time pointer */ if(message.tag_timepointer) message.tag_timepointer.html_tag.insertBefore(message.tag_message); /* the unread */ if(message.tag_unread) message.tag_unread.html_tag.insertBefore(message.tag_message); } this.fix_scroll(true); } private _destroy_view_entry(entry: PrivateConversationViewEntry) { if(!entry.html_tag) return; entry.html_tag.remove(); if('sender' in entry) this._destroy_message(entry); } private _build_entry_tag() { this._html_entry_tag = $("#tmpl_frame_chat_private_entry").renderTag({ client_name: this.client_name, last_time: tr("error no timestamp"), avatar: this.handle.handle.handle.fileManager.avatars.generate_chat_tag({id: this.client_id}, this.client_unique_id) }); this._html_entry_tag.on('click', event => { if(event.isDefaultPrevented()) return; this.handle.set_selected_conversation(this); }); this._html_entry_tag.find('.button-close').on('click', event => { event.preventDefault(); this.close_conversation(); }); this._update_message_timestamp(); } update_avatar() { const container = this._html_entry_tag.find(".container-avatar"); container.find(".avatar").remove(); container.append(this.handle.handle.handle.fileManager.avatars.generate_chat_tag({id: this.client_id}, this.client_unique_id)); } close_conversation() { this.handle.delete_conversation(this, true); } set_client_name(name: string) { if(this.client_name === name) return; this.client_name = name; this._html_entry_tag.find(".client-name").text(name); } set_unread_flag(flag: boolean, update_chat_counter?: boolean) { /* unread message pointer */ if(flag != (typeof(this._spacer_unread_message) !== "undefined")) { if(flag) { if(this._displayed_messages.length > 0) /* without messages we cant be unread */ return; if(!this._spacer_unread_message) { this._spacer_unread_message = this._displayed_messages[0]; this._spacer_unread_message.tag_unread = this._build_spacer(tr("Unread messages"), "new"); this._spacer_unread_message.tag_unread.html_tag.insertBefore(this._spacer_unread_message.tag_message); } } else { const ctree = this.handle.handle.handle.channelTree; if(ctree && ctree.tag_tree() && this.client_id) ctree.findClient(this.client_id)?.setUnread(false); if(this._spacer_unread_message) { this._destroy_view_entry(this._spacer_unread_message.tag_unread); this._spacer_unread_message.tag_unread = undefined; this._spacer_unread_message = undefined; } } } /* general notify */ this._html_entry_tag.toggleClass("unread", flag); if(typeof(update_chat_counter) !== "boolean" || update_chat_counter) this.handle.handle.info_frame().update_chat_counter(); } is_unread() : boolean { return !!this._spacer_unread_message; } private _append_state_change(state: "disconnect" | "disconnect_self" | "reconnect" | "closed") { let message; if(state == "closed") message = tr("Your chat partner has closed the conversation"); else if(state == "reconnect") message = this._state === PrivateConversationState.DISCONNECTED_SELF ?tr("You've reconnected to the server") : tr("Your chat partner has reconnected"); else if(state === "disconnect") message = tr("Your chat partner has disconnected"); else message = tr("You've disconnected from the server"); const spacer = this._build_spacer(message, state); this._register_displayed_message({ timestamp: Date.now(), message: spacer, message_type: "spacer", tag_message: spacer.html_tag, tag_timepointer: undefined, tag_unread: undefined }, state === "disconnect"); } state() : PrivateConversationState { return this._state; } set_state(state: PrivateConversationState) { if(this._state == state) return; if(state == PrivateConversationState.DISCONNECTED) { this._append_state_change("disconnect"); this.client_id = 0; } else if(state == PrivateConversationState.OPEN && this._state != PrivateConversationState.CLOSED) this._append_state_change("reconnect"); else if(state == PrivateConversationState.CLOSED) this._append_state_change("closed"); else if(state == PrivateConversationState.DISCONNECTED_SELF) this._append_state_change("disconnect_self"); this._state = state; } set_text_callback(callback: (text: string) => any, update_enabled_state?: boolean) { this._callback_message = callback; if(typeof (update_enabled_state) !== "boolean" || update_enabled_state) this.handle.update_chatbox_state(); } chat_enabled() { return typeof(this._callback_message) !== "undefined" && (this._state == PrivateConversationState.OPEN || this._state == PrivateConversationState.CLOSED); } append_error(message: string, date?: number) { const spacer = this._build_spacer(message, "error"); this._register_displayed_message({ timestamp: date || Date.now(), message: spacer, message_type: "spacer", tag_message: spacer.html_tag, tag_timepointer: undefined, tag_unread: undefined }, true); } call_message(message: string) { if(this._callback_message) this._callback_message(message); else { log.warn(LogCategory.CHAT, tr("Dropping conversation message for client %o because of no message callback."), { client_name: this.client_name, client_id: this.client_id, client_unique_id: this.client_unique_id }); } } private typing_expired() { this._update_message_timestamp(); if(this.handle.current_conversation() === this) this.handle.update_typing_state(); } trigger_typing() { let _new = Date.now() - this._last_typing > this._typing_timeout; this._last_typing = Date.now(); if(this._typing_timeout_task) clearTimeout(this._typing_timeout_task); if(_new) this._update_message_timestamp(); if(this.handle.current_conversation() === this) this.handle.update_typing_state(); this._typing_timeout_task = setTimeout(() => this.typing_expired(), this._typing_timeout); } typing_active() { return Date.now() - this._last_typing < this._typing_timeout; } } export class PrivateConverations { readonly handle: Frame; private _chat_box: ChatBox; private _html_tag: JQuery; private _container_conversation: JQuery; private _container_conversation_messages: JQuery; private _container_conversation_list: JQuery; private _container_typing: JQuery; private _html_no_chats: JQuery; private _conversations: PrivateConveration[] = []; private _current_conversation: PrivateConveration = undefined; private _select_read_timer: number; constructor(handle: Frame) { this.handle = handle; this._chat_box = new ChatBox(); this._build_html_tag(); this.update_chatbox_state(); this.update_typing_state(); this._chat_box.callback_text = message => { if(!this._current_conversation) { log.warn(LogCategory.CHAT, tr("Dropping conversation message because of no active conversation.")); return; } this._current_conversation.call_message(message); }; this._chat_box.callback_typing = () => { if(!this._current_conversation) { log.warn(LogCategory.CHAT, tr("Dropping conversation typing action because of no active conversation.")); return; } const connection = this.handle.handle.serverConnection; if(!connection || !connection.connected()) return; connection.send_command("clientchatcomposing", { clid: this._current_conversation.client_id }); } } clear_client_ids() { this._conversations.forEach(e => { e.client_id = 0; e.set_state(PrivateConversationState.DISCONNECTED_SELF); }); } html_tag() : JQuery { return this._html_tag; } destroy() { this._chat_box && this._chat_box.destroy(); this._chat_box = undefined; for(const conversation of this._conversations) conversation.destroy(); this._conversations = []; this._current_conversation = undefined; clearTimeout(this._select_read_timer); this._html_tag && this._html_tag.remove(); this._html_tag = undefined; } current_conversation() : PrivateConveration | undefined { return this._current_conversation; } conversations() : PrivateConveration[] { return this._conversations; } create_conversation(client_uid: string, client_name: string, client_id: number) : PrivateConveration { const conv = new PrivateConveration(this, client_uid, client_name, client_id); this._conversations.push(conv); this._html_no_chats.hide(); this._container_conversation_list.append(conv.entry_tag()); this.handle.info_frame().update_chat_counter(); return conv; } delete_conversation(conv: PrivateConveration, update_chat_couner?: boolean) { if(!this._conversations.remove(conv)) return; //TODO: May animate? conv.destroy(); conv.clear_messages(false); this._html_no_chats.toggle(this._conversations.length == 0); if(conv === this._current_conversation) this.set_selected_conversation(undefined); if(update_chat_couner || typeof(update_chat_couner) !== "boolean") this.handle.info_frame().update_chat_counter(); } find_conversation(partner: { name: string; unique_id: string; client_id: number }, mode: { create: boolean, attach: boolean }) : PrivateConveration | undefined { for(const conversation of this.conversations()) if(conversation.client_id == partner.client_id && (!partner.unique_id || conversation.client_unique_id == partner.unique_id)) { if(conversation.state() != PrivateConversationState.OPEN) conversation.set_state(PrivateConversationState.OPEN); return conversation; } let conv: PrivateConveration; if(mode.attach) { for(const conversation of this.conversations()) if(conversation.client_unique_id == partner.unique_id && conversation.state() != PrivateConversationState.OPEN) { conversation.set_state(PrivateConversationState.OPEN); conversation.client_id = partner.client_id; conversation.set_client_name(partner.name); conv = conversation; break; } } if(mode.create && !conv) { conv = this.create_conversation(partner.unique_id, partner.name, partner.client_id); conv.client_id = partner.client_id; conv.set_client_name(partner.name); } if(conv) { conv.set_text_callback(message => { log.debug(LogCategory.CLIENT, tr("Sending text message %s to %o"), message, partner); this.handle.handle.serverConnection.send_command("sendtextmessage", {"targetmode": 1, "target": partner.client_id, "msg": message}).catch(error => { if(error instanceof CommandResult) { if(error.id == ErrorID.CLIENT_INVALID_ID) { conv.set_state(PrivateConversationState.DISCONNECTED); conv.set_text_callback(undefined); } else if(error.id == ErrorID.PERMISSION_ERROR) { /* may notify for no permissions? */ } else { conv.append_error(tr("Failed to send message: ") + (error.extra_message || error.message)); } } else { conv.append_error(tr("Failed to send message. Lookup the console for more details")); log.error(LogCategory.CHAT, tr("Failed to send conversation message: %o"), error); } }); }); } return conv; } clear_conversations() { while(this._conversations.length > 0) this.delete_conversation(this._conversations[0], false); this.handle.info_frame().update_chat_counter(); } set_selected_conversation(conv: PrivateConveration | undefined) { if(conv === this._current_conversation) return; if(this._select_read_timer) clearTimeout(this._select_read_timer); if(this._current_conversation) this._current_conversation._html_message_container = undefined; this._container_conversation_list.find(".selected").removeClass("selected"); this._container_conversation_messages.children().detach(); this._current_conversation = conv; if(!this._current_conversation) { this.update_chatbox_state(); return; } this._current_conversation._html_message_container = this._container_conversation_messages; const messages = this._current_conversation.messages_tags(); /* TODO: Check if the messages are empty and display "No messages" */ this._container_conversation_messages.append(...messages); if(this._current_conversation.is_unread() && false) { this._select_read_timer = setTimeout(() => { this._current_conversation.set_unread_flag(false, true); }, 20 * 1000); /* Lets guess you've read the new messages within 5 seconds */ } this._current_conversation.fix_scroll(false); this._current_conversation.entry_tag().addClass("selected"); this.update_chatbox_state(); } update_chatbox_state() { this._chat_box.set_enabled(!!this._current_conversation && this._current_conversation.chat_enabled()); } update_typing_state() { this._container_typing.toggleClass("hidden", !this._current_conversation || !this._current_conversation.typing_active()); } private _build_html_tag() { this._html_tag = $("#tmpl_frame_chat_private").renderTag({ chatbox: this._chat_box.html_tag() }).dividerfy(); this._container_conversation = this._html_tag.find(".conversation"); this._container_conversation.on('click', event => { /* lets think if a user clicks within that field that he has read the messages */ if(this._current_conversation) this._current_conversation.set_unread_flag(false, true); /* only updates everything if the state changes */ }); this._container_conversation_messages = this._container_conversation.find(".container-messages"); this._container_conversation_messages.on('scroll', event => { if(!this._current_conversation) return; const current_view = this._container_conversation_messages[0].scrollTop + this._container_conversation_messages[0].clientHeight + this._container_conversation_messages[0].clientHeight * .125; if(current_view > this._container_conversation_messages[0].scrollHeight) this._current_conversation._scroll_position = undefined; else this._current_conversation._scroll_position = this._container_conversation_messages[0].scrollTop; }); this._container_conversation_list = this._html_tag.find(".conversation-list"); this._html_no_chats = this._container_conversation_list.find(".no-chats"); this._container_typing = this._html_tag.find(".container-typing"); this.update_input_format_helper(); } try_input_focus() { this._chat_box.focus_input(); } on_show() { if(this._current_conversation) this._current_conversation.fix_scroll(false); } update_input_format_helper() { const tag = this._html_tag.find(".container-format-helper"); if(settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN)) { tag.removeClass("hidden").text(tr("*italic*, **bold**, ~~strikethrough~~, `code`, and more...")); } else { tag.addClass("hidden"); } } }