Browse Source

Completed task #3:

Recording voice messages:
1) Record and play on Safari
2) Ripple effect
3) Encoding and decoding new waveform
master
morethanwords 4 years ago
parent
commit
ca4946f32e
  1. 66
      src/components/chatInput.ts
  2. 352
      src/components/wrappers.ts
  3. 44
      src/lib/appManagers/appDocsManager.ts
  4. 11
      src/lib/appManagers/appImManager.ts
  5. 33
      src/lib/appManagers/appMessagesManager.ts
  6. 4
      src/lib/mtproto/apiFileManager.ts
  7. 134
      src/lib/opusDecodeController.ts
  8. 5
      src/lib/opusProcessor.ts
  9. 29
      src/scss/partials/_chat.scss
  10. 4
      src/scss/partials/_chatBubble.scss

66
src/components/chatInput.ts

@ -11,7 +11,9 @@ import appMessagesManager from "../lib/appManagers/appMessagesManager"; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);

352
src/components/wrappers.ts

@ -192,7 +192,7 @@ let formatDate = (timestamp: number) => { @@ -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 { @@ -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 @@ -395,7 +429,7 @@ export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElem
div.innerHTML = `
<div class="audio-toggle audio-ico tgico-largeplay"></div>
<div class="audio-download"><div class="tgico-download"></div></div>
<div class="audio-download">${uploading ? '' : '<div class="tgico-download"></div>'}</div>
<div class="audio-time">${durationStr}</div>
`;
@ -414,9 +448,66 @@ export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElem @@ -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 = `
<rect x="${barX}" y="${barHeightMax - bar_value}" width="2" height="${bar_value}" rx="1" ry="1"></rect>
`;
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 @@ -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 @@ -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;
}

44
src/lib/appManagers/appDocsManager.ts

@ -1,9 +1,10 @@ @@ -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 { @@ -208,34 +209,49 @@ class AppDocsManager {
//historyDoc.progress = {enabled: !historyDoc.downloaded, percent: 1, total: doc.size};
let deferred = deferredPromise<Blob>();
deferred.cancel = () => {
downloadPromise.cancel();
};
// нет смысла делать объект с выполняющимися промисами, нижняя строка и так вернёт загружающийся
let downloadPromise: CancellablePromise<Blob> = 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 { @@ -248,7 +264,7 @@ class AppDocsManager {
//console.log('return downloadPromise:', downloadPromise);
return downloadPromise;
return deferred;
}
public downloadDocThumb(docID: any, thumbSize: string) {

11
src/lib/appManagers/appImManager.ts

@ -1737,11 +1737,18 @@ export class AppImManager { @@ -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');

33
src/lib/appManagers/appMessagesManager.ts

@ -597,9 +597,11 @@ export class AppMessagesManager { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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,

4
src/lib/mtproto/apiFileManager.ts

@ -413,8 +413,8 @@ export class ApiFileManager { @@ -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(() => {

134
src/lib/opusDecodeController.ts

@ -0,0 +1,134 @@ @@ -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<Task> = [];
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<Result>((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();

5
src/lib/opusProcessor.ts

@ -1,5 +0,0 @@ @@ -1,5 +0,0 @@
export class OpusProcessor {
}
export default new OpusProcessor();

29
src/scss/partials/_chat.scss

@ -107,12 +107,18 @@ $time-background: rgba(0, 0, 0, .35); @@ -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); @@ -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); @@ -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); @@ -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;

4
src/scss/partials/_chatBubble.scss

@ -1253,6 +1253,10 @@ @@ -1253,6 +1253,10 @@
&-toggle, &-download {
background-color: #4FAE4E;
}
&-download:empty {
display: none;
}
}
&.photo, &.video:not(.round) {

Loading…
Cancel
Save