Work
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <Quotient/settings.h>
|
||||
|
||||
#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<NeoChatRoom>();
|
||||
|
||||
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());
|
||||
|
||||
39
src/app/qml/IncomingCallDialog.qml
Normal file
39
src/app/qml/IncomingCallDialog.qml
Normal file
@@ -0,0 +1,39 @@
|
||||
// SPDX-FileCopyrightText: 2025 Tobias Fella <tobias.fella@kde.org>
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -8,6 +8,7 @@ target_sources(LibNeoChat PRIVATE
|
||||
neochatroom.cpp
|
||||
neochatroommember.cpp
|
||||
accountmanager.cpp
|
||||
callmanager.cpp
|
||||
chatbarcache.cpp
|
||||
chatdocumenthandler.cpp
|
||||
clipboard.cpp
|
||||
|
||||
166
src/libneochat/callmanager.cpp
Normal file
166
src/libneochat/callmanager.cpp
Normal file
@@ -0,0 +1,166 @@
|
||||
// SPDX-FileCopyrightText: 2025 Tobias Fella <tobias.fella@kde.org
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "callmanager.h"
|
||||
|
||||
#include "events/callmemberevent.h"
|
||||
#include "mediamanager.h"
|
||||
#include "mediaplayer2player.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
#include <QAudioOutput>
|
||||
#include <QDBusConnection>
|
||||
#include <QMediaPlayer>
|
||||
#include <QMimeDatabase>
|
||||
#include <QTimer>
|
||||
|
||||
#include <Quotient/qt_connection_util.h>
|
||||
|
||||
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<CallMemberEvent>(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<CallMemberEvent>(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);
|
||||
}
|
||||
68
src/libneochat/callmanager.h
Normal file
68
src/libneochat/callmanager.h
Normal file
@@ -0,0 +1,68 @@
|
||||
// SPDX-FileCopyrightText: 2025 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
#include <QTimer>
|
||||
|
||||
#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<NeoChatRoom> m_room;
|
||||
QString m_callingMember;
|
||||
};
|
||||
@@ -3,144 +3,16 @@
|
||||
|
||||
#include "mediamanager.h"
|
||||
|
||||
#include <QAudioOutput>
|
||||
#include <QDBusConnection>
|
||||
#include <QDirIterator>
|
||||
#include <QMediaPlayer>
|
||||
#include <QMimeDatabase>
|
||||
|
||||
#include <Quotient/qt_connection_util.h>
|
||||
|
||||
#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<CallMemberEvent>(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<CallMemberEvent>(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"
|
||||
|
||||
@@ -5,11 +5,6 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
#include <QTimer>
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -44,12 +44,12 @@
|
||||
#include <Quotient/thread.h>
|
||||
#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<CallNotifyEvent>()) {
|
||||
MediaManager::instance().ring(event->fullJson(), this);
|
||||
CallManager::instance().ring(event->fullJson(), this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
<?xml version="1.0" ?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2006-2012 the VideoLAN team (Mirsal Ennaime, Rafaël Carré, Jean-Paul Saman)
|
||||
SPDX-FileCopyrightText: 2005-2008 Milosz Derezynski
|
||||
SPDX-FileCopyrightText: 2008 Nick Welch
|
||||
SPDX-FileCopyrightText: 2010-2012 Alex Merry
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
-->
|
||||
<node xmlns:tp="http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0">
|
||||
<interface name="org.mpris.MediaPlayer2.Player">
|
||||
<method name="Next" tp:name-for-bindings="Next">
|
||||
|
||||
Reference in New Issue
Block a user