merge chat

This commit is contained in:
Peter Maquiran
2024-09-13 12:27:39 +01:00
471 changed files with 17419 additions and 53900 deletions
+31
View File
@@ -0,0 +1,31 @@
{
"compileOnSave": false,
"compilerOptions": {
"strict": true,
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"skipLibCheck": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "es2020",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2017",
//"target": "es5",
"lib": [
"es2018",
"dom"
],
"inlineSources": true,
"sourceRoot": "/"
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true
},
"include": [
"./**/*.ts" // Include all TypeScript files in the current directory and subdirectories
]
}
+192
View File
@@ -0,0 +1,192 @@
import { NgModule } from '@angular/core';
import { SignalRService } from 'src/app/infra/socket/signalR/signal-r.service';
import { ChatServiceService } from 'src/app/module/chat/domain/chat-service.service'
import { skip, switchMap } from 'rxjs/operators';
import { SessionStore } from 'src/app/store/session.service';
import { Subject, timer } from 'rxjs';
import { UserTypingLocalRepository } from './data/repository/typing/user-typing-local-data-source.service';
import { UserTypingRemoteRepositoryService } from './data/repository/typing/user-typing-live-data-source.service';
import { RoomService } from 'src/app/module/chat/domain/service/room.service'
import { HttpListenToMessageLoadHistoryAdapter } from './domain/adapter';
import { ISignalRService } from 'src/app/infra/socket/adapter';
import { HttpModule } from 'src/app/infra/http/http.module';
import { HttpListenToMessageLoadHistoryUseCase } from 'src/app/core/chat/usecase/message/http-listen-to-message-load-history-by-roomId-use-case';
import { IMessageLocalRepository } from 'src/app/core/chat/repository/message/message-local-repository';
import { MessageLocalDataSourceService } from './data/repository/message/message-local-data-source.service';
import { MessageRemoteDataSourceService } from './data/repository/message/message-remote-data-source.service';
import { IMessageRemoteRepository } from 'src/app/core/chat/repository/message/message-remote-repository';
import { IMessageSocketRepository } from 'src/app/core/chat/repository/message/message-socket-repository';
import { MessageSocketRepositoryService } from './data/repository/message/message-live-signalr-data-source.service';
import { MemberListLocalRepository } from './data/repository/member/member-list-local-repository.service';
import { IMemberLocalRepository } from 'src/app/core/chat/repository/member/member-local-repository';
import { MemberListRemoteRepository } from './data/repository/member/member-list-remote-repository.service';
import { IMemberRemoteRepository } from 'src/app/core/chat/repository/member/member-remote-repository';
import { IRoomLocalRepository } from 'src/app/core/chat/repository/room/room-local-repository';
import { RoomLocalRepository } from './data/repository/room/room-local-repository.service';
import { RoomRemoteDataSourceService } from './data/repository/room/room-remote-repository.service';
import { IRoomRemoteRepository } from 'src/app/core/chat/repository/room/room-remote-repository';
import { RoomSocketRepositoryService } from './data/repository/room/room-socket-repository.service';
import { IRoomSocketRepository } from 'src/app/core/chat/repository/room/room-socket-repository';
import { IAttachmentLocalRepository } from 'src/app/core/chat/repository/typing/typing-local-repository';
import { AttachmentLocalDataSource } from './data/repository/attachment/attachment-local-repository.service';
import { IAttachmentRemoteRepository } from 'src/app/core/chat/repository/attachment/attachment-remote-repository';
import { AttachmentRemoteDataSourceService } from './data/repository/attachment/attachment-remote-repository.service';
import { IDistributionLocalRepository } from 'src/app/core/chat/repository/distribution/distribution-local-repository';
import { DistributionLocalRepository } from './data/repository/destribution/destribution-local-repository';
import { DistributionService } from './domain/service/distribution.service'
import { BoldLocalRepository } from './data/repository/bold/bold-local-repository';
import { IBoldLocalRepository } from 'src/app/core/chat/repository/bold/bold-local-repository';
import { RoomLastMessageService } from 'src/app/module/chat/domain/service/room-last-message.service'
import { IUserPhotoLocalRepository } from 'src/app/core/chat/repository/user-photo/user-photo-local-repository';
import { UserPhotoLocalRepository } from './data/repository/user-foto/user-photo-local-repository.service';
import { IUserPhotoRemoteRepository } from 'src/app/core/chat/repository/user-photo/user-photo-remote-repository';
import { UserPhotoRemoteRepositoryService } from './data/repository/user-foto/user-photo-remote-repository.service';
@NgModule({
imports: [HttpModule],
providers: [
{
provide: ISignalRService,
useClass: SignalRService, // or MockDataService
},
{
provide: HttpListenToMessageLoadHistoryAdapter,
useClass: HttpListenToMessageLoadHistoryUseCase, // or MockDataService
},
// message repository
{
provide: IMessageLocalRepository,
useClass: MessageLocalDataSourceService
},
{
provide: IMessageRemoteRepository,
useClass: MessageRemoteDataSourceService
},
{
provide: IMessageSocketRepository,
useClass: MessageSocketRepositoryService
},
// member repository
{
provide: IMemberLocalRepository,
useClass: MemberListLocalRepository
},
{
provide: IMemberRemoteRepository,
useClass: MemberListRemoteRepository
},
// room repository
{
provide: IRoomLocalRepository,
useClass: RoomLocalRepository
},
{
provide: IRoomRemoteRepository,
useClass: RoomRemoteDataSourceService
},
{
provide: IRoomSocketRepository,
useClass: RoomSocketRepositoryService
},
// attachment
{
provide: IAttachmentLocalRepository,
useClass: AttachmentLocalDataSource
},
{
provide: IAttachmentRemoteRepository,
useClass: AttachmentRemoteDataSourceService
},
//
{
provide: IDistributionLocalRepository,
useClass: DistributionLocalRepository
},
{
provide: IBoldLocalRepository,
useClass: BoldLocalRepository
},
// user-photo
{
provide: IUserPhotoLocalRepository,
useClass: UserPhotoLocalRepository
},
//
{
provide: IUserPhotoRemoteRepository,
useClass: UserPhotoRemoteRepositoryService
}
],
declarations: [],
schemas: [],
entryComponents: []
})
export class ChatModule {
typingCallback: {[key: string]: Subject<any> } = {}
constructor(
private SignalRService: SignalRService,
private ChatServiceService: ChatServiceService,
private signalR: SignalRService,
private localDataSource: UserTypingLocalRepository,
private UserTypingRemoteRepositoryService: UserTypingRemoteRepositoryService,
private RoomService: RoomService,
private DistributionService: DistributionService,
private RoomLastMessageService: RoomLastMessageService
) {
this.RoomService.init()
this.syncMessage()
this.listenToTyping()
}
async listenToTyping() {
this.UserTypingRemoteRepositoryService.listenToTyping().subscribe(async(e) => {
// this.memoryDataSource.dispatch(removeUserTyping({data: {...e} as any}))
// this.memoryDataSource.dispatch(addUserTyping({data: {...e} as any}))
//
const value = await this.localDataSource.addUserTyping(e);
const id = e.roomId + '@' + e.userName
if(!this.typingCallback[id]) {
this.typingCallback[id] = new Subject()
this.typingCallback[id].pipe(
switchMap(() => timer(2000)),
).subscribe(() => {
// console.log('111111==============')
// this.memoryDataSource.dispatch(removeUserTyping({data: {...e} as any}))
this.localDataSource.removeUserTyping(e)
})
} else {
this.typingCallback[id].next()
}
})
}
async syncMessage() {
const connection = this.SignalRService.getConnectionState()
connection.pipe(
skip(1) // Skip the first value
).subscribe((value: boolean)=> {
if(value) {
// on reconnect
this.ChatServiceService.chatSync();
}
});
connection.subscribe((value: boolean) => {
if(value) {
// on connect
this.ChatServiceService.sendLocalMessages()
}
})
// on page reload sync
if(!(!SessionStore.user.Inactivity || !SessionStore.exist)) {
this.ChatServiceService.start();
}
}
}
@@ -0,0 +1,29 @@
import { DistributionTable } from "src/app/infra/database/dexie/instance/chat/schema/destribution";
import { DistributionOutPutDTO } from "src/app/module/chat/domain/service/distribution.service";
export function distributionListDetermineChanges(____serverDistributions: DistributionOutPutDTO[], localDistributions: DistributionTable[], messageId: string) {
const serverDistribution = ____serverDistributions.map( e=> {
return {
...e,
$messageIdMemberId: messageId + e.memberId
}
})
const serverDistributionMap = new Map(serverDistribution.map(distribution => [distribution.$messageIdMemberId, distribution]));
const localDistributionMap = new Map(localDistributions.map(distribution => [distribution.$messageIdMemberId, distribution]));
const distributionToInsert = serverDistribution.filter(distribution => !localDistributionMap.has(distribution.$messageIdMemberId));
const distributionToUpdate = serverDistribution.filter(distribution => {
const localDistribution = localDistributionMap.get(distribution.$messageIdMemberId);
return localDistribution && (
distribution.deliverAt !== localDistribution.deliverAt ||
distribution.readAt !== localDistribution.readAt
)
});
const distributionToDelete = localDistributions.filter(distribution => !serverDistributionMap.has(distribution.$messageIdMemberId));
return { distributionToInsert, distributionToUpdate, distributionToDelete };
}
@@ -0,0 +1,40 @@
import { MessageTable } from "src/app/infra/database/dexie/instance/chat/schema/message";
export function messageListDetermineChanges(serverList: MessageTable[], localList: MessageTable[]) {
localList = localList.filter(e => e.id)
// Convert lists to dictionaries for easier comparison
const localDict = localList.reduce((acc, item) => {
acc[item.id] = item;
return acc;
}, {});
const serverDict = serverList.reduce((acc, item) => {
acc[item.id] = item;
return acc;
}, {});
// Identify added and deleted items
const addedItems = serverList.filter(item => !localDict[item.id]);
const deletedItems = localList.filter(item => !serverDict[item.id]);
// Identify changed items
const changedItems = serverList.filter(item => {
const localItem = localDict[item.id];
if(localItem?.$id) {
item.$id = localItem.$id
}
return localItem && (item.editedAt !== localItem.editedAt || item.sentAt != item.sentAt || item.reactions.some((r, index) => {
const localReaction = localItem.reactions[index];
return !localReaction || r.reactedAt !== localReaction.reactedAt;
}));
});
return {
addedItems,
deletedItems,
changedItems
}
}
@@ -0,0 +1,33 @@
import { RoomTable } from "src/app/infra/database/dexie/instance/chat/schema/room";
import { RoomByIdOutputDTO } from "src/app/module/chat/domain/use-case/room/room-get-by-id-use-case.service";
import { RoomListItemOutPutDTO } from "src/app/module/chat/domain/use-case/room/room-get-list-use-case.service";
export function roomByIdDetermineChanges(serverResponse: RoomByIdOutputDTO, localRooms: RoomTable[]) {
const localRoomMap = new Map(localRooms.map(room => [room.id, room]));
let roomsToInsert:RoomByIdOutputDTO | undefined;
let roomsToUpdate: RoomByIdOutputDTO | undefined;
const fond = localRooms.filter(room => !localRoomMap.has(serverResponse.data.id));
if(!fond) {
roomsToInsert = serverResponse
}
const needToUpdate = localRooms.filter(room => {
const localRoom = localRoomMap.get(room.id);
return localRoom && (
room.roomName !== serverResponse.data.roomName ||
room.createdBy.wxUserId !== serverResponse.data.createdBy.wxUserId ||
room.createdAt !== serverResponse.data.createdAt ||
room.expirationDate !== serverResponse.data.expirationDate // ||
// room.chatRoom.roomType !== localRoom.roomType
);
});
if(needToUpdate) {
roomsToUpdate = serverResponse
}
return { roomsToInsert, roomsToUpdate };
}
@@ -0,0 +1,25 @@
import { RoomType } from "src/app/core/chat/entity/group";
import { RoomTable } from "src/app/infra/database/dexie/instance/chat/schema/room";
import { RoomListItemOutPutDTO } from "src/app/module/chat/domain/use-case/room/room-get-list-use-case.service";
export function roomListDetermineChanges(serverRooms: RoomListItemOutPutDTO[], localRooms: RoomTable[]) {
const serverRoomMap = new Map(serverRooms.map(room => [room.chatRoom.id, room]));
const localRoomMap = new Map(localRooms.map(room => [room.id, room]));
const roomsToInsert = serverRooms.filter(room => !localRoomMap.has(room.chatRoom.id));
const roomsToUpdate = serverRooms.filter(room => {
const localRoom = localRoomMap.get(room.chatRoom.id);
return localRoom && (
room.chatRoom.roomName !== localRoom.roomName && room.chatRoom.roomType == RoomType.Group ||
room.chatRoom.createdBy.wxUserId !== localRoom.createdBy.wxUserId ||
room.chatRoom.createdAt !== localRoom.createdAt ||
room.chatRoom.expirationDate !== localRoom.expirationDate // ||
// room.chatRoom.messages?.[0]?.id !== localRoom.messages?.[0]?.id
// room.chatRoom.roomType !== localRoom.roomType
);
});
const roomsToDelete = localRooms.filter(room => !serverRoomMap.has(room.id));
return { roomsToInsert, roomsToUpdate, roomsToDelete };
}
@@ -0,0 +1,31 @@
import { MemberTable } from "src/app/infra/database/dexie/instance/chat/schema/members";
import { RoomByIdMemberItemOutputDTO } from "src/app/module/chat/domain/use-case/room/room-get-by-id-use-case.service";
export function roomMemberListDetermineChanges(____serverRooms: RoomByIdMemberItemOutputDTO[], localRooms: MemberTable[], roomId: string) {
const PServerRooms: (RoomByIdMemberItemOutputDTO & {$roomIdUserId: string})[] = ____serverRooms.map( e=> {
return {
...e,
$roomIdUserId: roomId + e.user.wxUserId
}
})
const serverRoomMap = new Map(PServerRooms.map(room => [room.$roomIdUserId, room]));
const localRoomMap = new Map(localRooms.map(room => [room.$roomIdUserId, room]));
const membersToInsert = PServerRooms.filter(room => !localRoomMap.has(room.$roomIdUserId));
const membersToUpdate = PServerRooms.filter(room => {
const localRoom = localRoomMap.get(room.$roomIdUserId);
return localRoom && (
room.user.wxUserId !== localRoom.wxUserId ||
room.user.userPhoto !== localRoom.userPhoto ||
room.joinAt !== localRoom.joinAt,
room.isAdmin !== localRoom.isAdmin
)
});
const membersToDelete = localRooms.filter(room => !serverRoomMap.has(room.$roomIdUserId));
return { membersToInsert, membersToUpdate, membersToDelete };
}
@@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { IAttachmentLocalRepository } from 'src/app/core/chat/repository/typing/typing-local-repository';
import { AttachmentTable, AttachmentTableSchema } from 'src/app/infra/database/dexie/instance/chat/schema/attachment';
import { chatDatabase } from 'src/app/infra/database/dexie/service';
import { DexieRepository } from 'src/app/infra/repository/dexie/dexie-repository.service';
@Injectable({
providedIn: 'root'
})
export class AttachmentLocalDataSource extends DexieRepository<AttachmentTable, AttachmentTable> implements IAttachmentLocalRepository {
messageSubject = new Subject();
constructor() {
super(chatDatabase.attachment, AttachmentTableSchema)
}
}
@@ -0,0 +1,20 @@
import { Injectable, Input } from '@angular/core';
import { IAttachmentRemoteRepository } from 'src/app/core/chat/repository/attachment/attachment-remote-repository';
import { HttpService } from 'src/app/services/http.service';
import { DataSourceReturn } from 'src/app/services/Repositorys/type';
@Injectable({
providedIn: 'root'
})
export class AttachmentRemoteDataSourceService implements IAttachmentRemoteRepository {
private baseUrl = 'https://gdapi-dev.dyndns.info/stage/api/v2/Chat'; // Your base URL
constructor(
private httpService: HttpService
) { }
async getAttachment(id: string | number): DataSourceReturn<Blob> {
return await this.httpService.get(`${this.baseUrl}/attachment/${id}`, { responseType: 'blob' });
}
}
@@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { from } from "rxjs";
import { DexieRepository } from "src/app/infra/repository/dexie/dexie-repository.service";
import { Dexie, EntityTable, liveQuery, Observable } from 'Dexie';
import { BoldTable, BoldTableSchema } from "src/app/infra/database/dexie/instance/chat/schema/bold";
import { chatDatabase } from "src/app/infra/database/dexie/service";
import { IBoldLocalRepository } from "src/app/core/chat/repository/bold/bold-local-repository";
@Injectable({
providedIn: 'root'
})
export class BoldLocalRepository extends DexieRepository<BoldTable, BoldTable> implements IBoldLocalRepository {
constructor() {
super(chatDatabase.bold, BoldTableSchema)
}
open() {
return chatDatabase.open()
}
listen() {
return from(liveQuery(() => chatDatabase.bold.toArray()))
}
}
@@ -0,0 +1,14 @@
import { DistributionTable, DistributionTableSchema } from "src/app/infra/database/dexie/instance/chat/schema/destribution";
import { chatDatabase } from "src/app/infra/database/dexie/service";
import { DexieRepository } from "src/app/infra/repository/dexie/dexie-repository.service";
export class DistributionLocalRepository extends DexieRepository<DistributionTable, DistributionTable> {
constructor() {
super(chatDatabase.distribution, DistributionTableSchema)
chatDatabase.distribution.hook("creating", function (primKey, obj, transaction) {
obj.$messageIdMemberId = `${obj.messageId}${obj.memberId}`
});
}
}
@@ -0,0 +1,114 @@
import { Injectable } from '@angular/core';
import { Dexie, EntityTable, liveQuery, Observable } from 'Dexie';
import { z } from 'zod';
import { DexieRepository } from 'src/app/infra/repository/dexie/dexie-repository.service';
import { ok } from 'neverthrow';
import { err, Result } from 'neverthrow';
import { MemberListUPdateStatusInputDTO } from '../../../domain/use-case/socket/member-list-update-status-use-case.service';
import { from } from 'rxjs';
import { MemberTable, MemberTableSchema } from 'src/app/infra/database/dexie/instance/chat/schema/members';
import { chatDatabase } from 'src/app/infra/database/dexie/service';
import { IDirectMemberInput, IGetMemberLive, IMemberLocalRepository } from 'src/app/core/chat/repository/member/member-local-repository';
@Injectable({
providedIn: 'root'
})
export class MemberListLocalRepository extends DexieRepository<MemberTable, MemberTable> implements IMemberLocalRepository {
constructor() {
super(chatDatabase.members, MemberTableSchema)
}
async directMember({roomId, currentUserId}:IDirectMemberInput) {
try {
let a = await chatDatabase.members.where('roomId')
.equals(roomId)
.and(message => message.wxUserId !== currentUserId)
.first()
return ok(a as MemberTable)
} catch (e) {
return err(e)
}
}
async addMember(data: MemberTable) {
data.$roomIdUserId = data.roomId + data.wxUserId
return this.insert(data)
}
async updateMemberRole(data: MemberTable) {
try {
const result = await chatDatabase.members.where({
wxUserId: data.wxUserId,
roomId: data.roomId,
}).modify(data);
return ok(result)
} catch (e) {
return err(false)
}
}
async updateMembersStatus(data: MemberListUPdateStatusInputDTO): Promise<Result<true, any>> {
try {
await chatDatabase.members.toCollection().modify({ status: 'offline' });
for (const item of data) {
const wxUserId = item.value.userId; // Extract wxUserId
await chatDatabase.members.where('wxUserId').equals(wxUserId).modify({ status: 'online' });
}
return ok(true)
} catch (error) {
console.error("Error updating user statuses:", error);
return err(error)
}
}
allMemberOnline(roomId:string) {
return liveQuery(async () => {
const allMessages = await chatDatabase.members
.where('roomId')
.equals(roomId)
.toArray();
return allMessages.every(message => message?.status === "online");
});
}
async removeMemberFromRoom($roomIdUserId): Promise<Result<any ,any>> {
try {
const member = await chatDatabase.members.where({ $roomIdUserId: $roomIdUserId }).first();
if (member) {
const result = ok(await chatDatabase.members.delete($roomIdUserId));
return result
} else {
return err('not Found')
}
} catch (e) {
return err(false)
}
}
getMemberLive(data: IGetMemberLive) {
const $roomIdUserId = data.roomId + data.wxUserId
return liveQuery(() => chatDatabase.members.get($roomIdUserId));
}
async getRoomMemberById(roomId: any) {
return await chatDatabase.members.where({roomId}).toArray()
}
getRoomMemberByIdLive(roomId: any) {
return from (liveQuery(() => chatDatabase.members.where({roomId}).toArray()))
}
getRoomMemberNoneAdminByIdLive(roomId: any) {
return liveQuery(async() => {
const members = await chatDatabase.members.where({roomId}).toArray()
return members.filter(e => e.isAdmin != true)
})
}
}
@@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { Result } from 'neverthrow';
import { ValidateSchema } from 'src/app/services/decorators/validate-schema.decorator';
import { HttpService } from 'src/app/services/http.service';
import { DataSourceReturn } from 'src/app/services/Repositorys/type';
import { IMemberRemoteRepository } from 'src/app/core/chat/repository/member/member-remote-repository';
import { AddMemberToRoomInputDTO, AddMemberToRoomInputDTOSchema } from '../../../domain/use-case/member/member-add-use-case.service';
import { MemberSetAdminDTO } from '../../../domain/use-case/member/member-admin-use-case.service';
import { UserRemoveListInputDTOSchema, UserRemoveListInputDTO } from '../../../domain/use-case/room/room-leave-by-id-use-case.service';
@Injectable({
providedIn: 'root'
})
export class MemberListRemoteRepository implements IMemberRemoteRepository {
private baseUrl = 'https://gdapi-dev.dyndns.info/stage/api/v2/Chat'; // Your base URL
constructor(private httpService: HttpService) { }
@ValidateSchema(AddMemberToRoomInputDTOSchema)
async addMemberToRoom(data: AddMemberToRoomInputDTO): DataSourceReturn<AddMemberToRoomInputDTO> {
return await this.httpService.post<any>(`${this.baseUrl}/Room/${data.id}/Member`, { members:data.members });
}
@ValidateSchema(UserRemoveListInputDTOSchema)
async removeMemberFromRoom(data: UserRemoveListInputDTO): Promise<Result<any ,any>> {
return await this.httpService.delete<any>(`${this.baseUrl}/Room/${data.id}/Member`, {members:data.members});
}
async setAmin(data: MemberSetAdminDTO): Promise<Result<any ,any>> {
return await this.httpService.patch<any>(`${this.baseUrl}/Room/${data.roomId}/Member/${data.memberId}/admin`);
}
}
@@ -0,0 +1,171 @@
import { Injectable } from '@angular/core';
import { v4 as uuidv4 } from 'uuid'
import { InstanceId } from '../../../domain/chat-service.service';
import { MessageUpdateInput } from '../../../domain/use-case/message/message-update-by-id-use-case.service';
import { MessageReactionInput } from '../../../domain/use-case/message/message-reaction-by-id-use-case.service';
import { SignalRService } from 'src/app/infra/socket/signalR/signal-r.service';
import { filter, map } from 'rxjs/operators';
import { SocketMessage } from 'src/app/infra/socket/signalR/signalR';
import { IMessageSocketRepository } from 'src/app/core/chat/repository/message/message-socket-repository';
import { MessageCreateOutPutDataDTO, MessageInputDTO } from '../../../domain/use-case/message/message-create-use-case.service';
import { MessageMarkAsReadInput } from '../../../domain/use-case/message/message-mark-as-read-use-case.service';
import { MessageOutPutDataDTO } from 'src/app/core/chat/repository/dto/messageOutputDTO';
import { MessageDeleteInputDTO } from '../../../domain/use-case/message/message-delete-by-id-live-use-case.service';
import { BehaviorSubject, Observable } from 'rxjs';
interface sendDeliverAt {
memberId: number,
messageId:string,
roomId: string,
requestId: string
}
@Injectable({
providedIn: 'root'
})
export class MessageSocketRepositoryService implements IMessageSocketRepository {
private sendDataSubject: BehaviorSubject<{method: string, data: any}> = new BehaviorSubject<{method: string, data: any}>(null);
constructor(
private socket: SignalRService
) {}
connect() {
return this.socket.establishConnection();
}
async join() {
return await this.socket.join()
}
async sendGroupMessage(data: MessageInputDTO) {
if(!data.requestId) {
data.requestId = InstanceId +'@'+ uuidv4();
}
const result = await this.socket.sendData<MessageCreateOutPutDataDTO>({
method: 'sendMessage',
data: data,
})
this.sendDataSubject.next({
method: 'sendMessage',
data: data,
})
return result;
}
async sendDirectMessage(data: MessageInputDTO) {
if(!data.requestId) {
data.requestId = InstanceId +'@'+ uuidv4();
}
const result = await this.socket.sendData<MessageOutPutDataDTO>({
method: 'SendDirectMessage',
data: data as any,
})
this.sendDataSubject.next({
method: 'SendDirectMessage',
data: data,
})
return result;
}
async sendDeliverAt(data: sendDeliverAt) {
const result = await this.socket.sendData<any>({
method: 'DeliverAt',
data: data as any,
})
return result;
}
async sendReadAt(data: MessageMarkAsReadInput) {
const result = await this.socket.sendData<any>({
method: 'ReadAt',
data: data as any,
})
return result;
}
async sendDelete(data: MessageDeleteInputDTO) {
const result = await this.socket.sendData<any>({
method: 'ReadAt',
data: data,
})
return result;
}
listenToMessages() {
return this.socket.getData().pipe(
filter((e) : e is SocketMessage<MessageOutPutDataDTO>=> e?.method == 'ReceiveMessage'
),
map((e)=> e.data)
)
}
listenToDeleteMessages() {
return this.socket.getData().pipe(
filter((e) : e is SocketMessage<MessageOutPutDataDTO>=> e?.method == 'DeleteMessage'
),
map((e)=> e.data)
)
}
listenToUpdateMessages() {
return this.socket.getData().pipe(
filter((e) : e is SocketMessage<MessageOutPutDataDTO>=> e?.method == 'UpdateMessage'
),
map((e)=> e.data)
)
}
listenToSendMessage() {
return this.sendDataSubject.pipe(
filter((e) : e is SocketMessage<MessageInputDTO>=> {
console.log(e, e?.method == 'sendMessage' || e?.method == 'SendDirectMessage')
return e?.method == 'sendMessage' || e?.method == 'SendDirectMessage'
}
))
}
reactToMessageSocket(data: MessageReactionInput) {
this.socket.sendData({
method: 'ReactMessage',
data
})
}
updateMessage(input: MessageUpdateInput) {
this.socket.sendData({
method: 'EditMessage',
data: input,
})
}
sendMessageDelete(data: MessageDeleteInputDTO) {
data['requestId'] = InstanceId +'@'+ uuidv4();
const result = this.socket.sendData<any>({
method: 'DeleteMessage',
data: data,
})
return result;
}
}
@@ -0,0 +1,118 @@
import { Injectable } from '@angular/core';
import { liveQuery } from 'Dexie';
import { MessageEntity } from '../../../../../core/chat/entity/message';
import { DexieRepository } from 'src/app/infra/repository/dexie/dexie-repository.service';
import { Observable as DexieObservable, PromiseExtended } from 'Dexie';
import { DexieMessageTable, MessageTable, MessageTableSchema } from 'src/app/infra/database/dexie/instance/chat/schema/message';
import { chatDatabase } from 'src/app/infra/database/dexie/service';
import { IMessageLocalRepository } from 'src/app/core/chat/repository/message/message-local-repository';
import { BehaviorSubject, combineLatest, from, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { v4 as uuidv4 } from 'uuid'
@Injectable({
providedIn: 'root'
})
export class MessageLocalDataSourceService extends DexieRepository<MessageTable, MessageEntity, DexieMessageTable> implements IMessageLocalRepository {
private creatingSubject : BehaviorSubject<MessageTable> = new BehaviorSubject<MessageTable>(null);
private lastTimestamp = 0;
constructor() {
super(chatDatabase.message, MessageTableSchema, chatDatabase)
this.setAllSenderToFalse();
this.onCreatingHook()
}
private onCreatingHook() {
chatDatabase.message.hook('creating', (primaryKey, obj, transaction) => {
let now = Date.now();
// If the current time is the same as the last, increment
if (now <= this.lastTimestamp) {
obj.$createAt = this.lastTimestamp + 1;
this.lastTimestamp = this.lastTimestamp + 1;
} else {
this.lastTimestamp = now;
obj.$createAt = now;
}
if(obj.id) {
obj.$id = obj.id
} else {
obj.$id = 'Local-'+uuidv4()
}
this.creatingSubject.next(obj)
});
}
onCreateObservable() {
return this.creatingSubject.asObservable()
}
async setAllSenderToFalse() {
// this.createTransaction(async (table) => {
// const result = await this.find({sending: true })
// if(result.isOk()) {
// for(const message of result.value) {
// await this.update(message.$id, { sending: false })
// }
// }
// })
try {
await chatDatabase.transaction('rw', chatDatabase.message, async () => {
// Perform the update operation within the transaction
await chatDatabase.message.toCollection().modify({ sending: false });
});
// console.log('All messages updated successfully.');
} catch (error) {
console.error('Error updating messages:', error);
}
}
getItems(roomId: string): PromiseExtended<MessageEntity[]> {
return chatDatabase.message.where('roomId').equals(roomId).sortBy('sentAt') as any
}
getItemsLive(roomId: string): DexieObservable<MessageEntity[]> {
return liveQuery(() => chatDatabase.message.where('roomId').equals(roomId).sortBy('sentAt') as any)
}
async getOfflineMessages () {
try {
const allMessages = await chatDatabase.message
.filter(msg => typeof msg.id !== 'string' && msg.sending == false)
.toArray();
return allMessages as MessageEntity[];
} catch (error) {
console.error('Error fetching messages:', error);
}
}
getLastMessageForRooms(roomIds: string[]): Observable<any[]> {
const observables = roomIds.map(roomId =>
from (liveQuery(async() =>{
const messages = await chatDatabase.message
.where('roomId')
.equals(roomId)
.reverse()
.sortBy('timestamp')
return messages[0] || null; // Return the first item (latest message) or null if no message
})).pipe(
map((message) => ({ roomId, message: message || null })) // Attach roomId to the result
)
);
return combineLatest(observables); // Combine all observables into one array of results
}
}
@@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { HttpService } from 'src/app/services/http.service';
import { DataSourceReturn } from 'src/app/services/Repositorys/type';
import { SignalRService } from 'src/app/infra/socket/signalR/signal-r.service';
import { HttpAdapter } from 'src/app/infra/http/adapter';
import { IMessageGetAllByRoomIdOutPut } from 'src/app/core/chat/usecase/message/message-get-all-by-room-Id';
import { IMessageRemoteRepository } from 'src/app/core/chat/repository/message/message-remote-repository';
@Injectable({
providedIn: 'root'
})
export class MessageRemoteDataSourceService implements IMessageRemoteRepository {
private baseUrl = 'https://gdapi-dev.dyndns.info/stage/api/v2/Chat'; // Your base URL
constructor(
private httpService: HttpService,
private socket: SignalRService,
private http: HttpAdapter
) {}
// @APIReturn(MessageOutPutDTOSchema, 'get/Messages')
async getMessagesFromRoom(id: string): DataSourceReturn<IMessageGetAllByRoomIdOutPut> {
var a = await this.http.get<IMessageGetAllByRoomIdOutPut>(`${this.baseUrl}/Room/${id}/Messages`)
return a.map((e) => {
return e.data
})
// return await this.httpService.get(`${this.baseUrl}/Room/${id}/Messages`);
}
}
@@ -0,0 +1,39 @@
import { Injectable } from '@angular/core';
import { liveQuery, Observable } from 'Dexie';
import { DexieRepository } from 'src/app/infra/repository/dexie/dexie-repository.service';
import { from } from 'rxjs';
import { RoomTable, RoomTableSchema } from 'src/app/infra/database/dexie/instance/chat/schema/room';
import { chatDatabase } from 'src/app/infra/database/dexie/service';
import { IRoomLocalRepository } from 'src/app/core/chat/repository/room/room-local-repository';
@Injectable({
providedIn: 'root'
})
export class RoomLocalRepository extends DexieRepository<RoomTable, RoomTable> implements IRoomLocalRepository {
constructor() {
super(chatDatabase.room, RoomTableSchema)
chatDatabase.room.hook('updating', (modifications, primKey, oldValue, transaction) => {
// if((modifications as Partial<RoomTable>).messages?.[0].requestId == oldValue.messages?.[0].requestId) {
// (modifications as Partial<RoomTable>).messages[0].sentAt = oldValue.messages?.[0]?.sentAt;
// } else if ((modifications as Partial<RoomTable>).messages?.[0].id == oldValue.messages?.[0].id) {
// (modifications as Partial<RoomTable>).messages[0].sentAt = oldValue.messages?.[0]?.sentAt
// }
return modifications
});
}
getItemsLive() {
return from (liveQuery(() => chatDatabase.room.toArray()));
}
getRoomByIdLive(id: any) {
return from(liveQuery(() => chatDatabase.room.get(id)));
}
}
@@ -0,0 +1,85 @@
import { Injectable } from '@angular/core';
import { Result } from 'neverthrow';
import { HttpService } from 'src/app/services/http.service';
import { AddMemberToRoomInputDTO } from '../../../domain/use-case/member/member-add-use-case.service';
import { DataSourceReturn } from 'src/app/services/Repositorys/type';
import { SessionStore } from 'src/app/store/session.service';
import { SignalRService } from 'src/app/infra/socket/signalR/signal-r.service';
import { v4 as uuidv4 } from 'uuid'
import { CreateRoomInputDTO, RoomOutPutDTO } from '../../../domain/use-case/room/room-create-use-case.service';
import { IRoomRemoteRepository } from 'src/app/core/chat/repository/room/room-remote-repository';
import { RoomByIdOutputDTO } from 'src/app/module/chat/domain/use-case/room/room-get-by-id-use-case.service';
import { RoomUpdateInputDTO, RoomUpdateOutputDTO } from 'src/app/module/chat/domain/use-case/room/room-update-by-id-use-case.service';
import { RoomListOutPutDTO } from '../../../domain/use-case/room/room-get-list-use-case.service';
import { z } from 'zod';
import { HttpAdapter } from 'src/app/infra/http/adapter';
const RoomByIdInputDTOSchema = z.string()
type RoomByIdInputDTO = z.infer<typeof RoomByIdInputDTOSchema>
@Injectable({
providedIn: 'root'
})
export class RoomRemoteDataSourceService implements IRoomRemoteRepository {
private baseUrl = 'https://gdapi-dev.dyndns.info/stage/api/v2/Chat'; // Your base URL
constructor(
private httpService: HttpService,
private socket: SignalRService,
private Http: HttpAdapter
) {}
//@ValidateSchema(CreateRoomInputDTOSchema)
//@APIReturn(RoomOutPutDTOSchema, 'post/Room')
async createRoom(data: CreateRoomInputDTO): DataSourceReturn<RoomOutPutDTO> {
return await this.httpService.post<RoomOutPutDTO>(`${this.baseUrl}/Room`, data);
}
//@APIReturn(RoomListOutPutDTOSchema, 'get/Room')
async getRoomList(): Promise<DataSourceReturn<RoomListOutPutDTO>> {
const result = await this.Http.get<RoomListOutPutDTO>(`${this.baseUrl}/Room?userId=${SessionStore.user.UserId}`);
return result.map((e)=> e.data)
}
//@ValidateSchema(RoomByIdInputDTOSchema)
//@APIReturn(RoomByIdOutputDTOSchema,'get/Room/${id}')
async getRoom(id: RoomByIdInputDTO): DataSourceReturn<RoomByIdOutputDTO> {
const result = await this.Http.get(`${this.baseUrl}/Room/${id}`);
return result.map((e)=> e.data)
}
//@ValidateSchema(RoomUpdateInputDTOSchema)
//@APIReturn(RoomByIdOutputDTOSchema,'update/Room/${id}')
async updateRoom(data: RoomUpdateInputDTO): Promise<DataSourceReturn<RoomUpdateOutputDTO>> {
const id = data.roomId
delete data.roomId
return await this.httpService.put<any>(`${this.baseUrl}/Room/${id}`, data);
}
async deleteRoom(id: string): Promise<Result<any ,any>> {
return await this.httpService.delete<any>(`${this.baseUrl}/Room/${id}`, {});
}
async addMemberToRoomSocket(data: AddMemberToRoomInputDTO) {
const result = await this.socket.sendData({
method: 'AddRoomMember',
data: {
requestId: uuidv4(),
roomId: data.id,
members: data.members
}
})
console.log({result})
return result
}
}
@@ -0,0 +1,65 @@
import { Injectable } from '@angular/core';
import { SignalRService } from 'src/app/infra/socket/signalR/signal-r.service';
import { filter, map } from 'rxjs/operators';
import { z } from 'zod';
import { SocketMessage } from 'src/app/infra/socket/signalR/signalR';
import { v4 as uuidv4 } from 'uuid'
import { IRoomSocketRepository } from 'src/app/core/chat/repository/room/room-socket-repository';
import { CreateRoomInputDTO } from '../../../domain/use-case/room/room-create-use-case.service';
const listenToDeleteRoomInputSchema = z.object({
roomId: z.string()
})
export const RoomSocketOutPutDTOSchema = z.object({
id: z.string(),
roomName: z.string(),
createdBy: z.any().nullable(),
createdAt: z.string(),
expirationDate: z.string().nullable(),
roomType: z.any()
});
export type RoomSocketOutPutDTO = z.infer<typeof RoomSocketOutPutDTOSchema>
export type ListenToDeleteRoomInput = z.infer<typeof listenToDeleteRoomInputSchema>
@Injectable({
providedIn: 'root'
})
export class RoomSocketRepositoryService implements IRoomSocketRepository {
constructor(
private socket: SignalRService
) { }
async CreateGroup(data: CreateRoomInputDTO) {
const result = await this.socket.sendData<RoomSocketOutPutDTO>({
method: 'CreateGroup',
data: {
...data,
requestId: uuidv4()
} as any,
})
return result;
}
listenToCreateRoom() {
return this.socket.getData().pipe(
filter((data) => data?.method == 'UserAddGroup')
)
}
listenToDeleteRoom() {
return this.socket.getData<any>().pipe(
filter((data): data is SocketMessage<ListenToDeleteRoomInput> =>
data?.method === 'UserRemoveGroup'
),
map((e) => e.data)
);
}
}
@@ -0,0 +1,51 @@
import { Injectable } from '@angular/core';
import { SignalRService } from 'src/app/infra/socket/signalR/signal-r.service';
import { SessionStore } from 'src/app/store/session.service';
import { filter, map } from 'rxjs/operators';
import { SocketMessage } from 'src/app/infra/socket/signalR/signalR';
import { ITypingRemoteRepository } from 'src/app/core/chat/repository/typing/typing-remote-repository';
import { z } from "zod"
import { InstanceId } from '../../../domain/chat-service.service';
import { v4 as uuidv4 } from 'uuid'
export const UserTypingDTOSchema = z.object({
requestId: z.string(),
roomId: z.string(),
userId: z.number(),
userName: z.string()
})
export type UserTypingDTO = z.infer<typeof UserTypingDTOSchema>
@Injectable({
providedIn: 'root'
})
export class UserTypingRemoteRepositoryService implements ITypingRemoteRepository {
constructor(
private socket: SignalRService
) { }
sendTyping(roomId: string) {
return this.socket.sendData({
method: 'Typing',
data: {
roomId,
UserName:SessionStore.user.FullName,
userId:SessionStore.user.UserId,
requestId: InstanceId +'@'+ uuidv4(),
},
})
}
listenToTyping() {
return this.socket.getData().pipe(
filter((e) : e is SocketMessage<UserTypingDTO>=> e?.method == 'TypingMessage'
),
map((e)=> e.data)
)
}
}
@@ -0,0 +1,66 @@
import { Injectable } from '@angular/core';
import { z } from 'zod';
import { Dexie, EntityTable, liveQuery, Observable } from 'Dexie';
import { err, ok } from 'neverthrow';
import { chatDatabase } from 'src/app/infra/database/dexie/service';
import { TypingTable, TypingTableSchema } from 'src/app/infra/database/dexie/instance/chat/schema/typing';
import { ITypingLocalRepository } from 'src/app/core/chat/repository/attachment/attachment-local-repository';
import { DexieRepository } from 'src/app/infra/repository/dexie/dexie-repository.service';
@Injectable({
providedIn: 'root'
})
export class UserTypingLocalRepository extends DexieRepository<TypingTable, TypingTable> implements ITypingLocalRepository {
constructor() {
super(chatDatabase.typing, TypingTableSchema)
this.clear();
}
async clear() {
try {
const result = await chatDatabase.typing.clear()
return ok(result)
} catch (e) {
return err(false)
}
}
async addUserTyping(data: TypingTable) {
data.id = data.roomId + '@' + data.userName
try {
const result = await chatDatabase.typing.add(data)
return ok(result)
} catch (e) {
return err(false)
}
}
async removeUserTyping(data: TypingTable) {
const id = data.roomId + '@' + data.userName
try {
const result = await chatDatabase.typing.delete(id)
return ok(result)
} catch (e) {
return err(false)
}
}
getUserTypingLive() {
return liveQuery(() => chatDatabase.typing.toArray());
}
getUserTypingLiveByRoomId(roomId: string) {
return liveQuery(() => chatDatabase.typing
.where('roomId')
.equals(roomId)
.sortBy('id')
);
}
}
@@ -0,0 +1,15 @@
import { Injectable } from '@angular/core';
import Dexie, { PromiseExtended } from 'Dexie';
import { IUserPhotoLocalRepository } from 'src/app/core/chat/repository/user-photo/user-photo-local-repository';
import { UserPhotoTable, UserPhotoTableSchema } from 'src/app/infra/database/dexie/instance/chat/schema/user-foto';
import { chatDatabase } from 'src/app/infra/database/dexie/service';
import { DexieRepository } from 'src/app/infra/repository/dexie/dexie-repository.service';
@Injectable({
providedIn: 'root'
})
export class UserPhotoLocalRepository extends DexieRepository<UserPhotoTable, UserPhotoTable> implements IUserPhotoLocalRepository {
constructor() {
super(chatDatabase.userPhoto, UserPhotoTableSchema)
}
}
@@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { IGetUserPhotoByAttachmentId, IGetUserPhotoByAttachmentIdInputSchema, IUserPhotoRemoteRepository } from 'src/app/core/chat/repository/user-photo/user-photo-remote-repository';
import { HttpAdapter } from 'src/app/infra/http/adapter';
import { SafeValidateSchema } from 'src/app/services/decorators/validate-schema.decorator';
import { environment } from 'src/environments/environment';
import { z } from 'zod';
@Injectable({
providedIn: 'root'
})
export class UserPhotoRemoteRepositoryService implements IUserPhotoRemoteRepository{
constructor(
private http: HttpAdapter
) { }
@SafeValidateSchema(IGetUserPhotoByAttachmentIdInputSchema, 'GET/UserAuthentication/GetPhoto?UserPhoto=')
getUserPhotoByAttachmentId(input: IGetUserPhotoByAttachmentId) {
const geturl = environment.apiURL + `UserAuthentication/GetPhoto?UserPhoto=${input.attachmentId}`;
return this.http.get<string>(geturl)
}
}
+21
View File
@@ -0,0 +1,21 @@
import { HttpErrorResponse } from "@angular/common/http";
import { Result } from "neverthrow";
import { Observable } from "rxjs";
import { MessageEntity } from "src/app/core/chat/entity/message";
import { HttpListenToMessageLoadHistoryUseCaseInput } from "src/app/core/chat/usecase/message/http-listen-to-message-load-history-by-roomId-use-case";
import { HttpResult } from "src/app/infra/http/type";
import { UseCase } from "src/app/utils/use-case-interface";
import { IMessageGetAllByRoomIdOutPut } from 'src/app/core/chat/usecase/message/message-get-all-by-room-Id'
export abstract class ITemplateCreateAdapter implements UseCase<MessageEntity, any> {
execute(input: MessageEntity): Promise<any> {
throw new Error("Method not implemented.");
}
}
export abstract class SocketOnReconnectAdapter {
abstract execute(): Observable<boolean>
}
export abstract class HttpListenToMessageLoadHistoryAdapter {
abstract execute(input: HttpListenToMessageLoadHistoryUseCaseInput): Observable<IMessageGetAllByRoomIdOutPut>
}
@@ -0,0 +1,285 @@
import { Injectable } from '@angular/core';
import { MessageDeleteLiveUseCaseService } from 'src/app/module/chat/domain/use-case/message/message-delete-by-id-live-use-case.service'
import { SessionStore } from 'src/app/store/session.service';
import { MessageReactionInput, MessageReactionUseCaseService } from 'src/app/module/chat/domain/use-case/message/message-reaction-by-id-use-case.service';
import { MessageUpdateInput, MessageUpdateUseCaseService } from 'src/app/module/chat/domain/use-case/message/message-update-by-id-use-case.service';
import { MemberAdminUseCaseService, MemberSetAdminDTO } from 'src/app/module/chat/domain/use-case/member/member-admin-use-case.service';
import { MessageCreateUseCaseService } from 'src/app/module/chat/domain/use-case/message/message-create-use-case.service';
import { SignalRService } from 'src/app/infra/socket/signalR/signal-r.service';
import { SocketMessageDeleteUseCaseService } from 'src/app/module/chat/domain/use-case/socket/socket-message-delete-use-case.service';
import { SocketMessageUpdateUseCaseService } from 'src/app/module/chat/domain/use-case/socket/socket-message-update-use-case.service';
import { SocketMessageCreateUseCaseService } from 'src/app/module/chat/domain/use-case/socket/socket-message-create-use-case.service';
import { DownloadMessageAttachmentByMessageId, DownloadMessageAttachmentUserCaseService } from 'src/app/module/chat/domain/use-case/message/message-download-attachment-user-case.service';
import { ListenMessageByRoomIdNewUseCase } from 'src/app/module/chat/domain/use-case/message/listen-message-by-roomId.service';
import { MemberListUpdateStatusUseCaseService } from 'src/app/module/chat/domain/use-case/socket/member-list-update-status-use-case.service';
import { ListenMessageDeleteByRoomIdService } from './use-case/message/listene-message-delete-by-roomId.service';
import { ListenMessageUpdateByRoomIdUseCase } from './use-case/message/listen-message-update-by-roomId.service';
import { GetRoomByIdUseCaseService } from './use-case/room/room-get-by-id-use-case.service';
import { DeleteRoomUseCaseService } from './use-case/room/room-delete-by-id-use-case.service';
import { CreateRoomInputDTO, CreateRoomUseCaseService } from './use-case/room/room-create-use-case.service';
import { RoomLeaveUseCase, UserRemoveListInputDTO } from './use-case/room/room-leave-by-id-use-case.service';
import { SyncAllRoomMessagesService } from './use-case/message/sync-all-room-messages.service';
import { ListenSendMessageUseCase } from './use-case/message/listen-send-message.service';
import { SendLocalMessagesUseCaseService } from './use-case/message/messages-send-offline-use-case.service'
import { RemoveMemberUseCaseService } from './use-case/member/-use-case.service'
import { AddMemberUseCaseService } from './use-case/member/member-add-use-case.service'
import { RoomUpdateInputDTO, UpdateRoomByIdUseCaseService } from './use-case/room/room-update-by-id-use-case.service'
import { SocketConnectUseCaseService } from './use-case/socket-connect-use-case.service'
import { MessageMarkAsReadUseCaseService } from './use-case/message/message-mark-as-read-use-case.service'
import { MessageMarkAllMessageAsReadByRoomIdInputSchema, MessageMarkAllMessageAsReadByRoomIdService } from './use-case/message/message-mark-all-message-as-read-by-room-id.service'
import { GetRoomListUseCaseService } from 'src/app/module/chat/domain/use-case/room/room-get-list-use-case.service';
import { filter } from 'rxjs/operators';
import { v4 as uuidv4 } from 'uuid'
import { IMessage, MessageEntity } from '../../../core/chat/entity/message';
import { MessageAttachmentByMessageIdInput, MessageAttachmentByMessageIdUseCase } from './use-case/message/message-attachment-by-message-id.service';
import { AddMemberToRoomInputDTO } from '../domain/use-case/member/member-add-use-case.service';
import { RoomType } from "src/app/core/chat/entity/group";
import { HttpListenToMessageLoadHistoryAdapter } from './adapter'
import { HttpListenToMessageLoadHistoryUseCaseInput } from 'src/app/core/chat/usecase/message/http-listen-to-message-load-history-by-roomId-use-case';
import { MessageSocketRepositoryService } from 'src/app/module/chat/data/repository/message/message-live-signalr-data-source.service'
import { MessageMarkAsReadInput } from "src/app/module/chat/domain/use-case/message/message-mark-as-read-use-case.service";
import { BoldRemoveByRoomIdInput, BoldRemoveByRoomIdService } from 'src/app/module/chat/domain/use-case/bold/bold-remove-by-room-id.service';
import { MemberListHttpSyncUseCase } from 'src/app/module/chat/domain/use-case/member/member-list-http-sync-use-case.ts.service'
import { RoomBoldSyncUseCaseService } from 'src/app/module/chat/domain/use-case/room/room-bold-sync-use-case.service'
import { RoomSetLastMessageService } from 'src/app/module/chat/domain/use-case/room/room-set-last-message.service';
import { IUserPhotoGetByIdInput, UserPhotoGetByIdUseCase } from 'src/app/module/chat/domain/use-case/user-photo/user-photo-get-by-id-use-case.service'
export const getInstanceId = (): string => {
const storageKey = 'instanceId'; // Key for localStorage
let instanceId = localStorage.getItem(storageKey);
// If not found in localStorage, generate and set a new UUID
if (!instanceId) {
instanceId = uuidv4();
localStorage.setItem(storageKey, instanceId);
}
return instanceId;
};
export const InstanceId = getInstanceId();
@Injectable({
providedIn: 'root'
})
export class ChatServiceService {
constructor(
private MessageDeleteLiveUseCaseService: MessageDeleteLiveUseCaseService,
private MessageReactionUseCaseService: MessageReactionUseCaseService,
private MessageUpdateUseCaseService: MessageUpdateUseCaseService,
private SocketMessageDeleteUseCaseService: SocketMessageDeleteUseCaseService,
private messageLiveSignalRDataSourceService: SignalRService,
private SocketMessageUpdateUseCaseService: SocketMessageUpdateUseCaseService,
private SocketMessageCreateUseCaseService: SocketMessageCreateUseCaseService,
private MemberListUpdateStatusUseCaseService: MemberListUpdateStatusUseCaseService,
private MemberAdminUseCaseService: MemberAdminUseCaseService,
private MessageCreateUseCaseService: MessageCreateUseCaseService,
private ListenMessageByRoomIdNewUseCase: ListenMessageByRoomIdNewUseCase,
private ListenMessageDeleteService: ListenMessageDeleteByRoomIdService,
private ListenMessageUpdateByRoomIdUseCase: ListenMessageUpdateByRoomIdUseCase,
private ListenSendMessageUseCase: ListenSendMessageUseCase,
private MessageAttachmentByMessageIdService: MessageAttachmentByMessageIdUseCase,
private SyncAllRoomMessagesService: SyncAllRoomMessagesService,
private DownloadMessageAttachmentUserCaseService: DownloadMessageAttachmentUserCaseService,
private GetRoomListUseCaseService: GetRoomListUseCaseService,
private GetRoomByIdUseCaseService: GetRoomByIdUseCaseService,
private DeleteRoomUseCaseService: DeleteRoomUseCaseService,
private CreateRoomUseCaseService: CreateRoomUseCaseService,
private RoomLeaveUseCase: RoomLeaveUseCase,
private AddMemberUseCaseService: AddMemberUseCaseService,
private UpdateRoomByIdUseCaseService: UpdateRoomByIdUseCaseService,
private RemoveMemberUseCaseService: RemoveMemberUseCaseService,
private SendLocalMessagesUseCaseService: SendLocalMessagesUseCaseService,
private MessageMarkAsReadUseCaseService: MessageMarkAsReadUseCaseService,
private SocketConnectUseCaseService: SocketConnectUseCaseService,
private MessageMarkAllMessageAsReadByRoomIdService: MessageMarkAllMessageAsReadByRoomIdService,
private HttpListenToMessageLoadHistory: HttpListenToMessageLoadHistoryAdapter,
private MessageSocketRepositoryService: MessageSocketRepositoryService,
private BoldRemoveByRoomIdService: BoldRemoveByRoomIdService,
private MemberListHttpSyncUseCase: MemberListHttpSyncUseCase, // dont remove
private RoomBoldSyncUseCaseService: RoomBoldSyncUseCaseService, // dont remove
private RoomSetLastMessageService: RoomSetLastMessageService, // dont remove
private UserPhotoGetByIdUseCase: UserPhotoGetByIdUseCase
) {
this.MessageSocketRepositoryService.listenToDeleteMessages()
.pipe()
.subscribe(async (message) => {
if(message?.id) {
this.SocketMessageDeleteUseCaseService.execute(message)
}
})
this.MessageSocketRepositoryService.listenToUpdateMessages().pipe(
filter((message) => {
return !message?.requestId?.startsWith(InstanceId)
})
).subscribe(async (message) => {
if(message?.id) {
this.SocketMessageUpdateUseCaseService.execute(message)
}
})
this.MessageSocketRepositoryService.listenToMessages().pipe(
filter((message) => {
if(!message?.requestId?.startsWith(InstanceId) == false) {
// console.log('exclude my message---')
}
return !message?.requestId?.startsWith(InstanceId)
})
).subscribe(async (message) => {
if(message?.id) {
this.SocketMessageCreateUseCaseService.execute(message)
}
})
this.messageLiveSignalRDataSourceService.getData().pipe(
filter((message) => {
if(message?.method == 'AvailableUsers') {
// console.log('exclude my message---')
return true
}
})
).subscribe(async (message) => {
this.MemberListUpdateStatusUseCaseService.execute(message.data as any)
})
}
async start() {
this.chatSync()
this.SocketConnectUseCaseService.execute();
}
async chatSync() {
await this.getRoomList();
await this.asyncAllRoomMessage();
}
removeMemberToRoom(data: UserRemoveListInputDTO) {
return this.RemoveMemberUseCaseService.execute(data)
}
messageDelete(data: {roomId, messageId}) {
const params = {
...data,
senderId: SessionStore.user.UserId,
}
return this.MessageDeleteLiveUseCaseService.execute(params)
}
getUserPhoto(input: IUserPhotoGetByIdInput) {
return this.UserPhotoGetByIdUseCase.execute(input)
}
reactToMessage(input: MessageReactionInput) {
return this.MessageReactionUseCaseService.execute(input);
}
updateMessage(input: MessageUpdateInput) {
return this.MessageUpdateUseCaseService.execute(input);
}
setAdmin(input: MemberSetAdminDTO) {
return this.MemberAdminUseCaseService.execute(input)
}
sendMessage(input: IMessage, messageEnum: RoomType) {
return this.MessageCreateUseCaseService.execute(input, messageEnum);
}
asyncAllRoomMessage() {
return this.SyncAllRoomMessagesService.execute()
}
getMessageAttachmentByMessageId(input: MessageAttachmentByMessageIdInput) {
return this.MessageAttachmentByMessageIdService.execute(input)
}
downloadMessageAttachmentByMessageId(input: DownloadMessageAttachmentByMessageId) {
return this.DownloadMessageAttachmentUserCaseService.execute(input)
}
// getMessageAttachmentLocallyByMessageId(input: GetMessageAttachmentLocallyByMessageId) {
// return this.GetMessageAttachmentLocallyUseCaseService.execute(input)
// }
removeBoldFromRoom(input: BoldRemoveByRoomIdInput) {
return this.BoldRemoveByRoomIdService.execute(input)
}
listenToMessageLoadHistory(input: HttpListenToMessageLoadHistoryUseCaseInput) {
return this.HttpListenToMessageLoadHistory.execute(input)
}
getRoomList() {
return this.GetRoomListUseCaseService.execute()
}
getRoomById(id: string) {
return this.GetRoomByIdUseCaseService.execute(id)
}
deleteRoomById(id: string) {
return this.DeleteRoomUseCaseService.execute(id)
}
createRoom(data: CreateRoomInputDTO) {
return this.CreateRoomUseCaseService.execute(data)
}
leaveRoom(data: UserRemoveListInputDTO) {
return this.RoomLeaveUseCase.execute(data)
}
addMemberToRoom(data: AddMemberToRoomInputDTO) {
return this.AddMemberUseCaseService.execute(data)
}
updateRoomById(data: RoomUpdateInputDTO) {
return this.UpdateRoomByIdUseCaseService.execute(data)
}
// messageMarkAsRead(roomId) {
// return this.MessageReadAtByIdUseCaseService.execute({roomId})
// }
sendLocalMessages() {
return this.SendLocalMessagesUseCaseService.execute()
}
removeMember() {
}
sendReadAt(sendReadAt: MessageMarkAsReadInput) {
return this.MessageMarkAsReadUseCaseService.execute(sendReadAt)
}
markAllMessagesAsRead(input: MessageMarkAllMessageAsReadByRoomIdInputSchema) {
return this.MessageMarkAllMessageAsReadByRoomIdService.execute(input)
}
listenToIncomingMessage(roomId:string) {
return this.ListenMessageByRoomIdNewUseCase.execute({roomId})
}
listenToDeleteMessage(roomId: string) {
return this.ListenMessageDeleteService.execute({roomId})
}
listenToUpdateMessage(roomId: string) {
return this.ListenMessageUpdateByRoomIdUseCase.execute({roomId})
}
listenToSendMessage(roomId: string) {
return this.ListenSendMessageUseCase.execute({roomId})
}
}
@@ -0,0 +1,15 @@
import { MemberTable } from "src/app/infra/database/dexie/instance/chat/schema/members";
import { RoomByIdMemberItemOutputDTO } from "../use-case/room/room-get-by-id-use-case.service";
export function MemberListMapper(outputDto: RoomByIdMemberItemOutputDTO, roomId: string): MemberTable {
return {
roomId: roomId,
wxUserId: outputDto.user.wxUserId,
wxFullName: outputDto.user.wxFullName,
wxeMail: outputDto.user.wxFullName,
userPhoto: outputDto.user.userPhoto,
joinAt: outputDto.joinAt,
isAdmin: outputDto.isAdmin
}
}
@@ -0,0 +1,34 @@
import { MessageEntity, IMessage } from "../../../../core/chat/entity/message";
import { MessageOutPutDataDTO } from "src/app/core/chat/repository/dto/messageOutputDTO";
import { MessageInputDTO } from "../use-case/message/message-create-use-case.service";
export class MessageMapper {
static toDomain(DTO: MessageOutPutDataDTO) : MessageEntity {
return DTO as MessageEntity
}
static fromDomain(entity:IMessage, requestId): MessageInputDTO {
return {
receiverId: entity.receiverId,
canEdit: entity.canEdit,
message: entity.message,
messageType: entity.messageType,
oneShot: entity.oneShot,
requireUnlock: entity.requireUnlock,
roomId: entity.roomId,
senderId: entity.sender.wxUserId,
requestId: entity.requestId || requestId,
attachment: entity.attachments.map((e)=>({
fileType:e.fileType,
source: e.source,
file: e.file,
fileName: e.fileName,
applicationId: e.applicationId || 0,
docId: Number(e.docId) || 0,
mimeType: e.mimeType,
description: e.description
}))[0] || {}
}
}
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { DistributionService } from './distribution.service';
describe('DistributionService', () => {
let service: DistributionService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(DistributionService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,85 @@
import { Injectable } from '@angular/core';
import { filter, map } from 'rxjs/operators';
import { IMessageGetAllByRoomIdOutPut } from 'src/app/core/chat/usecase/message/message-get-all-by-room-Id';
import { HttpAdapter } from 'src/app/infra/http/adapter';
import { IDistributionLocalRepository } from 'src/app/core/chat/repository/distribution/distribution-local-repository'
import { z } from 'zod';
import { distributionListDetermineChanges } from '../../data/async/list/rooms/distributionListChangedetector';
import { DistributionEntity } from 'src/app/core/chat/entity/distribution';
export const DistributionOutPutDTOSchema = z.object({
$messageIdMemberId: z.string(),
memberId: z.number(),
readAt: z.string().nullable(),
deliverAt: z.string().nullable()
})
export type DistributionOutPutDTO = z.infer<typeof DistributionOutPutDTOSchema>
@Injectable({
providedIn: 'root'
})
export class DistributionService {
constructor(
private http: HttpAdapter,
private distributionLocalRepository: IDistributionLocalRepository
) {}
listenToLoadHistory() {
return this.http.listen().pipe(
filter((response)=> {
if(response?.isOk()) {
return response.value.url.endsWith('Messages')
}
return false
}),
map((response: any) => response.value.data as IMessageGetAllByRoomIdOutPut)
)
.subscribe(async (data) => {
const localList = await this.distributionLocalRepository.find({roomId: data.data[0].roomId})
data.data.map(async (message) => {
const serverList = message.info
if(localList.isOk() && localList.value) {
// const localListForCurrentMessage = localList.value.filter((e) => e.messageId == message.id)
// console.log('localListForCurrentMessage', localListForCurrentMessage)
// const { distributionToInsert, distributionToUpdate, distributionToDelete } = await distributionListDetermineChanges(serverList, localListForCurrentMessage, message.id)
// console.log({distributionToInsert, distributionToUpdate, distributionToDelete})
// const map = distributionToInsert.map((e) => {
// e.
// })
// this.distributionLocalRepository.insertMany(distributionToInsert)
// const distributionEntityToInsert = distributionToInsert.map((ee)=> {
// return new DistributionEntity({
// roomId: message.roomId,
// deliverAt: ee.deliverAt,
// memberId: ee.memberId,
// messageId: message.roomId,
// readAt: ee.readAt,
// })
// console.log('create')
// })
// if(distributionEntityToInsert.length >= 0) {
// // console.log('distributionEntityToInsert', distributionEntityToInsert)
// //this.distributionLocalRepository.insertMany(distributionEntityToInsert)
// }
}
})
})
}
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { RoomLastMessageService } from './room-last-message.service';
describe('RoomLastMessageService', () => {
let service: RoomLastMessageService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(RoomLastMessageService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import { filter, map } from 'rxjs/operators';
import { MessageEntity } from 'src/app/core/chat/entity/message';
import { IMessageSocketRepository } from 'src/app/core/chat/repository/message/message-socket-repository';
import { InstanceId } from '../chat-service.service';
import { IRoomLocalRepository } from 'src/app/core/chat/repository/room/room-local-repository';
@Injectable({
providedIn: 'root'
})
export class RoomLastMessageService {
constructor(
private MessageSocketRepositoryService: IMessageSocketRepository,
private roomLocalRepository: IRoomLocalRepository
) {
// this.listenToIncomingMessage()
}
listenToIncomingMessage() {
return this.MessageSocketRepositoryService.listenToMessages().pipe(
filter((message) => !message?.requestId?.startsWith(InstanceId)),
map(message => Object.assign(new MessageEntity(), message))
).subscribe(async (message) => {
this.roomLocalRepository.update(message.roomId, {
messages: [message]
})
});
}
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { RoomService } from './room.service';
describe('RoomService', () => {
let service: RoomService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(RoomService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import { RoomSocketRepositoryService } from 'src/app/module/chat/data/repository/room/room-socket-repository.service'
@Injectable({
providedIn: 'root'
})
export class RoomService {
constructor(
private RoomSocketRepositoryService: RoomSocketRepositoryService
) {}
init() {
this.OnReceiveCreateRoom()
this.OnDeleteCreateRoom()
}
OnReceiveCreateRoom() {
this.RoomSocketRepositoryService.listenToCreateRoom().subscribe((data)=> {
console.log('OnReceiveCreateRoom', data)
})
}
OnDeleteCreateRoom() {
this.RoomSocketRepositoryService.listenToDeleteRoom().subscribe((data)=> {
console.log('OnDeleteCreateRoom7', data)
})
}
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { BoldRemoveByRoomIdService } from './bold-remove-by-room-id.service';
describe('BoldRemoveByRoomIdService', () => {
let service: BoldRemoveByRoomIdService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(BoldRemoveByRoomIdService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { IBoldLocalRepository } from 'src/app/core/chat/repository/bold/bold-local-repository';
import { TracingType, XTracerAsync } from 'src/app/services/monitoring/opentelemetry/tracer';
import { zodSafeValidation } from 'src/app/utils/zodValidation';
import { z } from 'zod';
const BoldRemoveByRoomIdInputSchema = z.object({
roomId: z.string()
})
export type BoldRemoveByRoomIdInput = z.infer<typeof BoldRemoveByRoomIdInputSchema>
@Injectable({
providedIn: 'root'
})
export class BoldRemoveByRoomIdService {
constructor(
private boldLocalRepository: IBoldLocalRepository,
) { }
@XTracerAsync({name:'BoldRemoveByRoomIdService', module:'chat', bugPrint: true})
async execute(input: BoldRemoveByRoomIdInput, tracing?: TracingType) {
const validation = zodSafeValidation<any>(BoldRemoveByRoomIdInputSchema, input)
if(validation.isOk()) {
return await this.boldLocalRepository.delete(input.roomId)
} else {
tracing.hasError("invalid parameter")
}
}
}
@@ -0,0 +1,23 @@
import { Injectable } from '@angular/core';
import { captureAndReraiseAsync } from 'src/app/services/decorators/captureAndReraiseAsync';
import { RoomRemoteDataSourceService } from '../../../data/repository/room/room-remote-repository.service';
import { MemberListRemoteRepository } from '../../../data/repository/member/member-list-remote-repository.service';
import { UserRemoveListInputDTO } from '../room/room-leave-by-id-use-case.service';
@Injectable({
providedIn: 'root'
})
export class RemoveMemberUseCaseService {
constructor(
private memberRemoteDataSourceService: MemberListRemoteRepository,
) { }
@captureAndReraiseAsync('RoomRepositoryService/removeMemberToRoom')
async execute(data: UserRemoveListInputDTO) {
const result = await this.memberRemoteDataSourceService.removeMemberFromRoom(data)
return result
}
}
@@ -0,0 +1,36 @@
import { Injectable } from '@angular/core';
import { captureAndReraiseAsync } from 'src/app/services/decorators/captureAndReraiseAsync';
import { z } from 'zod';
import { IMemberRemoteRepository } from 'src/app/core/chat/repository/member/member-remote-repository';
export const AddMemberToRoomInputDTOSchema = z.object({
id: z.string(),
members: z.array(z.number()),
});
export type AddMemberToRoomInputDTO = z.infer<typeof AddMemberToRoomInputDTOSchema>
@Injectable({
providedIn: 'root'
})
export class AddMemberUseCaseService {
constructor(
private memberRemoteDataSourceService: IMemberRemoteRepository,
) { }
@captureAndReraiseAsync('RoomRepositoryService/addMemberToRoom')
async execute(data: AddMemberToRoomInputDTO) {
// return this.roomLiveSignalRDataSourceService.addMemberToRoom(data)
const result = await this.memberRemoteDataSourceService.addMemberToRoom(data)
return result
}
}
@@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import { z } from "zod";
import { ValidateSchema } from 'src/app/services/decorators/validate-schema.decorator';
import { IMemberRemoteRepository } from 'src/app/core/chat/repository/member/member-remote-repository';
// Define the schema for the entire response
const MemberSetAdminDTOSchema = z.object({
roomId: z.string(),
memberId: z.string()
});
export type MemberSetAdminDTO = z.infer<typeof MemberSetAdminDTOSchema>
@Injectable({
providedIn: 'root'
})
export class MemberAdminUseCaseService {
constructor(
public repository: IMemberRemoteRepository
) { }
@ValidateSchema(MemberSetAdminDTOSchema)
execute(input: MemberSetAdminDTO) {
return this.repository.setAmin(input)
}
}
@@ -0,0 +1,61 @@
import { Injectable } from '@angular/core';
import { filter, map } from 'rxjs/operators';
import { HttpAdapter } from 'src/app/infra/http/adapter';
import { RoomByIdOutputDTO } from '../../../domain/use-case/room/room-get-by-id-use-case.service';
import { IMemberLocalRepository } from 'src/app/core/chat/repository/member/member-local-repository';
import { MemberListMapper } from '../../../domain/mapper/memberLIstMapper';
import { roomMemberListDetermineChanges } from '../../../data/async/list/rooms/roomMembersChangeDetector';
@Injectable({
providedIn: 'root'
})
export class MemberListHttpSyncUseCase {
constructor(
private http: HttpAdapter,
private memberLocalRepository: IMemberLocalRepository
) {
this.http.listen().pipe(
filter((response)=> {
if(response?.isOk()) {
return response.value.url.includes('/Room/') && typeof response.value?.data?.data?.roomName == 'string'
}
return false
}),
map((response: any) => response.value.data as RoomByIdOutputDTO)
)
.subscribe(async (data) => {
this.syncMembers(data)
})
}
private async syncMembers(roomData: RoomByIdOutputDTO): Promise<void> {
const roomId = roomData.data.id;
// Fetch local members and determine changes
const localList = await this.memberLocalRepository.getRoomMemberById(roomId);
const { membersToInsert, membersToUpdate, membersToDelete } = roomMemberListDetermineChanges(roomData.data.members, localList, roomId);
// Sync members
// for (const user of membersToInsert) {
// await this.memberLocalRepository.addMember(MemberListMapper(user, roomId));
// }
// for (const user of membersToUpdate) {
// await this.memberLocalRepository.updateMemberRole(MemberListMapper(user, roomId));
// }
// for (const user of membersToDelete) {
// await this.memberLocalRepository.removeMemberFromRoom(user.$roomIdUserId);
// }
// console.log({membersToInsert, membersToUpdate, membersToDelete})
await Promise.all([
...membersToInsert.map(user => this.memberLocalRepository.addMember(MemberListMapper(user, roomId))),
...membersToUpdate.map(user => this.memberLocalRepository.updateMemberRole(MemberListMapper(user, roomId))),
...membersToDelete.map(user => this.memberLocalRepository.removeMemberFromRoom(user.$roomIdUserId))
]);
}
}
@@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import { filter, map } from 'rxjs/operators';
import { InstanceId } from '../../chat-service.service';
import { MessageEntity } from '../../../../../core/chat/entity/message';
import { z } from 'zod';
import { IMessageSocketRepository } from 'src/app/core/chat/repository/message/message-socket-repository';
export const ListenMessageByRoomIdNewInputDTOSchema = z.object({
roomId: z.string(),
});
export type ListenMessageByRoomIdNewInputDTO = z.infer<typeof ListenMessageByRoomIdNewInputDTOSchema>
@Injectable({
providedIn: 'root'
})
export class ListenMessageByRoomIdNewUseCase {
constructor(
private MessageSocketRepositoryService: IMessageSocketRepository
) { }
execute(data: ListenMessageByRoomIdNewInputDTO) {
return this.MessageSocketRepositoryService.listenToMessages().pipe(
filter((message) => !message?.requestId?.startsWith(InstanceId) && message?.roomId == data.roomId),
map(message => Object.assign(new MessageEntity(), message))
)
}
}
@@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { filter } from 'rxjs/operators';
import { z } from 'zod';
import { IMessageSocketRepository } from 'src/app/core/chat/repository/message/message-socket-repository';
export const ListenMessageUpdateByRoomIdInputDTOSchema = z.object({
roomId: z.string(),
});
export type ListenMessageUpdateByRoomIdInputDTO = z.infer<typeof ListenMessageUpdateByRoomIdInputDTOSchema>
@Injectable({
providedIn: 'root'
})
export class ListenMessageUpdateByRoomIdUseCase {
constructor(
private messageLiveSignalRDataSourceService: IMessageSocketRepository,
) { }
execute(input: ListenMessageUpdateByRoomIdInputDTO) {
return this.messageLiveSignalRDataSourceService.listenToUpdateMessages().pipe(
filter((message) => input.roomId == message?.roomId )
)
}
}
@@ -0,0 +1,34 @@
import { Injectable } from '@angular/core';
import { MessageSocketRepositoryService } from 'src/app/module/chat/data/repository/message/message-live-signalr-data-source.service'
import { InstanceId } from '../../chat-service.service';
import { filter, map } from 'rxjs/operators';
import { z } from 'zod';
export const ListenSendMessageInputDTOSchema = z.object({
roomId: z.string(),
});
export type ListenSendMessageInputDTO = z.infer<typeof ListenSendMessageInputDTOSchema>
@Injectable({
providedIn: 'root'
})
export class ListenSendMessageUseCase {
constructor(
private MessageSocketRepositoryService: MessageSocketRepositoryService
) { }
execute({roomId}: {roomId: string}) {
return this.MessageSocketRepositoryService.listenToMessages().pipe(
filter((message) => {
return message?.requestId?.startsWith(InstanceId) && message?.roomId == roomId
}),
map(message => message)
)
}
}
@@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import { filter } from 'rxjs/operators';
import { SignalRService } from 'src/app/infra/socket/signalR/signal-r.service';
import { MessageSocketRepositoryService } from 'src/app/module/chat/data/repository/message/message-live-signalr-data-source.service'
import { z } from 'zod';
export const ListenMessageDeleteByRoomIdInputDTOSchema = z.object({
roomId: z.string(),
});
export type ListenMessageDeleteByRoomIdInputDTO = z.infer<typeof ListenMessageDeleteByRoomIdInputDTOSchema>
@Injectable({
providedIn: 'root'
})
export class ListenMessageDeleteByRoomIdService {
constructor(
private MessageSocketRepositoryService: MessageSocketRepositoryService,
) { }
execute({roomId}: ListenMessageDeleteByRoomIdInputDTO) {
return this.MessageSocketRepositoryService.listenToDeleteMessages().pipe(
filter((message) => {
return roomId == message?.roomId
} )
)
}
}
@@ -0,0 +1,125 @@
import { Injectable } from '@angular/core';
import { z } from 'zod';
import { AttachmentRemoteDataSourceService } from 'src/app/module/chat/data/repository/attachment/attachment-remote-repository.service'
import { AttachmentLocalDataSource } from 'src/app/module/chat/data/repository/attachment/attachment-local-repository.service'
import { createBlobUrl } from 'src/app/utils/ToBase64';
import { err, Result } from 'neverthrow';
import { MessageEntitySchema } from '../../../../../core/chat/entity/message';
import { TracingType, XTracerAsync } from 'src/app/services/monitoring/opentelemetry/tracer';
import { isHttpResponse } from 'src/app/infra/http/http.service';
const MessageAttachmentByMessageIdSchema = MessageEntitySchema.pick({
$id: true,
id: true,
attachments: true,
})
export type MessageAttachmentByMessageIdInput = z.infer<typeof MessageAttachmentByMessageIdSchema>
@Injectable({
providedIn: 'root'
})
export class MessageAttachmentByMessageIdUseCase {
constructor(
private AttachmentRemoteDataSourceService: AttachmentRemoteDataSourceService,
private AttachmentLocalDataSource: AttachmentLocalDataSource
) { }
@XTracerAsync({name:'Message-Attachment-By-MessageIdUseCase', module:'chat', bugPrint: true, waitNThrow: 15000})
async execute(input: MessageAttachmentByMessageIdInput, tracing?: TracingType): Promise<Result<string, any>> {
tracing.setAttribute('messageId', input.id)
const getLocalAttachment = await this.AttachmentLocalDataSource.findOne({
$messageId: input.$id
})
if(getLocalAttachment.isOk() && getLocalAttachment.value) {
tracing.setAttribute('download', 'false')
// has blob
if(getLocalAttachment.value.file) {
const dataUrl = await createBlobUrl(getLocalAttachment.value.file)
if(dataUrl.isOk()) {
return dataUrl
} else {
return dataUrl
}
} else {
// has data url
return getLocalAttachment.map((e) => {
// Logger.info('restored file .', {
// data: e.base64.slice(0, 100)+'...'
// })
return e.base64
})
}
} else {
tracing.setAttribute('download', 'true')
tracing.setAttribute('attachmentId', input.attachments[0].id.toString())
const result = await this.AttachmentRemoteDataSourceService.getAttachment(input.attachments[0].id)
if(result.isErr()) {
tracing.hasError('failed to download message attachment', {
error: result.error,
data: 'document id '+ input.attachments[0].id,
messageId: input.id,
$messageId: input.$id
})
if(isHttpResponse(result.error)) {
tracing.setAttribute('attachmentUrl', result.error.message)
}
}
if(result.isOk()) {
const dataUrl = await createBlobUrl(result.value)
if(dataUrl.isOk()) {
//console.log('done convert')
//Logger.info('downloaded file .', {
// data: dataUrl.value.slice(0, 100)+'...'
//})
this.AttachmentLocalDataSource.insert({
$messageId: input.$id,
file: result.value,
fileType: input.attachments[0].fileType,
source: input.attachments[0].source,
fileName: input.attachments[0].fileName,
applicationId: input.attachments[0].applicationId,
docId: input.attachments[0].docId,
mimeType: input.attachments[0].mimeType,
}).then((e) => {
if(e.isErr()) {
tracing.hasError('failed to create attachment locally on send message', {
error: e.error,
// data: dataUrl.value.slice(0, 100)+'...'
})
}
})
return dataUrl
} else {
console.log('dataUrl eerror', dataUrl.error)
return err(false)
}
} else {
return result as any
}
}
}
}
@@ -0,0 +1,241 @@
import { Injectable } from '@angular/core';
import { IMessage, MessageAttachmentSource, MessageEntity, MessageEntitySchema, } from '../../../../../core/chat/entity/message';
import { AttachmentLocalDataSource } from "src/app/module/chat/data/repository/attachment/attachment-local-repository.service";
import { z } from 'zod';
import { v4 as uuidv4 } from 'uuid';
import { InstanceId } from '../../chat-service.service';
import { createBlobFromBase64, createDataURL } from 'src/app/utils/ToBase64';
import { zodSafeValidation } from 'src/app/utils/zodValidation';
import { Logger } from 'src/app/services/logger/main/service';
import { err, Result } from 'neverthrow';
import { MessageMapper } from '../../mapper/messageMapper';
import { RoomType } from "src/app/core/chat/entity/group";
import { TracingType, XTracerAsync } from 'src/app/services/monitoring/opentelemetry/tracer';
import { MemberListLocalRepository } from 'src/app/module/chat/data/repository/member/member-list-local-repository.service'
import { SessionStore } from 'src/app/store/session.service';
import { MessageTable } from 'src/app/infra/database/dexie/instance/chat/schema/message';
import { MessageAttachmentFileType, MessageOutPutDataDTO } from 'src/app/core/chat/repository/dto/messageOutputDTO';
import { IMessageLocalRepository } from 'src/app/core/chat/repository/message/message-local-repository';
import { IMessageSocketRepository } from 'src/app/core/chat/repository/message/message-socket-repository';
import { IMemberLocalRepository } from 'src/app/core/chat/repository/member/member-local-repository';
import { IAttachmentLocalRepository } from 'src/app/core/chat/repository/typing/typing-local-repository';
import { base64Schema } from 'src/app/utils/zod';
export const MessageInputDTOSchema = z.object({
roomId: z.string().uuid().optional(),
receiverId: z.number().optional(),
senderId: z.number(),
message: z.string().nullable().optional(),
messageType: z.number(),
canEdit: z.boolean(),
oneShot: z.boolean(),
requireUnlock: z.boolean(),
requestId: z.string(),
attachment: z.object({
fileType: z.nativeEnum(MessageAttachmentFileType),
source: z.nativeEnum(MessageAttachmentSource),
file: base64Schema.optional(),
fileName: z.string().optional(),
applicationId: z.number().optional(),
docId: z.number().optional(),
mimeType: z.string().nullable().optional(),
description: z.string().optional()
}).optional()
});
export type MessageInputDTO = z.infer<typeof MessageInputDTOSchema>
export const MessageCreatePutDataDTOSchema = z.object({
id: z.string(),
roomId: z.string(),
sender: z.object({
wxUserId: z.number(),
wxFullName: z.string(),
wxeMail: z.string(),
userPhoto: z.string().optional()
}),
message: z.string().nullable().optional(),
messageType: z.number(),
sentAt: z.string(),
canEdit: z.boolean(),
oneShot: z.boolean(),
requireUnlock: z.boolean(),
requestId: z.string().optional().nullable(),
reactions: z.object({
id: z.string(),
reactedAt: z.string(),
reaction: z.string(),
sender: z.object({}),
}).array(),
info: z.array(z.object({
memberId: z.number(),
readAt: z.string().nullable(),
deliverAt: z.string().nullable()
})),
attachments: z.array(z.object({
fileType: z.nativeEnum(MessageAttachmentFileType),
source: z.nativeEnum(MessageAttachmentSource),
file: z.string().optional(),
fileName: z.string().optional(),
applicationId: z.number().optional(),
docId: z.number().optional(),
id: z.string().optional()
}))
});
export type MessageCreateOutPutDataDTO = z.infer<typeof MessageCreatePutDataDTOSchema>
@Injectable({
providedIn: 'root'
})
export class MessageCreateUseCaseService {
constructor(
private AttachmentLocalRepositoryService: IAttachmentLocalRepository,
private messageLocalDataSourceService: IMessageLocalRepository,
private messageSocketRepositoryService: IMessageSocketRepository,
private MemberListLocalRepository: IMemberLocalRepository
) { }
@XTracerAsync({name:'MessageCreateUseCaseService', module:'chat', bugPrint: true, waitNThrow: 5000})
async execute(message: IMessage, messageEnum: RoomType, tracing?: TracingType) {
const validation = zodSafeValidation<IMessage>(MessageEntitySchema, message)
if(validation.isOk()) {
message.sendAttemp++;
message.requestId = InstanceId +'@'+ uuidv4();
message.sending = true;
const createMessageLocally = await this.messageLocalDataSourceService.insert(message)
if(createMessageLocally.isOk()) {
message.$id = createMessageLocally.value
if(message.hasAttachment) {
for (const attachment of message.attachments) {
if(attachment.source != MessageAttachmentSource.Webtrix) {
this.AttachmentLocalRepositoryService.insert({
$messageId: createMessageLocally.value,
file: createBlobFromBase64(attachment.file, attachment.mimeType),
fileType: attachment.fileType,
source: attachment.source,
fileName: attachment.fileName,
applicationId: attachment.applicationId,
docId: attachment.docId,
mimeType: attachment.mimeType,
base64: createDataURL(attachment.file, attachment.mimeType)
}).then((e) => {
if(e.isErr()) {
Logger.error('failed to create attachment locally on send message', {
error: e.error,
data: createDataURL(attachment.file, attachment.mimeType).slice(0, 100) +'...'
})
}
})
attachment.safeFile = createDataURL(attachment.file, attachment.mimeType)
}
}
}
//====================
message.sending = true
let sendMessageResult: Result<MessageOutPutDataDTO, any>
if(messageEnum == RoomType.Group) {
const DTO = MessageMapper.fromDomain(message, message.requestId)
sendMessageResult = await this.messageSocketRepositoryService.sendGroupMessage(DTO)
} else {
if(message.receiverId) {
const DTO = MessageMapper.fromDomain(message, message.requestId)
sendMessageResult = await this.messageSocketRepositoryService.sendDirectMessage(DTO)
} else {
const getRoomMembers = await this.MemberListLocalRepository.directMember({
roomId:message.roomId,
currentUserId: SessionStore.user.UserId
})
if(getRoomMembers.isOk()) {
message.receiverId = getRoomMembers.value.wxUserId
const DTO = MessageMapper.fromDomain(message, message.requestId)
sendMessageResult = await this.messageSocketRepositoryService.sendGroupMessage(DTO)
} else {
console.log('not found direct users', getRoomMembers.error)
}
}
}
// return this sendMessageResult
if(sendMessageResult.isOk()) {
message.id = sendMessageResult.value.id
console.log('sendMessageResult', sendMessageResult.value.id)
if(sendMessageResult.value.sender == undefined || sendMessageResult.value.sender == null) {
delete sendMessageResult.value.sender
}
let clone: MessageTable = {
...sendMessageResult.value,
id: sendMessageResult.value.id,
$id : message.$id
}
this.messageLocalDataSourceService.update(message.$id, {...clone, sending: false, roomId: message.roomId}).then((data)=> {
if(data.isOk()) {
} else {
tracing.hasError('failed to update send message')
console.log(sendMessageResult)
console.log(data.error)
}
})
return sendMessageResult
} else {
Logger.error('failed to send message to the server', {
error: sendMessageResult.error
})
await this.messageLocalDataSourceService.update(message.$id, {sending: false, $id: message.$id})
return err('no connection')
}
} else {
Logger.error('failed to insert locally', {
error: createMessageLocally.error.message
})
}
} else {
if(validation.error.formErrors.fieldErrors.attachments) {
Logger.error('failed to send message doe to invalid attachment', {
zodErrorList: validation.error.errors,
data: message.attachments
})
} else {
Logger.error('failed to send message, validation failed', {
zodErrorList: validation.error.errors,
data: message
})
}
}
}
}
@@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import { z } from 'zod';
import { MessageSocketRepositoryService } from '../../../data/repository/message/message-live-signalr-data-source.service';
import { XTracerAsync, TracingType } from 'src/app/services/monitoring/opentelemetry/tracer';
export const MessageDeleteInputDTOSchema = z.object({
requestId: z.string().optional(),
roomId: z.string(),
messageId: z.string(),
senderId: z.number(),
});
export type MessageDeleteInputDTO = z.infer<typeof MessageDeleteInputDTOSchema>
@Injectable({
providedIn: 'root'
})
export class MessageDeleteLiveUseCaseService {
constructor(
public repository: MessageSocketRepositoryService
) { }
@XTracerAsync({name:'MessageDeleteLiveUseCaseService', module:'chat', bugPrint: true, waitNThrow: 5000})
async execute(data: MessageDeleteInputDTO, tracing?: TracingType) {
tracing.log('MessageDeleteLiveUseCaseService payload', {
data: data
})
return this.repository.sendMessageDelete(data)
}
}
@@ -0,0 +1,68 @@
import { Injectable } from '@angular/core';
import { AttachmentRemoteDataSourceService } from 'src/app/module/chat/data/repository/attachment/attachment-remote-repository.service'
import { Logger } from 'src/app/services/logger/main/service';
import { convertBlobToDataURL, createBlobUrl } from 'src/app/utils/ToBase64';
import { AttachmentLocalDataSource } from 'src/app/module/chat/data/repository/attachment/attachment-local-repository.service'
import { z } from 'zod';
import { zodSafeValidation } from 'src/app/utils/zodValidation';
import { IMessage, MessageEntitySchema } from 'src/app/core/chat/entity/message';
const DownloadMessageAttachmentByMessageIdSchema = z.object({
$messageId: z.string(),
id: z.string()
})
export type DownloadMessageAttachmentByMessageId = z.infer<typeof DownloadMessageAttachmentByMessageIdSchema>
@Injectable({
providedIn: 'root'
})
export class DownloadMessageAttachmentUserCaseService {
constructor(
private AttachmentRemoteDataSourceService: AttachmentRemoteDataSourceService,
private AttachmentLocalDataSource: AttachmentLocalDataSource
) { }
async execute(input: DownloadMessageAttachmentByMessageId) {
const validation = zodSafeValidation<IMessage>(DownloadMessageAttachmentByMessageIdSchema, input)
if(validation.isOk()) {
const result = await this.AttachmentRemoteDataSourceService.getAttachment(input.id)
return result.asyncMap(async (blob) => {
const dataUrl = await createBlobUrl(blob)
if(dataUrl.isOk()) {
Logger.info('downloaded file #1', {
// data: dataUrl.slice(0, 100)+'...',
context: 'DownloadMessageAttachmentUserCaseService'
})
this.AttachmentLocalDataSource.insert({
$messageId: input.$messageId,
id: input.id,
file: blob,
})
return dataUrl.value
} else {
}
})
} else {
Logger.error('failed to download message doe to invalid attachment', {
zodErrorList: validation.error.errors,
data: input
})
return validation
}
}
}
@@ -0,0 +1,46 @@
import { Injectable } from '@angular/core';
import { MessageAttachmentByMessageIdInput } from './message-attachment-by-message-id.service';
import { AttachmentRemoteDataSourceService } from 'src/app/module/chat/data/repository/attachment/attachment-remote-repository.service'
import { AttachmentLocalDataSource } from 'src/app/module/chat/data/repository/attachment/attachment-local-repository.service'
import { err, Result } from 'neverthrow';
import { AttachmentTableSchema } from 'src/app/infra/database/dexie/instance/chat/schema/attachment';
import { z } from 'zod';
import { createBlobUrl } from 'src/app/utils/ToBase64';
const GetMessageAttachmentLocallyByMessageIdSchema = AttachmentTableSchema.pick({
$messageId: true
})
export type GetMessageAttachmentLocallyByMessageId = z.infer<typeof GetMessageAttachmentLocallyByMessageIdSchema>
@Injectable({
providedIn: 'root'
})
export class GetMessageAttachmentLocallyUseCaseService {
constructor(
private AttachmentRemoteDataSourceService: AttachmentRemoteDataSourceService,
private AttachmentLocalDataSource: AttachmentLocalDataSource
) { }
async execute(input: GetMessageAttachmentLocallyByMessageId): Promise<Result<string, any>> {
const getLocalAttachment = await this.AttachmentLocalDataSource.findOne({
$messageId: input.$messageId
})
if(getLocalAttachment.isOk()) {
if(getLocalAttachment.value) {
const dataUrl = await createBlobUrl(getLocalAttachment.value.file)
return dataUrl
}
} else {
return err(getLocalAttachment.error)
}
}
}
@@ -0,0 +1,43 @@
import { Injectable } from '@angular/core';
import { z } from 'zod';
import { MessageLocalDataSourceService } from 'src/app/module/chat/data/repository/message/message-local-data-source.service'
import { MessageSocketRepositoryService } from 'src/app/module/chat/data/repository/message/message-live-signalr-data-source.service'
import { SessionStore } from 'src/app/store/session.service';
const MessageMarkAllMessageAsReadByRoomIdInputSchema = z.object({
roomId: z.string(),
})
export type MessageMarkAllMessageAsReadByRoomIdInputSchema = z.infer<typeof MessageMarkAllMessageAsReadByRoomIdInputSchema>
/**
* @description avoid using it, avoid duplicate query. do it on the UI layer as the component load the message.
*/
@Injectable({
providedIn: 'root'
})
export class MessageMarkAllMessageAsReadByRoomIdService {
constructor(
private MessageLocalDataSourceService: MessageLocalDataSourceService,
private MessageSocketRepositoryService: MessageSocketRepositoryService
) { }
async execute(input: MessageMarkAllMessageAsReadByRoomIdInputSchema) {
const messages = await this.MessageLocalDataSourceService.find({roomId:input.roomId})
if(messages.isOk()) {
for(const message of messages.value) {
this.MessageSocketRepositoryService.sendReadAt({
memberId: SessionStore.user.UserId,
messageId: message.id,
roomId: input.roomId,
requestId: 'uuid'
});
}
}
}
}
@@ -0,0 +1,37 @@
import { Injectable } from '@angular/core';
import { MessageSocketRepositoryService } from '../../../data/repository/message/message-live-signalr-data-source.service';
import { XTracerAsync, TracingType } from 'src/app/services/monitoring/opentelemetry/tracer';
import { z } from 'zod';
export const MessageMarkAsReadInputSchema = z.object({
memberId: z.number(),
messageId: z.string(),
roomId: z.string(),
requestId: z.string()
})
export type MessageMarkAsReadInput = z.infer<typeof MessageMarkAsReadInputSchema>
@Injectable({
providedIn: 'root'
})
export class MessageMarkAsReadUseCaseService {
constructor(
private MessageSocketRepositoryService: MessageSocketRepositoryService,
) { }
@XTracerAsync({name:'MessageMarkAsReadUseCaseService', module:'chat', bugPrint: true})
async execute(sendReadAt: MessageMarkAsReadInput, tracing?: TracingType) {
const result = await this.MessageSocketRepositoryService.sendReadAt(sendReadAt as any)
if(result.isErr()) {
tracing.setAttribute('meesageId', sendReadAt.messageId)
tracing.hasError('failed to read message')
}
return result
}
}
@@ -0,0 +1,31 @@
import { Injectable } from '@angular/core';
import { object, z } from 'zod';
import { ValidateSchema } from 'src/app/services/decorators/validate-schema.decorator';
import { MessageRemoteDataSourceService } from '../../../data/repository/message/message-remote-data-source.service';
import { MessageSocketRepositoryService } from '../../../data/repository/message/message-live-signalr-data-source.service';
const MessageReactionInputDTOSchema = z.object({
memberId: z.number(),
messageId: z.string(),
roomId: z.string(),
reaction: z.string(),
requestId: z.string().optional()
})
export type MessageReactionInput = z.infer< typeof MessageReactionInputDTOSchema>
@Injectable({
providedIn: 'root'
})
export class MessageReactionUseCaseService {
constructor(
public repository: MessageSocketRepositoryService
) { }
@ValidateSchema(MessageReactionInputDTOSchema)
execute(input: MessageReactionInput) {
return this.repository.reactToMessageSocket(input)
}
}
@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { z } from 'zod';
import { ValidateSchema } from 'src/app/services/decorators/validate-schema.decorator';
import { MessageRemoteDataSourceService } from '../../../data/repository/message/message-remote-data-source.service';
import { MessageSocketRepositoryService } from '../../../data/repository/message/message-live-signalr-data-source.service';
const MessageUpdateInputDTOSchema = z.object({
memberId: z.number(),
messageId: z.string(),
roomId: z.string(),
message: z.string().optional().nullable(),
requestId: z.string().optional()
})
export type MessageUpdateInput = z.infer< typeof MessageUpdateInputDTOSchema>
@Injectable({
providedIn: 'root'
})
export class MessageUpdateUseCaseService {
constructor(
public repository: MessageSocketRepositoryService
) { }
@ValidateSchema(MessageUpdateInputDTOSchema)
execute(input: MessageUpdateInput) {
return this.repository.updateMessage(input);
}
}
@@ -0,0 +1,120 @@
import { Injectable } from '@angular/core';
import { MessageLocalDataSourceService } from '../../../data/repository/message/message-local-data-source.service';
import { MessageSocketRepositoryService } from '../../../data/repository/message/message-live-signalr-data-source.service';
import { InstanceId } from '../../chat-service.service';
import { MessageMapper } from '../../mapper/messageMapper';
import { v4 as uuidv4 } from 'uuid'
import { AttachmentLocalDataSource } from '../../../data/repository/attachment/attachment-local-repository.service';
import { RoomLocalRepository } from '../../../data/repository/room/room-local-repository.service';
import { MemberListLocalRepository } from 'src/app/module/chat/data/repository/member/member-list-local-repository.service'
import { Result } from 'neverthrow';
import { RoomType } from 'src/app/core/chat/entity/group';
import { SessionStore } from 'src/app/store/session.service';
import { MessageTable } from 'src/app/infra/database/dexie/instance/chat/schema/message';
import { MessageOutPutDataDTO } from 'src/app/core/chat/repository/dto/messageOutputDTO';
@Injectable({
providedIn: 'root'
})
export class SendLocalMessagesUseCaseService {
constructor(
private MessageSocketRepositoryService: MessageSocketRepositoryService,
private messageLocalDataSourceService: MessageLocalDataSourceService,
private AttachmentRepositoryService: AttachmentLocalDataSource,
private roomLocalDataSourceService: RoomLocalRepository,
private MemberListLocalRepository: MemberListLocalRepository,
private messageSocketRepositoryService: MessageSocketRepositoryService,
) { }
async execute() {
const allRooms = await this.roomLocalDataSourceService.findAll()
const messages = await this.messageLocalDataSourceService.getOfflineMessages()
if(allRooms.isOk()) {
if(allRooms.value.length == 0) {
} else {
for(const message of messages) {
const room = allRooms.value.find(e => e.id == message.roomId)
if(room) {
const attachments = await this.AttachmentRepositoryService.find({$messageId: message.$id})
if(attachments.isOk()) {
message.attachments = attachments.value.map(e => ({
fileType: e.fileType,
source: e.source,
fileName: e.fileName,
applicationId: e.applicationId,
docId: e.docId,
id: e.id,
mimeType: e.mimeType,
description: e.description,
file: e.base64.split(',')[1]
}))
console.log('to upload', messages)
const requestId = InstanceId +'@'+ uuidv4();
await this.messageLocalDataSourceService.update(message.$id, { sending: true })
let sendMessageResult: Result<MessageOutPutDataDTO, any>
if(room.roomType == RoomType.Group) {
const DTO = MessageMapper.fromDomain(message, message.requestId)
sendMessageResult = await this.MessageSocketRepositoryService.sendGroupMessage(DTO)
} else {
if(message.receiverId) {
const DTO = MessageMapper.fromDomain(message, message.requestId)
sendMessageResult = await this.messageSocketRepositoryService.sendDirectMessage(DTO)
} else {
const getRoomMembers = await this.MemberListLocalRepository.directMember({
roomId:message.roomId,
currentUserId: SessionStore.user.UserId
})
if(getRoomMembers.isOk() && getRoomMembers.value) {
message.receiverId = getRoomMembers.value.wxUserId
const DTO = MessageMapper.fromDomain(message, message.requestId)
sendMessageResult = await this.messageSocketRepositoryService.sendGroupMessage(DTO)
} else {
console.error('direct users not found', getRoomMembers)
}
}
}
if(sendMessageResult?.isOk()) {
if(sendMessageResult.value.sender == undefined || sendMessageResult.value.sender == null) {
delete sendMessageResult.value.sender
}
let clone: MessageTable = {
...sendMessageResult.value,
id: sendMessageResult.value.id,
$id : message.$id
}
clone.sending = false
console.log({clone})
console.log('send message local '+ messages.length)
this.messageLocalDataSourceService.update(message.$id, {id: sendMessageResult.value.id, sending: false})
} else {
this.messageLocalDataSourceService.update(message.$id, {sending: false})
}
}
}
}
}
}
}
}
@@ -0,0 +1,91 @@
import { Injectable } from '@angular/core';
import { MessageLocalDataSourceService } from '../../../data/repository/message/message-local-data-source.service';
import { messageListDetermineChanges } from '../../../data/async/list/rooms/messageListChangedetector';
import { MessageRemoteDataSourceService } from '../../../data/repository/message/message-remote-data-source.service';
import { MessageSocketRepositoryService } from '../../../data/repository/message/message-live-signalr-data-source.service';
import { ok } from 'neverthrow';
import { RoomLocalRepository } from '../../../data/repository/room/room-local-repository.service';
import { Logger } from 'src/app/services/logger/main/service';
import { XTracerAsync, TracingType } from 'src/app/services/monitoring/opentelemetry/tracer';
import { MessageTable } from 'src/app/infra/database/dexie/instance/chat/schema/message';
@Injectable({
providedIn: 'root'
})
export class SyncAllRoomMessagesService {
constructor(
private messageLocalDataSourceService: MessageLocalDataSourceService,
private messageRemoteDataSourceService: MessageRemoteDataSourceService,
private MessageSocketRepositoryService: MessageSocketRepositoryService,
private roomLocalDataSourceService: RoomLocalRepository,
) { }
@XTracerAsync({name:'SyncAllRoomMessagesService', module:'chat', bugPrint: true})
async execute(tracing?: TracingType) {
const allRooms = await this.roomLocalDataSourceService.findAll()
if(allRooms.isOk()) {
if(allRooms.value.length == 0) {
tracing.addEvent('no need to sync')
} else {
tracing.addEvent('total sync '+ allRooms.value.length)
}
let n =0
const roomPromises = allRooms.value.map(async (room) => {
const [result, localResult] = await Promise.all([
this.messageRemoteDataSourceService.getMessagesFromRoom(room.id),
this.messageLocalDataSourceService.getItems(room.id)
]);
tracing.addEvent('async n ' + n);
n++;
if (result.isOk()) {
const { addedItems, changedItems, deletedItems } = messageListDetermineChanges(result.value.data, localResult);
for (const message of changedItems) {
delete message.sentAt
let clone: MessageTable = { ...message, roomId: room.id };
this.messageLocalDataSourceService.update(clone.$id, clone);
// const me = message.info.find(e => e.memberId === SessionStore.user.UserId && typeof e.deliverAt === 'string');
// if (!me) {
// this.MessageSocketRepositoryService.sendDeliverAt({
// memberId: SessionStore.user.UserId,
// messageId: message.id,
// roomId: message.roomId,
// requestId: uuidv4()
// });
// tracing.addEvent('send deliver roomId ' + room.id);
// }
}
for (const message of addedItems) {
let clone: MessageTable = { ...message, roomId: room.id };
// You can perform operations with addedItems here if needed
}
this.messageLocalDataSourceService.insertMany(addedItems.reverse().map(e => {
e.origin = 'history'
return e
}));
} else {
Logger.error('failed to get room message ' + room.id);
}
});
// Wait for all the promises to resolve
await Promise.all(roomPromises);
return ok(true)
} else {
console.log('sync all error', allRooms)
}
}
}
@@ -0,0 +1,105 @@
import { Injectable } from '@angular/core';
import { filter, map } from 'rxjs/operators';
import { IMessageSocketRepository } from 'src/app/core/chat/repository/message/message-socket-repository';
import { MessageEntity } from 'src/app/core/chat/entity/message';
import { IBoldLocalRepository } from 'src/app/core/chat/repository/bold/bold-local-repository';
import { IMessageLocalRepository } from 'src/app/core/chat/repository/message/message-local-repository';
import { InstanceId } from '../../chat-service.service';
import { HttpAdapter } from 'src/app/infra/http/adapter';
import { SessionStore } from 'src/app/store/session.service';
import { TracingType, XTracerAsync } from 'src/app/services/monitoring/opentelemetry/tracer';
import { IRoomLocalRepository } from 'src/app/core/chat/repository/room/room-local-repository';
@Injectable({
providedIn: 'root'
})
export class RoomBoldSyncUseCaseService {
constructor(
private MessageSocketRepositoryService: IMessageSocketRepository,
private boldLocalRepository: IBoldLocalRepository,
private http: HttpAdapter,
private messageLocalRepository: IMessageLocalRepository,
private roomLocalDataSourceService: IRoomLocalRepository,
) {
this.listenToIncomingMessage();
// this.loadHistory()
this.onInsertToDB()
this.listenToUpdateMessages();
}
private listenToIncomingMessage() {
return this.MessageSocketRepositoryService.listenToMessages().pipe(
filter((message) => !message?.requestId?.startsWith(InstanceId)),
map(message => Object.assign(new MessageEntity(), message)),
filter((message) => !message.meSender())
).subscribe(async (message) => {
const result = await this.boldLocalRepository.findOne({roomId: message.roomId})
if(result.isOk() && !result.value) {
const result = await this.boldLocalRepository.insert({roomId: message.roomId, bold: 1})
} else if(result.isOk() && result.value.bold == 0) {
const result = await this.boldLocalRepository.update(message.roomId, {bold: 1})
}
});
}
private listenToUpdateMessages() {
return this.MessageSocketRepositoryService.listenToUpdateMessages().pipe(
filter((message) => !message?.requestId?.startsWith(InstanceId)),
map(message => Object.assign(new MessageEntity(), message))
).subscribe(async (message) => {
const haveSeen = MessageEntity.haveSeen(message.info)
if(haveSeen) {
const result = await this.boldLocalRepository.findOne({roomId: message.roomId})
if(result.isOk() && result.value?.bold == 1) {
const lastMessage = await this.roomLocalDataSourceService.findOne({id: message.roomId})
if(lastMessage.isOk()) {
if(lastMessage.value.messages[0].id == message.id) {
const result = await this.boldLocalRepository.update(message.roomId, {bold: 0})
}
}
}
}
});
}
@XTracerAsync({name:'RoomBoldSyncUseCaseService/onInsertToDB', module:'chat', bugPrint: true})
private onInsertToDB(tracing?: TracingType) {
let delay = 2000
this.messageLocalRepository.onCreateObservable().pipe(
filter(e => e?.origin == 'history'),
filter(e => e.sender.wxUserId != SessionStore.user.UserId),
).subscribe(async (newMessage)=> {
setTimeout(async ()=> {
const haveSeen = MessageEntity.haveSeen(newMessage.info)
if(!haveSeen) {
await this.boldLocalRepository.open()
const result = await this.boldLocalRepository.findOne({roomId: newMessage.roomId})
if(result.isOk() && !result.value?.bold) {
const result = await this.boldLocalRepository.insert({roomId: newMessage.roomId, bold: 1})
} else if(result.isOk() && result.value.bold == 0) {
const result = await this.boldLocalRepository.update(newMessage.roomId, {bold: 1})
} else {
// tracing.hasError("failed to set bold",{})
}
}
}, delay);
delay = 0
})
}
}
@@ -0,0 +1,63 @@
import { Injectable } from '@angular/core';
import { SessionStore } from 'src/app/store/session.service';
import { TracingType, XTracerAsync } from 'src/app/services/monitoring/opentelemetry/tracer';
import { z } from "zod";
import { IRoomRemoteRepository } from 'src/app/core/chat/repository/room/room-remote-repository';
import { IRoomLocalRepository } from 'src/app/core/chat/repository/room/room-local-repository';
import { IRoomSocketRepository } from 'src/app/core/chat/repository/room/room-socket-repository';
export const CreateRoomInputDTOSchema = z.object({
roomName: z.string(),
createdBy: z.number(),
roomType: z.number(),
expirationDate: z.string().nullable().optional(),
members: z.array(z.number())
});
export type CreateRoomInputDTO = z.infer<typeof CreateRoomInputDTOSchema>
export const RoomOutPutDTOSchema = z.object({
success: z.boolean(),
message: z.string(),
data: z.object({
id: z.string(),
roomName: z.string(),
createdBy: z.any().nullable(),
createdAt: z.string(),
expirationDate: z.string().nullable(),
roomType: z.any()
})
});
export type RoomOutPutDTO = z.infer<typeof RoomOutPutDTOSchema>
@Injectable({
providedIn: 'root'
})
export class CreateRoomUseCaseService {
constructor(
private roomRemoteDataSourceService: IRoomRemoteRepository,
private roomLocalDataSourceService: IRoomLocalRepository,
private RoomSocketRepositoryService: IRoomSocketRepository
) { }
@XTracerAsync({name:'room-create-use-case.service', module:'chat', bugPrint: true, waitNThrow: 5000})
async execute(data: CreateRoomInputDTO, tracing?: TracingType) {
const result = await this.RoomSocketRepositoryService.CreateGroup(data)
console.log('history', result)
// const result = await this.roomRemoteDataSourceService.createRoom(data)
if(result.isOk()) {
return result
} else {
tracing.hasError("socket close");
console.log(result.error)
}
return result
}
}
@@ -0,0 +1,46 @@
import { Injectable } from '@angular/core';
import { captureAndReraiseAsync } from 'src/app/services/decorators/captureAndReraiseAsync';
import { z } from "zod";
import { IRoomRemoteRepository } from 'src/app/core/chat/repository/room/room-remote-repository';
import { IRoomLocalRepository } from 'src/app/core/chat/repository/room/room-local-repository';
import { isHttpResponse } from 'src/app/infra/http/http.service';
export const DeleteRoomByIdInputDTOSchema = z.string()
export type DeleteRoomByIdInputDTO = z.infer<typeof DeleteRoomByIdInputDTOSchema>
@Injectable({
providedIn: 'root'
})
export class DeleteRoomUseCaseService {
constructor(
private roomRemoteDataSourceService: IRoomRemoteRepository,
// private roomMemoryDataSourceService: Store<RoomRemoteDataSourceState>,
private roomLocalDataSourceService: IRoomLocalRepository,
) { }
@captureAndReraiseAsync('RoomRepositoryService/deleteRoomById')
async execute(id: DeleteRoomByIdInputDTO) {
const result = await this.roomRemoteDataSourceService.deleteRoom(id)
if(result.isOk()) {
const result = await this.roomLocalDataSourceService.delete(id)
// this.messageLiveDataSourceService.sendMessage({
// type: 'createRoom',
// payload: {a: '5'}
// })
return result
} else if (isHttpResponse(result.error)) {
if(result.error.status == 404) {
await this.roomLocalDataSourceService.delete(id)
}
// this.httpErrorHandle.httpStatusHandle(result.error)
}
return result
}
}
@@ -0,0 +1,112 @@
import { Injectable } from '@angular/core';
import { isHttpResponse } from 'src/app/infra/http/http.service';
import { captureAndReraiseAsync } from 'src/app/services/decorators/captureAndReraiseAsync';
import { z } from 'zod';
import { IRoomRemoteRepository } from 'src/app/core/chat/repository/room/room-remote-repository';
import { IMemberLocalRepository } from 'src/app/core/chat/repository/member/member-local-repository';
import { IRoomLocalRepository } from 'src/app/core/chat/repository/room/room-local-repository';
import { GetRoomByIdMapper } from 'src/app/core/chat/mapper/getRoomByIdMapper';
import { RoomEntity, RoomType } from 'src/app/core/chat/entity/group';
import { diff, addedDiff, deletedDiff, updatedDiff, detailedDiff } from 'deep-object-diff';
import { zodSafeValidation } from 'src/app/utils/zodValidation';
const UserSchema = z.object({
wxUserId: z.number(),
wxFullName: z.string(),
wxeMail: z.string(),
userPhoto: z.string().nullable(),
});
const MemberSchema = z.object({
id: z.string(),
user: UserSchema,
joinAt: z.string(),
isAdmin: z.boolean()
});
export const RoomByIdOutputDTOSchema = z.object({
success: z.boolean(),
message: z.string(),
data: z.object({
id: z.string(),
roomName: z.string(),
createdBy: UserSchema,
createdAt: z.string(),
expirationDate: z.string().nullable(),
roomType: z.number(),
members: z.array(MemberSchema),
}),
})
export type RoomByIdMemberItemOutputDTO = z.infer<typeof MemberSchema>
export type RoomByIdOutputDTO = z.infer<typeof RoomByIdOutputDTOSchema>
export const RoomByIdInputDTOSchema = z.string()
export type RoomByIdInputDTO = z.infer<typeof RoomByIdInputDTOSchema>
@Injectable({
providedIn: 'root'
})
export class GetRoomByIdUseCaseService {
constructor(
private roomRemoteDataSourceService: IRoomRemoteRepository,
private roomLocalDataSourceService: IRoomLocalRepository,
private MemberListLocalRepository: IMemberLocalRepository
) { }
@captureAndReraiseAsync('RoomRepositoryService/getRoomById')
async execute(id: RoomByIdInputDTO) {
const result = await this.roomRemoteDataSourceService.getRoom(id)
if(result.isOk()) {
const validData = zodSafeValidation<RoomByIdOutputDTO>(RoomByIdOutputDTOSchema, result.value)
if(validData.isOk()) {
const localListRoom = await this.roomLocalDataSourceService.findAll()
if(localListRoom.isOk()) {
const getRoomById = await this.roomLocalDataSourceService.findOne({id:validData.value.data.id})
if(getRoomById.isOk() && getRoomById.value) {
const room = GetRoomByIdMapper.toDomain(validData.value)
const added: Partial<RoomEntity> = addedDiff(getRoomById.value, room);
const deleted: Partial<RoomEntity> = deletedDiff(getRoomById.value, room);
const updated: Partial<RoomEntity> = updatedDiff(getRoomById.value, room);
delete added.members
if(room.roomType == RoomType.Direct) {
delete updated.roomName
}
if(Object.keys(added).length >= 1 || Object.keys(updated).length >= 1) {
console.log('added', added);
console.log('deleted', deleted);
console.log('updated', updated);
this.roomLocalDataSourceService.update(room.id, room)
}
} else if (getRoomById.isOk() && !getRoomById.value) {
const room = GetRoomByIdMapper.toDomain(validData.value)
this.roomLocalDataSourceService.insert(room)
}
}
} else {
}
} else if (isHttpResponse(result.error) ) {
if(result.error.status == 404) {
await this.roomLocalDataSourceService.delete(id)
}
// this.httpErrorHandle.httpStatusHandle(result.error)
}
return result
}
}
@@ -0,0 +1,36 @@
import { Injectable } from '@angular/core';
import { GetRoomListUseCaseService } from './room-get-list-use-case.service'
import { RoomSocketRepositoryService } from '../../../data/repository/room/room-socket-repository.service'
@Injectable({
providedIn: 'root'
})
export class RoomGetListOnCreateUseCaseService {
constructor(
private RoomSocketRepositoryService: RoomSocketRepositoryService,
private getRoomListUseCaseService: GetRoomListUseCaseService
) {
this.init()
}
private init() {
this.OnReceiveCreateRoom()
this.OnDeleteCreateRoom()
}
private OnReceiveCreateRoom() {
this.RoomSocketRepositoryService.listenToCreateRoom().subscribe((data)=> {
console.log('OnReceiveCreateRoom', data)
this.getRoomListUseCaseService.execute()
})
}
private OnDeleteCreateRoom() {
this.RoomSocketRepositoryService.listenToDeleteRoom().subscribe((data)=> {
console.log('OnDeleteCreateRoom7', data)
})
}
}
@@ -0,0 +1,91 @@
import { Injectable } from '@angular/core';
import { roomListDetermineChanges } from '../../../data/async/list/rooms/roomListChangeDetector';
import { captureAndReraiseAsync } from 'src/app/services/decorators/captureAndReraiseAsync';
import { CronJobService } from 'src/app/utils/task-scheduler'
import { z } from "zod";
import { IRoomRemoteRepository } from 'src/app/core/chat/repository/room/room-remote-repository';
import { IRoomLocalRepository } from 'src/app/core/chat/repository/room/room-local-repository';
import { MessageEntitySchema } from 'src/app/core/chat/entity/message';
import { GetRoomListMapper } from 'src/app/core/chat/mapper/getRoomListMapper';
const CreatedBySchema = z.object({
wxUserId: z.number(),
wxFullName: z.string(),
wxeMail: z.string().email(),
userPhoto: z.string().nullable()// api check
});
const RoomListItemOutPutDTOSchema = z.object({
chatRoom: z.object({
id: z.string(),
roomName: z.string(),
createdBy: CreatedBySchema,
createdAt: z.string(),
expirationDate: z.string().nullable(), // api check
roomType: z.number(),
messages: MessageEntitySchema.array(),
user1: CreatedBySchema.nullable(),
user2: CreatedBySchema.nullable()
}),
joinAt: z.string()
})
// Define the schema for the entire response
export const RoomListOutPutDTOSchema = z.object({
success: z.boolean(),
message: z.string(),
data: z.array(RoomListItemOutPutDTOSchema),
});
export type RoomListItemOutPutDTO = z.infer<typeof RoomListItemOutPutDTOSchema>
export type RoomListOutPutDTO = z.infer<typeof RoomListOutPutDTOSchema>
@Injectable({
providedIn: 'root'
})
export class GetRoomListUseCaseService {
constructor(
private roomRemoteDataSourceService: IRoomRemoteRepository,
private roomLocalDataSourceService: IRoomLocalRepository,
private CronJobService: CronJobService
) { }
@captureAndReraiseAsync('RoomRepositoryService/list')
async execute() {
const result = await this.roomRemoteDataSourceService.getRoomList()
const localList = await this.roomLocalDataSourceService.findAll()
if(localList.isOk()) {
if(result.isOk()) {
const { roomsToDelete, roomsToInsert, roomsToUpdate } = roomListDetermineChanges(result.value.data, localList.value)
if(roomsToInsert) {
const roomsToInsertEntity = GetRoomListMapper.toDomain(roomsToInsert)
for( const room of roomsToInsertEntity) {
this.roomLocalDataSourceService.insert(room)
if(room.expirationDate) {
this.CronJobService.createCronJob('remove expired room', new Date(room.expirationDate), this.execute)
}
}
}
const roomsToUpdateEntity = GetRoomListMapper.toDomain(roomsToUpdate)
this.roomLocalDataSourceService.updateMany(roomsToUpdateEntity)
for( const room of roomsToDelete) {
this.roomLocalDataSourceService.delete(room.id)
}
}
}
return result
}
}
@@ -0,0 +1,43 @@
import { Injectable } from '@angular/core';
import { z } from "zod";
import { IMemberRemoteRepository } from 'src/app/core/chat/repository/member/member-remote-repository';
import { IRoomLocalRepository } from 'src/app/core/chat/repository/room/room-local-repository';
import { isHttpResponse } from 'src/app/infra/http/http.service';
// Define the schema for the entire response
export const UserRemoveListInputDTOSchema = z.object({
id: z.string(),
members: z.array(z.number())
});
export type UserRemoveListInputDTO = z.infer<typeof UserRemoveListInputDTOSchema>
@Injectable({
providedIn: 'root'
})
export class RoomLeaveUseCase {
constructor(
private memberRemoteDataSourceService: IMemberRemoteRepository,
private roomLocalDataSourceService: IRoomLocalRepository,
) { }
async execute(data: UserRemoveListInputDTO) {
const result = await this.memberRemoteDataSourceService.removeMemberFromRoom(data)
if(result.isOk()) {
this.roomLocalDataSourceService.delete(data.id)
} else if (isHttpResponse(result.error)) {
if(result.error.status == 404) {
await this.roomLocalDataSourceService.delete(data.id)
}
// this.httpErrorHandle.httpStatusHandle(result.error)
}
return result
}
}
@@ -0,0 +1,152 @@
import { Injectable } from '@angular/core';
import { IMessageSocketRepository } from 'src/app/core/chat/repository/message/message-socket-repository';
import { IRoomLocalRepository } from 'src/app/core/chat/repository/room/room-local-repository';
import { filter, map, tap } from 'rxjs/operators';
import { MessageEntity } from 'src/app/core/chat/entity/message';
import { HttpAdapter } from 'src/app/infra/http/adapter';
import { IMessageGetAllByRoomIdOutPut } from 'src/app/core/chat/usecase/message/message-get-all-by-room-Id';
import { RoomEntity } from 'src/app/core/chat/entity/group';
import { RoomTable } from 'src/app/infra/database/dexie/instance/chat/schema/room';
import { IMessageLocalRepository } from 'src/app/core/chat/repository/message/message-local-repository';
@Injectable({
providedIn: 'root'
})
export class RoomSetLastMessageService {
constructor(
private roomLocalRepository: IRoomLocalRepository,
private messageSocketRepository: IMessageSocketRepository,
private messageLocalRepository: IMessageLocalRepository,
private http: HttpAdapter,
) {
this.listenToIncomingMessage()
this.listenToOnSendDataToSocket()
this.loadHistory()
this.listenToUpdateMessage()
}
private listenToUpdateMessage() {
let roomList: RoomTable[] = []
this.roomLocalRepository.getItemsLive().pipe(
tap((_roomList) => {
roomList = _roomList
})
).subscribe();
this.messageSocketRepository.listenToUpdateMessages().subscribe(async(_message) => {
const message = {..._message}
for(const room of roomList) {
if(room.messages?.[0]?.id == message.id) {
console.log('listenToUpdateMessage', message.roomId)
const result = await this.roomLocalRepository.update(message.roomId, {
messages: [message]
})
}
}
})
this.messageSocketRepository.listenToDeleteMessages().subscribe(async(_message) => {
const message = {..._message}
for(const room of roomList) {
if(room.messages?.[0]?.id == message.id) {
console.log({...room.messages?.[0],isDeleted: true})
const result = await this.roomLocalRepository.update(message.roomId, {
messages: [{...room.messages?.[0],isDeleted: true}]
})
}
}
})
}
private listenToIncomingMessage() {
return this.messageSocketRepository.listenToMessages().pipe(
map(message => Object.assign(new MessageEntity(), message))
).subscribe(async (message) => {
if(message?.roomId) {
console.log('listenToIncomingMessage', message.roomId)
const result = await this.roomLocalRepository.update(message.roomId, {
messages: [message]
})
if(result.isErr()) {
console.log('failed to update last message')
} else {
console.log('set last message')
}
}
});
}
private listenToOnSendDataToSocket() {
this.messageLocalRepository.onCreateObservable().subscribe(async (message) => {
if(message?.roomId) {
setTimeout(async() => {
if(message.origin != 'history') {
const result = await this.roomLocalRepository.update(message.roomId, {
messages: [message]
})
}
}, 100)
}
})
}
private loadHistory() {
const regex = new RegExp("Room\\/([0-9a-fA-F]{8})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{12})\\/Messages");
return this.http.listen().pipe(
filter((response: any)=> {
return response?.isOk() && regex.test(response.value.url) && response.value.url.endsWith('/Messages')
}),
map((response: any) => response.value.data as IMessageGetAllByRoomIdOutPut)
).subscribe(async (data)=> {
const loadHistoryFirstMessage = data.data[0]
if(loadHistoryFirstMessage) {
const roomId = loadHistoryFirstMessage.roomId
const room = await this.roomLocalRepository.findOne({id: roomId})
if(room.isOk()) {
const roomEntity = new RoomEntity(room.value)
if(!roomEntity.hasLastMessage()) {
await this.roomLocalRepository.update(loadHistoryFirstMessage.roomId, {
messages: [loadHistoryFirstMessage]
})
} else if (roomEntity.hasLastMessage()) {
const localLastMessageDate = new Date(room.value.messages[0].sentAt).getTime()
const loadHistoryLastMessageDate = new Date(loadHistoryFirstMessage.sentAt).getTime()
if(loadHistoryFirstMessage.id == room.value.messages?.[0]?.id) {
// do nothing
} else if(loadHistoryLastMessageDate>localLastMessageDate) {
await this.roomLocalRepository.update(loadHistoryFirstMessage.roomId, {
messages: [loadHistoryFirstMessage]
})
} else if(loadHistoryLastMessageDate == localLastMessageDate) {
// do nothing
} else if(room.value.messages[0].isDeleted != loadHistoryFirstMessage.isDeleted) {
await this.roomLocalRepository.update(loadHistoryFirstMessage.roomId, {
messages: [loadHistoryFirstMessage]
})
}
}
}
}
})
}
}
@@ -0,0 +1,77 @@
import { Injectable } from '@angular/core';
import { captureAndReraiseAsync } from 'src/app/services/decorators/captureAndReraiseAsync';
import { z } from "zod";
import { DataSourceReturn } from 'src/app/services/Repositorys/type';
import { IRoomLocalRepository } from 'src/app/core/chat/repository/room/room-local-repository';
import { IRoomRemoteRepository } from 'src/app/core/chat/repository/room/room-remote-repository';
export const RoomUpdateInputDTOSchema = z.object({
roomName: z.string(),
roomId: z.string(),
roomType: z.number(),
});
export type RoomUpdateInputDTO = z.infer<typeof RoomUpdateInputDTOSchema>
const UserSchema = z.object({
wxUserId: z.number(),
wxFullName: z.string(),
wxeMail: z.string(),
userPhoto: z.string().nullable(),
});
const MemberSchema = z.object({
id: z.string(),
user: UserSchema,
joinAt: z.string(),
});
export const RoomUpdateOutputDTOSchema = z.object({
success: z.boolean(),
message: z.string(),
data: z.object({
id: z.string(),
roomName: z.string(),
createdBy: UserSchema,
createdAt: z.string(),
expirationDate: z.string().nullable(),
roomType: z.number(),
members: z.array(MemberSchema),
}),
});
export type RoomUpdateOutputDTO = z.infer<typeof RoomUpdateOutputDTOSchema>
@Injectable({
providedIn: 'root'
})
export class UpdateRoomByIdUseCaseService {
constructor(
private roomRemoteDataSourceService: IRoomRemoteRepository,
private roomLocalDataSourceService: IRoomLocalRepository,
) { }
@captureAndReraiseAsync('RoomRepositoryService/updateRoomBy')
async execute(data: RoomUpdateInputDTO): Promise<DataSourceReturn<RoomUpdateOutputDTO>> {
const result = await this.roomRemoteDataSourceService.updateRoom(data)
if(result.isOk()) {
const localList = await this.roomLocalDataSourceService.findAll()
// const { roomsToDelete, roomsToInsert, roomsToUpdate } = roomListDetermineChanges([result.value.data], localList)
// for( const roomData of roomsToUpdate) {
// if(!roomData.chatRoom.createdBy?.wxUserId) {
// delete roomData.chatRoom.createdBy;
// }
// this.roomLocalDataSourceService.updateRoom(roomData.chatRoom)
// }
}
return result
}
}
@@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { MessageSocketRepositoryService } from 'src/app/module/chat/data/repository/message/message-live-signalr-data-source.service';
import { XTracerAsync } from 'src/app/services/monitoring/opentelemetry/tracer';
@Injectable({
providedIn: 'root'
})
export class SocketConnectUseCaseService {
constructor(
private MessageSocketRepositoryService: MessageSocketRepositoryService
) { }
@XTracerAsync({name:'SocketConnectUseCaseService', module:'chat', bugPrint: true})
async execute() {
return await this.MessageSocketRepositoryService.connect()
}
}
@@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { MessageSocketRepositoryService } from 'src/app/module/chat/data/repository/message/message-live-signalr-data-source.service';
@Injectable({
providedIn: 'root'
})
export class SocketJoinUseCaseService {
constructor(
private MessageSocketRepositoryService: MessageSocketRepositoryService
) { }
execute() {
this.MessageSocketRepositoryService.sendDirectMessage
}
}
@@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import { z } from 'zod';
import { ValidateSchema } from 'src/app/services/decorators/validate-schema.decorator';
import { MemberListLocalRepository } from 'src/app/module/chat/data/repository/member/member-list-local-repository.service'
export const MemberListUPdateStatus = z.object({
key: z.string(),
value: z.object({
userId: z.number(),
userName: z.string()
})
}).array();
export type MemberListUPdateStatusInputDTO = z.infer<typeof MemberListUPdateStatus>
@Injectable({
providedIn: 'root'
})
export class MemberListUpdateStatusUseCaseService {
constructor(
private MemberListLocalRepository: MemberListLocalRepository
) { }
@ValidateSchema(MemberListUPdateStatus)
execute(input: MemberListUPdateStatusInputDTO) {
return this.MemberListLocalRepository.updateMembersStatus(input)
}
}
@@ -0,0 +1,88 @@
import { Injectable, Input } from '@angular/core';
import { MessageLocalDataSourceService } from '../../../data/repository/message/message-local-data-source.service';
import { TracingType, XTracerAsync } from 'src/app/services/monitoring/opentelemetry/tracer';
import { ParamsValidation } from 'src/app/services/decorators/validate-schema.decorator';
import { MessageEntitySchema } from 'src/app/core/chat/entity/message';
import { z } from 'zod';
const SocketMessageCreateOutputSchema = MessageEntitySchema.pick({
id: true,
attachments: true,
canEdit: true,
editedAt: true,
info: true,
isDeleted: true,
message: true,
messageType: true,
oneShot: true,
reactions: true,
receiverId: true,
requireUnlock: true,
roomId: true,
sender: true,
sending: true,
sentAt: true,
})
export type ISocketMessageCreateOutput = z.infer<typeof SocketMessageCreateOutputSchema>
@Injectable({
providedIn: 'root'
})
export class SocketMessageCreateUseCaseService {
private broadcastChannel: BroadcastChannel;
private processedMessages = new Set<string>();
constructor(
private messageLocalDataSourceService: MessageLocalDataSourceService,
) {
this.broadcastChannel = new BroadcastChannel('socket-message');
this.broadcastChannel.onmessage = (event) => {
console.log('hello', event.data)
const messageId = event.data;
this.processedMessages.add(messageId);
};
// this.broadcastChannel.postMessage('incomingMessage.id');
}
@XTracerAsync({name:'Socket-Message-Create-UseCase', module:'chat', bugPrint: true})
async execute(input: ISocketMessageCreateOutput, tracing?: TracingType) {
this.broadcastChannel.postMessage(input.id);
ParamsValidation(SocketMessageCreateOutputSchema, input, tracing)
const incomingMessage = {
...input,
sending: false
}
// Check if the message ID already exists in the processedMessages set
if (this.processedMessages.has(incomingMessage.id)) {
console.warn(`Duplicate message detected: ${incomingMessage.id}`);
return; // Exit early to prevent duplicate handling
} else {
console.log('no duplicate')
}
// Add the message ID to the processedMessages set and broadcast it
this.processedMessages.add(incomingMessage.id);
console.log('create message', { incomingMessage });
tracing?.addEvent("Message Create start");
const result = await this.messageLocalDataSourceService.insert(incomingMessage);
tracing?.addEvent("Message Create end");
if (result.isOk()) {
// Optionally, you can handle post-insertion logic here
} else {
tracing?.addEvent("error while creating message");
tracing.log("error while creating message", {
error: result.error
});
}
}
}
@@ -0,0 +1,47 @@
import { Injectable } from '@angular/core';
import { MessageLocalDataSourceService } from '../../../data/repository/message/message-local-data-source.service';
import { MessageEntitySchema } from 'src/app/core/chat/entity/message';
import { z } from 'zod';
const SocketMessageDeleteOutputSchema = MessageEntitySchema.pick({
id: true,
attachments: true,
canEdit: true,
editedAt: true,
info: true,
isDeleted: true,
message: true,
messageType: true,
oneShot: true,
reactions: true,
receiverId: true,
requireUnlock: true,
roomId: true,
sender: true,
sending: true,
sentAt: true,
})
export type ISocketMessageDeleteOutput = z.infer<typeof SocketMessageDeleteOutputSchema>
@Injectable({
providedIn: 'root'
})
export class SocketMessageDeleteUseCaseService {
constructor(
private messageLocalDataSourceService: MessageLocalDataSourceService
) { }
async execute(input: ISocketMessageDeleteOutput) {
const result = await this.messageLocalDataSourceService.update(input.id, { isDeleted: true})
if(result.isOk()) {
console.log('deleled', result.value)
} else {
console.log(result.error)
}
}
}
@@ -0,0 +1,63 @@
import { Injectable } from '@angular/core';
import { MessageLocalDataSourceService } from '../../../data/repository/message/message-local-data-source.service';
import { ParamsValidation, SafeValidateSchema, ValidateSchema } from 'src/app/services/decorators/validate-schema.decorator';
import { TracingType, XTracerAsync } from 'src/app/services/monitoring/opentelemetry/tracer';
import { MessageEntitySchema } from 'src/app/core/chat/entity/message';
import { z } from 'zod';
import { MessageTable } from 'src/app/infra/database/dexie/instance/chat/schema/message';
import { MessageOutPutDataDTOSchema } from 'src/app/core/chat/repository/dto/messageOutputDTO';
const SocketMessageUpdateOutputSchema = MessageEntitySchema.pick({
id: true,
attachments: true,
canEdit: true,
editedAt: true,
info: true,
isDeleted: true,
message: true,
messageType: true,
oneShot: true,
reactions: true,
receiverId: true,
requireUnlock: true,
roomId: true,
sender: true,
sending: true,
sentAt: true,
})
export type ISocketMessageUpdateOutput = z.infer<typeof SocketMessageUpdateOutputSchema>
@Injectable({
providedIn: 'root'
})
export class SocketMessageUpdateUseCaseService {
constructor(
private messageLocalDataSourceService: MessageLocalDataSourceService
) { }
@XTracerAsync({name:'Socket-Message-Update-UseCase', bugPrint: true, module:'chat',})
async execute(input: ISocketMessageUpdateOutput, tracing?: TracingType) {
ParamsValidation(MessageOutPutDataDTOSchema, input, tracing)
tracing?.addEvent("Message existe?")
const result = await this.messageLocalDataSourceService.findOne({id: input.id})
const messageToSave: MessageTable = input
messageToSave.sending = false
if(result.isOk() && result.value) {
tracing?.addEvent("Message found")
const updateResult = await this.messageLocalDataSourceService.update(result.value.$id, messageToSave)
tracing.setAttribute('outcome', 'success')
return updateResult
} else if(result.isOk() && !result.value) {
tracing?.addEvent("Message not found")
}
}
}
@@ -0,0 +1,18 @@
import { Injectable } from '@angular/core';
import { SessionStore } from 'src/app/store/session.service';
import { UserTypingRemoteRepositoryService } from '../../../data/repository/typing/user-typing-live-data-source.service';
@Injectable({
providedIn: 'root'
})
export class SendTypingUseCaseService {
constructor(
private UserTypingRemoteRepositoryService: UserTypingRemoteRepositoryService,
) { }
execute(roomId) {
return this.UserTypingRemoteRepositoryService.sendTyping(roomId)
}
}
@@ -0,0 +1,49 @@
import { Injectable } from '@angular/core';
import { IUserPhotoLocalRepository } from 'src/app/core/chat/repository/user-photo/user-photo-local-repository';
import { IUserPhotoRemoteRepository } from 'src/app/core/chat/repository/user-photo/user-photo-remote-repository';
import { UserPhotoTableSchema } from 'src/app/infra/database/dexie/instance/chat/schema/user-foto';
import { z } from 'zod';
export const UserPhotoGetByIdInputSchema = UserPhotoTableSchema.pick({
attachmentId: true,
wxUserId: true,
})
export type IUserPhotoGetByIdInput = z.infer<typeof UserPhotoGetByIdInputSchema>
@Injectable({
providedIn: 'root'
})
export class UserPhotoGetByIdUseCase {
constructor(
private userPhotoLocalRepository: IUserPhotoLocalRepository,
private userPhotoRemoteRepository: IUserPhotoRemoteRepository
) { }
async execute(input: IUserPhotoGetByIdInput) {
const result = await this.userPhotoLocalRepository.findOne({wxUserId: input.wxUserId})
if(result.isOk() && result.value) {
return result.map(e => {
return e.file
})
} else if(result.isOk() && !result.value) {
const remoteResult = await this.userPhotoRemoteRepository.getUserPhotoByAttachmentId({attachmentId: input.attachmentId})
if(remoteResult.isOk()) {
this.userPhotoLocalRepository.insert({
wxUserId: input.wxUserId,
file: remoteResult.value.data,
attachmentId: input.attachmentId
})
}
return remoteResult.map(e => {
return e.data
})
}
}
}
View File