diff --git a/content.js b/content.js index 8a6088f..e29cffc 100644 --- a/content.js +++ b/content.js @@ -1,13 +1,790 @@ const PERSON_ID_PREFIX = 'chat-topic-person-'; const ICON_ID_PREFIX = "presence-pill-"; const CHAT_ROSTER_PREFIX = "chat-roster-item-name-"; +const CHAT_TITLE_PREFIX = "title-chat-list-item_"; const SUGGEST_PEOPLE_PREFIX = "AUTOSUGGEST_SUGGESTION_PEOPLE"; const ROSTER_AVATAR_PREFIX = "roster-avatar-img-"; const SERP_PEOPLE_CARD_PREFIX = "serp-people-card-content-"; const PEOPLE_PICKER_PREFIX = "people-picker-entry-"; const PEOPLE_PICKER_SEL_PREFIX = "people-picker-selected-user-"; +const CHAT_CATEGORY_STORAGE_KEY = "chatCategories"; +const CHAT_CACHE_STORAGE_KEY = "chatCategoryChatCache"; +const CHAT_ORGANIZER_ROOT_ID = "teams-alias-chat-organizer-root"; +const CHAT_ORGANIZER_STYLE_ID = "teams-alias-chat-organizer-style"; +const CHAT_ORGANIZER_BUTTON_ID = "teams-alias-chat-organizer-button"; let debounceTimer = null; let isMutating = false; +let organizerStateLoaded = false; +let organizerSaveTimer = null; +const chatOrganizerState = { + categories: [], + chatCache: {}, + visibleChats: {}, + isModalOpen: false, + selectedCategoryId: "", + feedback: "", + draftCategoryName: "", + draftChatId: "", + visibleChatsExpanded: false, + expandedCategoryIds: {} +}; + +function getOrganizerRoot() { + return document.getElementById(CHAT_ORGANIZER_ROOT_ID); +} + +function escapeHtml(value) { + return String(value ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function normalizeChatId(rawValue) { + if (!rawValue) return ""; + let value = String(rawValue).trim(); + if (value.startsWith(CHAT_TITLE_PREFIX)) { + value = value.slice(CHAT_TITLE_PREFIX.length); + } + return value.startsWith("19:") ? value : ""; +} + +function createCategoryId() { + return `cat-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; +} + +function setOrganizerFeedback(message) { + chatOrganizerState.feedback = message || ""; + renderChatOrganizer(); +} + +function scheduleOrganizerStateSave() { + clearTimeout(organizerSaveTimer); + organizerSaveTimer = setTimeout(async () => { + await chrome.storage.local.set({ + [CHAT_CATEGORY_STORAGE_KEY]: chatOrganizerState.categories, + [CHAT_CACHE_STORAGE_KEY]: chatOrganizerState.chatCache + }); + }, 300); +} + +async function loadChatOrganizerState() { + if (organizerStateLoaded) return; + const result = await chrome.storage.local.get([ + CHAT_CATEGORY_STORAGE_KEY, + CHAT_CACHE_STORAGE_KEY + ]); + + chatOrganizerState.categories = Array.isArray(result[CHAT_CATEGORY_STORAGE_KEY]) + ? result[CHAT_CATEGORY_STORAGE_KEY] + : []; + chatOrganizerState.chatCache = result[CHAT_CACHE_STORAGE_KEY] || {}; + chatOrganizerState.selectedCategoryId = chatOrganizerState.categories[0]?.id || ""; + organizerStateLoaded = true; +} + +function getCategoryById(categoryId) { + return chatOrganizerState.categories.find(category => category.id === categoryId) || null; +} + +function getChatMeta(chatId) { + return chatOrganizerState.visibleChats[chatId] || chatOrganizerState.chatCache[chatId] || null; +} + +function inferAvatarFallback(name) { + const base = (name || "?").trim(); + return base.slice(0, 2).toUpperCase(); +} + +function findChatTitleElement(chatId) { + const normalizedChatId = normalizeChatId(chatId); + if (!normalizedChatId) return null; + return document.getElementById(`${CHAT_TITLE_PREFIX}${normalizedChatId}`); +} + +function findClickableChatElement(chatId) { + const titleElement = findChatTitleElement(chatId); + if (!titleElement) return null; + + const clickable = titleElement.closest('button, a, [role="button"], [role="link"], [role="listitem"]'); + if (clickable) return clickable; + + let current = titleElement; + for (let depth = 0; depth < 8 && current; depth += 1) { + if (typeof current.click === "function") { + return current; + } + current = current.parentElement; + } + return titleElement; +} + +function deepLinkToChat(chatId) { + const normalizedChatId = normalizeChatId(chatId); + if (!normalizedChatId) return false; + + const candidates = [ + `${window.location.origin}/l/chat/${encodeURIComponent(normalizedChatId)}/conversations`, + `https://teams.microsoft.com/l/chat/${encodeURIComponent(normalizedChatId)}/conversations` + ]; + + for (const url of candidates) { + try { + window.location.href = url; + return true; + } catch (error) {} + } + return false; +} + +function openChatById(chatId) { + const clickableElement = findClickableChatElement(chatId); + if (clickableElement) { + clickableElement.click(); + return true; + } + return deepLinkToChat(chatId); +} + +function extractChatAvatar(titleElement) { + const rowRoot = titleElement.closest('[role="listitem"], button, a, [role="button"]') || titleElement.parentElement; + if (!rowRoot) return ""; + + const avatarImage = rowRoot.querySelector("img"); + if (avatarImage?.src) { + return avatarImage.src; + } + + const avatarCandidate = rowRoot.querySelector('[style*="background-image"]'); + const inlineStyle = avatarCandidate?.style?.backgroundImage || ""; + const matched = inlineStyle.match(/url\(["']?(.*?)["']?\)/); + return matched?.[1] || ""; +} + +function scanVisibleChats() { + const titleElements = document.querySelectorAll(`[id^="${CHAT_TITLE_PREFIX}"]`); + let changed = false; + const nextVisibleChats = {}; + + titleElements.forEach(titleElement => { + const chatId = normalizeChatId(titleElement.id); + if (!chatId) return; + + const name = titleElement.textContent?.trim() || chatOrganizerState.chatCache[chatId]?.name || chatId; + const avatarUrl = extractChatAvatar(titleElement) || chatOrganizerState.chatCache[chatId]?.avatarUrl || ""; + const chatMeta = { + id: chatId, + name, + avatarUrl, + updatedAt: Date.now() + }; + + nextVisibleChats[chatId] = chatMeta; + + const cached = chatOrganizerState.chatCache[chatId]; + if (!cached || cached.name !== chatMeta.name || cached.avatarUrl !== chatMeta.avatarUrl) { + chatOrganizerState.chatCache[chatId] = chatMeta; + changed = true; + } + }); + + const visibleIds = Object.keys(nextVisibleChats); + const previousVisibleIds = Object.keys(chatOrganizerState.visibleChats); + if (visibleIds.length !== previousVisibleIds.length || visibleIds.some(id => !chatOrganizerState.visibleChats[id])) { + changed = true; + } + + chatOrganizerState.visibleChats = nextVisibleChats; + + if (changed) { + scheduleOrganizerStateSave(); + renderChatOrganizer(); + } +} + +function addChatToCategory(categoryId, rawChatId) { + const category = getCategoryById(categoryId); + const chatId = normalizeChatId(rawChatId); + + if (!category) { + setOrganizerFeedback("请选择一个分类。"); + return; + } + if (!chatId) { + setOrganizerFeedback("聊天 ID 格式无效,必须以 19: 开头。"); + return; + } + if (category.chatIds.includes(chatId)) { + setOrganizerFeedback("该聊天已存在于当前分类。"); + return; + } + + category.chatIds.push(chatId); + const meta = getChatMeta(chatId); + if (meta) { + chatOrganizerState.chatCache[chatId] = { + ...chatOrganizerState.chatCache[chatId], + ...meta, + updatedAt: Date.now() + }; + } else { + chatOrganizerState.chatCache[chatId] = { + id: chatId, + name: chatId, + avatarUrl: "", + updatedAt: Date.now() + }; + } + + scheduleOrganizerStateSave(); + setOrganizerFeedback("已加入分类。"); +} + +function removeChatFromCategory(categoryId, chatId) { + const category = getCategoryById(categoryId); + if (!category) return; + category.chatIds = category.chatIds.filter(id => id !== chatId); + scheduleOrganizerStateSave(); + setOrganizerFeedback("已移除聊天。"); +} + +function createCategory(name) { + const trimmedName = (name || "").trim(); + if (!trimmedName) { + setOrganizerFeedback("分类名称不能为空。"); + return; + } + + const category = { + id: createCategoryId(), + name: trimmedName, + chatIds: [] + }; + + chatOrganizerState.categories.push(category); + chatOrganizerState.expandedCategoryIds[category.id] = true; + chatOrganizerState.selectedCategoryId = category.id; + scheduleOrganizerStateSave(); + setOrganizerFeedback("分类已创建。"); +} + +function deleteCategory(categoryId) { + chatOrganizerState.categories = chatOrganizerState.categories.filter(category => category.id !== categoryId); + delete chatOrganizerState.expandedCategoryIds[categoryId]; + if (chatOrganizerState.selectedCategoryId === categoryId) { + chatOrganizerState.selectedCategoryId = chatOrganizerState.categories[0]?.id || ""; + } + scheduleOrganizerStateSave(); + setOrganizerFeedback("分类已删除。"); +} + +function isCategoryExpanded(categoryId) { + return Boolean(chatOrganizerState.expandedCategoryIds[categoryId]); +} + +function toggleCategoryExpanded(categoryId) { + chatOrganizerState.expandedCategoryIds[categoryId] = !isCategoryExpanded(categoryId); + renderChatOrganizer(); +} + +function toggleVisibleChatsExpanded() { + chatOrganizerState.visibleChatsExpanded = !chatOrganizerState.visibleChatsExpanded; + renderChatOrganizer(); +} + +function renderChatCard(chatId, categoryId) { + const meta = getChatMeta(chatId) || { id: chatId, name: chatId, avatarUrl: "" }; + const avatar = meta.avatarUrl + ? `` + : `${escapeHtml(inferAvatarFallback(meta.name))}`; + + return ` +
+ + +
+ `; +} + +function renderVisibleChatCandidate(chatId) { + const meta = chatOrganizerState.visibleChats[chatId]; + if (!meta) return ""; + const avatar = meta.avatarUrl + ? `` + : `${escapeHtml(inferAvatarFallback(meta.name))}`; + + return ` +
+ ${avatar} + + ${escapeHtml(meta.name || chatId)} + ${escapeHtml(chatId)} + + +
+ `; +} + +function ensureChatOrganizerUI() { + if (!document.body || getOrganizerRoot()) return; + + if (!document.getElementById(CHAT_ORGANIZER_STYLE_ID)) { + const style = document.createElement("style"); + style.id = CHAT_ORGANIZER_STYLE_ID; + style.textContent = ` + #${CHAT_ORGANIZER_BUTTON_ID} { + position: fixed; + top: 12px; + left: 12px; + z-index: 2147483644; + border: none; + border-radius: 999px; + background: #2563eb; + color: #fff; + font-size: 13px; + font-weight: 600; + padding: 10px 14px; + cursor: pointer; + box-shadow: 0 10px 30px rgba(37, 99, 235, 0.25); + } + + #${CHAT_ORGANIZER_ROOT_ID} { + position: fixed; + inset: 0; + z-index: 2147483643; + display: none; + } + + #${CHAT_ORGANIZER_ROOT_ID}.is-open { + display: block; + } + + .teams-alias-overlay { + position: absolute; + inset: 0; + background: rgba(15, 23, 42, 0.4); + backdrop-filter: blur(3px); + } + + .teams-alias-modal { + position: absolute; + top: 64px; + left: 24px; + width: min(920px, calc(100vw - 48px)); + max-height: calc(100vh - 88px); + overflow: auto; + border-radius: 18px; + background: #ffffff; + box-shadow: 0 24px 60px rgba(15, 23, 42, 0.22); + color: #0f172a; + font-family: "Segoe UI", sans-serif; + } + + .teams-alias-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 18px 20px 12px; + border-bottom: 1px solid #e5e7eb; + } + + .teams-alias-modal-header h2 { + margin: 0; + font-size: 18px; + } + + .teams-alias-close { + border: none; + background: #e2e8f0; + color: #0f172a; + border-radius: 999px; + width: 32px; + height: 32px; + cursor: pointer; + } + + .teams-alias-modal-body { + padding: 16px 20px 20px; + display: grid; + gap: 16px; + } + + .teams-alias-panel { + border: 1px solid #e5e7eb; + border-radius: 14px; + padding: 14px; + background: #f8fafc; + } + + .teams-alias-panel h3 { + margin: 0 0 12px; + font-size: 14px; + } + + .teams-alias-row { + display: flex; + gap: 10px; + flex-wrap: wrap; + align-items: center; + } + + .teams-alias-row input, + .teams-alias-row select { + min-width: 180px; + flex: 1; + border: 1px solid #cbd5e1; + border-radius: 10px; + padding: 10px 12px; + font-size: 13px; + background: #fff; + } + + .teams-alias-row button, + .teams-alias-visible-chat button, + .teams-alias-chat-remove { + border: none; + border-radius: 10px; + padding: 9px 12px; + font-size: 13px; + cursor: pointer; + background: #2563eb; + color: #fff; + } + + .teams-alias-category-list, + .teams-alias-visible-list, + .teams-alias-chat-list { + display: grid; + gap: 10px; + } + + .teams-alias-category-item { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + border: 1px solid #dbeafe; + border-radius: 12px; + padding: 12px; + background: #fff; + } + + .teams-alias-section-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin-bottom: 12px; + } + + .teams-alias-section-header h3 { + margin: 0; + } + + .teams-alias-ghost-button, + .teams-alias-category-actions button { + border: 1px solid #cbd5e1; + border-radius: 10px; + padding: 8px 12px; + font-size: 12px; + background: #fff; + color: #0f172a; + cursor: pointer; + } + + .teams-alias-category-actions { + display: flex; + gap: 8px; + align-items: center; + } + + .teams-alias-category-meta { + display: grid; + gap: 4px; + } + + .teams-alias-category-name { + font-size: 14px; + font-weight: 600; + } + + .teams-alias-category-count, + .teams-alias-chat-id, + .teams-alias-feedback { + font-size: 12px; + color: #475569; + } + + .teams-alias-chat-card, + .teams-alias-visible-chat { + display: flex; + align-items: center; + gap: 10px; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 10px; + background: #fff; + } + + .teams-alias-chat-open { + flex: 1; + display: flex; + align-items: center; + gap: 10px; + border: none; + padding: 0; + background: transparent; + cursor: pointer; + color: inherit; + text-align: left; + } + + .teams-alias-chat-avatar { + width: 36px; + height: 36px; + border-radius: 999px; + overflow: hidden; + display: inline-flex; + align-items: center; + justify-content: center; + background: #dbeafe; + color: #1d4ed8; + font-size: 12px; + font-weight: 700; + flex-shrink: 0; + } + + .teams-alias-chat-avatar-image { + width: 100%; + height: 100%; + object-fit: cover; + } + + .teams-alias-chat-main { + min-width: 0; + display: grid; + gap: 3px; + } + + .teams-alias-chat-name { + font-size: 13px; + font-weight: 600; + color: #0f172a; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .teams-alias-empty { + color: #64748b; + font-size: 13px; + } + + .teams-alias-collapsed { + display: none; + } + + @media (max-width: 900px) { + .teams-alias-modal { + left: 12px; + right: 12px; + width: auto; + top: 56px; + } + } + `; + document.head.appendChild(style); + } + + const organizerButton = document.createElement("button"); + organizerButton.id = CHAT_ORGANIZER_BUTTON_ID; + organizerButton.textContent = "聊天分类"; + organizerButton.addEventListener("click", () => { + chatOrganizerState.isModalOpen = true; + renderChatOrganizer(); + }); + + const root = document.createElement("div"); + root.id = CHAT_ORGANIZER_ROOT_ID; + root.addEventListener("click", event => { + const actionElement = event.target.closest("[data-action]"); + if (event.target.classList.contains("teams-alias-overlay")) { + chatOrganizerState.isModalOpen = false; + renderChatOrganizer(); + return; + } + + if (!actionElement) return; + const action = actionElement.getAttribute("data-action"); + + if (action === "close-modal") { + chatOrganizerState.isModalOpen = false; + renderChatOrganizer(); + return; + } + + if (action === "create-category") { + const input = root.querySelector('[data-role="new-category-name"]'); + createCategory(input?.value || ""); + chatOrganizerState.draftCategoryName = ""; + return; + } + + if (action === "delete-category") { + deleteCategory(actionElement.getAttribute("data-category-id")); + return; + } + + if (action === "toggle-category") { + toggleCategoryExpanded(actionElement.getAttribute("data-category-id")); + return; + } + + if (action === "toggle-visible-chats") { + toggleVisibleChatsExpanded(); + return; + } + + if (action === "add-chat-by-id") { + const input = root.querySelector('[data-role="chat-id-input"]'); + addChatToCategory(chatOrganizerState.selectedCategoryId, input?.value || ""); + chatOrganizerState.draftChatId = ""; + return; + } + + if (action === "quick-add") { + addChatToCategory(chatOrganizerState.selectedCategoryId, actionElement.getAttribute("data-chat-id")); + return; + } + + if (action === "remove-chat") { + removeChatFromCategory( + actionElement.getAttribute("data-category-id"), + actionElement.getAttribute("data-chat-id") + ); + return; + } + + if (action === "open-chat") { + const chatId = actionElement.getAttribute("data-chat-id"); + const opened = openChatById(chatId); + setOrganizerFeedback(opened ? "已尝试打开聊天。" : "未找到该聊天,且 deep link 跳转失败。"); + } + }); + + root.addEventListener("change", event => { + const target = event.target; + if (target.matches('[data-role="category-select"]')) { + chatOrganizerState.selectedCategoryId = target.value; + renderChatOrganizer(); + } + }); + + root.addEventListener("input", event => { + const target = event.target; + if (target.matches('[data-role="new-category-name"]')) { + chatOrganizerState.draftCategoryName = target.value; + return; + } + if (target.matches('[data-role="chat-id-input"]')) { + chatOrganizerState.draftChatId = target.value; + } + }); + + document.body.appendChild(organizerButton); + document.body.appendChild(root); + renderChatOrganizer(); +} + +function renderChatOrganizer() { + const root = getOrganizerRoot(); + if (!root) return; + + root.classList.toggle("is-open", chatOrganizerState.isModalOpen); + if (!chatOrganizerState.isModalOpen) { + root.innerHTML = ""; + return; + } + + const categoryOptions = chatOrganizerState.categories.map(category => ` + + `).join(""); + + const categoriesMarkup = chatOrganizerState.categories.length + ? chatOrganizerState.categories.map(category => ` +
+
+ ${escapeHtml(category.name)} + ${category.chatIds.length} 个聊天 +
+
+ + +
+
+
+ ${category.chatIds.length ? category.chatIds.map(chatId => renderChatCard(chatId, category.id)).join("") : '
当前分类还没有聊天。
'} +
+ `).join("") + : '
还没有分类,先创建一个。
'; + + const visibleChatIds = Object.keys(chatOrganizerState.visibleChats); + const visibleChatsMarkup = visibleChatIds.length + ? visibleChatIds.map(chatId => renderVisibleChatCandidate(chatId)).join("") + : '
当前页面还没有识别到左侧聊天列表。
'; + + root.innerHTML = ` +
+ + `; +} async function getAlias(id) { const key = id.replace(PERSON_ID_PREFIX, ""); @@ -315,7 +1092,6 @@ function applyPeopleInCall(el) { }); } } - } // 回应表情的人员别名 @@ -373,10 +1149,18 @@ function applyToAll() { const allReaction = document.querySelectorAll(`[data-tid="diverse-reaction-user-list-item"]`); allReaction.forEach(el => applyReactionAlias(el)); + + ensureChatOrganizerUI(); + scanVisibleChats(); } // 初始化逻辑 function init() { + loadChatOrganizerState().then(() => { + ensureChatOrganizerUI(); + scanVisibleChats(); + }); + const observer = new MutationObserver(() => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => {