webui: implement pinned conversations support (#21387)

* webui: implement pinned conversations support

* webui: linter/prettier pass

* Fix the unused handleMobileSidebarItemClick from the component.

* the search should find pinned conversations as well

Co-authored-by: Pascal <admin@serveurperso.com>

---------

Co-authored-by: Pascal <admin@serveurperso.com>
This commit is contained in:
Rémy Mathieu
2026-06-09 21:33:22 +02:00
committed by GitHub
co-authored by Pascal
parent d73cd07674
commit 76da2450a4
5 changed files with 117 additions and 7 deletions
@@ -506,6 +506,33 @@ class ConversationsStore {
}
}
/**
* Toggles the pinned status of a conversation.
* @param convId - The conversation ID to toggle
* @returns The new pinned status
*/
async toggleConversationPin(convId: string): Promise<boolean> {
try {
const newPinnedState = await DatabaseService.toggleConversationPin(convId);
const convIndex = this.conversations.findIndex((c) => c.id === convId);
if (convIndex !== -1) {
this.conversations[convIndex].pinned = newPinnedState;
this.conversations = [...this.conversations];
}
if (this.activeConversation?.id === convId) {
this.activeConversation = { ...this.activeConversation, pinned: newPinnedState };
}
return newPinnedState;
} catch (error) {
console.error('Failed to toggle conversation pin:', error);
return false;
}
}
/**
* Updates conversation title with optional confirmation dialog based on settings
* @param convId - The conversation ID to update
@@ -1057,6 +1084,14 @@ export const isConversationsInitialized = () => conversationsStore.isInitialized
* Builds a flat tree of conversations with depth levels for nested forks.
* Accepts a pre-filtered list so search filtering stays in the component.
*/
// Pinned conversations first, then by lastModified descending
const comparePinnedThenRecent = (a: DatabaseConversation, b: DatabaseConversation) => {
if (a.pinned && !b.pinned) return -1;
if (!a.pinned && b.pinned) return 1;
return b.lastModified - a.lastModified;
};
export function buildConversationTree(convs: DatabaseConversation[]): ConversationTreeItem[] {
const childrenByParent = new SvelteMap<string, DatabaseConversation[]>();
const forkIds = new SvelteSet<string>();
@@ -1081,7 +1116,7 @@ export function buildConversationTree(convs: DatabaseConversation[]): Conversati
const children = childrenByParent.get(conv.id);
if (children) {
children.sort((a, b) => b.lastModified - a.lastModified);
children.sort(comparePinnedThenRecent);
for (const child of children) {
walk(child, depth + 1);
@@ -1089,7 +1124,7 @@ export function buildConversationTree(convs: DatabaseConversation[]): Conversati
}
}
const roots = convs.filter((c) => !forkIds.has(c.id));
const roots = convs.filter((c) => !forkIds.has(c.id)).sort(comparePinnedThenRecent);
for (const root of roots) {
walk(root, 0);
}