From 3a0916866c5df59f100a982b5e674737b751a1f5 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 1 Jul 2025 22:34:08 +0200 Subject: [PATCH] Work --- src/app/CMakeLists.txt | 1 + src/app/controller.cpp | 6 + src/app/qml/IncomingCallDialog.qml | 39 ++++ src/app/qml/Main.qml | 10 ++ src/libneochat/CMakeLists.txt | 1 + src/libneochat/callmanager.cpp | 166 ++++++++++++++++++ src/libneochat/callmanager.h | 68 +++++++ src/libneochat/mediamanager.cpp | 128 -------------- src/libneochat/mediamanager.h | 19 -- src/libneochat/neochatroom.cpp | 4 +- .../org.mpris.MediaPlayer2.Player.xml | 7 + 11 files changed, 300 insertions(+), 149 deletions(-) create mode 100644 src/app/qml/IncomingCallDialog.qml create mode 100644 src/libneochat/callmanager.cpp create mode 100644 src/libneochat/callmanager.h diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index ef568c335..ae3941f31 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -99,6 +99,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE qml/ReasonDialog.qml qml/NewPollDialog.qml qml/UserMenu.qml + qml/IncomingCallDialog.qml DEPENDENCIES QtCore QtQuick diff --git a/src/app/controller.cpp b/src/app/controller.cpp index 19644d058..7d9b0d432 100644 --- a/src/app/controller.cpp +++ b/src/app/controller.cpp @@ -18,6 +18,7 @@ #include #include "accountmanager.h" +#include "callmanager.h" #include "enums/roomsortparameter.h" #include "mediasizehelper.h" #include "models/actionsmodel.h" @@ -72,6 +73,11 @@ Controller::Controller(QObject *parent) { Connection::setRoomType(); + CallManager::instance().setCallsEnabled(NeoChatConfig::calls()); + connect(NeoChatConfig::self(), &NeoChatConfig::CallsChanged, this, []() { + CallManager::instance().setCallsEnabled(NeoChatConfig::calls()); + }); + Connection::setDirectChatEncryptionDefault(NeoChatConfig::preferUsingEncryption()); connect(NeoChatConfig::self(), &NeoChatConfig::PreferUsingEncryptionChanged, this, [] { Connection::setDirectChatEncryptionDefault(NeoChatConfig::preferUsingEncryption()); diff --git a/src/app/qml/IncomingCallDialog.qml b/src/app/qml/IncomingCallDialog.qml new file mode 100644 index 000000000..087ffbacc --- /dev/null +++ b/src/app/qml/IncomingCallDialog.qml @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2025 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick +import QtQuick.Controls as QQC2 + +import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.formcard as FormCard +import org.kde.kirigamiaddons.components as Components + +import org.kde.neochat.libneochat + +FormCard.FormCardPage { + id: root + + title: i18nc("@title:dialog", "Incoming Call") + + FormCard.FormCard { + topPadding: Kirigami.Units.largeSpacing + FormCard.AbstractFormDelegate { + contentItem: Components.Avatar { + name: CallManager.room.displayName + source: CallManager.room.avatarMediaUrl + } + } + FormCard.FormTextDelegate { + text: i18nc("@info", "%1 is calling you.", CallManager.callingMember.htmlSafeDisplayName) + } + FormCard.FormButtonDelegate { + text: i18nc("@action:button", "Accept Call") + onClicked: console.warn("unimplemented") + } + FormCard.FormButtonDelegate { + text: i18nc("@action:button", "Decline Call") + onClicked: CallManager.declineCall() + } + } + +} diff --git a/src/app/qml/Main.qml b/src/app/qml/Main.qml index ea8955674..f108bc471 100644 --- a/src/app/qml/Main.qml +++ b/src/app/qml/Main.qml @@ -83,6 +83,16 @@ Kirigami.ApplicationWindow { } } + Connections { + target: CallManager + property IncomingCallDialog dialog + function onIsRingingChanged(): void { + if (CallManager.isRinging) { + root.pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat", "IncomingCallDialog")); + } + } + } + Loader { active: Kirigami.Settings.hasPlatformMenuBar && !Kirigami.Settings.isMobile sourceComponent: GlobalMenu { diff --git a/src/libneochat/CMakeLists.txt b/src/libneochat/CMakeLists.txt index 1d66e905c..48c12aaa5 100644 --- a/src/libneochat/CMakeLists.txt +++ b/src/libneochat/CMakeLists.txt @@ -8,6 +8,7 @@ target_sources(LibNeoChat PRIVATE neochatroom.cpp neochatroommember.cpp accountmanager.cpp + callmanager.cpp chatbarcache.cpp chatdocumenthandler.cpp clipboard.cpp diff --git a/src/libneochat/callmanager.cpp b/src/libneochat/callmanager.cpp new file mode 100644 index 000000000..0f528803d --- /dev/null +++ b/src/libneochat/callmanager.cpp @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: 2025 Tobias Fella +#include +#include +#include +#include + +#include + +using namespace Quotient; +using namespace Qt::Literals::StringLiterals; + +void CallManager::ring(const QJsonObject &json, NeoChatRoom *room) +{ + if (!m_callsEnabled) { + return; + } + // TODO: check sender != us + // Consider multiple accounts being logged in + if (json["content"_L1]["application"_L1].toString() != "m.call"_L1) { + return; + } + if (!json["content"_L1]["m.mentions"_L1]["room"_L1].toBool() || json["sender"_L1].toString() == room->connection()->userId()) { + if (std::ranges::none_of(json["content"_L1]["m.mentions"_L1]["user_ids"_L1].toArray(), [room](const auto &user) { + return user.toString() == room->connection()->userId(); + })) { + return; + } + } + if (json["content"_L1]["notify_type"_L1].toString() != "ring"_L1) { + return; + } + if (room->pushNotificationState() == PushNotificationState::Mute) { + return; + } + if (isRinging()) { + return; + } + if (const auto &event = room->currentState().get(room->connection()->userId())) { + if (event) { + auto memberships = event->contentJson()["memberships"_L1].toArray(); + for (const auto &m : memberships) { + const auto &membership = m.toObject(); + if (membership["application"_L1] == "m.call"_L1 && membership["call_id"_L1].toString().isEmpty()) { + qWarning() << "already in a call"; + return; + } + } + } + } + connectUntil(room, &NeoChatRoom::changed, this, [this, room]() { + if (const auto &event = room->currentState().get(room->connection()->userId())) { + auto memberships = event->contentJson()["memberships"_L1].toArray(); + for (const auto &m : memberships) { + const auto &membership = m.toObject(); + if (membership["application"_L1] == "m.call"_L1 && membership["call_id"_L1].toString().isEmpty()) { + stopRinging(); + return true; + } + } + } + return false; + }); + if (json["unsigned"_L1]["age"_L1].toInt() > 10000) { + return; + } + + m_room = room; + m_callingMember = json["sender"_L1].toString(); + Q_EMIT roomChanged(); + QTimer::singleShot(60000, this, &CallManager::stopRinging); + ringUnchecked(); +} + +void CallManager::ringUnchecked() +{ + MediaManager::instance().startPlayback(); + // Pause all media players registered with the system + for (const auto &iface : QDBusConnection::sessionBus().interface()->registeredServiceNames().value()) { + if (iface.startsWith("org.mpris.MediaPlayer2"_L1)) { + OrgMprisMediaPlayer2PlayerInterface mprisInterface(iface, "/org/mpris/MediaPlayer2"_L1, QDBusConnection::sessionBus()); + QString status = mprisInterface.playbackStatus(); + if (status == "Playing"_L1) { + if (mprisInterface.canPause()) { + mprisInterface.Pause(); + } else { + mprisInterface.Stop(); + } + } + } + } + + static QString path; + if (path.isEmpty()) { + for (const auto &dir : QString::fromUtf8(qgetenv("XDG_DATA_DIRS")).split(u':')) { + if (QFileInfo(dir + QStringLiteral("/sounds/freedesktop/stereo/phone-incoming-call.oga")).exists()) { + path = dir + QStringLiteral("/sounds/freedesktop/stereo/phone-incoming-call.oga"); + break; + } + } + } + if (path.isEmpty()) { + return; + } + + m_player->setSource(QUrl::fromLocalFile(path)); + m_player->play(); + + m_ringing = true; + Q_EMIT isRingingChanged(); +} + +bool CallManager::isRinging() const +{ + return m_ringing; +} + +void CallManager::stopRinging() +{ + m_ringing = false; + m_player->pause(); + m_timer.stop(); + Q_EMIT isRingingChanged(); +} + +void CallManager::setCallsEnabled(bool enabled) +{ + m_callsEnabled = enabled; +} + +CallManager::CallManager() + : QObject(nullptr) + , m_player(new QMediaPlayer()) + , m_output(new QAudioOutput()) +{ + m_player->setAudioOutput(m_output); + m_timer.setInterval(1000); + m_timer.setSingleShot(true); + connect(&m_timer, &QTimer::timeout, this, [this]() { + m_player->play(); + }); + connect(m_player, &QMediaPlayer::playbackStateChanged, this, [this]() { + if (m_player->playbackState() == QMediaPlayer::StoppedState) { + m_timer.start(); + } + }); +} + +NeoChatRoom *CallManager::room() const +{ + return m_room.get(); +} + +NeochatRoomMember *CallManager::callingMember() const +{ + return m_room->qmlSafeMember(m_callingMember); +} diff --git a/src/libneochat/callmanager.h b/src/libneochat/callmanager.h new file mode 100644 index 000000000..e23d2b21f --- /dev/null +++ b/src/libneochat/callmanager.h @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2025 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "neochatroom.h" +#include "neochatroommember.h" + +class QAudioOutput; +class QMediaPlayer; + +/** + * @class CallManager + * + * Manages calls. + */ +class CallManager : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(bool isRinging READ isRinging NOTIFY isRingingChanged) + Q_PROPERTY(NeoChatRoom *room READ room NOTIFY roomChanged) + Q_PROPERTY(NeochatRoomMember *callingMember READ callingMember NOTIFY roomChanged) + +public: + static CallManager &instance() + { + static CallManager _instance; + return _instance; + } + static CallManager *create(QQmlEngine *, QJSEngine *) + { + QQmlEngine::setObjectOwnership(&instance(), QQmlEngine::CppOwnership); + return &instance(); + } + + void ring(const QJsonObject &json, NeoChatRoom *room); + void stopRinging(); + + bool isRinging() const; + + void setCallsEnabled(bool enabled); + + NeoChatRoom *room() const; + NeochatRoomMember *callingMember() const; + +Q_SIGNALS: + void isRingingChanged(); + void roomChanged(); + +private: + CallManager(); + + void ringUnchecked(); + bool m_ringing = false; + QMediaPlayer *m_player; + QAudioOutput *m_output; + QTimer m_timer; + bool m_callsEnabled = false; + QPointer m_room; + QString m_callingMember; +}; diff --git a/src/libneochat/mediamanager.cpp b/src/libneochat/mediamanager.cpp index 8357d40b6..43600f04e 100644 --- a/src/libneochat/mediamanager.cpp +++ b/src/libneochat/mediamanager.cpp @@ -3,144 +3,16 @@ #include "mediamanager.h" -#include -#include -#include -#include -#include - #include -#include "events/callmemberevent.h" -#include "mediaplayer2player.h" -#include "neochatroom.h" - -using namespace Qt::Literals::StringLiterals; -using namespace Quotient; - void MediaManager::startPlayback() { Q_EMIT playbackStarted(); } -void MediaManager::ring(const QJsonObject &json, NeoChatRoom *room) -{ - // todo: check sender != us - if (json["content"_L1]["application"_L1].toString() != "m.call"_L1) { - return; - } - if (!json["content"_L1]["m.mentions"_L1]["room"_L1].toBool() || json["sender"_L1].toString() == room->connection()->userId()) { - if (std::ranges::none_of(json["content"_L1]["m.mentions"_L1]["user_ids"_L1].toArray(), [room](const auto &user) { - return user.toString() == room->connection()->userId(); - })) { - return; - } - } - if (json["content"_L1]["notify_type"_L1].toString() != "ring"_L1) { - return; - } - if (room->pushNotificationState() == PushNotificationState::Mute) { - return; - } - if (isRinging()) { - return; - } - if (const auto &event = room->currentState().get(room->connection()->userId())) { - if (event) { - auto memberships = event->contentJson()["memberships"_L1].toArray(); - for (const auto &m : memberships) { - const auto &membership = m.toObject(); - if (membership["application"_L1] == "m.call"_L1 && membership["call_id"_L1].toString().isEmpty()) { - qWarning() << "already in a call"; - return; - } - } - } - } - connectUntil(room, &NeoChatRoom::changed, this, [this, room]() { - if (const auto &event = room->currentState().get(room->connection()->userId())) { - auto memberships = event->contentJson()["memberships"_L1].toArray(); - for (const auto &m : memberships) { - const auto &membership = m.toObject(); - if (membership["application"_L1] == "m.call"_L1 && membership["call_id"_L1].toString().isEmpty()) { - stopRinging(); - return true; - } - } - } - return false; - }); - if (json["unsigned"_L1]["age"_L1].toInt() > 10000) { - return; - } - - QTimer::singleShot(60000, this, &MediaManager::stopRinging); - ringUnchecked(); -} - -void MediaManager::ringUnchecked() -{ - // Pause all media players registered with the system - for (const auto &iface : QDBusConnection::sessionBus().interface()->registeredServiceNames().value()) { - if (iface.startsWith("org.mpris.MediaPlayer2"_L1)) { - OrgMprisMediaPlayer2PlayerInterface mprisInterface(iface, "/org/mpris/MediaPlayer2"_L1, QDBusConnection::sessionBus()); - QString status = mprisInterface.playbackStatus(); - if (status == "Playing"_L1) { - if (mprisInterface.canPause()) { - mprisInterface.Pause(); - } else { - mprisInterface.Stop(); - } - } - } - } - - static QString path; - if (path.isEmpty()) { - for (const auto &dir : QString::fromUtf8(qgetenv("XDG_DATA_DIRS")).split(u':')) { - if (QFileInfo(dir + QStringLiteral("/sounds/freedesktop/stereo/phone-incoming-call.oga")).exists()) { - path = dir + QStringLiteral("/sounds/freedesktop/stereo/phone-incoming-call.oga"); - break; - } - } - } - if (path.isEmpty()) { - return; - } - - m_player->setSource(QUrl::fromLocalFile(path)); - m_player->play(); -} - MediaManager::MediaManager() : QObject(nullptr) - , m_player(new QMediaPlayer()) - , m_output(new QAudioOutput()) { - m_player->setAudioOutput(m_output); - m_timer.setInterval(1000); - m_timer.setSingleShot(true); - connect(&m_timer, &QTimer::timeout, this, [this]() { - m_player->play(); - }); - connect(m_player, &QMediaPlayer::playbackStateChanged, this, [this]() { - if (m_player->playbackState() == QMediaPlayer::StoppedState) { - m_timer.start(); - } - }); -} - -bool MediaManager::isRinging() const -{ - return m_ringing; -} - -void MediaManager::stopRinging() -{ - m_ringing = false; - m_player->pause(); - m_timer.stop(); - Q_EMIT isRingingChanged(); } #include "moc_mediamanager.cpp" diff --git a/src/libneochat/mediamanager.h b/src/libneochat/mediamanager.h index 7e3d18fda..ef18591da 100644 --- a/src/libneochat/mediamanager.h +++ b/src/libneochat/mediamanager.h @@ -5,11 +5,6 @@ #include #include -#include - -class NeoChatRoom; -class QAudioOutput; -class QMediaPlayer; /** * @class MediaManager @@ -22,8 +17,6 @@ class MediaManager : public QObject QML_ELEMENT QML_SINGLETON - Q_PROPERTY(bool isRinging READ isRinging NOTIFY isRingingChanged) - public: static MediaManager &instance() { @@ -41,24 +34,12 @@ public: */ Q_INVOKABLE void startPlayback(); - void ring(const QJsonObject &json, NeoChatRoom *room); - void stopRinging(); - - bool isRinging() const; - Q_SIGNALS: /** * @brief Emitted when any media player starts playing. Other objects should stop / pause playback. */ void playbackStarted(); - void isRingingChanged(); private: MediaManager(); - - void ringUnchecked(); - bool m_ringing = false; - QMediaPlayer *m_player; - QAudioOutput *m_output; - QTimer m_timer; }; diff --git a/src/libneochat/neochatroom.cpp b/src/libneochat/neochatroom.cpp index a12e1a5d7..8cb545243 100644 --- a/src/libneochat/neochatroom.cpp +++ b/src/libneochat/neochatroom.cpp @@ -44,12 +44,12 @@ #include #endif +#include "callmanager.h" #include "chatbarcache.h" #include "clipboard.h" #include "events/callnotifyevent.h" #include "events/pollevent.h" #include "filetransferpseudojob.h" -#include "mediamanager.h" #include "neochatconnection.h" #include "roomlastmessageprovider.h" #include "spacehierarchycache.h" @@ -122,7 +122,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS connect(this, &NeoChatRoom::aboutToAddNewMessages, this, [this](const auto &events) { for (const auto &event : events) { if (event->template is()) { - MediaManager::instance().ring(event->fullJson(), this); + CallManager::instance().ring(event->fullJson(), this); } } }); diff --git a/src/libneochat/org.mpris.MediaPlayer2.Player.xml b/src/libneochat/org.mpris.MediaPlayer2.Player.xml index 8b094bd1c..232facd90 100644 --- a/src/libneochat/org.mpris.MediaPlayer2.Player.xml +++ b/src/libneochat/org.mpris.MediaPlayer2.Player.xml @@ -1,4 +1,11 @@ +