send and receive message

This commit is contained in:
Peter Maquiran
2024-07-11 10:28:21 +01:00
parent 386ca67315
commit b0325a5558
50 changed files with 720 additions and 372 deletions
@@ -0,0 +1,12 @@
import { Injectable } from '@angular/core';
import { MessageAsyncService } from 'src/app/module/chat/data/async/socket/message-async.service'
@Injectable({
providedIn: 'root'
})
export class ChatServiceService {
constructor(
private MessageAsyncService: MessageAsyncService
) { }
}
@@ -0,0 +1,22 @@
import { TableRoom } from "../../../data-source/room/rooom-local-data-source.service";
import { RoomListItemOutPutDTO, RoomListOutPutDTO } from "../../../dto/room/roomListOutputDTO";
export function roomListDetermineChanges(serverRooms: RoomListItemOutPutDTO[], localRooms: TableRoom[]) {
const serverRoomMap = new Map(serverRooms.map(room => [room.id, room]));
const localRoomMap = new Map(localRooms.map(room => [room.id, room]));
const roomsToInsert = serverRooms.filter(room => !localRoomMap.has(room.id));
const roomsToUpdate = serverRooms.filter(room => {
const localRoom = localRoomMap.get(room.id);
return localRoom && (
room.roomName !== localRoom.roomName ||
room.createdBy.wxUserId !== localRoom.createdBy.wxUserId ||
room.createdAt !== localRoom.createdAt ||
room.expirationDate !== localRoom.expirationDate ||
room.roomType !== localRoom.roomType
);
});
const roomsToDelete = localRooms.filter(room => !serverRoomMap.has(room.id));
return { roomsToInsert, roomsToUpdate, roomsToDelete };
}
@@ -0,0 +1,30 @@
import { TableMemberList } from "../../../data-source/room/rooom-local-data-source.service";
import { RoomByIdMemberItemOutputDTO } from "../../../dto/room/roomByIdOutputDTO";
export function roomMemberListDetermineChanges(____serverRooms: RoomByIdMemberItemOutputDTO[], localRooms: TableMemberList[], 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.user.wxUserId ||
room.user.userPhoto !== localRoom.user.userPhoto ||
room.joinAt !== localRoom.joinAt
)
});
const membersToDelete = localRooms.filter(room => !serverRoomMap.has(room.$roomIdUserId));
return { membersToInsert, membersToUpdate, membersToDelete };
}
@@ -0,0 +1,49 @@
import { Injectable } from '@angular/core';
import { MessageLiveDataSourceService } from '../../data-source/message/message-live-data-source.service';
import { MessageLocalDataSourceService } from '../../data-source/message/message-local-data-source.service';
import { MessageRemoteDataSourceService } from '../../data-source/message/message-remote-data-source.service';
import { SignalRService } from '../../../infra/socket/signal-r.service';
import { filter } from 'rxjs/operators';
import { InstanceId } from '../../repository/message-respository.service';
@Injectable({
providedIn: 'root'
})
export class MessageAsyncService {
constructor(
private messageRemoteDataSourceService: MessageRemoteDataSourceService,
private messageLiveDataSourceService: MessageLiveDataSourceService,
private messageLiveSignalRDataSourceService: SignalRService,
private messageLocalDataSourceService: MessageLocalDataSourceService
) {
this.messageLiveSignalRDataSourceService.getMessage().pipe(
filter((message: any) => {
return !message?.requestId?.startsWith(InstanceId) && message?.requestId
})
).subscribe(async (message: any) => {
console.log('message async ', message)
const id = message.id
delete message.id;
// const result = await this.messageLocalDataSourceService.createMessage({
// messageId: id,
// sending: false,
// ...message
// })
// if(result.isOk()) {
// console.log(result.value)
// } else {
// console.log(result.error)
// }
})
}
}
@@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { err, ok } from 'neverthrow';
import { WebSocketMessage, WebSocketService } from '../../../infra/socket/socket';
import { SignalRService } from '../../../infra/socket/signal-r.service';
@Injectable({
providedIn: 'root'
})
export class MessageLiveDataSourceService {
constructor(
public socket: WebSocketService,
private signalR: SignalRService) {}
async sendMessage(data: WebSocketMessage) {
try {
const result = await this.socket.sendMessage(data).toPromise()
return ok(result)
} catch (e) {
return err(e)
}
}
}
@@ -0,0 +1,38 @@
import { Injectable } from '@angular/core';
import { err, ok } from 'neverthrow';
import { SignalRService } from '../../../infra/socket/signal-r.service';
interface msgObj {
roomId: string;
senderId: string;
message:string;
messageType:1;
canEdit:Boolean;
oneShot:Boolean;
requestId: string;
}
@Injectable({
providedIn: 'root'
})
export class MessageLiveDataSourceService {
constructor(
private messageLiveSignalRDataSourceService: SignalRService
) {}
async sendMessage(data: msgObj) {
try {
const result = await this.messageLiveSignalRDataSourceService.sendMessage(data)
return ok(result)
} catch (e) {
return err(e)
}
}
}
@@ -0,0 +1,139 @@
import { Injectable } from '@angular/core';
import { Dexie, EntityTable, liveQuery } from 'Dexie';
import { err, ok } from 'neverthrow';
import { z } from 'zod';
import { from, Observable, Subject } from 'rxjs';
import { filter, switchMap } from 'rxjs/operators';
import { MessageInputDTO } from '../../dto/message/messageInputDtO';
const tableSchema = z.object({
id: z.any().optional(),
messageId: z.string().optional(),
roomId: z.string().uuid(),
senderId: z.number(),
message: z.string(),
messageType: z.number(),
canEdit: z.boolean(),
oneShot: z.boolean(),
sentAt: z.string().optional(),
requireUnlock: z.boolean(),
sender: z.object({
wxUserId: z.number(),
wxFullName: z.string(),
wxeMail: z.string(),
userPhoto: z.string(),
}),
sending: z.boolean().optional()
})
export type TableMessage = z.infer<typeof tableSchema>
// Database declaration (move this to its own module also)
export const messageDataSource = new Dexie('chat-message') as Dexie & {
message: EntityTable<TableMessage, 'id'>;
};
messageDataSource.version(1).stores({
message: '++id, roomId, senderId, message, messageType, canEdit, oneShot, requireUnlock, messageId'
});
@Injectable({
providedIn: 'root'
})
export class MessageLocalDataSourceService {
messageSubject = new Subject();
constructor() {
messageDataSource.message.hook('creating', (primKey, obj, trans) => {
// const newMessage = await trans.table('message').get(primKey);
this.messageSubject.next(obj);
// return newMessage
})
}
async sendMessage(data: MessageInputDTO) {
(data as TableMessage).sending = true
try {
const result = await messageDataSource.message.add(data)
return ok(result as string)
} catch (e) {
return err(false)
}
}
async createMessage(data: MessageInputDTO) {
try {
const result = await messageDataSource.message.add(data)
return ok(result as string)
} catch (e) {
return err(false)
}
}
async update(data: TableMessage ) {
try {
const result = await messageDataSource.message.update(data.id, data)
return ok(result)
} catch (e) {
return err(false)
}
}
async findOrUpdate(data: TableMessage) {
const findResult = await this.findMessageById(data.messageId)
if(findResult.isOk()) {
return this.update({...findResult.value, ...data})
} else {
return this.createMessage(data)
}
}
getItemsLive(roomId: string) {
return liveQuery(() =>
messageDataSource.message.where('roomId').equals(roomId).sortBy('id')
)
}
async findMessageById(messageId: string) {
try {
const a = await messageDataSource.message.where('messageId').equals(messageId).first()
if(a) {
return ok(a)
} else {
return err('not found')
}
} catch (e) {
return err('DB error')
}
}
subscribeToNewMessage(roomId: string): Observable<TableMessage> {
return this.messageSubject.pipe(
filter((message: TableMessage) =>
message.roomId === roomId
)
)
}
}
@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { HttpService } from 'src/app/services/http.service';
import { MessageInputDTO, MessageInputDTOSchema } from '../../dto/message/messageInputDtO';
import { ValidateSchema } from 'src/app/services/decorators/validate-schema.decorator';
import { APIReturn } from 'src/app/services/decorators/api-validate-schema.decorator';
import { MessageOutPutDTO, MessageOutPutDTOSchema } from '../../dto/message/messageOutputDTO';
import { MessageListOutput } from '../../dto/message/messageListOutputDTO';
import { DataSourceReturn } from 'src/app/services/Repositorys/type';
@Injectable({
providedIn: 'root'
})
export class MessageRemoteDataSourceService {
private baseUrl = 'https://gdapi-dev.dyndns.info/stage/api/v2/Chat'; // Your base URL
constructor(private httpService: HttpService) {}
@APIReturn(MessageOutPutDTOSchema, 'get/Messages')
@ValidateSchema(MessageInputDTOSchema)
async sendMessage(data: MessageInputDTO) {
return await this.httpService.post<MessageOutPutDTO>(`${this.baseUrl}/Messages`, data);
}
async reactToMessage(id: string, reaction: any) {
return await this.httpService.post<any>(`${this.baseUrl}/Messages/${id}/React`, reaction);
}
async getMessagesFromRoom(id: string): DataSourceReturn<MessageListOutput> {
return await this.httpService.get(`${this.baseUrl}/Room/${id}/Messages`);
}
}
@@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { WebSocketMessage, WebSocketService } from '../../../infra/socket/socket';
import { err, ok } from 'neverthrow';
@Injectable({
providedIn: 'root'
})
export class RoomLiveDataSourceService {
constructor(private socket: WebSocketService) {}
async getRoomById(data: WebSocketMessage) {
try {
const result = await this.socket.sendMessage(data).toPromise()
console.log({result})
return ok(result)
} catch (e) {
return err(e)
}
}
}
@@ -0,0 +1,65 @@
import { createAction, createFeatureSelector, createReducer, createSelector, on, props } from "@ngrx/store";
import { TableRoom } from "./rooom-local-data-source.service";
import { RoomOutPutDTO } from "../../dto/room/roomOutputDTO";
export interface ChatRoom {
[roomId: string]: TableRoom[];
}
export interface RoomRemoteDataSourceState {
chatRooms: ChatRoom;
}
export const initialState: RoomRemoteDataSourceState = {
chatRooms: {
// add more rooms as needed
}
};
export const addMessage = createAction(
'[Chat] Add Message',
props<{ roomId: string; message: any }>()
);
export const addRoom = createAction(
'[Chat] Add Room',
props<RoomOutPutDTO>()
);
const _chatReducer = createReducer(
initialState,
on(addMessage, (state, { roomId, message }) => ({
...state,
chatRooms: {
...state.chatRooms,
[roomId]: [...(state.chatRooms[roomId] || []), message]
}
})),
on(addRoom, (state, roomData: RoomOutPutDTO) => ({
...state,
chatRooms: {
...state.chatRooms,
[roomData.data.roomName]: roomData.data
}
}))
);
export function chatReducer(state, action) {
return _chatReducer(state, action);
}
// Create a feature selector
export const selectChatState = createFeatureSelector<RoomRemoteDataSourceState>('chat');
// Create a selector for a specific room's messages
export const selectMessagesByRoom = (roomId: string) => createSelector(
selectChatState,
(state: RoomRemoteDataSourceState) => state.chatRooms[roomId] || []
);
// Create a selector for the list of rooms
export const selectAllRooms = createSelector(
selectChatState,
(state: RoomRemoteDataSourceState) => Object.keys(state.chatRooms)
);
@@ -0,0 +1,68 @@
import { Injectable } from '@angular/core';
import { Result } from 'neverthrow';
import { HttpService } from 'src/app/services/http.service';
import { RoomListOutPutDTO, RoomListOutPutDTOSchema } from '../../dto/room/roomListOutputDTO';
import { RoomInputDTO, RoomInputDTOSchema } from '../../dto/room/roomInputDTO';
import { RoomOutPutDTO, RoomOutPutDTOSchema } from '../../dto/room/roomOutputDTO';
import { AddMemberToRoomInputDTO, AddMemberToRoomInputDTOSchema } from '../../dto/room/addMemberToRoomInputDto';
import { ValidateSchema } from 'src/app/services/decorators/validate-schema.decorator';
import { RoomByIdInputDTO, RoomByIdInputDTOSchema } from '../../dto/room/roomByIdInputDTO';
import { RoomByIdOutputDTO, RoomByIdOutputDTOSchema } from '../../dto/room/roomByIdOutputDTO';
import { APIReturn } from 'src/app/services/decorators/api-validate-schema.decorator';
import { UserRemoveListInputDTO, UserRemoveListInputDTOSchema } from '../../dto/room/userRemoveListInputDTO';
import { RoomUpdateInputDTO, RoomUpdateInputDTOSchema } from '../../dto/room/roomUpdateInputDTO';
import { RoomUpdateOutputDTO } from '../../dto/room/roomUpdateOutputDTO';
import { DataSourceReturn } from 'src/app/services/Repositorys/type';
@Injectable({
providedIn: 'root'
})
export class RoomRemoteDataSourceService {
private baseUrl = 'https://gdapi-dev.dyndns.info/stage/api/v2/Chat'; // Your base URL
constructor(private httpService: HttpService) {}
@ValidateSchema(RoomInputDTOSchema)
@APIReturn(RoomOutPutDTOSchema, 'post/Room')
async createRoom(data: RoomInputDTO): DataSourceReturn<RoomOutPutDTO> {
return await this.httpService.post<RoomOutPutDTO>(`${this.baseUrl}/Room`, data);
}
@APIReturn(RoomListOutPutDTOSchema, 'get/Room')
async getRoomList(): Promise<DataSourceReturn<RoomListOutPutDTO>> {
return await this.httpService.get<RoomListOutPutDTO>(`${this.baseUrl}/Room`);
}
@ValidateSchema(RoomByIdInputDTOSchema)
@APIReturn(RoomByIdOutputDTOSchema,'get/Room/${id}')
async getRoom(id: RoomByIdInputDTO): DataSourceReturn<RoomByIdOutputDTO> {
return await this.httpService.get(`${this.baseUrl}/Room/${id}`);
}
@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}`, {});
}
@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});
}
}
@@ -0,0 +1,173 @@
import { Injectable } from '@angular/core';
import { RoomListItemOutPutDTO, RoomListOutPutDTO } from '../../dto/room/roomListOutputDTO';
import { Dexie, EntityTable, liveQuery, Observable } from 'Dexie';
import { err, ok, Result } from 'neverthrow';
import { z } from 'zod';
import { ValidateSchema } from 'src/app/services/decorators/validate-schema.decorator';
const tableSchema = z.object({
id: z.string(),
roomName: z.string(),
createdBy: z.object({
wxUserId: z.number(),
wxFullName: z.string(),
wxeMail: z.string().email(),
userPhoto: z.string().nullable().optional()// api check
}),
createdAt: z.any(),
expirationDate: z.any(),
roomType: z.any(),
// lastMessage: z.object({
// sentAt: z.string(),
// message: z.string(),
// sender: z.object({
// wxUserId: z.any(),
// wxFullName: z.any(),
// }).optional(),s
// })
})
const TableMemberListSchema = z.object({
$roomIdUserId: z.string().optional(),
id: z.string(),
roomId: z.string(),
user: z.object({
wxUserId: z.number(),
wxFullName: z.string(),
wxeMail: z.string(),
userPhoto: z.string().nullable(),
}),
joinAt: z.string()
})
export type TableRoom = z.infer<typeof tableSchema>
export type TableMemberList = z.infer<typeof TableMemberListSchema>
// Database declaration (move this to its own module also)
export const roomDataSource = new Dexie('FriendDatabase') as Dexie & {
room: EntityTable<TableRoom, 'id'>;
memberList: EntityTable<TableMemberList, '$roomIdUserId'>;
};
roomDataSource.version(1).stores({
room: 'id, createdBy, roomName, roomType, expirationDate, lastMessage',
memberList: '$roomIdUserId, id, user, joinAt, roomId',
});
@Injectable({
providedIn: 'root'
})
export class RoomLocalDataSourceService {
private baseUrl = 'https://gdapi-dev.dyndns.info/stage/api/v2/Chat'; // Your base URL
constructor() {}
@ValidateSchema(tableSchema)
async createRoom(data: TableRoom) {
try {
const result = await roomDataSource.room.add(data)
return ok(result)
} catch (e) {
return err(false)
}
}
async deleteRoomById(id: string) {
try {
const result = await roomDataSource.room.delete(id)
return ok(result)
} catch (e) {
return err(false)
}
}
async leaveRoom(id: string) {
return this.deleteRoomById(id)
}
async updateRoom(data: TableRoom) {
try {
const result = await roomDataSource.room.update(data.id, data);
return ok(result)
} catch (e) {
return err(false)
}
}
async createOrUpdateRoom(data: TableRoom) {
const createResult = await this.createRoom(data)
if(createResult.isOk()) {
return createResult
} else {
return this.updateRoom(data)
}
}
async addMember(data: TableMemberList) {
try {
data.$roomIdUserId = data.roomId + data.user.wxUserId
const result = await roomDataSource.memberList.add(data)
return ok(result)
} catch (e) {
return err(false)
}
}
async removeMemberFromRoom($roomIdUserId): Promise<Result<any ,any>> {
try {
const member = await roomDataSource.memberList.where({ $roomIdUserId: $roomIdUserId }).first();
if (member) {
const result = await ok(roomDataSource.memberList.delete($roomIdUserId));
console.log(`Member with $roomIdUserId ${$roomIdUserId} removed from room ${member.roomId}.`);
return result
} else {
console.log(`No member found with $roomIdUserId ${$roomIdUserId}`);
return err('not Found')
}
} catch (e) {
return err(false)
}
}
async getRoomById(id: any) {
try {
const result = await roomDataSource.room.get(id)
return ok(result)
} catch (e) {
return err(false)
}
}
async getRoomList() {
return await roomDataSource.room.toArray()
}
getItemsLive(): Observable<RoomListOutPutDTO[]> {
return liveQuery(() => roomDataSource.room.toArray()) as any;
}
getRoomByIdLive(id: any): Observable<RoomListItemOutPutDTO | undefined> {
return liveQuery(() => roomDataSource.room.get(id)) as any;
}
async getRoomMemberById(roomId: any) {
return await roomDataSource.memberList.where('roomId').equals(roomId).toArray()
}
getRoomMemberByIdLive(roomId: any) {
return liveQuery(() => roomDataSource.memberList.where('roomId').equals(roomId).toArray())
}
}
@@ -0,0 +1,14 @@
import { z } from "zod";
export const MessageInputDTOSchema = z.object({
roomId: z.string().uuid(),
senderId: z.number(),
message: z.string(),
messageType: z.number(),
canEdit: z.boolean(),
oneShot: z.boolean(),
requireUnlock: z.boolean(),
});
export type MessageInputDTO = z.infer<typeof MessageInputDTOSchema>
@@ -0,0 +1,7 @@
import { z } from "zod"
const MessageListInputDTOSchema = z.object({
roomId: z.string(),
});
export type MessageListInputDTO = z.infer<typeof MessageListInputDTOSchema>
@@ -0,0 +1,29 @@
import { z } from "zod"
const SenderSchema = z.object({
wxUserId: z.number(),
wxFullName: z.string(),
wxeMail: z.string(),
userPhoto: z.string()
});
const DataSchema = z.object({
id: z.string(),
sender: SenderSchema,
message: z.string(),
messageType: z.number(),
sentAt: z.string().datetime(),
deliverAt: z.string().datetime().nullable(),
canEdit: z.boolean(),
oneShot: z.boolean(),
requireUnlock: z.boolean()
});
const MessageListOutputSchema = z.object({
success: z.boolean(),
message: z.string().nullable(),
data: z.array(DataSchema)
});
export type MessageListOutput = z.infer<typeof MessageListOutputSchema>
@@ -0,0 +1,29 @@
import { z } from "zod"
const DataSchema = z.object({
id: z.string(),
chatRoomId: z.string(),
wxUserId: z.number(),
sender: z.object({
wxUserId: z.number(),
wxFullName: z.string(),
wxeMail: z.string(),
userPhoto: z.string().optional()
}).nullable(),
message: z.string(),
messageType: z.number(),
sentAt: z.string(),
deliverAt: z.string().datetime().nullable(),
canEdit: z.boolean(),
oneShot: z.boolean(),
requireUnlock: z.boolean()
});
export const MessageOutPutDTOSchema = z.object({
success: z.boolean(),
message: z.string(),
data: DataSchema
});
export type MessageOutPutDTO = z.infer<typeof MessageOutPutDTOSchema>
@@ -0,0 +1,8 @@
import { z } from "zod";
export const AddMemberToRoomInputDTOSchema = z.object({
id: z.string(),
members: z.array(z.number()),
});
export type AddMemberToRoomInputDTO = z.infer<typeof AddMemberToRoomInputDTOSchema>
@@ -0,0 +1,9 @@
import { z } from "zod";
const OutPutDTOSchema = z.object({
success: z.string(),
message: z.string(),
data: z.any()
});
export type OutPutDTO = z.infer<typeof OutPutDTOSchema>
@@ -0,0 +1,6 @@
import { z } from "zod";
export const RoomByIdInputDTOSchema = z.string()
export type RoomByIdInputDTO = z.infer<typeof RoomByIdInputDTOSchema>
@@ -0,0 +1,32 @@
import { z } from "zod";
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 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>
@@ -0,0 +1,12 @@
import { z } from "zod";
export const RoomInputDTOSchema = z.object({
roomName: z.string(),
createdBy: z.number(),
roomType: z.number(),
expirationDate: z.string().datetime().nullable(),
members: z.array(z.number())
});
export type RoomInputDTO = z.infer<typeof RoomInputDTOSchema>
@@ -0,0 +1,29 @@
import { z } from "zod";
const CreatedBySchema = z.object({
wxUserId: z.number(),
wxFullName: z.string(),
wxeMail: z.string().email(),
userPhoto: z.string().nullable()// api check
});
const RoomListItemOutPutDTOSchema = z.object({
id: z.string(),
roomName: z.string(),
createdBy: CreatedBySchema,
createdAt: z.string(),
expirationDate: z.string().nullable(), // api check
roomType: z.number()
})
// 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>
@@ -0,0 +1,16 @@
import { z } from "zod";
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>
@@ -0,0 +1,9 @@
import { z } from "zod";
export const RoomUpdateInputDTOSchema = z.object({
roomName: z.string(),
roomId: z.string(),
roomType: z.number(),
});
export type RoomUpdateInputDTO = z.infer<typeof RoomUpdateInputDTOSchema>
@@ -0,0 +1,30 @@
import { z } from "zod";
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>
@@ -0,0 +1,9 @@
import { z } from "zod";
// 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>
@@ -0,0 +1,103 @@
import { Injectable } from '@angular/core';
import { MessageRemoteDataSourceService } from '../data-source/message/message-remote-data-source.service';
import { MessageLiveDataSourceService } from '../data-source/message/message-live-data-source.service';
import { MessageInputDTO } from '../dto/message/messageInputDtO';
import { MessageLocalDataSourceService, TableMessage } from '../data-source/message/message-local-data-source.service';
import { SessionStore } from 'src/app/store/session.service';
import { SignalRService } from '../../infra/socket/signal-r.service';
import { v4 as uuidv4 } from 'uuid'
export const InstanceId = uuidv4();
@Injectable({
providedIn: 'root'
})
export class MessageRepositoryService {
constructor(
private messageRemoteDataSourceService: MessageRemoteDataSourceService,
private messageLiveDataSourceService: MessageLiveDataSourceService,
private messageLiveSignalRDataSourceService: SignalRService,
private messageLocalDataSourceService: MessageLocalDataSourceService
) {
this.messageLiveDataSourceService.socket.messages$.subscribe(({payload, requestId, type}) => {
})
}
async sendMessage(data: MessageInputDTO) {
(data as TableMessage).sender = {
userPhoto: '',
wxeMail: SessionStore.user.Email,
wxFullName: SessionStore.user.FullName,
wxUserId: SessionStore.user.UserId
}
data['requestId'] = InstanceId +'@'+ uuidv4();
const localActionResult = await this.messageLocalDataSourceService.sendMessage(data)
this.messageLiveDataSourceService.sendMessage({
type: 'sendMessage',
payload: data
})
if(localActionResult.isOk()) {
const sendMessageResult = await this.messageLiveSignalRDataSourceService.sendMessage(data)
//const sendMessageResult = await this.messageRemoteDataSourceService.sendMessage(data)
if(sendMessageResult.isOk()) {
if(sendMessageResult.value.sender == undefined || sendMessageResult.value.sender == null) {
console.log({sendMessageResult})
delete sendMessageResult.value.sender
}
let clone: TableMessage = {
...sendMessageResult.value,
messageId: sendMessageResult.value.id,
id : localActionResult.value
}
return this.messageLocalDataSourceService.update({...clone, sending: false})
}
} else {
return this.messageLocalDataSourceService.update({sending: false})
}
}
async listAllMessagesByRoomId(id: string) {
const result = await this.messageRemoteDataSourceService.getMessagesFromRoom(id)
if(result.isOk()) {
for(const message of result.value.data) {
let clone: TableMessage = message
clone.messageId = message.id
clone.roomId = id
delete clone.id
await this.messageLocalDataSourceService.findOrUpdate(clone)
}
}
return result
}
getItemsLive (roomId: string) {
return this.messageLocalDataSourceService.getItemsLive(roomId)
}
subscribeToNewMessages(roomId: any) {
return this.messageLocalDataSourceService.subscribeToNewMessage(roomId)
}
}
@@ -0,0 +1,299 @@
import { Injectable } from '@angular/core';
import { RoomRemoteDataSourceService } from '../data-source/room/room-remote-data-source.service'
import { RoomInputDTO } from '../dto/room/roomInputDTO';
import { AddMemberToRoomInputDTO } from '../dto/room/addMemberToRoomInputDto';
import { RoomLocalDataSourceService } from '../data-source/room/rooom-local-data-source.service';
import { RoomByIdInputDTO } from '../dto/room/roomByIdInputDTO';
import { roomListDetermineChanges } from '../async/list/rooms/roomListChangeDetector';
import { UserRemoveListInputDTO } from '../dto/room/userRemoveListInputDTO';
import { roomMemberListDetermineChanges } from '../async/list/rooms/roomMembersChangeDetector';
import { captureAndReraiseAsync } from 'src/app/services/decorators/captureAndReraiseAsync';
import { RoomUpdateInputDTO } from '../dto/room/roomUpdateInputDTO';
import { SessionStore } from 'src/app/store/session.service';
import { RoomLiveDataSourceService } from '../data-source/room/room-live-data-source.service';
import { isHttpResponse } from 'src/app/services/http.service';
import { MessageLiveDataSourceService } from '../data-source/message/message-live-data-source.service';
function date(isoDateString) {
let date = new Date(isoDateString);
const tzOffset = -date.getTimezoneOffset(); // in minutes
const diff = tzOffset >= 0 ? '+' : '-';
const pad = (n: number) => (n < 10 ? '0' : '') + n;
return date.getFullYear() +
'-' + pad(date.getMonth() + 1) +
'-' + pad(date.getDate()) +
'T' + pad(date.getHours()) +
':' + pad(date.getMinutes()) +
':' + pad(date.getSeconds()) +
diff + pad(Math.floor(Math.abs(tzOffset) / 60)) +
':' + pad(Math.abs(tzOffset) % 60);
}
@Injectable({
providedIn: 'root'
})
export class RoomRepositoryService {
constructor(
private roomRemoteDataSourceService: RoomRemoteDataSourceService,
// private roomMemoryDataSourceService: Store<RoomRemoteDataSourceState>,
private roomLocalDataSourceService: RoomLocalDataSourceService,
private roomLiveDataSourceService: RoomLiveDataSourceService,
private messageLiveDataSourceService: MessageLiveDataSourceService,
) {
// this.messageLiveDataSourceService.socket.messages$.subscribe(({payload, requestId, type}) => {
// if(payload.sender == null) {
// delete payload.sender
// }
// if(type == 'sendMessage') {
// let clone: TableMessage = {
// ...payload,
// messageId: payload.id,
// }
// this.roomLocalDataSourceService.updateRoom({lastMessage: clone})
// }
// })
}
@captureAndReraiseAsync('RoomRepositoryService/list')
async list() {
const result = await this.roomRemoteDataSourceService.getRoomList()
const localList = await this.roomLocalDataSourceService.getRoomList()
if(result.isOk()) {
const { roomsToDelete, roomsToInsert, roomsToUpdate } = roomListDetermineChanges(result.value.data, localList)
for( const roomData of roomsToInsert) {
// roomData["lastMessage"] = {
// sentAt: date(roomData.createdAt),
// message: "",
// sender: {
// wxUserId: "",
// wxFullName: "",
// },
// }
this.roomLocalDataSourceService.createRoom(roomData)
}
for( const roomData of roomsToUpdate) {
// roomData["lastMessage"] = {
// sentAt: date(roomData.createdAt),
// message: "",
// sender: {
// wxUserId: "",
// wxFullName: "",
// },
// }
this.roomLocalDataSourceService.updateRoom(roomData)
}
for( const roomData of roomsToDelete) {
// roomData["lastMessage"] = {
// sentAt: date(roomData.createdAt),
// message: "",
// sender: {
// wxUserId: 0,
// wxFullName: "",
// },
// }
this.roomLocalDataSourceService.deleteRoomById(roomData.id)
}
}
return result
}
@captureAndReraiseAsync('RoomRepositoryService/updateRoomBy')
async updateRoomBy(data: RoomUpdateInputDTO) {
const result = await this.roomRemoteDataSourceService.updateRoom(data)
if(result.isOk()) {
const localList = await this.roomLocalDataSourceService.getRoomList()
const { roomsToDelete, roomsToInsert, roomsToUpdate } = roomListDetermineChanges([result.value.data], localList)
for( const roomData of roomsToUpdate) {
if(!roomData.createdBy?.wxUserId) {
delete roomData.createdBy;
}
this.roomLocalDataSourceService.updateRoom(roomData)
}
}
return result
}
@captureAndReraiseAsync('RoomRepositoryService/getRoomById')
async getRoomById(id: RoomByIdInputDTO) {
const result = await this.roomRemoteDataSourceService.getRoom(id)
if(result.isOk()) {
const localListRoom = await this.roomLocalDataSourceService.getRoomList()
const { roomsToDelete, roomsToInsert, roomsToUpdate } = roomListDetermineChanges([result.value.data], localListRoom)
for( const roomData of roomsToUpdate) {
this.roomLocalDataSourceService.updateRoom(roomData)
}
// ============================
const localList = await this.roomLocalDataSourceService.getRoomMemberById(id)
const { membersToInsert, membersToUpdate, membersToDelete } = roomMemberListDetermineChanges(result.value.data.members, localList, id)
for (const user of membersToInsert) {
await this.roomLocalDataSourceService.addMember({...user, roomId:id})
}
for(const user of membersToDelete) {
await this.roomLocalDataSourceService.removeMemberFromRoom(user.$roomIdUserId)
}
const __localListRoom = await this.roomLocalDataSourceService.getRoomList()
// this.roomLiveDataSourceService.getRoomById({
// type:'memberList',
// payload: __localListRoom
// })
} else if (isHttpResponse(result.error) ) {
if(result.error.status == 404) {
await this.roomLocalDataSourceService.deleteRoomById(id)
}
// this.httpErrorHandle.httpStatusHandle(result.error)
}
return result
}
@captureAndReraiseAsync('RoomRepositoryService/deleteRoomById')
async deleteRoomById(id: RoomByIdInputDTO) {
const result = await this.roomRemoteDataSourceService.deleteRoom(id)
if(result.isOk()) {
const result = await this.roomLocalDataSourceService.deleteRoomById(id)
this.messageLiveDataSourceService.sendMessage({
type: 'createRoom',
payload: {a: '5'}
})
return result
} else if (isHttpResponse(result.error)) {
if(result.error.status == 404) {
await this.roomLocalDataSourceService.deleteRoomById(id)
}
// this.httpErrorHandle.httpStatusHandle(result.error)
}
return result
}
@captureAndReraiseAsync('RoomRepositoryService/create')
async create(data: RoomInputDTO) {
const result = await this.roomRemoteDataSourceService.createRoom(data)
if(result.isOk()) {
if(!result.value.data.createdBy) {
let dataObject;
result.value.data.createdBy = {
wxeMail: SessionStore.user.Email,
wxFullName: SessionStore.user.FullName,
wxUserId: SessionStore.user.UserId,
}
dataObject = result.value.data
dataObject.lastMessage = {
sentAt: date(new Date()),
message: "",
sender: {
wxUserId: "",
wxFullName: "",
},
}
}
// this.roomMemoryDataSourceService.dispatch(addRoom(result.value))
const localResult = await this.roomLocalDataSourceService.createRoom(result.value.data)
this.messageLiveDataSourceService.sendMessage({
type: 'createRoom',
payload: {a: '5'}
})
return localResult.map(e => result.value)
}
return result
}
@captureAndReraiseAsync('RoomRepositoryService/addMemberToRoom')
async addMemberToRoom(data: AddMemberToRoomInputDTO) {
const result = await this.roomRemoteDataSourceService.addMemberToRoom(data)
return result
}
@captureAndReraiseAsync('RoomRepositoryService/removeMemberToRoom')
async removeMemberToRoom(data: UserRemoveListInputDTO) {
const result = await this.roomRemoteDataSourceService.removeMemberFromRoom(data)
return result
}
async leaveRoom(data: UserRemoveListInputDTO) {
const result = await this.roomRemoteDataSourceService.removeMemberFromRoom(data)
if(result.isOk()) {
this.roomLocalDataSourceService.leaveRoom(data.id)
} else if (isHttpResponse(result.error)) {
if(result.error.status == 404) {
await this.roomLocalDataSourceService.deleteRoomById(data.id)
}
// this.httpErrorHandle.httpStatusHandle(result.error)
}
return result
}
getItemsLive() {
return this.roomLocalDataSourceService.getItemsLive()
}
getItemByIdLive(id: any) {
return this.roomLocalDataSourceService.getRoomByIdLive(id)
}
getRoomMemberByIdLive(roomId: any) {
return this.roomLocalDataSourceService.getRoomMemberByIdLive(roomId)
}
getRoomMemberById(roomId: any) {
return this.roomLocalDataSourceService.getRoomMemberById(roomId)
}
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { SignalRService } from './signal-r.service';
describe('SignalRService', () => {
let service: SignalRService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SignalRService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,78 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { Platform } from '@ionic/angular';
import { SignalRConnection } from './signalR';
import { Plugins } from '@capacitor/core';
const { App } = Plugins;
@Injectable({
providedIn: 'root'
})
export class SignalRService {
private connection: SignalRConnection;
private messageSubject: BehaviorSubject<string> = new BehaviorSubject<any>(null);
private connectingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(null);
constructor(
private platform: Platform,) {
// this.startConnection();
// this.addMessageListener();
try {
if (!this.platform.is('desktop')) {
App.addListener('appStateChange', ({ isActive }) => {
if (isActive) {
// The app is in the foreground.
console.log('App is in the foreground');
this.newConnection()
} else {
// The app is in the background.
console.log('App is in the background');
// You can perform actions specific to the background state here.
}
});
}
} catch(error) {}
this.establishConnection()
}
private async establishConnection() {
const connection = new SignalRConnection({url:'https://gdapi-dev.dyndns.info/stage/chathub'})
const attempConnection = await connection.establishConnection()
if(attempConnection.isOk()) {
this.connection?.closeConnection()
this.connection = connection
this.connection.getSendLater().subscribe(data => {
})
this.connection.getMessages().subscribe((data) => {
this.messageSubject.next(data)
})
}
}
getMessage() {
return this.messageSubject.asObservable()
}
async sendMessage(data: Object) {
return await this.connection.sendMessage(data as any)
}
newConnection() {
this.establishConnection()
}
}
+140
View File
@@ -0,0 +1,140 @@
import * as signalR from '@microsoft/signalr';
import { BehaviorSubject, Observable } from 'rxjs';
import { ok, Result, err } from 'neverthrow';
import { SessionStore } from 'src/app/store/session.service';
import { filter, first } from 'rxjs/operators';
export class SignalRConnection {
private hubConnection: signalR.HubConnection;
private messageSubject: BehaviorSubject<string> = new BehaviorSubject<any>(null);
private connectionStateSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
private disconnectSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
private reconnectSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
private sendLaterSubject: BehaviorSubject<Object> = new BehaviorSubject<Object>(false);
private reconnect = true
url: string
constructor({url}) {
this.url = url
}
establishConnection(): Promise<Result<signalR.HubConnection, false>> {
return new Promise((resolve, reject) => {
const hubConnection = new signalR.HubConnectionBuilder()
.withUrl(this.url)
.build();
this.hubConnection = hubConnection
this.join()
hubConnection
.start()
.then(() => {
console.log('Connection started');
this.connectionStateSubject.next(true);
this.hubConnection = hubConnection
this.addMessageListener()
this.join()
resolve(ok(hubConnection))
})
.catch(error => {
console.log('Error while starting connection: ' + error);
this.connectionStateSubject.next(false);
reject(err(false))
});
hubConnection.onclose(() => {
console.log('Connection closed');
this.connectionStateSubject.next(false);
this.disconnectSubject.next(true)
if(this.reconnect) {
this.attempReconnect();
}
});
})
}
async attempReconnect() {
const attempConnection = await this.establishConnection()
if(attempConnection.isOk()) {
this.reconnectSubject.next(true)
}
}
public join() {
if(this.connectionStateSubject.value == true) {
console.log('join=============')
this.hubConnection.invoke("Join", 105, "UserFirefox");
} else {
this.sendLaterSubject.next({method: 'SendMessage', args:["Join", 312, "Daniel"]})
}
}
public async sendMessage(data: Object & { requestId}):Promise<Result<any, any>> {
return new Promise((resolve, reject) => {
if(this.connectionStateSubject.value == true) {
console.log('sendMessage')
this.hubConnection.invoke("SendMessage", data)
this.messageSubject.pipe(
filter((message: any) => data.requestId == message?.requestId),
first()
).subscribe(value => {
resolve(ok(value))
console.log('Received valid value:', value);
});
} else {
this.sendLaterSubject.next({method: 'SendMessage', args: data})
return reject(err(false))
}
})
}
private addMessageListener(): void {
this.hubConnection.on('ReceiveMessage', (message) => {
console.log('ReceiveMessage', message)
this.messageSubject.next(message);
});
}
public getMessages(): Observable<string> {
return this.messageSubject.asObservable()
}
public getConnectionState(): Observable<boolean> {
return this.connectionStateSubject.asObservable();
}
public getDisconnectTrigger(): Observable<boolean> {
return this.disconnectSubject.asObservable();
}
public getSendLater() {
return this.sendLaterSubject.asObservable();
}
public closeConnection(): void {
this.reconnect = false
if (this.hubConnection) {
this.hubConnection
.stop()
.then(() => {
console.log('Connection closed by user');
this.connectionStateSubject.next(false);
})
.catch(err => console.log('Error while closing connection: ' + err));
}
}
}
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<title>SignalR Client</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.0/signalr.min.js"></script>
</head>
<body>
<h1>SignalR Client</h1>
<div id="messages"></div>
<input id="chatbox">
<script>
var msgObj = {
roomId: "882fcb86-4028-4242-bb47-fca0170dcc65",
senderId:105,
message:"message enviada",
messageType:1,
canEdit:true,
oneShot:false,
};
const connection = new signalR.HubConnectionBuilder()
.withAutomaticReconnect()
.withUrl("https://gdapi-dev.dyndns.info/stage/chathub")
.build();
connection.start().then(function () {
connection.invoke("Join", 105, "UserFirefox");
document.getElementById("chatbox").addEventListener("keyup", function (event) {
if (event.key === "Enter") {
msgObj.Message = chatbox.value;
connection.invoke("SendMessage", msgObj);
event.target.value = "";
}
});
}).catch(function (err) {
return console.error(err.toString());
});
connection.on("ReceiveMessage", function (message) {
console.log(message);
const messages = document.getElementById("messages");
messages.innerHTML += `<p>${message.message}</p>`;
});
</script>
</body>
</html>
+129
View File
@@ -0,0 +1,129 @@
import { Injectable } from '@angular/core';
import { Observable, Subject, BehaviorSubject } from 'rxjs';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { catchError, retryWhen, tap, delay } from 'rxjs/operators';
import { SessionStore } from 'src/app/store/session.service';
import { v4 as uuidv4 } from 'uuid'
export interface WebSocketMessage {
type: string;
payload: any;
requestId?: string;
}
interface WebSocketError {
type: string;
error: any;
}
@Injectable({
providedIn: 'root'
})
export class WebSocketService {
private socket$: WebSocketSubject<WebSocketMessage>;
private messageSubject$: Subject<WebSocketMessage>;
private connectionStatus$: BehaviorSubject<boolean>;
private reconnectAttempts = 0;
private readonly maxReconnectAttempts = 5;
callback: {[key: string]: Function} = {}
constructor() {
this.messageSubject$ = new Subject<WebSocketMessage>();
this.connectionStatus$ = new BehaviorSubject<boolean>(false);
this.connect('https://5-180-182-151.cloud-xip.com:85/ws/')
this.messages$.subscribe(({payload, requestId, type}) => {
if(this.callback[requestId]) {
this.callback[requestId]({payload, requestId, type})
delete this.callback[requestId]
}
})
}
public connect(url: string) {
this.socket$ = webSocket<WebSocketMessage>(url);
this.socket$.pipe(
tap({
error: () => {
this.connectionStatus$.next(false);
}
}),
retryWhen(errors => errors.pipe(
tap(() => {
this.reconnectAttempts++;
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
throw new Error('Max reconnect attempts reached');
}
}),
delay(1000)
))
).subscribe(
(message) => {
this.messageSubject$.next(message);
if (!this.connectionStatus$.getValue()) {
this.connectionStatus$.next(true);
this.reconnectAttempts = 0;
// Send a message when the connection is established
this.sendMessage(SessionStore.user.UserId as any).subscribe();
}
},
(err) => {
console.error('WebSocket connection error:', err);
},
() => {
console.log('WebSocket connection closed');
this.connectionStatus$.next(false);
}
);
}
public sendMessage(message: WebSocketMessage): Observable<any> {
return new Observable<void>(observer => {
if(typeof message == 'object') {
message.requestId = uuidv4()
this.socket$.next(message);
this.callback[message.requestId] = ({payload, requestId})=> {
observer.next(payload as any);
observer.complete();
}
} else {
this.socket$.next(message);
observer.next({} as any);
observer.complete();
}
}).pipe(
catchError(err => {
console.error('Send message error:', err);
return new Observable<never>(observer => {
observer.error({ type: 'SEND_ERROR', error: err });
});
})
);
}
public get messages$(): Observable<WebSocketMessage> {
return this.messageSubject$.asObservable();
}
public get connectionStatus(): Observable<boolean> {
return this.connectionStatus$.asObservable();
}
}
View File