-
🌐 admin_panel.phpWeb, 1.54 kB⬇
-
🌐 api.jsWeb, 12.79 kB⬇
-
🌐 api.phpWeb, 7.79 kB⬇
-
🌐 app.jsWeb, 150.45 kB⬇
-
🌐 base.cssWeb, 2.23 kB⬇
-
🌐 context.jsWeb, 9.26 kB⬇
-
🌐 conversation-index.jsWeb, 10.34 kB⬇
-
🌐 conversation.jsWeb, 1.11 kB⬇
-
🌐 conversations.jsWeb, 23.87 kB⬇
-
🌐 diction.jsWeb, 935 B⬇
-
🌐 document.jsWeb, 947 B⬇
-
🌐 dynamic.phpWeb, 533 B⬇
-
📷 favicon-lg.pngImage, 9.11 kB⬇
-
📷 favicon.pngImage, 3.12 kB⬇
-
🌐 index.phpWeb, 30.31 kB⬇
-
🌐 indexjs.jsWeb, 580 B⬇
-
🌐 login.phpWeb, 876 B⬇
-
❓ manifest.webmanifestUnknown, 262 B⬇
-
🌐 memory.jsWeb, 407 B⬇
-
🌐 model-presets.jsonWeb, 7.89 kB⬇
-
🌐 models.jsonWeb, 12.83 kB⬇
-
🌐 notebook-index.jsWeb, 721 B⬇
-
⚠️ php.iniOS File, 356 B⬇
-
🌐 scratchpad-conversation.jsWeb, 2.70 kB⬇
-
🌐 scratchpad-index.jsWeb, 14.47 kB⬇
-
🌐 storage.jsWeb, 20.49 kB⬇
-
🌐 style.cssWeb, 49.16 kB⬇
-
🌐 users.jsWeb, 45.79 kB⬇
-
🌐 utility-models.jsonWeb, 1.46 kB⬇
import { createElementFromHTML } from './util/dom-utils.js';
import { SelectionManager } from './util/selection-manager.js';
import { customConfirm } from './util/confirm-dialog.js';
import { escapeHtml } from './util/format-utils.js';
/**
* Class ConversationsList
* Manages the primary navigation list of conversations.
* Handles rendering the list, filtering active/archived conversations,
* and managing multi-selection for bulk actions.
*/
class ConversationsList {
#storage;
#app_callbacks;
#selection;
div_list;
div_title;
btn_select_conversations;
btn_archive_conversations;
btn_delete_conversations;
btn_archives_toggle;
btn_conversations;
btn_show_conversations_new;
BREAKPOINT_MOBILE;
BREAKPOINT_TABLET;
/**
* Initializes the ConversationsList instance with storage, callbacks, and element references.
* @param {Storage} storage_instance - Storage manager instance.
* @param {object} app_callbacks - Application callback methods.
* @param {object} elements - DOM element references.
* @param {object} breakpoints - Responsive breakpoint constants.
*/
constructor(storage_instance, app_callbacks, elements, breakpoints) {
this.#storage = storage_instance;
this.#app_callbacks = app_callbacks;
this.div_list = elements.div_list;
this.div_title = elements.div_title;
this.btn_select_conversations = elements.btn_select_conversations;
this.btn_archive_conversations = elements.btn_archive_conversations;
this.btn_delete_conversations = elements.btn_delete_conversations;
this.btn_archives_toggle = elements.btn_archives_toggle;
this.btn_conversations = elements.btn_conversations;
this.btn_show_conversations_new = elements.btn_show_conversations_new;
this.BREAKPOINT_MOBILE = breakpoints.BREAKPOINT_MOBILE;
this.BREAKPOINT_TABLET = breakpoints.BREAKPOINT_TABLET;
this.#selection = new SelectionManager(() => this._update_selection_ui());
if (this.btn_show_conversations_new) {
this.btn_show_conversations_new.onclick = () => {
this.#app_callbacks.close_conversation();
};
}
}
//region Conversation Management
/**
* Selects and activates a conversation, handling responsive panel transitions.
* @param {string} guid - The conversation GUID.
* @param {string} type - The conversation type.
* @private
*/
_select_and_open_conversation(guid, type) {
if (window.innerWidth <= this.BREAKPOINT_MOBILE) {
this.#app_callbacks.set_v_open(false);
this.#app_callbacks.set_i_open(false);
}
this.#app_callbacks.apply_panels_layout();
this.#app_callbacks.update_app_config(this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID, guid);
if (type === this.#storage.CONVERSATION_TYPE_CHAT || type === this.#storage.CONVERSATION_TYPE_SCRATCHPAD) {
this.#app_callbacks.set_active_tab('conversation');
} else if (type === this.#storage.CONVERSATION_TYPE_NOTEBOOK) {
this.#app_callbacks.set_active_tab('document');
}
this.#app_callbacks.on_conversation_updated();
this.apply_selected_index_class();
}
/**
* Creates a new conversation, updates the index, and sets it as the currently selected conversation.
* @param {string} type - The conversation type.
*/
index_new(type) {
const guid = this.#storage.update_app_index('', false, type);
const conv = this.#storage.get_conversation(guid);
conv[this.#storage.KEY_CONVERSATION_TYPE] = type;
if (type === this.#storage.CONVERSATION_TYPE_SCRATCHPAD) {
conv[this.#storage.KEY_CONVERSATION_TITLE] = 'New Scratchpad';
} else if (type === this.#storage.CONVERSATION_TYPE_NOTEBOOK) {
conv[this.#storage.KEY_CONVERSATION_TITLE] = 'New Notebook';
} else {
conv[this.#storage.KEY_CONVERSATION_TITLE] = 'New Conversation';
}
this.#storage.save_conversation(guid, conv);
this.on_app_index_updated();
this._select_and_open_conversation(guid, type);
}
/**
* Deletes a conversation by its GUID from the application index and localStorage.
* @param {string} guid - The unique identifier of the conversation to delete.
*/
index_delete(guid) {
if (this.#app_callbacks.abort_request) {
this.#app_callbacks.abort_request(guid);
}
this.#storage.index_delete(guid);
this.#app_callbacks.apply_panels_layout();
}
/**
* Determines the display title for a conversation.
* @param {Object} conversation - The conversation object.
* @returns {string} The title.
*/
get_conversation_title(conversation) {
if (!conversation) return 'New Conversation';
const history = conversation[this.#storage.KEY_CONVERSATION_HISTORY] || [];
if (history.length > 0) {
for (let i = history.length - 1; i >= 0; i--) {
const item = history[i];
if (item && (item.title || item.conversationTitle)) {
return item.title || item.conversationTitle;
}
}
}
const defaultTitle = conversation[this.#storage.KEY_CONVERSATION_TYPE] === this.#storage.CONVERSATION_TYPE_NOTEBOOK
? 'New Notebook'
: 'New Conversation';
return conversation[this.#storage.KEY_CONVERSATION_TITLE] || defaultTitle;
}
/**
* Returns the count of scratchpad conversations in the index.
* @returns {number} The count of scratchpad conversations.
*/
get_scratchpad_count() {
const index = this.#storage.get_app_index() || [];
let count = 0;
index.forEach(item => {
const guid = item[this.#storage.KEY_INDEX_GUID];
const conv = this.#storage.get_conversation(guid);
const type = conv?.[this.#storage.KEY_CONVERSATION_TYPE] || item?.[this.#storage.KEY_CONVERSATION_TYPE];
if (type === this.#storage.CONVERSATION_TYPE_SCRATCHPAD && conv) {
count++;
}
});
return count;
}
//endregion
//region Selection and Actions
/**
* Updates the UI elements related to conversation selection (button text and disabled states).
* @private
*/
_update_selection_ui() {
const config = this.#storage.get_app_config();
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
const selectedConv = selected_guid ? this.#storage.get_conversation(selected_guid) : null;
const isChatOrNotebook = selectedConv && selectedConv[this.#storage.KEY_CONVERSATION_TYPE] !== this.#storage.CONVERSATION_TYPE_SCRATCHPAD;
if (this.#selection.isSelectionMode) {
this.#selection.updateControls(
this.btn_select_conversations,
[this.btn_archive_conversations, this.btn_delete_conversations],
'Select'
);
} else {
if (this.btn_select_conversations) {
this.btn_select_conversations.textContent = 'Select';
}
if (this.btn_archive_conversations) {
this.btn_archive_conversations.disabled = !isChatOrNotebook;
}
if (this.btn_delete_conversations) {
this.btn_delete_conversations.disabled = true;
}
}
}
/**
* Toggles the multi-selection mode for managing the list of conversations.
*/
toggle_conversation_selection_mode() {
this.#selection.toggleMode();
this._update_selection_ui();
this.on_app_index_updated();
}
/**
* Archives or unarchives all conversations currently selected in the list,
* or toggles the archive status of the active conversation if no selection.
*/
async archive_selected_conversations() {
const selectedGuids = this.#selection.selectedItems;
if (selectedGuids.length === 0) {
const config = this.#storage.get_app_config();
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
if (!selected_guid) return;
const conversation = this.#storage.get_conversation(selected_guid);
if (!conversation) return;
const isArchived = conversation[this.#storage.KEY_CONVERSATION_ARCHIVED] || false;
const show_archives = config[this.#storage.KEY_SHOW_ARCHIVES] || false;
if (!isArchived) {
if (!show_archives) {
if (await customConfirm('Archive Conversation', 'Are you sure you want to archive and close this conversation?')) {
conversation[this.#storage.KEY_CONVERSATION_ARCHIVED] = true;
this.#storage.save_conversation(selected_guid, conversation);
this.#app_callbacks.close_conversation();
this.on_app_index_updated();
}
} else {
conversation[this.#storage.KEY_CONVERSATION_ARCHIVED] = true;
this.#storage.save_conversation(selected_guid, conversation);
this.on_app_index_updated();
}
} else {
conversation[this.#storage.KEY_CONVERSATION_ARCHIVED] = false;
this.#storage.save_conversation(selected_guid, conversation);
this.on_app_index_updated();
}
return;
}
const config = this.#storage.get_app_config();
const show_archives = config[this.#storage.KEY_SHOW_ARCHIVES] || false;
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
let closeActive = false;
selectedGuids.forEach(guid => {
const conversation = this.#storage.get_conversation(guid);
if (conversation) {
const isArchived = conversation[this.#storage.KEY_CONVERSATION_ARCHIVED] || false;
conversation[this.#storage.KEY_CONVERSATION_ARCHIVED] = !isArchived;
this.#storage.save_conversation(guid, conversation);
if (!show_archives && guid === selected_guid && !isArchived) {
closeActive = true;
}
}
});
if (closeActive) {
this.#app_callbacks.close_conversation();
}
this.#selection.clear();
this._update_selection_ui();
this.on_app_index_updated();
}
/**
* Deletes all conversations currently selected in the list after user confirmation.
*/
async delete_selected_conversations() {
const selectedGuids = this.#selection.selectedItems;
const count = selectedGuids.length;
if (count === 0) return;
const message = count === 1
? 'Are you sure you want to delete this conversation?'
: `Are you sure you want to delete ${count} selected conversations?`;
if (await customConfirm('Delete Conversations', message)) {
const config = this.#storage.get_app_config();
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
const toDelete = [...selectedGuids];
toDelete.forEach(guid => {
if (selected_guid === guid) {
this.#app_callbacks.close_conversation();
}
this.index_delete(guid);
});
this.#selection.clear();
this._update_selection_ui();
this.on_app_index_updated();
}
}
/**
* Toggles whether archived conversations are displayed in the main conversations list.
*/
toggle_archives() {
const config = this.#storage.get_app_config();
const current = config[this.#storage.KEY_SHOW_ARCHIVES] || false;
const next = !current;
this.#app_callbacks.update_app_config(this.#storage.KEY_SHOW_ARCHIVES, next);
if (!next) {
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
if (selected_guid) {
const conversation = this.#storage.get_conversation(selected_guid);
if (conversation && conversation[this.#storage.KEY_CONVERSATION_ARCHIVED]) {
this.#app_callbacks.close_conversation();
}
}
}
this.on_app_index_updated();
}
//endregion
//region HTML Generation
/**
* Creates and returns a DOM element for a single conversation entry in the navigation list.
* @param {Object} data - The metadata from the application index.
* @param {string} guid - The GUID of the conversation.
* @param {string} title - The display title of the conversation.
* @param {boolean} is_archived - Whether the conversation is currently archived.
* @param {string} type - The type of conversation.
* @param {boolean} [is_loading=false] - Whether an API request is currently running for this conversation.
* @param {boolean} [is_selected=false] - Whether this conversation is currently selected.
* @returns {HTMLElement} The created list item element.
*/
create_index_item_element(data, guid, title, is_archived, type, is_loading = false, is_selected = false) {
const isChecked = this.#selection.isSelected(guid);
const html = this._get_index_item_html(guid, title, is_archived, this.#selection.isSelectionMode, isChecked, type, is_loading, is_selected);
const div = createElementFromHTML(html);
if (!div) return document.createElement('div');
div.onclick = () => {
this._select_and_open_conversation(guid, type);
};
const chk = div.querySelector('.index-item-checkbox');
if (chk) {
chk.onclick = (e) => {
e.stopPropagation();
this.#selection.toggleItem(guid, chk.checked);
this._update_selection_ui();
};
}
return div;
}
/**
* Generates the HTML string for a single conversation index item.
* @param {string} guid - The GUID of the conversation.
* @param {string} title - The display title of the conversation.
* @param {boolean} is_archived - Whether the conversation is currently archived.
* @param {boolean} isSelectionMode - Whether conversation selection mode is active.
* @param {boolean} isChecked - Whether the checkbox for this item should be checked.
* @param {string} type - The type of conversation.
* @param {boolean} [is_loading=false] - Whether an API request is in progress.
* @param {boolean} [is_selected=false] - Whether this item is currently selected.
* @returns {string} The HTML string for the conversation index item.
*/
_get_index_item_html(guid, title, is_archived, isSelectionMode, isChecked, type, is_loading = false, is_selected = false) {
const icon = type === this.#storage.CONVERSATION_TYPE_NOTEBOOK ? '📝' : '💬';
const escapedTitle = escapeHtml(title);
return `
<div class="div-index-item div-index-item-flex ${is_archived ? 'div-conversation-item-archived' : ''} ${is_loading ? 'div-index-item-loading' : ''} ${is_selected ? 'div-index-item-selected' : ''}"
id="conversation-${guid}"
data-guid="${guid}">
${isSelectionMode ? `<input type="checkbox" ${isChecked ? 'checked' : ''} class="index-item-checkbox index-item-checkbox-margin">` : ''}
<span class="index-item-icon as-icon">${icon}</span>
<span class="index-item-text-grow" title="${escapedTitle}">${escapedTitle}</span>
<span class="index-item-indicator as-icon" style="display: ${is_loading ? 'inline-block' : 'none'};" title="Processing...">●</span>
</div>
`;
}
/**
* Creates and returns the DOM element for the permanent Scratchpad entry in the navigation list.
* @returns {HTMLElement} The scratchpad navigation item.
*/
create_scratchpad_item_element() {
const config = this.#storage.get_app_config();
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
const conversation = this.#storage.get_conversation(selected_guid);
const is_selected = (selected_guid === 'scratchpad') || (conversation && conversation[this.#storage.KEY_CONVERSATION_TYPE] === this.#storage.CONVERSATION_TYPE_SCRATCHPAD);
const count = this.get_scratchpad_count();
const is_loading = this.#app_callbacks.is_scratchpad_active_loading ? this.#app_callbacks.is_scratchpad_active_loading() : false;
const html = `
<div class="div-index-item div-index-item-flex ${is_selected ? 'div-index-item-selected' : ''}" id="conversation-scratchpad">
<span class="index-item-icon as-icon">📝</span>
<span class="index-item-text-grow">Scratchpads</span>
<span class="index-item-count">${count}</span>
<span class="index-item-indicator as-icon" style="display: ${is_loading ? 'inline-block' : 'none'};" title="Processing...">●</span>
</div>
`;
const div = createElementFromHTML(html);
if (!div) return document.createElement('div');
div.onclick = () => {
if (window.innerWidth <= this.BREAKPOINT_MOBILE) {
this.#app_callbacks.set_v_open(false);
this.#app_callbacks.set_i_open(true);
} else if (window.innerWidth <= this.BREAKPOINT_TABLET) {
this.#app_callbacks.set_v_open(false);
this.#app_callbacks.set_i_open(true);
} else {
this.#app_callbacks.set_i_open(true);
}
this.#app_callbacks.apply_panels_layout();
const active_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
const is_scratchpad_active = (active_guid === 'scratchpad');
const active_conv = active_guid && !is_scratchpad_active ? this.#storage.get_conversation(active_guid) : null;
const is_active_scratchpad_conv = active_conv?.[this.#storage.KEY_CONVERSATION_TYPE] === this.#storage.CONVERSATION_TYPE_SCRATCHPAD;
if (!is_scratchpad_active && !is_active_scratchpad_conv) {
const scratchpads = [];
const index = this.#storage.get_app_index() || [];
index.forEach(item => {
const guid = item[this.#storage.KEY_INDEX_GUID];
const conv = this.#storage.get_conversation(guid);
const type = conv?.[this.#storage.KEY_CONVERSATION_TYPE] || item?.[this.#storage.KEY_CONVERSATION_TYPE];
if (type === this.#storage.CONVERSATION_TYPE_SCRATCHPAD && conv) {
scratchpads.push(guid);
}
});
if (scratchpads.length > 0) {
this.#app_callbacks.update_app_config(this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID, scratchpads[0]);
} else {
this.#app_callbacks.update_app_config(this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID, 'scratchpad');
}
}
this.#app_callbacks.set_active_tab('conversation');
this.#app_callbacks.on_conversation_updated();
this.apply_selected_index_class();
};
return div;
}
/**
* Creates and returns the DOM element for the "New Conversation" trigger button.
* @returns {HTMLElement} The trigger button element.
*/
create_new_conversation_trigger_item() {
const html = `
<button class="btn-new-conversation">
<span class="index-item-icon as-icon" style="margin-right: 0.5em;">➕</span>
<span>New Chat</span>
</button>
`;
const div = createElementFromHTML(html);
if (!div) return document.createElement('button');
div.onclick = (e) => {
e.stopPropagation();
this.index_new(this.#storage.CONVERSATION_TYPE_CHAT);
};
return div;
}
/**
* Generates HTML string for the archive section header divider.
* @returns {string} The HTML string.
* @private
*/
_get_archive_header_html() {
return `
<div class="div-index-archive-header">
<span>ARCHIVES</span>
</div>
`;
}
/**
* Generates HTML string for empty archive placeholder.
* @returns {string} The HTML string.
* @private
*/
_get_no_archive_message_html() {
return `
<div class="div-index-archive-empty">
<span>No archived conversations</span>
</div>
`;
}
//endregion
//region Rendering
/**
* Updates the loading indicator for a specific conversation in the navigation list.
* @param {string} guid - The conversation GUID.
* @param {boolean} isLoading - Whether the request is active.
*/
set_conversation_loading(guid, isLoading) {
if (!this.div_list) return;
const itemEl = document.getElementById(`conversation-${guid}`) || this.div_list.querySelector(`[data-guid="${guid}"]`);
if (itemEl && this.div_list.contains(itemEl)) {
if (isLoading) {
itemEl.classList.add('div-index-item-loading');
} else {
itemEl.classList.remove('div-index-item-loading');
}
const indicator = itemEl.querySelector('.index-item-indicator');
if (indicator) {
indicator.style.display = isLoading ? 'inline-block' : 'none';
}
}
}
/**
* Updates the loading indicator for the root scratchpad navigation item.
* @param {boolean} isLoading - Whether any scratchpad conversation is currently loading.
*/
set_scratchpad_root_loading(isLoading) {
if (!this.div_list) return;
const itemEl = document.getElementById('conversation-scratchpad') || this.div_list.querySelector('#conversation-scratchpad');
if (itemEl && this.div_list.contains(itemEl)) {
if (isLoading) {
itemEl.classList.add('div-index-item-loading');
} else {
itemEl.classList.remove('div-index-item-loading');
}
const indicator = itemEl.querySelector('.index-item-indicator');
if (indicator) {
indicator.style.display = isLoading ? 'inline-block' : 'none';
}
}
}
apply_selected_index_class() {
if (!this.div_list) return;
const config = this.#storage.get_app_config();
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
const conversation = this.#storage.get_conversation(selected_guid);
const isScratchpad = selected_guid === 'scratchpad' || (conversation && conversation[this.#storage.KEY_CONVERSATION_TYPE] === this.#storage.CONVERSATION_TYPE_SCRATCHPAD);
const items = this.div_list.querySelectorAll('.div-index-item');
items.forEach(el => {
el.classList.remove('div-index-item-selected');
});
if (isScratchpad) {
const scratchpadEl = document.getElementById('conversation-scratchpad') || this.div_list.querySelector('#conversation-scratchpad');
if (scratchpadEl) {
scratchpadEl.classList.add('div-index-item-selected');
}
} else if (selected_guid) {
const activeEl = document.getElementById(`conversation-${selected_guid}`);
if (activeEl && this.div_list.contains(activeEl)) {
activeEl.classList.add('div-index-item-selected');
}
}
const scratchpadCountEl = this.div_list.querySelector('#conversation-scratchpad .index-item-count');
if (scratchpadCountEl) {
scratchpadCountEl.textContent = this.get_scratchpad_count();
}
this._update_selection_ui();
}
/**
* Re-renders the full conversations list in the DOM based on current storage and filter state.
*/
on_app_index_updated() {
const rawIndex = this.#storage.get_app_index() || [];
const index = [...rawIndex];
index.sort((a, b) => (b[this.#storage.KEY_INDEX_DATE_UPDATED] || 0) - (a[this.#storage.KEY_INDEX_DATE_UPDATED] || 0));
const config = this.#storage.get_app_config();
let show_archives = config[this.#storage.KEY_SHOW_ARCHIVES] || false;
const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
const active_items = [];
const archived_items = [];
index.forEach(item => {
const guid = item[this.#storage.KEY_INDEX_GUID];
const conversation = this.#storage.get_conversation(guid);
if (!conversation) return;
const type = conversation[this.#storage.KEY_CONVERSATION_TYPE] || this.#storage.CONVERSATION_TYPE_CHAT;
if (type === this.#storage.CONVERSATION_TYPE_SCRATCHPAD) {
return;
}
const is_archived = conversation[this.#storage.KEY_CONVERSATION_ARCHIVED] || false;
if (is_archived) {
archived_items.push({ item, guid, conversation, type });
} else {
active_items.push({ item, guid, conversation, type });
}
});
if (archived_items.length === 0 && show_archives) {
show_archives = false;
this.#app_callbacks.update_app_config(this.#storage.KEY_SHOW_ARCHIVES, false);
}
if (this.div_list) {
this.div_list.innerHTML = '';
const fragment = document.createDocumentFragment();
fragment.appendChild(this.create_scratchpad_item_element());
fragment.appendChild(this.create_new_conversation_trigger_item());
active_items.forEach(({ item, guid, conversation, type }) => {
const title = this.get_conversation_title(conversation);
const is_loading = this.#app_callbacks.is_request_active ? this.#app_callbacks.is_request_active(guid) : false;
const is_selected = (guid === selected_guid);
fragment.appendChild(this.create_index_item_element(item, guid, title, false, type, is_loading, is_selected));
});
if (show_archives) {
const archive_header = createElementFromHTML(this._get_archive_header_html());
if (archive_header) fragment.appendChild(archive_header);
archived_items.forEach(({ item, guid, conversation, type }) => {
const title = this.get_conversation_title(conversation);
const is_loading = this.#app_callbacks.is_request_active ? this.#app_callbacks.is_request_active(guid) : false;
const is_selected = (guid === selected_guid);
fragment.appendChild(this.create_index_item_element(item, guid, title, true, type, is_loading, is_selected));
});
}
this.div_list.appendChild(fragment);
}
this.apply_selected_index_class();
const total_selectable = active_items.length + (show_archives ? archived_items.length : 0);
if (this.btn_select_conversations) {
this.btn_select_conversations.disabled = (total_selectable === 0);
}
if (total_selectable === 0 && this.#selection.isSelectionMode) {
this.#selection.clear();
}
if (this.btn_archives_toggle) {
this.btn_archives_toggle.disabled = (archived_items.length === 0);
this.btn_archives_toggle.textContent = show_archives ? 'Hide Archives' : 'Show Archives';
}
}
//endregion
}
export default ConversationsList;
conversations.js
×
Type: Web, text/plain
23.87 Kilobytes
Last Modified 2026-09-23 02:24:24
⬇ Download File
Type: Web, text/plain
23.87 Kilobytes
Last Modified 2026-09-23 02:24:24
⬇ Download File