diff --git a/src/app/infra/repository/dexie/dexie-repository.service.ts b/src/app/infra/repository/dexie/dexie-repository.service.ts index 40ac02bb9..6fa47ee3b 100644 --- a/src/app/infra/repository/dexie/dexie-repository.service.ts +++ b/src/app/infra/repository/dexie/dexie-repository.service.ts @@ -1,26 +1,47 @@ import { Result, ok, err, ResultAsync } from 'neverthrow'; import { Dexie, EntityTable, liveQuery, Observable } from 'Dexie'; +import { ZodError, ZodObject, ZodSchema } from 'zod'; // Define a type for the Result of repository operations -type RepositoryResult = Result; +type RepositoryResult = Result>; + export class DexieRepository { private table: EntityTable; + private ZodSchema: ZodSchema + private ZodPartialSchema: ZodSchema - constructor(table: EntityTable) { + constructor(table: EntityTable, ZodSchema: ZodSchema) { this.table = table as any + this.ZodSchema = ZodSchema + this.ZodPartialSchema = (ZodSchema as ZodObject).partial() as any; } - async insert(document: T): Promise> { - try { - const id = await this.table.add(document as any); - return ok(id); - } catch (error) { - return err(new Error('Failed to insert document: ' + error.message)); + async insert(document: T): Promise> { + + const dataValidation = this.ZodSchema.safeParse(document) + + if(dataValidation.success) { + try { + const id = await this.table.add(dataValidation.data); + return ok(id); + } catch (error) { + return err(new Error('Failed to insert document: ' + error.message)); + } + } else { + return err((dataValidation as unknown as ZodError)) } } - async insertMany(documents: T[]): Promise> { + async insertMany(documents: T[]): Promise>> { + // Validate each document + const schema = this.ZodSchema.array() + + const validationResult = schema.safeParse(documents) + if(!validationResult.success) { + return err((validationResult as unknown as ZodError)) + } + try { const ids = await this.table.bulkAdd(documents as any); return ok(ids); @@ -30,15 +51,22 @@ export class DexieRepository { } async update(id: any, updatedDocument: Partial) { - try { - const updatedCount = await this.table.update(id, updatedDocument as any); - return ok(updatedCount); - } catch (error) { - return err(new Error('Failed to update document: ' + error.message)); + + const dataValidation = this.ZodPartialSchema.safeParse(document) + + if(dataValidation.success) { + try { + const updatedCount = await this.table.update(id, dataValidation.data); + return ok(updatedCount); + } catch (error) { + return err(new Error('Failed to update document: ' + error.message)); + } + } else { + return err((dataValidation as unknown as ZodError)) } } - async delete(id: any): Promise> { + async delete(id: any): Promise> { try { await this.table.delete(id); return ok(undefined); @@ -65,7 +93,7 @@ export class DexieRepository { } } - async findOne(filter: Object): Promise> { + async findOne(filter: Object): Promise> { try { const document = await this.table.where(filter).first(); return ok(document); @@ -74,7 +102,7 @@ export class DexieRepository { } } - async findAll(): Promise> { + async findAll(): Promise> { try { const documents = await this.table.toArray(); return ok(documents); @@ -83,7 +111,7 @@ export class DexieRepository { } } - async count(filter?: Object): Promise> { + async count(filter?: Object): Promise> { try { const count = filter ? await this.table.where(filter).count() : await this.table.count(); return ok(count); diff --git a/src/app/module/chat/data/data-source/attachment/attachment-local-data-source.service.ts b/src/app/module/chat/data/data-source/attachment/attachment-local-data-source.service.ts new file mode 100644 index 000000000..1b597ee7c --- /dev/null +++ b/src/app/module/chat/data/data-source/attachment/attachment-local-data-source.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs'; +import { DexieRepository } from 'src/app/infra/repository/dexie/dexie-repository.service'; +import { chatDatabase } from '../../../infra/database/dexie/service'; +import { AttachmentTable, AttachmentTableSchema } from '../../../infra/database/dexie/schema/attachment'; + +@Injectable({ + providedIn: 'root' +}) +export class AttachmentLocalDataSource extends DexieRepository { + + messageSubject = new Subject(); + + constructor() { + super(chatDatabase.attachment, AttachmentTableSchema) + } + +} + diff --git a/src/app/module/chat/data/data-source/attachment/attachment-remote-data-source.service.ts b/src/app/module/chat/data/data-source/attachment/attachment-remote-data-source.service.ts new file mode 100644 index 000000000..5fca7dfe1 --- /dev/null +++ b/src/app/module/chat/data/data-source/attachment/attachment-remote-data-source.service.ts @@ -0,0 +1,21 @@ +import { Injectable, Input } from '@angular/core'; +import { HttpService } from 'src/app/services/http.service'; +import { DataSourceReturn } from 'src/app/services/Repositorys/type'; +import { MessageOutPutDTO } from '../../dto/message/messageOutputDTO'; +import { MessageAttachmentByMessageIdInput } from '../../../domain/use-case/message-attachment-by-message-id.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AttachmentRemoteDataSourceService { + private baseUrl = 'https://gdapi-dev.dyndns.info/stage/api/v2/Chat'; // Your base URL + + constructor( + private httpService: HttpService + ) { } + + async getAttachment(Input: MessageAttachmentByMessageIdInput): DataSourceReturn { + return await this.httpService.get(`${this.baseUrl}/attachment/${Input.id}`, { responseType: 'blob' }); + } + +} diff --git a/src/app/module/chat/data/data-source/attachment/message-local-data-source.service.ts b/src/app/module/chat/data/data-source/attachment/message-local-data-source.service.ts deleted file mode 100644 index 1cd4875c9..000000000 --- a/src/app/module/chat/data/data-source/attachment/message-local-data-source.service.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Observable, Subject } from 'rxjs'; -import { DexieRepository } from 'src/app/infra/repository/dexie/dexie-repository.service'; -import { chatDatabase } from '../../../infra/database/dexie/service'; -import { Observable as DexieObservable, PromiseExtended } from 'Dexie'; -import { AttachmentTable } from '../../../infra/database/dexie/schema/attachment'; - -@Injectable({ - providedIn: 'root' -}) -export class AttachmentLocalDataSourceService extends DexieRepository { - - messageSubject = new Subject(); - - constructor() { - super(chatDatabase.attachment) - } - - -} - diff --git a/src/app/module/chat/data/data-source/member-list/member-list-local-data-source.service.ts b/src/app/module/chat/data/data-source/member-list/member-list-local-data-source.service.ts index f99e80074..5abe78f16 100644 --- a/src/app/module/chat/data/data-source/member-list/member-list-local-data-source.service.ts +++ b/src/app/module/chat/data/data-source/member-list/member-list-local-data-source.service.ts @@ -18,7 +18,6 @@ const MemberTableSchema = z.object({ joinAt: z.string(), status: z.string() }) - export type IMemberTable = z.infer type IMemberTableSchema = EntityTable @@ -37,7 +36,7 @@ roomMemberList.version(1).stores({ export class MemberListLocalDataSourceService extends DexieRepository { constructor() { - super(roomMemberList.memberList); + super(roomMemberList.memberList, MemberTableSchema); // messageDataSource.message.hook('creating', (primKey, obj, trans) => { // // const newMessage = await trans.table('message').get(primKey); diff --git a/src/app/module/chat/data/data-source/message/message-local-data-source.service.ts b/src/app/module/chat/data/data-source/message/message-local-data-source.service.ts index 9111f7697..e72401f3c 100644 --- a/src/app/module/chat/data/data-source/message/message-local-data-source.service.ts +++ b/src/app/module/chat/data/data-source/message/message-local-data-source.service.ts @@ -5,7 +5,7 @@ import { Observable, Subject } from 'rxjs'; import { filter } from 'rxjs/operators'; import { MessageEntity } from '../../../domain/entity/message'; import { DexieRepository } from 'src/app/infra/repository/dexie/dexie-repository.service'; -import { MessageTable } from 'src/app/module/chat/infra/database/dexie/schema/message'; +import { MessageTable, MessageTableSchema } from 'src/app/module/chat/infra/database/dexie/schema/message'; import { chatDatabase } from '../../../infra/database/dexie/service'; import { Observable as DexieObservable, PromiseExtended } from 'Dexie'; @@ -17,7 +17,7 @@ export class MessageLocalDataSourceService extends DexieRepository messageSubject = new Subject(); constructor() { - super(chatDatabase.message) + super(chatDatabase.message, MessageTableSchema) } async setAllSenderToFalse() { @@ -63,16 +63,24 @@ export class MessageLocalDataSourceService extends DexieRepository async sendMessage(data: MessageTable) { - (data as MessageTable).sending = true + const dataValidation = MessageTableSchema.safeParse(data) + if(dataValidation.success) { - try { - const result = await chatDatabase.message.add(data) - this.messageSubject.next({roomId: data.roomId}); - return ok(result as number) - } catch (e) { - return err(false) + const safeData = dataValidation.data + safeData.sending = true + + try { + const result = await chatDatabase.message.add(safeData) + this.messageSubject.next({roomId: safeData.roomId}); + return ok(result as number) + } catch (e) { + return err(false) + } + + } else { + console.log(dataValidation) + return err(dataValidation) } - } diff --git a/src/app/module/chat/data/repository/attachment-repository.service.ts b/src/app/module/chat/data/repository/attachment-repository.service.ts new file mode 100644 index 000000000..5dbb93e8e --- /dev/null +++ b/src/app/module/chat/data/repository/attachment-repository.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { AttachmentLocalDataSource } from 'src/app/module/chat/data/data-source/attachment/attachment-local-data-source.service' +import { AttachmentTable } from '../../infra/database/dexie/schema/attachment'; + +@Injectable({ + providedIn: 'root' +}) +export class AttachmentRepositoryService { + + constructor( + private AttachmentLocalDataSourceService: AttachmentLocalDataSource + ) { } + + create(data: AttachmentTable) { + return this.AttachmentLocalDataSourceService.insert(data) + } + + get findOne() { + return this.AttachmentLocalDataSourceService.findOne + } + + get insert() { + return this.AttachmentLocalDataSourceService.insert + } + + get update() { + return this.AttachmentLocalDataSourceService.update + } + + +} diff --git a/src/app/module/chat/data/repository/message-respository.service.ts b/src/app/module/chat/data/repository/message-respository.service.ts index 8f2ee3462..742904faa 100644 --- a/src/app/module/chat/data/repository/message-respository.service.ts +++ b/src/app/module/chat/data/repository/message-respository.service.ts @@ -14,8 +14,7 @@ import { MessageOutPutDataDTO } from '../dto/message/messageOutputDTO'; import { MessageTable } from 'src/app/module/chat/infra/database/dexie/schema/message'; import { MessageLocalDataSourceService } from '../data-source/message/message-local-data-source.service'; import { MessageLiveDataSourceService } from '../data-source/message/message-live-signalr-data-source.service'; -import { AttachmentLocalDataSourceService } from 'src/app/module/chat/data/data-source/attachment/message-local-data-source.service' -import { Subject } from 'rxjs'; +import { AttachmentLocalDataSource } from 'src/app/module/chat/data/data-source/attachment/attachment-local-data-source.service' @Injectable({ providedIn: 'root' @@ -27,22 +26,13 @@ export class MessageRepositoryService { private messageLiveDataSourceService: MessageLiveDataSourceService, private messageLiveSignalRDataSourceService: SignalRService, private messageLocalDataSourceService: MessageLocalDataSourceService, - private AttachmentLocalDataSourceService: AttachmentLocalDataSourceService + private AttachmentLocalDataSourceService: AttachmentLocalDataSource ) {} async createMessageLocally(entity: MessageEntity) { - const requestId = InstanceId +'@'+ uuidv4(); - const roomId = entity.roomId - - return await this.messageLocalDataSourceService.sendMessage(entity) - - } - - - async createMessage(entity: MessageEntity) { //const requestId = InstanceId +'@'+ uuidv4(); - + const localActionResult = await this.messageLocalDataSourceService.sendMessage(entity) return localActionResult.map(e => { @@ -52,42 +42,32 @@ export class MessageRepositoryService { } async sendMessage(entity: MessageEntity) { - const messageSubject = new Subject(); - - const roomId = entity.roomId entity.sending = true - const localActionResult = await this.messageLocalDataSourceService.sendMessage(entity) - if(localActionResult.isOk()) { - const DTO = MessageMapper.fromDomain(entity, entity.requestId) - const sendMessageResult = await this.messageLiveSignalRDataSourceService.sendMessage(DTO) - // return this sendMessageResult + const DTO = MessageMapper.fromDomain(entity, entity.requestId) + const sendMessageResult = await this.messageLiveSignalRDataSourceService.sendMessage(DTO) + // return this sendMessageResult - if(sendMessageResult.isOk()) { + if(sendMessageResult.isOk()) { - if(sendMessageResult.value.sender == undefined || sendMessageResult.value.sender == null) { + if(sendMessageResult.value.sender == undefined || sendMessageResult.value.sender == null) { - delete sendMessageResult.value.sender - } - - let clone: MessageTable = { - ...sendMessageResult.value, - id: sendMessageResult.value.id, - $id : localActionResult.value - } - - // console.log('sendMessageResult.value', sendMessageResult.value) - // this.attachment(sendMessageResult.value.attachments[0].id) - return this.messageLocalDataSourceService.update(localActionResult.value, {...clone, sending: false, roomId: entity.roomId}) - } else { - await this.messageLocalDataSourceService.update(localActionResult.value, {sending: false, $id: localActionResult.value}) - return err('no connection') + delete sendMessageResult.value.sender } + let clone: MessageTable = { + ...sendMessageResult.value, + id: sendMessageResult.value.id, + $id : entity.$id + } + + return this.messageLocalDataSourceService.update(entity.$id, {...clone, sending: false, roomId: entity.roomId}) } else { - // return this.messageLocalDataSourceService.update({sending: false}) + await this.messageLocalDataSourceService.update(entity.$id, {sending: false, $id: entity.$id}) + return err('no connection') } + } diff --git a/src/app/module/chat/domain/chat-service.service.ts b/src/app/module/chat/domain/chat-service.service.ts index ece7ef4ab..584bac83d 100644 --- a/src/app/module/chat/domain/chat-service.service.ts +++ b/src/app/module/chat/domain/chat-service.service.ts @@ -17,6 +17,7 @@ import { ListenSendMessageUseCase } from './use-case/listen-send-message.service import { filter } from 'rxjs/operators'; import { v4 as uuidv4 } from 'uuid' import { MessageEntity } from './entity/message'; +import { MessageAttachmentByMessageIdInput, MessageAttachmentByMessageIdUseCase } from './use-case/message-attachment-by-message-id.service'; export const InstanceId = uuidv4(); @@ -40,7 +41,8 @@ export class ChatServiceService { private ListenMessageByRoomIdNewUseCase: ListenMessageByRoomIdNewUseCase, private ListenMessageDeleteService: ListenMessageDeleteByRoomIdService, private ListenMessageUpdateByRoomIdUseCase: ListenMessageUpdateByRoomIdUseCase, - private ListenSendMessageUseCase: ListenSendMessageUseCase + private ListenSendMessageUseCase: ListenSendMessageUseCase, + private MessageAttachmentByMessageIdService: MessageAttachmentByMessageIdUseCase ) { this.messageLiveSignalRDataSourceService.getMessageDelete() .pipe() @@ -117,6 +119,10 @@ export class ChatServiceService { return this.MessageCreateUseCaseService.execute(input); } + getMessageAttachmentByMessageId(input: MessageAttachmentByMessageIdInput) { + return this.MessageAttachmentByMessageIdService.execute(input) + } + 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 7d82d6ba5..dee96cf08 100644 --- a/src/app/module/chat/domain/entity/message.ts +++ b/src/app/module/chat/domain/entity/message.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { MessageAttachmentFileType, MessageAttachmentSource } from "../../data/dto/message/messageOutputDTO"; +import { SafeResourceUrl } from "@angular/platform-browser"; const MessageEntitySchema = z.object({ $id: z.any().optional(), @@ -55,6 +56,7 @@ export class MessageEntity implements Message { sendAttemp = 0 attachments: { + safeFile?: SafeResourceUrl; fileType: MessageAttachmentFileType, source: MessageAttachmentSource, file?: string, @@ -63,11 +65,7 @@ export class MessageEntity implements Message { docId?: string, mimeType?: string, description?: string - }[] = [] - - attachmentsSource: { - id: string, - file: string + id?: string }[] = [] reactions = [] diff --git a/src/app/module/chat/domain/use-case/message-attachment-by-message-id.service.ts b/src/app/module/chat/domain/use-case/message-attachment-by-message-id.service.ts new file mode 100644 index 000000000..aec4e5be0 --- /dev/null +++ b/src/app/module/chat/domain/use-case/message-attachment-by-message-id.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; +import { z } from 'zod'; +import { AttachmentRemoteDataSourceService } from 'src/app/module/chat/data/data-source/attachment/attachment-remote-data-source.service' +import { AttachmentLocalDataSource } from 'src/app/module/chat/data/data-source/attachment/attachment-local-data-source.service' +import { convertBlobToDataURL } from 'src/app/utils/ToBase64'; +import { Result } from 'neverthrow'; + +const MessageAttachmentByMessageIdSchema = z.object({ + $messageId: z.number(), + id: z.string() +}) + +export type MessageAttachmentByMessageIdInput = z.infer + +@Injectable({ + providedIn: 'root' +}) +export class MessageAttachmentByMessageIdUseCase { + + constructor( + private AttachmentRemoteDataSourceService: AttachmentRemoteDataSourceService, + private AttachmentLocalDataSource: AttachmentLocalDataSource + ) { } + + async execute(input: MessageAttachmentByMessageIdInput): Promise> { + + const getLocalAttachment = await this.AttachmentLocalDataSource.findOne({ + $messageId: input.$messageId + }) + + if(getLocalAttachment.isOk() && getLocalAttachment.value) { + if(getLocalAttachment.value) { + console.log('found local', getLocalAttachment.value) + return getLocalAttachment.map(e => e.file) + } + } else { + const result = await this.AttachmentRemoteDataSourceService.getAttachment(input) + return result.asyncMap(async (e) => { + + const dataUrl = await convertBlobToDataURL(e) + + this.AttachmentLocalDataSource.insert({ + $messageId: input.$messageId, + id: input.id, + file: dataUrl + }) + + return dataUrl + }) + } + + } +} 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 37039cb03..31f98b95c 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 @@ -1,10 +1,12 @@ import { Injectable } from '@angular/core'; import { MessageEntity } from '../entity/message'; -import { SessionStore } from 'src/app/store/session.service'; import { MessageRepositoryService } from "src/app/module/chat/data/repository/message-respository.service" +import { AttachmentRepositoryService } from "src/app/module/chat/data/repository/attachment-repository.service" import { z } from 'zod'; import { v4 as uuidv4 } from 'uuid' import { InstanceId } from '../chat-service.service'; +import { createDataURL } from 'src/app/utils/ToBase64'; +import { DomSanitizer } from '@angular/platform-browser'; const MessageInputUseCaseSchema = z.object({ memberId: z.number(), @@ -20,17 +22,55 @@ export type MessageInputUseCase = z.infer< typeof MessageInputUseCaseSchema> export class MessageCreateUseCaseService { constructor( - private MessageRepositoryService: MessageRepositoryService + private MessageRepositoryService: MessageRepositoryService, + private AttachmentRepositoryService: AttachmentRepositoryService, + private sanitizer: DomSanitizer ) { } - async execute(input: MessageEntity) { + async execute(message: MessageEntity) { - input.sendAttemp++; + message.sendAttemp++; - input.requestId = InstanceId +'@'+ uuidv4(); + message.requestId = InstanceId +'@'+ uuidv4(); - const result = await this.MessageRepositoryService.sendMessage(input) + const createMessageLocally = await this.MessageRepositoryService.createMessageLocally(message) + + if(createMessageLocally.isOk()) { + + console.log('==========================',message); + if(message.hasAttachment) { + + for (const attachment of message.attachments) { + + const createAttachmentLocally = this.AttachmentRepositoryService.create({ + $messageId: createMessageLocally.value.$id, + file: createDataURL(attachment.file, attachment.mimeType) + }) + + attachment.safeFile = createDataURL(attachment.file, attachment.mimeType) + } + + } + + const sendToServer = await this.MessageRepositoryService.sendMessage(message) + + + // if(sendToServer.isOk()) { + // for (const attachment of message.attachments) { + + // const attachment = await this.AttachmentRepositoryService.findOne({ + // $messageId: createMessageLocally.value.$id + // }) + // } + + // } + + + return sendToServer + } + + const result = await this.MessageRepositoryService.sendMessage(message) return result } diff --git a/src/app/module/chat/domain/use-case/message-send-use-case.service.ts b/src/app/module/chat/domain/use-case/message-send-use-case.service.ts index 3c3405cab..acf14e388 100644 --- a/src/app/module/chat/domain/use-case/message-send-use-case.service.ts +++ b/src/app/module/chat/domain/use-case/message-send-use-case.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@angular/core'; import { MessageEntity } from '../entity/message'; -import { SessionStore } from 'src/app/store/session.service'; import { MessageRepositoryService } from "src/app/module/chat/data/repository/message-respository.service" import { z } from 'zod'; @@ -24,12 +23,12 @@ export class MessageCreateUseCaseService { async execute(input: MessageEntity) { - const result = await this.MessageRepositoryService.createMessage(input) + // const result = await this.MessageRepositoryService.(input) - if(result.isOk()) { + // if(result.isOk()) { - } + // } - return result + // return result } } 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 e88219b77..c7a830436 100644 --- a/src/app/module/chat/infra/database/dexie/schema/attachment.ts +++ b/src/app/module/chat/infra/database/dexie/schema/attachment.ts @@ -1,13 +1,15 @@ import { z } from "zod"; import { EntityTable } from 'Dexie'; +import { zodDataUrlSchema } from "src/app/utils/zod"; export const AttachmentTableSchema = z.object({ - id: z.string(), - $id: z.string(), - messageId: z.string(), - file: z.string(), + id: z.string().optional(), // attachment id + $id: z.number().optional(), // local id + $messageId: z.number(), + attachmentId: z.string().optional(), + file: zodDataUrlSchema, }) export type AttachmentTable = z.infer export type DexieAttachmentsTableSchema = EntityTable; -export const AttachmentTableColumn = '++$id, id, messageId, file' +export const AttachmentTableColumn = '++$id, id, $messageId, messageId, file' 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 f84732dd0..976cff357 100644 --- a/src/app/module/chat/infra/database/dexie/schema/message.ts +++ b/src/app/module/chat/infra/database/dexie/schema/message.ts @@ -2,11 +2,11 @@ import { nativeEnum, z } from "zod"; import { EntityTable } from 'Dexie'; import { MessageAttachmentFileType, MessageAttachmentSource } from "src/app/module/chat/data/dto/message/messageOutputDTO"; -export const MessageTable = z.object({ +export const MessageTableSchema = z.object({ $id: z.number().optional(), id: z.string().optional(), roomId: z.string().uuid(), - message: z.string().nullable(), + message: z.string().nullable().optional(), messageType: z.number(), canEdit: z.boolean(), oneShot: z.boolean(), @@ -29,14 +29,13 @@ export const MessageTable = z.object({ attachments: z.array(z.object({ fileType: z.nativeEnum(MessageAttachmentFileType), source: z.nativeEnum(MessageAttachmentSource), - file: z.string().optional(), fileName: z.string().optional(), applicationId: z.string().optional(), docId: z.string().optional() })).optional() }) -export type MessageTable = z.infer +export type MessageTable = z.infer export type DexieMessageTable = EntityTable; export const messageTableColumn = '++$id, id, roomId, senderId, message, messageType, canEdit, oneShot, requireUnlock' diff --git a/src/app/services/http.service.ts b/src/app/services/http.service.ts index 7faabb81e..9aa3dc210 100644 --- a/src/app/services/http.service.ts +++ b/src/app/services/http.service.ts @@ -19,9 +19,9 @@ export class HttpService { } } - async get(url: string): Promise> { + async get(url: string, options ={}): Promise> { try { - const result = await this.http.get(url).toPromise() + const result = await this.http.get(url, options).toPromise() return ok (result as T) } catch (e) { return err(e as HttpErrorResponse) diff --git a/src/app/shared/chat/messages/messages.page.html b/src/app/shared/chat/messages/messages.page.html index 4dd35de41..7f0526dbd 100644 --- a/src/app/shared/chat/messages/messages.page.html +++ b/src/app/shared/chat/messages/messages.page.html @@ -55,7 +55,7 @@ {{ message.message }} -
+
@@ -66,6 +66,10 @@
+
+ +
+
@@ -107,13 +111,6 @@ - - @@ -123,25 +120,6 @@ - - - - - diff --git a/src/app/shared/chat/messages/messages.page.ts b/src/app/shared/chat/messages/messages.page.ts index 363229920..977b5cfd1 100644 --- a/src/app/shared/chat/messages/messages.page.ts +++ b/src/app/shared/chat/messages/messages.page.ts @@ -37,7 +37,7 @@ import { MessageEntity } from 'src/app/module/chat/domain/entity/message'; import { MemberTable } from 'src/app/module/chat/infra/database/dexie/schema/members'; import { TypingTable } from 'src/app/module/chat/infra/database/dexie/schema/typing'; import { MessageAttachmentFileType, MessageAttachmentSource } from 'src/app/module/chat/data/dto/message/messageOutputDTO'; -import { JSFileToBase64 } from 'src/app/utils/ToBase64'; +import { JSFileToDataUrl } from 'src/app/utils/ToBase64'; import { CameraService } from 'src/app/infra/camera/camera.service' import { compressImageBase64 } from '../../../utils/imageCompressore'; import { FilePickerWebService } from 'src/app/infra/file-picker/web/file-picker-web.service' @@ -192,8 +192,35 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy async getMessages() { this.messages1[this.roomId] = [] - const messages = await this.messageRepositoryService.getItems(this.roomId) + let messages = await this.messageRepositoryService.getItems(this.roomId) + this.messages1[this.roomId].unshift(...messages) + this.loadAttachment() + + setTimeout(() => { + this.scrollToBottomClicked() + }, 100) + + } + + async loadAttachment() { + for(const message of this.messages1[this.roomId]) { + if(message.hasAttachment) { + + const result = await this.chatServiceService.getMessageAttachmentByMessageId({ + $messageId: message.$id, + id: message.attachments[0].id + }) + + if(result.isOk()) { + + message.attachments[0].safeFile = result.value + + } + } + + } + } listenToIncomingMessage() { @@ -226,7 +253,6 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy this.messageUpdateSubject?.unsubscribe(); this.messageUpdateSubject = this.chatServiceService.listenToUpdateMessage(this.roomId).subscribe((updateMessage) => { - console.log('update message', updateMessage); const index = this.messages1[this.roomId].findIndex(e => e?.id === updateMessage.id); // Use triple equals for comparison @@ -254,6 +280,14 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy this.messages1[this.roomId][index].id = updateMessage.id + let attachmentIndex = 0; + + for(const message of updateMessage.attachments) { + console.log('set attachmen id', message) + this.messages1[this.roomId][index].attachments[attachmentIndex].id = message.id + attachmentIndex++; + } + } else { // console.log('message not found'); } @@ -856,7 +890,6 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy fileType: MessageAttachmentFileType.Image, mimeType: 'image/'+file.value.format, description: '' - }] this.messages1[this.roomId].push(message) @@ -891,7 +924,7 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy console.log('FILE rigth?', file) - let fileBase64 = await JSFileToBase64(file.value); + let fileBase64 = await JSFileToDataUrl(file.value); if(fileBase64.isOk()) { diff --git a/src/app/utils/ToBase64.ts b/src/app/utils/ToBase64.ts index b3ed42c50..f15f3ee5e 100644 --- a/src/app/utils/ToBase64.ts +++ b/src/app/utils/ToBase64.ts @@ -11,11 +11,11 @@ function getFileReader(): FileReader { } /** - * Converts a `File` object to a Base64 encoded string. + * Converts a `File` object to a data url encoded string. * @param {File} file - The file to be converted. * @returns {Promise>} A promise that resolves with a `Result` object containing either the Base64 encoded string or an error. */ -export function JSFileToBase64(file: File): Promise> { +export function JSFileToDataUrl(file: File): Promise> { return new Promise((resolve, reject) => { var reader = getFileReader(); reader.readAsDataURL(file); @@ -28,3 +28,64 @@ export function JSFileToBase64(file: File): Promise> { }; }); } + + + +/** + * Creates a Data URL from a base64-encoded string and MIME type. + * + * @param base64String - The base64-encoded data as a string. This should not include the `data:[mime-type];base64,` prefix. + * @param mimeType - The MIME type of the data (e.g., `image/png`, `application/pdf`). + * + * @returns A Data URL formatted string. + * + * @example + * ```typescript + * const base64String = 'iVBORw0KGgoAAAANSUhEUgAAAAU...'; // Your base64 string + * const mimeType = 'image/png'; // Your MIME type + * + * const dataURL = createDataURL(base64String, mimeType); + * console.log(dataURL); + * ``` + */ +export function createDataURL(base64String: string, mimeType: string): string { + // Make sure the base64 string doesn't have the data URL scheme or extra padding + const cleanedBase64String = base64String.replace(/^data:[a-z]+\/[a-z]+;base64,/, ''); + return `data:${mimeType};base64,${cleanedBase64String}`; +} + + + + + + + + + + + + + +/** + * Converts a `Blob` to a Data URL. + * @param {Blob} blob - The `Blob` to be converted. + * @returns {Promise} A promise that resolves with the Data URL representation of the `Blob`. + */ +export function convertBlobToDataURL(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onloadend = () => { + // Resolve the promise with the Data URL + resolve(reader.result as string); + }; + + reader.onerror = () => { + // Reject the promise on error + reject(new Error('Failed to convert Blob to Data URL')); + }; + + // Read the Blob as a Data URL + reader.readAsDataURL(blob); + }); +} diff --git a/src/app/utils/zod.ts b/src/app/utils/zod.ts new file mode 100644 index 000000000..9ee0d209f --- /dev/null +++ b/src/app/utils/zod.ts @@ -0,0 +1,50 @@ +import { z } from 'zod'; + +/** + * Regular expression to validate a data URL. + * The pattern matches data URLs that start with `data:`, optionally followed by a media type, + * optionally followed by an encoding (e.g., `base64`), and ending with base64 encoded data. + */ +export const zodDataUrlRegex = /^data:(?:[\w+\/-]+)?(?:[\w+\/-]+)?;base64,[\s\S]*$/; + +/** + * Zod schema for validating data URLs. + * This schema ensures that the input string matches the format of a data URL. + * + * @example + * const result = dataUrlSchema.safeParse('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDCAAAAC0BEMAAW9R3AAAAAElFTkSuQmCC'); + * if (result.success) { + * console.log('Valid data URL:', result.data); + * } else { + * console.error('Validation error:', result.error.errors); + * } + */ +export const zodDataUrlSchema = z.string().refine(value => zodDataUrlRegex.test(value), { + message: 'Invalid data URL', +}); + +/** + * Validates if a string is a valid data URL. + * + * @param url - The string to validate as a data URL. + * @returns {void} - Logs the result of the validation. + * + * @example + * validateDataUrl('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDCAAAAC0BEMAAW9R3AAAAAElFTkSuQmCC'); + * // Output: Valid data URL: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDCAAAAC0BEMAAW9R3AAAAAElFTkSuQmCC + * + * validateDataUrl('invalid-url'); + * // Output: Validation error: [ ... ] + */ +const validateDataUrl = (url: string) => { + const result: any = zodDataUrlSchema.safeParse(url); + if (result.success) { + console.log('Valid data URL:', url); + } else { + console.error('Validation error:', result.error.errors); + } +}; + +// Test the schema +validateDataUrl('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDCAAAAC0BEMAAW9R3AAAAAElFTkSuQmCC'); +validateDataUrl('invalid-url'); \ No newline at end of file