@chat21/chat21-web-widget 5.0.88 → 5.0.89-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -6
- package/package.json +1 -1
- package/src/app/app.module.ts +5 -2
- package/src/app/component/conversation-detail/conversation/conversation.component.html +0 -1
- package/src/app/component/conversation-detail/conversation/conversation.component.ts +3 -4
- package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +29 -0
- package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.scss +103 -0
- package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +23 -0
- package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +96 -0
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +14 -3
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +26 -10
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +123 -41
- package/src/app/component/message/audio-track/audio-track.component.html +32 -0
- package/src/app/component/message/audio-track/audio-track.component.scss +107 -0
- package/src/app/component/message/{audio/audio.component.spec.ts → audio-track/audio-track.component.spec.ts} +6 -6
- package/src/app/component/message/audio-track/audio-track.component.ts +147 -0
- package/src/app/component/message/bubble-message/bubble-message.component.html +30 -5
- package/src/app/component/message/bubble-message/bubble-message.component.scss +7 -0
- package/src/app/component/message/bubble-message/bubble-message.component.ts +1 -0
- package/src/app/modals/confirm-close/confirm-close.component.html +6 -6
- package/src/app/modals/confirm-close/confirm-close.component.scss +3 -14
- package/src/app/modals/confirm-close/confirm-close.component.ts +4 -19
- package/src/app/utils/globals.ts +1 -1
- package/src/assets/twp/index-dev.html +1 -0
- package/src/app/component/message/audio/audio.component.html +0 -20
- package/src/app/component/message/audio/audio.component.scss +0 -122
- package/src/app/component/message/audio/audio.component.ts +0 -122
package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts
CHANGED
|
@@ -60,6 +60,8 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
60
60
|
// ========= end:: send image ========= //
|
|
61
61
|
|
|
62
62
|
// isNewConversation = true;
|
|
63
|
+
isStartRec: boolean = false;
|
|
64
|
+
isStopRec: boolean = false;
|
|
63
65
|
textInputTextArea: string;
|
|
64
66
|
conversationHandlerService: ConversationHandlerService
|
|
65
67
|
|
|
@@ -99,7 +101,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
ngAfterViewInit() {
|
|
102
|
-
this.logger.
|
|
104
|
+
this.logger.log('[CONV-FOOTER] --------ngAfterViewInit: conversation-footer-------- ');
|
|
103
105
|
// setTimeout(() => {
|
|
104
106
|
this.showEmojiPicker = true
|
|
105
107
|
// }, 500);
|
|
@@ -109,19 +111,18 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
109
111
|
// START LOAD IMAGE //
|
|
110
112
|
/** load the selected image locally and open the pop up preview */
|
|
111
113
|
detectFiles(event) {
|
|
112
|
-
this.logger.
|
|
113
|
-
|
|
114
|
+
this.logger.log('[CONV-FOOTER] detectFiles: ', event);
|
|
114
115
|
if (event) {
|
|
115
116
|
this.selectedFiles = event.target.files;
|
|
116
|
-
this.logger.
|
|
117
|
+
this.logger.log('[CONV-FOOTER] AppComponent:detectFiles::selectedFiles', this.selectedFiles);
|
|
117
118
|
// this.onAttachmentButtonClicked.emit(this.selectedFiles)
|
|
118
119
|
if (this.selectedFiles == null) {
|
|
119
120
|
this.isFilePendingToUpload = false;
|
|
120
121
|
} else {
|
|
121
122
|
this.isFilePendingToUpload = true;
|
|
122
123
|
}
|
|
123
|
-
this.logger.
|
|
124
|
-
this.logger.
|
|
124
|
+
this.logger.log('[CONV-FOOTER] AppComponent:detectFiles::selectedFiles::isFilePendingToUpload', this.isFilePendingToUpload);
|
|
125
|
+
this.logger.log('[CONV-FOOTER] fileChange: ', event.target.files);
|
|
125
126
|
if (event.target.files.length <= 0) {
|
|
126
127
|
this.isFilePendingToUpload = false;
|
|
127
128
|
} else {
|
|
@@ -184,16 +185,18 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
|
|
189
|
+
|
|
187
190
|
|
|
188
191
|
loadFile() {
|
|
189
|
-
this.logger.
|
|
192
|
+
this.logger.log('[CONV-FOOTER] that.fileXLoad: ', this.arrayFilesLoad);
|
|
190
193
|
// at the moment I only manage the upload of one image at a time
|
|
191
194
|
if (this.arrayFilesLoad[0] && this.arrayFilesLoad[0].file) {
|
|
192
195
|
const fileXLoad = this.arrayFilesLoad[0].file;
|
|
193
196
|
const uid = this.arrayFilesLoad[0].uid;
|
|
194
197
|
const type = this.arrayFilesLoad[0].type;
|
|
195
198
|
const size = this.arrayFilesLoad[0].size
|
|
196
|
-
this.logger.
|
|
199
|
+
this.logger.log('[CONV-FOOTER] that.fileXLoad: ', type);
|
|
197
200
|
let metadata;
|
|
198
201
|
if (type.startsWith('image') && !type.includes('svg')) {
|
|
199
202
|
metadata = {
|
|
@@ -214,7 +217,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
214
217
|
'size': size
|
|
215
218
|
};
|
|
216
219
|
}
|
|
217
|
-
this.logger.
|
|
220
|
+
this.logger.log('[CONV-FOOTER] metadata -------> ', metadata);
|
|
218
221
|
// this.scrollToBottom();
|
|
219
222
|
// 1 - aggiungo messaggio localmente
|
|
220
223
|
// this.addLocalMessageImage(metadata);
|
|
@@ -231,17 +234,20 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
231
234
|
|
|
232
235
|
uploadSingle(metadata, file, messageText?: string) {
|
|
233
236
|
const that = this;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
+
try {
|
|
238
|
+
const send_order_btn = <HTMLInputElement>document.getElementById('chat21-start-upload-doc');
|
|
239
|
+
send_order_btn.disabled = true;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
that.logger.log('[CONV-FOOTER] error::', error);
|
|
242
|
+
}
|
|
243
|
+
that.logger.log('[CONV-FOOTER] AppComponent::uploadSingle::', metadata, file);
|
|
237
244
|
// const file = this.selectedFiles.item(0);
|
|
238
245
|
const currentUpload = new UploadModel(file);
|
|
239
|
-
|
|
240
246
|
// const uploadTask = this.upSvc.pushUpload(currentUpload);
|
|
241
247
|
// uploadTask.then(snapshot => {
|
|
242
248
|
// return snapshot.ref.getDownloadURL(); // Will return a promise with the download link
|
|
243
249
|
// }).then(downloadURL => {
|
|
244
|
-
// that.logger.
|
|
250
|
+
// that.logger.log('[CONV-FOOTER] AppComponent::uploadSingle:: downloadURL', downloadURL]);
|
|
245
251
|
// that.g.wdLog([`Successfully uploaded file and got download link - ${downloadURL}`]);
|
|
246
252
|
|
|
247
253
|
// metadata.src = downloadURL;
|
|
@@ -261,8 +267,8 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
261
267
|
// this.resetLoadImage();
|
|
262
268
|
|
|
263
269
|
this.uploadService.upload(this.senderId, currentUpload).then(data => {
|
|
264
|
-
that.logger.
|
|
265
|
-
that.logger.
|
|
270
|
+
that.logger.log('[CONV-FOOTER] AppComponent::uploadSingle:: downloadURL', data);
|
|
271
|
+
that.logger.log(`[CONV-FOOTER] Successfully uploaded file and got download link - ${data}`);
|
|
266
272
|
|
|
267
273
|
metadata.src = data.src;
|
|
268
274
|
metadata.downloadURL = data.downloadURL;
|
|
@@ -291,7 +297,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
291
297
|
that.logger.error(`[CONV-FOOTER] uploadSingle:: Failed to upload file and get link - ${error}`);
|
|
292
298
|
that.isFilePendingToUpload = false;
|
|
293
299
|
});
|
|
294
|
-
that.logger.
|
|
300
|
+
that.logger.log('[CONV-FOOTER] reader-result: ', file);
|
|
295
301
|
}
|
|
296
302
|
|
|
297
303
|
/**
|
|
@@ -304,7 +310,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
304
310
|
sendMessage(msg: string, type: string, metadata?: any, additional_attributes?: any) { // sponziello
|
|
305
311
|
(metadata) ? metadata = metadata : metadata = '';
|
|
306
312
|
this.onEmojiiPickerShow.emit(false)
|
|
307
|
-
this.logger.
|
|
313
|
+
this.logger.log('[CONV-FOOTER] SEND MESSAGE: ', msg, type, metadata, additional_attributes);
|
|
308
314
|
if (msg && msg.trim() !== '' || type === TYPE_MSG_IMAGE || type === TYPE_MSG_FILE ) {
|
|
309
315
|
|
|
310
316
|
// msg = htmlEntities(msg);
|
|
@@ -387,7 +393,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
387
393
|
}
|
|
388
394
|
|
|
389
395
|
private restoreTextArea() {
|
|
390
|
-
// that.logger.
|
|
396
|
+
// that.logger.log('[CONV-FOOTER] AppComponent:restoreTextArea::restoreTextArea');
|
|
391
397
|
this.resizeInputField();
|
|
392
398
|
const textArea = (<HTMLInputElement>document.getElementById('chat21-main-message-context'));
|
|
393
399
|
this.textInputTextArea = ''; // clear the textarea
|
|
@@ -397,7 +403,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
397
403
|
if(textArea.style.height > this.HEIGHT_DEFAULT){
|
|
398
404
|
document.getElementById('chat21-button-send').style.removeProperty('right')
|
|
399
405
|
}
|
|
400
|
-
this.logger.
|
|
406
|
+
this.logger.log('[CONV-FOOTER] AppComponent:restoreTextArea::restoreTextArea::textArea:', 'restored');
|
|
401
407
|
}
|
|
402
408
|
this.setFocusOnId('chat21-main-message-context');
|
|
403
409
|
}
|
|
@@ -411,7 +417,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
411
417
|
try {
|
|
412
418
|
const target = document.getElementById('chat21-main-message-context') as HTMLInputElement;
|
|
413
419
|
// tslint:disable-next-line:max-line-length
|
|
414
|
-
// that.logger.
|
|
420
|
+
// that.logger.log('[CONV-FOOTER] H:: this.textInputTextArea', (document.getElementById('chat21-main-message-context') as HTMLInputElement).value , target.style.height, target.scrollHeight, target.offsetHeight, target.clientHeight);
|
|
415
421
|
target? target.style.height = '100%': null;
|
|
416
422
|
if (target && target.value === '\n') {
|
|
417
423
|
target.value = '';
|
|
@@ -431,28 +437,104 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
431
437
|
}
|
|
432
438
|
}
|
|
433
439
|
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
onStartRecording() {
|
|
443
|
+
this.isStartRec = true;
|
|
444
|
+
this.isStopRec = false;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
onDeleteRecording(){
|
|
448
|
+
this.isStartRec = false;
|
|
449
|
+
this.isStopRec = false;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
onEndRecording(audioBlob: Blob | null) {
|
|
453
|
+
this.isStartRec = false;
|
|
454
|
+
this.isStopRec = true;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
onSendRecording(audioBlob: Blob | null) {
|
|
460
|
+
this.isStartRec = false;
|
|
461
|
+
if (audioBlob) {
|
|
462
|
+
this.convertBlobToBase64(audioBlob);
|
|
463
|
+
this.isStopRec = false;
|
|
464
|
+
} else {
|
|
465
|
+
this.isStopRec = false;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
// Funzione per convertire Blob in Base64 usando FileReader
|
|
472
|
+
async convertBlobToBase64(audioBlob: Blob) {
|
|
473
|
+
let that = this;
|
|
474
|
+
if (audioBlob) {
|
|
475
|
+
this.isFilePendingToUpload = true;
|
|
476
|
+
} else {
|
|
477
|
+
this.isFilePendingToUpload = false;
|
|
478
|
+
}
|
|
479
|
+
const type = audioBlob.type;
|
|
480
|
+
const size = audioBlob.size;
|
|
481
|
+
const reader = new FileReader();
|
|
482
|
+
reader.readAsDataURL(audioBlob);
|
|
483
|
+
//reader.addEventListener('load', function () {
|
|
484
|
+
reader.onloadend = () => {
|
|
485
|
+
that.isFileSelected = true;
|
|
486
|
+
// const base64data = reader.result as string;
|
|
487
|
+
that.logger.log('[CONV-FOOTER] onload file');
|
|
488
|
+
const fileXLoad = {
|
|
489
|
+
src: reader.result.toString(),
|
|
490
|
+
title: 'audio-file'
|
|
491
|
+
};
|
|
492
|
+
const uid = (new Date().getTime()).toString(36);
|
|
493
|
+
that.arrayFilesLoad[0] = { uid: uid, file: fileXLoad, type: type, size: size };
|
|
494
|
+
that.logger.log('[CONV-FOOTER] OK: ', that.arrayFilesLoad[0]);
|
|
495
|
+
// SEND MESSAGE
|
|
496
|
+
let metadata = {
|
|
497
|
+
'name': 'audio-file',
|
|
498
|
+
'src': fileXLoad.src,
|
|
499
|
+
'type': type,
|
|
500
|
+
'uid': uid,
|
|
501
|
+
'size': size
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const file = new File([audioBlob], "audio-file.mp3", {
|
|
505
|
+
type: audioBlob.type,
|
|
506
|
+
lastModified: Date.now()
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
that.uploadSingle(metadata, file, '');
|
|
510
|
+
};
|
|
511
|
+
//}, false);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
|
|
434
516
|
onTextAreaChange(){
|
|
435
517
|
this.resizeInputField()
|
|
436
518
|
this.setWritingMessages(this.textInputTextArea)
|
|
437
519
|
}
|
|
438
520
|
|
|
439
521
|
onSendPressed(event) {
|
|
440
|
-
this.logger.
|
|
522
|
+
this.logger.log('[CONV-FOOTER] onSendPressed:event', event);
|
|
441
523
|
event.preventDefault();
|
|
442
|
-
this.logger.
|
|
524
|
+
this.logger.log('[CONV-FOOTER] AppComponent::onSendPressed::isFilePendingToUpload:', this.isFilePendingToUpload);
|
|
443
525
|
if (this.isFilePendingToUpload) {
|
|
444
|
-
this.logger.
|
|
526
|
+
this.logger.log('[CONV-FOOTER] AppComponent::onSendPressed', 'is a file');
|
|
445
527
|
// its a file
|
|
446
528
|
this.loadFile();
|
|
447
529
|
this.isFilePendingToUpload = false;
|
|
448
530
|
// disabilito pulsanti
|
|
449
|
-
this.logger.
|
|
531
|
+
this.logger.log('[CONV-FOOTER] AppComponent::onSendPressed::isFilePendingToUpload:', this.isFilePendingToUpload);
|
|
450
532
|
} else {
|
|
451
533
|
if ( this.textInputTextArea && this.textInputTextArea.length > 0 ) {
|
|
452
|
-
this.logger.
|
|
534
|
+
this.logger.log('[CONV-FOOTER] AppComponent::onSendPressed', 'is a message');
|
|
453
535
|
// its a message
|
|
454
536
|
if (this.textInputTextArea.trim() !== '') {
|
|
455
|
-
// that.logger.
|
|
537
|
+
// that.logger.log('[CONV-FOOTER] sendMessage -> ', this.textInputTextArea);
|
|
456
538
|
// this.resizeInputField();
|
|
457
539
|
// this.messagingService.sendMessage(msg, TYPE_MSG_TEXT);
|
|
458
540
|
// this.setDepartment();
|
|
@@ -526,7 +608,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
526
608
|
setTimeout(function () {
|
|
527
609
|
const textarea = document.getElementById(id);
|
|
528
610
|
if (textarea) {
|
|
529
|
-
// that.logger.
|
|
611
|
+
// that.logger.log('[CONV-FOOTER] 1--------> FOCUSSSSSS : ', textarea);
|
|
530
612
|
textarea.setAttribute('value', ' ');
|
|
531
613
|
textarea.focus();
|
|
532
614
|
}
|
|
@@ -566,7 +648,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
566
648
|
this.textInputTextArea = ((document.getElementById('chat21-main-message-context') as HTMLInputElement).value);
|
|
567
649
|
if (keyCode === 13) { // ENTER pressed
|
|
568
650
|
if (this.textInputTextArea && this.textInputTextArea.trim() !== '') {
|
|
569
|
-
// that.logger.
|
|
651
|
+
// that.logger.log('[CONV-FOOTER] sendMessage -> ', this.textInputTextArea);
|
|
570
652
|
// this.resizeInputField();
|
|
571
653
|
// this.messagingService.sendMessage(msg, TYPE_MSG_TEXT);
|
|
572
654
|
// this.setDepartment();
|
|
@@ -598,14 +680,14 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
598
680
|
this.resizeInputField()
|
|
599
681
|
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
|
|
600
682
|
let file = null;
|
|
601
|
-
this.logger.
|
|
683
|
+
this.logger.log('[CONV-FOOTER] onPaste items ', items);
|
|
602
684
|
for (const item of items) {
|
|
603
|
-
this.logger.
|
|
604
|
-
this.logger.
|
|
685
|
+
this.logger.log('[CONV-FOOTER] onPaste item ', item);
|
|
686
|
+
this.logger.log('[CONV-FOOTER] onPaste item.type ', item.type);
|
|
605
687
|
if (item.type.startsWith("image")) {
|
|
606
688
|
// SEND TEXT MESSAGE IF EXIST
|
|
607
689
|
// if(this.textInputTextArea){
|
|
608
|
-
// this.logger.
|
|
690
|
+
// this.logger.log('[CONV-FOOTER] onPaste texttt ', this.textInputTextArea);
|
|
609
691
|
// this.sendMessage(this.textInputTextArea, TYPE_MSG_TEXT)
|
|
610
692
|
// }
|
|
611
693
|
|
|
@@ -615,12 +697,12 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
615
697
|
this.logger.error('[CONV-FOOTER] onPaste - error while restoring textArea:',e)
|
|
616
698
|
}
|
|
617
699
|
|
|
618
|
-
this.logger.
|
|
700
|
+
this.logger.log('[CONV-FOOTER] onPaste item.type', item.type);
|
|
619
701
|
file = item.getAsFile();
|
|
620
702
|
const data = {target: new ClipboardEvent('').clipboardData || new DataTransfer()};
|
|
621
703
|
data.target.items.add(new File([file], file.name, { type: file.type }));
|
|
622
|
-
this.logger.
|
|
623
|
-
this.logger.
|
|
704
|
+
this.logger.log('[CONV-FOOTER] onPaste data', data);
|
|
705
|
+
this.logger.log('[CONV-FOOTER] onPaste file ', file);
|
|
624
706
|
this.detectFiles(data)
|
|
625
707
|
}
|
|
626
708
|
}
|
|
@@ -629,15 +711,15 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
|
|
|
629
711
|
onDrop(event){
|
|
630
712
|
const items = event.dataTransfer.files;
|
|
631
713
|
let file = null;
|
|
632
|
-
this.logger.
|
|
714
|
+
this.logger.log('[CONV-FOOTER] onDrop items ', items);
|
|
633
715
|
for (const item of items) {
|
|
634
|
-
this.logger.
|
|
635
|
-
this.logger.
|
|
716
|
+
this.logger.log('[CONV-FOOTER] onDrop item ', item);
|
|
717
|
+
this.logger.log('[CONV-FOOTER] onDrop item.type ', item.type);
|
|
636
718
|
|
|
637
719
|
const data = {target: {files: new DataTransfer()}}
|
|
638
720
|
data.target.files = items
|
|
639
|
-
this.logger.
|
|
640
|
-
this.logger.
|
|
721
|
+
this.logger.log('[CONV-FOOTER] onDrop data', data);
|
|
722
|
+
this.logger.log('[CONV-FOOTER] onDrop file ', file);
|
|
641
723
|
this.detectFiles(data)
|
|
642
724
|
}
|
|
643
725
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
<!-- <audio *ngIf="metadata" controls>
|
|
3
|
+
<source [src]="metadata.src" type="audio/mpeg">
|
|
4
|
+
</audio> *ngIf="!metadata"-->
|
|
5
|
+
<div class="audio-container">
|
|
6
|
+
|
|
7
|
+
<div class="audio-track">
|
|
8
|
+
<button *ngIf="!isPlaying" class="play-pause" (click)="playPauseAudio()">
|
|
9
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px">
|
|
10
|
+
<path d="M320-200v-560l440 280-440 280Z"/>
|
|
11
|
+
</svg>
|
|
12
|
+
<!-- <i class="material-icons">play_arrow</i> -->
|
|
13
|
+
</button>
|
|
14
|
+
<button *ngIf="isPlaying" class="play-pause" (click)="playPauseAudio()">
|
|
15
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px">
|
|
16
|
+
<path d="M560-200v-560h160v560H560Zm-320 0v-560h160v560H240Z"/>
|
|
17
|
+
</svg>
|
|
18
|
+
<!-- <i class="material-icons">pause</i> -->
|
|
19
|
+
</button>
|
|
20
|
+
<div class="duration" [style.color]="color" [style.font-size]="fontSize" >
|
|
21
|
+
<span *ngIf="!isPlaying">{{ audioDuration ? formatTime(audioDuration) : '00:00' }}</span>
|
|
22
|
+
<span *ngIf="isPlaying">{{ formatTime(currentTime) }}</span>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="audio-player-custom">
|
|
28
|
+
<audio #audioElement [src]="audioUrl"></audio>
|
|
29
|
+
<canvas #canvasElement class="waveformCanvas"></canvas>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
</div>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
|
|
2
|
+
:host {
|
|
3
|
+
--backgroundColor: #{var(--blue)};
|
|
4
|
+
--textColor: #{var(--bck-msg-sent)};
|
|
5
|
+
--hoverBackgroundColor: #{var(--bck-msg-sent)};
|
|
6
|
+
--hoverTextColor: #{var(--blue)};
|
|
7
|
+
--max-width: #{var(--button-in-msg-max-width)};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.audio-container{
|
|
11
|
+
position: relative;
|
|
12
|
+
display: inline-flex;
|
|
13
|
+
width: 100%;
|
|
14
|
+
padding: 0px 12px;
|
|
15
|
+
margin: 6px 0px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
audio {
|
|
19
|
+
width: 272px;
|
|
20
|
+
height: 30px;
|
|
21
|
+
margin: 0;
|
|
22
|
+
padding: 10px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.audio-recorder {
|
|
26
|
+
text-align: center;
|
|
27
|
+
margin: 0px;
|
|
28
|
+
display: inline-flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
height: 100%;
|
|
32
|
+
width: 100%;
|
|
33
|
+
float: left;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
button {
|
|
37
|
+
margin: 0px;
|
|
38
|
+
padding: 0px;
|
|
39
|
+
font-size: 16px;
|
|
40
|
+
border: none;
|
|
41
|
+
background-color: transparent;
|
|
42
|
+
color: var(--icon-fill-color);
|
|
43
|
+
fill: var(--icon-fill-color);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.waveformCanvas {
|
|
47
|
+
width: 100%;
|
|
48
|
+
height: 28px;
|
|
49
|
+
z-index: 1;
|
|
50
|
+
padding: 0px;
|
|
51
|
+
margin: 0%;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.audio-track {
|
|
55
|
+
// width: 247px;//272px;
|
|
56
|
+
// height: 30px;
|
|
57
|
+
position: relative;
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
// margin: 0 13px;
|
|
61
|
+
margin: 0px;
|
|
62
|
+
.play-pause {
|
|
63
|
+
font-size: 20px;
|
|
64
|
+
width: 30px;
|
|
65
|
+
background-color: transparent;
|
|
66
|
+
border-radius: 50%;
|
|
67
|
+
height: 30px;
|
|
68
|
+
margin: 0px;
|
|
69
|
+
transition: background-color 0.5s ease;
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
.play-pause:hover {
|
|
73
|
+
// background-color: #ddd;
|
|
74
|
+
// background-color: rgb(82, 160, 252);
|
|
75
|
+
background-color: #fff;
|
|
76
|
+
svg{
|
|
77
|
+
//fill:#fff;
|
|
78
|
+
fill: rgb(82, 160, 252);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
.duration {
|
|
82
|
+
padding: 0 3px;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.audio-player-custom {
|
|
87
|
+
// width: 200px;
|
|
88
|
+
// height: 32px;
|
|
89
|
+
// margin-left: 75px;
|
|
90
|
+
// position: absolute;
|
|
91
|
+
// overflow: hidden;
|
|
92
|
+
// z-index: 1;
|
|
93
|
+
// display: flex;
|
|
94
|
+
// align-items: center;
|
|
95
|
+
|
|
96
|
+
// width: calc(100% - 75px);
|
|
97
|
+
// height: 32px;
|
|
98
|
+
// margin-left: 65px;
|
|
99
|
+
// position: absolute;
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
z-index: 1;
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
width: 100%;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { AudioTrackComponent } from './audio-track.component';
|
|
4
4
|
|
|
5
|
-
describe('
|
|
6
|
-
let component:
|
|
7
|
-
let fixture: ComponentFixture<
|
|
5
|
+
describe('AudioTrackComponent', () => {
|
|
6
|
+
let component: AudioTrackComponent;
|
|
7
|
+
let fixture: ComponentFixture<AudioTrackComponent>;
|
|
8
8
|
|
|
9
9
|
beforeEach(async () => {
|
|
10
10
|
await TestBed.configureTestingModule({
|
|
11
|
-
declarations: [
|
|
11
|
+
declarations: [ AudioTrackComponent ]
|
|
12
12
|
})
|
|
13
13
|
.compileComponents();
|
|
14
14
|
|
|
15
|
-
fixture = TestBed.createComponent(
|
|
15
|
+
fixture = TestBed.createComponent(AudioTrackComponent);
|
|
16
16
|
component = fixture.componentInstance;
|
|
17
17
|
fixture.detectChanges();
|
|
18
18
|
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Component, ElementRef, AfterViewInit, Input, ViewChild } from '@angular/core';
|
|
2
|
+
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
|
3
|
+
import { convertColorToRGBA } from 'src/chat21-core/utils/utils';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'chat-audio-track',
|
|
7
|
+
templateUrl: './audio-track.component.html',
|
|
8
|
+
styleUrls: ['./audio-track.component.scss']
|
|
9
|
+
})
|
|
10
|
+
export class AudioTrackComponent implements AfterViewInit {
|
|
11
|
+
|
|
12
|
+
@ViewChild('audioElement', { static: true }) audioElement!: ElementRef<HTMLAudioElement>;
|
|
13
|
+
@ViewChild('canvasElement', { static: true }) waveformCanvas!: ElementRef<HTMLCanvasElement>;
|
|
14
|
+
|
|
15
|
+
@Input() audioBlob: Blob | null = null;
|
|
16
|
+
@Input() metadata: any | null = null;
|
|
17
|
+
@Input() color: string;
|
|
18
|
+
@Input() fontSize: string;
|
|
19
|
+
@Input() stylesMap: Map<string, string>;
|
|
20
|
+
|
|
21
|
+
audioUrl: SafeUrl | null = null;
|
|
22
|
+
rawAudioUrl: string | null = null;
|
|
23
|
+
audioContext!: AudioContext;
|
|
24
|
+
audioBuffer!: AudioBuffer;
|
|
25
|
+
audioDuration: number | null = null;
|
|
26
|
+
currentTime: number = 0;
|
|
27
|
+
isPlaying: boolean = false;
|
|
28
|
+
|
|
29
|
+
constructor(private sanitizer: DomSanitizer) {}
|
|
30
|
+
|
|
31
|
+
ngAfterViewInit() {
|
|
32
|
+
console.log('stylesssss', this.stylesMap)
|
|
33
|
+
if (this.audioBlob) {
|
|
34
|
+
this.rawAudioUrl = URL.createObjectURL(this.audioBlob);
|
|
35
|
+
this.audioUrl = this.sanitizer.bypassSecurityTrustUrl(this.rawAudioUrl);
|
|
36
|
+
this.setupAudioContext();
|
|
37
|
+
} else {
|
|
38
|
+
this.rawAudioUrl = this.metadata.src;
|
|
39
|
+
this.audioUrl = this.sanitizer.bypassSecurityTrustUrl(this.rawAudioUrl);
|
|
40
|
+
this.setupAudioContext();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async setupAudioContext() {
|
|
45
|
+
this.audioContext = new AudioContext();
|
|
46
|
+
if (this.rawAudioUrl) {
|
|
47
|
+
const response = await fetch(this.rawAudioUrl);
|
|
48
|
+
const audioData = await response.arrayBuffer();
|
|
49
|
+
this.audioBuffer = await this.audioContext.decodeAudioData(audioData);
|
|
50
|
+
this.getAudioDuration();
|
|
51
|
+
this.drawWaveform(this.audioBuffer);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
drawWaveform(audioBuffer: AudioBuffer) {
|
|
56
|
+
const canvas = this.waveformCanvas.nativeElement;
|
|
57
|
+
const canvasCtx = canvas.getContext('2d');
|
|
58
|
+
if (!canvasCtx) return;
|
|
59
|
+
const width = canvas.width;
|
|
60
|
+
const height = canvas.height;
|
|
61
|
+
const rawData = audioBuffer.getChannelData(0);
|
|
62
|
+
|
|
63
|
+
const samples = 60;
|
|
64
|
+
const blockSize = Math.floor(rawData.length / samples);
|
|
65
|
+
const waveform = new Float32Array(samples);
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < samples; i++) {
|
|
68
|
+
let sum = 0;
|
|
69
|
+
for (let j = 0; j < blockSize; j++) {
|
|
70
|
+
sum += Math.abs(rawData[i * blockSize + j]);
|
|
71
|
+
}
|
|
72
|
+
waveform[i] = sum / blockSize;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
canvasCtx.clearRect(0, 0, width, height);
|
|
76
|
+
const padding = 1;
|
|
77
|
+
const barWidth = (width / samples) - padding * 2;
|
|
78
|
+
const audio = this.audioElement.nativeElement;
|
|
79
|
+
const playedPercent = audio.currentTime / this.audioDuration;
|
|
80
|
+
// console.log('playedPercent: ', audio.currentTime, this.audioDuration);
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < samples; i++) {
|
|
83
|
+
var barHeight = waveform[i] * height * 4;
|
|
84
|
+
if (barHeight < 4) barHeight = 4;
|
|
85
|
+
const x = i * (barWidth + padding * 2) + padding;
|
|
86
|
+
|
|
87
|
+
if (i / samples < playedPercent) {
|
|
88
|
+
canvasCtx.fillStyle = this.color;
|
|
89
|
+
} else {
|
|
90
|
+
canvasCtx.fillStyle = convertColorToRGBA(this.color, 50);;
|
|
91
|
+
}
|
|
92
|
+
canvasCtx.fillRect(x, height / 2 - barHeight, barWidth, barHeight);
|
|
93
|
+
canvasCtx.fillRect(x, height / 2, barWidth, barHeight);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
playPauseAudio() {
|
|
98
|
+
const audio = this.audioElement.nativeElement;
|
|
99
|
+
if (audio.paused) {
|
|
100
|
+
this.isPlaying = true;
|
|
101
|
+
this.updateWaveform();
|
|
102
|
+
audio.play();
|
|
103
|
+
this.audioContext.resume();
|
|
104
|
+
} else {
|
|
105
|
+
audio.pause();
|
|
106
|
+
this.isPlaying = false;
|
|
107
|
+
}
|
|
108
|
+
audio.ontimeupdate = () => {
|
|
109
|
+
this.currentTime = audio.currentTime;
|
|
110
|
+
this.updateWaveform();
|
|
111
|
+
};
|
|
112
|
+
audio.onended = () => {
|
|
113
|
+
this.isPlaying = false;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
updateWaveform() {
|
|
119
|
+
this.drawWaveform(this.audioBuffer);
|
|
120
|
+
if (this.isPlaying) {
|
|
121
|
+
requestAnimationFrame(() => this.updateWaveform());
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
formatTime(seconds: number): string {
|
|
126
|
+
const minutes = Math.floor(seconds / 60);
|
|
127
|
+
const sec = Math.floor(seconds % 60);
|
|
128
|
+
return `${minutes}:${sec < 10 ? '0' + sec : sec}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getAudioDuration() {
|
|
132
|
+
const audio = new Audio();
|
|
133
|
+
audio.src = this.rawAudioUrl!;
|
|
134
|
+
audio.addEventListener('loadedmetadata', () => {
|
|
135
|
+
if (audio.duration === Infinity) {
|
|
136
|
+
audio.currentTime = Number.MAX_SAFE_INTEGER;
|
|
137
|
+
audio.ontimeupdate = () => {
|
|
138
|
+
audio.ontimeupdate = null;
|
|
139
|
+
audio.currentTime = 0;
|
|
140
|
+
this.audioDuration = audio.duration;
|
|
141
|
+
};
|
|
142
|
+
} else {
|
|
143
|
+
this.audioDuration = audio.duration;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|