From 5b31a186c255a3e63bd60c3797f6037988a4f32b Mon Sep 17 00:00:00 2001 From: Peter Maquiran Date: Fri, 9 Aug 2024 14:56:36 +0100 Subject: [PATCH] send audio as attachment --- src/app/infra/speaker/speaker.service.ts | 105 ++++ .../shared/chat/messages/messages.page.html | 18 +- src/app/shared/chat/messages/messages.page.ts | 457 +++--------------- 3 files changed, 196 insertions(+), 384 deletions(-) create mode 100644 src/app/infra/speaker/speaker.service.ts diff --git a/src/app/infra/speaker/speaker.service.ts b/src/app/infra/speaker/speaker.service.ts new file mode 100644 index 000000000..8e28cc607 --- /dev/null +++ b/src/app/infra/speaker/speaker.service.ts @@ -0,0 +1,105 @@ +import { Injectable } from '@angular/core'; +import { GenericResponse, RecordingData, VoiceRecorder } from 'capacitor-voice-recorder'; +import { err, ok, Result } from 'neverthrow'; + +export enum StartRecordingResultError { + NoSpeaker, + NeedPermission, + alreadyRecording +} + +export enum StopRecordingResultError { + haventStartYet, + NoValue, + UnknownError +} + +@Injectable({ + providedIn: 'root' +}) +export class SpeakerService { + + + recording = false; + + constructor() { } + + + async startRecording(): Promise> { + // Request audio recording permission + await VoiceRecorder.requestAudioRecordingPermission(); + + // Check if the device can record audio + const canRecord = await VoiceRecorder.canDeviceVoiceRecord(); + if (!canRecord.value) { + return err(StartRecordingResultError.NoSpeaker); + } + + // Check if the app has permission to record audio + const hasPermission = await VoiceRecorder.requestAudioRecordingPermission(); + if (!hasPermission.value) { + return err(StartRecordingResultError.NeedPermission); + } + + // Check if recording is already in progress + if (this.recording) { + return err(StartRecordingResultError.alreadyRecording) + } + + // Start recording + this.recording = true; + VoiceRecorder.startRecording(); + return ok(true); + } + + + /** + * Example of a poorly structured startRecording method (commented out). + * + * - This example demonstrates improper chaining of promises and lack of proper error handling. + * - Avoid using this approach for better readability and maintainability. + */ + // bad code example + // async startRecording() { + // VoiceRecorder.requestAudioRecordingPermission(); + // if (await VoiceRecorder.canDeviceVoiceRecord().then((result: GenericResponse) => { return result.value })) { + // if (await VoiceRecorder.requestAudioRecordingPermission().then((result: GenericResponse) => { return result.value })) { + // //if(await this.hasAudioRecordingPermission()){ + // if (this.recording) { + // return; + // } + // this.recording = true; + // VoiceRecorder.startRecording() + // //} + // } + // else { + // return err('need Permission'); + // } + // } + // else { + // return err('no speaker'); + // } + // } + + async stopRecording(): Promise> { + if (!this.recording) { + return err(StopRecordingResultError.haventStartYet); + } + + this.recording = false; + + try { + const result = await VoiceRecorder.stopRecording(); + + if (result.value && result.value.recordDataBase64) { + const recordData = result.value.recordDataBase64; + return ok(result); + } else { + return err(StopRecordingResultError.NoValue); + } + } catch (error) { + // Handle any unexpected errors that might occur during stopRecording + return err(StopRecordingResultError.UnknownError); + } + } +} diff --git a/src/app/shared/chat/messages/messages.page.html b/src/app/shared/chat/messages/messages.page.html index e747bbd80..028fd08e9 100644 --- a/src/app/shared/chat/messages/messages.page.html +++ b/src/app/shared/chat/messages/messages.page.html @@ -141,9 +141,9 @@ está a escrever... -->
- {{durationDisplay}} + {{durationDisplay}}
-
+
@@ -189,13 +189,21 @@
-
- + + -
+ diff --git a/src/app/shared/chat/messages/messages.page.ts b/src/app/shared/chat/messages/messages.page.ts index cb819a40e..699aae4ce 100644 --- a/src/app/shared/chat/messages/messages.page.ts +++ b/src/app/shared/chat/messages/messages.page.ts @@ -13,8 +13,6 @@ import { ViewDocumentPage } from 'src/app/modals/view-document/view-document.pag import { ThemeService } from 'src/app/services/theme.service'; import { ViewEventPage } from 'src/app/modals/view-event/view-event.page'; import { Storage } from '@ionic/storage'; -// import { RochetChatConnectorService } from 'src/app/services/chat/rochet-chat-connector.service' -// simport { MessageService } from 'src/app/services/chat/message.service'; import { FileType } from 'src/app/models/fileType'; import { SearchPage } from 'src/app/pages/search/search.page'; import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; @@ -28,21 +26,17 @@ import { FileOpener } from '@awesome-cordova-plugins/file-opener/ngx'; import { SessionStore } from 'src/app/store/session.service'; import { Howl } from 'howler'; import { ViewMediaPage } from 'src/app/modals/view-media/view-media.page'; -import { ChatMessageDebuggingPage } from 'src/app/shared/popover/chat-message-debugging/chat-message-debugging.page'; import { PermissionService } from 'src/app/services/permission.service'; -import { FileValidatorService } from "src/app/services/file/file-validator.service" import { ChatPopoverPage } from '../../popover/chat-popover/chat-popover.page'; import { Observable as DexieObservable } from 'Dexie'; import { Observable, Subscription } from 'rxjs'; import { MessageRepositoryService } from 'src/app/module/chat/data/repository/message-respository.service' import { RoomRepositoryService } from 'src/app/module/chat/data/repository/room-repository.service' import { MessageTable } from 'src/app/module/chat/infra/database/dexie/schema/message'; -import { MessageInputDTO } from 'src/app/module/chat/data/dto/message/messageInputDtO'; import { RoomListItemOutPutDTO } from 'src/app/module/chat/data/dto/room/roomListOutputDTO'; import { UserTypingServiceRepository } from 'src/app/module/chat/data/repository/user-typing-repository.service'; import { ChatServiceService } from 'src/app/module/chat/domain/chat-service.service'; import { EditMessagePage } from 'src/app/modals/edit-message/edit-message.page'; -import { tap } from 'rxjs/operators'; 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'; @@ -52,7 +46,7 @@ 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'; - +import { SpeakerService, StartRecordingResultError, StopRecordingResultError } from 'src/app/infra/speaker/speaker.service' const IMAGE_DIR = 'stored-images'; @Component({ selector: 'app-messages', @@ -69,7 +63,6 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy dm: any; userPresence = ''; dmUsers: any; - checktimeOut: boolean; downloadProgess = 0; @Input() roomId: string; @@ -95,17 +88,15 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy mesageItemDropdownOptions: boolean = false; scrollToBottomBtn = false; longPressActive = false; - frameUrl: any; downloadFile: string; - massages = [] showAvatar = true; recording = false; allowTyping = true; - storedFileNames = []; lastAudioRecorded = ''; - audioRecorded: any = ""; + audioRecordedSafe: any = ""; + audioRecordedDataUrl: any = ""; audioDownloaded: any = ""; durationDisplay = ''; duration = 0; @@ -139,6 +130,8 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy selectedMessage: any = null; emojis: string[] = ['😊', '😂', '❤️', '👍', '😢']; // Add more emojis as needed totalMessage = 0 + recordData:RecordingData + constructor( public popoverController: PopoverController, private modalController: ModalController, @@ -156,13 +149,13 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy private platform: Platform, private fileOpener: FileOpener, public p: PermissionService, - private FileValidatorService: FileValidatorService, private roomRepositoryService: RoomRepositoryService, private messageRepositoryService: MessageRepositoryService, private userTypingServiceRepository: UserTypingServiceRepository, private chatServiceService: ChatServiceService, private CameraService: CameraService, - private FilePickerWebService: FilePickerWebService + private FilePickerWebService: FilePickerWebService, + private SpeakerService: SpeakerService ) { // update this.checkAudioPermission() @@ -288,7 +281,7 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy this.getChatMembers(); this.deleteRecording(); - this.loadFiles(); + // this.loadFiles(); } @@ -309,7 +302,6 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy } load = () => { - this.checktimeOut = true; this.getChatMembers(); } @@ -336,21 +328,13 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy } - // @HostListener('scroll', ['$event']) - // onScroll(event: any): void { - // // const scrollTop = event.target.scrollTop; - // // const scrollHeight = event.target.scrollHeight; - // // const clientHeight = event.target.clientHeight; - - // // if (scrollTop === 0) { - // // console.log('load more') - // // } - // } - ngAfterViewInit() { // this.scrollChangeCallback = () => this.onContentScrolled(event); // window.addEventListener('scroll', this.scrollChangeCallback, true); } + ngOnDestroy() { + window.removeEventListener('scroll', this.scrollChangeCallback, true); + } onContentScrolled(e) { this.startPosition = e.srcElement.scrollTop; @@ -394,107 +378,90 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy }) const base64sound = audioFile.data; const base64Response = await fetch(`data:audio/ogg;base64,${base64sound}`); - this.audioRecorded = base64Response.url; - } - - async loadFiles() { - try { - this.storage.get('fileName').then((fileName) => { - this.lastAudioRecorded = fileName; - }) - - this.storage.get('recordData').then((recordData) => { - - if (recordData?.value?.recordDataBase64.includes('data:audio')) { - this.audioRecorded = this.sanitiser.bypassSecurityTrustResourceUrl(recordData?.value?.recordDataBase64); - } - else if (recordData?.value?.mimeType && recordData?.value?.recordDataBase64) { - this.audioRecorded = this.sanitiser.bypassSecurityTrustResourceUrl(`data:${recordData.value.mimeType};base64,${recordData?.value?.recordDataBase64}`); - } - }); - } catch (error) { } - - - this.storage.get('recordData').then((recordData) => { - - if (recordData?.value?.recordDataBase64?.includes('data:audio')) { - this.audioRecorded = this.sanitiser.bypassSecurityTrustResourceUrl(recordData.value.recordDataBase64); - } - else if (recordData?.value?.mimeType && recordData?.value?.recordDataBase64) { - this.audioRecorded = this.sanitiser.bypassSecurityTrustResourceUrl(`data:${recordData.value.mimeType};base64,${recordData.value.recordDataBase64}`); - } - }); + this.audioRecordedSafe = base64Response.url; } async startRecording() { - VoiceRecorder.requestAudioRecordingPermission(); - if (await VoiceRecorder.canDeviceVoiceRecord().then((result: GenericResponse) => { return result.value })) { - if (await VoiceRecorder.requestAudioRecordingPermission().then((result: GenericResponse) => { return result.value })) { - //if(await this.hasAudioRecordingPermission()){ - if (this.recording) { - return; - } - this.recording = true; - VoiceRecorder.startRecording(); - this.calculateDuration(); - //} - } - else { - this.toastService._badRequest('Para gravar uma mensagem de voz, permita o acesso do Gabinete Digital ao seu microfone.'); - } - } - else { + + const start = await this.SpeakerService.startRecording() + + if(start.isOk()) { + this.recording = true; + this.calculateDuration(); + } else if(start.error == StartRecordingResultError.NoSpeaker) { this.toastService._badRequest('Este dispositivo não tem capacidade para gravação de áudio!'); + } else if (start.error == StartRecordingResultError.NeedPermission) { + this.toastService._badRequest('Para gravar uma mensagem de voz, permita o acesso do Gabinete Digital ao seu microfone.'); + } else if(start.error == StartRecordingResultError.alreadyRecording) { + } + } - stopRecording() { + async stopRecording() { this.deleteRecording(); this.allowTyping = false; - if (!this.recording) { - return; - } - this.recording = false; - VoiceRecorder.stopRecording().then(async (result: RecordingData) => { + const stop = await this.SpeakerService.stopRecording() + if(stop.isOk()) { + this.lastAudioRecorded = 'audio' this.recording = false; - if (result.value && result.value.recordDataBase64) { - const recordData = result.value.recordDataBase64; - // - const fileName = new Date().getTime() + ".mp3"; - //Save file - this.storage.set('fileName', fileName); - this.storage.set('recordData', result).then(() => { + const recordData = stop.value + this.recordData = recordData + if (recordData.value.recordDataBase64.includes('data:audio')) { + console.log({recordData}) - }) + this.audioRecordedDataUrl = recordData.value.recordDataBase64 + this.audioRecordedSafe = this.sanitiser.bypassSecurityTrustResourceUrl(recordData.value.recordDataBase64); } - }) - setTimeout(async () => { - this.loadFiles(); - }, 1000); + else if (recordData.value.mimeType && recordData?.value?.recordDataBase64) { + console.log({recordData}) + + this.audioRecordedDataUrl = `data:${recordData.value.mimeType};base64,${recordData.value.recordDataBase64}` + this.audioRecordedSafe = this.sanitiser.bypassSecurityTrustResourceUrl(this.audioRecordedDataUrl); + + } + + } else if (stop.error == StopRecordingResultError.haventStartYet) { + return + } + + } + + async sendAudio(fileName) { + const roomId = this.roomId + //Converting base64 to blob + const encodedData = this.audioRecordedDataUrl; + + const message = new MessageEntity(); + message.roomId = this.roomId + + message.sender = { + userPhoto: '', + wxeMail: SessionStore.user.Email, + wxFullName: SessionStore.user.FullName, + wxUserId: SessionStore.user.UserId + } + + message.attachments = [{ + file: encodedData, + fileName: "audio", + source: MessageAttachmentSource.Device, + fileType: MessageAttachmentFileType.Audio + }] + + this.chatServiceService.sendMessage(message) + + + this.deleteRecording(); } async deleteRecording() { - this.storage.remove('fileName'); - this.storage.remove('recordData'); - this.allowTyping = true; this.lastAudioRecorded = ''; - this.loadFiles(); - } - - ngOnDestroy() { - this.checktimeOut = false; - window.removeEventListener('scroll', this.scrollChangeCallback, true); - } - - openBookMeetingComponent() { - let data = { - roomId: this.roomId, - members: [] - } - this.openNewEventPage.emit(data); + this.audioRecordedSafe = '' + this.audioRecordedDataUrl = '' } showDateDuration(start: any) { @@ -525,18 +492,6 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy sendMessage() { - // const data: MessageInputDTO = { - // roomId: this.roomId, - // senderId: SessionStore.user.UserId, - // message: this.textField, - // messageType: 0, - // canEdit: false, - // oneShot:false, - // requireUnlock: false, - // } - - // this.messageRepositoryService.sendMessage(data) - const message = new MessageEntity(); message.message = this.textField message.roomId = this.roomId @@ -554,46 +509,7 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy } - async sendAudio(fileName) { - const roomId = this.roomId - let audioFile; - this.storage.get('recordData').then(async (recordData) => { - audioFile = recordData; - - if (recordData?.value?.recordDataBase64?.includes('data:audio')) { - this.audioRecorded = recordData.value.recordDataBase64; - } - else { - this.audioRecorded = `data:${recordData.value.mimeType};base64,${recordData?.value?.recordDataBase64}`; - } - //Converting base64 to blob - const encodedData = btoa(this.audioRecorded); - const blob = this.fileService.base64toBlob(encodedData, recordData.value.mimeType) - - const formData = new FormData(); - formData.append("blobFile", blob); - - // this.ChatSystemService.getDmRoom(roomId).send({ - // file: { - // "type": "application/audio", - // "msDuration": audioFile.value.msDuration, - // "mimeType": audioFile.value.mimeType, - // }, - // attachments: [{ - // "title": fileName, - // "title_link_download": true, - // "type": "audio" - // }], - // temporaryData: formData, - // attachmentsModelData: { - // fileBase64: encodedData, - // } - // }) - - }); - this.deleteRecording(); - } async openViewDocumentModal(file: any) { let task = { @@ -644,31 +560,8 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy await modal.present(); } - getChatMembers() { - // + getChatMembers() {} - // this.showLoader = true; - // this.chatService.getMembers(this.roomId).subscribe(res => { - // this.members = res['members']; - // this.dmUsers = res['members'].filter(data => data.username != this.sessionStore.user.UserName) - // this.showLoader = false; - // }); - - // this.dmUsers = this.ChatSystemService.getDmRoom(this.roomId).membersExcludeMe - } - - async openMessagesOptions(ev: any) { - const popover = await this.popoverController.create({ - component: MessagesOptionsPage, - componentProps: { - roomId: this.dm._id, - }, - cssClass: 'messages-options', - event: ev, - translucent: true, - }); - return await popover.present(); - } async addContacts() { const modal = await this.modalController.create({ @@ -678,24 +571,10 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy backdropDismiss: false }); - - modal.onDidDismiss(); await modal.present(); } - async openChatOptions(ev: any) { - const popover = await this.popoverController.create({ - component: ChatOptionsPopoverPage, - cssClass: 'chat-options-popover', - event: ev, - translucent: true - }); - return await popover.present(); - } - - - async _openMessagesOptions() { const enterAnimation = (baseEl: any) => { const backdropAnimation = this.animationController.create() @@ -735,7 +614,7 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy modal.onDidDismiss().then(res => { if (res.data == 'leave') { - this.getRoomInfo(); + // this.getRoomInfo(); this.closeAllDesktopComponents.emit(); this.showEmptyContainer.emit(); // this.ChatSystemService.hidingRoom(this.roomId).catch((error) => console.error(error)); @@ -764,32 +643,6 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy this.openGroupContacts.emit(this.roomId); } - - dataURItoBlob(dataURI) { - // convert base64 to raw binary data held in a string - // doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this - var byteString = atob(dataURI.split(',')[1]); - - // separate out the mime component - var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0] - - // write the bytes of the string to an ArrayBuffer - var ab = new ArrayBuffer(byteString.length); - - // create a view into the buffer - var ia = new Uint8Array(ab); - - // set the bytes of the buffer to the correct values - for (var i = 0; i < byteString.length; i++) { - ia[i] = byteString.charCodeAt(i); - } - - // write the ArrayBuffer to a blob, and you're done - var blob = new Blob([ab], { type: mimeString }); - return blob; - - } - async takePictureMobile() { const picture = await this.CameraService.takePicture({ @@ -834,44 +687,6 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy } - async takePicture() { - - const file = await Camera.getPhoto({ - quality: 90, - // allowEditing: true, - resultType: CameraResultType.Base64, - source: CameraSource.Camera - }); - - console.log('FILE CHAT', file) - - const imageBase64 = 'data:image/jpeg;base64,' + file.base64String - const blob = this.dataURItoBlob(imageBase64) - - console.log(imageBase64) - - const formData = new FormData(); - formData.append("blobFile", blob); - - - // this.ChatSystemService.getDmRoom(roomId).send({ - // file: { - // "type": "application/img", - // "guid": '' - // }, - // temporaryData: formData, - // attachments: [{ - // "title": "file.jpg", - // "text": "description", - // "title_link_download": false, - // }], - // attachmentsModelData: { - // fileBase64: imageBase64, - // } - // }) - - } - async addFile() { this.addFileToChat(['.doc', '.docx', '.pdf'], MessageAttachmentFileType.Doc) } @@ -1018,51 +833,6 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy } } - _getBase64(file) { - return new Promise((resolve, reject) => { - var reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = function () { - resolve(reader.result) - }; - reader.onerror = function (error) { - console.log('Error: ', error); - }; - }) - } - getFileReader(): FileReader { - const fileReader = new FileReader(); - const zoneOriginalInstance = (fileReader as any)["__zone_symbol__originalInstance"]; - return zoneOriginalInstance || fileReader; - } - - getBase64(file) { - var reader = this.getFileReader(); - reader.readAsDataURL(file); - return new Promise(resolve => { - reader.onload = function () { - resolve(reader.result) - }; - reader.onerror = function (error) { - - }; - }); - - } - - - bookMeeting() { - let data = { - roomId: this.roomId, - members: [] - } - this.openNewEventPage.emit(data); - } - - - chatsend() { - - } async _openChatOptions() { const roomId = this.roomId; @@ -1141,15 +911,6 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy } - - - pdfPreview() { - const options: DocumentViewerOptions = { - title: 'My App' - }; - DocumentViewer.viewDocument - } - async audioPreview(msg) { if (!msg.attachments[0].title_link || msg.attachments[0].title_link === null || msg.attachments[0].title_link === '') { @@ -1314,68 +1075,6 @@ export class MessagesPage implements OnInit, OnChanges, AfterViewInit, OnDestroy }, 1000) } - async getRoomInfo() { - // let room = await this.chatService.getRoomInfo(this.roomId).toPromise(); - // this.room = room['room']; - // if (this.room.name) { - // try { - // this.roomName = this.room.name.split('-').join(' '); - // } catch (error) { - // this.roomName = this.room.name; - // } - - // } - - - // if (SessionStore.user.ChatData.data.userId == this.room.u._id) { - // this.isAdmin = true - // } else { - // this.isAdmin = false - // } - - // if (this.room.customFields.countDownDate) { - // this.roomCountDownDate = this.room.customFields.countDownDate; - // } - } - - - async 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); - resolve(compressedBase64); - }; - - image.onerror = (error) => { - reject(error); - }; - }); - } - - }