1212 lines
41 KiB
JavaScript
1212 lines
41 KiB
JavaScript
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, """)
|
||
.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
|
||
? `<img src="${escapeHtml(meta.avatarUrl)}" alt="" class="teams-alias-chat-avatar-image">`
|
||
: `<span class="teams-alias-chat-avatar-fallback">${escapeHtml(inferAvatarFallback(meta.name))}</span>`;
|
||
|
||
return `
|
||
<div class="teams-alias-chat-card">
|
||
<button class="teams-alias-chat-open" data-action="open-chat" data-chat-id="${escapeHtml(chatId)}">
|
||
<span class="teams-alias-chat-avatar">${avatar}</span>
|
||
<span class="teams-alias-chat-main">
|
||
<span class="teams-alias-chat-name">${escapeHtml(meta.name || chatId)}</span>
|
||
<span class="teams-alias-chat-id">${escapeHtml(chatId)}</span>
|
||
</span>
|
||
</button>
|
||
<button class="teams-alias-chat-remove" data-action="remove-chat" data-category-id="${escapeHtml(categoryId)}" data-chat-id="${escapeHtml(chatId)}">移除</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderVisibleChatCandidate(chatId) {
|
||
const meta = chatOrganizerState.visibleChats[chatId];
|
||
if (!meta) return "";
|
||
const avatar = meta.avatarUrl
|
||
? `<img src="${escapeHtml(meta.avatarUrl)}" alt="" class="teams-alias-chat-avatar-image">`
|
||
: `<span class="teams-alias-chat-avatar-fallback">${escapeHtml(inferAvatarFallback(meta.name))}</span>`;
|
||
|
||
return `
|
||
<div class="teams-alias-visible-chat">
|
||
<span class="teams-alias-chat-avatar">${avatar}</span>
|
||
<span class="teams-alias-chat-main">
|
||
<span class="teams-alias-chat-name">${escapeHtml(meta.name || chatId)}</span>
|
||
<span class="teams-alias-chat-id">${escapeHtml(chatId)}</span>
|
||
</span>
|
||
<button data-action="quick-add" data-chat-id="${escapeHtml(chatId)}">加入分类</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
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 => `
|
||
<option value="${escapeHtml(category.id)}" ${category.id === chatOrganizerState.selectedCategoryId ? "selected" : ""}>
|
||
${escapeHtml(category.name)}
|
||
</option>
|
||
`).join("");
|
||
|
||
const categoriesMarkup = chatOrganizerState.categories.length
|
||
? chatOrganizerState.categories.map(category => `
|
||
<div class="teams-alias-category-item">
|
||
<div class="teams-alias-category-meta">
|
||
<span class="teams-alias-category-name">${escapeHtml(category.name)}</span>
|
||
<span class="teams-alias-category-count">${category.chatIds.length} 个聊天</span>
|
||
</div>
|
||
<div class="teams-alias-category-actions">
|
||
<button data-action="toggle-category" data-category-id="${escapeHtml(category.id)}">${isCategoryExpanded(category.id) ? "收起" : "展开"}</button>
|
||
<button data-action="delete-category" data-category-id="${escapeHtml(category.id)}">删除分类</button>
|
||
</div>
|
||
</div>
|
||
<div class="teams-alias-chat-list ${isCategoryExpanded(category.id) ? "" : "teams-alias-collapsed"}">
|
||
${category.chatIds.length ? category.chatIds.map(chatId => renderChatCard(chatId, category.id)).join("") : '<div class="teams-alias-empty">当前分类还没有聊天。</div>'}
|
||
</div>
|
||
`).join("")
|
||
: '<div class="teams-alias-empty">还没有分类,先创建一个。</div>';
|
||
|
||
const visibleChatIds = Object.keys(chatOrganizerState.visibleChats);
|
||
const visibleChatsMarkup = visibleChatIds.length
|
||
? visibleChatIds.map(chatId => renderVisibleChatCandidate(chatId)).join("")
|
||
: '<div class="teams-alias-empty">当前页面还没有识别到左侧聊天列表。</div>';
|
||
|
||
root.innerHTML = `
|
||
<div class="teams-alias-overlay"></div>
|
||
<div class="teams-alias-modal" role="dialog" aria-modal="true" aria-label="聊天分类管理">
|
||
<div class="teams-alias-modal-header">
|
||
<h2>聊天分类管理</h2>
|
||
<button class="teams-alias-close" data-action="close-modal">×</button>
|
||
</div>
|
||
<div class="teams-alias-modal-body">
|
||
<div class="teams-alias-panel">
|
||
<h3>创建分类</h3>
|
||
<div class="teams-alias-row">
|
||
<input type="text" placeholder="例如:重点客户 / 项目群" data-role="new-category-name" value="${escapeHtml(chatOrganizerState.draftCategoryName)}">
|
||
<button data-action="create-category">创建</button>
|
||
</div>
|
||
</div>
|
||
<div class="teams-alias-panel">
|
||
<h3>加入聊天</h3>
|
||
<div class="teams-alias-row">
|
||
<select data-role="category-select">
|
||
<option value="">请选择分类</option>
|
||
${categoryOptions}
|
||
</select>
|
||
<input type="text" placeholder="输入 19: 开头的聊天 ID" data-role="chat-id-input" value="${escapeHtml(chatOrganizerState.draftChatId)}">
|
||
<button data-action="add-chat-by-id">按 ID 添加</button>
|
||
</div>
|
||
<div class="teams-alias-feedback">${escapeHtml(chatOrganizerState.feedback || "点击聊天卡片会在原 Teams 界面中打开会话。")}</div>
|
||
</div>
|
||
<div class="teams-alias-panel">
|
||
<div class="teams-alias-section-header">
|
||
<h3>当前识别到的聊天</h3>
|
||
<button class="teams-alias-ghost-button" data-action="toggle-visible-chats">${chatOrganizerState.visibleChatsExpanded ? "收起" : "展开"}</button>
|
||
</div>
|
||
<div class="teams-alias-visible-list ${chatOrganizerState.visibleChatsExpanded ? "" : "teams-alias-collapsed"}">${visibleChatsMarkup}</div>
|
||
</div>
|
||
<div class="teams-alias-panel">
|
||
<h3>分类内容</h3>
|
||
<div class="teams-alias-category-list">${categoriesMarkup}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
async function getAlias(id) {
|
||
const key = id.replace(PERSON_ID_PREFIX, "");
|
||
const result = await chrome.storage.local.get('aliases');
|
||
const aliases = result.aliases || {};
|
||
return aliases[key] || null;
|
||
}
|
||
|
||
// 设置别名显示 + 按钮添加
|
||
function applyAliasAndButton(el) {
|
||
const id = el.id;
|
||
if (!id || !id.startsWith(PERSON_ID_PREFIX)) return;
|
||
|
||
const rawId = id.replace(PERSON_ID_PREFIX, "");
|
||
|
||
const existingBtn = document.querySelector(`[data-floating-btn-for="${id}"]`);
|
||
if (!existingBtn) {
|
||
const rect = el.getBoundingClientRect();
|
||
|
||
const button = document.createElement('button');
|
||
button.textContent = '显示ID'; // 改为显示ID
|
||
button.style.position = 'fixed';
|
||
button.style.left = `${rect.left + window.scrollX}px`;
|
||
button.style.top = `${rect.bottom + window.scrollY + 20}px`;
|
||
button.style.zIndex = '99999';
|
||
button.style.padding = '4px 8px';
|
||
button.style.fontSize = '12px';
|
||
button.style.backgroundColor = '#6c757d'; // 灰色,表示只是信息查看
|
||
button.style.color = '#fff';
|
||
button.style.border = 'none';
|
||
button.style.borderRadius = '4px';
|
||
button.style.cursor = 'pointer';
|
||
button.setAttribute('data-floating-btn-for', id);
|
||
|
||
button.addEventListener('click', () => {
|
||
// 弹窗显示 ID,方便复制
|
||
prompt("该用户的账号ID为 (请复制):", rawId);
|
||
});
|
||
|
||
document.body.appendChild(button);
|
||
}
|
||
|
||
// 应用别名(异步)
|
||
getAlias(rawId).then(alias => {
|
||
if (alias && el.textContent !== alias) {
|
||
el.textContent = alias;
|
||
}
|
||
});
|
||
}
|
||
|
||
// 主要查找右侧消息列表中的名字并修改
|
||
function applyRightChatAlias(el) {
|
||
let id = el.id;
|
||
if (!id || !id.startsWith(ICON_ID_PREFIX)) return;
|
||
|
||
let parent = el;
|
||
// 向上查找 4 个父元素
|
||
for (let i = 0; i < 4; i++) {
|
||
if (parent.parentElement) {
|
||
parent = parent.parentElement;
|
||
} else {
|
||
return; // 如果不足4层,就跳过
|
||
}
|
||
}
|
||
|
||
// 获取前一个兄弟元素
|
||
const prevSibling = parent.previousElementSibling;
|
||
if (!prevSibling) return;
|
||
|
||
// 向下查找第 4 个子元素(层级式)
|
||
let target = prevSibling;
|
||
for (let i = 0; i < 4; i++) {
|
||
if (target.children.length > 0) {
|
||
target = target.children[0]; // 每层往下取第一个子元素
|
||
} else {
|
||
return; // 不足4层,跳过
|
||
}
|
||
}
|
||
// 判断符合才修改
|
||
if (target.getAttribute('data-tid') === 'message-author-name') {
|
||
getAlias(id.replace(ICON_ID_PREFIX, "")).then(alias => {
|
||
if (alias && target.textContent !== alias) {
|
||
target.textContent = alias;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 主要查找左侧消息列表中的名字并修改
|
||
function applyLeftChatAlias(el) {
|
||
let id = el.id;
|
||
if (!id || !id.startsWith(ICON_ID_PREFIX)) return;
|
||
|
||
let parent = el;
|
||
// 向上查找 4 个父元素
|
||
for (let i = 0; i < 4; i++) {
|
||
if (parent.parentElement) {
|
||
parent = parent.parentElement;
|
||
} else {
|
||
return; // 如果不足4层,就跳过
|
||
}
|
||
}
|
||
|
||
// 获取后一个兄弟元素
|
||
const nextSibling = parent.nextElementSibling;
|
||
if (!nextSibling) return;
|
||
|
||
// 向下查找第 7 个子元素(层级式)
|
||
let target = nextSibling;
|
||
for (let i = 0; i < 7; i++) {
|
||
if (target.children.length > 0) {
|
||
target = target.children[0]; // 每层往下取第一个子元素
|
||
} else {
|
||
return; // 不足7层,跳过
|
||
}
|
||
}
|
||
// 判断符合才修改
|
||
if (target.id.startsWith('title-chat-list-item')) {
|
||
getAlias(id.replace(ICON_ID_PREFIX, "")).then(alias => {
|
||
if (alias && target.textContent !== alias) {
|
||
target.textContent = alias;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 修改群组人员的名称
|
||
function applyChatRosterAlias(el) {
|
||
let id = el.id;
|
||
if (!id || !id.startsWith(CHAT_ROSTER_PREFIX)) return;
|
||
|
||
getAlias(id.replace(CHAT_ROSTER_PREFIX, "")).then(alias => {
|
||
if (alias && el.textContent !== alias) {
|
||
el.textContent = alias;
|
||
}
|
||
});
|
||
}
|
||
|
||
// 群组添加人员的别名
|
||
function applyPeoplePickerAlias(el) {
|
||
let tid = el.getAttribute('data-tid');
|
||
if (!tid || !tid.startsWith(PEOPLE_PICKER_PREFIX)) return;
|
||
|
||
let child = el.children[1];
|
||
// 向下查找第 3 个子元素(层级式)
|
||
for (let i = 0; i < 3; i++) {
|
||
if (child.children.length > 0) {
|
||
child = child.children[0]; // 每层往下取第一个子元素
|
||
} else {
|
||
return; // 不足3层,跳过
|
||
}
|
||
}
|
||
if (child.tagName.toLowerCase() === 'span') {
|
||
let id = "8:" + tid.replace(PEOPLE_PICKER_PREFIX, "");
|
||
getAlias(id).then(alias => {
|
||
if (alias && child.textContent !== alias) {
|
||
child.textContent = alias;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 群组添加人员时选中的人员
|
||
function applyPeoplePickerSelectedAlias(el) {
|
||
let tid = el.getAttribute('data-tid');
|
||
if (!tid || !tid.startsWith(PEOPLE_PICKER_SEL_PREFIX)) return;
|
||
|
||
let id = "8:" + tid.replace(PEOPLE_PICKER_SEL_PREFIX, "");
|
||
getAlias(id).then(alias => {
|
||
if (alias && el.textContent !== alias) {
|
||
el.textContent = alias;
|
||
}
|
||
});
|
||
}
|
||
|
||
// 追加搜索框中的人员别名
|
||
function applySuggestPeopleAlias(el) {
|
||
let tid = el.getAttribute('data-tid');
|
||
if (!tid || !tid.startsWith(SUGGEST_PEOPLE_PREFIX)) return;
|
||
|
||
let child = el.children[1]; // 第二个子元素
|
||
// 向下查找第 2 个子元素(层级式)
|
||
for (let i = 0; i < 2; i++) {
|
||
if (child.children.length > 0) {
|
||
child = child.children[0]; // 每层往下取第一个子元素
|
||
} else {
|
||
return; // 不足2层,跳过
|
||
}
|
||
}
|
||
if (child.getAttribute('data-tid') !== 'AUTOSUGGEST_SUGGESTION_TITLE') return;
|
||
|
||
let id = tid.replace(SUGGEST_PEOPLE_PREFIX, "");
|
||
getAlias(id).then(alias => {
|
||
if (alias) {
|
||
let lastSpan = child.lastElementChild;
|
||
if (lastSpan.id.startsWith("suggest-alias-attached")) {
|
||
if (lastSpan.textContent === `[${alias}]`) return;
|
||
lastSpan.textContent = `[${alias}]`;
|
||
} else {
|
||
const span = document.createElement('span');
|
||
span.id = `suggest-alias-attached-${id}`;
|
||
span.textContent = `[${alias}]`;
|
||
span.style.marginLeft = '4px';
|
||
span.style.color = document.documentElement.classList.contains("theme-tfl-default") ? '#ed0833' : '#78ef0b';
|
||
child.appendChild(span);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 追加人员搜索中的别名
|
||
function applySerpPeopleAlias(el) {
|
||
let id = el.id;
|
||
if (!id || !id.startsWith(SERP_PEOPLE_CARD_PREFIX)) return;
|
||
|
||
let child = el.children[2];
|
||
// 向下查找第 4 个子元素(层级式)
|
||
for (let i = 0; i < 4; i++) {
|
||
if (child.children.length > 0) {
|
||
child = child.children[0]; // 每层往下取第一个子元素
|
||
} else {
|
||
return; // 不足4层,跳过
|
||
}
|
||
}
|
||
|
||
id = id.replace(SERP_PEOPLE_CARD_PREFIX, "");
|
||
getAlias(id).then(alias => {
|
||
if (alias) {
|
||
let lastSpan = child.lastElementChild;
|
||
if (lastSpan.id.startsWith("people-card-attached")) {
|
||
if (lastSpan.textContent === `[${alias}]`) return;
|
||
lastSpan.textContent = `[${alias}]`;
|
||
} else {
|
||
const span = document.createElement('span');
|
||
span.id = `people-card-attached-${id}`;
|
||
span.textContent = `[${alias}]`;
|
||
span.style.marginLeft = '4px';
|
||
span.style.color = document.documentElement.classList.contains("theme-tfl-default") ? '#ed0833' : '#78ef0b';
|
||
child.appendChild(span);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 修改通话中的人名
|
||
function applyCallingAlias(el) {
|
||
if (el.getAttribute('data-cid') !== 'calling-participant-stream') return;
|
||
|
||
let child = el.children[1];
|
||
// 向下查找第 5 个子元素(层级式)
|
||
for (let i = 0; i < 5; i++) {
|
||
if (child.children.length > 0) {
|
||
child = child.children[0]; // 每层往下取第一个子元素
|
||
} else {
|
||
return; // 不足5层,跳过
|
||
}
|
||
}
|
||
if (child.tagName.toLowerCase() === 'span') {
|
||
getAlias(el.getAttribute('data-acc-element-id')).then(alias => {
|
||
if (alias && child.textContent !== alias) {
|
||
child.textContent = alias;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 修改通话右侧人名的别名
|
||
function applyRosterAvatarAlias(el) {
|
||
let id = el.id;
|
||
if (!id || !id.startsWith(ROSTER_AVATAR_PREFIX)) return;
|
||
|
||
getAlias(id.replace(ROSTER_AVATAR_PREFIX, "")).then(alias => {
|
||
if (alias && el.textContent !== alias) {
|
||
el.textContent = alias;
|
||
}
|
||
});
|
||
}
|
||
|
||
// 查看此通话中的人员
|
||
function applyPeopleInCall(el) {
|
||
let id = el.id;
|
||
if (!id || !id.startsWith(ICON_ID_PREFIX)) return;
|
||
|
||
let parent = el;
|
||
// 向上查找 2 个父元素
|
||
for (let i = 0; i < 2; i++) {
|
||
if (parent.parentElement) {
|
||
parent = parent.parentElement;
|
||
} else {
|
||
return; // 如果不足2层,就跳过
|
||
}
|
||
}
|
||
// 一个是查看此通话中的参与者,一个是呼叫其他人加入
|
||
if ([
|
||
"audio_dropin_add_participants_dialog_renderer",
|
||
"audio-drop-in-live-roster"
|
||
].includes(parent.getAttribute("data-tid"))) {
|
||
let target = parent.nextElementSibling;
|
||
if (!target) return;
|
||
if (target.tagName.toLowerCase() === "span") {
|
||
getAlias(id.replace(ICON_ID_PREFIX, "")).then(alias => {
|
||
if (alias && target.textContent !== alias) {
|
||
target.textContent = alias;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// 回应表情的人员别名
|
||
function applyReactionAlias(el) {
|
||
if (el.getAttribute('data-tid') !== 'diverse-reaction-user-list-item') return;
|
||
try {
|
||
const tabster = JSON.parse(el.getAttribute("data-tabster"));
|
||
|
||
let child = el.children[1];
|
||
let target = child.children[0];
|
||
let id = tabster.observed.names[0];
|
||
getAlias(id).then(alias => {
|
||
if (alias && target.textContent !== alias) {
|
||
target.textContent = alias;
|
||
}
|
||
});
|
||
|
||
} catch (error) {}
|
||
}
|
||
|
||
// 查找所有目标元素应用别名和按钮
|
||
function applyToAll() {
|
||
document.querySelectorAll('[data-floating-btn-for]').forEach(btn => btn.remove());
|
||
|
||
const allPersons = document.querySelectorAll(`[id^="${PERSON_ID_PREFIX}"]`);
|
||
allPersons.forEach(el => applyAliasAndButton(el));
|
||
|
||
const allIcons = document.querySelectorAll(`[id^="${ICON_ID_PREFIX}"]`);
|
||
allIcons.forEach(el => {
|
||
applyRightChatAlias(el);
|
||
applyLeftChatAlias(el);
|
||
applyPeopleInCall(el);
|
||
});
|
||
|
||
const allChatRoster = document.querySelectorAll(`[id^="${CHAT_ROSTER_PREFIX}"]`);
|
||
allChatRoster.forEach(el => applyChatRosterAlias(el));
|
||
|
||
const allSuggestPeople = document.querySelectorAll(`[data-tid^="${SUGGEST_PEOPLE_PREFIX}"]`);
|
||
allSuggestPeople.forEach(el => applySuggestPeopleAlias(el));
|
||
|
||
const allCalling = document.querySelectorAll(`[data-cid="calling-participant-stream"]`);
|
||
allCalling.forEach(el => applyCallingAlias(el));
|
||
|
||
const allRosterAvatar = document.querySelectorAll(`[id^="${ROSTER_AVATAR_PREFIX}"]`);
|
||
allRosterAvatar.forEach(el => applyRosterAvatarAlias(el));
|
||
|
||
const allSerpPeople = document.querySelectorAll(`[id^="${SERP_PEOPLE_CARD_PREFIX}"]`);
|
||
allSerpPeople.forEach(el => applySerpPeopleAlias(el));
|
||
|
||
const allPeoplePicker = document.querySelectorAll(`[data-tid^="${PEOPLE_PICKER_PREFIX}"]`);
|
||
allPeoplePicker.forEach(el => applyPeoplePickerAlias(el));
|
||
|
||
const allPeoplePickerSelected = document.querySelectorAll(`[data-tid^="${PEOPLE_PICKER_SEL_PREFIX}"]`);
|
||
allPeoplePickerSelected.forEach(el => applyPeoplePickerSelectedAlias(el));
|
||
|
||
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(() => {
|
||
if (isMutating) return; // 🧠 防止自己触发自己
|
||
isMutating = true;
|
||
|
||
applyToAll(); // 页面内容变动后重新应用
|
||
|
||
// 给浏览器一点时间完成 DOM 更新后再允许 observer 响应
|
||
setTimeout(() => {
|
||
isMutating = false;
|
||
}, 500); // 至少比这个高才行,不然会一直触发
|
||
}, 300);
|
||
});
|
||
|
||
observer.observe(document.body, {
|
||
childList: true,
|
||
subtree: true,
|
||
});
|
||
|
||
// 自动同步检查
|
||
chrome.storage.local.get('lastSync').then(({ lastSync }) => {
|
||
const now = Date.now();
|
||
// 24 小时 = 86400000 ms
|
||
if (!lastSync || (now - lastSync > 86400000)) {
|
||
console.log("Teams Alias: 正在进行后台同步...");
|
||
if (typeof fetchAliasesFromDB === 'function') {
|
||
fetchAliasesFromDB().then(aliases => {
|
||
chrome.storage.local.set({ aliases, lastSync: now });
|
||
console.log("Teams Alias: 自动同步成功");
|
||
}).catch(err => {
|
||
console.error("Teams Alias: 自动同步失败", err);
|
||
});
|
||
} else {
|
||
console.warn("Teams Alias: fetchAliasesFromDB 未定义,无法同步。");
|
||
}
|
||
}
|
||
});
|
||
|
||
applyToAll(); // 初始执行
|
||
|
||
// 兜底:每 2 秒再扫一次(避免漏掉异步更新)
|
||
// setInterval(() => {
|
||
// applyToAll();
|
||
// }, 2000);
|
||
}
|
||
|
||
init();
|