import type { Plugin } from "../../core/types"; import { booleanSetting } from "@/plugins/core/settingsHelpers"; import { waitForElm } from "@/seqta/utils/waitForElm"; import styles from "./styles.css?inline"; const messageFoldersSettings = { showTagsInAllMessages: booleanSetting({ default: true, title: "Show folder tags in All Messages", description: "When off, folder tags are not shown on the message list until you select a folder.", }), hideFolderedMessagesInAll: booleanSetting({ default: true, title: "Hide foldered messages in All Messages", description: "When on, messages assigned to a custom folder are hidden from the inbox until you open that folder.", }), } as const; interface Folder { id: string; name: string; color: string; emoji: string; } interface MessageFoldersStorage { folders: Folder[]; messageAssignments: Record; } const FOLDER_COLORS = [ "#3b82f6", "#ef4444", "#22c55e", "#f59e0b", "#8b5cf6", "#ec4899", "#14b8a6", "#f97316", ]; const FOLDER_HEROICONS = [ ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ]; const FOLDER_ICON_SVG = ``; const PLUS_SVG = ``; const CHECK_SVG_WHITE = ``; const CLOSE_SVG = ``; const EDIT_SVG = ``; const TRASH_SVG = ``; const CHEVRON_SVG = ``; const DRAG_SVG = ``; function generateId(): string { return Date.now().toString(36) + Math.random().toString(36).slice(2, 7); } const messageFoldersPlugin: Plugin = { id: "messageFolders", name: "Message Folders", description: "Organize direct messages into custom folders", version: "2.0.0", settings: messageFoldersSettings, disableToggle: true, defaultEnabled: true, run: async (api) => { const styleEl = document.createElement("style"); styleEl.textContent = styles; document.head.appendChild(styleEl); await api.storage.loaded; if (!api.storage.folders) api.storage.folders = []; if (!api.storage.messageAssignments) api.storage.messageAssignments = {}; let activeFolderId: string | null = null; let messageListObserver: MutationObserver | null = null; let sidebarObserver: MutationObserver | null = null; let actionsObserver: MutationObserver | null = null; let openDropdown: HTMLElement | null = null; let dropdownCloseHandler: ((e: MouseEvent) => void) | null = null; let foldedSection: HTMLElement | null = null; const unregisters: Array<{ unregister: () => void }> = []; const getFolders = (): Folder[] => api.storage.folders ?? []; const getAssignments = (): Record => api.storage.messageAssignments ?? {}; const saveFolders = (folders: Folder[]) => { api.storage.folders = [...folders]; }; const saveAssignments = (assignments: Record) => { api.storage.messageAssignments = { ...assignments }; }; const getMessageFolderIds = (messageId: string): string[] => { const assignments = getAssignments(); const ids: string[] = []; for (const [folderId, msgIds] of Object.entries(assignments)) { if (msgIds.includes(messageId)) ids.push(folderId); } return ids; }; const assignMessageToFolder = (messageId: string, folderId: string, add: boolean) => { const assignments = getAssignments(); if (!assignments[folderId]) assignments[folderId] = []; const idx = assignments[folderId].indexOf(messageId); if (add && idx < 0) { assignments[folderId].push(messageId); } else if (!add && idx >= 0) { assignments[folderId].splice(idx, 1); } saveAssignments(assignments); }; const toggleMessageInFolder = (messageId: string, folderId: string) => { const assignments = getAssignments(); if (!assignments[folderId]) assignments[folderId] = []; const idx = assignments[folderId].indexOf(messageId); if (idx >= 0) { assignments[folderId].splice(idx, 1); } else { assignments[folderId].push(messageId); } saveAssignments(assignments); }; const getFolderMessageCount = (folderId: string): number => { return (getAssignments()[folderId] ?? []).length; }; const restoreSubjectPlain = (subject: Element) => { subject.querySelector(".bsplus-msg-badges")?.remove(); const textWrap = subject.querySelector(".bsplus-subject-text"); if (textWrap) { subject.textContent = textWrap.textContent ?? ""; } }; const isMessageInAnyCustomFolder = (messageId: string): boolean => { for (const msgIds of Object.values(getAssignments())) { if (msgIds.includes(messageId)) return true; } return false; }; const shouldShowBadgesInList = (): boolean => { return api.settings.showTagsInAllMessages || activeFolderId !== null; }; const getSelectedMessageId = (): string | null => { const selectedMsg = document.querySelector("[class*='MessageList__selected___']"); return selectedMsg?.getAttribute("data-message") ?? null; }; const getMessageIdFromEvent = (target: HTMLElement): string | null => { const li = target.closest("li[data-message]"); return li?.getAttribute("data-message") ?? null; }; const getAllVisibleMessageIds = (): string[] => { const ids: string[] = []; document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]").forEach((li) => { const id = li.getAttribute("data-message"); if (id) ids.push(id); }); return ids; }; const showConfirmModal = (title: string, message: string, onConfirm: () => void) => { const overlay = document.createElement("div"); overlay.className = "bsplus-modal-overlay"; const modal = document.createElement("div"); modal.className = "bsplus-modal"; modal.innerHTML = `

${title}

${message}

`; overlay.appendChild(modal); const remove = () => { overlay.remove(); document.removeEventListener("keydown", onKey); }; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") remove(); }; overlay.addEventListener("click", (e) => { if (e.target === overlay) remove(); }); modal.querySelector(".bsplus-modal-btn-cancel")!.addEventListener("click", remove); modal.querySelector(".bsplus-modal-btn-danger")!.addEventListener("click", () => { onConfirm(); remove(); }); document.body.appendChild(overlay); document.addEventListener("keydown", onKey); }; const renderSidebarFolders = () => { const sidebar = document.querySelector("[class*='Viewer__sidebar___']"); if (!sidebar) return; const ol = sidebar.querySelector("ol"); if (!ol) return; let section = ol.querySelector(".bsplus-folders-section") as HTMLElement; if (!section) { section = document.createElement("div"); section.className = "bsplus-folders-section"; ol.appendChild(section); } foldedSection = section; const folders = getFolders(); section.innerHTML = ""; const header = document.createElement("div"); header.className = "bsplus-folders-header"; header.dataset.folded = "false"; const collapseBtn = document.createElement("button"); collapseBtn.className = "bsplus-folders-collapse"; collapseBtn.innerHTML = CHEVRON_SVG; collapseBtn.title = "Collapse"; collapseBtn.addEventListener("click", (e) => { e.stopPropagation(); const isFolded = collapseBtn.classList.toggle("bsplus-folded"); section.classList.toggle("bsplus-section-folded", isFolded); collapseBtn.title = isFolded ? "Expand" : "Collapse"; }); header.appendChild(collapseBtn); const label = document.createElement("span"); label.textContent = "Folders"; header.appendChild(label); const addBtn = document.createElement("button"); addBtn.className = "bsplus-folders-add-btn"; addBtn.title = "New folder"; addBtn.innerHTML = PLUS_SVG; addBtn.addEventListener("click", (e) => { e.stopPropagation(); showNewFolderInput(section!); }); header.appendChild(addBtn); section.appendChild(header); const allItem = document.createElement("div"); allItem.className = `bsplus-folder-item bsplus-all-msgs${activeFolderId === null ? " bsplus-folder-active" : ""}`; allItem.innerHTML = ` All Messages `; allItem.addEventListener("click", () => { activeFolderId = null; applyFolderFilter(); applyBadges(); renderSidebarFolders(); setTimeout(() => { applyFolderFilter(); applyBadges(); }, 100); }); section.appendChild(allItem); for (const folder of folders) { const item = document.createElement("div"); item.className = `bsplus-folder-item${activeFolderId === folder.id ? " bsplus-folder-active" : ""}`; item.dataset.folderId = folder.id; item.draggable = true; const dragHandle = document.createElement("div"); dragHandle.className = "bsplus-folder-drag"; dragHandle.innerHTML = DRAG_SVG; item.appendChild(dragHandle); const dot = document.createElement("div"); dot.className = "bsplus-folder-dot"; dot.style.background = folder.color; item.appendChild(dot); const iconSpan = document.createElement("span"); iconSpan.className = "bsplus-folder-icon"; iconSpan.innerHTML = folder.emoji || FOLDER_HEROICONS[0]; item.appendChild(iconSpan); const name = document.createElement("span"); name.className = "bsplus-folder-name"; name.textContent = folder.name; item.appendChild(name); const actions = document.createElement("div"); actions.className = "bsplus-folder-actions"; const editBtn = document.createElement("button"); editBtn.className = "bsplus-folder-action-btn"; editBtn.title = "Rename"; editBtn.innerHTML = EDIT_SVG; editBtn.addEventListener("click", (e) => { e.stopPropagation(); showEditFolderInput(section!, folder); }); actions.appendChild(editBtn); const deleteBtn = document.createElement("button"); deleteBtn.className = "bsplus-folder-action-btn"; deleteBtn.title = "Delete"; deleteBtn.innerHTML = TRASH_SVG; deleteBtn.addEventListener("click", (e) => { e.stopPropagation(); showConfirmModal("Delete folder", `Remove "${folder.name}"? Messages won't be deleted.`, () => { const folders = getFolders().filter((f) => f.id !== folder.id); saveFolders(folders); const assignments = getAssignments(); delete assignments[folder.id]; saveAssignments(assignments); if (activeFolderId === folder.id) activeFolderId = null; applyFolderFilter(); applyBadges(); renderSidebarFolders(); }); }); actions.appendChild(deleteBtn); item.appendChild(actions); const count = document.createElement("span"); count.className = "bsplus-folder-count"; const c = getFolderMessageCount(folder.id); count.textContent = c > 0 ? String(c) : ""; item.appendChild(count); item.addEventListener("click", () => { activeFolderId = folder.id; applyFolderFilter(); applyBadges(); renderSidebarFolders(); setTimeout(() => { applyFolderFilter(); applyBadges(); }, 100); }); item.addEventListener("dragstart", (e) => { e.dataTransfer?.setData("text/plain", `reorder:${folder.id}`); item.classList.add("bsplus-dragging"); }); item.addEventListener("dragend", () => { item.classList.remove("bsplus-dragging"); document.querySelectorAll(".bsplus-folder-item").forEach((el) => el.classList.remove("bsplus-drag-over")); }); item.addEventListener("dragover", (e) => { e.preventDefault(); const data = e.dataTransfer?.getData("text/plain") || ""; if (data.startsWith("reorder:") && !data.includes(folder.id)) { item.classList.add("bsplus-drag-over"); } }); item.addEventListener("dragleave", () => { item.classList.remove("bsplus-drag-over"); }); item.addEventListener("drop", (e) => { e.preventDefault(); item.classList.remove("bsplus-drag-over"); const data = e.dataTransfer?.getData("text/plain") || ""; if (data.startsWith("reorder:")) { const draggedId = data.replace("reorder:", ""); const folders = getFolders(); const draggedIdx = folders.findIndex((f) => f.id === draggedId); const targetIdx = folders.findIndex((f) => f.id === folder.id); if (draggedIdx >= 0 && targetIdx >= 0 && draggedIdx !== targetIdx) { const [removed] = folders.splice(draggedIdx, 1); folders.splice(targetIdx, 0, removed); saveFolders(folders); renderSidebarFolders(); } } }); section.appendChild(item); } section.addEventListener("dragover", (e) => { e.preventDefault(); }); section.addEventListener("drop", (e) => { e.preventDefault(); const data = e.dataTransfer?.getData("text/plain") || ""; if (data.startsWith("msg:")) { const messageId = data.replace("msg:", ""); const folderId = (e.target as HTMLElement).closest("[data-folder-id]")?.getAttribute("data-folder-id"); if (messageId && folderId) { assignMessageToFolder(messageId, folderId, true); applyBadges(); applyFolderFilter(); renderSidebarFolders(); } } }); attachDragListeners(); }; const attachDragListeners = () => { document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]").forEach((li) => { if (li.getAttribute("data-bsplus-drag") === "true") return; li.setAttribute("data-bsplus-drag", "true"); li.draggable = true; li.addEventListener("dragstart", (e) => { const id = li.getAttribute("data-message"); if (id) { e.dataTransfer?.setData("text/plain", `msg:${id}`); li.classList.add("bsplus-msg-dragging"); } }); li.addEventListener("dragend", () => { li.classList.remove("bsplus-msg-dragging"); document.querySelectorAll(".bsplus-folder-item").forEach((el) => el.classList.remove("bsplus-drag-over")); }); }); }; const showNewFolderInput = (container: Element, editFolder?: Folder) => { const existing = container.querySelector(".bsplus-folder-input"); if (existing) existing.remove(); container.querySelector(".bsplus-folder-colors")?.remove(); let selectedColor = editFolder?.color ?? FOLDER_COLORS[Math.floor(Math.random() * FOLDER_COLORS.length)]; let selectedIcon = editFolder?.emoji ?? FOLDER_HEROICONS[Math.floor(Math.random() * FOLDER_HEROICONS.length)]; const row = document.createElement("div"); row.className = "bsplus-folder-input"; const input = document.createElement("input"); input.type = "text"; input.placeholder = editFolder ? "Rename folder\u2026" : "Folder name\u2026"; input.value = editFolder?.name ?? ""; input.maxLength = 30; const iconBtn = document.createElement("button"); iconBtn.className = "bsplus-folder-icon-btn"; iconBtn.title = "Pick icon"; iconBtn.innerHTML = selectedIcon; iconBtn.addEventListener("click", (e) => { e.stopPropagation(); const picker = container.querySelector(".bsplus-icon-picker") as HTMLElement | null; if (picker) { picker.remove(); return; } showIconPicker(container, (iconSvg) => { selectedIcon = iconSvg; iconBtn.innerHTML = iconSvg; }); }); const confirmBtn = document.createElement("button"); confirmBtn.className = "bsplus-folder-input-confirm"; confirmBtn.innerHTML = CHECK_SVG_WHITE; const cancelBtn = document.createElement("button"); cancelBtn.className = "bsplus-folder-input-cancel"; cancelBtn.innerHTML = CLOSE_SVG; row.appendChild(iconBtn); row.appendChild(input); row.appendChild(confirmBtn); row.appendChild(cancelBtn); const colorRow = document.createElement("div"); colorRow.className = "bsplus-folder-colors"; for (const color of FOLDER_COLORS) { const swatch = document.createElement("button"); swatch.className = `bsplus-folder-color-opt${color === selectedColor ? " bsplus-color-selected" : ""}`; swatch.style.background = color; swatch.addEventListener("click", (e) => { e.stopPropagation(); selectedColor = color; colorRow.querySelectorAll(".bsplus-folder-color-opt").forEach((s) => s.classList.toggle("bsplus-color-selected", (s as HTMLElement).style.background === color), ); }); colorRow.appendChild(swatch); } const confirm = () => { const name = input.value.trim(); if (!name) return; if (editFolder) { const folders = getFolders().map((f) => f.id === editFolder.id ? { ...f, name, color: selectedColor, emoji: selectedIcon } : f, ); saveFolders(folders); } else { const folder: Folder = { id: generateId(), name, color: selectedColor, emoji: selectedIcon }; saveFolders([...getFolders(), folder]); } applyBadges(); renderSidebarFolders(); }; confirmBtn.addEventListener("click", (e) => { e.stopPropagation(); confirm(); }); cancelBtn.addEventListener("click", (e) => { e.stopPropagation(); renderSidebarFolders(); }); input.addEventListener("keydown", (e) => { if (e.key === "Enter") confirm(); if (e.key === "Escape") renderSidebarFolders(); }); container.appendChild(row); container.appendChild(colorRow); requestAnimationFrame(() => input.focus()); }; const showIconPicker = (container: Element, onSelect: (iconSvg: string) => void) => { const existing = container.querySelector(".bsplus-icon-picker"); if (existing) existing.remove(); const picker = document.createElement("div"); picker.className = "bsplus-icon-picker"; for (const icon of FOLDER_HEROICONS) { const btn = document.createElement("button"); btn.className = "bsplus-icon-opt"; btn.innerHTML = icon; btn.addEventListener("click", (e) => { e.stopPropagation(); onSelect(icon); picker.remove(); }); picker.appendChild(btn); } container.appendChild(picker); }; const showEditFolderInput = (container: Element, folder: Folder) => { showNewFolderInput(container, folder); }; const attachNativeSidebarListeners = () => { const sidebar = document.querySelector("[class*='Viewer__sidebar___']"); if (!sidebar) return; const ol = sidebar.querySelector("ol"); if (!ol) return; ol.addEventListener("click", (e) => { const target = e.target as HTMLElement; if (target.closest(".bsplus-folders-section")) return; const li = target.closest("li"); if (li && ol.contains(li)) { if (activeFolderId !== null) { activeFolderId = null; applyFolderFilter(); applyBadges(); renderSidebarFolders(); } } }); }; const closeDropdown = () => { if (openDropdown) { openDropdown.remove(); openDropdown = null; } if (dropdownCloseHandler) { document.removeEventListener("click", dropdownCloseHandler, true); dropdownCloseHandler = null; } }; const showFolderDropdown = (anchor: HTMLElement, messageId: string) => { closeDropdown(); const dropdown = document.createElement("div"); dropdown.className = "bsplus-folder-dropdown"; dropdown.dataset.msgId = messageId; const folders = getFolders(); const currentFolderIds = getMessageFolderIds(messageId); if (folders.length === 0) { const empty = document.createElement("div"); empty.className = "bsplus-folder-dropdown-empty"; empty.textContent = "No folders yet"; dropdown.appendChild(empty); } else { for (const folder of folders) { const isChecked = currentFolderIds.includes(folder.id); const item = document.createElement("button"); item.className = `bsplus-folder-dropdown-item${isChecked ? " bsplus-checked" : ""}`; item.dataset.folderId = folder.id; const check = document.createElement("div"); check.className = "bsplus-folder-dropdown-check"; check.style.borderColor = isChecked ? folder.color : ""; check.style.background = isChecked ? folder.color : ""; check.innerHTML = CHECK_SVG_WHITE; const dot = document.createElement("div"); dot.className = "bsplus-folder-dot"; dot.style.background = folder.color; const iconSpan = document.createElement("span"); iconSpan.className = "bsplus-folder-icon"; iconSpan.innerHTML = folder.emoji || FOLDER_HEROICONS[0]; const name = document.createElement("span"); name.textContent = folder.name; item.appendChild(check); item.appendChild(dot); item.appendChild(iconSpan); item.appendChild(name); item.addEventListener("click", (e) => { e.stopPropagation(); e.preventDefault(); toggleMessageInFolder(messageId, folder.id); const nowChecked = getMessageFolderIds(messageId).includes(folder.id); item.classList.toggle("bsplus-checked", nowChecked); check.style.borderColor = nowChecked ? folder.color : ""; check.style.background = nowChecked ? folder.color : ""; applyBadges(); applyFolderFilter(); renderSidebarFolders(); }); dropdown.appendChild(item); } } anchor.appendChild(dropdown); openDropdown = dropdown; dropdownCloseHandler = (e: MouseEvent) => { if (!dropdown.contains(e.target as Node) && !anchor.contains(e.target as Node)) { closeDropdown(); } }; setTimeout(() => { document.addEventListener("click", dropdownCloseHandler!, true); }, 0); }; const injectFolderButton = (actionsBar: Element) => { if (actionsBar.querySelector(".bsplus-folder-btn")) return; const wrapper = document.createElement("div"); wrapper.className = "bsplus-folder-btn"; wrapper.style.position = "relative"; wrapper.style.display = "inline-block"; const btn = document.createElement("button"); const btnClasses = actionsBar.querySelector("button")?.className ?? ""; btn.className = btnClasses; btn.title = "Add to folder"; btn.innerHTML = FOLDER_ICON_SVG; btn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); const selectedMsg = document.querySelector("[class*='MessageList__selected___']"); const messageId = selectedMsg?.getAttribute("data-message"); if (!messageId) return; showFolderDropdown(wrapper, messageId); }); wrapper.appendChild(btn); const moreMenu = actionsBar.querySelector("[class*='MenuButton__Menu___']"); if (moreMenu) { actionsBar.insertBefore(wrapper, moreMenu); } else { actionsBar.appendChild(wrapper); } }; const showContextMenu = (e: MouseEvent, messageId: string) => { e.preventDefault(); e.stopPropagation(); closeDropdown(); const existing = document.querySelector(".bsplus-context-menu"); if (existing) existing.remove(); const menu = document.createElement("div"); menu.className = "bsplus-context-menu"; menu.style.left = `${e.clientX}px`; menu.style.top = `${e.clientY}px`; const title = document.createElement("div"); title.className = "bsplus-context-title"; title.textContent = "Add to folder"; menu.appendChild(title); const folders = getFolders(); const currentFolderIds = getMessageFolderIds(messageId); if (folders.length === 0) { const empty = document.createElement("div"); empty.className = "bsplus-context-empty"; empty.textContent = "No folders"; menu.appendChild(empty); } else { for (const folder of folders) { const isChecked = currentFolderIds.includes(folder.id); const item = document.createElement("button"); item.className = `bsplus-context-item${isChecked ? " bsplus-context-checked" : ""}`; const dot = document.createElement("div"); dot.className = "bsplus-folder-dot"; dot.style.background = folder.color; const iconSpan = document.createElement("span"); iconSpan.className = "bsplus-folder-icon"; iconSpan.innerHTML = folder.emoji || FOLDER_HEROICONS[0]; const name = document.createElement("span"); name.textContent = folder.name; item.appendChild(dot); item.appendChild(iconSpan); item.appendChild(name); if (isChecked) { const check = document.createElement("span"); check.className = "bsplus-context-checkmark"; check.textContent = "\u2713"; item.appendChild(check); } item.addEventListener("click", (e) => { e.stopPropagation(); toggleMessageInFolder(messageId, folder.id); applyBadges(); applyFolderFilter(); renderSidebarFolders(); menu.remove(); }); menu.appendChild(item); } } document.body.appendChild(menu); const closeMenu = (ev: MouseEvent) => { if (!menu.contains(ev.target as Node)) { menu.remove(); document.removeEventListener("click", closeMenu); } }; setTimeout(() => document.addEventListener("click", closeMenu), 0); }; const applyBadges = () => { const messageItems = document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]"); if (!shouldShowBadgesInList()) { for (const li of messageItems) { const subject = li.querySelector("[class*='MessageList__subject___']"); if (subject && (subject.querySelector(".bsplus-msg-badges") || subject.querySelector(".bsplus-subject-text"))) { restoreSubjectPlain(subject); } else { li.querySelector(".bsplus-msg-badges")?.remove(); } } return; } const folders = getFolders(); const assignments = getAssignments(); for (const li of messageItems) { const msgId = li.getAttribute("data-message"); if (!msgId) continue; let badgeContainer = li.querySelector(".bsplus-msg-badges") as HTMLElement | null; const folderIds: string[] = []; for (const [fId, mIds] of Object.entries(assignments)) { if (mIds.includes(msgId)) folderIds.push(fId); } if (folderIds.length === 0) { badgeContainer?.remove(); continue; } if (!badgeContainer) { badgeContainer = document.createElement("div"); badgeContainer.className = "bsplus-msg-badges"; const subject = li.querySelector("[class*='MessageList__subject___']"); if (subject) { if (!subject.querySelector(".bsplus-subject-text")) { const textWrap = document.createElement("span"); textWrap.className = "bsplus-subject-text"; textWrap.textContent = subject.textContent; subject.textContent = ""; subject.appendChild(textWrap); } subject.appendChild(badgeContainer); } else { li.appendChild(badgeContainer); } } badgeContainer.innerHTML = ""; for (const fId of folderIds) { const folder = folders.find((f) => f.id === fId); if (!folder) continue; const badge = document.createElement("span"); badge.className = "bsplus-msg-badge"; badge.style.background = folder.color; badge.innerHTML = `${folder.emoji ? `${folder.emoji}` : ""}${folder.name}`; badge.title = `Filter by "${folder.name}"`; badge.addEventListener("click", (e) => { e.stopPropagation(); activeFolderId = folder.id; applyFolderFilter(); applyBadges(); renderSidebarFolders(); }); badgeContainer.appendChild(badge); } } }; const applyFolderFilter = () => { const messageItems = document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]"); const moreBtn = document.querySelector("[class*='MessageList__MessageList___'] ol > button"); if (activeFolderId === null) { if (api.settings.hideFolderedMessagesInAll) { for (const li of messageItems) { const msgId = li.getAttribute("data-message"); if (msgId && isMessageInAnyCustomFolder(msgId)) { li.classList.add("bsplus-folder-hidden"); } else { li.classList.remove("bsplus-folder-hidden"); } } } else { for (const li of messageItems) { li.classList.remove("bsplus-folder-hidden"); } } if (moreBtn) (moreBtn as HTMLElement).classList.remove("bsplus-folder-hidden"); return; } const folderMsgIds = getAssignments()[activeFolderId] ?? []; for (const li of messageItems) { const msgId = li.getAttribute("data-message"); if (msgId && folderMsgIds.includes(msgId)) { li.classList.remove("bsplus-folder-hidden"); } else { li.classList.add("bsplus-folder-hidden"); } } if (moreBtn) (moreBtn as HTMLElement).classList.add("bsplus-folder-hidden"); }; const setupMessageListObserver = () => { const messageList = document.querySelector("[class*='MessageList__MessageList___'] ol"); if (!messageList || messageListObserver) return; messageListObserver = new MutationObserver(() => { applyBadges(); applyFolderFilter(); attachDragListeners(); attachContextMenuListeners(); }); messageListObserver.observe(messageList, { childList: true, subtree: false }); }; const attachContextMenuListeners = () => { document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]").forEach((li) => { if (li.getAttribute("data-bsplus-ctx") === "true") return; li.setAttribute("data-bsplus-ctx", "true"); li.addEventListener("contextmenu", (e) => { const msgId = li.getAttribute("data-message"); if (msgId) { showContextMenu(e, msgId); } }); }); }; const setupActionsObserver = () => { if (actionsObserver) return; const target = document.querySelector("[class*='Viewer__Viewer___']") ?? document.querySelector("div.messages"); if (!target) return; actionsObserver = new MutationObserver(() => { const actionsBar = document.querySelector("[class*='Message__actions___']"); if (actionsBar && !actionsBar.querySelector(".bsplus-folder-btn")) { injectFolderButton(actionsBar); } }); actionsObserver.observe(target, { childList: true, subtree: true }); }; const handleMessagesPage = async () => { await waitForElm("[class*='Viewer__sidebar___'] ol", true, 50, 100); renderSidebarFolders(); attachNativeSidebarListeners(); await waitForElm("[class*='MessageList__MessageList___'] ol", true, 50, 100); applyBadges(); applyFolderFilter(); setupMessageListObserver(); setupActionsObserver(); attachDragListeners(); attachContextMenuListeners(); const actionsBar = document.querySelector("[class*='Message__actions___']"); if (actionsBar) injectFolderButton(actionsBar); const sidebar = document.querySelector("[class*='Viewer__sidebar___']"); if (sidebar && !sidebarObserver) { sidebarObserver = new MutationObserver(() => { const ol = sidebar.querySelector("ol"); if (ol && !ol.querySelector(".bsplus-folders-section")) { renderSidebarFolders(); attachNativeSidebarListeners(); } }); sidebarObserver.observe(sidebar, { childList: true, subtree: true }); } }; const mountUnsub = api.seqta.onMount("div.messages", handleMessagesPage); unregisters.push(mountUnsub); unregisters.push( api.settings.onChange("showTagsInAllMessages", () => { applyBadges(); }), ); unregisters.push( api.settings.onChange("hideFolderedMessagesInAll", () => { applyFolderFilter(); }), ); return () => { for (const u of unregisters) u.unregister(); messageListObserver?.disconnect(); sidebarObserver?.disconnect(); actionsObserver?.disconnect(); closeDropdown(); styleEl.remove(); document.querySelectorAll(".bsplus-folders-section").forEach((el) => el.remove()); document.querySelectorAll(".bsplus-folder-btn").forEach((el) => el.remove()); document.querySelectorAll(".bsplus-msg-badges").forEach((el) => el.remove()); document.querySelectorAll(".bsplus-context-menu").forEach((el) => el.remove()); document.querySelectorAll("[class*='MessageList__subject___']").forEach((subject) => { if (subject.querySelector(".bsplus-subject-text")) { restoreSubjectPlain(subject); } }); document.querySelectorAll(".bsplus-folder-hidden").forEach((el) => el.classList.remove("bsplus-folder-hidden"), ); document.querySelectorAll(".bsplus-modal-overlay").forEach((el) => el.remove()); }; }, }; export default messageFoldersPlugin;