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
|
EmojiDialog.qml
|
||||||
EmojiTonesPicker.qml
|
EmojiTonesPicker.qml
|
||||||
ImageEditorPage.qml
|
ImageEditorPage.qml
|
||||||
|
VoiceMessageDialog.qml
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -150,6 +150,19 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
tooltip: text
|
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 {
|
BusyAction {
|
||||||
id: sendAction
|
id: sendAction
|
||||||
|
|
||||||
@@ -548,6 +561,11 @@ QQC2.Control {
|
|||||||
NewPollDialog {}
|
NewPollDialog {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: voiceMessageDialog
|
||||||
|
VoiceMessageDialog {}
|
||||||
|
}
|
||||||
|
|
||||||
CompletionMenu {
|
CompletionMenu {
|
||||||
id: completionMenu
|
id: completionMenu
|
||||||
chatDocumentHandler: documentHandler
|
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
|
texthandler.cpp
|
||||||
urlhelper.cpp
|
urlhelper.cpp
|
||||||
utils.cpp
|
utils.cpp
|
||||||
|
voicerecorder.cpp
|
||||||
enums/chatbartype.h
|
enums/chatbartype.h
|
||||||
enums/messagecomponenttype.h
|
enums/messagecomponenttype.h
|
||||||
enums/messagetype.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