Greasy Fork is available in English.

Element Voice Messages

Adds a button to record voice messages. Click the button to start recording, click again to stop.

// ==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');
        return;
    }

    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});
})();