| 1 | import { createStore } from "/js/AlpineStore.js"; |
| 2 | import { callJsonApi } from "/js/api.js"; |
| 3 | import { toastFrontendError } from "/components/notifications/notification-store.js"; |
| 4 | import { store as sidebarStore } from "/components/sidebar/sidebar-store.js"; |
| 5 | |
| 6 | const PLUGIN_ID = "_pin_to_top"; |
| 7 | |
| 8 | const model = { |
| 9 | pins: { chat: {}, task: {} }, |
| 10 | _initialized: false, |
| 11 | |
| 12 | async init() { |
| 13 | if (this._initialized) return; |
| 14 | this._initialized = true; |
| 15 | |
| 16 | for (const kind of ["chat", "task"]) { |
| 17 | sidebarStore.registerRowListExtension(kind, PLUGIN_ID, { |
| 18 | sort: (items) => this.sortItems(kind, items), |
| 19 | dividerBefore: (item, index, items) => |
| 20 | this.dividerBefore(kind, item, index, items), |
| 21 | }); |
| 22 | } |
| 23 | |
| 24 | await this.loadPins(); |
| 25 | }, |
| 26 | |
| 27 | async loadPins() { |
| 28 | try { |
| 29 | const response = await callJsonApi(`/plugins/${PLUGIN_ID}/get_pins`, {}); |
| 30 | this.pins = { |
| 31 | chat: { ...(response?.pins?.chat || {}) }, |
| 32 | task: { ...(response?.pins?.task || {}) }, |
| 33 | }; |
| 34 | } catch (error) { |
| 35 | void toastFrontendError(error?.message || "Failed to load pinned items.", "Pin to Top"); |
| 36 | } |
| 37 | }, |
| 38 | |
| 39 | async toggleFromMenu(menuId, kind) { |
| 40 | const itemId = this.itemIdFromMenu(menuId, kind); |
| 41 | if (!itemId) return; |
| 42 | |
| 43 | try { |
| 44 | const response = await callJsonApi(`/plugins/${PLUGIN_ID}/toggle_pin`, { |
| 45 | kind, |
| 46 | item_id: itemId, |
| 47 | }); |
| 48 | const kindPins = { ...(this.pins[kind] || {}) }; |
| 49 | if (response?.pinned) kindPins[itemId] = response.timestamp; |
| 50 | else delete kindPins[itemId]; |
| 51 | this.pins = { ...this.pins, [kind]: kindPins }; |
| 52 | } catch (error) { |
| 53 | void toastFrontendError(error?.message || "Failed to update the pin.", "Pin to Top"); |
| 54 | } |
| 55 | }, |
| 56 | |
| 57 | itemIdFromMenu(menuId, kind) { |
| 58 | const prefix = `${kind}:`; |
| 59 | return typeof menuId === "string" && menuId.startsWith(prefix) |
| 60 | ? menuId.slice(prefix.length) |
| 61 | : ""; |
| 62 | }, |
| 63 | |
| 64 | isMenuItemPinned(menuId, kind) { |
| 65 | return this.isPinned(kind, this.itemIdFromMenu(menuId, kind)); |
| 66 | }, |
| 67 | |
| 68 | isPinned(kind, itemId) { |
| 69 | return Object.prototype.hasOwnProperty.call(this.pins[kind] || {}, itemId); |
| 70 | }, |
| 71 | |
| 72 | sortItems(kind, items) { |
| 73 | const kindPins = this.pins[kind] || {}; |
| 74 | return items |
| 75 | .map((item, index) => ({ item, index })) |
| 76 | .sort((left, right) => { |
| 77 | const leftPin = kindPins[left.item.id]; |
| 78 | const rightPin = kindPins[right.item.id]; |
| 79 | const leftPinned = leftPin !== undefined; |
| 80 | const rightPinned = rightPin !== undefined; |
| 81 | if (leftPinned !== rightPinned) return leftPinned ? -1 : 1; |
| 82 | if (leftPinned && leftPin !== rightPin) return leftPin - rightPin; |
| 83 | return left.index - right.index; |
| 84 | }) |
| 85 | .map(({ item }) => item); |
| 86 | }, |
| 87 | |
| 88 | dividerBefore(kind, item, index, items) { |
| 89 | return index > 0 |
| 90 | && !this.isPinned(kind, item.id) |
| 91 | && this.isPinned(kind, items[index - 1]?.id); |
| 92 | }, |
| 93 | }; |
| 94 | |
| 95 | export const store = createStore("pinToTop", model); |