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 = `
-
+
${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) {