update api and sync mesage when send
This commit is contained in:
parent
8241badf0c
commit
6cb5535a09
|
|
@ -12,6 +12,9 @@ dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,84 +1,4 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
|
|
||||||
export const EXTENTION_ROOT_ID = "bid-extensions";
|
|
||||||
|
|
||||||
export function extractModelId(url: string) {
|
|
||||||
switch (extractDomain(url)) {
|
|
||||||
case webs.grays: {
|
|
||||||
const match = url.match(/\/lot\/([\d-]+)\//);
|
|
||||||
return match ? match[1] : null;
|
|
||||||
}
|
|
||||||
case webs.langtons: {
|
|
||||||
const match = url.match(/auc-var-\d+/);
|
|
||||||
return match?.[0] || null;
|
|
||||||
}
|
|
||||||
case webs.lawsons: {
|
|
||||||
const match = url.split("_");
|
|
||||||
return match ? match[1] : null;
|
|
||||||
}
|
|
||||||
case webs.pickles: {
|
|
||||||
const model = url.split("/").pop();
|
|
||||||
return model ? model : null;
|
|
||||||
}
|
|
||||||
case webs.allbids: {
|
|
||||||
// eslint-disable-next-line no-useless-escape
|
|
||||||
const match = url.match(/-(\d+)(?:[\?#]|$)/);
|
|
||||||
return match ? match[1] : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extractDomain(url: string) {
|
|
||||||
try {
|
|
||||||
const parsedUrl = new URL(url);
|
|
||||||
return parsedUrl.origin;
|
|
||||||
} catch (error: any) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const webs = {
|
|
||||||
grays: "https://www.grays.com",
|
|
||||||
langtons: "https://www.langtons.com.au",
|
|
||||||
lawsons: "https://www.lawsons.com.au",
|
|
||||||
pickles: "https://www.pickles.com.au",
|
|
||||||
allbids: "https://www.allbids.com.au",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMode = (data: { metadata: any[] }) => {
|
|
||||||
return (
|
|
||||||
data.metadata.find((item) => item.key_name === "mode_key")?.value || "live"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getEarlyTrackingSeconds = (
|
|
||||||
data: { metadata: any; web_bid?: any },
|
|
||||||
outsiteMode = null
|
|
||||||
) => {
|
|
||||||
const mode = outsiteMode ? outsiteMode : getMode(data);
|
|
||||||
|
|
||||||
return (
|
|
||||||
data.metadata.find(
|
|
||||||
(item: { key_name: string }) =>
|
|
||||||
item.key_name === `early_tracking_seconds_${mode}`
|
|
||||||
)?.value || data.web_bid.early_tracking_seconds
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getArrivalOffsetSeconds = (
|
|
||||||
data: { metadata: any; web_bid?: any },
|
|
||||||
outsiteMode = null
|
|
||||||
) => {
|
|
||||||
const mode = outsiteMode ? outsiteMode : getMode(data);
|
|
||||||
|
|
||||||
return (
|
|
||||||
data.metadata.find(
|
|
||||||
(item: { key_name: string }) =>
|
|
||||||
item.key_name === `arrival_offset_seconds_${mode}`
|
|
||||||
)?.value || data.web_bid.arrival_offset_seconds
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function removeFalsyValues<T extends Record<string, any>>(
|
export function removeFalsyValues<T extends Record<string, any>>(
|
||||||
obj: T,
|
obj: T,
|
||||||
|
|
@ -95,67 +15,3 @@ export function removeFalsyValues<T extends Record<string, any>>(
|
||||||
export function delay(ms: number): Promise<void> {
|
export function delay(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSecondsFromNow(datetime: {
|
|
||||||
date: Date | undefined;
|
|
||||||
time: string;
|
|
||||||
}): number | null {
|
|
||||||
if (!datetime.date) {
|
|
||||||
return null; // Trả về null nếu date không được chọn
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tách giờ, phút, giây từ time string (HH:mm:ss)
|
|
||||||
const [hours, minutes, seconds] = datetime.time.split(":").map(Number);
|
|
||||||
|
|
||||||
// Tạo Date object mới từ date và time
|
|
||||||
const targetDate = new Date(datetime.date);
|
|
||||||
targetDate.setHours(hours, minutes, seconds, 0);
|
|
||||||
|
|
||||||
// Lấy thời điểm hiện tại
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
// Tính khoảng cách thời gian (mili giây)
|
|
||||||
const diffInMs = targetDate.getTime() - now.getTime();
|
|
||||||
|
|
||||||
// Chuyển sang giây (làm tròn xuống)
|
|
||||||
const diffInSeconds = Math.floor(diffInMs / 1000);
|
|
||||||
|
|
||||||
return diffInSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatTimeFromMinutes(minutes: number): string {
|
|
||||||
// Tính ngày, giờ, phút từ số phút
|
|
||||||
const days = Math.floor(minutes / (60 * 24));
|
|
||||||
const hours = Math.floor((minutes % (60 * 24)) / 60);
|
|
||||||
const mins = minutes % 60;
|
|
||||||
|
|
||||||
let result = "";
|
|
||||||
|
|
||||||
if (days > 0) result += `${days} ${days > 1 ? "days" : "day"} `;
|
|
||||||
if (hours > 0) result += `${hours} ${hours > 1 ? "hours" : "hour"} `;
|
|
||||||
if (mins > 0 || result === "") result += `${mins} minutes`;
|
|
||||||
|
|
||||||
return result.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDatetimeFromSeconds(seconds: number): {
|
|
||||||
date: Date | undefined;
|
|
||||||
time: string;
|
|
||||||
} {
|
|
||||||
// Lấy thời điểm hiện tại
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
// Tính thời điểm tương lai bằng cách cộng số giây vào hiện tại
|
|
||||||
const targetDate = new Date(now.getTime() + seconds * 1000);
|
|
||||||
|
|
||||||
// Lấy giờ, phút, giây và định dạng thành chuỗi "HH:mm:ss"
|
|
||||||
const hours = String(targetDate.getHours()).padStart(2, "0");
|
|
||||||
const minutes = String(targetDate.getMinutes()).padStart(2, "0");
|
|
||||||
const secondsStr = String(targetDate.getSeconds()).padStart(2, "0");
|
|
||||||
const time = `${hours}:${minutes}:${secondsStr}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
date: targetDate,
|
|
||||||
time,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,88 @@ export class ContentService {
|
||||||
return el?.click();
|
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> {
|
private async _waitToloadMessages(stableTime = 300): Promise<any> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const chatPaneList = document.getElementById("chat-pane-list");
|
const chatPaneList = document.getElementById("chat-pane-list");
|
||||||
|
|
@ -246,6 +328,7 @@ export class ContentService {
|
||||||
if (room_id != msg.data.id) {
|
if (room_id != msg.data.id) {
|
||||||
this._clickToConversation(msg.data.id);
|
this._clickToConversation(msg.data.id);
|
||||||
|
|
||||||
|
await this._waitForMessagesToAppear();
|
||||||
await this._waitToloadMessages();
|
await this._waitToloadMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -275,6 +358,27 @@ export class ContentService {
|
||||||
await delay(200);
|
await delay(200);
|
||||||
|
|
||||||
await typeingService.send(message);
|
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>);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -284,12 +388,14 @@ export class ContentService {
|
||||||
queue.add(async () => {
|
queue.add(async () => {
|
||||||
console.log("[Queue] Handling REPLY_MESSAGE");
|
console.log("[Queue] Handling REPLY_MESSAGE");
|
||||||
|
|
||||||
|
let initialMessages = null;
|
||||||
const { room_id } = this.service.getCurrentRoomInfo();
|
const { room_id } = this.service.getCurrentRoomInfo();
|
||||||
|
|
||||||
// Nếu đang active room thì không cần chờ load
|
// Nếu đang active room thì không cần chờ load
|
||||||
if (room_id != conversation_id) {
|
if (room_id != conversation_id) {
|
||||||
this._clickToConversation(conversation_id);
|
this._clickToConversation(conversation_id);
|
||||||
|
|
||||||
|
initialMessages = await this._waitForMessagesToAppear(); // Đợi có item đầu tiên
|
||||||
await this._waitToloadMessages();
|
await this._waitToloadMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -310,6 +416,27 @@ export class ContentService {
|
||||||
console.log({ message });
|
console.log({ message });
|
||||||
|
|
||||||
await typeingService.send(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>);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,7 @@ export class TeamsChatService {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _scrollToBottomByXPath(
|
public async _scrollToBottomByXPath(
|
||||||
xpath: string = this.elTags.container_scroll,
|
xpath: string = this.elTags.container_scroll,
|
||||||
options?: {
|
options?: {
|
||||||
maxStableRounds?: number; // Số vòng scroll không thay đổi trước khi dừng
|
maxStableRounds?: number; // Số vòng scroll không thay đổi trước khi dừng
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,17 @@ class TypingService {
|
||||||
): Promise<{ status: boolean; typed: string } | null> {
|
): Promise<{ status: boolean; typed: string } | null> {
|
||||||
if (!this.axios) return null;
|
if (!this.axios) return null;
|
||||||
|
|
||||||
const { data } = await this.axios({
|
// const { data } = await this.axios({
|
||||||
method: "POST",
|
// method: "POST",
|
||||||
url: "type",
|
// url: "type",
|
||||||
data: {
|
// data: {
|
||||||
message,
|
// message,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
return data;
|
// return data;
|
||||||
|
|
||||||
|
return { status: true, typed: message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,6 @@ export class Conversation {
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
|
|
||||||
@OneToMany(() => Message, (message) => message.group)
|
@OneToMany(() => Message, (message) => message.conversation)
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,5 +47,5 @@ export class Message {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
@JoinColumn({ name: 'room_id', referencedColumnName: 'id' })
|
@JoinColumn({ name: 'room_id', referencedColumnName: 'id' })
|
||||||
group: Conversation;
|
conversation: Conversation;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function delay(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
@ -1,33 +1,34 @@
|
||||||
import { Conversation } from '@/entities/conversation.entity';
|
import { Conversation } from '@/entities/conversation.entity';
|
||||||
import AppResponse from '@/system/filters/response/app-response';
|
import AppResponse from '@/system/filters/response/app-response';
|
||||||
|
import { SystemLang } from '@/system/lang/system.lang';
|
||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { paginate, PaginateQuery } from 'nestjs-paginate';
|
import { paginate, PaginateQuery } from 'nestjs-paginate';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { MessagesListener } from '../messages/messages.listener';
|
|
||||||
import { SystemLang } from '@/system/lang/system.lang';
|
|
||||||
import { Message } from '@/entities/message.entity';
|
|
||||||
import { MessagesService } from '../messages/messages.service';
|
|
||||||
import { CreateMessageDto } from '../messages/dtos/create-message.dto';
|
import { CreateMessageDto } from '../messages/dtos/create-message.dto';
|
||||||
|
import { MessagesEventService } from '../messages/messages-event.service';
|
||||||
|
import { MessagesService } from '../messages/messages.service';
|
||||||
|
import { delay } from '@/features/delay';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ConversationsService {
|
export class ConversationsService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Conversation)
|
@InjectRepository(Conversation)
|
||||||
readonly repo: Repository<Conversation>,
|
readonly repo: Repository<Conversation>,
|
||||||
private event: MessagesListener,
|
private event: MessagesEventService,
|
||||||
private messageService: MessagesService,
|
private messageService: MessagesService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async index(query: PaginateQuery) {
|
async index(query: PaginateQuery) {
|
||||||
await this.event.sendEvent(MessagesListener.EVENTS.GET_CONVERSATIONS, {});
|
await this.event.sendEvent(
|
||||||
|
MessagesEventService.EVENTS.GET_CONVERSATIONS,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await this.event.waitForEvent<Conversation[]>(
|
await this.event.waitForEvent<Conversation[]>(
|
||||||
MessagesListener.EVENTS.RECEIVE_CONVERSATIONS,
|
MessagesEventService.LOCAL_EVENTS.RECEIVE_CONVERSATIONS,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.repo.save(data);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
|
|
@ -47,13 +48,13 @@ export class ConversationsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConversation(id: Conversation['id']) {
|
async getConversation(id: Conversation['id']) {
|
||||||
await this.event.sendEvent(MessagesListener.EVENTS.GET_CONVERSATION, {
|
await this.event.sendEvent(MessagesEventService.EVENTS.GET_CONVERSATION, {
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await this.event.waitForEvent<CreateMessageDto[]>(
|
const data = await this.event.waitForEvent<CreateMessageDto[]>(
|
||||||
MessagesListener.EVENTS.RECEIVE_CONVERSATION,
|
MessagesEventService.LOCAL_EVENTS.RECEIVE_CONVERSATION,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.messageService.bulkCreate(data);
|
await this.messageService.bulkCreate(data);
|
||||||
|
|
@ -68,6 +69,9 @@ export class ConversationsService {
|
||||||
const messages = await this.messageService.repo.find({
|
const messages = await this.messageService.repo.find({
|
||||||
where: { room_id: result.id },
|
where: { room_id: result.id },
|
||||||
take: 20,
|
take: 20,
|
||||||
|
order: {
|
||||||
|
time_raw: 'DESC',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
result.messages = messages;
|
result.messages = messages;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
// user.listener.ts
|
||||||
|
import { Message } from '@/entities/message.entity';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MessagesEventService {
|
||||||
|
public static EVENTS = {
|
||||||
|
GET_CONVERSATIONS: 'messages.get-conversations',
|
||||||
|
GET_CONVERSATION: 'messages.get-conversation',
|
||||||
|
RECEIVE_CONVERSATIONS: 'messages.receive-conversations',
|
||||||
|
RECEIVE_CONVERSATION: 'messages.receive-conversation',
|
||||||
|
SEND_MESSAGE: 'messages.send-messsage',
|
||||||
|
REPLY_MESSAGE: 'messages.reply-messsage',
|
||||||
|
};
|
||||||
|
|
||||||
|
public static LOCAL_EVENTS = {
|
||||||
|
GET_CONVERSATIONS: 'local_messages.get-conversations',
|
||||||
|
GET_CONVERSATION: 'local_messages.get-conversation',
|
||||||
|
RECEIVE_CONVERSATIONS: 'local_messages.receive-conversations',
|
||||||
|
RECEIVE_CONVERSATION: 'local_messages.receive-conversation',
|
||||||
|
SEND_MESSAGE: 'local_messages.send-messsage',
|
||||||
|
REPLY_MESSAGE: 'local_messages.reply-messsage',
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private event: EventEmitter2,
|
||||||
|
@InjectRepository(Message)
|
||||||
|
readonly repo: Repository<Message>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async waitForEvent<T>(
|
||||||
|
eventName: string,
|
||||||
|
timeoutMs = 10000,
|
||||||
|
): Promise<T> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
this.event.off(eventName, listener); // cleanup
|
||||||
|
resolve(null); // hoặc reject(new Error('Timeout')) nếu muốn
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
const listener = (data: any) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
this.event.off(eventName, listener); // cleanup
|
||||||
|
resolve(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.event.on(eventName, listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendEvent<T>(eventName: string, data: T) {
|
||||||
|
return this.event.emit(eventName, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,14 +4,15 @@ import { CreateMessageDto } from './dtos/create-message.dto';
|
||||||
import { CreateBulkMessageDto } from './dtos/create-bulk-message.dto';
|
import { CreateBulkMessageDto } from './dtos/create-bulk-message.dto';
|
||||||
import { SendMessageDto } from './dtos/send-message.dto';
|
import { SendMessageDto } from './dtos/send-message.dto';
|
||||||
import { ReplyMessageDto } from './dtos/reply-message.dto';
|
import { ReplyMessageDto } from './dtos/reply-message.dto';
|
||||||
|
import { Paginate, PaginateQuery } from 'nestjs-paginate';
|
||||||
|
|
||||||
@Controller('messages')
|
@Controller('messages')
|
||||||
export class MessagesController {
|
export class MessagesController {
|
||||||
constructor(private readonly service: MessagesService) {}
|
constructor(private readonly service: MessagesService) {}
|
||||||
|
|
||||||
@Post()
|
@Get('')
|
||||||
create(@Body() dto: CreateMessageDto) {
|
index(@Paginate() query: PaginateQuery) {
|
||||||
return this.service.create(dto);
|
return this.service.index(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('send-message')
|
@Post('send-message')
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
WebSocketServer,
|
WebSocketServer,
|
||||||
} from '@nestjs/websockets';
|
} from '@nestjs/websockets';
|
||||||
import { Server, Socket } from 'socket.io';
|
import { Server, Socket } from 'socket.io';
|
||||||
import { MessagesListener } from './messages.listener';
|
import { MessagesEventService } from './messages-event.service';
|
||||||
|
|
||||||
@WebSocketGateway({
|
@WebSocketGateway({
|
||||||
cors: {
|
cors: {
|
||||||
|
|
@ -22,10 +22,10 @@ export class MessagesGateway implements OnGatewayConnection {
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
const eventsToForward = [
|
const eventsToForward = [
|
||||||
MessagesListener.EVENTS.GET_CONVERSATIONS,
|
MessagesEventService.EVENTS.GET_CONVERSATIONS,
|
||||||
MessagesListener.EVENTS.GET_CONVERSATION,
|
MessagesEventService.EVENTS.GET_CONVERSATION,
|
||||||
MessagesListener.EVENTS.SEND_MESSAGE,
|
MessagesEventService.EVENTS.SEND_MESSAGE,
|
||||||
MessagesListener.EVENTS.REPLY_MESSAGE,
|
MessagesEventService.EVENTS.REPLY_MESSAGE,
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const event of eventsToForward) {
|
for (const event of eventsToForward) {
|
||||||
|
|
@ -38,13 +38,13 @@ export class MessagesGateway implements OnGatewayConnection {
|
||||||
console.log(`📢 Client connected: ${client.id}`);
|
console.log(`📢 Client connected: ${client.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SubscribeMessage(MessagesListener.EVENTS.RECEIVE_CONVERSATIONS)
|
@SubscribeMessage(MessagesEventService.EVENTS.RECEIVE_CONVERSATIONS)
|
||||||
async handleReiveConversations(@MessageBody() data: any) {
|
async handleReiveConversations(@MessageBody() data: any) {
|
||||||
this.event.emit(MessagesListener.EVENTS.RECEIVE_CONVERSATIONS, data);
|
this.event.emit(MessagesEventService.EVENTS.RECEIVE_CONVERSATIONS, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SubscribeMessage(MessagesListener.EVENTS.RECEIVE_CONVERSATION)
|
@SubscribeMessage(MessagesEventService.EVENTS.RECEIVE_CONVERSATION)
|
||||||
async handleReiveConversation(@MessageBody() data: any) {
|
async handleReiveConversation(@MessageBody() data: any) {
|
||||||
this.event.emit(MessagesListener.EVENTS.RECEIVE_CONVERSATION, data);
|
this.event.emit(MessagesEventService.EVENTS.RECEIVE_CONVERSATION, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,43 @@
|
||||||
// user.listener.ts
|
// messages.listener.ts
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { MessagesService } from './messages.service';
|
||||||
|
import { CreateMessageDto } from './dtos/create-message.dto';
|
||||||
|
import { MessagesEventService } from './messages-event.service';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Conversation } from '@/entities/conversation.entity';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MessagesListener {
|
export class MessagesListener {
|
||||||
public static EVENTS = {
|
constructor(
|
||||||
GET_CONVERSATIONS: 'messages.get-conversations',
|
private service: MessagesService,
|
||||||
GET_CONVERSATION: 'messages.get-conversation',
|
private eventService: MessagesEventService,
|
||||||
RECEIVE_CONVERSATIONS: 'messages.receive-conversations',
|
@InjectRepository(Conversation)
|
||||||
RECEIVE_CONVERSATION: 'messages.receive-conversation',
|
readonly repoConversation: Repository<Conversation>,
|
||||||
SEND_MESSAGE: 'messages.send-messsage',
|
) {}
|
||||||
REPLY_MESSAGE: 'messages.reply-messsage',
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(private event: EventEmitter2) {}
|
// Tự động được gọi khi emit 'messages.receive-conversation'
|
||||||
|
@OnEvent(MessagesEventService.EVENTS.RECEIVE_CONVERSATION)
|
||||||
|
async handleReceiveConversation(payload: CreateMessageDto[]) {
|
||||||
|
const result = await this.service.bulkCreate(payload);
|
||||||
|
|
||||||
public async waitForEvent<T>(
|
// Send local event
|
||||||
eventName: string,
|
this.eventService.sendEvent(
|
||||||
timeoutMs = 10000,
|
MessagesEventService.LOCAL_EVENTS.RECEIVE_CONVERSATION,
|
||||||
): Promise<T> {
|
result,
|
||||||
return new Promise((resolve, reject) => {
|
);
|
||||||
const timer = setTimeout(() => {
|
|
||||||
this.event.off(eventName, listener); // cleanup
|
|
||||||
resolve(null); // hoặc reject(new Error('Timeout')) nếu muốn
|
|
||||||
}, timeoutMs);
|
|
||||||
|
|
||||||
const listener = (data: any) => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
this.event.off(eventName, listener); // cleanup
|
|
||||||
resolve(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.event.on(eventName, listener);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendEvent<T>(eventName: string, data: T) {
|
// Tự động được gọi khi emit 'messages.receive-conversations'
|
||||||
return this.event.emit(eventName, data);
|
@OnEvent(MessagesEventService.EVENTS.RECEIVE_CONVERSATIONS)
|
||||||
|
async handleReceiveConversations(data: Conversation[]) {
|
||||||
|
const result = await this.repoConversation.save(data);
|
||||||
|
|
||||||
|
// Send local event
|
||||||
|
this.eventService.sendEvent(
|
||||||
|
MessagesEventService.LOCAL_EVENTS.RECEIVE_CONVERSATIONS,
|
||||||
|
result,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,22 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Conversation } from '@/entities/conversation.entity';
|
||||||
import { MessagesService } from './messages.service';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { Message } from '@/entities/message.entity';
|
import { Message } from '@/entities/message.entity';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { MessagesEventService } from './messages-event.service';
|
||||||
import { MessagesController } from './messages.controller';
|
import { MessagesController } from './messages.controller';
|
||||||
import { MessagesGateway } from './messages.gateway';
|
import { MessagesGateway } from './messages.gateway';
|
||||||
|
import { MessagesService } from './messages.service';
|
||||||
import { MessagesListener } from './messages.listener';
|
import { MessagesListener } from './messages.listener';
|
||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
|
||||||
import { Conversation } from '@/entities/conversation.entity';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Message, Conversation])],
|
imports: [TypeOrmModule.forFeature([Message, Conversation])],
|
||||||
providers: [MessagesService, MessagesGateway, MessagesListener],
|
providers: [
|
||||||
|
MessagesService,
|
||||||
|
MessagesGateway,
|
||||||
|
MessagesEventService,
|
||||||
|
MessagesListener,
|
||||||
|
],
|
||||||
controllers: [MessagesController],
|
controllers: [MessagesController],
|
||||||
exports: [MessagesGateway, MessagesService, MessagesListener],
|
exports: [MessagesGateway, MessagesService, MessagesEventService],
|
||||||
})
|
})
|
||||||
export class MessagesModule {}
|
export class MessagesModule {}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,15 @@
|
||||||
|
import { Conversation } from '@/entities/conversation.entity';
|
||||||
import { Message } from '@/entities/message.entity';
|
import { Message } from '@/entities/message.entity';
|
||||||
import AppResponse from '@/system/filters/response/app-response';
|
import AppResponse from '@/system/filters/response/app-response';
|
||||||
import { SystemLang } from '@/system/lang/system.lang';
|
import { SystemLang } from '@/system/lang/system.lang';
|
||||||
import {
|
import { HttpStatus, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
BadRequestException,
|
|
||||||
HttpStatus,
|
|
||||||
Injectable,
|
|
||||||
NotFoundException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { In, Repository } from 'typeorm';
|
import { paginate, PaginateQuery } from 'nestjs-paginate';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
import { CreateMessageDto } from './dtos/create-message.dto';
|
import { CreateMessageDto } from './dtos/create-message.dto';
|
||||||
import { MessagesListener } from './messages.listener';
|
|
||||||
import { Conversation } from '@/entities/conversation.entity';
|
|
||||||
import { SendMessageDto } from './dtos/send-message.dto';
|
|
||||||
import { ConversationsService } from '../conversations/conversations.service';
|
|
||||||
import { ReplyMessageDto } from './dtos/reply-message.dto';
|
import { ReplyMessageDto } from './dtos/reply-message.dto';
|
||||||
|
import { SendMessageDto } from './dtos/send-message.dto';
|
||||||
|
import { MessagesEventService } from './messages-event.service';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MessagesService {
|
export class MessagesService {
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -22,9 +17,29 @@ export class MessagesService {
|
||||||
readonly repo: Repository<Message>,
|
readonly repo: Repository<Message>,
|
||||||
@InjectRepository(Conversation)
|
@InjectRepository(Conversation)
|
||||||
readonly conversationRepo: Repository<Conversation>,
|
readonly conversationRepo: Repository<Conversation>,
|
||||||
private event: MessagesListener,
|
private event: MessagesEventService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
async index(query: PaginateQuery) {
|
||||||
|
const result = await paginate(query, this.repo, {
|
||||||
|
sortableColumns: ['created_at'],
|
||||||
|
searchableColumns: ['message'],
|
||||||
|
defaultLimit: 10,
|
||||||
|
filterableColumns: {
|
||||||
|
id: true,
|
||||||
|
time: true,
|
||||||
|
message: true,
|
||||||
|
updated_at: true,
|
||||||
|
created_at: true,
|
||||||
|
'conversation.id': true,
|
||||||
|
},
|
||||||
|
maxLimit: 100,
|
||||||
|
defaultSortBy: [['time_raw', 'DESC']],
|
||||||
|
});
|
||||||
|
|
||||||
|
return AppResponse.toPagination<Message>(result, true, Message);
|
||||||
|
}
|
||||||
|
|
||||||
async create(dto: CreateMessageDto): Promise<Message> {
|
async create(dto: CreateMessageDto): Promise<Message> {
|
||||||
const time = new Date(dto.time);
|
const time = new Date(dto.time);
|
||||||
|
|
||||||
|
|
@ -111,7 +126,7 @@ export class MessagesService {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.event.sendEvent(MessagesListener.EVENTS.SEND_MESSAGE, data);
|
this.event.sendEvent(MessagesEventService.EVENTS.SEND_MESSAGE, data);
|
||||||
|
|
||||||
return AppResponse.toResponse({
|
return AppResponse.toResponse({
|
||||||
conversation,
|
conversation,
|
||||||
|
|
@ -136,7 +151,7 @@ export class MessagesService {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.event.sendEvent(MessagesListener.EVENTS.REPLY_MESSAGE, data);
|
this.event.sendEvent(MessagesEventService.EVENTS.REPLY_MESSAGE, data);
|
||||||
|
|
||||||
return AppResponse.toResponse({
|
return AppResponse.toResponse({
|
||||||
conversation,
|
conversation,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue