Skip to content

Instantly share code, notes, and snippets.

@vctls
Last active September 21, 2021 17:45

Revisions

  1. Victor Toulouse revised this gist May 14, 2021. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions element_voice_messages.js
    Original file line number Diff line number Diff line change
    @@ -24,6 +24,7 @@ audio.controls = true;
    function setupRecorder(composer) {
    if (!navigator.mediaDevices.getUserMedia) {
    console.log('getUserMedia not supported');
    return;
    }

    const button = document.createElement('div');
  2. Victor Toulouse created this gist May 14, 2021.
    122 changes: 122 additions & 0 deletions element_voice_messages.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,122 @@
    // ==UserScript==
    // @name Element Voice Messages
    // @namespace http://tampermonkey.net/
    // @version 0.1
    // @description Adds a button to record voice messages. Click the button to start recording, click again to stop.
    // @author vctls
    // @match https://app.element.io/
    // @grant none
    // ==/UserScript==

    /*
    As of 2021-05-14, Element.io does not have voice messages.
    But the attachment button contains a hidden file input, which if changed
    automatically triggers the Upload modal.
    This script uses the MediaRecorder interface to record audio blobs,
    which are then put into the hidden input using a DataTransfer object.
    An <audio> html element is appended to the upload modal to preview the recorded audio.
    */

    // Create an audio element to preview recorded audio.
    const audio = document.createElement('audio');
    audio.controls = true;

    function setupRecorder(composer) {
    if (!navigator.mediaDevices.getUserMedia) {
    console.log('getUserMedia not supported');
    }

    const button = document.createElement('div');
    button.id = 'recordButton';
    button.className = 'mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_record';
    button.role = 'button';

    // Add some style.
    const style = document.createElement('style');
    style.appendChild(document.createTextNode(
    '#recordButton::before{background-color: indianred; border-radius: 20px; border-style: double;}' +
    '.recording{animation: blinker 1s linear infinite;}' +
    '.recording:before{background-color:red !important;}' +
    '@keyframes blinker {50% {opacity: 0;}}'
    ));
    document.getElementsByTagName('head')[0].appendChild(style);

    const constraints = {audio: true};
    let chunks = [];

    let onSuccess = function (stream) {
    const mediaRecorder = new MediaRecorder(stream);

    button.onclick = function () {
    if (mediaRecorder.state === 'recording') {
    mediaRecorder.stop();
    } else {
    mediaRecorder.start();
    // Set two minutes security timeout.
    setTimeout(function () {
    mediaRecorder.stop();
    }, 120000);
    }
    }

    mediaRecorder.onstart = function (e) {
    document.getElementById('recordButton').classList.add('recording');
    };

    mediaRecorder.onstop = function (e) {
    document.getElementById('recordButton').classList.remove('recording');

    const blob = new Blob(chunks, {'type': 'audio/ogg; codecs=opus'});
    chunks = [];
    audio.src = window.URL.createObjectURL(blob);

    // Put blob in file input.
    let file = new File([blob], "audio.opus",{type:"audio/ogg", lastModified:new Date().getTime()});
    let container = new DataTransfer();
    container.items.add(file);
    let fileInput = document.querySelector('.mx_MessageComposer_upload input[type="file"]');
    fileInput.files = container.files;

    // Trigger input change.
    let event = document.createEvent("UIEvents");
    event.initUIEvent("change", true, true);
    fileInput.dispatchEvent(event);

    }

    mediaRecorder.ondataavailable = function (e) {
    chunks.push(e.data);
    }
    }

    let onError = function (err) {
    console.log('The following error occured: ' + err);
    }

    navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError);
    composer.append(button);
    }

    function check(changes, observer) {
    const composer = document.querySelector('.mx_MessageComposer_row');
    if (composer !== null && !composer.hasButton) {
    setupRecorder(composer);
    composer.hasButton = true;
    }

    const modalContent = document.getElementById('mx_Dialog_content');
    if (
    modalContent !== null
    && !modalContent.hasAudio
    && modalContent.textContent.includes('audio.opus')
    ) {
    console.log('Appending audio clip');
    modalContent.append(audio);
    modalContent.hasAudio = true;
    }
    }

    (function () {
    'use strict';
    (new MutationObserver(check)).observe(document, {childList: true, subtree: true});
    })();