Add conversations modal
This commit is contained in:
@@ -39,6 +39,27 @@ export function extractText(content) {
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
// Conversations created from this UI have no title. Their label
|
||||
// falls back to the creation date, so the picker stays readable.
|
||||
// Both dates render in the browser locale.
|
||||
const CONVERSATION_DATE = new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
});
|
||||
|
||||
/**
|
||||
* conversationLabel returns the text that names a conversation in
|
||||
* the picker: its title, or its creation date when it has none.
|
||||
* @param {{title: string | null, created_at: string}} conversation
|
||||
* @returns {string}
|
||||
*/
|
||||
export function conversationLabel(conversation) {
|
||||
return (
|
||||
conversation.title ??
|
||||
CONVERSATION_DATE.format(new Date(conversation.created_at))
|
||||
);
|
||||
}
|
||||
|
||||
export class UI {
|
||||
/**
|
||||
* @param {Object} options
|
||||
@@ -88,6 +109,18 @@ export class UI {
|
||||
this.$systemMessageInput = document.getElementById('system-message-input');
|
||||
this.$saveSystemMessage = document.getElementById('save-system-message');
|
||||
|
||||
// Conversations modal.
|
||||
this.$conversations = document.getElementById('conversations');
|
||||
this.$conversationsOverlay = document.getElementById(
|
||||
'conversations-overlay',
|
||||
);
|
||||
this.$conversationsClose = document.getElementById('conversations-close');
|
||||
this.$conversationsSearch = document.getElementById('conversation-search');
|
||||
this.$conversationsList = document.getElementById('conversation-list');
|
||||
this.$tplConversationItem = document.getElementById(
|
||||
'tpl-conversation-item',
|
||||
);
|
||||
|
||||
// Compose settings modal.
|
||||
this.$composeSettings = document.getElementById('compose-settings');
|
||||
this.$composeSettingsOverlay = document.getElementById(
|
||||
@@ -824,6 +857,65 @@ export class UI {
|
||||
this.$composeSettingsClose.focus();
|
||||
}
|
||||
|
||||
// ====================
|
||||
// CONVERSATIONS MODAL
|
||||
// ====================
|
||||
|
||||
/**
|
||||
* openConversations shows the conversation picker and asks
|
||||
* main.js for a fresh list. The search field starts empty and
|
||||
* holds the caret, so typing filters the list at once.
|
||||
*/
|
||||
openConversations() {
|
||||
this.$conversationsSearch.value = '';
|
||||
this.$conversationsOverlay.classList.add('open');
|
||||
this.$conversationsSearch.focus();
|
||||
this.handlers.onConversationsOpen();
|
||||
}
|
||||
|
||||
/** closeConversations hides the picker. */
|
||||
closeConversations() {
|
||||
this.$conversationsOverlay.classList.remove('open');
|
||||
this.$conversations.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* renderConversationList fills the picker with the rows given,
|
||||
* in the order given. main.js filters the rows; the row that
|
||||
* matches currentId is marked as the conversation in view. The
|
||||
* search field decides which empty message shows.
|
||||
* @param {Array<{id: string, title: string | null, created_at: string, updated_at: string}>} rows
|
||||
* @param {{currentId?: string | null}} [options]
|
||||
*/
|
||||
renderConversationList(rows, { currentId = null } = {}) {
|
||||
this.$conversationsList.replaceChildren();
|
||||
if (rows.length === 0) {
|
||||
const $empty = this.document.createElement('div');
|
||||
$empty.className = 'conversations__empty';
|
||||
$empty.textContent = this.$conversationsSearch.value
|
||||
? 'No conversations match the search.'
|
||||
: 'No conversations yet.';
|
||||
this.$conversationsList.appendChild($empty);
|
||||
return;
|
||||
}
|
||||
for (const conversation of rows) {
|
||||
const $item =
|
||||
this.$tplConversationItem.content.cloneNode(true).firstElementChild;
|
||||
$item.dataset.conversationId = conversation.id;
|
||||
$item.querySelector('.conversations__item-title').textContent =
|
||||
conversationLabel(conversation);
|
||||
$item.querySelector('.conversations__item-updated').textContent =
|
||||
`Updated ${CONVERSATION_DATE.format(
|
||||
new Date(conversation.updated_at),
|
||||
)}`;
|
||||
if (conversation.id === currentId) {
|
||||
$item.classList.add('conversations__item--current');
|
||||
$item.setAttribute('aria-current', 'true');
|
||||
}
|
||||
this.$conversationsList.appendChild($item);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
// MUTE
|
||||
// ====================
|
||||
@@ -1025,6 +1117,35 @@ export class UI {
|
||||
this.handlers.onVoiceChange(this.$voice.value),
|
||||
);
|
||||
|
||||
// Conversations modal. The toolbar button opens it through the
|
||||
// data-action route; these listeners close it and report the
|
||||
// search text and the selected row.
|
||||
this.$conversationsClose.addEventListener('click', () =>
|
||||
this.closeConversations(),
|
||||
);
|
||||
this.$conversationsOverlay.addEventListener('click', (event) => {
|
||||
if (event.target === this.$conversationsOverlay) {
|
||||
this.closeConversations();
|
||||
}
|
||||
});
|
||||
this.document.addEventListener('keydown', (event) => {
|
||||
if (
|
||||
event.key === 'Escape' &&
|
||||
this.$conversationsOverlay.classList.contains('open')
|
||||
) {
|
||||
event.preventDefault();
|
||||
this.closeConversations();
|
||||
}
|
||||
});
|
||||
this.$conversationsSearch.addEventListener('input', () =>
|
||||
this.handlers.onConversationSearch(this.$conversationsSearch.value),
|
||||
);
|
||||
this.$conversationsList.addEventListener('click', (event) => {
|
||||
const $item = event.target.closest('[data-conversation-id]');
|
||||
if (!$item) return;
|
||||
this.handlers.onSelectConversation($item.dataset.conversationId);
|
||||
});
|
||||
|
||||
// Compose settings modal.
|
||||
this.$composeSettings.addEventListener('click', () =>
|
||||
this.handlers.onComposeSettings(),
|
||||
@@ -1152,6 +1273,10 @@ export class UI {
|
||||
this._clearConfirm();
|
||||
this.handlers.onLocationToggle();
|
||||
break;
|
||||
case 'conversations':
|
||||
this._clearConfirm();
|
||||
this.openConversations();
|
||||
break;
|
||||
case 'settings':
|
||||
this._clearConfirm();
|
||||
this._openSettings();
|
||||
|
||||
Reference in New Issue
Block a user