Implement sending voice messages

This commit is contained in:
Tobias Fella
2026-02-14 16:04:22 +00:00
committed by Joshua Goins
parent 3d07f723c8
commit 593ad27e8c
6 changed files with 262 additions and 0 deletions

View File

@@ -16,4 +16,5 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
EmojiDialog.qml
EmojiTonesPicker.qml
ImageEditorPage.qml
VoiceMessageDialog.qml
)

View File

@@ -150,6 +150,19 @@ QQC2.Control {
}
tooltip: text
},
BusyAction {
icon.name: "microphone"
isBusy: false
text: i18nc("@action:button", "Send a Voice Message")
displayHint: QQC2.AbstractButton.IconOnly
onTriggered: {
let dialog = voiceMessageDialog.createObject(root, {
room: root.currentRoom
}) as VoiceMessageDialog;
dialog.open();
}
tooltip: text
},
BusyAction {
id: sendAction
@@ -548,6 +561,11 @@ QQC2.Control {
NewPollDialog {}
}
Component {
id: voiceMessageDialog
VoiceMessageDialog {}
}
CompletionMenu {
id: completionMenu
chatDocumentHandler: documentHandler

View File

@@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2026 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtMultimedia
import org.kde.kirigami as Kirigami
import org.kde.coreaddons
import org.kde.neochat
QQC2.Dialog {
id: root
required property NeoChatRoom room
VoiceRecorder {
id: voiceRecorder
readonly property bool recording: recorder.recorderState == MediaRecorder.RecordingState
room: root.room
}
width: Kirigami.Units.gridUnit * 24
standardButtons: QQC2.DialogButtonBox.Cancel
title: i18nc("@title:dialog", "Record Voice Message")
contentItem: ColumnLayout {
QQC2.RoundButton {
icon.name: voiceRecorder.recording ? "media-playback-stop" : "media-record"
text: voiceRecorder.recording ? i18nc("@action:button Stop audio recording", "Stop Recording") : i18nc("@action:button Start audio recording", "Start Recording")
Layout.preferredHeight: Kirigami.Units.gridUnit * 4
Layout.preferredWidth: Kirigami.Units.gridUnit * 4
Layout.alignment: Qt.AlignHCenter
display: QQC2.RoundButton.IconOnly
enabled: voiceRecorder.isSupported
onClicked: voiceRecorder.recording ? voiceRecorder.stopRecording() : voiceRecorder.startRecording()
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
QQC2.Label {
text: i18nc("@info Duration being the length of an audio recording", "Duration: %1", Format.formatDuration(voiceRecorder.recorder.duration))
}
}
Kirigami.InlineMessage {
Layout.fillWidth: true
text: i18nc("@info", "Voice message recording requires a newer Qt version than is currently installed on this system.")
visible: !voiceRecorder.isSupported
}
}
footer: QQC2.DialogButtonBox {
QQC2.Button {
text: i18nc("@action:button Send the voice message", "Send")
icon.name: "document-send"
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
onClicked: voiceRecorder.send()
enabled: !voiceRecorder.recording && voiceRecorder.recorder.duration > 0 && voiceRecorder.isSupported
}
}
}

View File

@@ -23,6 +23,7 @@ target_sources(LibNeoChat PRIVATE
texthandler.cpp
urlhelper.cpp
utils.cpp
voicerecorder.cpp
enums/chatbartype.h
enums/messagecomponenttype.h
enums/messagetype.h

View File

@@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: 2026 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "voicerecorder.h"
#include <QFile>
#include <QTemporaryFile>
#include <KFormat>
#include <Quotient/events/filesourceinfo.h>
using namespace Qt::Literals::StringLiterals;
VoiceRecorder::VoiceRecorder(QObject *parent)
: QObject(parent)
, m_buffer(new QBuffer)
, m_format(QMediaFormat::FileFormat::Ogg)
{
m_session.setAudioInput(&m_input);
m_recorder.setAudioBitRate(24000);
m_recorder.setAudioSampleRate(48000);
m_format.setAudioCodec(QMediaFormat::AudioCodec::Opus);
m_recorder.setAudioChannelCount(1);
m_recorder.setMediaFormat(m_format);
m_buffer->open(QIODevice::ReadWrite);
m_recorder.setOutputDevice(m_buffer);
m_session.setRecorder(&m_recorder);
}
VoiceRecorder::~VoiceRecorder()
{
delete m_buffer;
}
void VoiceRecorder::startRecording()
{
m_buffer->setData({});
m_recorder.record();
}
void VoiceRecorder::stopRecording()
{
m_recorder.stop();
}
QMediaRecorder *VoiceRecorder::recorder()
{
return &m_recorder;
}
void VoiceRecorder::send()
{
Quotient::FileSourceInfo fileMetadata;
QByteArray data;
m_buffer->seek(0);
if (m_room->usesEncryption()) {
std::tie(fileMetadata, data) = Quotient::encryptFile(m_buffer->data());
m_buffer->close();
m_buffer->setData(data);
m_buffer->open(QIODevice::ReadOnly);
}
auto room = m_room;
auto buffer = m_buffer;
auto duration = m_recorder.duration();
m_buffer = nullptr;
m_room->connection()->uploadContent(buffer, {}, u"audio/ogg"_s).then([fileMetadata, room, buffer, duration](const auto &job) mutable {
QJsonObject mscFile{
{u"mimetype"_s, u"audio/ogg"_s},
{u"name"_s, u"Voice Message"_s},
{u"size"_s, buffer->size()},
};
if (room->usesEncryption()) {
mscFile[u"file"_s] = toJson(fileMetadata);
} else {
mscFile[u"url"_s] = job->contentUri().toString();
}
Quotient::setUrlInSourceInfo(fileMetadata, job->contentUri());
QJsonObject content{
{u"body"_s, u"Voice message"_s},
{u"msgtype"_s, u"m.audio"_s},
{u"org.matrix.msc1767.text"_s,
QJsonObject{{u"body"_s, u"Voice Message (%1, %2)"_s.arg(KFormat().formatDuration(duration), KFormat().formatByteSize(buffer->size()))}}},
{u"org.matrix.msc1767.file"_s, mscFile},
{u"info"_s,
QJsonObject{
{u"mimetype"_s, u"audio/ogg"_s},
{u"size"_s, buffer->size()},
{u"duration"_s, duration},
}},
{u"org.matrix.msc1767.audio"_s,
QJsonObject{
{u"duration"_s, duration},
{u"waveform"_s, QJsonArray{}}, // TODO
}},
{u"org.matrix.msc3245.voice"_s, QJsonObject{}}};
if (room->usesEncryption()) {
content[u"file"_s] = toJson(fileMetadata);
} else {
content[u"url"_s] = job->contentUri().toString();
}
room->postJson(u"m.room.message"_s, content);
});
}
void VoiceRecorder::setRoom(NeoChatRoom *room)
{
m_room = room;
Q_EMIT roomChanged();
}
NeoChatRoom *VoiceRecorder::room() const
{
return m_room.get();
}
bool VoiceRecorder::isSupported() const
{
return m_format.isSupported(QMediaFormat::Encode);
}

View File

@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2026 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
#include <qqmlintegration.h>
#include "neochatroom.h"
#include <QAudioInput>
#include <QBuffer>
#include <QMediaCaptureSession>
#include <QMediaFormat>
#include <QMediaRecorder>
class VoiceRecorder : public QObject
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QMediaRecorder *recorder READ recorder CONSTANT)
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged REQUIRED)
// TODO: Remove once no longer required
Q_PROPERTY(bool isSupported READ isSupported CONSTANT)
public:
explicit VoiceRecorder(QObject *parent = nullptr);
~VoiceRecorder() override;
Q_INVOKABLE void startRecording();
Q_INVOKABLE void stopRecording();
Q_INVOKABLE void send();
QMediaRecorder *recorder();
NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
bool isSupported() const;
Q_SIGNALS:
void roomChanged();
private:
QAudioInput m_input;
QMediaCaptureSession m_session;
QMediaRecorder m_recorder;
QBuffer *m_buffer;
QPointer<NeoChatRoom> m_room;
QMediaFormat m_format;
};