diff --git a/gabinete-digital-fo.code-workspace b/gabinete-digital-fo.code-workspace index 4c26e3783..5a212dcbc 100644 --- a/gabinete-digital-fo.code-workspace +++ b/gabinete-digital-fo.code-workspace @@ -21,6 +21,9 @@ }, { "path": "../sentry" + }, + { + "path": "../ionic6" } ], "settings": { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 957f75fff..afcf9e498 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -123,7 +123,7 @@ import { FirebaseX } from '@ionic-native/firebase-x/ngx'; */ ], beforeSend(event) { if (event.level === 'error') { - console.log(event.exception.values[0].value) + // console.log(event.exception.values[0].value) openTelemetryLogging.send({ type: 'graylog', payload: { diff --git a/src/app/core/chat/repository/message/socket-repositoty-parameter.ts b/src/app/core/chat/repository/message/socket-repositoty-parameter.ts new file mode 100644 index 000000000..7f89cdb05 --- /dev/null +++ b/src/app/core/chat/repository/message/socket-repositoty-parameter.ts @@ -0,0 +1,8 @@ + + +export interface sendReadAtInput { + memberId: number, + messageId:string, + roomId: string, + requestId: string + } diff --git a/src/app/home/home-routing.module.ts b/src/app/home/home-routing.module.ts index 3e49758f9..c86dca641 100644 --- a/src/app/home/home-routing.module.ts +++ b/src/app/home/home-routing.module.ts @@ -78,206 +78,206 @@ const routes: Routes = [ ], }, - // { - // path: 'agenda', - // children: [ - // { - // path:'', - // loadChildren: () => AgendaPageModule - // }, - // { - // path:':eventId/:caller', - // loadChildren: () => import('../pages/agenda/view-event/view-event.module').then( m => m.ViewEventPageModule), - // }, - // { - // path: 'eventId/:caller', - // loadChildren: () => import('../pages/agenda/view-event/view-event.module').then( m => m.ViewEventPageModule), - // }, - // { - // path: 'edit-event', - // loadChildren: () => import('../pages/agenda/edit-event/edit-event.module').then( m => m.EditEventPageModule) - // }, - // { - // path: 'emend-message-modal', - // loadChildren: () => import('../pages/gabinete-digital/event-list/approve-event-modal/approve-event-modal.module').then( m => m.ApproveEventModalPageModule) - // }, - // { - // path: 'view-event', - // loadChildren: () => import('../pages/agenda/view-event/view-event.module').then( m => m.ViewEventPageModule) - // }, - // { - // path:'event-list', - // children: [ - // { - // path:'', - // loadChildren: ()=> import('../pages/gabinete-digital/event-list/event-list.module').then(m => m.EventListPageModule) - // }, - // { - // path:'approve-event', - // children : [ - // { - // path:':serialNumber/:caller', - // loadChildren: ()=> import('../pages/gabinete-digital/event-list/approve-event/approve-event.module').then(m => m.ApproveEventPageModule) - // }, - // ] - // } - // ] - // }, - // ], + { + path: 'agenda', + children: [ + { + path:'', + loadChildren: () => AgendaPageModule + }, + { + path:':eventId/:caller', + loadChildren: () => import('../pages/agenda/view-event/view-event.module').then( m => m.ViewEventPageModule), + }, + { + path: 'eventId/:caller', + loadChildren: () => import('../pages/agenda/view-event/view-event.module').then( m => m.ViewEventPageModule), + }, + { + path: 'edit-event', + loadChildren: () => import('../pages/agenda/edit-event/edit-event.module').then( m => m.EditEventPageModule) + }, + { + path: 'emend-message-modal', + loadChildren: () => import('../pages/gabinete-digital/event-list/approve-event-modal/approve-event-modal.module').then( m => m.ApproveEventModalPageModule) + }, + { + path: 'view-event', + loadChildren: () => import('../pages/agenda/view-event/view-event.module').then( m => m.ViewEventPageModule) + }, + { + path:'event-list', + children: [ + { + path:'', + loadChildren: ()=> import('../pages/gabinete-digital/event-list/event-list.module').then(m => m.EventListPageModule) + }, + { + path:'approve-event', + children : [ + { + path:':serialNumber/:caller', + loadChildren: ()=> import('../pages/gabinete-digital/event-list/approve-event/approve-event.module').then(m => m.ApproveEventPageModule) + }, + ] + } + ] + }, + ], - // }, - // { - // path: 'gabinete-digital', - // children: [ - // { - // path:'', - // loadChildren: ()=> GabineteDigitalPageModule - // }, - // { - // path:'expediente', - // children: [ - // { - // path:'', - // loadChildren: ()=> import('../pages/gabinete-digital/expediente/expediente.module').then(m => m.ExpedientePageModule) - // }, - // { - // path:':SerialNumber', - // loadChildren: ()=> import('../pages/gabinete-digital/expediente/expediente-detail/expediente-detail.module').then(m => m.ExpedienteDetailPageModule) - // }, - // { - // path:':SerialNumber/:caller', - // loadChildren: ()=> import('../pages/gabinete-digital/expediente/expediente-detail/expediente-detail.module').then(m => m.ExpedienteDetailPageModule) - // }, - // { - // path:'events/:eventId/:caller', - // loadChildren: ()=> import('../pages/events/event-detail/event-detail.module').then(m => m.EventDetailPageModule), - // }, - // { - // path:'expediente-task-modal', - // loadChildren: ()=> import('../pages/gabinete-digital/expediente/expedient-task-modal/expedient-task-modal.module').then(m => m.ExpedientTaskModalPageModule), - // }, - // ] - // }, - // { - // path: 'expedientes-pr', - // children: [ - // { - // path: '', - // loadChildren: ()=> import('../pages/gabinete-digital/expedientes-pr/expedientes-pr.module').then(m => m.ExpedientesPrPageModule), - // }, - // { - // path:':SerialNumber/:caller', - // loadChildren: ()=> import('../pages/gabinete-digital/expedientes-pr/expediente-pr/expediente-pr.module').then(m => m.ExpedientePrPageModule), - // }, - // ] - // }, - // { - // path: 'despachos', - // children: [ - // { - // path: '', - // loadChildren: ()=> import('../pages/gabinete-digital/despachos/despachos.module').then(m => m.DespachosPageModule), - // }, - // { - // path:':SerialNumber/:caller', - // loadChildren: ()=> import('../pages/gabinete-digital/despachos/despacho/despacho.module').then(m => m.DespachoPageModule), - // }, - // ] - // }, - // { - // path: 'despachos-pr', - // children: [ - // { - // path: '', - // loadChildren: ()=> import('../pages/gabinete-digital/despachos-pr/despachos-pr.module').then(m => m.DespachosPrPageModule), - // }, - // { - // path:':SerialNumber/:caller', - // loadChildren: ()=> import('../pages/gabinete-digital/despachos-pr/despacho-pr/despacho-pr.module').then(m => m.DespachoPrPageModule), - // }, - // ] - // }, - // { - // path: 'pedidos', - // children: [ - // { - // path: '', - // loadChildren: ()=> import('../pages/gabinete-digital/pedidos/pedidos.module').then(m => m.PedidosPageModule), - // }, - // { - // path:':SerialNumber/:caller', - // loadChildren: ()=> import('../pages/gabinete-digital/pedidos/pedido/pedido.module').then(m => m.PedidoPageModule), - // }, - // ] - // }, - // { - // path: 'diplomas', - // children: [ - // { - // path: '', - // loadChildren: ()=> import('../pages/gabinete-digital/diplomas/diplomas.module').then(m => m.DiplomasPageModule), - // }, - // { - // path:':SerialNumber/:caller', - // loadChildren: ()=> import('../pages/gabinete-digital/diplomas/diploma/diploma.module').then(m => m.DiplomaPageModule), - // }, - // ] - // }, - // { - // path: 'diplomas-assinar', - // children: [ - // { - // path: '', - // loadChildren: ()=> import('../pages/gabinete-digital/diplomas-assinar/diplomas-assinar.module').then(m => m.DiplomasAssinarPageModule), - // }, - // { - // path:':SerialNumber/:caller', - // loadChildren: ()=> import('../pages/gabinete-digital/diplomas-assinar/diploma-assinar/diploma-assinar.module').then(m => m.DiplomaAssinarPageModule), - // }, - // ] - // }, - // { - // path: 'diplomas-gerar', - // children: [ - // { - // path: '', - // loadChildren: ()=> import('../pages/gabinete-digital/diplomas-gerar/diplomas-gerar.module').then(m => m.DiplomasGerarPageModule), - // }, - // { - // path:':SerialNumber/:caller', - // loadChildren: ()=> import('../pages/gabinete-digital/diplomas-gerar/diplomas-gerar/diplomas-gerar-routing.module').then(m => m.DiplomasGerarPageRoutingModule), - // }, - // ] - // }, - // { - // path:'event-list', - // children: [ - // { - // path:'', - // loadChildren: ()=> import('../pages/gabinete-digital/event-list/event-list.module').then(m => m.EventListPageModule) - // }, - // { - // path:'approve-event', - // children : [ - // { - // path:':serialNumber/:caller', - // loadChildren: ()=> import('../pages/gabinete-digital/event-list/approve-event/approve-event.module').then(m => m.ApproveEventPageModule) - // }, - // ] - // } - // ] - // }, - // { - // path: 'events-to-approve', - // children: [ - // { - // path: '', - // loadChildren: ()=> import('../shared/gabinete-digital/edit-event-to-approve/edit-event.module') - // } - // ] - // } - // ], - // }, + }, + { + path: 'gabinete-digital', + children: [ + { + path:'', + loadChildren: ()=> GabineteDigitalPageModule + }, + { + path:'expediente', + children: [ + { + path:'', + loadChildren: ()=> import('../pages/gabinete-digital/expediente/expediente.module').then(m => m.ExpedientePageModule) + }, + { + path:':SerialNumber', + loadChildren: ()=> import('../pages/gabinete-digital/expediente/expediente-detail/expediente-detail.module').then(m => m.ExpedienteDetailPageModule) + }, + { + path:':SerialNumber/:caller', + loadChildren: ()=> import('../pages/gabinete-digital/expediente/expediente-detail/expediente-detail.module').then(m => m.ExpedienteDetailPageModule) + }, + { + path:'events/:eventId/:caller', + loadChildren: ()=> import('../pages/events/event-detail/event-detail.module').then(m => m.EventDetailPageModule), + }, + { + path:'expediente-task-modal', + loadChildren: ()=> import('../pages/gabinete-digital/expediente/expedient-task-modal/expedient-task-modal.module').then(m => m.ExpedientTaskModalPageModule), + }, + ] + }, + { + path: 'expedientes-pr', + children: [ + { + path: '', + loadChildren: ()=> import('../pages/gabinete-digital/expedientes-pr/expedientes-pr.module').then(m => m.ExpedientesPrPageModule), + }, + { + path:':SerialNumber/:caller', + loadChildren: ()=> import('../pages/gabinete-digital/expedientes-pr/expediente-pr/expediente-pr.module').then(m => m.ExpedientePrPageModule), + }, + ] + }, + { + path: 'despachos', + children: [ + { + path: '', + loadChildren: ()=> import('../pages/gabinete-digital/despachos/despachos.module').then(m => m.DespachosPageModule), + }, + { + path:':SerialNumber/:caller', + loadChildren: ()=> import('../pages/gabinete-digital/despachos/despacho/despacho.module').then(m => m.DespachoPageModule), + }, + ] + }, + { + path: 'despachos-pr', + children: [ + { + path: '', + loadChildren: ()=> import('../pages/gabinete-digital/despachos-pr/despachos-pr.module').then(m => m.DespachosPrPageModule), + }, + { + path:':SerialNumber/:caller', + loadChildren: ()=> import('../pages/gabinete-digital/despachos-pr/despacho-pr/despacho-pr.module').then(m => m.DespachoPrPageModule), + }, + ] + }, + { + path: 'pedidos', + children: [ + { + path: '', + loadChildren: ()=> import('../pages/gabinete-digital/pedidos/pedidos.module').then(m => m.PedidosPageModule), + }, + { + path:':SerialNumber/:caller', + loadChildren: ()=> import('../pages/gabinete-digital/pedidos/pedido/pedido.module').then(m => m.PedidoPageModule), + }, + ] + }, + { + path: 'diplomas', + children: [ + { + path: '', + loadChildren: ()=> import('../pages/gabinete-digital/diplomas/diplomas.module').then(m => m.DiplomasPageModule), + }, + { + path:':SerialNumber/:caller', + loadChildren: ()=> import('../pages/gabinete-digital/diplomas/diploma/diploma.module').then(m => m.DiplomaPageModule), + }, + ] + }, + { + path: 'diplomas-assinar', + children: [ + { + path: '', + loadChildren: ()=> import('../pages/gabinete-digital/diplomas-assinar/diplomas-assinar.module').then(m => m.DiplomasAssinarPageModule), + }, + { + path:':SerialNumber/:caller', + loadChildren: ()=> import('../pages/gabinete-digital/diplomas-assinar/diploma-assinar/diploma-assinar.module').then(m => m.DiplomaAssinarPageModule), + }, + ] + }, + { + path: 'diplomas-gerar', + children: [ + { + path: '', + loadChildren: ()=> import('../pages/gabinete-digital/diplomas-gerar/diplomas-gerar.module').then(m => m.DiplomasGerarPageModule), + }, + { + path:':SerialNumber/:caller', + loadChildren: ()=> import('../pages/gabinete-digital/diplomas-gerar/diplomas-gerar/diplomas-gerar-routing.module').then(m => m.DiplomasGerarPageRoutingModule), + }, + ] + }, + { + path:'event-list', + children: [ + { + path:'', + loadChildren: ()=> import('../pages/gabinete-digital/event-list/event-list.module').then(m => m.EventListPageModule) + }, + { + path:'approve-event', + children : [ + { + path:':serialNumber/:caller', + loadChildren: ()=> import('../pages/gabinete-digital/event-list/approve-event/approve-event.module').then(m => m.ApproveEventPageModule) + }, + ] + } + ] + }, + { + path: 'events-to-approve', + children: [ + { + path: '', + loadChildren: ()=> import('../shared/gabinete-digital/edit-event-to-approve/edit-event.module') + } + ] + } + ], + }, { path: 'search', children: [ diff --git a/src/app/interceptors/token.interceptors.ts b/src/app/interceptors/token.interceptors.ts index b0916499f..e3a903375 100644 --- a/src/app/interceptors/token.interceptors.ts +++ b/src/app/interceptors/token.interceptors.ts @@ -43,8 +43,11 @@ export class TokenInterceptor implements HttpInterceptor { return next.handle(request).pipe( catchError((error) => { + console.log('interceptor ',error) if (error instanceof HttpErrorResponse && error.status === 401) { return this.handle401Error(request, next); + } else if (error.url.includes('https://gdapi-dev.dyndns.info/stage/api/v2') && error.status === 0){ + return this.handle401Error(request, next); } else { return throwError(error); } @@ -125,7 +128,7 @@ export class TokenInterceptor implements HttpInterceptor { if(pathBeforeGoOut != "/auth") { this.httpErrorHandle.httpsSucessMessagge('sessonExpired') } - + }) } return of(false); diff --git a/src/app/module/chat/chat.module.ts b/src/app/module/chat/chat.module.ts index c7169db22..77b889d88 100644 --- a/src/app/module/chat/chat.module.ts +++ b/src/app/module/chat/chat.module.ts @@ -43,7 +43,7 @@ export class ChatModule { this.typingCallback[id].pipe( switchMap(() => timer(2000)), ).subscribe(() => { - console.log('111111==============') + // console.log('111111==============') // this.memoryDataSource.dispatch(removeUserTyping({data: {...e} as any})) this.localDataSource.removeUserTyping(e) }) @@ -51,8 +51,6 @@ export class ChatModule { this.typingCallback[id].next() } - } else { - console.log('e--', e) } }) } diff --git a/src/app/module/chat/data/async/list/rooms/messageListChangedetector.ts b/src/app/module/chat/data/async/list/rooms/messageListChangedetector.ts index 8fe22c622..bbee8ab8b 100644 --- a/src/app/module/chat/data/async/list/rooms/messageListChangedetector.ts +++ b/src/app/module/chat/data/async/list/rooms/messageListChangedetector.ts @@ -1,6 +1,7 @@ +import { MessageTable } from "src/app/module/chat/infra/database/dexie/schema/message"; import { RoomListItemOutPutDTO, RoomListOutPutDTO } from "../../../dto/room/roomListOutputDTO"; -export function messageListDetermineChanges(serverList: any[], localList: any[]) { +export function messageListDetermineChanges(serverList: MessageTable[], localList: MessageTable[]) { localList = localList.filter(e => e.id) // Convert lists to dictionaries for easier comparison diff --git a/src/app/module/chat/data/dto/message/messageOutputDTO.ts b/src/app/module/chat/data/dto/message/messageOutputDTO.ts index 4d9858e9e..764258122 100644 --- a/src/app/module/chat/data/dto/message/messageOutputDTO.ts +++ b/src/app/module/chat/data/dto/message/messageOutputDTO.ts @@ -34,7 +34,11 @@ export const MessageOutPutDataDTOSchema = z.object({ reaction: z.string(), sender: z.object({}), }).array(), - info: z.array(z.object({})), + 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), diff --git a/src/app/module/chat/data/repository/member-list-local-repository.service.ts b/src/app/module/chat/data/repository/member-list-local-repository.service.ts index baf86e842..fb03fd6f7 100644 --- a/src/app/module/chat/data/repository/member-list-local-repository.service.ts +++ b/src/app/module/chat/data/repository/member-list-local-repository.service.ts @@ -8,38 +8,41 @@ import { chatDatabase } from '../../infra/database/dexie/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 { MemberTable } from '../../infra/database/dexie/schema/members'; +import { MemberTable, MemberTableSchema } from '../../infra/database/dexie/schema/members'; +import { from } from 'rxjs'; @Injectable({ providedIn: 'root' }) -export class MemberListLocalRepository extends DexieRepository { +export class MemberListLocalRepository extends DexieRepository { constructor() { - super(chatDatabase.room, RoomTableSchema) + super(chatDatabase.members, MemberTableSchema) + } - // messageDataSource.message.hook('creating', (primKey, obj, trans) => { - // // const newMessage = await trans.table('message').get(primKey); - // this.messageSubject.next(obj); - // // return newMessage - // }) + async directMember({roomId, currentUserId}) { + try { + let a = await chatDatabase.members.where('roomId') + .equals(roomId) + .and(message => message.wxUserId !== currentUserId) + .first() + + return ok(a) + } catch (e) { + return err(e) + } } async addMember(data: MemberTable) { - try { - data.$roomIdUserId = data.roomId + data.wxUserId - const result = await chatDatabase.members.add(data) - return ok(result) - } catch (e) { - return err(false) - } + data.$roomIdUserId = data.roomId + data.wxUserId + return this.insert(data) } async updateMemberRole(data: MemberTable) { try { const result = await chatDatabase.members.where({ - wxUserId:data.wxUserId, + wxUserId: data.wxUserId, roomId: data.roomId, }).modify(data); @@ -100,19 +103,11 @@ export class MemberListLocalRepository extends DexieRepository { return liveQuery(() => chatDatabase.members.get($roomIdUserId)) as any; } - getItemsLive(): Observable { - return liveQuery(() => chatDatabase.room.toArray()) as any; - } - - getRoomByIdLive(id: any): Observable { - return liveQuery(() => chatDatabase.room.get(id)) as any; - } - async getRoomMemberById(roomId: any) { return await chatDatabase.members.where({roomId}).toArray() } getRoomMemberByIdLive(roomId: any) { - return liveQuery(() => chatDatabase.members.where({roomId}).toArray()) + return from (liveQuery(() => chatDatabase.members.where({roomId}).toArray())) } getRoomMemberNoneAdminByIdLive(roomId: any) { diff --git a/src/app/module/chat/data/repository/message/message-live-signalr-data-source.service.ts b/src/app/module/chat/data/repository/message/message-live-signalr-data-source.service.ts index c0939ad15..cad24eec7 100644 --- a/src/app/module/chat/data/repository/message/message-live-signalr-data-source.service.ts +++ b/src/app/module/chat/data/repository/message/message-live-signalr-data-source.service.ts @@ -19,6 +19,22 @@ interface msgObj { requestId: string; } +interface sendDeliverAt { + memberId: number, + messageId:string, + roomId: string, + requestId: string +} + + +export interface sendReadAt { + memberId: number, + messageId:string, + roomId: string, + requestId: string +} + + @Injectable({ providedIn: 'root' }) @@ -48,6 +64,24 @@ export class MessageSocketRepositoryService { return result; } + async sendDeliverAt(data: sendDeliverAt) { + const result = await this.socket.sendData({ + method: 'DeliverAt', + data: data as any, + }) + + return result; + } + + async sendReadAt(data: sendReadAt) { + const result = await this.socket.sendData({ + method: 'ReadAt', + data: data as any, + }) + + return result; + } + listenToMessages() { return this.socket.getMessage() } @@ -62,7 +96,7 @@ export class MessageSocketRepositoryService { return this.socket.getMessageUpdate() } - + reactToMessageSocket(data) { this.socket.sendData({ method: 'ReactMessage', @@ -94,6 +128,6 @@ export class MessageSocketRepositoryService { } - + } diff --git a/src/app/module/chat/data/repository/room-local-repository.service.ts b/src/app/module/chat/data/repository/room-local-repository.service.ts index 9fd278d1e..748ff080c 100644 --- a/src/app/module/chat/data/repository/room-local-repository.service.ts +++ b/src/app/module/chat/data/repository/room-local-repository.service.ts @@ -7,6 +7,7 @@ import { ValidateSchema } from 'src/app/services/decorators/validate-schema.deco import { chatDatabase } from '../../infra/database/dexie/service'; import { RoomTable, RoomTableSchema } from '../../infra/database/dexie/schema/room'; import { DexieRepository } from 'src/app/infra/repository/dexie/dexie-repository.service'; +import { from } from 'rxjs'; @Injectable({ providedIn: 'root' @@ -81,7 +82,7 @@ export class RoomLocalRepository extends DexieRepository { getItemsLive(){ - return liveQuery(() => chatDatabase.room.toArray()); + return from (liveQuery(() => chatDatabase.room.toArray())); } getRoomByIdLive(id: any) { diff --git a/src/app/module/chat/domain/chat-service.service.ts b/src/app/module/chat/domain/chat-service.service.ts index 9ae2252ae..6197cde3e 100644 --- a/src/app/module/chat/domain/chat-service.service.ts +++ b/src/app/module/chat/domain/chat-service.service.ts @@ -25,6 +25,7 @@ import { SendLocalMessagesUseCaseService } from './use-case/messages-send-offlin import { RemoveMemberUseCaseService } from './use-case/member/-use-case.service' import { AddMemberUseCaseService } from './use-case/member-add-use-case.service' import { UpdateRoomByIdUseCaseService } from './use-case/room-update-by-id-use-case.service' +import { MessageMarkAsReadUseCaseService } from './use-case/message/message-mark-as-read-use-case.service' import { GetMessageAttachmentLocallyUseCaseService } from 'src/app/module/chat/domain/use-case/message-get-attachment-localy-use-case.service'; import { GetRoomListUseCaseService } from 'src/app/module/chat/domain/use-case/room-get-list-use-case.service'; import { filter } from 'rxjs/operators'; @@ -36,6 +37,7 @@ import { UserRemoveListInputDTO } from '../data/dto/room/userRemoveListInputDTO' import { AddMemberToRoomInputDTO } from '../data/dto/room/addMemberToRoomInputDto'; import { RoomUpdateInputDTO } from '../data/dto/room/roomUpdateInputDTO'; import { RoomType } from "src/app/module/chat/domain/entity/group"; +import { sendReadAt } from "src/app/module/chat/data/repository/message/message-live-signalr-data-source.service"; export const InstanceId = uuidv4(); @@ -73,7 +75,8 @@ export class ChatServiceService { private UpdateRoomByIdUseCaseService: UpdateRoomByIdUseCaseService, private RemoveMemberUseCaseService: RemoveMemberUseCaseService, private MessageReadAtByIdUseCaseService: MessageReadAtByIdUseCaseService, - private SendLocalMessagesUseCaseService: SendLocalMessagesUseCaseService + private SendLocalMessagesUseCaseService: SendLocalMessagesUseCaseService, + private MessageMarkAsReadUseCaseService: MessageMarkAsReadUseCaseService ) { this.messageLiveSignalRDataSourceService.getMessageDelete() .pipe() @@ -102,7 +105,6 @@ export class ChatServiceService { }) ).subscribe(async (message) => { if(message?.id) { - console.log('create message') this.SocketMessageCreateUseCaseService.execute(message) } }) @@ -115,7 +117,6 @@ export class ChatServiceService { } }) ).subscribe(async (message) => { - console.log('123', message) this.MemberListUpdateStatusUseCaseService.execute(message.data as any) }) @@ -213,6 +214,10 @@ export class ChatServiceService { } + sendReadAt(sendReadAt: sendReadAt) { + return this.MessageMarkAsReadUseCaseService.execute(sendReadAt) + } + listenToIncomingMessage(roomId:string) { return this.ListenMessageByRoomIdNewUseCase.execute({roomId}) } diff --git a/src/app/module/chat/domain/entity/message.ts b/src/app/module/chat/domain/entity/message.ts index 8b3aa1948..bc8280ac4 100644 --- a/src/app/module/chat/domain/entity/message.ts +++ b/src/app/module/chat/domain/entity/message.ts @@ -14,12 +14,18 @@ export const MessageEntitySchema = z.object({ oneShot: z.boolean(), sentAt: z.string().optional(), requireUnlock: z.boolean(), + editedAt: z.string().nullable().optional(), sender: z.object({ wxUserId: z.number(), wxFullName: z.string(), wxeMail: z.string(), userPhoto: z.string(), }), + info: z.array(z.object({ + memberId: z.number(), + readAt: z.string().nullable(), + deliverAt: z.string().nullable() + })).optional(), sending: z.boolean().optional(), attachments: z.array(z.object({ fileType: z.nativeEnum(MessageAttachmentFileType), @@ -47,6 +53,11 @@ export class MessageEntity implements Message { oneShot: boolean = false sentAt: string requireUnlock: boolean = false + info: { + memberId?: number + readAt?: string, + deliverAt?: string + }[] = [] sender: { wxUserId: number, wxFullName: string, diff --git a/src/app/module/chat/domain/service/room-expiration.service.ts b/src/app/module/chat/domain/service/room-expiration.service.ts new file mode 100644 index 000000000..14fcbf1d4 --- /dev/null +++ b/src/app/module/chat/domain/service/room-expiration.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class RoomExpirationService { + + constructor() { } +} diff --git a/src/app/module/chat/domain/use-case/listen-send-message.service.ts b/src/app/module/chat/domain/use-case/listen-send-message.service.ts index e4f260b5e..8d4564411 100644 --- a/src/app/module/chat/domain/use-case/listen-send-message.service.ts +++ b/src/app/module/chat/domain/use-case/listen-send-message.service.ts @@ -14,8 +14,12 @@ export class ListenSendMessageUseCase { execute({roomId}: {roomId: string}) { + console.log('reciee message') return this.MessageSocketRepositoryService.listenToMessages().pipe( - filter((message) => message?.requestId?.startsWith(InstanceId) && message?.roomId == roomId), + filter((message) => { + console.log(message, roomId) + return message?.requestId?.startsWith(InstanceId) && message?.roomId == roomId + }), map(message => message) ) diff --git a/src/app/module/chat/domain/use-case/message-create-use-case.service.ts b/src/app/module/chat/domain/use-case/message-create-use-case.service.ts index 8237dcbca..372278ad3 100644 --- a/src/app/module/chat/domain/use-case/message-create-use-case.service.ts +++ b/src/app/module/chat/domain/use-case/message-create-use-case.service.ts @@ -15,8 +15,9 @@ import { MessageTable } from '../../infra/database/dexie/schema/message'; import { MessageMapper } from '../mapper/messageMapper'; import { SignalRService } from '../../infra/socket/signal-r.service'; import { RoomType } from "src/app/module/chat/domain/entity/group"; - - +import { TracingType, XTracerAsync } from 'src/app/services/monitoring/opentelemetry/tracer'; +import { MemberListLocalRepository } from 'src/app/module/chat/data/repository/member-list-local-repository.service' +import { SessionStore } from 'src/app/store/session.service'; const MessageInputUseCaseSchema = z.object({ memberId: z.number(), roomId: z.string(), @@ -39,11 +40,13 @@ export class MessageCreateUseCaseService { private AttachmentLocalRepositoryService: AttachmentLocalDataSource, private messageLocalDataSourceService: MessageLocalDataSourceService, private messageLiveSignalRDataSourceService: SignalRService, - private messageSocketRepositoryService: MessageSocketRepositoryService + private messageSocketRepositoryService: MessageSocketRepositoryService, + private MemberListLocalRepository: MemberListLocalRepository ) { } - async execute(message: MessageEntity, messageEnum: RoomType) { + @XTracerAsync({name:'MessageCreateUseCaseService', module:'chat', bugPrint: true}) + async execute(message: MessageEntity, messageEnum: RoomType, tracing?: TracingType) { const validation = zodSafeValidation(MessageEntitySchema, message) @@ -93,15 +96,32 @@ export class MessageCreateUseCaseService { //==================== message.sending = true - const DTO = MessageMapper.fromDomain(message, message.requestId) - let sendMessageResult: Result if(messageEnum == RoomType.Group) { + const DTO = MessageMapper.fromDomain(message, message.requestId) sendMessageResult = await this.messageLiveSignalRDataSourceService.sendMessage(DTO) } else { - sendMessageResult = await this.messageSocketRepositoryService.sendDirectMessage(DTO) + + 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.sendDirectMessage(DTO) + } else { + console.log('not found direct users', getRoomMembers.error) + } + } + + } - + // return this sendMessageResult if(sendMessageResult.isOk()) { @@ -127,7 +147,7 @@ export class MessageCreateUseCaseService { await this.messageLocalDataSourceService.update(message.$id, {sending: false, $id: message.$id}) return err('no connection') } - + } } else { @@ -136,6 +156,11 @@ export class MessageCreateUseCaseService { zodErrorList: validation.error.errors, data: message.attachments }) + } else { + Logger.error('failed to send message, validation failed', { + zodErrorList: validation.error.errors, + data: message + }) } } diff --git a/src/app/module/chat/domain/use-case/message-delete-by-id-live-use-case.service.ts b/src/app/module/chat/domain/use-case/message-delete-by-id-live-use-case.service.ts index c9be08715..58cc0e923 100644 --- a/src/app/module/chat/domain/use-case/message-delete-by-id-live-use-case.service.ts +++ b/src/app/module/chat/domain/use-case/message-delete-by-id-live-use-case.service.ts @@ -3,8 +3,7 @@ import { z } from 'zod'; import { SafeValidateSchema, 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'; - - +import { XTracerAsync, TracingType } from 'src/app/services/monitoring/opentelemetry/tracer'; export const MessageDeleteInputDTOSchema = z.object({ requestId: z.string().optional(), roomId: z.string(), @@ -22,8 +21,11 @@ export class MessageDeleteLiveUseCaseService { public repository: MessageSocketRepositoryService ) { } - @SafeValidateSchema(MessageDeleteInputDTOSchema, 'MessageDeleteUseCaseService') - async execute(data: MessageDeleteInputDTO) { + @XTracerAsync({name:'MessageDeleteLiveUseCaseService', module:'chat', bugPrint: true}) + async execute(data: MessageDeleteInputDTO, tracing?: TracingType) { + tracing.log('MessageDeleteLiveUseCaseService payload', { + data: data + }) return this.repository.sendMessageDelete(data) } } diff --git a/src/app/module/chat/domain/use-case/message/message-mark-as-read-use-case.service.ts b/src/app/module/chat/domain/use-case/message/message-mark-as-read-use-case.service.ts new file mode 100644 index 000000000..6f50b444c --- /dev/null +++ b/src/app/module/chat/domain/use-case/message/message-mark-as-read-use-case.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; +import { MessageSocketRepositoryService, sendReadAt } from '../../../data/repository/message/message-live-signalr-data-source.service'; +import { XTracerAsync, TracingType } from 'src/app/services/monitoring/opentelemetry/tracer'; +@Injectable({ + providedIn: 'root' +}) +export class MessageMarkAsReadUseCaseService { + + constructor( + private MessageSocketRepositoryService: MessageSocketRepositoryService, + ) { } + + + @XTracerAsync({name:'MessageMarkAsReadUseCaseService', module:'chat', bugPrint: true}) + async execute(sendReadAt: sendReadAt, tracing?: TracingType) { + return this.MessageSocketRepositoryService.sendReadAt(sendReadAt) + } +} diff --git a/src/app/module/chat/domain/use-case/room-get-list-use-case.service.ts b/src/app/module/chat/domain/use-case/room-get-list-use-case.service.ts index 5908166b5..8afd7eeed 100644 --- a/src/app/module/chat/domain/use-case/room-get-list-use-case.service.ts +++ b/src/app/module/chat/domain/use-case/room-get-list-use-case.service.ts @@ -4,7 +4,7 @@ import { RoomRemoteDataSourceService } from '../../data/repository/room-remote-r import { RoomLocalRepository } from '../../data/repository/room-local-repository.service'; import { captureAndReraiseAsync } from 'src/app/services/decorators/captureAndReraiseAsync'; import { isHttpResponse } from 'src/app/services/http.service'; - +import { CronJobService } from 'src/app/utils/task-scheduler' @Injectable({ providedIn: 'root' }) @@ -14,6 +14,7 @@ export class GetRoomListUseCaseService { private roomRemoteDataSourceService: RoomRemoteDataSourceService, // private roomMemoryDataSourceService: Store, private roomLocalDataSourceService: RoomLocalRepository, + private CronJobService: CronJobService ) { } @captureAndReraiseAsync('RoomRepositoryService/list') @@ -28,6 +29,12 @@ export class GetRoomListUseCaseService { for( const roomData of roomsToInsert) { this.roomLocalDataSourceService.createRoom(roomData.chatRoom) + + if(roomData.chatRoom.expirationDate) { + console.log('room expiration date schedule') + this.CronJobService.createCronJob('remove expired room', new Date(roomData.chatRoom.expirationDate), this.execute) + } + } for( const roomData of roomsToUpdate) { diff --git a/src/app/module/chat/domain/use-case/socket/socket-message-create-use-case.service.ts b/src/app/module/chat/domain/use-case/socket/socket-message-create-use-case.service.ts index 4cd13aaf7..63883ab6a 100644 --- a/src/app/module/chat/domain/use-case/socket/socket-message-create-use-case.service.ts +++ b/src/app/module/chat/domain/use-case/socket/socket-message-create-use-case.service.ts @@ -13,7 +13,7 @@ export class SocketMessageCreateUseCaseService { private messageLocalDataSourceService: MessageLocalDataSourceService, ) { } - @XTracerAsync({name:'Socket-Message-Create-UseCase', bugPrint: true}) + @XTracerAsync({name:'Socket-Message-Create-UseCase', module:'chat', bugPrint: true}) async execute(input: any, tracing?: TracingType) { ParamsValidation(MessageOutPutDataDTOSchema, input, tracing) diff --git a/src/app/module/chat/domain/use-case/socket/socket-message-update-use-case.service.ts b/src/app/module/chat/domain/use-case/socket/socket-message-update-use-case.service.ts index b4167e679..7dc58b98c 100644 --- a/src/app/module/chat/domain/use-case/socket/socket-message-update-use-case.service.ts +++ b/src/app/module/chat/domain/use-case/socket/socket-message-update-use-case.service.ts @@ -15,7 +15,7 @@ export class SocketMessageUpdateUseCaseService { ) { } - @XTracerAsync({name:'Socket-Message-Update-UseCase', bugPrint: true}) + @XTracerAsync({name:'Socket-Message-Update-UseCase', bugPrint: true, module:'chat',}) async execute(data: MessageOutPutDataDTO, tracing?: TracingType) { ParamsValidation(MessageOutPutDataDTOSchema, data, tracing) @@ -30,11 +30,13 @@ export class SocketMessageUpdateUseCaseService { if(result.isOk()) { tracing?.addEvent("Message found") - return await this.messageLocalDataSourceService.update(result.value.$id, incomingMessage) + const updateResult = await this.messageLocalDataSourceService.update(result.value.$id, incomingMessage) + tracing.setAttribute('outcome', 'success') + return updateResult } else { tracing?.addEvent("Message not found") } - tracing.setAttribute('outcome', 'success') + } } diff --git a/src/app/module/chat/domain/use-case/sync-all-room-messages.service.ts b/src/app/module/chat/domain/use-case/sync-all-room-messages.service.ts index ac19c82a6..5a1196086 100644 --- a/src/app/module/chat/domain/use-case/sync-all-room-messages.service.ts +++ b/src/app/module/chat/domain/use-case/sync-all-room-messages.service.ts @@ -3,8 +3,11 @@ import { MessageLocalDataSourceService } from '../../data/repository/message/mes import { messageListDetermineChanges } from '../../data/async/list/rooms/messageListChangedetector'; import { MessageTable } from '../../infra/database/dexie/schema/message'; 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-local-repository.service'; +import { SessionStore } from 'src/app/store/session.service'; +import { Logger } from 'src/app/services/logger/main/service'; @Injectable({ providedIn: 'root' @@ -14,6 +17,7 @@ export class SyncAllRoomMessagesService { constructor( private messageLocalDataSourceService: MessageLocalDataSourceService, private messageRemoteDataSourceService: MessageRemoteDataSourceService, + private MessageSocketRepositoryService: MessageSocketRepositoryService, private roomLocalDataSourceService: RoomLocalRepository, ) { } @@ -35,15 +39,38 @@ export class SyncAllRoomMessagesService { const { addedItems, changedItems, deletedItems } = messageListDetermineChanges(result.value.data, localResult) + for(const message of changedItems) { let clone: MessageTable = message clone.roomId = room.id + this.messageLocalDataSourceService.findOrUpdate(clone) + + const me = message.info.find(e => e.memberId == SessionStore.user.UserId) + + if(!me) { + this.MessageSocketRepositoryService.sendDeliverAt({ + memberId: SessionStore.user.UserId, + messageId: message.id, + roomId: message.roomId, + requestId: "string" + }) + } } for(const message of addedItems) { let clone: MessageTable = message clone.roomId = room.id + + const me = message.info.find(e => e.memberId == SessionStore.user.UserId) + if(!me) { + this.MessageSocketRepositoryService.sendDeliverAt({ + memberId: SessionStore.user.UserId, + messageId: message.id, + roomId: message.roomId, + requestId: "string" + }) + } } @@ -54,6 +81,8 @@ export class SyncAllRoomMessagesService { this.messageLocalDataSourceService.deleteByMessageId(message.id) } + } else { + Logger.error('failed to get room message '+room.id) } } diff --git a/src/app/module/chat/infra/database/dexie/schema/attachment.ts b/src/app/module/chat/infra/database/dexie/schema/attachment.ts index 77c83afdc..9daa9b257 100644 --- a/src/app/module/chat/infra/database/dexie/schema/attachment.ts +++ b/src/app/module/chat/infra/database/dexie/schema/attachment.ts @@ -16,7 +16,7 @@ export const AttachmentTableSchema = z.object({ docId: z.string().optional(), mimeType: z.string().optional(), id: z.string().optional(), - description: z.string().optional(), + description: z.string().optional() }) export type AttachmentTable = z.infer diff --git a/src/app/module/chat/infra/database/dexie/schema/message.ts b/src/app/module/chat/infra/database/dexie/schema/message.ts index 37e521d7d..8a8ee2109 100644 --- a/src/app/module/chat/infra/database/dexie/schema/message.ts +++ b/src/app/module/chat/infra/database/dexie/schema/message.ts @@ -11,6 +11,7 @@ export const MessageTableSchema = z.object({ canEdit: z.boolean(), oneShot: z.boolean(), sentAt: z.string().optional(), + editedAt: z.string().nullable().optional(), requireUnlock: z.boolean(), sender: z.object({ wxUserId: z.number(), @@ -26,7 +27,11 @@ export const MessageTableSchema = z.object({ reaction: z.string(), sender: z.object({}), }).array().optional(), - info: z.array(z.object({})).optional(), + info: z.array(z.object({ + memberId: z.number(), + readAt: z.string().nullable(), + deliverAt: z.string().nullable() + })).optional(), attachments: z.array(z.object({ fileType: z.nativeEnum(MessageAttachmentFileType), source: z.nativeEnum(MessageAttachmentSource), diff --git a/src/app/module/chat/infra/socket/signalR.ts b/src/app/module/chat/infra/socket/signalR.ts index 34e1246e1..61cac1359 100644 --- a/src/app/module/chat/infra/socket/signalR.ts +++ b/src/app/module/chat/infra/socket/signalR.ts @@ -157,7 +157,7 @@ export class SignalRConnection { const requestId = uuidv4() if(this.connectionStateSubject.value == true) { - console.log('send typing', data) + // console.log('send typing', data) try { this.hubConnection.invoke("Typing", data) @@ -276,7 +276,6 @@ export class SignalRConnection { }); this.hubConnection.on('TypingMessage', (_typing: UserTypingDTO) => { - console.log('Typing', _typing) this.typingSubject.next(_typing); this.sendDataSubject.next({ method: 'ReceiveMessage', @@ -285,7 +284,6 @@ export class SignalRConnection { }); this.hubConnection.on('AvailableUsers', (data: any) => { - console.log('AvailableUsers', data) this.typingSubject.next(data); this.sendDataSubject.next({ method: 'AvailableUsers', diff --git a/src/app/services/logger/main/service.ts b/src/app/services/logger/main/service.ts index ec6ddca93..8cf14216b 100644 --- a/src/app/services/logger/main/service.ts +++ b/src/app/services/logger/main/service.ts @@ -1,5 +1,11 @@ // import { DateUtils } from './date'; +import { environment } from "src/environments/environment"; +import { openTelemetryLogging } from "../../monitoring/opentelemetry/logging"; +import { SessionStore } from "src/app/store/session.service"; +import { DeviceInfo } from "@capacitor/device"; +import { Device } from '@capacitor/device'; + export type MessageType = { message: string; context?: string; @@ -15,6 +21,12 @@ function getCurrentTime() { return `${hours}:${minutes}:${seconds}.${milliseconds}`; } +let device: DeviceInfo; + +Device.getInfo().then(e => { + device = e +}); + export class Logger { constructor() {} @@ -25,6 +37,25 @@ export class Logger { 'color: #00897B', // CSS Style Object.assign(obj, { createdAt: getCurrentTime(), message }) ); + + + if(environment.apiURL != 'https://gdqas-api.oapr.gov.ao/api/') { + openTelemetryLogging.send({ + type: 'graylog', + spanContext: null, + payload: { + message: message, + object: { + ...obj, + spanId: null, + name, + user: SessionStore?.user?.FullName, + device_name: device?.name || device?.model, + commit_date: environment.version.lastCommitTime, + } + } + }) + } } static debug(message: string, obj = {}): void { @@ -33,6 +64,24 @@ export class Logger { 'color: #039BE5', // CSS Style Object.assign(obj, {createdAt: getCurrentTime(), message }) ); + + if(environment.apiURL != 'https://gdqas-api.oapr.gov.ao/api/') { + openTelemetryLogging.send({ + type: 'graylog', + spanContext: null, + payload: { + message: message, + object: { + ...obj, + spanId: null, + name, + user: SessionStore?.user?.FullName, + device_name: device?.name || device?.model, + commit_date: environment.version.lastCommitTime, + } + } + }) + } } static info(message: string, obj = {}): void { @@ -44,6 +93,24 @@ export class Logger { obj, '\n', ); + + if(environment.apiURL != 'https://gdqas-api.oapr.gov.ao/api/') { + openTelemetryLogging.send({ + type: 'graylog', + spanContext: null, + payload: { + message: message, + object: { + ...obj, + spanId: null, + name, + user: SessionStore?.user?.FullName, + device_name: device?.name || device?.model, + commit_date: environment.version.lastCommitTime, + } + } + }) + } } static warn(message: string, obj = {}): void { @@ -55,6 +122,24 @@ export class Logger { obj, '\n', ); + + if(environment.apiURL != 'https://gdqas-api.oapr.gov.ao/api/') { + openTelemetryLogging.send({ + type: 'graylog', + spanContext: null, + payload: { + message: message, + object: { + ...obj, + spanId: null, + name, + user: SessionStore?.user?.FullName, + device_name: device?.name || device?.model, + commit_date: environment.version.lastCommitTime, + } + } + }) + } } static error(message?: string, obj = {}): void { @@ -67,6 +152,24 @@ export class Logger { obj, '\n', ); + + if(environment.apiURL != 'https://gdqas-api.oapr.gov.ao/api/') { + openTelemetryLogging.send({ + type: 'graylog', + spanContext: null, + payload: { + message: message, + object: { + ...obj, + spanId: null, + name, + user: SessionStore?.user?.FullName, + device_name: device?.name || device?.model, + commit_date: environment.version.lastCommitTime, + } + } + }) + } } fatal(error?: any, message?: string, context?: string): void {} diff --git a/src/app/services/monitoring/opentelemetry/tracer.ts b/src/app/services/monitoring/opentelemetry/tracer.ts index 128e0c384..7421b6195 100644 --- a/src/app/services/monitoring/opentelemetry/tracer.ts +++ b/src/app/services/monitoring/opentelemetry/tracer.ts @@ -1,7 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { Tracer, Span } from '@opentelemetry/sdk-trace-base'; -import { OpentelemetryAgendaProvider, OpentelemetryInterceptorProvider, OpentelemetryLogging, OpentelemetryNotificationProvider } from './opentelemetry'; +import { OpentelemetryAgendaProvider, OpentelemetryChatProvider, OpentelemetryInterceptorProvider, OpentelemetryLogging, OpentelemetryNotificationProvider } from './opentelemetry'; import { Device, DeviceInfo } from '@capacitor/device'; import { SessionStore } from 'src/app/store/session.service'; import { environment } from 'src/environments/environment'; @@ -12,7 +12,7 @@ import { } from '@opentelemetry/api'; const tracerInstance = OpentelemetryAgendaProvider.getTracer('example-tracer-hole', '111', {}) const tracerNotificationInstance = OpentelemetryNotificationProvider.getTracer('example-tracer-hole', '111', {}) - +const tracerChat = OpentelemetryChatProvider.getTracer('OpentelemetryChatProvider','some' ,{}) let device: DeviceInfo; Device.getInfo().then(e => { @@ -45,6 +45,8 @@ const createTracingInstance = ({bugPrint, name, module, autoFinish}): TracingTyp if(module == 'notification') { _tracerInstance = tracerNotificationInstance + } else if (module == 'chat') { + _tracerInstance = tracerChat } else { _tracerInstance = tracerInstance } @@ -184,6 +186,9 @@ export function XTracerAsync({ name, bugPrint, module = null, autoFinish = true, return result } catch (e) { tracing.setAttribute('catch', 'true') + tracing.log("cath", { + error: e + }) if(autoFinish) { setTimeout(tracing.finish , daley) @@ -226,6 +231,9 @@ export function XTracer({ name, bugPrint, module, autoFinish = true, daley =0 }) } catch (e) { tracing.setAttribute('catch', 'true') + tracing.log("cath", { + error: e + }) if(autoFinish) { setTimeout(tracing.finish , daley) diff --git a/src/app/ui/chat/chat.page.html b/src/app/ui/chat/chat.page.html index c03347fd8..bb9812741 100644 --- a/src/app/ui/chat/chat.page.html +++ b/src/app/ui/chat/chat.page.html @@ -67,6 +67,9 @@ +
+ {{ expirationDate[room.id] !== null ? expirationDate[room.id] + ' seconds left' : 'No expiration' }} +
+
+ {{ expirationDate[room.id] !== null ? expirationDate[room.id] + ' seconds left' : 'No expiration' }} +
+ +
+ + + + + +
@@ -147,7 +158,7 @@ - +