6 directories, 29 files

tinai

Home / tinai
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 ? '&#128221;' : '&#128172;';
		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...">&#9679;</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">&#128221;</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...">&#9679;</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;">&#10133;</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