diff --git a/src/components/chatInput.ts b/src/components/chatInput.ts index 1c5f70b9..6e2e89c7 100644 --- a/src/components/chatInput.ts +++ b/src/components/chatInput.ts @@ -11,7 +11,9 @@ import appMessagesManager from "../lib/appManagers/appMessagesManager"; import initEmoticonsDropdown, { EMOTICONSSTICKERGROUP } from "./emoticonsDropdown"; import lottieLoader from "../lib/lottieLoader"; import { Layouter, RectPart } from "./groupedLayout"; -import Recorder from '../opus-recorder/dist/recorder.min'; +import Recorder from '../../public/recorder.min'; +//import Recorder from '../opus-recorder/dist/recorder.min'; +import opusDecodeController from "../lib/opusDecodeController"; export class ChatInput { public pageEl = document.getElementById('page-chats') as HTMLDivElement; @@ -20,7 +22,7 @@ export class ChatInput { public inputMessageContainer = document.getElementsByClassName('input-message-container')[0] as HTMLDivElement; public inputScroll = new Scrollable(this.inputMessageContainer); public btnSend = document.getElementById('btn-send') as HTMLButtonElement; - public btnCancelRecord = this.btnSend.previousElementSibling as HTMLButtonElement; + public btnCancelRecord = this.btnSend.parentElement.previousElementSibling as HTMLButtonElement; public emoticonsDropdown: HTMLDivElement = null; public emoticonsTimeout: number = 0; public toggleEmoticons: HTMLButtonElement; @@ -28,7 +30,7 @@ export class ChatInput { public lastUrl = ''; public lastTimeType = 0; - private inputContainer = this.btnSend.parentElement as HTMLDivElement; + private inputContainer = this.btnSend.parentElement.parentElement as HTMLDivElement; public attachMenu: { container?: HTMLButtonElement, @@ -68,6 +70,8 @@ export class ChatInput { private recording = false; private recordCanceled = false; private recordTimeEl = this.inputContainer.querySelector('.record-time') as HTMLDivElement; + private recordRippleEl = this.inputContainer.querySelector('.record-ripple') as HTMLDivElement; + private recordStartTime = 0; constructor() { this.toggleEmoticons = this.pageEl.querySelector('.toggle-emoticons') as HTMLButtonElement; @@ -470,12 +474,37 @@ export class ChatInput { this.btnSend.classList.add('tgico-send'); this.inputContainer.classList.add('is-recording'); this.recording = true; + opusDecodeController.setKeepAlive(true); - let startTime = Date.now(); + this.recordStartTime = Date.now(); + + const sourceNode: MediaStreamAudioSourceNode = this.recorder.sourceNode; + const context = sourceNode.context; + + const analyser = context.createAnalyser(); + sourceNode.connect(analyser); + //analyser.connect(context.destination); + analyser.fftSize = 32; + + const frequencyData = new Uint8Array(analyser.frequencyBinCount); + const max = frequencyData.length * 255; + const min = 54 / 150; let r = () => { if(!this.recording) return; - let diff = Date.now() - startTime; + analyser.getByteFrequencyData(frequencyData); + + let sum = 0; + frequencyData.forEach(value => { + sum += value; + }); + + let percents = Math.min(1, (sum / max) + min); + console.log('frequencyData', frequencyData, percents); + + this.recordRippleEl.style.transform = `scale(${percents})`; + + let diff = Date.now() - this.recordStartTime; let ms = diff % 1000; let formatted = ('' + (diff / 1000)).toHHMMSS() + ',' + ('00' + Math.round(ms / 10)).slice(-2); @@ -495,20 +524,23 @@ export class ChatInput { this.btnCancelRecord.addEventListener('click', () => { this.recordCanceled = true; this.recorder.stop(); + opusDecodeController.setKeepAlive(false); }); this.recorder.onstop = () => { this.recording = false; this.inputContainer.classList.remove('is-recording'); this.btnSend.classList.remove('tgico-send'); + this.recordRippleEl.style.transform = ''; }; this.recorder.ondataavailable = (typedArray: Uint8Array) => { if(this.recordCanceled) return; + const duration = (Date.now() - this.recordStartTime) / 1000 | 0; const dataBlob = new Blob([typedArray], {type: 'audio/ogg'}); - const fileName = new Date().toISOString() + ".opus"; - console.log('Recorder data received', typedArray, dataBlob); + /* const fileName = new Date().toISOString() + ".opus"; + console.log('Recorder data received', typedArray, dataBlob); */ /* var url = URL.createObjectURL( dataBlob ); @@ -529,11 +561,21 @@ export class ChatInput { return; */ - let peerID = appImManager.peerID; - appMessagesManager.sendFile(peerID, dataBlob, { - isVoiceMessage: true, - duration: 0, - isMedia: true + let perf = performance.now(); + opusDecodeController.decode(typedArray, true).then(result => { + console.log('WAVEFORM!:', /* waveform, */performance.now() - perf); + + opusDecodeController.setKeepAlive(false); + + let peerID = appImManager.peerID; + // тут objectURL ставится уже с audio/wav + appMessagesManager.sendFile(peerID, dataBlob, { + isVoiceMessage: true, + isMedia: true, + duration, + waveform: result.waveform, + objectURL: result.url + }); }); /* const url = URL.createObjectURL(dataBlob); diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 907ed0ac..a1537b0a 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -192,7 +192,7 @@ let formatDate = (timestamp: number) => { export function wrapDocument(doc: MTDocument, withTime = false, uploading = false): HTMLDivElement { if(doc.type == 'voice') { - return wrapVoiceMessage(doc, withTime); + return wrapVoiceMessage(doc, uploading); } else if(doc.type == 'audio') { return wrapAudio(doc, withTime); } @@ -384,8 +384,42 @@ export function wrapAudio(doc: MTDocument, withTime = false): HTMLDivElement { return div; } +// https://github.com/LonamiWebs/Telethon/blob/4393ec0b83d511b6a20d8a20334138730f084375/telethon/utils.py#L1285 +function decodeWaveform(waveform: Uint8Array | number[]) { + if(!(waveform instanceof Uint8Array)) { + waveform = new Uint8Array(waveform); + } + + var bitCount = waveform.length * 8; + var valueCount = bitCount / 5 | 0; + if(!valueCount) { + return new Uint8Array([]); + } + + var dataView = new DataView(waveform.buffer); + var result = new Uint8Array(valueCount); + for(var i = 0; i < valueCount; i++) { + var byteIndex = i * 5 / 8 | 0; + var bitShift = i * 5 % 8; + var value = dataView.getUint16(byteIndex, true); + result[i] = (value >> bitShift) & 0b00011111; + } + + /* var byteIndex = (valueCount - 1) / 8 | 0; + var bitShift = (valueCount - 1) % 8; + if(byteIndex == waveform.length - 1) { + var value = waveform[byteIndex]; + } else { + var value = dataView.getUint16(byteIndex, true); + } + + console.log('decoded waveform, setting last value:', value, byteIndex, bitShift); + result[valueCount - 1] = (value >> bitShift) & 0b00011111; */ + return result; +} + let lastAudioToggle: HTMLDivElement = null; -export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElement { +export function wrapVoiceMessage(doc: MTDocument, uploading = false): HTMLDivElement { let div = document.createElement('div'); div.classList.add('audio', 'is-voice'); @@ -395,7 +429,7 @@ export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElem div.innerHTML = `
-
+
${uploading ? '' : '
'}
${durationStr}
`; @@ -414,9 +448,66 @@ export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElem svg.setAttributeNS(null, 'viewBox', '0 0 190 23'); div.insertBefore(svg, div.lastElementChild); - let wave = doc.attributes[0].waveform as Uint8Array; + + const barWidth = 2; + const barMargin = 1; + const barHeightMin = 2; + const barHeightMax = 23; + + let waveform = doc.attributes[0].waveform || []; + waveform = decodeWaveform(waveform.slice()); + + //console.log('decoded waveform:', waveform); + + const normValue = Math.max(...waveform); + const wfSize = waveform.length ? waveform.length : 100; + const availW = 190; + const barCount = Math.min((availW / (barWidth + barMargin)) | 0, wfSize); + + let maxValue = 0; + let maxDelta = barHeightMax - barHeightMin; + + let html = ''; + for(let i = 0, barX = 0, sumI = 0; i < wfSize; ++i) { + const value = waveform[i] || 0; + if((sumI + barCount) >= wfSize) { // draw bar + sumI = sumI + barCount - wfSize; + if(sumI < (barCount + 1) / 2) { + if(maxValue < value) maxValue = value; + } + + const bar_value = Math.max(((maxValue * maxDelta) + ((normValue + 1) / 2)) / (normValue + 1), barHeightMin); + + let h = ` + + `; + html += h; + + /* if(barX >= activeW) { + p.fillRect(nameleft + barX, bottom - bar_value, barWidth, barHeightMin + bar_value, inactive); + } else if (barX + barWidth <= activeW) { + p.fillRect(nameleft + barX, bottom - bar_value, barWidth, barHeightMin + bar_value, active); + } else { + p.fillRect(nameleft + barX, bottom - bar_value, activeW - barX, barHeightMin + bar_value, active); + p.fillRect(nameleft + activeW, bottom - bar_value, barWidth - (activeW - barX), barHeightMin + bar_value, inactive); + } */ + barX += barWidth + barMargin; + + if(sumI < (barCount + 1) / 2) { + maxValue = 0; + } else { + maxValue = value; + } + } else { + if(maxValue < value) maxValue = value; + + sumI += barCount; + } + } + + svg.insertAdjacentHTML('beforeend', html); - let index = 0; + /* let index = 0; let skipped = 0; let h = ''; for(let uint8 of wave) { @@ -425,10 +516,11 @@ export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElem ++skipped; continue; } - let percents = uint8 / 255; + //let percents = uint8 / 255; + let percents = uint8 / 31; let height = 23 * percents; - if(/* !height || */height < 2) { + if(height < 2) { height = 2; } @@ -438,141 +530,149 @@ export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElem ++index; } - svg.insertAdjacentHTML('beforeend', h); + svg.insertAdjacentHTML('beforeend', h); */ let progress = div.querySelector('.audio-waveform') as HTMLDivElement; - - let onClick = () => { - if(!promise) { - if(!preloader) { - preloader = new ProgressivePreloader(null, true); - } - - promise = appDocsManager.downloadDoc(doc.id); - preloader.attach(downloadDiv, true, promise); - - promise.then(blob => { - downloadDiv.classList.remove('downloading'); - downloadDiv.remove(); + + let onLoad = () => { + let audio = document.createElement('audio'); + let source = document.createElement('source'); + source.src = doc.url; + //source.type = doc.mime_type; + source.type = 'audio/wav'; + + audio.volume = 1; + + let toggle = div.querySelector('.audio-toggle') as HTMLDivElement; + + let interval = 0; + let lastIndex = 0; + + toggle.addEventListener('click', () => { + if(audio.paused) { + if(lastAudioToggle && lastAudioToggle.classList.contains('tgico-largepause')) { + lastAudioToggle.click(); + } - let audio = document.createElement('audio'); - let source = document.createElement('source'); - source.src = doc.url; - source.type = doc.mime_type; + audio.currentTime = 0; + audio.play(); - audio.volume = 1; + lastAudioToggle = toggle; - div.removeEventListener('click', onClick); - let toggle = div.querySelector('.audio-toggle') as HTMLDivElement; + toggle.classList.remove('tgico-largeplay'); + toggle.classList.add('tgico-largepause'); - let interval = 0; - let lastIndex = 0; + (Array.from(svg.children) as HTMLElement[]).forEach(node => node.classList.remove('active')); - toggle.addEventListener('click', () => { - if(audio.paused) { - if(lastAudioToggle && lastAudioToggle.classList.contains('tgico-largepause')) { - lastAudioToggle.click(); - } - - audio.currentTime = 0; - audio.play(); - - lastAudioToggle = toggle; - - toggle.classList.remove('tgico-largeplay'); - toggle.classList.add('tgico-largepause'); - - (Array.from(svg.children) as HTMLElement[]).forEach(node => node.classList.remove('active')); - - interval = setInterval(() => { - if(lastIndex > svg.childElementCount || isNaN(audio.duration)) { - clearInterval(interval); - return; - } - - timeDiv.innerText = String(audio.currentTime | 0).toHHMMSS(true); - - lastIndex = Math.round(audio.currentTime / audio.duration * 47); - - //svg.children[lastIndex].setAttributeNS(null, 'fill', '#000'); - //svg.children[lastIndex].classList.add('active'); #Иногда пропускает полоски.. - (Array.from(svg.children) as HTMLElement[]).slice(0,lastIndex+1).forEach(node => node.classList.add('active')); - //++lastIndex; - //console.log('lastIndex:', lastIndex, audio.currentTime); - //}, duration * 1000 / svg.childElementCount | 0/* 63 * duration / 10 */); - }, 20); - } else { - audio.pause(); - toggle.classList.add('tgico-largeplay'); - toggle.classList.remove('tgico-largepause'); - + interval = setInterval(() => { + if(lastIndex > svg.childElementCount || isNaN(audio.duration)) { clearInterval(interval); + return; } - }); - - audio.addEventListener('ended', () => { - toggle.classList.add('tgico-largeplay'); - toggle.classList.remove('tgico-largepause'); - clearInterval(interval); - (Array.from(svg.children) as HTMLElement[]).forEach(node => node.classList.remove('active')); - + timeDiv.innerText = String(audio.currentTime | 0).toHHMMSS(true); - }); - - let mousedown = false, mousemove = false; - progress.addEventListener('mouseleave', (e) => { - if(mousedown) { - audio.play(); - mousedown = false; - } - mousemove = false; - }) - progress.addEventListener('mousemove', (e) => { - mousemove = true; - if(mousedown) scrub(e, audio, progress); - }); - progress.addEventListener('mousedown', (e) => { - e.preventDefault(); - if(!audio.paused) { - audio.pause(); - scrub(e, audio, progress); - mousedown = true; - } - }); - progress.addEventListener('mouseup', (e) => { - if (mousemove && mousedown) { - audio.play(); - mousedown = false; - } - }); - progress.addEventListener('click', (e) => { - if(!audio.paused) scrub(e, audio, progress); - }); - - function scrub(e: MouseEvent, audio: HTMLAudioElement, progress: HTMLDivElement) { - let scrubTime = e.offsetX / 190 /* width */ * audio.duration; - (Array.from(svg.children) as HTMLElement[]).forEach(node => node.classList.remove('active')); - lastIndex = Math.round(scrubTime / audio.duration * 47); + lastIndex = Math.round(audio.currentTime / audio.duration * 47); + + //svg.children[lastIndex].setAttributeNS(null, 'fill', '#000'); + //svg.children[lastIndex].classList.add('active'); #Иногда пропускает полоски.. (Array.from(svg.children) as HTMLElement[]).slice(0,lastIndex+1).forEach(node => node.classList.add('active')); - audio.currentTime = scrubTime; - } + //++lastIndex; + //console.log('lastIndex:', lastIndex, audio.currentTime); + //}, duration * 1000 / svg.childElementCount | 0/* 63 * duration / 10 */); + }, 20); + } else { + audio.pause(); + toggle.classList.add('tgico-largeplay'); + toggle.classList.remove('tgico-largepause'); - audio.style.display = 'none'; - audio.append(source); - div.append(audio); - }); + clearInterval(interval); + } + }); + + audio.addEventListener('ended', () => { + toggle.classList.add('tgico-largeplay'); + toggle.classList.remove('tgico-largepause'); + clearInterval(interval); + (Array.from(svg.children) as HTMLElement[]).forEach(node => node.classList.remove('active')); - downloadDiv.classList.add('downloading'); - } else { - downloadDiv.classList.remove('downloading'); - promise.cancel(); - promise = null; + timeDiv.innerText = String(audio.currentTime | 0).toHHMMSS(true); + }); + + let mousedown = false, mousemove = false; + progress.addEventListener('mouseleave', (e) => { + if(mousedown) { + audio.play(); + mousedown = false; + } + mousemove = false; + }) + progress.addEventListener('mousemove', (e) => { + mousemove = true; + if(mousedown) scrub(e, audio, progress); + }); + progress.addEventListener('mousedown', (e) => { + e.preventDefault(); + if(!audio.paused) { + audio.pause(); + scrub(e, audio, progress); + mousedown = true; + } + }); + progress.addEventListener('mouseup', (e) => { + if (mousemove && mousedown) { + audio.play(); + mousedown = false; + } + }); + progress.addEventListener('click', (e) => { + if(!audio.paused) scrub(e, audio, progress); + }); + + function scrub(e: MouseEvent, audio: HTMLAudioElement, progress: HTMLDivElement) { + let scrubTime = e.offsetX / 190 /* width */ * audio.duration; + (Array.from(svg.children) as HTMLElement[]).forEach(node => node.classList.remove('active')); + lastIndex = Math.round(scrubTime / audio.duration * 47); + + (Array.from(svg.children) as HTMLElement[]).slice(0,lastIndex+1).forEach(node => node.classList.add('active')); + audio.currentTime = scrubTime; } + + audio.style.display = 'none'; + audio.append(source); + div.append(audio); }; - - div.addEventListener('click', onClick); - div.click(); + + if(!uploading) { + let onClick = () => { + if(!promise) { + if(!preloader) { + preloader = new ProgressivePreloader(null, true); + } + + promise = appDocsManager.downloadDoc(doc.id); + preloader.attach(downloadDiv, true, promise); + + promise.then(() => { + downloadDiv.classList.remove('downloading'); + downloadDiv.remove(); + div.removeEventListener('click', onClick); + onLoad(); + }); + + downloadDiv.classList.add('downloading'); + } else { + downloadDiv.classList.remove('downloading'); + promise.cancel(); + promise = null; + } + }; + + div.addEventListener('click', onClick); + div.click(); + } else { + onLoad(); + } return div; } diff --git a/src/lib/appManagers/appDocsManager.ts b/src/lib/appManagers/appDocsManager.ts index 2d5ceb04..62abfae1 100644 --- a/src/lib/appManagers/appDocsManager.ts +++ b/src/lib/appManagers/appDocsManager.ts @@ -1,9 +1,10 @@ import apiFileManager from '../mtproto/apiFileManager'; import FileManager from '../filemanager'; import {RichTextProcessor} from '../richtextprocessor'; -import { CancellablePromise } from '../polyfill'; +import { CancellablePromise, deferredPromise } from '../polyfill'; import { MTDocument } from '../../components/wrappers'; import { isObject } from '../utils'; +import opusDecodeController from '../opusDecodeController'; class AppDocsManager { private docs: {[docID: string]: MTDocument} = {}; @@ -208,34 +209,49 @@ class AppDocsManager { //historyDoc.progress = {enabled: !historyDoc.downloaded, percent: 1, total: doc.size}; + let deferred = deferredPromise(); + + deferred.cancel = () => { + downloadPromise.cancel(); + }; + // нет смысла делать объект с выполняющимися промисами, нижняя строка и так вернёт загружающийся - let downloadPromise: CancellablePromise = apiFileManager.downloadFile(doc.dc_id, inputFileLocation, doc.size, { + let downloadPromise = apiFileManager.downloadFile(doc.dc_id, inputFileLocation, doc.size, { mimeType: doc.mime_type || 'application/octet-stream', toFileEntry: toFileEntry, stickerType: doc.sticker }); - + downloadPromise.then((blob) => { if(blob) { doc.downloaded = true; - if(doc.type && doc.sticker != 2) { + if(doc.type == 'voice'/* && false */) { + let reader = new FileReader(); + + reader.onloadend = (e) => { + let uint8 = new Uint8Array(e.target.result as ArrayBuffer); + //console.log('sending uint8 to decoder:', uint8); + opusDecodeController.decode(uint8).then(result => { + doc.url = result.url; + deferred.resolve(blob); + }, deferred.reject); + }; + + reader.readAsArrayBuffer(blob); + + return; + } else if(doc.type && doc.sticker != 2) { doc.url = URL.createObjectURL(blob); } } - - /* doc.progress.percent = 100; - setTimeout(() => { - delete doc.progress; - }, 0); */ - // console.log('file save done') - - return blob; + + deferred.resolve(blob); }, (e) => { console.log('document download failed', e); //historyDoc.progress.enabled = false; }); - + /* downloadPromise.notify = (progress) => { console.log('dl progress', progress); historyDoc.progress.enabled = true; @@ -248,7 +264,7 @@ class AppDocsManager { //console.log('return downloadPromise:', downloadPromise); - return downloadPromise; + return deferred; } public downloadDocThumb(docID: any, thumbSize: string) { diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 6347f035..5d78c3b0 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -1737,11 +1737,18 @@ export class AppImManager { } case 'audio': + case 'voice': case 'document': { - let docDiv = wrapDocument(pending, false, true); + let doc = appDocsManager.getDoc(message.id); + this.log('will wrap pending doc:', doc); + let docDiv = wrapDocument(doc, false, true); - let icoDiv = docDiv.querySelector('.document-ico'); + let icoDiv = docDiv.querySelector('.audio-download, .document-ico'); preloader.attach(icoDiv, false); + + if(pending.type == 'voice') { + bubble.classList.add('bubble-audio'); + } bubble.classList.remove('is-message-empty'); messageDiv.classList.add((pending.type || 'document') + '-message'); diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 862bf540..18c78c0b 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -597,9 +597,11 @@ export class AppMessagesManager { height: number, objectURL: string, isRoundMessage: boolean, - isVoiceMessage: boolean, duration: number, - background: boolean + background: boolean, + + isVoiceMessage: boolean, + waveform: Uint8Array }> = {}) { peerID = appPeersManager.getPeerMigratedTo(peerID) || peerID; var messageID = this.tempID--; @@ -621,6 +623,8 @@ export class AppMessagesManager { let date = tsNow(true) + ServerTimeManager.serverTimeOffset; + console.log('sendFile', file, fileType); + if(caption) { let entities = options.entities || []; caption = RichTextProcessor.parseMarkdown(caption, entities); @@ -667,6 +671,7 @@ export class AppMessagesManager { if(options.isVoiceMessage) { flags |= 1 << 10; flags |= 1 << 2; + attachType = 'voice'; } let attribute = { @@ -674,10 +679,10 @@ export class AppMessagesManager { flags: flags, pFlags: { // that's only for client, not going to telegram voice: options.isVoiceMessage - }, - waveform: new Uint8Array([0, 0, 0, 0, 0, 0, 128, 35, 8, 25, 34, 132, 16, 66, 8, 0, 0, 0, 0, 0, 0, 0, 96, 60, 254, 255, 255, 79, 223, 255, 63, 183, 226, 107, 255, 255, 255, 255, 191, 188, 255, 255, 246, 255, 255, 255, 255, 63, 155, 117, 135, 24, 249, 191, 167, 51, 149, 0, 0, 0, 0, 0, 0]), + }, + waveform: options.waveform, voice: options.isVoiceMessage, - duration: options.duration || 0, + duration: options.duration || 0 }; attributes.push(attribute); @@ -703,7 +708,15 @@ export class AppMessagesManager { }; attributes.push(videoAttribute); + } else { + attachType = 'document'; + apiFileName = 'document.' + fileType.split('/')[1]; + actionName = 'sendMessageUploadDocumentAction'; + } + attributes.push({_: 'documentAttributeFilename', file_name: fileName || apiFileName}); + + if(['document', 'video', 'audio', 'voice'].indexOf(attachType) !== -1) { let doc: any = { _: 'document', id: '' + messageID, @@ -719,10 +732,6 @@ export class AppMessagesManager { }; appDocsManager.saveDoc(doc); - } else { - attachType = 'document'; - apiFileName = 'document.' + fileType.split('/')[1]; - actionName = 'sendMessageUploadDocumentAction'; } console.log('AMM: sendFile', attachType, apiFileName, file.type, options); @@ -769,8 +778,6 @@ export class AppMessagesManager { } }; - attributes.push({_: 'documentAttributeFilename', file_name: media.file_name}); - preloader.preloader.onclick = () => { console.log('cancelling upload', media); appImManager.setTyping('sendMessageCancelAction'); @@ -786,12 +793,12 @@ export class AppMessagesManager { pFlags: pFlags, date: date, message: caption, - media: isDocument ? { + media: /* isDocument ? { _: 'messageMediaDocument', pFlags: {}, flags: 1, document: file - } : media, + } : */media, random_id: randomIDS, reply_to_msg_id: replyToMsgID, views: asChannel && 1, diff --git a/src/lib/mtproto/apiFileManager.ts b/src/lib/mtproto/apiFileManager.ts index 17a7c878..92e784d5 100644 --- a/src/lib/mtproto/apiFileManager.ts +++ b/src/lib/mtproto/apiFileManager.ts @@ -413,8 +413,8 @@ export class ApiFileManager { limit: limit }, { dcID: dcID, - fileDownload: true, - singleInRequest: 'safari' in window + fileDownload: true/* , + singleInRequest: 'safari' in window */ }); }, dcID).then((result: any) => { writeFilePromise.then(() => { diff --git a/src/lib/opusDecodeController.ts b/src/lib/opusDecodeController.ts new file mode 100644 index 00000000..dffaf5cc --- /dev/null +++ b/src/lib/opusDecodeController.ts @@ -0,0 +1,134 @@ +type Result = { + bytes: Uint8Array, + waveform?: Uint8Array +}; + +type Task = { + pages: Uint8Array, + withWaveform: boolean, + waveform?: Uint8Array, + callback: {resolve: (result: Result) => void, reject: (err: Error) => void} +}; + +export class OpusDecodeController { + private worker: Worker; + private wavWorker : Worker; + private sampleRate = 48000; + private tasks: Array = []; + private keepAlive = false; + + public loadWavWorker() { + if(this.wavWorker) return; + + this.wavWorker = new Worker('waveWorker.min.js'); + this.wavWorker.addEventListener('message', (e) => { + const data = e.data; + + if(data && data.page) { + const bytes = data.page; + this.onTaskEnd(this.tasks.shift(), bytes); + } + }); + } + + public loadWorker() { + if(this.worker) return; + + this.worker = new Worker('decoderWorker.min.js'); + this.worker.addEventListener('message', (e) => { + const data = e.data; + + if(data.type == 'done') { + this.wavWorker.postMessage({command: 'done'}); + + if(data.waveform) { + this.tasks[0].waveform = data.waveform; + } + } else { // e.data contains decoded buffers as float32 values + this.wavWorker.postMessage({ + command: 'encode', + buffers: e.data + }, data.map((typedArray: Uint8Array) => typedArray.buffer)); + } + }); + } + + public setKeepAlive(keepAlive: boolean) { + this.keepAlive = keepAlive; + if(this.keepAlive) { + this.loadWorker(); + this.loadWavWorker(); + } else if(!this.tasks.length) { + this.terminateWorkers(); + } + } + + public onTaskEnd(task: Task, result: Uint8Array) { + task.callback.resolve({bytes: result, waveform: task.waveform}); + + if(this.tasks.length) { + this.executeNewTask(this.tasks[0]); + } + + this.terminateWorkers(); + } + + public terminateWorkers() { + if(this.keepAlive || this.tasks.length) return; + + this.worker.terminate(); + this.worker = null; + + this.wavWorker.terminate(); + this.wavWorker = null; + } + + public executeNewTask(task: Task) { + this.worker.postMessage({ + command: 'init', + decoderSampleRate: this.sampleRate, + outputBufferSampleRate: this.sampleRate + }); + + this.wavWorker.postMessage({ + command: 'init', + wavBitDepth: 16, + wavSampleRate: this.sampleRate + }); + + //console.log('sending command to worker:', task); + //setTimeout(() => { + this.worker.postMessage({ + command: 'decode', + pages: task.pages, + waveform: task.withWaveform + }, [task.pages.buffer]); + //}, 1e3); + } + + public pushDecodeTask(pages: Uint8Array, withWaveform: boolean) { + return new Promise((resolve, reject) => { + const task = { + pages, + withWaveform, + callback: {resolve, reject} + }; + + this.loadWorker(); + this.loadWavWorker(); + + if(this.tasks.push(task) == 1) { + this.executeNewTask(task); + } + }); + } + + public async decode(typedArray: Uint8Array, withWaveform = false) { + return this.pushDecodeTask(typedArray, withWaveform).then(result => { + const dataBlob = new Blob([result.bytes], {type: "audio/wav"}); + return {url: URL.createObjectURL(dataBlob), waveform: result.waveform}; + }); + } +} + +export default new OpusDecodeController(); \ No newline at end of file diff --git a/src/lib/opusProcessor.ts b/src/lib/opusProcessor.ts deleted file mode 100644 index 036cc841..00000000 --- a/src/lib/opusProcessor.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class OpusProcessor { - -} - -export default new OpusProcessor(); \ No newline at end of file diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index 6f20be23..c571cece 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -107,12 +107,18 @@ $time-background: rgba(0, 0, 0, .35); opacity: 0; transition: width .1s .1s, margin-right .1s .1s, visibility 0s .1s, opacity .1s 0s; padding: 0; + z-index: 3; + } + + .btn-send-container { + flex: 0 0 auto; + position: relative; + align-self: flex-end; + z-index: 2; } #btn-send { - flex: 0 0 auto; color: #9e9e9e; - align-self: flex-end; &.tgico-send { color: $color-blue; @@ -141,6 +147,19 @@ $time-background: rgba(0, 0, 0, .35); animation: recordBlink 1.25s infinite; } } + + .record-ripple { + border-radius: 50%; + background-color: rgba(0, 0, 0, .2); + width: 150px; + height: 150px; + transform: scale(0); + position: absolute; + top: -48px; + left: -48px; + transition: transform .03s, visibility .1s; + visibility: hidden; + } &.is-recording { #btn-record-cancel { @@ -157,6 +176,11 @@ $time-background: rgba(0, 0, 0, .35); .record-time { display: block; } + + .record-ripple { + transition: transform .03s, visibility 0s; + visibility: visible; + } } &:not(.is-recording) { @@ -249,6 +273,7 @@ $time-background: rgba(0, 0, 0, .35); caret-color: $button-primary-background; flex: 1; position: relative; + z-index: 3; &:after { position: absolute; diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 1a212703..79a3ddc8 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -1253,6 +1253,10 @@ &-toggle, &-download { background-color: #4FAE4E; } + + &-download:empty { + display: none; + } } &.photo, &.video:not(.round) {