diff --git a/.gitignore b/.gitignore index 96feeabd6..2f5e8c055 100644 --- a/.gitignore +++ b/.gitignore @@ -88,7 +88,7 @@ cypress/videos src/app/models/beast-orm-pro.ts - +doc/TypeDoc src/app/pipes/process.service.spec.ts src/app/pipes/process.service.ts diff --git a/doc/TypeDoc/functions/JSFileToBase64.html b/doc/TypeDoc/functions/JSFileToBase64.html index f22116e49..46a5a5d45 100644 --- a/doc/TypeDoc/functions/JSFileToBase64.html +++ b/doc/TypeDoc/functions/JSFileToBase64.html @@ -1,4 +1,4 @@ JSFileToBase64 | gabinete-digital

Function JSFileToBase64

  • Converts a File object to a Base64 encoded string.

    Parameters

    • file: File

      The file to be converted.

    Returns Promise<Result<string, any>>

    A promise that resolves with a Result object containing either the Base64 encoded string or an error.

    -
+
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index a67b8e8aa..e17803e28 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -77,7 +77,7 @@ export class AppComponent { if(this.platform.is('ios')){ this.screenOrientation.lock('portrait') } else { - window.screen.orientation.lock('portrait'); + (window.screen.orientation as any).lock('portrait'); } } }); diff --git a/src/app/infra/camera/camera.service.spec.ts b/src/app/infra/camera/camera.service.spec.ts new file mode 100644 index 000000000..73b57d3a5 --- /dev/null +++ b/src/app/infra/camera/camera.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CameraService } from './camera.service'; + +describe('CameraService', () => { + let service: CameraService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CameraService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/infra/camera/camera.service.ts b/src/app/infra/camera/camera.service.ts new file mode 100644 index 000000000..452ab3381 --- /dev/null +++ b/src/app/infra/camera/camera.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import { err, ok } from 'neverthrow'; + + +type takePictureParams = { + quality?: number + cameraResultType: CameraResultType +} + +@Injectable({ + providedIn: 'root' +}) +export class CameraService { + + constructor() { } + + + async takePicture({quality = 90, cameraResultType }: takePictureParams) { + + try { + const file = await Camera.getPhoto({ + quality: quality, + // allowEditing: true, + resultType: cameraResultType, + source: CameraSource.Camera + }); + + return ok(file) + } catch (e) { + return err(e) + } + } +} diff --git a/src/app/infra/file-picker/web/file-picker-web.service.ts b/src/app/infra/file-picker/web/file-picker-web.service.ts index 9a203669a..1cd39cf59 100644 --- a/src/app/infra/file-picker/web/file-picker-web.service.ts +++ b/src/app/infra/file-picker/web/file-picker-web.service.ts @@ -1,6 +1,13 @@ import { Injectable } from '@angular/core'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import { err, ok, Result } from 'neverthrow'; import { FileType } from 'src/app/models/fileType'; +type PickPictureParams = { + quality?: number, + cameraResultType?: CameraResultType +} + @Injectable({ providedIn: 'root' }) @@ -8,7 +15,7 @@ export class FilePickerWebService { constructor() { } - getFileFromDevice(types: typeof FileType[]): Promise { + getFileFromDevice(types: typeof FileType[]): Promise> { let input = document.createElement('input'); input.type = 'file'; input.accept = types.join(', ') @@ -18,9 +25,24 @@ export class FilePickerWebService { return new Promise((resolve, reject)=>{ input.onchange = async () => { const file = Array.from(input.files) - resolve(file[0] as File); + resolve(ok(file[0] as File)); }; }) } + async getPicture({quality = 90, cameraResultType =CameraResultType.DataUrl }: PickPictureParams) { + try { + const file = await Camera.getPhoto({ + quality: quality, + // allowEditing: true, + resultType: cameraResultType, + source: CameraSource.Photos + }); + + return ok(file) + } catch (e) { + return err(e) + } + } + } diff --git a/src/app/module/chat/domain/entity/message.ts b/src/app/module/chat/domain/entity/message.ts index 119873719..3c5df5d25 100644 --- a/src/app/module/chat/domain/entity/message.ts +++ b/src/app/module/chat/domain/entity/message.ts @@ -53,7 +53,7 @@ export class MessageEntity implements Message { attachments: { fileType: MessageAttachmentFileType, source: MessageAttachmentSource, - file: string, + file?: string, fileName: string, applicationId?: string, docId?: string diff --git a/src/app/shared/chat/messages/messages.page.html b/src/app/shared/chat/messages/messages.page.html index 7df76e47d..e747bbd80 100644 --- a/src/app/shared/chat/messages/messages.page.html +++ b/src/app/shared/chat/messages/messages.page.html @@ -162,7 +162,7 @@ - + diff --git a/src/app/shared/chat/messages/messages.page.ts b/src/app/shared/chat/messages/messages.page.ts index f6e61f2da..cb819a40e 100644 --- a/src/app/shared/chat/messages/messages.page.ts +++ b/src/app/shared/chat/messages/messages.page.ts @@ -48,7 +48,10 @@ import { MemberTable } from 'src/app/module/chat/infra/database/dexie/schema/mem 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 { 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' +import { allowedDocExtension } from 'src/app/utils/allowedDocExtension'; const IMAGE_DIR = 'stored-images'; @Component({ @@ -158,6 +161,8 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy private messageRepositoryService: MessageRepositoryService, private userTypingServiceRepository: UserTypingServiceRepository, private chatServiceService: ChatServiceService, + private CameraService: CameraService, + private FilePickerWebService: FilePickerWebService ) { // update this.checkAudioPermission() @@ -679,17 +684,6 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy await modal.present(); } - openSendMessageOptions(ev?: any) { - if (window.innerWidth < 701) { - - this.openChatOptions(ev); - } - else { - - this._openChatOptions(); - } - } - async openChatOptions(ev: any) { const popover = await this.popoverController.create({ component: ChatOptionsPopoverPage, @@ -798,57 +792,50 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy async takePictureMobile() { + const picture = await this.CameraService.takePicture({ + cameraResultType: CameraResultType.DataUrl, + quality: 90 + }) - const roomId = this.roomId + if(picture.isOk()) { + const file = picture.value - const file = await Camera.getPhoto({ - quality: 90, - // allowEditing: true, - resultType: CameraResultType.Base64, - source: CameraSource.Camera - }); - console.log('Selected: ', file) - var base64 = 'data:image/jpeg;base64,' + file.base64String - const compressedImage = await this.compressImageBase64( - base64, - 800, // maxWidth - 800, // maxHeight - 0.9 // quality - ).then((picture) => { + const compressedImage = await compressImageBase64( + file.dataUrl, + 800, // maxWidth + 800, // maxHeight + 0.9 // quality + ) - base64 = picture - }); + if(compressedImage.isOk()) { + const message = new MessageEntity(); + message.roomId = this.roomId - const blob = this.dataURItoBlob(base64) + message.sender = { + userPhoto: '', + wxeMail: SessionStore.user.Email, + wxFullName: SessionStore.user.FullName, + wxUserId: SessionStore.user.UserId + } - const formData = new FormData(); - formData.append("blobFile", blob); + message.attachments = [{ + file: compressedImage.value, + fileName: "foto", + source: MessageAttachmentSource.Device, + fileType: MessageAttachmentFileType.Image + }] - // this.ChatSystemService.getDmRoom(roomId).send({ - // file: { - // "type": "application/img", - // "guid": '', - // }, - // temporaryData: formData, - // attachments: [{ - // "title": file.path, - // // "image_url": "", - // //"image_url": 'data:image/jpeg;base64,' + file.base64String, - // "text": "description", - // "title_link_download": false, - // }], - // attachmentsModelData: { - // fileBase64: base64, - // } - // }) + this.chatServiceService.sendMessage(message) + + } + + } } async takePicture() { - const roomId = this.roomId - const file = await Camera.getPhoto({ quality: 90, // allowEditing: true, @@ -885,10 +872,6 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy } - async addImage() { - this.addFileToChatMobile(['image/apng', 'image/jpeg', 'image/png']) - } - async addFile() { this.addFileToChat(['.doc', '.docx', '.pdf'], MessageAttachmentFileType.Doc) } @@ -907,29 +890,32 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy modal.onDidDismiss().then(async res => { const data = res.data; - const roomId = this.roomId if (data.selected) { - // this.ChatSystemService.getDmRoom(roomId).send({ - // file: { - // "name": res.data.selected.Assunto, - // "type": "application/webtrix", - // "ApplicationId": res.data.selected.ApplicationType, - // "DocId": res.data.selected.Id, - // "Assunto": res.data.selected.Assunto, - // }, - // temporaryData: res, - // attachments: [{ - // "title": res.data.selected.Assunto, - // "description": res.data.selected.DocTypeDesc, - // "title_link_download": true, - // "type": "webtrix", - // "text": res.data.selected.DocTypeDesc, - // "thumb_url": "https://static.ichimura.ed.jp/uploads/2017/10/pdf-icon.png", - // }], - // }) + // "title": res.data.selected.Assunto, + // "description": res.data.selected.DocTypeDesc, + const message = new MessageEntity(); + message.message = this.textField + message.roomId = this.roomId + + message.sender = { + userPhoto: '', + wxeMail: SessionStore.user.Email, + wxFullName: SessionStore.user.FullName, + wxUserId: SessionStore.user.UserId + } + message.attachments = [{ + fileName: res.data.selected.Assunto, + source: MessageAttachmentSource.Webtrix, + fileType: MessageAttachmentFileType.Doc, + applicationId: res.data.selected.ApplicationType, + docId: res.data.selected.Id, + }] + + this.chatServiceService.sendMessage(message) + this.textField = '' } }); @@ -937,58 +923,48 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy await modal.present(); } - async addFileToChatMobile(types: typeof FileType[]) { - const roomId = this.roomId + async pickPicture() { - const file = await Camera.getPhoto({ - quality: 90, - // allowEditing: true, - resultType: CameraResultType.Base64, - source: CameraSource.Photos - }); + const file = await this.FilePickerWebService.getPicture({ + cameraResultType: CameraResultType.Base64 + }) - //const imageData = await this.fileToBase64Service.convert(file) - // - console.log('Selected: ', file) - var base64 = 'data:image/jpeg;base64,' + file.base64String - if (file.format == "jpeg" || file.format == "png" || file.format == "gif") { + if(file.isOk()) { - const compressedImage = await this.compressImageBase64( - base64, - 800, // maxWidth - 800, // maxHeight - 0.9 // quality - ).then((picture) => { + var base64 = 'data:image/jpeg;base64,' + file.value.base64String + if (file.value.format == "jpeg" || file.value.format == "png" || file.value.format == "gif") { - base64 = picture - }); + const compressedImage = await compressImageBase64( + base64, + 800, // maxWidth + 800, // maxHeight + 0.9 // quality + ) - const response = await fetch(base64); - const blob = await response.blob(); + if(compressedImage.isOk()) { - console.log(base64) + const message = new MessageEntity(); + message.roomId = this.roomId - const formData = new FormData(); - formData.append("blobFile", blob); + message.sender = { + userPhoto: '', + wxeMail: SessionStore.user.Email, + wxFullName: SessionStore.user.FullName, + wxUserId: SessionStore.user.UserId + } - // this.ChatSystemService.getDmRoom(roomId).send({ - // file: { - // "type": "application/img", - // "guid": '' - // }, - // temporaryData: formData, - // attachments: [{ - // "title": file.path, - // //"image_url": 'data:image/jpeg;base64,' + file.base64String, - // "text": "description", - // "title_link_download": false, - // }], - // attachmentsModelData: { - // fileBase64: base64, - // } - // }) + message.attachments = [{ + file: compressedImage.value, + fileName: "foto", + source: MessageAttachmentSource.Device, + fileType: MessageAttachmentFileType.Image + }] + + this.chatServiceService.sendMessage(message) + } } + } } @@ -1003,20 +979,16 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy async addFileToChat(types: typeof FileType[], attachmentFileType:MessageAttachmentFileType) { - const file = await this.fileService.getFileFromDevice(types); + const file = await this.FilePickerWebService.getFileFromDevice(types); + if(file.isOk()) { - if (file.type == 'application/pdf' || file.type == 'application/doc' || file.type == 'application/docx' || - file.type == 'application/xls' || file.type == 'application/xlsx' || file.type == 'application/ppt' || - file.type == 'application/pptx' || file.type == 'application/txt') { + const fileExtension = await allowedDocExtension(file.value.type) - console.log('FILE rigth?', file) + if(fileExtension.isOk()) { - const fileName = file.name + console.log('FILE rigth?', file) - const FilenameValidation = this.FileValidatorService.fileNameValidation(fileName) - - if (FilenameValidation.isOk) { - let fileBase64 = await JSFileToBase64(file); + let fileBase64 = await JSFileToBase64(file.value); if(fileBase64.isOk()) { @@ -1032,7 +1004,7 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy message.attachments = [{ file: fileBase64.value, - fileName: file.name, + fileName: file.value.name, source: MessageAttachmentSource.Device, fileType: MessageAttachmentFileType.Doc }] @@ -1043,9 +1015,7 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy } else { this.toastService._badRequest("Ficheiro inválido") } - } - } _getBase64(file) { @@ -1151,7 +1121,7 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy } else if (res['data'] == 'add-picture') { - this.addImage() + this.pickPicture() } else if (res['data'] == 'add-document') { @@ -1371,7 +1341,7 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy async compressImageBase64(base64String: string, maxWidth: number, maxHeight: number, quality: number): Promise { return new Promise((resolve, reject) => { - const image = new (window as any).Image(); + const image = new Image(); image.src = base64String; image.onload = async () => { diff --git a/src/app/utils/allowedDocExtension.ts b/src/app/utils/allowedDocExtension.ts new file mode 100644 index 000000000..914de8f1f --- /dev/null +++ b/src/app/utils/allowedDocExtension.ts @@ -0,0 +1,12 @@ +import { err, ok } from "neverthrow" + +export function allowedDocExtension(type: string) { + if (type == 'application/pdf' || type == 'application/doc' || type == 'application/docx' || + type == 'application/xls' || type == 'application/xlsx' || type == 'application/ppt' || + type == 'application/pptx' || type == 'application/txt') { + + return ok(true) + } else { + return err(false) + } +} diff --git a/src/app/utils/imageCompressore.ts b/src/app/utils/imageCompressore.ts new file mode 100644 index 000000000..eac58129e --- /dev/null +++ b/src/app/utils/imageCompressore.ts @@ -0,0 +1,72 @@ +import { err, ok, Result } from "neverthrow"; + +/** + * Compresses an image represented as a Base64 string. + * + * This function resizes the image to fit within the specified maximum width and height while maintaining the aspect ratio. + * The image is then compressed to a JPEG format with the given quality level. + * + * @param base64String - The Base64 string of the image to be compressed. + * @param maxWidth - The maximum width of the compressed image. The aspect ratio is preserved. + * @param maxHeight - The maximum height of the compressed image. The aspect ratio is preserved. + * @param quality - The quality of the compressed image, ranging from 0 to 1, where 1 is the best quality. + * + * @returns A Promise that resolves to a `Result` containing either: + * - `ok` with the compressed image as a Base64 string, or + * - `err` with an error if the image fails to load or compress. + * + * @example + * ```typescript + * compressImageBase64('data:image/png;base64,...', 800, 600, 0.8) + * .then(result => { + * if (result.isOk()) { + * console.log('Compressed image:', result.value); + * } else { + * console.error('Error compressing image:', result.error); + * } + * }); + * ``` + */ +export function compressImageBase64(base64String: string, maxWidth: number, maxHeight: number, quality: number): Promise> { + return new Promise((resolve, reject) => { + const image = new Image(); + image.src = base64String; + + image.onload = async () => { + const canvas = document.createElement('canvas'); + let newWidth = image.width; + let newHeight = image.height; + + if (newWidth > maxWidth) { + newHeight *= maxWidth / newWidth; + newWidth = maxWidth; + } + + if (newHeight > maxHeight) { + newWidth *= maxHeight / newHeight; + newHeight = maxHeight; + } + + canvas.width = newWidth; + canvas.height = newHeight; + + const context = canvas.getContext('2d'); + context?.drawImage(image, 0, 0, newWidth, newHeight); + + const compressedBase64 = canvas.toDataURL('image/jpeg', quality); + + // Calculate the compression percentage + const originalSize = base64String.length; + const compressedSize = compressedBase64.length; + const compressionPercentage = ((originalSize - compressedSize) / originalSize) * 100; + + console.log(`Compression achieved: ${compressionPercentage.toFixed(2)}%`); + + resolve(ok(compressedBase64)); + }; + + image.onerror = (error) => { + resolve(err(error)); + }; + }); +}