Last active
September 21, 2021 17:45
Adds a button next to the composer field to record voice messages.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Element.io doesn't have a dedicated voice message button yet, although it already supports sending and playing inline audio.Update: Element.io now supports voice messages natively, so this script is definitely obsolete.
This script adds a record button that uses the recent MediaStream API to record audio directly in the browser, which is then sent using the existing attachment feature.
Just a proof of concept. It will probably become obsolete soon, when the feature is implemented in Element.
Tested on Firefox 89 with GreaseMonkey.
https://www.youtube.com/watch?v=I8-bsGZPerg
Also available on GreasyFork:
https://greasyfork.org/en/scripts/426488-element-voice-messages
Apparently some browser versions show microphone activity all the time when this is loaded.
There are probably better ways of handling the permission.
(btw I'm not a JS dev. It probably shows, a bit.)