Implement sending voice messages
This commit is contained in:
committed by
Joshua Goins
parent
3d07f723c8
commit
593ad27e8c
@@ -16,4 +16,5 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
|
||||
EmojiDialog.qml
|
||||
EmojiTonesPicker.qml
|
||||
ImageEditorPage.qml
|
||||
VoiceMessageDialog.qml
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
68
src/chatbar/VoiceMessageDialog.qml
Normal file
68
src/chatbar/VoiceMessageDialog.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
124
src/libneochat/voicerecorder.cpp
Normal file
124
src/libneochat/voicerecorder.cpp
Normal 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);
|
||||
}
|
||||
50
src/libneochat/voicerecorder.h
Normal file
50
src/libneochat/voicerecorder.h
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user