add classification
This commit is contained in:
786
content.js
786
content.js
@@ -1,13 +1,790 @@
|
|||||||
const PERSON_ID_PREFIX = 'chat-topic-person-';
|
const PERSON_ID_PREFIX = 'chat-topic-person-';
|
||||||
const ICON_ID_PREFIX = "presence-pill-";
|
const ICON_ID_PREFIX = "presence-pill-";
|
||||||
const CHAT_ROSTER_PREFIX = "chat-roster-item-name-";
|
const CHAT_ROSTER_PREFIX = "chat-roster-item-name-";
|
||||||
|
const CHAT_TITLE_PREFIX = "title-chat-list-item_";
|
||||||
const SUGGEST_PEOPLE_PREFIX = "AUTOSUGGEST_SUGGESTION_PEOPLE";
|
const SUGGEST_PEOPLE_PREFIX = "AUTOSUGGEST_SUGGESTION_PEOPLE";
|
||||||
const ROSTER_AVATAR_PREFIX = "roster-avatar-img-";
|
const ROSTER_AVATAR_PREFIX = "roster-avatar-img-";
|
||||||
const SERP_PEOPLE_CARD_PREFIX = "serp-people-card-content-";
|
const SERP_PEOPLE_CARD_PREFIX = "serp-people-card-content-";
|
||||||
const PEOPLE_PICKER_PREFIX = "people-picker-entry-";
|
const PEOPLE_PICKER_PREFIX = "people-picker-entry-";
|
||||||
const PEOPLE_PICKER_SEL_PREFIX = "people-picker-selected-user-";
|
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 debounceTimer = null;
|
||||||
let isMutating = false;
|
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) {
|
async function getAlias(id) {
|
||||||
const key = id.replace(PERSON_ID_PREFIX, "");
|
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"]`);
|
const allReaction = document.querySelectorAll(`[data-tid="diverse-reaction-user-list-item"]`);
|
||||||
allReaction.forEach(el => applyReactionAlias(el));
|
allReaction.forEach(el => applyReactionAlias(el));
|
||||||
|
|
||||||
|
ensureChatOrganizerUI();
|
||||||
|
scanVisibleChats();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化逻辑
|
// 初始化逻辑
|
||||||
function init() {
|
function init() {
|
||||||
|
loadChatOrganizerState().then(() => {
|
||||||
|
ensureChatOrganizerUI();
|
||||||
|
scanVisibleChats();
|
||||||
|
});
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
clearTimeout(debounceTimer);
|
clearTimeout(debounceTimer);
|
||||||
debounceTimer = setTimeout(() => {
|
debounceTimer = setTimeout(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user