465 lines
13 KiB
TypeScript
465 lines
13 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
import { queue } from "@/lib/queue";
|
|
import { TeamsChatService } from "./teams-chat.service";
|
|
import { EVENTS } from "@/lib/event";
|
|
import { typeingService } from "./typing.service";
|
|
import { delay } from "@/features/app";
|
|
|
|
export class ContentService {
|
|
service: TeamsChatService;
|
|
port;
|
|
constructor(port: any) {
|
|
this.service = new TeamsChatService();
|
|
this.port = port;
|
|
}
|
|
_forceHeightObserver: any;
|
|
|
|
getElementByXPath(xpath: string) {
|
|
return document.evaluate(
|
|
xpath,
|
|
document,
|
|
null,
|
|
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
null
|
|
).singleNodeValue;
|
|
}
|
|
|
|
forceHeight(el: HTMLElement, height = "100px") {
|
|
if (!el) return;
|
|
|
|
el.style.setProperty("height", height, "important");
|
|
|
|
// Gỡ bỏ auto-resize (nếu là textarea)
|
|
el.style.setProperty("resize", "none", "important");
|
|
|
|
// Lặp lại khi element bị focus/input
|
|
const keepHeight = () => {
|
|
el.style.setProperty("height", height, "important");
|
|
};
|
|
|
|
el.addEventListener("focus", keepHeight);
|
|
el.addEventListener("input", keepHeight);
|
|
el.addEventListener("blur", keepHeight);
|
|
|
|
// Thêm mutation observer nếu bị script khác chỉnh sửa
|
|
const observer = new MutationObserver(() => {
|
|
el.style.setProperty("height", height, "important");
|
|
});
|
|
|
|
observer.observe(el, {
|
|
attributes: true,
|
|
attributeFilter: ["style"],
|
|
});
|
|
|
|
// Lưu lại để debug nếu cần
|
|
this._forceHeightObserver = observer;
|
|
}
|
|
|
|
private _clickToConversation(id: string) {
|
|
const el = document.getElementById(`chat-list-item_${id}`);
|
|
|
|
if (el) {
|
|
el.scrollIntoView({
|
|
behavior: "smooth", // hoặc "auto"
|
|
block: "center", // cuộn sao cho phần tử nằm giữa view
|
|
});
|
|
|
|
// Đợi 1 chút cho scroll xong (nếu cần)
|
|
setTimeout(() => el.click(), 200); // delay 200ms là đủ mượt
|
|
}
|
|
return el?.click();
|
|
}
|
|
|
|
private async _waitForMessagesToAppear(
|
|
timeoutMs = 10000
|
|
): Promise<HTMLElement[]> {
|
|
return new Promise((resolve, reject) => {
|
|
const chatPaneList = document.getElementById("chat-pane-list");
|
|
let timeout: any = null;
|
|
if (!chatPaneList) {
|
|
return reject(new Error("#chat-pane-list not found"));
|
|
}
|
|
|
|
const getChildren = () =>
|
|
Array.from(chatPaneList.children) as HTMLElement[];
|
|
|
|
const checkAndResolve = () => {
|
|
const children = getChildren();
|
|
if (children.length > 0) {
|
|
observer.disconnect();
|
|
if (timeout) clearTimeout(timeout);
|
|
resolve(children);
|
|
}
|
|
};
|
|
|
|
const observer = new MutationObserver(() => {
|
|
checkAndResolve();
|
|
});
|
|
|
|
observer.observe(chatPaneList, {
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
|
|
// Kiểm tra ngay lập tức nếu đã có sẵn item
|
|
checkAndResolve();
|
|
|
|
timeout = setTimeout(() => {
|
|
observer.disconnect();
|
|
reject(new Error("Timeout waiting for chat messages to appear"));
|
|
}, timeoutMs);
|
|
});
|
|
}
|
|
|
|
private async _waitForNewMessages(
|
|
existingItems: HTMLElement[],
|
|
timeoutMs = 10000
|
|
): Promise<HTMLElement[]> {
|
|
return new Promise((resolve, reject) => {
|
|
const chatPaneList = document.getElementById("chat-pane-list");
|
|
|
|
if (!chatPaneList) {
|
|
return reject(new Error("#chat-pane-list not found"));
|
|
}
|
|
|
|
const getChildren = () =>
|
|
Array.from(chatPaneList.children) as HTMLElement[];
|
|
|
|
const existingSet = new Set(existingItems);
|
|
|
|
let timeoutHandle: any = null;
|
|
|
|
const observer = new MutationObserver(() => {
|
|
const currentChildren = getChildren();
|
|
const newItems = currentChildren.filter((el) => !existingSet.has(el));
|
|
|
|
if (newItems.length > 0) {
|
|
observer.disconnect();
|
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
resolve(newItems);
|
|
}
|
|
});
|
|
|
|
observer.observe(chatPaneList, {
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
|
|
timeoutHandle = setTimeout(() => {
|
|
observer.disconnect();
|
|
reject(new Error("Timeout waiting for new messages"));
|
|
}, timeoutMs);
|
|
});
|
|
}
|
|
|
|
private async _waitToloadMessages(stableTime = 300): Promise<any> {
|
|
return new Promise((resolve) => {
|
|
const chatPaneList = document.getElementById("chat-pane-list");
|
|
|
|
if (!chatPaneList) {
|
|
throw new Error("#chat-pane-list not found");
|
|
}
|
|
|
|
let timeout: NodeJS.Timeout | null = null;
|
|
let lastChildren: HTMLElement[] = [];
|
|
|
|
const observer = new MutationObserver(() => {
|
|
// Reset lại mỗi khi có thay đổi
|
|
if (timeout) clearTimeout(timeout);
|
|
|
|
// Cập nhật danh sách item mới nhất
|
|
lastChildren = Array.from(chatPaneList.children) as HTMLElement[];
|
|
|
|
timeout = setTimeout(() => {
|
|
observer.disconnect(); // Khi không có thay đổi trong khoảng stableTime
|
|
resolve(lastChildren);
|
|
}, stableTime);
|
|
});
|
|
|
|
// Quan sát cả subtree nếu có nested thay đổi
|
|
observer.observe(chatPaneList, {
|
|
childList: true,
|
|
subtree: true,
|
|
attributes: true,
|
|
characterData: true,
|
|
});
|
|
});
|
|
}
|
|
|
|
private _getTypeGeo() {
|
|
const el = document.querySelector(".ck-placeholder");
|
|
|
|
if (el) {
|
|
const rect = el.getBoundingClientRect();
|
|
|
|
return {
|
|
top: rect.top,
|
|
left: rect.left,
|
|
right: rect.right,
|
|
bottom: rect.bottom,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private async _rightClickMessage(date_time: number) {
|
|
const xpath = this.service.elTags.container_chat;
|
|
const itemId = `content-${date_time}`;
|
|
|
|
const findElementByXPath = (xpath: string): HTMLElement | null => {
|
|
const result = document.evaluate(
|
|
xpath,
|
|
document,
|
|
null,
|
|
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
null
|
|
);
|
|
return result.singleNodeValue as HTMLElement | null;
|
|
};
|
|
|
|
const maxScrollAttempts = 30;
|
|
const scrollStep = 200;
|
|
let attempts = 0;
|
|
|
|
let wrapper = findElementByXPath(xpath);
|
|
if (!wrapper) {
|
|
console.error("Wrapper not found:", xpath);
|
|
return;
|
|
}
|
|
|
|
let element = wrapper.querySelector<HTMLElement>(`#${itemId}`);
|
|
|
|
while (!element && attempts < maxScrollAttempts) {
|
|
const scrollContainer = wrapper;
|
|
const prevScrollTop = scrollContainer.scrollTop;
|
|
|
|
scrollContainer.scrollTop = Math.max(
|
|
0,
|
|
scrollContainer.scrollTop - scrollStep
|
|
);
|
|
await delay(200); // Đợi scroll và DOM render lại
|
|
|
|
wrapper = findElementByXPath(xpath);
|
|
if (!wrapper) break;
|
|
|
|
element = wrapper.querySelector<HTMLElement>(`#${itemId}`);
|
|
|
|
if (scrollContainer.scrollTop === prevScrollTop) break;
|
|
|
|
attempts++;
|
|
}
|
|
|
|
if (!element) {
|
|
console.error("Không tìm thấy phần tử:", itemId);
|
|
return;
|
|
}
|
|
|
|
// Scroll phần tử vào giữa màn hình
|
|
element.scrollIntoView({ behavior: "auto", block: "center" });
|
|
await delay(100); // Đợi scroll
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
const event = new MouseEvent("contextmenu", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
view: window,
|
|
button: 2,
|
|
buttons: 2,
|
|
clientX: rect.left + rect.width / 2,
|
|
clientY: rect.top + rect.height / 2,
|
|
});
|
|
|
|
element.dispatchEvent(event);
|
|
}
|
|
|
|
private _clickIfExists(xpath: string): Promise<boolean> {
|
|
return new Promise((resolve) => {
|
|
const result = document.evaluate(
|
|
xpath,
|
|
document,
|
|
null,
|
|
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
null
|
|
);
|
|
|
|
const element = result.singleNodeValue as HTMLElement | null;
|
|
|
|
if (element) {
|
|
element.scrollIntoView({ behavior: "auto", block: "center" });
|
|
setTimeout(() => {
|
|
element.click();
|
|
resolve(true);
|
|
}, 100); // Đợi scroll một chút rồi click
|
|
} else {
|
|
console.warn("Không tìm thấy phần tử:", xpath);
|
|
resolve(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
async getConversations(_: IMsg<any>) {
|
|
queue.add(async () => {
|
|
console.log("[Queue] Handling GET_CONVERSATIONS");
|
|
|
|
const data = await this.service.handleGetConversations();
|
|
|
|
// Gửi dữ liệu ngược về background
|
|
this.port.postMessage({
|
|
type: "socket-response",
|
|
event: EVENTS.RECEIVE_CONVERSATIONS,
|
|
data,
|
|
} as IMsg<any>);
|
|
});
|
|
}
|
|
|
|
async getConversation(msg: IMsg<{ id: string }>) {
|
|
queue.add(async () => {
|
|
console.log("[Queue] Handling GET_CONVERSATION");
|
|
|
|
if (!msg.data?.id) return;
|
|
|
|
const { room_id } = this.service.getCurrentRoomInfo();
|
|
|
|
// Nếu đang active room thì không cần chờ load
|
|
if (room_id != msg.data.id) {
|
|
this._clickToConversation(msg.data.id);
|
|
|
|
await this._waitForMessagesToAppear();
|
|
await this._waitToloadMessages();
|
|
}
|
|
|
|
const data = this.service.extractAllMessages();
|
|
|
|
this.port.postMessage({
|
|
type: "socket-response",
|
|
event: EVENTS.RECEIVE_CONVERSATION,
|
|
data,
|
|
} as IMsg<any>);
|
|
});
|
|
}
|
|
|
|
async sendMessage({
|
|
data: { conversation_id, message },
|
|
}: IMsg<{ conversation_id: string; message: string }>) {
|
|
queue.add(async () => {
|
|
console.log("[Queue] Handling SEND_MESSAGE");
|
|
|
|
const { room_id } = this.service.getCurrentRoomInfo();
|
|
|
|
// Nếu đang active room thì không cần chờ load
|
|
if (room_id != conversation_id) {
|
|
this._clickToConversation(conversation_id);
|
|
}
|
|
|
|
await delay(200);
|
|
|
|
await typeingService.send(message);
|
|
|
|
// // Sroll xuống
|
|
// this.service._scrollToBottomByXPath();
|
|
|
|
// const initialMessages = await this._waitForMessagesToAppear(3000); // Đợi có item đầu tiên
|
|
// console.log("Initial messages:", initialMessages);
|
|
|
|
// const newMessages = await this._waitForNewMessages(initialMessages, 3000);
|
|
// console.log("New messages appeared:", newMessages);
|
|
|
|
// await this._waitToloadMessages();
|
|
|
|
// // Lấy hết message mới nhất
|
|
// const data = this.service.extractAllMessages();
|
|
|
|
// // Gửi lên server cập nhật
|
|
// this.port.postMessage({
|
|
// type: "socket-response",
|
|
// event: EVENTS.RECEIVE_CONVERSATION,
|
|
// data,
|
|
// } as IMsg<any>);
|
|
});
|
|
}
|
|
|
|
async replyMessage({
|
|
data: { conversation_id, message, time },
|
|
}: IMsg<{ conversation_id: string; message: string; time: number }>) {
|
|
queue.add(async () => {
|
|
console.log("[Queue] Handling REPLY_MESSAGE");
|
|
|
|
let initialMessages = null;
|
|
const { room_id } = this.service.getCurrentRoomInfo();
|
|
|
|
// Nếu đang active room thì không cần chờ load
|
|
if (room_id != conversation_id) {
|
|
this._clickToConversation(conversation_id);
|
|
|
|
initialMessages = await this._waitForMessagesToAppear(); // Đợi có item đầu tiên
|
|
await this._waitToloadMessages();
|
|
}
|
|
|
|
this._clickIfExists(this.service.elTags.close_reply_btn);
|
|
|
|
await this._rightClickMessage(time);
|
|
|
|
const replyBtn: any = document.querySelector(
|
|
this.service.elTags.reply_btn
|
|
);
|
|
|
|
if (replyBtn) {
|
|
replyBtn.click();
|
|
}
|
|
|
|
await delay(200);
|
|
|
|
console.log({ message });
|
|
|
|
await typeingService.send(message);
|
|
|
|
// if (!initialMessages) return;
|
|
|
|
// // Sroll xuống
|
|
// this.service._scrollToBottomByXPath();
|
|
|
|
// // Theo dỗi lấy detech new message
|
|
// const newMessages = await this._waitForNewMessages(initialMessages, 3000);
|
|
// console.log("New messages appeared:", newMessages);
|
|
|
|
// await this._waitToloadMessages();
|
|
|
|
// // Lấy hết message mới nhất
|
|
// const data = this.service.extractAllMessages();
|
|
|
|
// // Gửi lên server cập nhật
|
|
// this.port.postMessage({
|
|
// type: "socket-response",
|
|
// event: EVENTS.RECEIVE_CONVERSATION,
|
|
// data,
|
|
// } as IMsg<any>);
|
|
});
|
|
}
|
|
|
|
fixedHeightChatInput(retry = 20, interval = 1000) {
|
|
const tryFind = () => {
|
|
const el = this.getElementByXPath(
|
|
this.service.elTags.chat_input
|
|
) as HTMLElement;
|
|
|
|
if (el) {
|
|
this.forceHeight(el, "100px");
|
|
console.log("✔ Fixed height applied to chat input");
|
|
} else if (retry > 0) {
|
|
setTimeout(
|
|
() => this.fixedHeightChatInput(retry - 1, interval),
|
|
interval
|
|
);
|
|
} else {
|
|
console.warn("✘ Element not found with provided XPath after retries");
|
|
}
|
|
};
|
|
|
|
tryFind();
|
|
}
|
|
}
|