Compare commits
114 Commits
work/tobia
...
work/calls
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c65183f93a | ||
|
|
39ee17cfa1 | ||
|
|
12bb75e5b0 | ||
|
|
481f12337a | ||
|
|
5e533b8e03 | ||
|
|
718060c757 | ||
|
|
4092cd8b6a | ||
|
|
ea99c26556 | ||
|
|
7527fd47cd | ||
|
|
c72c9c5cba | ||
|
|
09ded20409 | ||
|
|
0fd3de6215 | ||
|
|
1de160cb19 | ||
|
|
921abac3c1 | ||
|
|
7c75a2fd06 | ||
|
|
105be518c7 | ||
|
|
9425f24315 | ||
|
|
47c28ce9a2 | ||
|
|
72c85af407 | ||
|
|
22694fe5e4 | ||
|
|
a02232dc19 | ||
|
|
252e099e75 | ||
|
|
76a697c3f6 | ||
|
|
5b23593fd2 | ||
|
|
c2580c1d2d | ||
|
|
3303d2c7db | ||
|
|
22107fc598 | ||
|
|
bc4c4f8519 | ||
|
|
16c63dbe93 | ||
|
|
9e78ab3328 | ||
|
|
e6dc1f54b3 | ||
|
|
09025fa16d | ||
|
|
357b148944 | ||
|
|
8b71e56a5f | ||
|
|
f5aa5ac7f4 | ||
|
|
075d2fda4d | ||
|
|
b5c781212c | ||
|
|
af136943c3 | ||
|
|
b2a29c8d45 | ||
|
|
5d16d78914 | ||
|
|
94e970e15a | ||
|
|
1f4b984664 | ||
|
|
a2a27e78d1 | ||
|
|
915a5c188f | ||
|
|
e8f0420ad5 | ||
|
|
7a01b3ea28 | ||
|
|
7cff2aaa97 | ||
|
|
9588c7d8ef | ||
|
|
d5a6c7683e | ||
|
|
1de4e2ecd3 | ||
|
|
fa37f28c94 | ||
|
|
f1be509d13 | ||
|
|
fed528c6d8 | ||
|
|
d09cd5d71b | ||
|
|
4e40f5c7d9 | ||
|
|
3b5bb06b4a | ||
|
|
8f4e5a41c5 | ||
|
|
9b1005efc3 | ||
|
|
e1d4b66479 | ||
|
|
f2aa375b43 | ||
|
|
ee53793a6d | ||
|
|
528d46be9f | ||
|
|
cc80e69644 | ||
|
|
09ced090f2 | ||
|
|
8ad23e7a40 | ||
|
|
a94f46f904 | ||
|
|
41a5d2557e | ||
|
|
0b36db0ecc | ||
|
|
aae5519bed | ||
|
|
df849accfa | ||
|
|
b098937d1b | ||
|
|
fbe4fc49e1 | ||
|
|
8741fd8fed | ||
|
|
ccc952b65e | ||
|
|
6a68375b10 | ||
|
|
4dcb19edb7 | ||
|
|
838abc9af3 | ||
|
|
7283b2dd4d | ||
|
|
2c1f00da03 | ||
|
|
c7caec894e | ||
|
|
b254b4aa9b | ||
|
|
67107959e4 | ||
|
|
26d3e16e6d | ||
|
|
bb8ee4ef44 | ||
|
|
ee65ad22e6 | ||
|
|
b823f2c6b9 | ||
|
|
0901c59de9 | ||
|
|
69bfa1394c | ||
|
|
5b69a3e07c | ||
|
|
37220ab872 | ||
|
|
9dd7ceb966 | ||
|
|
ae6056615d | ||
|
|
d74253d149 | ||
|
|
805332c599 | ||
|
|
e42c1c1f69 | ||
|
|
f7881d0661 | ||
|
|
5eb86bbc88 | ||
|
|
53dc32fac1 | ||
|
|
a5a2c0b03e | ||
|
|
4d2e64cb80 | ||
|
|
dee064a758 | ||
|
|
5bdd67dcc1 | ||
|
|
2ff92bea70 | ||
|
|
0fbb070bf2 | ||
|
|
1a1c7ef23e | ||
|
|
fabd3da5a7 | ||
|
|
6f90d33f3a | ||
|
|
3bb0ee17cd | ||
|
|
fa8164cdba | ||
|
|
c63f1f0452 | ||
|
|
e0df447998 | ||
|
|
a64d4d0989 | ||
|
|
4c575f18d1 | ||
|
|
b629961a70 |
@@ -103,7 +103,6 @@
|
||||
{
|
||||
"name": "libQuotient",
|
||||
"buildsystem": "cmake-ninja",
|
||||
"config-opts": [ "-DBUILD_TESTING=OFF" ],
|
||||
"sources": [
|
||||
{
|
||||
"type": "git",
|
||||
@@ -113,7 +112,8 @@
|
||||
}
|
||||
],
|
||||
"config-opts": [
|
||||
"-DQuotient_ENABLE_E2EE=ON"
|
||||
"-DQuotient_ENABLE_E2EE=ON",
|
||||
"-DBUILD_TESTING=OFF"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
include:
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
|
||||
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-qt6.yml
|
||||
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-qt6.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd.yml
|
||||
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.yml
|
||||
|
||||
@@ -59,4 +59,4 @@ Dependencies:
|
||||
'frameworks/kdbusaddons': '@latest-kf6'
|
||||
|
||||
Options:
|
||||
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD', 'Windows/Qt5' ]
|
||||
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD' ]
|
||||
|
||||
@@ -145,6 +145,14 @@ if(ANDROID)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
|
||||
endif()
|
||||
|
||||
include(FindPkgConfig)
|
||||
pkg_check_modules(GSTREAMER IMPORTED_TARGET gstreamer-sdp-1.0>1.18 gstreamer-webrtc-1.0>=1.18)
|
||||
if (TARGET PkgConfig::GSTREAMER)
|
||||
add_feature_info(voip ON "GStreamer found. Call support is enabled.")
|
||||
else()
|
||||
add_feature_info(voip OFF "GStreamer not found. Call support is disabled.")
|
||||
endif()
|
||||
|
||||
ki18n_install(po)
|
||||
|
||||
install(FILES org.kde.neochat.desktop DESTINATION ${KDE_INSTALL_APPDIR})
|
||||
|
||||
@@ -14,3 +14,9 @@ ecm_add_test(
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME texthandlertest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
delegatesizehelpertest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME delegatesizehelpertest
|
||||
)
|
||||
|
||||
156
autotests/delegatesizehelpertest.cpp
Normal file
156
autotests/delegatesizehelpertest.cpp
Normal file
@@ -0,0 +1,156 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <QObject>
|
||||
#include <QTest>
|
||||
|
||||
#include "delegatesizehelper.h"
|
||||
|
||||
class DelegateSizeHelperTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void risingPercentage_data();
|
||||
void risingPercentage();
|
||||
|
||||
void fallingPercentage_data();
|
||||
void fallingPercentage();
|
||||
|
||||
void equalPercentage_data();
|
||||
void equalPercentage();
|
||||
|
||||
void equalBreakpoint_data();
|
||||
void equalBreakpoint();
|
||||
};
|
||||
|
||||
void DelegateSizeHelperTest::risingPercentage_data()
|
||||
{
|
||||
QTest::addColumn<qreal>("parentWidth");
|
||||
QTest::addColumn<int>("currentPercentageWidth");
|
||||
QTest::addColumn<qreal>("currentWidth");
|
||||
|
||||
QTest::newRow("zero") << qreal(0) << int(0) << qreal(0);
|
||||
QTest::newRow("one hundred") << qreal(100) << int(0) << qreal(0);
|
||||
QTest::newRow("one fifty") << qreal(150) << int(50) << qreal(75);
|
||||
QTest::newRow("two hundred") << qreal(200) << int(100) << qreal(200);
|
||||
QTest::newRow("three hundred") << qreal(300) << int(100) << qreal(300);
|
||||
}
|
||||
|
||||
void DelegateSizeHelperTest::risingPercentage()
|
||||
{
|
||||
QFETCH(qreal, parentWidth);
|
||||
QFETCH(int, currentPercentageWidth);
|
||||
QFETCH(qreal, currentWidth);
|
||||
|
||||
DelegateSizeHelper delegateSizeHelper;
|
||||
delegateSizeHelper.setStartBreakpoint(100);
|
||||
delegateSizeHelper.setEndBreakpoint(200);
|
||||
delegateSizeHelper.setStartPercentWidth(0);
|
||||
delegateSizeHelper.setEndPercentWidth(100);
|
||||
|
||||
delegateSizeHelper.setParentWidth(parentWidth);
|
||||
|
||||
QCOMPARE(delegateSizeHelper.currentPercentageWidth(), currentPercentageWidth);
|
||||
QCOMPARE(delegateSizeHelper.currentWidth(), currentWidth);
|
||||
}
|
||||
|
||||
void DelegateSizeHelperTest::fallingPercentage_data()
|
||||
{
|
||||
QTest::addColumn<qreal>("parentWidth");
|
||||
QTest::addColumn<int>("currentPercentageWidth");
|
||||
QTest::addColumn<qreal>("currentWidth");
|
||||
|
||||
QTest::newRow("zero") << qreal(0) << int(100) << qreal(0);
|
||||
QTest::newRow("one hundred") << qreal(100) << int(100) << qreal(100);
|
||||
QTest::newRow("one fifty") << qreal(150) << int(50) << qreal(75);
|
||||
QTest::newRow("two hundred") << qreal(200) << int(0) << qreal(0);
|
||||
QTest::newRow("three hundred") << qreal(300) << int(0) << qreal(0);
|
||||
}
|
||||
|
||||
void DelegateSizeHelperTest::fallingPercentage()
|
||||
{
|
||||
QFETCH(qreal, parentWidth);
|
||||
QFETCH(int, currentPercentageWidth);
|
||||
QFETCH(qreal, currentWidth);
|
||||
|
||||
DelegateSizeHelper delegateSizeHelper;
|
||||
delegateSizeHelper.setStartBreakpoint(100);
|
||||
delegateSizeHelper.setEndBreakpoint(200);
|
||||
delegateSizeHelper.setStartPercentWidth(100);
|
||||
delegateSizeHelper.setEndPercentWidth(0);
|
||||
|
||||
delegateSizeHelper.setParentWidth(parentWidth);
|
||||
|
||||
QCOMPARE(delegateSizeHelper.currentPercentageWidth(), currentPercentageWidth);
|
||||
QCOMPARE(delegateSizeHelper.currentWidth(), currentWidth);
|
||||
}
|
||||
|
||||
void DelegateSizeHelperTest::equalPercentage_data()
|
||||
{
|
||||
QTest::addColumn<qreal>("parentWidth");
|
||||
QTest::addColumn<int>("currentPercentageWidth");
|
||||
QTest::addColumn<qreal>("currentWidth");
|
||||
|
||||
QTest::newRow("zero") << qreal(0) << int(50) << qreal(0);
|
||||
QTest::newRow("one hundred") << qreal(100) << int(50) << qreal(50);
|
||||
QTest::newRow("one fifty") << qreal(150) << int(50) << qreal(75);
|
||||
QTest::newRow("two hundred") << qreal(200) << int(50) << qreal(100);
|
||||
QTest::newRow("three hundred") << qreal(300) << int(50) << qreal(150);
|
||||
}
|
||||
|
||||
void DelegateSizeHelperTest::equalPercentage()
|
||||
{
|
||||
QFETCH(qreal, parentWidth);
|
||||
QFETCH(int, currentPercentageWidth);
|
||||
QFETCH(qreal, currentWidth);
|
||||
|
||||
DelegateSizeHelper delegateSizeHelper;
|
||||
delegateSizeHelper.setStartBreakpoint(100);
|
||||
delegateSizeHelper.setEndBreakpoint(200);
|
||||
delegateSizeHelper.setStartPercentWidth(50);
|
||||
delegateSizeHelper.setEndPercentWidth(50);
|
||||
|
||||
delegateSizeHelper.setParentWidth(parentWidth);
|
||||
|
||||
QCOMPARE(delegateSizeHelper.currentPercentageWidth(), currentPercentageWidth);
|
||||
QCOMPARE(delegateSizeHelper.currentWidth(), currentWidth);
|
||||
}
|
||||
|
||||
void DelegateSizeHelperTest::equalBreakpoint_data()
|
||||
{
|
||||
QTest::addColumn<int>("startPercentageWidth");
|
||||
QTest::addColumn<int>("endPercentageWidth");
|
||||
QTest::addColumn<int>("currentPercentageWidth");
|
||||
QTest::addColumn<qreal>("currentWidth");
|
||||
|
||||
QTest::newRow("start higher") << int(100) << int(0) << int(-1) << qreal(0);
|
||||
QTest::newRow("equal") << int(50) << int(50) << int(50) << qreal(500);
|
||||
QTest::newRow("end higher") << int(0) << int(100) << int(-1) << qreal(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* We expect a default return except in the case where the the two percentages are
|
||||
* equal as that case can be calculated without dividing by zero.
|
||||
*/
|
||||
void DelegateSizeHelperTest::equalBreakpoint()
|
||||
{
|
||||
QFETCH(int, startPercentageWidth);
|
||||
QFETCH(int, endPercentageWidth);
|
||||
QFETCH(int, currentPercentageWidth);
|
||||
QFETCH(qreal, currentWidth);
|
||||
|
||||
DelegateSizeHelper delegateSizeHelper;
|
||||
delegateSizeHelper.setStartBreakpoint(100);
|
||||
delegateSizeHelper.setEndBreakpoint(100);
|
||||
delegateSizeHelper.setStartPercentWidth(startPercentageWidth);
|
||||
delegateSizeHelper.setEndPercentWidth(endPercentageWidth);
|
||||
|
||||
delegateSizeHelper.setParentWidth(1000);
|
||||
|
||||
QCOMPARE(delegateSizeHelper.currentPercentageWidth(), currentPercentageWidth);
|
||||
QCOMPARE(delegateSizeHelper.currentWidth(), currentWidth);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(DelegateSizeHelperTest)
|
||||
#include "delegatesizehelpertest.moc"
|
||||
@@ -22,6 +22,7 @@
|
||||
<name xml:lang="eu">NeoChat</name>
|
||||
<name xml:lang="fi">NeoChat</name>
|
||||
<name xml:lang="fr">NeoChat</name>
|
||||
<name xml:lang="gl">NeoChat</name>
|
||||
<name xml:lang="hu">NeoChat</name>
|
||||
<name xml:lang="ia">Neochat</name>
|
||||
<name xml:lang="id">NeoChat</name>
|
||||
@@ -57,6 +58,7 @@
|
||||
<summary xml:lang="eu">Matrix, deszentralizatutako komunikazio protokolorako bezero bat</summary>
|
||||
<summary xml:lang="fi">Asiakas Matrixille, hajautetulle viestintäyhteyskäytännölle</summary>
|
||||
<summary xml:lang="fr">Un client pour « Matrix », le protocole décentralisé de communications.</summary>
|
||||
<summary xml:lang="gl">Un cliente para Matrix, o protocolo de comunicación descentralizada</summary>
|
||||
<summary xml:lang="hu">Kliens a matrixhoz, a decentralizált kommunikációs protokollhoz</summary>
|
||||
<summary xml:lang="ia">Un cliente per Matrix, le protocollo de communication decentralisate</summary>
|
||||
<summary xml:lang="id">Klien untuk matrix, protokol komunikasi terdesentralisasi</summary>
|
||||
@@ -74,6 +76,7 @@
|
||||
<summary xml:lang="sk">Klient pre matrix, decentralizovaný komunikačný protokol</summary>
|
||||
<summary xml:lang="sl">Odjemalec za matrix, decentralizirani komunikacijski protokol</summary>
|
||||
<summary xml:lang="sv">En klient för Matrix, det decentraliserade kommunikationsprotokollet</summary>
|
||||
<summary xml:lang="ta">மையமில்லா தகவல் பரிமாற்ற நெறிமுறையான மேட்ரிக்ஸுக்கான செயலி</summary>
|
||||
<summary xml:lang="tr">Merkezi olmayan iletişim protokolü Matrix için bir istemci</summary>
|
||||
<summary xml:lang="uk">Клієнт matrix, децентралізованого протоколу обміну даними</summary>
|
||||
<summary xml:lang="x-test">xxA client for matrix, the decentralized communication protocolxx</summary>
|
||||
@@ -84,7 +87,12 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<p xml:lang="ar">نيوتشات هو عميل ماتركس Matrix، (ميفاق الاتصال اللامركزي للمراسلة الفورية). يتيح لك نيوتشات إرسال رسائل نصية ومقاطع فيديو وملفات صوتية إلى عائلتك وزملائك وأصدقائك. يستخدم أطر عمل كيدي وأبرزها Kirigami لتوفير تجربة متقاربة عبر منصات متعددة.</p>
|
||||
<p xml:lang="ca">El NeoChat és un client de Matrix, el protocol descentralitzat de comunicacions de missatgeria instantània. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics. Fa servir els Frameworks de KDE i, sobretot, el Kirigami per a proporcionar una experiència convergent a través de diverses plataformes.</p>
|
||||
<p xml:lang="ca-valencia">NeoChat és un client de Matrix, el protocol descentralitzat de comunicacions de missatgeria instantània. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics. Utilitza els Frameworks de KDE i, sobretot, Kirigami per a proporcionar una experiència convergent a través de diverses plataformes.</p>
|
||||
<p xml:lang="en-GB">NeoChat is a client for Matrix, the decentralised communication protocol for instant messaging. It allows you to send text messages, videos and audio files to your family, colleagues and friends. It uses KDE frameworks and most notably Kirigami to provide a convergent experience across multiple platforms.</p>
|
||||
<p xml:lang="es">NeoChat es un cliente para Matrix, el protocolo de comunicaciones descentralizado para mensajería instantánea. Le permite enviar mensajes de texto, vídeos y archivos de sonido a su familia, compañeros de trabajo y amigos. Usa la infraestructura de KDE y, en particular, Kirigami para proporcionar una experiencia convergente en muchas plataformas.</p>
|
||||
<p xml:lang="fr">NeoChat est un client pour le protocole Matrix, un protocole décentralisé de communications pour messagerie instantané. Il vous permet d'envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos amis. Il utilise les environnements de développement et plus précisément Kirigami pour fournir une expérience convergente sur plusieurs plate-formes. </p>
|
||||
<p xml:lang="gl">NeoChat é un cliente para Matrix, o protocolo de comunicación descentralizada para mensaxería instantánea. Podes enviar mensaxes de texto, vídeos e ficheiros de son á túa familia, colegas e amizades. Usas infraestruturas de KDE e principalmente Kirigami para proporcionar unha experiencia de uso converxente para varias plataformas.</p>
|
||||
<p xml:lang="ia">NeoChat es un cliente per Matrix, le protocollo de communication decentralisate per messager instantanee. Illo te permitte inviar messager de texto, files de video e audio a tu familia, collegas e amicos usante. Illo usa KDE frameworks e super toto Kirigamii forni un experientia convergente trans platteforme multiple.</p>
|
||||
<p xml:lang="it">NeoChat è un client per Matrix, il protocollo di comunicazione decentralizzato per la messaggistica istantanea. Ti consente di inviare messaggi di testo, video e file audio a familiari, colleghi e amici. Utilizza i framework KDE e in particolare Kirigami per fornire un'esperienza convergente su più piattaforme.</p>
|
||||
<p xml:lang="ka">NeoChat არის Matrix კლიენტი. ის საშუალებას გაძლევთ გაგზავნოთ ტექსტური შეტყობინებები, ვიდეოები და აუდიო ფაილები თქვენს ოჯახს, კოლეგებსა და მეგობრებს მატრიქსის პროტოკოლის გამოყენებით.</p>
|
||||
<p xml:lang="nl">NeoChat is een client voor Matrix, het gedecentraliseerde communicatieprotocol voor instant messages. Het biedt u het verzenden van tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden. Het gebruik KDE frameworks en het meest opmerkelijk Kirigami om een convergente ervaring te leveren op meerdere platforms.</p>
|
||||
<p xml:lang="pl">NeoChat jest programem do Matriksa, protokołu rozproszonego porozumiewania się w czasie rzeczywistym. Umożliwia wysyłanie wiadomości tekstowych, filmów oraz dźwięku do twojej rodziny, znajomych oraz przyjaciół. Używa szkieletów KDE i głównie Kirigami, aby zapewnić spójne wrażenia na wielu platformach</p>
|
||||
@@ -97,26 +105,36 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<p xml:lang="ar">يهدف نيوتشات إلى أن يكون تطبيقًا كامل الميزات لمواصفات ماتركس. على هذا النحو يتم دعم كل شيء في المواصفات المستقرة الحالية مع الاستثناءات الملحوظة لـ VoIP والخيوط وبعض جوانب التشفير من طرف إلى طرف. هناك عدد قليل من الإغفالات الصغيرة الأخرى بسبب حقيقة أن مواصفات ماتركس تتطور باستمرار ، ولكن يبقى الهدف توفير الدعم النهائي للمواصفات بأكملها.</p>
|
||||
<p xml:lang="ca">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de la VoIP, fils i alguns aspectes de l'encriptatge d'extrem a extrem. Hi ha algunes altres omissions més petites a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu segueix sent proporcionar suport eventual per a tota l'especificació.</p>
|
||||
<p xml:lang="ca-valencia">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de VoIP, fils i alguns aspectes de l'encriptació d'extrem a extrem. Hi ha algunes altres omissions més xicotetes a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu seguix sent proporcionar suport eventual per a tota l'especificació.</p>
|
||||
<p xml:lang="en-GB">NeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.</p>
|
||||
<p xml:lang="es">NeoChat pretende ser una aplicación con todas las funciones para la especificación de Matrix. Como tal, admite todo en la especificación estable actual, con las notables excepciones de VoIP, subprocesos y algunas funciones de cifrado de extremo a extremo. Existen algunas omisiones menos importantes debido al hecho de que la especificación de Matrix está en constante evolución, pero el objetivo sigue siendo brindar compatibilidad final con toda la especificación.</p>
|
||||
<p xml:lang="fr">L'objectif de NeoChat est d'être une application complète pour le protocole Matrix. En tant que tel, tout dans la spécification stable actuelle avec les exceptions notables de VoIP, les processus et certains aspects du chiffrement de bout en bout sont pris en charge. Il y a quelques autres petites omissions en raison du fait que la spécification du protocole Matrix est en constante évolution. Cependant, l'objectif reste de fournir un soutien éventuel pour l'ensemble de la spécification.</p>
|
||||
<p xml:lang="gl">NeoChat pretende ser unha aplicación completa para a especificación de Matrix. Coas excepcións de VoIP, conversas fiadas e algúns aspectos da cifraxe de extremo a extremo, a versión estábel segue as especificacións. Existen algunhas outras pequenas omisións debido ao feito de que Matrix está en continua evolución pero a intención é implementar a especificación completa.</p>
|
||||
<p xml:lang="ia">NeoChat aspira a esser un application plenemente eminente per le specification de Matrix. Tal como omne cosas in le specification currentemente stabile con le exceptiones notabile de VOIP, threads e alcun aspectos del cryptation End-to-End es supportate. Il ha ltere pauc omissiones, debite al facto que le specification de Matrix es in evolution constante ma le aspiration remane a fornir supporto eventual per le integre specification.</p>
|
||||
<p xml:lang="it">NeoChat mira ad essere un'applicazione completa per le specifiche Matrix. Pertanto, sono supportati tutti gli elementi dell'attuale specifica stabile con le notevoli eccezioni di VoIP, conversazioni e alcuni aspetti della cifratura end-to-end. Ci sono alcune altre piccole omissioni dovute al fatto che le specifiche Matrix sono in continua evoluzione, ma l'obiettivo rimane quello di fornire un eventuale supporto per l'intera specifica.</p>
|
||||
<p xml:lang="ka">NeoChat-ი მიზნად ისახავს Matrix სპეციფიკაციის სრული განხორციელება ჰქონდეს. როგორც ასეთი, ყველაფერი მიმდინარე სპეციფიკაციიდან, VoIP-ის, ძაფებისა და გამჭოლი დაშიფვრის ზოგიერთი ასპექტის გარდა, მხარდაჭერილია. შეძლება ასევე იყოს მცირე ლაფსუსებიც იმის გამო, რომ Matrix-ის სპეციფიკაცია მუდმივად ვითარგდება, მაგრამ ჩვენი მიზანი მისი სრული მხარდაჭერაა.</p>
|
||||
<p xml:lang="nl">NeoChat richt zich op het volledig bieden van alle mogelijkheden van de Matrix-specificatie. Alles in de huidige stabiele specificatie met merkbare uitzondering van VoIP, gekoppelde discussies en sommige aspecten van eind-tot-eind versleuteling worden ondersteund. Er zijn een paar andere kleinere omissies vanwege het feit dat de Matrix specificatie constant evolueert maar het doel blijft het eventueel bieden van ondersteuning van de gehele specificatie.</p>
|
||||
<p xml:lang="pl">NeoChat w zamyśle ma być pełnowartościową aplikacją wg wytycznych Matriksa. Z tego powodu, wszystko, co jest obecnie w stabilnych wytycznych z pominięciem VoIP, wątków i niektórych części szyfrowania Użytkownik-do-Użytkownika są obecnie obsługiwane. Pominięto też kilka mniejszych rzeczy ze względu na ciągły rozwój wytycznych Matriksa, lecz celem nadal jest zapewnienie obsługi wszystkich wytycznych.</p>
|
||||
<p xml:lang="pt">O NeoChat pretende ser uma aplicação completa para a especificação do Matrix. Como tal, tudo o que existe na especificação estável actual, com as notáveis excepções do VoIP, tópicos e alguns aspectos da Encriptação Ponto-a-Ponto, são suportados. Existem mais algumas omissões, devido ao facto que a norma do Matrix está em constante evolução, mas o objectivo continua a ser oferecer o suporte eventual para a norma por inteiro.</p>
|
||||
<p xml:lang="sl">Neochat cilja, da bi bila popolna aplikacija po specifikaciji Matrixa. Kot takšna vsebuje vse v trenutni stabilni specifikaciji z pomembnimi izjemami pri VoIP, nitih in nekaterih vidikov šifriranja od konca do konca. Obstaja nekaj drugih manjših opustitev zaradi dejstva, da se specifikacija Matrix nenehno razvija, vendar cilj ostaja zagotoviti morebitno podporo celotni specifikaciji.</p>
|
||||
<p xml:lang="tr">NeoChat, Matrix spesifikasyonu için tam özellikli bir uygulama olmayı hedefler. Bu nedenle; VoIP, ileti zincirleri ve Uçtan Uca Şifrelemenin bazı yönleri gibi dikkate değer istisnalar dışında var olan kararlı belirtimdeki her şey desteklenir. Matrix belirtiminin sürekli gelişmesi nedeniyle birkaç küçük eksiklik daha var; ancak amaç tüm belirtim için nihai destek sağlamak olmayı sürdürüyor.</p>
|
||||
<p xml:lang="tr">NeoChat, Matrix belirtimi için tam özellikli bir uygulama olmayı hedefler. Bu nedenle; VoIP, ileti zincirleri ve Uçtan Uca Şifreleme'nin bazı yönleri gibi dikkate değer istisnalar dışında var olan kararlı belirtimdeki her şey desteklenir. Matrix belirtiminin sürekli gelişmesi nedeniyle birkaç küçük eksiklik daha var; ancak amaç tüm belirtim için nihai destek sağlamak olmayı sürdürüyor.</p>
|
||||
<p xml:lang="uk">Метою створення NeoChat є повноцінна реалізація програми для специфікації Matrix. Як наслідок, реалізовано усе у поточній стабільній специфікації, окрім голосового інтернет-зв'язку, потоків та деяких аспектів міжвузлового шифрування. Є також декілька інших незначних прогалин через те, що специфікація Matrix постійно змінюється, але метою лишається повна підтримка специфікації.</p>
|
||||
<p xml:lang="x-test">xxNeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.xx</p>
|
||||
<p>Due to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:</p>
|
||||
<p xml:lang="ar">نظرًا لطبيعة تطوير مواصفات ماتركس، يدعم نيوتشات أيضًا العديد من الميزات غير المستقرة وهي:</p>
|
||||
<p xml:lang="ca">A causa de la naturalesa del desenvolupament de l'especificació de Matrix, el NeoChat també implementa nombroses característiques inestables. Actualment són:</p>
|
||||
<p xml:lang="ca-valencia">A causa de la naturalea del desenvolupament de l'especificació de Matrix, NeoChat també implementa nombroses característiques inestables. Actualment són:</p>
|
||||
<p xml:lang="en-GB">Due to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:</p>
|
||||
<p xml:lang="es">Debido a la naturaleza del desarrollo de la especificación de Matrix, NeoChat también permite numerosas funciones no estables, como:</p>
|
||||
<p xml:lang="fr">En raison de la nature du développement des spécifications du protocole Matrix, NeoChat prend également en charge de nombreuses fonctionnalités instables. Actuellement, ce sont :</p>
|
||||
<p xml:lang="gl">Debido á natureza do desenvolvemento da especificación de Matrix, NeoChat tamén inclúe varias funcionalidades non estábeis:</p>
|
||||
<p xml:lang="ia">Debite al natura del disveloppamento de specification de Matrix NeoChat tamben supporta numerose characteristicas instabile. Currentemente istes es:</p>
|
||||
<p xml:lang="it">A causa della natura dello sviluppo delle specifiche Matrix, NeoChat supporta anche numerose funzionalità instabili. Attualmente queste sono:</p>
|
||||
<p xml:lang="ka">Matrix-ის სპეციფიკაციის განვითარების ბუნების გამო NeoChat-ს ასევე აქვს უამრავი არასტაბილური ფუნქციაც. ახლა ისინია:</p>
|
||||
<p xml:lang="nl">Vanwege de aard van de ontwikkeling van de Matrix specificatie ondersteunt NeoChat ook talloze onstabiele mogelijkheden. Dit zijn nu:</p>
|
||||
<p xml:lang="pl">Ze względu na sposób rozwoju Matriksa, NeoChat obsługuje także kilka niestabilnych możliwości. Obecnie są to:</p>
|
||||
<p xml:lang="pt">Devido à natureza do desenvolvimento da especificação do Matrix, o NeoChat também suporta diversas funcionalidades instáveis. De momento são:</p>
|
||||
<p xml:lang="sl">Zaradi narave razvoja specifikacije Matrixa NeoChat podpira tudi številne nestabilne zmožnosti. Trenutno so to:</p>
|
||||
<p xml:lang="tr">NeoChat, Matrix belirtim geliştirmesinin doğası gereği çok sayıda kararsız özelliği de destekler. Şu anda bunlar:</p>
|
||||
<p xml:lang="tr">NeoChat, Matrix belirtimi geliştirmesinin doğası gereği çok sayıda kararsız özelliği de destekler. Şu anda bunlar:</p>
|
||||
<p xml:lang="uk">Через природу розробки специфікації Matrix, у NeoChat також передбачено підтримку численних нестабільних можливостей. У поточній версії цими можливостями є:</p>
|
||||
<p xml:lang="x-test">xxDue to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:xx</p>
|
||||
<ul>
|
||||
@@ -124,12 +142,17 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="ar">التصويت - MSC3381</li>
|
||||
<li xml:lang="ca">Enquestes - MSC3381</li>
|
||||
<li xml:lang="ca-valencia">Enquestes - MSC3381</li>
|
||||
<li xml:lang="en-GB">Polls - MSC3381</li>
|
||||
<li xml:lang="es">Encuestas - MSC3381</li>
|
||||
<li xml:lang="fr">Sondages - MSC3381</li>
|
||||
<li xml:lang="gl">Enquisas - MSC3381</li>
|
||||
<li xml:lang="ia">Inquestas - MSC3381</li>
|
||||
<li xml:lang="it">Sondaggi - MSC3381</li>
|
||||
<li xml:lang="ka">Polls - MSC3381</li>
|
||||
<li xml:lang="nl">Polls - MSC3381</li>
|
||||
<li xml:lang="pt">Inquéritos - MSC3381</li>
|
||||
<li xml:lang="sl">Polls - MSC3381</li>
|
||||
<li xml:lang="ta">வாக்கெடுப்புகள் - MSC3381</li>
|
||||
<li xml:lang="tr">Anketler - MSC3381</li>
|
||||
<li xml:lang="uk">Опитування - MSC3381</li>
|
||||
<li xml:lang="x-test">xxPolls - MSC3381xx</li>
|
||||
@@ -137,11 +160,17 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="ar">حزم الملصقات - MSC2545</li>
|
||||
<li xml:lang="ca">Paquets d'adhesius - MSC2545</li>
|
||||
<li xml:lang="ca-valencia">Paquets d'adhesius - MSC2545</li>
|
||||
<li xml:lang="en-GB">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="es">Paquetes de pegatinas - MSC2545</li>
|
||||
<li xml:lang="fr">Paquets d'auto-collants - MSC2545</li>
|
||||
<li xml:lang="gl">Paquetes de adhesivos - MSC2545</li>
|
||||
<li xml:lang="ia">Etiquetta gummate (sticker) -MSC2545</li>
|
||||
<li xml:lang="it">Pacchetti di adesivi - MSC2545</li>
|
||||
<li xml:lang="ka">სტიკერების პაკეტები - MSC2545</li>
|
||||
<li xml:lang="nl">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="pt">Pacotes de Autocolantes - MSC2545</li>
|
||||
<li xml:lang="sl">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="ta">ஒட்டி தொகுப்புகள் - MSC2545</li>
|
||||
<li xml:lang="tr">Yapışkan Paketleri - MSC2545</li>
|
||||
<li xml:lang="uk">Пакунки наліпок - MSC2545</li>
|
||||
<li xml:lang="x-test">xxSticker Packs - MSC2545xx</li>
|
||||
@@ -149,11 +178,17 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="ar">موقع الأحداث - MSC3488</li>
|
||||
<li xml:lang="ca">Esdeveniments d'ubicació - MSC3488</li>
|
||||
<li xml:lang="ca-valencia">Esdeveniments d'ubicació - MSC3488</li>
|
||||
<li xml:lang="en-GB">Location Events - MSC3488</li>
|
||||
<li xml:lang="es">Eventos de ubicación - MSC3488</li>
|
||||
<li xml:lang="fr">Événements de lieu - MSC3488</li>
|
||||
<li xml:lang="gl">Localización de eventos - MSC3488</li>
|
||||
<li xml:lang="ia">Eventos de Location - MSC3488</li>
|
||||
<li xml:lang="it">Località eventi - MSC3488</li>
|
||||
<li xml:lang="ka">მდებარეობის მოვლენები - MSC3488</li>
|
||||
<li xml:lang="nl">Locatie gebeurtenissen - MSC3488</li>
|
||||
<li xml:lang="pt">Eventos com Localizações - MSC3488</li>
|
||||
<li xml:lang="sl">Location Events - MSC3488</li>
|
||||
<li xml:lang="ta">இட நிகழ்வுகள் - MSC3488</li>
|
||||
<li xml:lang="tr">Konum Etkinlikleri - MSC3488</li>
|
||||
<li xml:lang="uk">Місцеві зустрічі - MSC3488</li>
|
||||
<li xml:lang="x-test">xxLocation Events - MSC3488xx</li>
|
||||
@@ -177,6 +212,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<developer_name xml:lang="eu">KDE komunitatea</developer_name>
|
||||
<developer_name xml:lang="fi">KDE-yhteisö</developer_name>
|
||||
<developer_name xml:lang="fr">La communauté de KDE</developer_name>
|
||||
<developer_name xml:lang="gl">A comunidade KDE</developer_name>
|
||||
<developer_name xml:lang="hu">A KDE Közösség</developer_name>
|
||||
<developer_name xml:lang="ia">Le communitate de KDE</developer_name>
|
||||
<developer_name xml:lang="id">Komunitas KDE</developer_name>
|
||||
@@ -194,6 +230,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<developer_name xml:lang="sk">KDE Komunita</developer_name>
|
||||
<developer_name xml:lang="sl">Skupnost KDE</developer_name>
|
||||
<developer_name xml:lang="sv">KDE-gemenskapen</developer_name>
|
||||
<developer_name xml:lang="ta">கே.டீ.யீ. சமூகம்</developer_name>
|
||||
<developer_name xml:lang="tr">KDE Topluluğu</developer_name>
|
||||
<developer_name xml:lang="uk">Спільнота KDE</developer_name>
|
||||
<developer_name xml:lang="x-test">xxThe KDE Communityxx</developer_name>
|
||||
@@ -203,6 +240,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<custom>
|
||||
<value key="KDE::matrix">#neochat:kde.org</value>
|
||||
<value key="KDE::windows_store">https://www.microsoft.com/store/apps/9PNXWVNRC29H</value>
|
||||
<value key="KDE::mastodon">https://kde.social/@neochat</value>
|
||||
</custom>
|
||||
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
|
||||
<screenshots>
|
||||
@@ -217,6 +255,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
<release version="23.04.2" date="2023-06-08"/>
|
||||
<release version="23.04.1" date="2023-05-11"/>
|
||||
<release version="23.04.0" date="2023-04-20">
|
||||
<artifacts>
|
||||
|
||||
@@ -15,6 +15,7 @@ Name[es]=NeoChat
|
||||
Name[eu]=NeoChat
|
||||
Name[fi]=NeoChat
|
||||
Name[fr]=NeoChat
|
||||
Name[gl]=NeoChat
|
||||
Name[hu]=NeoChat
|
||||
Name[ia]=Neochat
|
||||
Name[id]=NeoChat
|
||||
@@ -52,6 +53,7 @@ GenericName[es]=Cliente para Matrix
|
||||
GenericName[eu]=Matrix bezeroa
|
||||
GenericName[fi]=Matrix-asiakas
|
||||
GenericName[fr]=Client « Matrix »
|
||||
GenericName[gl]=Cliente de Matrix
|
||||
GenericName[hu]=Matrix kliens
|
||||
GenericName[ia]=Cliente de Matrice
|
||||
GenericName[id]=Klien Matrix
|
||||
@@ -88,6 +90,7 @@ Comment[es]=Cliente para el protocolo Matrix
|
||||
Comment[eu]=Matrix protokolorako bezeroa
|
||||
Comment[fi]=Asiakas Matrix-yhteyskäytännölle
|
||||
Comment[fr]=Client pour le protocole « Matrix »
|
||||
Comment[gl]=Cliente para o protocolo Matrix
|
||||
Comment[hu]=Kliens a Matrix protokollhoz
|
||||
Comment[ia]=Cliente per le protocollo de Matrix
|
||||
Comment[id]=Klien untuk protokol Matrix
|
||||
|
||||
721
po/ar/neochat.po
721
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
732
po/az/neochat.po
732
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
671
po/ca/neochat.po
671
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
773
po/cs/neochat.po
773
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
658
po/da/neochat.po
658
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
747
po/de/neochat.po
747
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
722
po/el/neochat.po
722
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
689
po/es/neochat.po
689
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
724
po/eu/neochat.po
724
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
724
po/fi/neochat.po
724
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
721
po/fr/neochat.po
721
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
727
po/hu/neochat.po
727
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
722
po/ia/neochat.po
722
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
952
po/id/neochat.po
952
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
713
po/ie/neochat.po
713
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
697
po/it/neochat.po
697
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
649
po/ja/neochat.po
649
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
687
po/ka/neochat.po
687
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
728
po/ko/neochat.po
728
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
647
po/lt/neochat.po
647
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
689
po/nl/neochat.po
689
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
660
po/nn/neochat.po
660
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
713
po/pa/neochat.po
713
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
728
po/pl/neochat.po
728
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
665
po/pt/neochat.po
665
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
729
po/ru/neochat.po
729
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
713
po/sk/neochat.po
713
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
697
po/sl/neochat.po
697
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
726
po/sv/neochat.po
726
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
695
po/ta/neochat.po
695
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
693
po/tr/neochat.po
693
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
695
po/uk/neochat.po
695
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ add_library(neochat STATIC
|
||||
models/messagefiltermodel.cpp
|
||||
models/roomlistmodel.cpp
|
||||
models/sortfilterspacelistmodel.cpp
|
||||
models/accountemoticonmodel.cpp
|
||||
spacehierarchycache.cpp
|
||||
roommanager.cpp
|
||||
neochatroom.cpp
|
||||
@@ -24,6 +25,7 @@ add_library(neochat STATIC
|
||||
models/publicroomlistmodel.cpp
|
||||
models/userdirectorylistmodel.cpp
|
||||
models/keywordnotificationrulemodel.cpp
|
||||
models/emoticonfiltermodel.cpp
|
||||
notificationsmanager.cpp
|
||||
models/sortfilterroomlistmodel.cpp
|
||||
chatdocumenthandler.cpp
|
||||
@@ -52,6 +54,8 @@ add_library(neochat STATIC
|
||||
events/imagepackevent.cpp
|
||||
events/joinrulesevent.cpp
|
||||
events/stickerevent.cpp
|
||||
models/reactionmodel.cpp
|
||||
delegatesizehelper.cpp
|
||||
)
|
||||
|
||||
ecm_qt_declare_logging_category(neochat
|
||||
@@ -61,6 +65,12 @@ ecm_qt_declare_logging_category(neochat
|
||||
DEFAULT_SEVERITY Info
|
||||
)
|
||||
|
||||
ecm_qt_declare_logging_category(neochat
|
||||
HEADER "voip_logging.h"
|
||||
IDENTIFIER "voip"
|
||||
CATEGORY_NAME "org.kde.neochat.voip"
|
||||
)
|
||||
|
||||
add_executable(neochat-app
|
||||
main.cpp
|
||||
res.qrc
|
||||
@@ -107,6 +117,20 @@ endif()
|
||||
|
||||
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
|
||||
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient${QUOTIENT_SUFFIX} cmark::cmark ${QTKEYCHAIN_LIBRARIES} QCoro::Core)
|
||||
|
||||
if (TARGET PkgConfig::GSTREAMER)
|
||||
target_link_libraries(neochat PUBLIC PkgConfig::GSTREAMER)
|
||||
target_sources(neochat PRIVATE
|
||||
call/callmanager.cpp
|
||||
call/callsession.cpp
|
||||
call/audiosources.cpp
|
||||
call/videosources.cpp
|
||||
call/devicemonitor.cpp
|
||||
models/callparticipantsmodel.cpp
|
||||
call/callparticipant.cpp
|
||||
)
|
||||
target_compile_definitions(neochat PUBLIC GSTREAMER_AVAILABLE)
|
||||
endif()
|
||||
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
|
||||
|
||||
if(NEOCHAT_FLATPAK)
|
||||
|
||||
99
src/call/audiosources.cpp
Normal file
99
src/call/audiosources.cpp
Normal file
@@ -0,0 +1,99 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "audiosources.h"
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QString>
|
||||
|
||||
#include "devicemonitor.h"
|
||||
|
||||
#include "neochatconfig.h"
|
||||
|
||||
int AudioSources::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return DeviceMonitor::instance().audioSources().size();
|
||||
}
|
||||
|
||||
QVariant AudioSources::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (index.row() >= DeviceMonitor::instance().audioSources().size()) {
|
||||
return QVariant(QStringLiteral("DEADBEEF"));
|
||||
}
|
||||
if (role == TitleRole) {
|
||||
return DeviceMonitor::instance().audioSources()[index.row()]->title;
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> AudioSources::roleNames() const
|
||||
{
|
||||
return {
|
||||
{TitleRole, "title"},
|
||||
};
|
||||
}
|
||||
|
||||
AudioSources::AudioSources()
|
||||
: QAbstractListModel()
|
||||
{
|
||||
connect(&DeviceMonitor::instance(), &DeviceMonitor::audioSourceAdded, this, [this]() {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
Q_EMIT currentIndexChanged();
|
||||
});
|
||||
connect(&DeviceMonitor::instance(), &DeviceMonitor::audioSourceRemoved, this, [this]() {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
Q_EMIT currentIndexChanged();
|
||||
});
|
||||
}
|
||||
|
||||
GstDevice *AudioSources::currentDevice() const
|
||||
{
|
||||
const auto config = NeoChatConfig::self();
|
||||
const QString name = config->microphone();
|
||||
for (const auto &audioSource : DeviceMonitor::instance().audioSources()) {
|
||||
if (audioSource->title == name) {
|
||||
qDebug() << "WebRTC: microphone:" << name;
|
||||
return audioSource->device;
|
||||
}
|
||||
}
|
||||
return DeviceMonitor::instance().audioSources()[0]->device;
|
||||
}
|
||||
|
||||
void AudioSources::setCurrentIndex(int index) const
|
||||
{
|
||||
if (DeviceMonitor::instance().audioSources().size() == 0) {
|
||||
return;
|
||||
}
|
||||
NeoChatConfig::setMicrophone(DeviceMonitor::instance().audioSources()[index]->title);
|
||||
NeoChatConfig::self()->save();
|
||||
}
|
||||
|
||||
int AudioSources::currentIndex() const
|
||||
{
|
||||
const auto config = NeoChatConfig::self();
|
||||
const QString name = config->microphone();
|
||||
if (name.isEmpty()) {
|
||||
return getDefaultDeviceIndex();
|
||||
}
|
||||
for (auto i = 0; i < DeviceMonitor::instance().audioSources().size(); i++) {
|
||||
if (DeviceMonitor::instance().audioSources()[i]->title == name) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int AudioSources::getDefaultDeviceIndex() const
|
||||
{
|
||||
for (auto i = 0; i < DeviceMonitor::instance().audioSources().size(); i++) {
|
||||
if (DeviceMonitor::instance().audioSources()[i]->isDefault) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
41
src/call/audiosources.h
Normal file
41
src/call/audiosources.h
Normal file
@@ -0,0 +1,41 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QAbstractListModel>
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
class AudioSources : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
TitleRole = Qt::UserRole + 1,
|
||||
};
|
||||
|
||||
static AudioSources &instance()
|
||||
{
|
||||
static AudioSources _instance;
|
||||
return _instance;
|
||||
}
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
GstDevice *currentDevice() const;
|
||||
|
||||
void setCurrentIndex(int index) const;
|
||||
int currentIndex() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void currentIndexChanged();
|
||||
|
||||
private:
|
||||
AudioSources();
|
||||
int getDefaultDeviceIndex() const;
|
||||
};
|
||||
199
src/call/calldevices.cpp
Normal file
199
src/call/calldevices.cpp
Normal file
@@ -0,0 +1,199 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "calldevices.h"
|
||||
#include "audiodevicesmodel.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "videodevicesmodel.h"
|
||||
#include <QStringView>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
|
||||
#include "voiplogging.h"
|
||||
|
||||
#ifdef GSTREAMER_AVAILABLE
|
||||
extern "C" {
|
||||
#include "gst/gst.h"
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef GSTREAMER_AVAILABLE
|
||||
|
||||
CallDevices::CallDevices()
|
||||
: QObject()
|
||||
, m_audioDevicesModel(new AudioDevicesModel(this))
|
||||
, m_videoDevicesModel(new VideoDevicesModel(this))
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
AudioDevicesModel *CallDevices::audioDevicesModel() const
|
||||
{
|
||||
return m_audioDevicesModel;
|
||||
}
|
||||
|
||||
VideoDevicesModel *CallDevices::videoDevicesModel() const
|
||||
{
|
||||
return m_videoDevicesModel;
|
||||
}
|
||||
|
||||
void CallDevices::addDevice(GstDevice *device)
|
||||
{
|
||||
if (!device)
|
||||
return;
|
||||
|
||||
gchar *type = gst_device_get_device_class(device);
|
||||
bool isVideo = !std::strncmp(type, "Video", 5);
|
||||
g_free(type);
|
||||
if (isVideo) {
|
||||
m_videoDevicesModel->addDevice(device);
|
||||
m_videoDevicesModel->setDefaultDevice();
|
||||
} else {
|
||||
m_audioDevicesModel->addDevice(device);
|
||||
m_audioDevicesModel->setDefaultDevice();
|
||||
}
|
||||
}
|
||||
|
||||
void CallDevices::removeDevice(GstDevice *device, bool changed)
|
||||
{
|
||||
if (device) {
|
||||
if (m_audioDevicesModel->removeDevice(device, changed) || m_videoDevicesModel->removeDevice(device, changed))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
gboolean newBusMessage(GstBus *bus, GstMessage *msg, gpointer user_data)
|
||||
{
|
||||
Q_UNUSED(bus)
|
||||
Q_UNUSED(user_data)
|
||||
|
||||
switch (GST_MESSAGE_TYPE(msg)) {
|
||||
case GST_MESSAGE_DEVICE_ADDED: {
|
||||
GstDevice *device;
|
||||
gst_message_parse_device_added(msg, &device);
|
||||
CallDevices::instance().addDevice(device);
|
||||
Q_EMIT CallDevices::instance().devicesChanged();
|
||||
break;
|
||||
}
|
||||
case GST_MESSAGE_DEVICE_REMOVED: {
|
||||
GstDevice *device;
|
||||
gst_message_parse_device_removed(msg, &device);
|
||||
CallDevices::instance().removeDevice(device, false);
|
||||
Q_EMIT CallDevices::instance().devicesChanged();
|
||||
break;
|
||||
}
|
||||
case GST_MESSAGE_DEVICE_CHANGED: {
|
||||
GstDevice *device;
|
||||
GstDevice *oldDevice;
|
||||
gst_message_parse_device_changed(msg, &device, &oldDevice);
|
||||
CallDevices::instance().removeDevice(oldDevice, true);
|
||||
CallDevices::instance().addDevice(device);
|
||||
Q_EMIT CallDevices::instance().devicesChanged();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void CallDevices::init()
|
||||
{
|
||||
static GstDeviceMonitor *monitor = nullptr;
|
||||
if (!monitor) {
|
||||
monitor = gst_device_monitor_new();
|
||||
Q_ASSERT(monitor);
|
||||
GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw");
|
||||
gst_device_monitor_add_filter(monitor, "Audio/Source", caps);
|
||||
gst_device_monitor_add_filter(monitor, "Audio/Duplex", caps);
|
||||
gst_caps_unref(caps);
|
||||
caps = gst_caps_new_empty_simple("video/x-raw");
|
||||
gst_device_monitor_add_filter(monitor, "Video/Source", caps);
|
||||
gst_device_monitor_add_filter(monitor, "Video/Duplex", caps);
|
||||
gst_caps_unref(caps);
|
||||
|
||||
GstBus *bus = gst_device_monitor_get_bus(monitor);
|
||||
gst_bus_add_watch(bus, newBusMessage, nullptr);
|
||||
gst_object_unref(bus);
|
||||
if (!gst_device_monitor_start(monitor)) {
|
||||
qCCritical(voip) << "Failed to start device monitor";
|
||||
return;
|
||||
} else {
|
||||
qCDebug(voip) << "Device monitor started";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool CallDevices::hasMicrophone() const
|
||||
{
|
||||
return m_audioDevicesModel->hasMicrophone();
|
||||
}
|
||||
|
||||
bool CallDevices::hasCamera() const
|
||||
{
|
||||
return m_videoDevicesModel->hasCamera();
|
||||
}
|
||||
|
||||
QStringList CallDevices::resolutions(const QString &cameraName) const
|
||||
{
|
||||
return m_videoDevicesModel->resolutions(cameraName);
|
||||
}
|
||||
|
||||
QStringList CallDevices::frameRates(const QString &cameraName, const QString &resolution) const
|
||||
{
|
||||
if (auto s = m_videoDevicesModel->getVideoSource(cameraName); s) {
|
||||
if (auto it = std::find_if(s->caps.cbegin(),
|
||||
s->caps.cend(),
|
||||
[&](const auto &c) {
|
||||
return c.resolution == resolution;
|
||||
});
|
||||
it != s->caps.cend())
|
||||
return it->frameRates;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
GstDevice *CallDevices::audioDevice() const
|
||||
{
|
||||
return m_audioDevicesModel->currentDevice();
|
||||
}
|
||||
|
||||
GstDevice *CallDevices::videoDevice(QPair<int, int> &resolution, QPair<int, int> &frameRate) const
|
||||
{
|
||||
return m_videoDevicesModel->currentDevice(resolution, frameRate);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
bool CallDevices::hasMicrophone() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CallDevices::hasCamera() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QStringList CallDevices::names(bool, const QString &) const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
QStringList CallDevices::resolutions(const QString &) const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
QStringList CallDevices::frameRates(const QString &, const QString &) const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
#endif
|
||||
64
src/call/calldevices.h
Normal file
64
src/call/calldevices.h
Normal file
@@ -0,0 +1,64 @@
|
||||
// SPDX-FileCopyrightText: 2021 Contributors
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <QObject>
|
||||
|
||||
typedef struct _GstDevice GstDevice;
|
||||
|
||||
class CallDevices;
|
||||
class AudioDevicesModel;
|
||||
class VideoDevicesModel;
|
||||
|
||||
class CallDevices : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(AudioDevicesModel *audioDevices READ audioDevicesModel CONSTANT);
|
||||
Q_PROPERTY(VideoDevicesModel *videoDevices READ videoDevicesModel CONSTANT);
|
||||
|
||||
public:
|
||||
static CallDevices &instance()
|
||||
{
|
||||
static CallDevices instance;
|
||||
return instance;
|
||||
}
|
||||
CallDevices(CallDevices const &) = delete;
|
||||
void operator=(CallDevices const &) = delete;
|
||||
|
||||
bool hasMicrophone() const;
|
||||
bool hasCamera() const;
|
||||
QStringList names(bool isVideo, const QString &defaultDevice) const;
|
||||
QStringList resolutions(const QString &cameraName) const;
|
||||
QStringList frameRates(const QString &cameraName, const QString &resolution) const;
|
||||
|
||||
AudioDevicesModel *audioDevicesModel() const;
|
||||
VideoDevicesModel *videoDevicesModel() const;
|
||||
|
||||
void addDevice(GstDevice *device);
|
||||
void removeDevice(GstDevice *device, bool changed);
|
||||
|
||||
Q_SIGNALS:
|
||||
void devicesChanged();
|
||||
|
||||
private:
|
||||
CallDevices();
|
||||
|
||||
void init();
|
||||
GstDevice *audioDevice() const;
|
||||
GstDevice *videoDevice(QPair<int, int> &resolution, QPair<int, int> &frameRate) const;
|
||||
|
||||
AudioDevicesModel *m_audioDevicesModel;
|
||||
VideoDevicesModel *m_videoDevicesModel;
|
||||
|
||||
friend class CallSession;
|
||||
friend class Audio;
|
||||
};
|
||||
617
src/call/callmanager.cpp
Normal file
617
src/call/callmanager.cpp
Normal file
@@ -0,0 +1,617 @@
|
||||
// SPDX-FileCopyrightText: 2020-2021 Nheko Authors
|
||||
// SPDX-FileCopyrightText: 2021-2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "callmanager.h"
|
||||
|
||||
#include "controller.h"
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
#include "voiplogging.h"
|
||||
#include <KLocalizedString>
|
||||
#include <QDateTime>
|
||||
|
||||
#include <QMediaPlaylist>
|
||||
#include <QMimeDatabase>
|
||||
#include <qcoro/qcorosignal.h>
|
||||
#include <qt_connection_util.h>
|
||||
|
||||
#include "neochatconfig.h"
|
||||
|
||||
#define CALL_VERSION "1"
|
||||
|
||||
CallManager::CallManager()
|
||||
{
|
||||
init();
|
||||
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this] {
|
||||
updateTurnServers();
|
||||
});
|
||||
}
|
||||
|
||||
QCoro::Task<void> CallManager::updateTurnServers()
|
||||
{
|
||||
if (m_cachedTurnUrisValidUntil > QDateTime::currentDateTime()) {
|
||||
co_return;
|
||||
}
|
||||
Controller::instance().activeConnection()->getTurnServers();
|
||||
|
||||
auto servers = co_await qCoro(Controller::instance().activeConnection(), &Connection::turnServersChanged);
|
||||
m_cachedTurnUrisValidUntil = QDateTime::currentDateTime().addSecs(servers["ttl"].toInt());
|
||||
|
||||
const auto password = servers["password"].toString();
|
||||
const auto username = servers["username"].toString();
|
||||
const auto uris = servers["uris"].toArray();
|
||||
|
||||
m_cachedTurnUris.clear();
|
||||
for (const auto &u : uris) {
|
||||
QString uri = u.toString();
|
||||
auto c = uri.indexOf(':');
|
||||
if (c == -1) {
|
||||
qCWarning(voip) << "Invalid TURN URI:" << uri;
|
||||
continue;
|
||||
}
|
||||
QString scheme = uri.left(c);
|
||||
if (scheme != "turn" && scheme != "turns") {
|
||||
qCWarning(voip) << "Invalid TURN scheme:" << scheme;
|
||||
continue;
|
||||
}
|
||||
m_cachedTurnUris += QStringLiteral("%1://%2:%3@%4").arg(scheme, QUrl::toPercentEncoding(username), QUrl::toPercentEncoding(password), uri.mid(c + 1));
|
||||
}
|
||||
}
|
||||
|
||||
QString CallManager::callId() const
|
||||
{
|
||||
return m_callId;
|
||||
}
|
||||
|
||||
void CallManager::handleCallEvent(NeoChatRoom *room, const Quotient::RoomEvent *event)
|
||||
{
|
||||
if (const auto &inviteEvent = eventCast<const CallInviteEvent>(event)) {
|
||||
handleInvite(room, inviteEvent);
|
||||
} else if (const auto &hangupEvent = eventCast<const CallHangupEvent>(event)) {
|
||||
handleHangup(room, hangupEvent);
|
||||
} else if (const auto &candidatesEvent = eventCast<const CallCandidatesEvent>(event)) {
|
||||
handleCandidates(room, candidatesEvent);
|
||||
} else if (const auto &answerEvent = eventCast<const CallAnswerEvent>(event)) {
|
||||
handleAnswer(room, answerEvent);
|
||||
} else if (const auto &negotiateEvent = eventCast<const CallNegotiateEvent>(event)) {
|
||||
handleNegotiate(room, negotiateEvent);
|
||||
}
|
||||
}
|
||||
|
||||
void CallManager::checkStartCall()
|
||||
{
|
||||
if ((m_incomingCandidates.isEmpty() && !m_incomingSdp.contains("candidates"_ls)) || m_incomingSdp.isEmpty()) {
|
||||
qCDebug(voip) << "Not ready to start this call yet";
|
||||
return;
|
||||
}
|
||||
m_session->acceptAnswer(m_incomingSdp, m_incomingCandidates, m_remoteUser->id());
|
||||
m_incomingCandidates.clear();
|
||||
m_incomingSdp.clear();
|
||||
setGlobalState(ACTIVE);
|
||||
}
|
||||
|
||||
void CallManager::handleAnswer(NeoChatRoom *room, const Quotient::CallAnswerEvent *event)
|
||||
{
|
||||
if (globalState() != OUTGOING) {
|
||||
qCDebug(voip) << "Ignoring answer while in state" << globalState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->callId() != m_callId) {
|
||||
qCDebug(voip) << "Ignoring answer for unknown call id" << event->callId() << ". Our call id is" << m_callId;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->senderId() == room->localUser()->id() && partyId() == event->contentJson()["party_id"].toString()) {
|
||||
qCDebug(voip) << "Ignoring echo for answer";
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->senderId() == room->localUser()->id()) {
|
||||
qCDebug(voip) << "Call was accepted on a different device";
|
||||
// Show the user that call was accepted on a different device
|
||||
// Stop ringing
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO handle that MSC wrt to accepting on other devices
|
||||
m_session->setMetadata(event->contentJson()["org.matrix.msc3077.sdp_stream_metadata"].toObject());
|
||||
m_remotePartyId = event->contentJson()["party_id"].toString();
|
||||
m_incomingSdp = event->sdp();
|
||||
checkStartCall();
|
||||
}
|
||||
|
||||
void CallManager::handleCandidates(NeoChatRoom *room, const Quotient::CallCandidatesEvent *event)
|
||||
{
|
||||
// TODO what if candidates come before invite? this looks wrong
|
||||
if (globalState() == IDLE) {
|
||||
qCDebug(voip) << "Ignoring candidates in state" << globalState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->senderId() == room->localUser()->id()) {
|
||||
qCDebug(voip) << "Ignoring candidates sent by ourself";
|
||||
return;
|
||||
}
|
||||
|
||||
if (globalState() == ACTIVE) {
|
||||
QVector<Candidate> candidates;
|
||||
for (const auto &candidate : event->candidates()) {
|
||||
candidates += Candidate{candidate.toObject()["candidate"].toString(),
|
||||
candidate.toObject()["sdpMLineIndex"].toInt(),
|
||||
candidate.toObject()["sdpMid"].toString()};
|
||||
}
|
||||
m_session->acceptCandidates(candidates);
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(voip) << "Storing" << event->candidates().size() << "incoming candidates";
|
||||
for (const auto &candidate : event->candidates()) {
|
||||
m_incomingCandidates +=
|
||||
Candidate{candidate.toObject()["candidate"].toString(), candidate.toObject()["sdpMLineIndex"].toInt(), candidate.toObject()["sdpMid"].toString()};
|
||||
}
|
||||
|
||||
if (globalState() == OUTGOING) {
|
||||
checkStartCall();
|
||||
}
|
||||
}
|
||||
|
||||
void CallManager::handleInvite(NeoChatRoom *room, const Quotient::CallInviteEvent *event)
|
||||
{
|
||||
if (event->senderId() == room->localUser()->id()) {
|
||||
qCDebug(voip) << "Igoring invite sent by ourself";
|
||||
return;
|
||||
}
|
||||
if (globalState() != IDLE) {
|
||||
// TODO handle glare
|
||||
qCDebug(voip) << "Ignoring invite while already in a call";
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->originTimestamp() < QDateTime::currentDateTime().addSecs(-60)) {
|
||||
qCDebug(voip) << "Ignoring outdated invite; sent at:" << event->originTimestamp() << "current:" << QDateTime::currentDateTime();
|
||||
return;
|
||||
}
|
||||
|
||||
setGlobalState(INCOMING);
|
||||
|
||||
m_incomingSdp = event->sdp();
|
||||
setRemoteUser(dynamic_cast<NeoChatUser *>(room->user(event->senderId())));
|
||||
setRoom(room);
|
||||
setCallId(event->callId());
|
||||
setPartyId(generatePartyId());
|
||||
m_remotePartyId = event->contentJson()["party_id"].toString();
|
||||
setLifetime(event->lifetime());
|
||||
Q_EMIT incomingCall(remoteUser(), room, event->lifetime(), callId());
|
||||
ring(event->lifetime());
|
||||
}
|
||||
|
||||
void CallManager::handleNegotiate(NeoChatRoom *room, const Quotient::CallNegotiateEvent *event)
|
||||
{
|
||||
Q_UNUSED(room);
|
||||
if (event->callId() != m_callId) {
|
||||
qCDebug(voip) << "Ignoring negotiate for unknown call id" << event->callId() << ". Our call id is" << m_callId;
|
||||
return;
|
||||
}
|
||||
if (event->partyId() != m_remotePartyId) {
|
||||
qCDebug(voip) << "Ignoring negotiate for unknown party id" << event->partyId() << ". Remote party id is" << m_remotePartyId;
|
||||
return;
|
||||
}
|
||||
if (event->senderId() != m_remoteUser->id()) {
|
||||
qCDebug(voip) << "Ignoring negotiate for unknown user id" << event->senderId() << ". Remote user id is" << m_remoteUser->id();
|
||||
return;
|
||||
}
|
||||
// TODO DUPLICATES FFS
|
||||
m_session->setMetadata(event->contentJson()["org.matrix.msc3077.sdp_stream_metadata"].toObject());
|
||||
m_session->renegotiateOffer(event->sdp(), m_remoteUser->id(), event->contentJson()["description"]["type"] == QStringLiteral("answer"));
|
||||
}
|
||||
|
||||
void CallManager::ring(int lifetime)
|
||||
{
|
||||
// TODO put a better default ringtone in the kcfg
|
||||
// TODO which one? ship one? plasma-mobile-sounds?
|
||||
if (!QFileInfo::exists(NeoChatConfig::ringtone())) {
|
||||
qCWarning(voip) << "Ringtone file doesn't exist. Not audibly ringing";
|
||||
return;
|
||||
}
|
||||
auto ringtone = QUrl::fromLocalFile(NeoChatConfig::ringtone());
|
||||
m_playlist.setPlaybackMode(QMediaPlaylist::Loop);
|
||||
m_playlist.clear();
|
||||
m_ringPlayer.setPlaylist(&m_playlist);
|
||||
m_playlist.addMedia(ringtone);
|
||||
m_ringPlayer.play();
|
||||
QTimer::singleShot(lifetime, this, [this]() {
|
||||
stopRinging();
|
||||
Q_EMIT callEnded();
|
||||
});
|
||||
}
|
||||
|
||||
void CallManager::stopRinging()
|
||||
{
|
||||
m_ringPlayer.stop();
|
||||
}
|
||||
|
||||
void CallManager::handleHangup(NeoChatRoom *room, const Quotient::CallHangupEvent *event)
|
||||
{
|
||||
if (globalState() == IDLE) {
|
||||
qCDebug(voip) << "Ignoring hangup since we're not in a call";
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->senderId() == room->localUser()->id()) {
|
||||
qCDebug(voip) << "Ignoring hangup we sent ourselves";
|
||||
// TODO hangup-to-decline by different device?
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->callId() != m_callId) {
|
||||
qCDebug(voip) << "Hangup not for this call. Event's call id:" << event->callId() << ". Our call id" << m_callId;
|
||||
return;
|
||||
}
|
||||
|
||||
stopRinging();
|
||||
if (m_session) {
|
||||
m_session->end();
|
||||
delete m_session;
|
||||
}
|
||||
setGlobalState(IDLE);
|
||||
Q_EMIT callEnded();
|
||||
}
|
||||
|
||||
void CallManager::acceptCall()
|
||||
{
|
||||
// TODO metadata for this case
|
||||
if (globalState() != INCOMING) {
|
||||
qCWarning(voip) << "Not accepting call while state is" << globalState();
|
||||
return;
|
||||
}
|
||||
|
||||
stopRinging();
|
||||
|
||||
if (!checkPlugins()) {
|
||||
qCCritical(voip) << "Missing plugins; can't accept call";
|
||||
}
|
||||
|
||||
updateTurnServers();
|
||||
// TODO wait until candidates are here
|
||||
|
||||
m_session = CallSession::acceptCall(m_incomingSdp, m_incomingCandidates, m_cachedTurnUris, m_remoteUser->id(), this);
|
||||
m_participants->clear();
|
||||
connect(m_session.data(), &CallSession::stateChanged, this, [this] {
|
||||
Q_EMIT stateChanged();
|
||||
if (state() == CallSession::ICEFAILED) {
|
||||
Q_EMIT callEnded();
|
||||
}
|
||||
}); // TODO refactor away?
|
||||
m_incomingCandidates.clear();
|
||||
connectSingleShot(m_session.data(), &CallSession::answerCreated, this, [this](const QString &_sdp, const QVector<Candidate> &candidates) {
|
||||
const auto &[uuids, sdp] = mangleSdp(_sdp);
|
||||
QVector<std::pair<QString, QString>> msidToPurpose;
|
||||
for (const auto &uuid : uuids) {
|
||||
msidToPurpose += {uuid, "m.usermedia"}; // TODO
|
||||
}
|
||||
auto answer = createAnswer(m_callId, sdp, msidToPurpose);
|
||||
m_room->postJson("m.call.answer", answer);
|
||||
qCWarning(voip) << "Sending Answer";
|
||||
auto c = createCandidates(m_callId, candidates);
|
||||
auto cand = createCandidates(m_callId, candidates);
|
||||
m_room->postJson("m.call.candidates", cand);
|
||||
qCWarning(voip) << "Sending Candidates";
|
||||
setGlobalState(ACTIVE);
|
||||
});
|
||||
}
|
||||
|
||||
void CallManager::hangupCall()
|
||||
{
|
||||
qCDebug(voip) << "Ending call";
|
||||
if (m_session) {
|
||||
m_session->end();
|
||||
delete m_session;
|
||||
m_session = nullptr;
|
||||
}
|
||||
stopRinging();
|
||||
m_room->postJson("m.call.hangup", createHangup(m_callId));
|
||||
setGlobalState(IDLE);
|
||||
Q_EMIT callEnded();
|
||||
}
|
||||
|
||||
NeoChatUser *CallManager::remoteUser() const
|
||||
{
|
||||
return m_remoteUser;
|
||||
}
|
||||
|
||||
NeoChatRoom *CallManager::room() const
|
||||
{
|
||||
return m_room;
|
||||
}
|
||||
|
||||
CallSession::State CallManager::state() const
|
||||
{
|
||||
if (!m_session) {
|
||||
return CallSession::DISCONNECTED;
|
||||
}
|
||||
return m_session->state();
|
||||
}
|
||||
|
||||
int CallManager::lifetime() const
|
||||
{
|
||||
return m_lifetime;
|
||||
}
|
||||
|
||||
void CallManager::ignoreCall()
|
||||
{
|
||||
setLifetime(0);
|
||||
setCallId({});
|
||||
setRoom(nullptr);
|
||||
setRemoteUser(nullptr);
|
||||
}
|
||||
|
||||
void CallManager::startCall(NeoChatRoom *room)
|
||||
{
|
||||
if (m_session) {
|
||||
// Don't start calls if there already is one
|
||||
Q_EMIT Controller::instance().errorOccured(i18n("A call is already started"));
|
||||
return;
|
||||
}
|
||||
if (room->users().size() != 2) {
|
||||
// Don't start calls if the room doesn't have exactly two members
|
||||
Q_EMIT Controller::instance().errorOccured(i18n("Calls are limited to 1:1 rooms"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto missingPlugins = CallSession::missingPlugins();
|
||||
if (!missingPlugins.isEmpty()) {
|
||||
qCCritical(voip) << "Missing GStreamer plugins:" << missingPlugins;
|
||||
Q_EMIT Controller::instance().errorOccured("Missing GStreamer plugins.");
|
||||
return;
|
||||
}
|
||||
|
||||
setLifetime(60000);
|
||||
setRoom(room);
|
||||
setRemoteUser(otherUser(room));
|
||||
|
||||
updateTurnServers();
|
||||
|
||||
setCallId(generateCallId());
|
||||
setPartyId(generatePartyId());
|
||||
|
||||
m_participants->clear();
|
||||
for (const auto &user : m_room->users()) {
|
||||
auto participant = new CallParticipant(m_session);
|
||||
participant->m_user = dynamic_cast<NeoChatUser *>(user);
|
||||
m_participants->addParticipant(participant);
|
||||
}
|
||||
|
||||
m_session = CallSession::startCall(m_cachedTurnUris, this);
|
||||
setGlobalState(OUTGOING);
|
||||
connect(m_session, &CallSession::stateChanged, this, [this] {
|
||||
Q_EMIT stateChanged();
|
||||
if (state() == CallSession::ICEFAILED) {
|
||||
Q_EMIT callEnded();
|
||||
}
|
||||
});
|
||||
|
||||
connectSingleShot(m_session.data(), &CallSession::offerCreated, this, [this](const QString &_sdp, const QVector<Candidate> &candidates) {
|
||||
const auto &[uuids, sdp] = mangleSdp(_sdp);
|
||||
QVector<std::pair<QString, QString>> msidToPurpose;
|
||||
for (const auto &uuid : uuids) {
|
||||
msidToPurpose += {uuid, "m.usermedia"}; // TODO
|
||||
}
|
||||
qCWarning(voip) << "Sending Invite";
|
||||
qCWarning(voip) << "Sending Candidates";
|
||||
auto invite = createInvite(m_callId, sdp, msidToPurpose);
|
||||
auto c = createCandidates(m_callId, candidates);
|
||||
m_room->postJson("m.call.invite", invite);
|
||||
m_room->postJson("m.call.candidates", c);
|
||||
});
|
||||
|
||||
connect(m_session, &CallSession::renegotiate, this, [this](const QString &sdp, const QString &type) {
|
||||
QVector<std::pair<QString, QString>> msidToPurpose;
|
||||
const auto &[uuids, _sdp] = mangleSdp(sdp);
|
||||
for (const auto &uuid : uuids) {
|
||||
msidToPurpose += {uuid, "m.usermedia"}; // TODO
|
||||
}
|
||||
QJsonObject json{
|
||||
{QStringLiteral("lifetime"), 60000},
|
||||
{QStringLiteral("version"), 1},
|
||||
{QStringLiteral("description"), QJsonObject{{QStringLiteral("type"), type}, {QStringLiteral("sdp"), _sdp}}}, // AAAAA
|
||||
{QStringLiteral("party_id"), m_partyId},
|
||||
{QStringLiteral("call_id"), m_callId},
|
||||
};
|
||||
QJsonObject metadata;
|
||||
for (const auto &[stream, purpose] : msidToPurpose) {
|
||||
QJsonObject data = {{"purpose", purpose}};
|
||||
metadata[stream] = data;
|
||||
}
|
||||
json["org.matrix.msc3077.sdp_stream_metadata"] = metadata;
|
||||
m_room->postJson("m.call.negotiate", json);
|
||||
});
|
||||
}
|
||||
|
||||
QString CallManager::generateCallId() const
|
||||
{
|
||||
return QDateTime::currentDateTime().toString("yyyyMMddhhmmsszzz");
|
||||
}
|
||||
|
||||
QString CallManager::generatePartyId() const
|
||||
{
|
||||
return QUuid::createUuid().toString();
|
||||
}
|
||||
|
||||
void CallManager::setCallId(const QString &callId)
|
||||
{
|
||||
m_callId = callId;
|
||||
Q_EMIT callIdChanged();
|
||||
}
|
||||
|
||||
void CallManager::setPartyId(const QString &partyId)
|
||||
{
|
||||
m_partyId = partyId;
|
||||
}
|
||||
|
||||
void CallManager::setMuted(bool muted)
|
||||
{
|
||||
if (!m_session) {
|
||||
return;
|
||||
}
|
||||
m_session->setMuted(muted);
|
||||
Q_EMIT mutedChanged();
|
||||
}
|
||||
|
||||
bool CallManager::muted() const
|
||||
{
|
||||
if (!m_session) {
|
||||
return false;
|
||||
}
|
||||
return m_session->muted();
|
||||
}
|
||||
|
||||
bool CallManager::init()
|
||||
{
|
||||
qRegisterMetaType<Candidate>();
|
||||
qRegisterMetaType<QVector<Candidate>>();
|
||||
GError *error = nullptr;
|
||||
if (!gst_init_check(nullptr, nullptr, &error)) {
|
||||
QString strError;
|
||||
if (error) {
|
||||
strError += error->message;
|
||||
g_error_free(error);
|
||||
}
|
||||
qCCritical(voip) << "Failed to initialize GStreamer:" << strError;
|
||||
return false;
|
||||
}
|
||||
|
||||
gchar *version = gst_version_string();
|
||||
qCDebug(voip) << "GStreamer version" << version;
|
||||
g_free(version);
|
||||
|
||||
// Required to register the qml types
|
||||
auto _sink = gst_element_factory_make("qmlglsink", nullptr);
|
||||
Q_ASSERT(_sink);
|
||||
gst_object_unref(_sink);
|
||||
return true;
|
||||
}
|
||||
|
||||
void CallManager::setLifetime(int lifetime)
|
||||
{
|
||||
m_lifetime = lifetime;
|
||||
Q_EMIT lifetimeChanged();
|
||||
}
|
||||
|
||||
void CallManager::setRoom(NeoChatRoom *room)
|
||||
{
|
||||
m_room = room;
|
||||
Q_EMIT roomChanged();
|
||||
}
|
||||
|
||||
void CallManager::setRemoteUser(NeoChatUser *user)
|
||||
{
|
||||
m_remoteUser = user;
|
||||
Q_EMIT roomChanged();
|
||||
}
|
||||
|
||||
NeoChatUser *CallManager::otherUser(NeoChatRoom *room)
|
||||
{
|
||||
return dynamic_cast<NeoChatUser *>(room->users()[0]->id() == room->localUser()->id() ? room->users()[1] : room->users()[0]);
|
||||
}
|
||||
|
||||
QJsonObject CallManager::createCandidates(const QString &callId, const QVector<Candidate> &candidates) const
|
||||
{
|
||||
QJsonArray candidatesJson;
|
||||
for (const auto &candidate : candidates) {
|
||||
candidatesJson += QJsonObject{{"candidate", candidate.candidate}, {"sdpMid", candidate.sdpMid}, {"sdpMLineIndex", candidate.sdpMLineIndex}};
|
||||
}
|
||||
return QJsonObject{{"call_id", callId}, {"candidates", candidatesJson}, {"version", CALL_VERSION}, {"party_id", "todopartyid"}};
|
||||
}
|
||||
|
||||
void CallManager::setGlobalState(GlobalState globalState)
|
||||
{
|
||||
if (m_globalState == globalState) {
|
||||
return;
|
||||
}
|
||||
m_globalState = globalState;
|
||||
Q_EMIT globalStateChanged();
|
||||
}
|
||||
|
||||
CallManager::GlobalState CallManager::globalState() const
|
||||
{
|
||||
return m_globalState;
|
||||
}
|
||||
|
||||
CallParticipantsModel *CallManager::callParticipants() const
|
||||
{
|
||||
return m_participants;
|
||||
}
|
||||
|
||||
std::pair<QStringList, QString> CallManager::mangleSdp(const QString &_sdp)
|
||||
{
|
||||
QString sdp = _sdp;
|
||||
QRegularExpression regex("msid:user[0-9]+@host-[0-9a-f]+ webrtctransceiver([0-9])");
|
||||
auto iter = regex.globalMatch(sdp);
|
||||
QStringList uuids;
|
||||
|
||||
while (iter.hasNext()) {
|
||||
auto uuid = QUuid::createUuid();
|
||||
auto match = iter.next();
|
||||
uuids += uuid.toString();
|
||||
sdp.replace(match.captured(), QStringLiteral("msid:") + uuid.toString() + QStringLiteral(" foo"));
|
||||
}
|
||||
return {uuids, sdp};
|
||||
}
|
||||
|
||||
QJsonObject CallManager::createInvite(const QString &callId, const QString &sdp, const QVector<std::pair<QString, QString>> &msidToPurpose) const
|
||||
{
|
||||
QJsonObject metadata;
|
||||
for (const auto &[msid, purpose] : msidToPurpose) {
|
||||
metadata[msid] = QJsonObject{{"purpose", purpose}};
|
||||
}
|
||||
return {{"call_id", callId},
|
||||
{"party_id", "todopartyid"},
|
||||
{"lifetime", 60000},
|
||||
{"capabilities", QJsonObject{{"m.call.transferee", false}}},
|
||||
{"offer", QJsonObject{{"sdp", sdp}, {"type", "offer"}}},
|
||||
{"org.matrix.msc3077.sdp_stream_metadata", metadata},
|
||||
{"version", CALL_VERSION}};
|
||||
}
|
||||
|
||||
QJsonObject CallManager::createHangup(const QString &callId) const
|
||||
{
|
||||
return {{"call_id", callId}, {"party_id", "todopartyid"}, {"version", CALL_VERSION}};
|
||||
}
|
||||
|
||||
QJsonObject CallManager::createAnswer(const QString &callId, const QString &sdp, const QVector<std::pair<QString, QString>> &msidToPurpose) const
|
||||
{
|
||||
Q_ASSERT(!callId.isEmpty());
|
||||
QJsonObject metadata;
|
||||
for (const auto &[msid, purpose] : msidToPurpose) {
|
||||
metadata[msid] = QJsonObject{{"purpose", purpose}};
|
||||
}
|
||||
return {{"call_id", callId},
|
||||
{"party_id", "todopartyid"},
|
||||
{"lifetime", "lifetime"},
|
||||
{"capabilities", QJsonObject{{"m.call.transferee", false}}},
|
||||
{"offer", QJsonObject{{"sdp", sdp}, {"type", "offer"}}},
|
||||
{"org.matrix.msc3077.sdp_stream_metadata", metadata},
|
||||
{"version", CALL_VERSION}};
|
||||
}
|
||||
|
||||
void CallManager::toggleCamera()
|
||||
{
|
||||
m_session->toggleCamera();
|
||||
}
|
||||
QString CallManager::partyId() const
|
||||
{
|
||||
return m_partyId;
|
||||
}
|
||||
|
||||
bool CallManager::checkPlugins() const
|
||||
{
|
||||
auto missingPlugins = m_session->missingPlugins();
|
||||
if (!missingPlugins.isEmpty()) {
|
||||
qCCritical(voip) << "Missing GStreamer plugins:" << missingPlugins;
|
||||
Q_EMIT Controller::instance().errorOccured("Missing GStreamer plugins.");
|
||||
}
|
||||
return !missingPlugins.isEmpty();
|
||||
}
|
||||
159
src/call/callmanager.h
Normal file
159
src/call/callmanager.h
Normal file
@@ -0,0 +1,159 @@
|
||||
// SPDX-FileCopyrightText: 2020-2021 Nheko Authors
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "neochatroom.h"
|
||||
#include "neochatuser.h"
|
||||
#include <QAbstractListModel>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <events/roomevent.h>
|
||||
|
||||
#include "callsession.h"
|
||||
|
||||
#include "models/callparticipantsmodel.h"
|
||||
#include <events/callevents.h>
|
||||
|
||||
#include <QMediaPlayer>
|
||||
#include <QMediaPlaylist>
|
||||
#include <QTimer>
|
||||
#include <qcoro/task.h>
|
||||
|
||||
#include <qobjectdefs.h>
|
||||
|
||||
class CallSession;
|
||||
class QQuickItem;
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class CallManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum GlobalState {
|
||||
IDLE,
|
||||
INCOMING,
|
||||
OUTGOING,
|
||||
ACTIVE,
|
||||
};
|
||||
Q_ENUM(GlobalState);
|
||||
|
||||
Q_PROPERTY(GlobalState globalState READ globalState NOTIFY globalStateChanged)
|
||||
Q_PROPERTY(NeoChatUser *remoteUser READ remoteUser NOTIFY remoteUserChanged)
|
||||
Q_PROPERTY(QString callId READ callId NOTIFY callIdChanged)
|
||||
Q_PROPERTY(NeoChatRoom *room READ room NOTIFY roomChanged)
|
||||
Q_PROPERTY(int lifetime READ lifetime NOTIFY lifetimeChanged)
|
||||
Q_PROPERTY(bool muted READ muted WRITE setMuted NOTIFY mutedChanged)
|
||||
Q_PROPERTY(QQuickItem *item MEMBER m_item) // TODO allow for different devices for each session
|
||||
Q_PROPERTY(CallSession::State state READ state NOTIFY stateChanged)
|
||||
Q_PROPERTY(CallParticipantsModel *callParticipants READ callParticipants CONSTANT)
|
||||
|
||||
static CallManager &instance()
|
||||
{
|
||||
static CallManager _instance;
|
||||
return _instance;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString callId() const;
|
||||
[[nodiscard]] QString partyId() const;
|
||||
|
||||
CallSession::State state() const;
|
||||
|
||||
NeoChatUser *remoteUser() const;
|
||||
NeoChatRoom *room() const;
|
||||
|
||||
int lifetime() const;
|
||||
|
||||
bool muted() const;
|
||||
void setMuted(bool muted);
|
||||
|
||||
CallManager::GlobalState globalState() const;
|
||||
|
||||
void handleCallEvent(NeoChatRoom *room, const RoomEvent *event);
|
||||
|
||||
Q_INVOKABLE void startCall(NeoChatRoom *room);
|
||||
Q_INVOKABLE void acceptCall();
|
||||
Q_INVOKABLE void hangupCall();
|
||||
Q_INVOKABLE void ignoreCall();
|
||||
|
||||
Q_INVOKABLE void toggleCamera();
|
||||
|
||||
QCoro::Task<void> updateTurnServers();
|
||||
|
||||
[[nodiscard]] CallParticipantsModel *callParticipants() const;
|
||||
|
||||
QQuickItem *m_item = nullptr;
|
||||
|
||||
Q_SIGNALS:
|
||||
void currentCallIdChanged();
|
||||
void incomingCall(NeoChatUser *user, NeoChatRoom *room, int timeout, const QString &callId);
|
||||
void callEnded();
|
||||
void remoteUserChanged();
|
||||
void callIdChanged();
|
||||
void roomChanged();
|
||||
void stateChanged();
|
||||
void lifetimeChanged();
|
||||
void mutedChanged();
|
||||
void globalStateChanged();
|
||||
|
||||
private:
|
||||
CallManager();
|
||||
QString m_callId;
|
||||
|
||||
QVector<Candidate> m_incomingCandidates;
|
||||
QString m_incomingSdp;
|
||||
|
||||
[[nodiscard]] bool checkPlugins() const;
|
||||
|
||||
QStringList m_cachedTurnUris;
|
||||
QDateTime m_cachedTurnUrisValidUntil = QDateTime::fromSecsSinceEpoch(0);
|
||||
|
||||
NeoChatUser *m_remoteUser = nullptr;
|
||||
NeoChatRoom *m_room = nullptr;
|
||||
QString m_remotePartyId;
|
||||
QString m_partyId;
|
||||
int m_lifetime = 0;
|
||||
|
||||
GlobalState m_globalState = IDLE;
|
||||
|
||||
void handleInvite(NeoChatRoom *room, const CallInviteEvent *event);
|
||||
void handleHangup(NeoChatRoom *room, const CallHangupEvent *event);
|
||||
void handleCandidates(NeoChatRoom *room, const CallCandidatesEvent *event);
|
||||
void handleAnswer(NeoChatRoom *room, const CallAnswerEvent *event);
|
||||
void handleNegotiate(NeoChatRoom *room, const CallNegotiateEvent *event);
|
||||
void checkStartCall();
|
||||
|
||||
void ring(int lifetime);
|
||||
void stopRinging();
|
||||
|
||||
[[nodiscard]] QString generateCallId() const;
|
||||
[[nodiscard]] QString generatePartyId() const;
|
||||
bool init();
|
||||
|
||||
bool m_initialised = false;
|
||||
QPointer<CallSession> m_session = nullptr;
|
||||
|
||||
void setLifetime(int lifetime);
|
||||
void setRoom(NeoChatRoom *room);
|
||||
void setRemoteUser(NeoChatUser *user);
|
||||
void setCallId(const QString &callId);
|
||||
void setPartyId(const QString &partyId);
|
||||
void setGlobalState(GlobalState state);
|
||||
|
||||
std::pair<QStringList, QString> mangleSdp(const QString &sdp);
|
||||
|
||||
CallParticipantsModel *m_participants = new CallParticipantsModel();
|
||||
|
||||
NeoChatUser *otherUser(NeoChatRoom *room);
|
||||
|
||||
[[nodiscard]] QJsonObject createCandidates(const QString &callId, const QVector<Candidate> &candidates) const;
|
||||
[[nodiscard]] QJsonObject createInvite(const QString &callId, const QString &sdp, const QVector<std::pair<QString, QString>> &msidToPurpose) const;
|
||||
[[nodiscard]] QJsonObject createHangup(const QString &callId) const;
|
||||
[[nodiscard]] QJsonObject createAnswer(const QString &callId, const QString &sdp, const QVector<std::pair<QString, QString>> &msidToPurpose) const;
|
||||
|
||||
QMediaPlayer m_ringPlayer;
|
||||
QMediaPlaylist m_playlist;
|
||||
};
|
||||
51
src/call/callnegotiateevent.cpp
Normal file
51
src/call/callnegotiateevent.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "callnegotiateevent.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
CallNegotiateEvent::CallNegotiateEvent(const QString &callId,
|
||||
const QString &partyId,
|
||||
int lifetime,
|
||||
const QString &sdp,
|
||||
bool answer,
|
||||
QVector<std::pair<QString, QString>> msidToPurpose)
|
||||
: EventTemplate(callId,
|
||||
{
|
||||
{QStringLiteral("lifetime"), lifetime},
|
||||
{QStringLiteral("version"), 1},
|
||||
{QStringLiteral("description"),
|
||||
QJsonObject{{QStringLiteral("type"), answer ? QStringLiteral("answer") : QStringLiteral("offer")}, {QStringLiteral("sdp"), sdp}}},
|
||||
{QStringLiteral("party_id"), partyId},
|
||||
})
|
||||
{
|
||||
QJsonObject metadata;
|
||||
for (const auto &[stream, purpose] : msidToPurpose) {
|
||||
QJsonObject data = {{"purpose", purpose}};
|
||||
metadata[stream] = purpose;
|
||||
}
|
||||
auto content = editJson();
|
||||
content["org.matrix.msc3077.sdp_stream_metadata"] = metadata;
|
||||
editJson()["content"] = content;
|
||||
}
|
||||
|
||||
CallNegotiateEvent::CallNegotiateEvent(const QJsonObject &json)
|
||||
: EventTemplate(json)
|
||||
{
|
||||
}
|
||||
|
||||
QString CallNegotiateEvent::partyId() const
|
||||
{
|
||||
return contentJson()["party_id"].toString();
|
||||
}
|
||||
|
||||
QString CallNegotiateEvent::sdp() const
|
||||
{
|
||||
return contentJson()["description"]["sdp"].toString();
|
||||
}
|
||||
|
||||
QJsonObject CallNegotiateEvent::sdpStreamMetadata() const
|
||||
{
|
||||
return contentJson()["org.matrix.msc3077.sdp_stream_metadata"].toObject();
|
||||
}
|
||||
30
src/call/callnegotiateevent.h
Normal file
30
src/call/callnegotiateevent.h
Normal file
@@ -0,0 +1,30 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <events/callevents.h>
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
|
||||
class CallNegotiateEvent : public EventTemplate<CallNegotiateEvent, CallEvent>
|
||||
{
|
||||
public:
|
||||
QUO_EVENT(CallNegotiateEvent, "m.call.negotiate")
|
||||
|
||||
explicit CallNegotiateEvent(const QJsonObject &obj);
|
||||
|
||||
explicit CallNegotiateEvent(const QString &callId,
|
||||
const QString &partyId,
|
||||
int lifetime,
|
||||
const QString &sdp,
|
||||
bool answer,
|
||||
QVector<std::pair<QString, QString>> msidToPurpose);
|
||||
|
||||
QString partyId() const;
|
||||
QString sdp() const;
|
||||
// TODO make this a struct instead
|
||||
QJsonObject sdpStreamMetadata() const;
|
||||
};
|
||||
}
|
||||
26
src/call/callparticipant.cpp
Normal file
26
src/call/callparticipant.cpp
Normal file
@@ -0,0 +1,26 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "callparticipant.h"
|
||||
|
||||
NeoChatUser *CallParticipant::user() const
|
||||
{
|
||||
return m_user;
|
||||
}
|
||||
|
||||
bool CallParticipant::hasCamera() const
|
||||
{
|
||||
return m_hasCamera;
|
||||
}
|
||||
|
||||
CallParticipant::CallParticipant(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void CallParticipant::initCamera(QQuickItem *item)
|
||||
{
|
||||
QTimer::singleShot(500, this, [=] {
|
||||
Q_EMIT initialized(item);
|
||||
});
|
||||
}
|
||||
36
src/call/callparticipant.h
Normal file
36
src/call/callparticipant.h
Normal file
@@ -0,0 +1,36 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
|
||||
#include "neochatuser.h"
|
||||
|
||||
class QQuickItem;
|
||||
|
||||
class CallParticipant : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(NeoChatUser *user READ user CONSTANT)
|
||||
Q_PROPERTY(bool hasCamera READ hasCamera NOTIFY hasCameraChanged)
|
||||
|
||||
public:
|
||||
NeoChatUser *m_user = nullptr;
|
||||
bool m_hasCamera = false;
|
||||
|
||||
Q_INVOKABLE void initCamera(QQuickItem *item);
|
||||
|
||||
[[nodiscard]] NeoChatUser *user() const;
|
||||
|
||||
[[nodiscard]] bool hasCamera() const;
|
||||
|
||||
explicit CallParticipant(QObject *parent = nullptr);
|
||||
|
||||
Q_SIGNALS:
|
||||
void initialized(QQuickItem *item);
|
||||
void heightChanged();
|
||||
void widthChanged();
|
||||
void hasCameraChanged();
|
||||
};
|
||||
916
src/call/callsession.cpp
Normal file
916
src/call/callsession.cpp
Normal file
@@ -0,0 +1,916 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-FileCopyrightText: 2021-2022 Tobias Fella <fella@posteo.de>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "calldevices.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QThread>
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
#define GST_USE_UNSTABLE_API
|
||||
#include <gst/webrtc/webrtc.h>
|
||||
#undef GST_USE_UNSTABLE_API
|
||||
|
||||
#include "voiplogging.h"
|
||||
|
||||
#include "audiosources.h"
|
||||
#include "videosources.h"
|
||||
|
||||
#include <qcoro/qcorosignal.h>
|
||||
|
||||
#define private public
|
||||
#include "callsession.h"
|
||||
#undef private
|
||||
#include "callmanager.h"
|
||||
#include <qt_connection_util.h>
|
||||
|
||||
#define STUN_SERVER "stun://turn.matrix.org:3478" // TODO make STUN server configurable
|
||||
|
||||
#define INSTANCE \
|
||||
Q_ASSERT(user_data); \
|
||||
auto instance = static_cast<CallSession *>(user_data);
|
||||
|
||||
GstElement *createElement(const char *type, GstElement *pipe, const char *name = nullptr)
|
||||
{
|
||||
auto element = gst_element_factory_make(type, name);
|
||||
Q_ASSERT_X(element, __FUNCTION__, QStringLiteral("Failed to create element %1 %2").arg(type, name).toLatin1());
|
||||
if (pipe) {
|
||||
gst_bin_add_many(GST_BIN(pipe), element, nullptr);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
GstElement *binGetByName(GstElement *bin, const char *name)
|
||||
{
|
||||
auto element = gst_bin_get_by_name(GST_BIN(bin), name);
|
||||
Q_ASSERT_X(element, __FUNCTION__, QStringLiteral("Failed to get element by name: %1").arg(name).toLatin1());
|
||||
return element;
|
||||
}
|
||||
|
||||
struct KeyFrameRequestData {
|
||||
GstElement *pipe = nullptr;
|
||||
GstElement *decodeBin = nullptr;
|
||||
gint packetsLost = 0;
|
||||
guint timerId = 0;
|
||||
QString statsField;
|
||||
} keyFrameRequestData;
|
||||
|
||||
std::pair<int, int> getResolution(GstPad *pad)
|
||||
{
|
||||
std::pair<int, int> ret;
|
||||
auto caps = gst_pad_get_current_caps(pad);
|
||||
auto structure = gst_caps_get_structure(caps, 0);
|
||||
gst_structure_get_int(structure, "width", &ret.first);
|
||||
gst_structure_get_int(structure, "height", &ret.second);
|
||||
gst_caps_unref(caps);
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::pair<int, int> getResolution(GstElement *pipe, const gchar *elementName, const gchar *padName)
|
||||
{
|
||||
auto element = binGetByName(pipe, elementName);
|
||||
auto pad = gst_element_get_static_pad(element, padName);
|
||||
auto ret = getResolution(pad);
|
||||
gst_object_unref(pad);
|
||||
gst_object_unref(element);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void setLocalDescription(GstPromise *promise, gpointer user_data)
|
||||
{
|
||||
INSTANCE
|
||||
qCDebug(voip) << "Setting local description";
|
||||
const GstStructure *reply = gst_promise_get_reply(promise);
|
||||
gboolean isAnswer = gst_structure_id_has_field(reply, g_quark_from_string("answer"));
|
||||
GstWebRTCSessionDescription *gstsdp = nullptr;
|
||||
gst_structure_get(reply, isAnswer ? "answer" : "offer", GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &gstsdp, nullptr);
|
||||
gst_promise_unref(promise);
|
||||
auto webrtcbin = binGetByName(instance->m_pipe, "webrtcbin");
|
||||
Q_ASSERT(gstsdp);
|
||||
g_signal_emit_by_name(webrtcbin, "set-local-description", gstsdp, nullptr);
|
||||
gchar *sdp = gst_sdp_message_as_text(gstsdp->sdp);
|
||||
if (!instance->m_localSdp.isEmpty()) {
|
||||
// This is a renegotiation
|
||||
qWarning() << "emitting renegotiate";
|
||||
Q_EMIT instance->renegotiate(QString(sdp), isAnswer ? QStringLiteral("answer") : QStringLiteral("offer"));
|
||||
}
|
||||
instance->m_localSdp = QString(sdp);
|
||||
g_free(sdp);
|
||||
gst_webrtc_session_description_free(gstsdp);
|
||||
qCDebug(voip) << "Local description set:" << isAnswer;
|
||||
}
|
||||
|
||||
bool contains(std::string_view str1, std::string_view str2)
|
||||
{
|
||||
return std::search(str1.cbegin(),
|
||||
str1.cend(),
|
||||
str2.cbegin(),
|
||||
str2.cend(),
|
||||
[](unsigned char c1, unsigned char c2) {
|
||||
return std::tolower(c1) == std::tolower(c2);
|
||||
})
|
||||
!= str1.cend();
|
||||
}
|
||||
|
||||
void createOffer(GstElement *webrtc, CallSession *session)
|
||||
{
|
||||
// TODO ?!?
|
||||
if (!session->m_localSdp.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
qCWarning(voip) << "Creating Offer";
|
||||
auto promise = gst_promise_new_with_change_func(setLocalDescription, session, nullptr);
|
||||
g_signal_emit_by_name(webrtc, "create-offer", nullptr, promise);
|
||||
}
|
||||
|
||||
void createAnswer(GstPromise *promise, gpointer user_data)
|
||||
{
|
||||
INSTANCE
|
||||
qCDebug(voip) << "Creating Answer";
|
||||
gst_promise_unref(promise);
|
||||
promise = gst_promise_new_with_change_func(setLocalDescription, instance, nullptr);
|
||||
auto webrtcbin = binGetByName(instance->m_pipe, "webrtcbin");
|
||||
g_signal_emit_by_name(webrtcbin, "create-answer", nullptr, promise);
|
||||
}
|
||||
|
||||
bool getMediaAttributes(const GstSDPMessage *sdp, const char *mediaType, const char *encoding, int &payloadType, bool &receiveOnly, bool &sendOnly)
|
||||
{
|
||||
payloadType = -1;
|
||||
receiveOnly = false;
|
||||
sendOnly = false;
|
||||
for (guint mlineIndex = 0; mlineIndex < gst_sdp_message_medias_len(sdp); mlineIndex++) {
|
||||
const GstSDPMedia *media = gst_sdp_message_get_media(sdp, mlineIndex);
|
||||
if (!strcmp(gst_sdp_media_get_media(media), mediaType)) {
|
||||
receiveOnly = gst_sdp_media_get_attribute_val(media, "recvonly") != nullptr;
|
||||
sendOnly = gst_sdp_media_get_attribute_val(media, "sendonly") != nullptr;
|
||||
const gchar *rtpval = nullptr;
|
||||
for (guint n = 0; n == 0 || rtpval; n++) {
|
||||
rtpval = gst_sdp_media_get_attribute_val_n(media, "rtpmap", n);
|
||||
if (rtpval && contains(rtpval, encoding)) {
|
||||
payloadType = QString::fromLatin1(rtpval).toInt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
GstWebRTCSessionDescription *parseSDP(const QString &sdp, GstWebRTCSDPType type)
|
||||
{
|
||||
GstSDPMessage *message;
|
||||
gst_sdp_message_new(&message);
|
||||
if (gst_sdp_message_parse_buffer((guint8 *)sdp.toLatin1().data(), sdp.size(), message) == GST_SDP_OK) {
|
||||
return gst_webrtc_session_description_new(type, message);
|
||||
} else {
|
||||
qCCritical(voip) << "Failed to parse remote SDP";
|
||||
gst_sdp_message_free(message);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void addLocalICECandidate(GstElement *webrtc, guint mlineIndex, const gchar *candidate, gpointer user_data)
|
||||
{
|
||||
Q_UNUSED(webrtc);
|
||||
INSTANCE
|
||||
// qCWarning(voip) << "Adding local ICE Candidates";
|
||||
instance->m_localCandidates += Candidate{candidate, static_cast<int>(mlineIndex), QString()};
|
||||
}
|
||||
|
||||
void iceConnectionStateChanged(GstElement *webrtc, GParamSpec *pspec, gpointer user_data)
|
||||
{
|
||||
Q_UNUSED(pspec);
|
||||
INSTANCE
|
||||
GstWebRTCICEConnectionState newState;
|
||||
g_object_get(webrtc, "ice-connection-state", &newState, nullptr);
|
||||
switch (newState) {
|
||||
case GST_WEBRTC_ICE_CONNECTION_STATE_NEW:
|
||||
case GST_WEBRTC_ICE_CONNECTION_STATE_CHECKING:
|
||||
instance->setState(CallSession::CONNECTING);
|
||||
break;
|
||||
case GST_WEBRTC_ICE_CONNECTION_STATE_FAILED:
|
||||
instance->setState(CallSession::ICEFAILED);
|
||||
break;
|
||||
case GST_WEBRTC_ICE_CONNECTION_STATE_CONNECTED:
|
||||
instance->setState(CallSession::CONNECTED);
|
||||
case GST_WEBRTC_ICE_CONNECTION_STATE_COMPLETED:
|
||||
case GST_WEBRTC_ICE_CONNECTION_STATE_DISCONNECTED:
|
||||
case GST_WEBRTC_ICE_CONNECTION_STATE_CLOSED:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
GstElement *newAudioSinkChain(GstElement *pipe)
|
||||
{
|
||||
qCWarning(voip) << "New Audio Sink Chain";
|
||||
GstElement *queue = createElement("queue", pipe);
|
||||
GstElement *convert = createElement("audioconvert", pipe);
|
||||
GstElement *resample = createElement("audioresample", pipe);
|
||||
GstElement *sink = createElement("autoaudiosink", pipe);
|
||||
gst_element_link_many(queue, convert, resample, sink, nullptr);
|
||||
gst_element_sync_state_with_parent(queue);
|
||||
gst_element_sync_state_with_parent(convert);
|
||||
gst_element_sync_state_with_parent(resample);
|
||||
gst_element_sync_state_with_parent(sink);
|
||||
return queue;
|
||||
}
|
||||
|
||||
void sendKeyFrameRequest()
|
||||
{
|
||||
auto sinkpad = gst_element_get_static_pad(keyFrameRequestData.decodeBin, "sink");
|
||||
if (!gst_pad_push_event(sinkpad, gst_event_new_custom(GST_EVENT_CUSTOM_UPSTREAM, gst_structure_new_empty("GstForceKeyUnit")))) {
|
||||
qCWarning(voip) << "Keyframe request failed";
|
||||
}
|
||||
gst_object_unref(sinkpad);
|
||||
}
|
||||
|
||||
void onGetStats(GstPromise *promise, gpointer)
|
||||
{
|
||||
auto reply = gst_promise_get_reply(promise);
|
||||
GstStructure *rtpStats;
|
||||
if (!gst_structure_get(reply, keyFrameRequestData.statsField.toLatin1().data(), GST_TYPE_STRUCTURE, &rtpStats, nullptr)) {
|
||||
gst_promise_unref(promise);
|
||||
return;
|
||||
}
|
||||
auto packetsLost = 0;
|
||||
gst_structure_get_int(rtpStats, "packets-lost", &packetsLost);
|
||||
gst_structure_free(rtpStats);
|
||||
gst_promise_unref(promise);
|
||||
if (packetsLost > keyFrameRequestData.packetsLost) {
|
||||
qCWarning(voip) << "inbound video lost packet count:" << packetsLost;
|
||||
keyFrameRequestData.packetsLost = packetsLost;
|
||||
sendKeyFrameRequest();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO port to QTimer?
|
||||
gboolean testPacketLoss(gpointer)
|
||||
{
|
||||
if (!keyFrameRequestData.pipe) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto webrtc = binGetByName(keyFrameRequestData.pipe, "webrtcbin");
|
||||
auto promise = gst_promise_new_with_change_func(onGetStats, nullptr, nullptr);
|
||||
g_signal_emit_by_name(webrtc, "get-stats", nullptr, promise);
|
||||
gst_object_unref(webrtc);
|
||||
return true;
|
||||
}
|
||||
|
||||
GstElement *newVideoSinkChain(GstElement *pipe, QQuickItem *quickItem)
|
||||
{
|
||||
Q_ASSERT(pipe);
|
||||
Q_ASSERT(quickItem);
|
||||
qCWarning(voip) << "Creating Video Sink Chain";
|
||||
auto queue = createElement("queue", pipe);
|
||||
auto compositor = createElement("compositor", pipe);
|
||||
auto glupload = createElement("glupload", pipe);
|
||||
auto glcolorconvert = createElement("glcolorconvert", pipe);
|
||||
auto qmlglsink = createElement("qmlglsink", nullptr);
|
||||
auto glsinkbin = createElement("glsinkbin", pipe);
|
||||
g_object_set(qmlglsink, "widget", quickItem, nullptr);
|
||||
g_object_set(glsinkbin, "sink", qmlglsink, nullptr);
|
||||
gst_element_link_many(queue, compositor, glupload, glcolorconvert, glsinkbin, nullptr);
|
||||
gst_element_sync_state_with_parent(queue);
|
||||
gst_element_sync_state_with_parent(compositor);
|
||||
gst_element_sync_state_with_parent(glupload);
|
||||
gst_element_sync_state_with_parent(glcolorconvert);
|
||||
gst_element_sync_state_with_parent(glsinkbin);
|
||||
return queue;
|
||||
}
|
||||
|
||||
static GstPadProbeReturn pad_cb(GstPad *pad, GstPadProbeInfo *info, gpointer user_data)
|
||||
{
|
||||
Q_UNUSED(pad);
|
||||
// auto stream = static_cast<VideoStream *>(user_data);
|
||||
auto event = GST_PAD_PROBE_INFO_EVENT(info);
|
||||
if (GST_EVENT_CAPS == GST_EVENT_TYPE(event)) {
|
||||
GstCaps *caps = gst_caps_new_any();
|
||||
int width, height;
|
||||
gst_event_parse_caps(event, &caps);
|
||||
auto structure = gst_caps_get_structure(caps, 0);
|
||||
gst_structure_get_int(structure, "width", &width);
|
||||
gst_structure_get_int(structure, "height", &height);
|
||||
// stream->setWidth(width);
|
||||
// stream->setHeight(height);
|
||||
// TODO needed?
|
||||
}
|
||||
return GST_PAD_PROBE_OK;
|
||||
}
|
||||
|
||||
void linkNewPad(GstElement *decodeBin, GstPad *newpad, gpointer user_data)
|
||||
{
|
||||
INSTANCE
|
||||
qCWarning(voip) << "Linking New Pad";
|
||||
auto sinkpad = gst_element_get_static_pad(decodeBin, "sink");
|
||||
auto sinkcaps = gst_pad_get_current_caps(sinkpad);
|
||||
auto structure = gst_caps_get_structure(sinkcaps, 0);
|
||||
|
||||
gchar *mediaType = nullptr;
|
||||
guint ssrc = 0;
|
||||
gst_structure_get(structure, "media", G_TYPE_STRING, &mediaType, "ssrc", G_TYPE_UINT, &ssrc, nullptr);
|
||||
gst_caps_unref(sinkcaps);
|
||||
gst_object_unref(sinkpad);
|
||||
|
||||
GstElement *queue = nullptr;
|
||||
if (!strcmp(mediaType, "audio")) {
|
||||
qCWarning(voip) << "Receiving audio stream";
|
||||
queue = newAudioSinkChain(instance->m_pipe);
|
||||
} else if (!strcmp(mediaType, "video")) {
|
||||
qCWarning(voip) << "Receiving video stream";
|
||||
auto fake = createElement("fakesink", instance->m_pipe);
|
||||
auto selector = createElement("output-selector", instance->m_pipe);
|
||||
auto selectorSink = gst_element_get_static_pad(selector, "sink");
|
||||
auto selectorSrc1 = gst_element_request_pad_simple(selector, "src_%u");
|
||||
gst_pad_link(newpad, selectorSink);
|
||||
auto fakepad = gst_element_get_static_pad(fake, "sink");
|
||||
gst_pad_link(selectorSrc1, fakepad);
|
||||
g_object_set(selector, "active-pad", selectorSrc1, nullptr);
|
||||
|
||||
auto msid = instance->ssrcToMsid[ssrc];
|
||||
|
||||
// gst_pad_add_probe(newpad, GST_PAD_PROBE_TYPE_EVENT_BOTH, pad_cb, stream, nullptr);
|
||||
auto manager = dynamic_cast<CallManager *>(instance->parent());
|
||||
auto participants = manager->callParticipants();
|
||||
auto user = dynamic_cast<NeoChatUser *>(manager->room()->user(instance->msidToUserId[msid]));
|
||||
participants->setHasCamera(user, true);
|
||||
|
||||
auto participant = participants->callParticipantForUser(user);
|
||||
|
||||
// gst_pad_add_probe(newpad, GST_PAD_PROBE_TYPE_EVENT_BOTH, pad_cb, nullptr, nullptr);
|
||||
connectSingleShot(participant, &CallParticipant::initialized, instance, [=](QQuickItem *item) {
|
||||
gst_pad_unlink(newpad, fakepad);
|
||||
auto queue = newVideoSinkChain(instance->m_pipe, item);
|
||||
auto queuepad = gst_element_get_static_pad(queue, "sink");
|
||||
Q_ASSERT(queuepad);
|
||||
auto selectorSrc = gst_element_request_pad_simple(selector, "src_%u");
|
||||
auto ok = GST_PAD_LINK_SUCCESSFUL(gst_pad_link(selectorSrc, queuepad));
|
||||
Q_ASSERT(ok);
|
||||
g_object_set(selector, "active-pad", selectorSrc, nullptr);
|
||||
instance->setState(CallSession::CONNECTED);
|
||||
keyFrameRequestData.pipe = instance->m_pipe;
|
||||
keyFrameRequestData.decodeBin = decodeBin;
|
||||
keyFrameRequestData.timerId = g_timeout_add_seconds(3, testPacketLoss, nullptr);
|
||||
keyFrameRequestData.statsField = QStringLiteral("rtp-inbound-stream-stats_") + QString::number(ssrc);
|
||||
gst_object_unref(queuepad);
|
||||
g_free(mediaType);
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
g_free(mediaType);
|
||||
qCWarning(voip) << "Unknown pad type:" << GST_PAD_NAME(newpad);
|
||||
return;
|
||||
}
|
||||
auto queuepad = gst_element_get_static_pad(queue, "sink");
|
||||
Q_ASSERT(queuepad);
|
||||
auto ok = GST_PAD_LINK_SUCCESSFUL(gst_pad_link(newpad, queuepad));
|
||||
Q_ASSERT(ok);
|
||||
gst_object_unref(queuepad);
|
||||
g_free(mediaType);
|
||||
}
|
||||
|
||||
void setWaitForKeyFrame(GstBin *decodeBin, GstElement *element, gpointer)
|
||||
{
|
||||
Q_UNUSED(decodeBin);
|
||||
if (!strcmp(gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(gst_element_get_factory(element))), "rtpvp8depay")) {
|
||||
g_object_set(element, "wait-for-keyframe", TRUE, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void addDecodeBin(GstElement *webrtc, GstPad *newpad, gpointer user_data)
|
||||
{
|
||||
Q_UNUSED(webrtc);
|
||||
if (GST_PAD_DIRECTION(newpad) != GST_PAD_SRC) {
|
||||
return;
|
||||
}
|
||||
|
||||
INSTANCE
|
||||
|
||||
auto decodeBin = createElement("decodebin", instance->m_pipe);
|
||||
// Investigate hardware, see nheko source
|
||||
g_object_set(decodeBin, "force-sw-decoders", TRUE, nullptr);
|
||||
g_signal_connect(decodeBin, "pad-added", G_CALLBACK(linkNewPad), instance);
|
||||
g_signal_connect(decodeBin, "element-added", G_CALLBACK(setWaitForKeyFrame), nullptr);
|
||||
gst_element_sync_state_with_parent(decodeBin);
|
||||
auto sinkpad = gst_element_get_static_pad(decodeBin, "sink");
|
||||
if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, sinkpad))) {
|
||||
// TODO: Error handling
|
||||
qCWarning(voip) << "Unable to link decodebin";
|
||||
}
|
||||
gst_object_unref(sinkpad);
|
||||
}
|
||||
|
||||
void iceGatheringStateChanged(GstElement *webrtc, GParamSpec *pspec, gpointer user_data)
|
||||
{
|
||||
Q_UNUSED(pspec);
|
||||
INSTANCE
|
||||
|
||||
GstWebRTCICEGatheringState newState;
|
||||
g_object_get(webrtc, "ice-gathering-state", &newState, nullptr);
|
||||
if (newState == GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE) {
|
||||
qCWarning(voip) << "GstWebRTCICEGatheringState -> Complete";
|
||||
if (instance->m_isOffering) {
|
||||
Q_EMIT instance->offerCreated(instance->m_localSdp, instance->m_localCandidates);
|
||||
instance->setState(CallSession::OFFERSENT);
|
||||
} else {
|
||||
Q_EMIT instance->answerCreated(instance->m_localSdp, instance->m_localCandidates);
|
||||
instance->setState(CallSession::ANSWERSENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gboolean newBusMessage(GstBus *bus, GstMessage *msg, gpointer user_data)
|
||||
{
|
||||
Q_UNUSED(bus);
|
||||
INSTANCE
|
||||
|
||||
switch (GST_MESSAGE_TYPE(msg)) {
|
||||
case GST_MESSAGE_EOS:
|
||||
qCWarning(voip) << "End of stream";
|
||||
// TODO: Error handling
|
||||
instance->end();
|
||||
break;
|
||||
case GST_MESSAGE_ERROR:
|
||||
GError *error;
|
||||
gchar *debug;
|
||||
gst_message_parse_error(msg, &error, &debug);
|
||||
qCWarning(voip) << "Error from element:" << GST_OBJECT_NAME(msg->src) << error->message;
|
||||
// TODO: Error handling
|
||||
g_clear_error(&error);
|
||||
g_free(debug);
|
||||
instance->end();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
CallSession::CallSession(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void CallSession::acceptAnswer(const QString &sdp, const QVector<Candidate> &candidates, const QString &userId)
|
||||
{
|
||||
qCDebug(voip) << "Accepting Answer";
|
||||
if (m_state != CallSession::OFFERSENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
GstWebRTCSessionDescription *answer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_ANSWER);
|
||||
if (!answer) {
|
||||
end();
|
||||
return;
|
||||
}
|
||||
|
||||
acceptCandidates(candidates);
|
||||
|
||||
setRemoteDescription(answer, userId);
|
||||
}
|
||||
|
||||
void CallSession::setRemoteDescription(GstWebRTCSessionDescription *remote, const QString &userId, GstPromise *promise)
|
||||
{
|
||||
GstElement *webrtcbin = binGetByName(m_pipe, "webrtcbin");
|
||||
auto sdp = remote->sdp;
|
||||
for (guint i = 0; i < gst_sdp_message_medias_len(sdp); i++) {
|
||||
auto media = gst_sdp_message_get_media(sdp, i);
|
||||
QList<uint32_t> ssrcs;
|
||||
QString msid;
|
||||
for (guint j = 0; j < gst_sdp_media_attributes_len(media); j++) {
|
||||
auto attribute = gst_sdp_media_get_attribute(media, j);
|
||||
if (!strcmp(attribute->key, "ssrc")) {
|
||||
ssrcs += QString(attribute->value).split(" ")[0].toUInt();
|
||||
}
|
||||
if (!strcmp(attribute->key, "msid")) {
|
||||
msid = QString(attribute->value).split(" ")[0];
|
||||
}
|
||||
}
|
||||
for (const auto &ssrc : ssrcs) {
|
||||
ssrcToMsid[ssrc] = msid;
|
||||
}
|
||||
msidToUserId[msid] = userId;
|
||||
}
|
||||
g_signal_emit_by_name(webrtcbin, "set-remote-description", remote, promise);
|
||||
}
|
||||
|
||||
void CallSession::renegotiateOffer(const QString &_offer, const QString &userId, bool answer)
|
||||
{
|
||||
GstWebRTCSessionDescription *sdp = parseSDP(_offer, answer ? GST_WEBRTC_SDP_TYPE_ANSWER : GST_WEBRTC_SDP_TYPE_OFFER);
|
||||
if (!sdp) {
|
||||
Q_ASSERT(false);
|
||||
}
|
||||
GstElement *webrtcbin = binGetByName(m_pipe, "webrtcbin");
|
||||
|
||||
setRemoteDescription(sdp, userId);
|
||||
qWarning() << "answer:" << answer;
|
||||
if (!answer) {
|
||||
GstPromise *promise = gst_promise_new_with_change_func(setLocalDescription, this, nullptr);
|
||||
g_signal_emit_by_name(webrtcbin, "create-answer", nullptr, promise);
|
||||
}
|
||||
}
|
||||
|
||||
void CallSession::acceptOffer(const QString &sdp, const QVector<Candidate> remoteCandidates, const QString &userId)
|
||||
{
|
||||
Q_ASSERT(!sdp.isEmpty());
|
||||
Q_ASSERT(!remoteCandidates.isEmpty());
|
||||
qCDebug(voip) << "Accepting offer";
|
||||
if (m_state != CallSession::DISCONNECTED) {
|
||||
return;
|
||||
}
|
||||
m_isOffering = false;
|
||||
|
||||
GstWebRTCSessionDescription *offer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_OFFER);
|
||||
if (!offer) {
|
||||
qCCritical(voip) << "Not an offer";
|
||||
return;
|
||||
}
|
||||
|
||||
int opusPayloadType;
|
||||
bool receiveOnly;
|
||||
bool sendOnly;
|
||||
if (getMediaAttributes(offer->sdp, "audio", "opus", opusPayloadType, receiveOnly, sendOnly)) {
|
||||
if (opusPayloadType == -1) {
|
||||
qCCritical(voip) << "No OPUS in offer";
|
||||
gst_webrtc_session_description_free(offer);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
qCCritical(voip) << "No audio in offer";
|
||||
gst_webrtc_session_description_free(offer);
|
||||
return;
|
||||
}
|
||||
startPipeline();
|
||||
|
||||
QThread::msleep(1000); // ?
|
||||
|
||||
acceptCandidates(remoteCandidates);
|
||||
|
||||
auto promise = gst_promise_new_with_change_func(createAnswer, this, nullptr);
|
||||
setRemoteDescription(offer, userId, promise);
|
||||
gst_webrtc_session_description_free(offer);
|
||||
}
|
||||
|
||||
void CallSession::createCall()
|
||||
{
|
||||
qCDebug(voip) << "Creating call";
|
||||
m_isOffering = true;
|
||||
startPipeline();
|
||||
}
|
||||
|
||||
void CallSession::startPipeline()
|
||||
{
|
||||
qCDebug(voip) << "Starting Pipeline";
|
||||
if (m_state != CallSession::DISCONNECTED) {
|
||||
return;
|
||||
}
|
||||
m_state = CallSession::INITIATING;
|
||||
Q_EMIT stateChanged();
|
||||
|
||||
createPipeline();
|
||||
|
||||
auto webrtcbin = binGetByName(m_pipe, "webrtcbin");
|
||||
Q_ASSERT(webrtcbin);
|
||||
if (false /*TODO: CHECK USE STUN*/) {
|
||||
qCDebug(voip) << "Setting STUN server:" << STUN_SERVER;
|
||||
g_object_set(webrtcbin, "stun-server", STUN_SERVER, nullptr);
|
||||
}
|
||||
|
||||
for (const auto &uri : m_turnServers) {
|
||||
qCDebug(voip) << "Setting turn server:" << uri;
|
||||
gboolean udata;
|
||||
g_signal_emit_by_name(webrtcbin, "add-turn-server", uri.toLatin1().data(), (gpointer)(&udata));
|
||||
}
|
||||
|
||||
if (m_turnServers.empty()) {
|
||||
qCWarning(voip) << "No TURN servers provided";
|
||||
}
|
||||
|
||||
if (m_isOffering) {
|
||||
g_signal_connect(webrtcbin, "on-negotiation-needed", G_CALLBACK(::createOffer), this);
|
||||
}
|
||||
|
||||
g_signal_connect(webrtcbin, "on-ice-candidate", G_CALLBACK(addLocalICECandidate), this);
|
||||
g_signal_connect(webrtcbin, "notify::ice-connection-state", G_CALLBACK(iceConnectionStateChanged), this);
|
||||
|
||||
gst_element_set_state(m_pipe, GST_STATE_READY);
|
||||
g_signal_connect(webrtcbin, "pad-added", G_CALLBACK(addDecodeBin), this);
|
||||
|
||||
g_signal_connect(webrtcbin, "notify::ice-gathering-state", G_CALLBACK(iceGatheringStateChanged), this);
|
||||
gst_object_unref(webrtcbin);
|
||||
|
||||
GstStateChangeReturn ret = gst_element_set_state(m_pipe, GST_STATE_PLAYING);
|
||||
if (ret == GST_STATE_CHANGE_FAILURE) {
|
||||
// TODO: Error handling
|
||||
qCCritical(voip) << "Unable to start pipeline";
|
||||
end();
|
||||
return;
|
||||
}
|
||||
|
||||
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(m_pipe));
|
||||
m_busWatchId = gst_bus_add_watch(bus, newBusMessage, this);
|
||||
gst_object_unref(bus);
|
||||
|
||||
m_state = CallSession::INITIATED;
|
||||
Q_EMIT stateChanged();
|
||||
}
|
||||
|
||||
void CallSession::end()
|
||||
{
|
||||
qCDebug(voip) << "Ending Call";
|
||||
if (m_pipe) {
|
||||
gst_element_set_state(m_pipe, GST_STATE_NULL);
|
||||
gst_object_unref(m_pipe);
|
||||
m_pipe = nullptr;
|
||||
keyFrameRequestData.pipe = nullptr;
|
||||
if (m_busWatchId) {
|
||||
g_source_remove(m_busWatchId);
|
||||
m_busWatchId = 0;
|
||||
}
|
||||
}
|
||||
if (m_state != CallSession::DISCONNECTED) {
|
||||
m_state = CallSession::DISCONNECTED;
|
||||
Q_EMIT stateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void CallSession::createPipeline()
|
||||
{
|
||||
qCWarning(voip) << "Creating Pipeline";
|
||||
auto device = AudioSources::instance().currentDevice();
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
m_pipe = gst_pipeline_new(nullptr);
|
||||
auto source = gst_device_create_element(device, nullptr);
|
||||
auto volume = createElement("volume", m_pipe, "srclevel");
|
||||
auto convert = createElement("audioconvert", m_pipe);
|
||||
auto resample = createElement("audioresample", m_pipe);
|
||||
auto queue1 = createElement("queue", m_pipe);
|
||||
auto opusenc = createElement("opusenc", m_pipe);
|
||||
auto rtp = createElement("rtpopuspay", m_pipe);
|
||||
auto queue2 = createElement("queue", m_pipe);
|
||||
auto capsfilter = createElement("capsfilter", m_pipe);
|
||||
|
||||
auto rtpcaps = gst_caps_new_simple("application/x-rtp",
|
||||
"media",
|
||||
G_TYPE_STRING,
|
||||
"audio",
|
||||
"encoding-name",
|
||||
G_TYPE_STRING,
|
||||
"OPUS",
|
||||
"payload",
|
||||
G_TYPE_INT,
|
||||
OPUS_PAYLOAD_TYPE,
|
||||
nullptr);
|
||||
Q_ASSERT(rtpcaps);
|
||||
g_object_set(capsfilter, "caps", rtpcaps, nullptr);
|
||||
gst_caps_unref(rtpcaps);
|
||||
|
||||
auto webrtcbin = createElement("webrtcbin", m_pipe, "webrtcbin");
|
||||
g_object_set(webrtcbin, "bundle-policy", GST_WEBRTC_BUNDLE_POLICY_MAX_BUNDLE, nullptr);
|
||||
|
||||
gst_bin_add_many(GST_BIN(m_pipe), source, nullptr);
|
||||
|
||||
if (!gst_element_link_many(source, volume, convert, resample, queue1, opusenc, rtp, queue2, capsfilter, webrtcbin, nullptr)) {
|
||||
qCCritical(voip) << "Failed to link pipeline";
|
||||
// TODO propagate errors up and end call
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void CallSession::toggleCamera()
|
||||
{
|
||||
// TODO do this only once
|
||||
static bool inited = false;
|
||||
if (!inited) {
|
||||
addVideoPipeline();
|
||||
inited = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool CallSession::addVideoPipeline()
|
||||
{
|
||||
qCDebug(voip) << "Adding Video Pipeline";
|
||||
auto videoconvert = createElement("videoconvertscale", m_pipe);
|
||||
auto tee = createElement("tee", m_pipe);
|
||||
auto device = VideoSources::instance().currentDevice();
|
||||
auto deviceCaps = device->caps[VideoSources::instance().capsIndex()];
|
||||
int width = deviceCaps.width;
|
||||
int height = deviceCaps.height;
|
||||
int framerate = deviceCaps.framerates.back();
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
auto camera = gst_device_create_element(device->device, nullptr);
|
||||
gst_bin_add_many(GST_BIN(m_pipe), camera, nullptr);
|
||||
|
||||
auto caps =
|
||||
gst_caps_new_simple("video/x-raw", "width", G_TYPE_INT, width, "height", G_TYPE_INT, height, "framerate", GST_TYPE_FRACTION, framerate, 1, nullptr);
|
||||
auto camerafilter = createElement("capsfilter", m_pipe);
|
||||
g_object_set(camerafilter, "caps", caps, nullptr);
|
||||
gst_caps_unref(caps);
|
||||
|
||||
gst_element_link(camera, videoconvert);
|
||||
|
||||
if (!gst_element_link_many(videoconvert, camerafilter, nullptr)) {
|
||||
qCWarning(voip) << "Failed to link camera elements";
|
||||
// TODO: Error handling
|
||||
return false;
|
||||
}
|
||||
if (!gst_element_link(camerafilter, tee)) {
|
||||
qCWarning(voip) << "Failed to link camerafilter -> tee";
|
||||
// TODO: Error handling
|
||||
return false;
|
||||
}
|
||||
|
||||
auto queue = createElement("queue", m_pipe);
|
||||
g_object_set(queue, "leaky", true, nullptr);
|
||||
auto vp8enc = createElement("vp8enc", m_pipe);
|
||||
g_object_set(vp8enc, "deadline", 1, nullptr);
|
||||
g_object_set(vp8enc, "error-resilient", 1, nullptr);
|
||||
auto rtpvp8pay = createElement("rtpvp8pay", m_pipe);
|
||||
auto rtpqueue = createElement("queue", m_pipe);
|
||||
auto rtpcapsfilter = createElement("capsfilter", m_pipe);
|
||||
auto rtpcaps = gst_caps_new_simple("application/x-rtp",
|
||||
"media",
|
||||
G_TYPE_STRING,
|
||||
"video",
|
||||
"encoding-name",
|
||||
G_TYPE_STRING,
|
||||
"VP8",
|
||||
"payload",
|
||||
G_TYPE_INT,
|
||||
VP8_PAYLOAD_TYPE,
|
||||
nullptr);
|
||||
g_object_set(rtpcapsfilter, "caps", rtpcaps, nullptr);
|
||||
gst_caps_unref(rtpcaps);
|
||||
|
||||
auto webrtcbin = binGetByName(m_pipe, "webrtcbin");
|
||||
if (!gst_element_link_many(tee, queue, vp8enc, rtpvp8pay, rtpqueue, rtpcapsfilter, webrtcbin, nullptr)) {
|
||||
qCCritical(voip) << "Failed to link rtp video elements";
|
||||
gst_object_unref(webrtcbin);
|
||||
return false;
|
||||
}
|
||||
auto promise = gst_promise_new_with_change_func(setLocalDescription, this, nullptr);
|
||||
g_signal_emit_by_name(webrtcbin, "create-offer", nullptr, promise);
|
||||
|
||||
gst_object_unref(webrtcbin);
|
||||
|
||||
auto newpad = gst_element_request_pad_simple(tee, "src_%u");
|
||||
Q_ASSERT(newpad);
|
||||
|
||||
auto fake = createElement("fakesink", m_pipe);
|
||||
auto selector = createElement("output-selector", m_pipe);
|
||||
auto selectorSink = gst_element_get_static_pad(selector, "sink");
|
||||
auto selectorSrc1 = gst_element_request_pad_simple(selector, "src_%u");
|
||||
gst_pad_link(newpad, selectorSink);
|
||||
auto fakepad = gst_element_get_static_pad(fake, "sink");
|
||||
gst_pad_link(selectorSrc1, fakepad);
|
||||
g_object_set(selector, "active-pad", selectorSrc1, nullptr);
|
||||
|
||||
// gst_pad_add_probe(newpad, GST_PAD_PROBE_TYPE_EVENT_BOTH, pad_cb, stream, nullptr);
|
||||
auto manager = dynamic_cast<CallManager *>(parent());
|
||||
auto participants = manager->callParticipants();
|
||||
auto user = dynamic_cast<NeoChatUser *>(manager->room()->localUser());
|
||||
participants->setHasCamera(user, true);
|
||||
|
||||
connectSingleShot(participants->callParticipantForUser(user), &CallParticipant::initialized, this, [=](QQuickItem *item) {
|
||||
gst_pad_unlink(newpad, fakepad);
|
||||
Q_ASSERT(item);
|
||||
|
||||
auto queue = newVideoSinkChain(m_pipe, item);
|
||||
Q_ASSERT(queue);
|
||||
auto queuepad = gst_element_get_static_pad(queue, "sink");
|
||||
Q_ASSERT(queuepad);
|
||||
auto selectorSrc = gst_element_request_pad_simple(selector, "src_%u");
|
||||
Q_ASSERT(selectorSrc);
|
||||
auto ok = GST_PAD_LINK_SUCCESSFUL(gst_pad_link(selectorSrc, queuepad));
|
||||
Q_ASSERT(ok);
|
||||
g_object_set(selector, "active-pad", selectorSrc, nullptr);
|
||||
gst_object_unref(queuepad);
|
||||
gst_element_set_state(m_pipe, GST_STATE_READY); // TODO experimental
|
||||
gst_element_set_state(m_pipe, GST_STATE_PLAYING); // TODO experimental
|
||||
GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(m_pipe), GST_DEBUG_GRAPH_SHOW_ALL, "foo");
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void CallSession::setTurnServers(QStringList servers)
|
||||
{
|
||||
qCDebug(voip) << "Setting Turn Servers";
|
||||
qWarning() << "TURN SERVERS" << servers;
|
||||
m_turnServers = servers;
|
||||
}
|
||||
|
||||
void CallSession::acceptCandidates(const QVector<Candidate> &candidates)
|
||||
{
|
||||
qCDebug(voip) << "Accepting ICE Candidates";
|
||||
auto webrtcbin = binGetByName(m_pipe, "webrtcbin");
|
||||
for (const auto &c : candidates) {
|
||||
qCDebug(voip) << "Remote candidate:" << c.candidate << c.sdpMLineIndex;
|
||||
g_signal_emit_by_name(webrtcbin, "add-ice-candidate", c.sdpMLineIndex, c.candidate.toLatin1().data());
|
||||
}
|
||||
}
|
||||
|
||||
QStringList CallSession::missingPlugins()
|
||||
{
|
||||
GstRegistry *registry = gst_registry_get();
|
||||
static const QVector<QString> videoPlugins = {
|
||||
QLatin1String("compositor"),
|
||||
QLatin1String("opengl"),
|
||||
QLatin1String("qmlgl"),
|
||||
QLatin1String("rtp"),
|
||||
QLatin1String("videoconvertscale"),
|
||||
QLatin1String("vpx"),
|
||||
};
|
||||
static const QVector<QString> audioPlugins = {
|
||||
QStringLiteral("audioconvert"),
|
||||
QStringLiteral("audioresample"),
|
||||
QStringLiteral("autodetect"),
|
||||
QStringLiteral("dtls"),
|
||||
QStringLiteral("nice"),
|
||||
QStringLiteral("opus"),
|
||||
QStringLiteral("playback"),
|
||||
QStringLiteral("rtpmanager"),
|
||||
QStringLiteral("srtp"),
|
||||
QStringLiteral("volume"),
|
||||
QStringLiteral("webrtc"),
|
||||
};
|
||||
QStringList missingPlugins;
|
||||
for (const auto &pluginName : videoPlugins + audioPlugins) {
|
||||
auto plugin = gst_registry_find_plugin(registry, pluginName.toLatin1().data());
|
||||
if (!plugin) {
|
||||
missingPlugins << pluginName;
|
||||
}
|
||||
gst_object_unref(plugin);
|
||||
}
|
||||
return missingPlugins;
|
||||
}
|
||||
|
||||
void CallSession::setMuted(bool muted)
|
||||
{
|
||||
const auto srclevel = binGetByName(m_pipe, "srclevel");
|
||||
g_object_set(srclevel, "mute", muted, nullptr);
|
||||
gst_object_unref(srclevel);
|
||||
Q_EMIT mutedChanged();
|
||||
}
|
||||
|
||||
bool CallSession::muted() const
|
||||
{
|
||||
if (m_state < CallSession::CONNECTING) {
|
||||
return false;
|
||||
}
|
||||
if (!m_pipe) {
|
||||
return false;
|
||||
}
|
||||
const auto srclevel = binGetByName(m_pipe, "srclevel");
|
||||
bool muted;
|
||||
if (!srclevel) {
|
||||
return false;
|
||||
}
|
||||
g_object_get(srclevel, "mute", &muted, nullptr);
|
||||
// gst_object_unref(srclevel); //TODO why does this crash?
|
||||
return muted;
|
||||
}
|
||||
|
||||
CallSession *
|
||||
CallSession::acceptCall(const QString &sdp, const QVector<Candidate> &candidates, const QStringList &turnUris, const QString &userId, QObject *parent)
|
||||
{
|
||||
auto instance = new CallSession(parent);
|
||||
instance->setTurnServers(turnUris);
|
||||
instance->acceptOffer(sdp, candidates, userId);
|
||||
return instance;
|
||||
}
|
||||
|
||||
CallSession *CallSession::startCall(const QStringList &turnUris, QObject *parent)
|
||||
{
|
||||
auto instance = new CallSession(parent);
|
||||
|
||||
instance->setTurnServers(turnUris);
|
||||
instance->createCall();
|
||||
return instance;
|
||||
}
|
||||
|
||||
CallSession::State CallSession::state() const
|
||||
{
|
||||
return m_state;
|
||||
}
|
||||
|
||||
void CallSession::setState(CallSession::State state)
|
||||
{
|
||||
qCWarning(voip) << "Setting state" << state;
|
||||
m_state = state;
|
||||
Q_EMIT stateChanged();
|
||||
}
|
||||
|
||||
void CallSession::setMetadata(QJsonObject metadata)
|
||||
{
|
||||
m_metadata = metadata;
|
||||
}
|
||||
113
src/call/callsession.h
Normal file
113
src/call/callsession.h
Normal file
@@ -0,0 +1,113 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-FileCopyrightText: 2021-2022 Tobias Fella <fella@posteo.de>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QMetaType>
|
||||
#include <QObject>
|
||||
#include <QQuickItem>
|
||||
#include <QString>
|
||||
#include <variant>
|
||||
#define GST_USE_UNSTABLE_API
|
||||
#include <gst/webrtc/webrtc.h>
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
#define OPUS_PAYLOAD_TYPE 111
|
||||
#define VP8_PAYLOAD_TYPE 96
|
||||
|
||||
class CallDevices;
|
||||
class VideoStream;
|
||||
|
||||
struct Candidate {
|
||||
QString candidate;
|
||||
int sdpMLineIndex;
|
||||
QString sdpMid;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(Candidate)
|
||||
Q_DECLARE_METATYPE(QVector<Candidate>)
|
||||
|
||||
class CallSession : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum State {
|
||||
DISCONNECTED,
|
||||
ICEFAILED,
|
||||
INITIATING,
|
||||
INITIATED,
|
||||
OFFERSENT,
|
||||
ANSWERSENT,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
};
|
||||
Q_ENUM(State);
|
||||
|
||||
Q_PROPERTY(CallSession::State state READ state NOTIFY stateChanged)
|
||||
Q_PROPERTY(bool muted READ muted WRITE setMuted NOTIFY mutedChanged)
|
||||
|
||||
// For outgoing calls
|
||||
static CallSession *startCall(const QStringList &turnUris, QObject *parent = nullptr);
|
||||
void acceptAnswer(const QString &sdp, const QVector<Candidate> &candidates, const QString &parent);
|
||||
|
||||
// For incoming calls
|
||||
static CallSession *
|
||||
acceptCall(const QString &sdp, const QVector<Candidate> &candidates, const QStringList &turnUris, const QString &userId, QObject *parent = nullptr);
|
||||
|
||||
void end();
|
||||
|
||||
void renegotiateOffer(const QString &offer, const QString &userId, bool answer);
|
||||
void setTurnServers(QStringList servers);
|
||||
|
||||
static QStringList missingPlugins();
|
||||
|
||||
CallSession::State state() const;
|
||||
|
||||
void toggleCamera();
|
||||
bool muted() const;
|
||||
void setMuted(bool muted);
|
||||
void setMetadata(QJsonObject metadata);
|
||||
void acceptCandidates(const QVector<Candidate> &candidates);
|
||||
|
||||
QMap<QString, QString> msidToUserId;
|
||||
Q_SIGNALS:
|
||||
void stateChanged();
|
||||
void offerCreated(const QString &sdp, const QVector<Candidate> &candidates);
|
||||
|
||||
void answerCreated(const QString &sdp, const QVector<Candidate> &candidates);
|
||||
|
||||
void mutedChanged();
|
||||
void newVideoStream(VideoStream *stream);
|
||||
|
||||
void renegotiate(QString sdp, const QString &type);
|
||||
|
||||
private:
|
||||
CallSession(QObject *parent = nullptr);
|
||||
void acceptOffer(const QString &sdp, const QVector<Candidate> remoteCandidates, const QString &userId);
|
||||
void createCall();
|
||||
|
||||
void setRemoteDescription(GstWebRTCSessionDescription *remote, const QString &userId, GstPromise *promise = nullptr);
|
||||
void startPipeline();
|
||||
void createPipeline();
|
||||
bool addVideoPipeline();
|
||||
|
||||
void setState(CallSession::State state);
|
||||
GstPad *m_activePad;
|
||||
GstElement *m_inputSelector;
|
||||
CallSession::State m_state = CallSession::DISCONNECTED;
|
||||
unsigned int m_busWatchId = 0;
|
||||
QStringList m_turnServers;
|
||||
QVector<Candidate> m_localCandidates;
|
||||
QString m_localSdp;
|
||||
GstElement *m_pipe = nullptr;
|
||||
bool m_isOffering = false;
|
||||
QMap<int, QString> ssrcToMsid;
|
||||
QJsonObject m_metadata;
|
||||
GstPad *m_inactivePad;
|
||||
};
|
||||
165
src/call/devicemonitor.cpp
Normal file
165
src/call/devicemonitor.cpp
Normal file
@@ -0,0 +1,165 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "devicemonitor.h"
|
||||
#include "voiplogging.h"
|
||||
#include <QTimer>
|
||||
|
||||
QDebug operator<<(QDebug dbg, const GstStructure *props)
|
||||
{
|
||||
QDebugStateSaver saver(dbg);
|
||||
auto asStr = gst_structure_to_string(props);
|
||||
dbg << asStr;
|
||||
g_free(asStr);
|
||||
return dbg;
|
||||
}
|
||||
|
||||
static gboolean deviceCallback(GstBus *bus, GstMessage *message, gpointer user_data)
|
||||
{
|
||||
Q_UNUSED(bus);
|
||||
auto monitor = static_cast<DeviceMonitor *>(user_data);
|
||||
return monitor->callback(message);
|
||||
}
|
||||
|
||||
DeviceMonitor::DeviceMonitor()
|
||||
: QObject()
|
||||
{
|
||||
QTimer::singleShot(0, this, &DeviceMonitor::init);
|
||||
}
|
||||
|
||||
void DeviceMonitor::init()
|
||||
{
|
||||
if (m_monitor) {
|
||||
return;
|
||||
}
|
||||
m_monitor = gst_device_monitor_new();
|
||||
GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw");
|
||||
gst_device_monitor_add_filter(m_monitor, "Audio/Source", caps);
|
||||
|
||||
gst_caps_unref(caps);
|
||||
caps = gst_caps_new_empty_simple("video/x-raw");
|
||||
gst_device_monitor_add_filter(m_monitor, "Video/Source", caps);
|
||||
gst_caps_unref(caps);
|
||||
|
||||
GstBus *bus = gst_device_monitor_get_bus(m_monitor);
|
||||
gst_bus_add_watch(bus, deviceCallback, this);
|
||||
gst_object_unref(bus);
|
||||
|
||||
if (!gst_device_monitor_start(m_monitor)) {
|
||||
qWarning() << "Failed to start device monitor";
|
||||
}
|
||||
}
|
||||
|
||||
QVector<AudioSource *> DeviceMonitor::audioSources() const
|
||||
{
|
||||
return m_audioSources;
|
||||
}
|
||||
|
||||
QVector<VideoSource *> DeviceMonitor::videoSources() const
|
||||
{
|
||||
return m_videoSources;
|
||||
}
|
||||
|
||||
void DeviceMonitor::handleVideoSource(GstDevice *device)
|
||||
{
|
||||
auto source = new VideoSource();
|
||||
auto title = gst_device_get_display_name(device);
|
||||
source->title = QString(title);
|
||||
g_free(title);
|
||||
source->device = device;
|
||||
|
||||
auto caps = gst_device_get_caps(device);
|
||||
auto size = gst_caps_get_size(caps);
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
VideoCap videoCap;
|
||||
GstStructure *cap = gst_caps_get_structure(caps, i);
|
||||
const gchar *name = gst_structure_get_name(cap);
|
||||
if (strcmp(name, "video/x-raw")) {
|
||||
// TODO g_free(name);
|
||||
continue;
|
||||
}
|
||||
// TODO g_free(name);
|
||||
gst_structure_get(cap, "width", G_TYPE_INT, &videoCap.width, "height", G_TYPE_INT, &videoCap.height, nullptr);
|
||||
const auto framerate = gst_structure_get_value(cap, "framerate");
|
||||
if (GST_VALUE_HOLDS_FRACTION(framerate)) {
|
||||
auto numerator = gst_value_get_fraction_numerator(framerate);
|
||||
auto denominator = gst_value_get_fraction_denominator(framerate);
|
||||
videoCap.framerates += (float)numerator / denominator;
|
||||
}
|
||||
// unref cap?
|
||||
source->caps += videoCap;
|
||||
}
|
||||
m_videoSources += source;
|
||||
Q_EMIT videoSourceAdded();
|
||||
}
|
||||
|
||||
void DeviceMonitor::handleAudioSource(GstDevice *device)
|
||||
{
|
||||
auto source = new AudioSource();
|
||||
auto title = gst_device_get_display_name(device);
|
||||
source->title = QString(title);
|
||||
g_free(title);
|
||||
|
||||
GstStructure *props = gst_device_get_properties(device);
|
||||
gboolean isDefault = false;
|
||||
if (gst_structure_has_field(props, "is-default")) {
|
||||
gst_structure_get_boolean(props, "is-default", &isDefault);
|
||||
}
|
||||
gst_structure_free(props);
|
||||
source->isDefault = isDefault;
|
||||
|
||||
source->device = device;
|
||||
m_audioSources += source;
|
||||
Q_EMIT audioSourceAdded();
|
||||
}
|
||||
|
||||
bool DeviceMonitor::callback(GstMessage *message)
|
||||
{
|
||||
GstDevice *device;
|
||||
switch (GST_MESSAGE_TYPE(message)) {
|
||||
case GST_MESSAGE_DEVICE_ADDED: {
|
||||
gst_message_parse_device_added(message, &device);
|
||||
auto name = gst_device_get_display_name(device);
|
||||
auto props = gst_device_get_properties(device);
|
||||
qCDebug(voip) << name << props;
|
||||
gst_structure_free(props);
|
||||
if (gst_device_has_classes(device, "Video/Source")) {
|
||||
handleVideoSource(device);
|
||||
} else if (gst_device_has_classes(device, "Audio/Source")) {
|
||||
handleAudioSource(device);
|
||||
}
|
||||
g_free(name);
|
||||
gst_object_unref(device);
|
||||
break;
|
||||
}
|
||||
case GST_MESSAGE_DEVICE_REMOVED: {
|
||||
gst_message_parse_device_removed(message, &device);
|
||||
auto name = gst_device_get_display_name(device);
|
||||
auto props = gst_device_get_properties(device);
|
||||
qCDebug(voip) << name << props;
|
||||
if (gst_device_has_classes(device, "Video/Source")) {
|
||||
m_videoSources.erase(std::remove_if(m_videoSources.begin(),
|
||||
m_videoSources.end(),
|
||||
[name](auto d) {
|
||||
return d->title == QString(name);
|
||||
}),
|
||||
m_videoSources.end());
|
||||
Q_EMIT videoSourceRemoved();
|
||||
} else if (gst_device_has_classes(device, "Audio/Source")) {
|
||||
m_audioSources.erase(std::remove_if(m_audioSources.begin(),
|
||||
m_audioSources.end(),
|
||||
[name](auto d) {
|
||||
return d->title == QString(name);
|
||||
}),
|
||||
m_audioSources.end());
|
||||
Q_EMIT audioSourceRemoved();
|
||||
}
|
||||
g_free(name);
|
||||
gst_object_unref(device);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
59
src/call/devicemonitor.h
Normal file
59
src/call/devicemonitor.h
Normal file
@@ -0,0 +1,59 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QVector>
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
struct AudioSource {
|
||||
QString title;
|
||||
GstDevice *device;
|
||||
bool isDefault;
|
||||
};
|
||||
struct VideoCap {
|
||||
int width;
|
||||
int height;
|
||||
QVector<float> framerates;
|
||||
};
|
||||
|
||||
struct VideoSource {
|
||||
QString title;
|
||||
GstDevice *device;
|
||||
QVector<VideoCap> caps;
|
||||
};
|
||||
|
||||
class DeviceMonitor : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static DeviceMonitor &instance()
|
||||
{
|
||||
static DeviceMonitor _instance;
|
||||
return _instance;
|
||||
}
|
||||
|
||||
QVector<AudioSource *> audioSources() const;
|
||||
QVector<VideoSource *> videoSources() const;
|
||||
bool callback(GstMessage *message);
|
||||
void init();
|
||||
|
||||
Q_SIGNALS:
|
||||
void videoSourceAdded();
|
||||
void audioSourceAdded();
|
||||
|
||||
void videoSourceRemoved();
|
||||
void audioSourceRemoved();
|
||||
|
||||
private:
|
||||
DeviceMonitor();
|
||||
GstDeviceMonitor *m_monitor = nullptr;
|
||||
QVector<AudioSource *> m_audioSources;
|
||||
QVector<VideoSource *> m_videoSources;
|
||||
void handleVideoSource(GstDevice *device);
|
||||
void handleAudioSource(GstDevice *device);
|
||||
};
|
||||
142
src/call/videosources.cpp
Normal file
142
src/call/videosources.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "videosources.h"
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
// #include "pipelinemanager.h"
|
||||
#include <QDebug>
|
||||
#include <QString>
|
||||
|
||||
#include "devicemonitor.h"
|
||||
#include "neochatconfig.h"
|
||||
|
||||
int VideoSources::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return DeviceMonitor::instance().videoSources().size();
|
||||
}
|
||||
|
||||
QVariant VideoSources::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (index.row() >= DeviceMonitor::instance().videoSources().size()) {
|
||||
return QVariant(QStringLiteral("DEADBEEF"));
|
||||
}
|
||||
if (role == TitleRole) {
|
||||
return DeviceMonitor::instance().videoSources()[index.row()]->title;
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> VideoSources::roleNames() const
|
||||
{
|
||||
return {
|
||||
{TitleRole, "title"},
|
||||
};
|
||||
}
|
||||
|
||||
VideoSources::VideoSources()
|
||||
: QAbstractListModel()
|
||||
{
|
||||
connect(&DeviceMonitor::instance(), &DeviceMonitor::videoSourceAdded, this, [this]() {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
Q_EMIT currentIndexChanged();
|
||||
});
|
||||
connect(&DeviceMonitor::instance(), &DeviceMonitor::videoSourceRemoved, this, [this]() {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
Q_EMIT currentIndexChanged();
|
||||
});
|
||||
}
|
||||
|
||||
void VideoSources::foo(int index)
|
||||
{
|
||||
auto device = DeviceMonitor::instance().videoSources()[index]->device;
|
||||
|
||||
auto bin = gst_bin_new(nullptr);
|
||||
|
||||
GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr);
|
||||
// GstElement *videorate = gst_element_factory_make("videorate", nullptr);
|
||||
|
||||
GstElement *filter = gst_element_factory_make("capsfilter", nullptr);
|
||||
GstCaps *caps = gst_caps_new_simple("video/x-raw", "width", G_TYPE_INT, 1920, "height", G_TYPE_INT, 1080, "framerate", GST_TYPE_FRACTION, 5, 1, nullptr);
|
||||
g_object_set(filter, "caps", caps, nullptr);
|
||||
gst_caps_unref(caps);
|
||||
GstElement *deviceElement = gst_device_create_element(device, nullptr);
|
||||
|
||||
gst_bin_add_many(GST_BIN(bin), deviceElement, videoconvert, filter, nullptr);
|
||||
gst_element_link_many(deviceElement, videoconvert, filter, nullptr);
|
||||
|
||||
// GstPad *pad = gst_element_get_static_pad(filter, "src");
|
||||
GstPad *pad = gst_element_get_static_pad(filter, "src");
|
||||
auto ghostpad = gst_ghost_pad_new("src", pad);
|
||||
gst_element_add_pad(bin, ghostpad);
|
||||
gst_object_unref(pad);
|
||||
// PipelineManager::instance().add(bin);
|
||||
}
|
||||
|
||||
const VideoSource *VideoSources::currentDevice() const
|
||||
{
|
||||
const auto config = NeoChatConfig::self();
|
||||
const QString name = config->camera();
|
||||
for (const auto &videoSource : DeviceMonitor::instance().videoSources()) {
|
||||
if (videoSource->title == name) {
|
||||
qDebug() << "WebRTC: camera:" << name;
|
||||
return videoSource;
|
||||
}
|
||||
}
|
||||
if (DeviceMonitor::instance().videoSources().length() == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
return DeviceMonitor::instance().videoSources()[0];
|
||||
}
|
||||
|
||||
void VideoSources::setCurrentIndex(int index)
|
||||
{
|
||||
if (DeviceMonitor::instance().videoSources().size() == 0) {
|
||||
return;
|
||||
}
|
||||
NeoChatConfig::setCamera(DeviceMonitor::instance().videoSources()[index]->title);
|
||||
NeoChatConfig::self()->save();
|
||||
|
||||
setCapsIndex(0);
|
||||
}
|
||||
|
||||
int VideoSources::currentIndex() const
|
||||
{
|
||||
const auto config = NeoChatConfig::self();
|
||||
const QString name = config->camera();
|
||||
for (auto i = 0; i < DeviceMonitor::instance().videoSources().size(); i++) {
|
||||
if (DeviceMonitor::instance().videoSources()[i]->title == name) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
QStringList VideoSources::caps(int index) const
|
||||
{
|
||||
if (index >= DeviceMonitor::instance().videoSources().size()) {
|
||||
return QStringList();
|
||||
}
|
||||
const auto &caps = DeviceMonitor::instance().videoSources()[index]->caps;
|
||||
QStringList strings;
|
||||
for (const auto &cap : caps) {
|
||||
strings += QStringLiteral("%1x%2, %3 FPS").arg(cap.width).arg(cap.height).arg(cap.framerates.back());
|
||||
}
|
||||
return strings;
|
||||
}
|
||||
|
||||
void VideoSources::setCapsIndex(int index)
|
||||
{
|
||||
NeoChatConfig::self()->setCameraCaps(index);
|
||||
NeoChatConfig::self()->save();
|
||||
Q_EMIT capsIndexChanged();
|
||||
}
|
||||
|
||||
int VideoSources::capsIndex() const
|
||||
{
|
||||
return NeoChatConfig::self()->cameraCaps();
|
||||
}
|
||||
51
src/call/videosources.h
Normal file
51
src/call/videosources.h
Normal file
@@ -0,0 +1,51 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QAbstractListModel>
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
#include "devicemonitor.h"
|
||||
|
||||
class VideoSources : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
|
||||
Q_PROPERTY(int capsIndex READ capsIndex WRITE setCapsIndex NOTIFY capsIndexChanged)
|
||||
public:
|
||||
enum Roles {
|
||||
TitleRole = Qt::UserRole + 1,
|
||||
DeviceRole,
|
||||
};
|
||||
|
||||
static VideoSources &instance()
|
||||
{
|
||||
static VideoSources _instance;
|
||||
return _instance;
|
||||
}
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE void foo(int index);
|
||||
|
||||
const VideoSource *currentDevice() const;
|
||||
|
||||
void setCurrentIndex(int index);
|
||||
int currentIndex() const;
|
||||
|
||||
void setCapsIndex(int index);
|
||||
int capsIndex() const;
|
||||
|
||||
Q_INVOKABLE QStringList caps(int index) const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void currentIndexChanged();
|
||||
void capsIndexChanged();
|
||||
|
||||
private:
|
||||
VideoSources();
|
||||
};
|
||||
@@ -159,7 +159,7 @@ private:
|
||||
|
||||
bool m_isEdit = false;
|
||||
|
||||
QQuickTextDocument *m_document;
|
||||
QPointer<QQuickTextDocument> m_document;
|
||||
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
bool completionVisible = false;
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
#include <qt_connection_util.h>
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
#include <csapi/notifications.h>
|
||||
#include <eventstats.h>
|
||||
#endif
|
||||
|
||||
@@ -109,93 +108,26 @@ Controller::Controller(QObject *parent)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
connect(&Accounts, &AccountRegistry::accountCountChanged, this, &Controller::activeConnectionIndexChanged);
|
||||
#else
|
||||
connect(&AccountRegistry::instance(), &AccountRegistry::accountCountChanged, this, &Controller::activeConnectionIndexChanged);
|
||||
#endif
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
static int oldAccountCount = 0;
|
||||
connect(&AccountRegistry::instance(), &AccountRegistry::accountCountChanged, this, [=]() {
|
||||
if (AccountRegistry::instance().size() > oldAccountCount) {
|
||||
auto connection = AccountRegistry::instance().accounts()[AccountRegistry::instance().size() - 1];
|
||||
connect(connection, &Connection::syncDone, this, [=]() {
|
||||
handleNotifications(connection);
|
||||
connect(&Accounts, &AccountRegistry::accountCountChanged, this, [this]() {
|
||||
if (Accounts.size() > oldAccountCount) {
|
||||
auto connection = Accounts.accounts()[Accounts.size() - 1];
|
||||
connect(connection, &Connection::syncDone, this, [connection]() {
|
||||
NotificationsManager::instance().handleNotifications(connection);
|
||||
});
|
||||
}
|
||||
oldAccountCount = AccountRegistry::instance().size();
|
||||
oldAccountCount = Accounts.size();
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
void Controller::handleNotifications(QPointer<Quotient::Connection> connection)
|
||||
{
|
||||
static QStringList initial;
|
||||
static QStringList oldNotifications;
|
||||
auto job = connection->callApi<GetNotificationsJob>();
|
||||
|
||||
connect(job, &BaseJob::success, this, [job, connection]() {
|
||||
const auto notifications = job->jsonData()["notifications"].toArray();
|
||||
if (!initial.contains(connection->user()->id())) {
|
||||
initial.append(connection->user()->id());
|
||||
for (const auto &n : notifications) {
|
||||
oldNotifications += n["event"]["event_id"].toString();
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const auto &n : notifications) {
|
||||
const auto ¬ification = n.toObject();
|
||||
if (notification["read"].toBool()) {
|
||||
oldNotifications.removeOne(notification["event"]["event_id"].toString());
|
||||
continue;
|
||||
}
|
||||
if (oldNotifications.contains(notification["event"]["event_id"].toString())) {
|
||||
continue;
|
||||
}
|
||||
oldNotifications += notification["event"]["event_id"].toString();
|
||||
auto room = connection->room(notification["room_id"].toString());
|
||||
|
||||
// If room exists, room is NOT active OR the application is NOT active, show notification
|
||||
if (room
|
||||
&& !(RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id()
|
||||
&& QGuiApplication::applicationState() == Qt::ApplicationActive)) {
|
||||
// The room might have been deleted (for example rejected invitation).
|
||||
auto sender = room->user(notification["event"]["sender"].toString());
|
||||
|
||||
QString body;
|
||||
|
||||
if (notification["event"]["type"].toString() == "org.matrix.msc3381.poll.start") {
|
||||
body = notification["event"]["content"]["org.matrix.msc3381.poll.start"]["question"]["body"].toString();
|
||||
} else {
|
||||
body = notification["event"]["content"]["body"].toString();
|
||||
}
|
||||
|
||||
if (notification["event"]["type"] == "m.room.encrypted") {
|
||||
#ifdef Quotient_E2EE_ENABLED
|
||||
const auto decrypted = connection->decryptNotification(notification);
|
||||
body = decrypted["content"]["body"].toString();
|
||||
#endif
|
||||
if (body.isEmpty()) {
|
||||
body = i18n("Encrypted Message");
|
||||
}
|
||||
}
|
||||
|
||||
QImage avatar_image;
|
||||
if (!sender->avatarUrl(room).isEmpty()) {
|
||||
avatar_image = sender->avatar(128, room);
|
||||
} else {
|
||||
avatar_image = room->avatar(128);
|
||||
}
|
||||
NotificationsManager::instance().postNotification(dynamic_cast<NeoChatRoom *>(room),
|
||||
sender->displayname(room),
|
||||
body,
|
||||
avatar_image,
|
||||
notification["event"].toObject()["event_id"].toString(),
|
||||
true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
Controller &Controller::instance()
|
||||
{
|
||||
static Controller _instance;
|
||||
@@ -224,10 +156,18 @@ void Controller::logout(Connection *conn, bool serverSideLogout)
|
||||
job.start();
|
||||
loop.exec();
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
if (Accounts.count() > 1) {
|
||||
#else
|
||||
if (AccountRegistry::instance().count() > 1) {
|
||||
#endif
|
||||
// Only set the connection if the the account being logged out is currently active
|
||||
if (conn == activeConnection()) {
|
||||
#ifdef QUOTIENT_07
|
||||
setActiveConnection(Accounts.accounts()[0]);
|
||||
#else
|
||||
setActiveConnection(AccountRegistry::instance().accounts()[0]);
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
setActiveConnection(nullptr);
|
||||
@@ -242,7 +182,9 @@ void Controller::addConnection(Connection *c)
|
||||
{
|
||||
Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection");
|
||||
|
||||
#ifndef QUOTIENT_07
|
||||
#ifdef QUOTIENT_07
|
||||
Accounts.add(c);
|
||||
#else
|
||||
AccountRegistry::instance().add(c);
|
||||
#endif
|
||||
|
||||
@@ -399,15 +341,9 @@ bool Controller::saveAccessTokenToKeyChain(const AccountSettings &account, const
|
||||
void Controller::changeAvatar(Connection *conn, const QUrl &localFile)
|
||||
{
|
||||
auto job = conn->uploadFile(localFile.toLocalFile());
|
||||
#ifdef QUOTIENT_07
|
||||
if (isJobPending(job)) {
|
||||
#else
|
||||
if (isJobRunning(job)) {
|
||||
#endif
|
||||
connect(job, &BaseJob::success, this, [conn, job] {
|
||||
conn->callApi<SetAvatarUrlJob>(conn->userId(), job->contentUri());
|
||||
});
|
||||
}
|
||||
connect(job, &BaseJob::success, this, [conn, job] {
|
||||
conn->callApi<SetAvatarUrlJob>(conn->userId(), job->contentUri());
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::markAllMessagesAsRead(Connection *conn)
|
||||
@@ -491,7 +427,11 @@ NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, b
|
||||
|
||||
int Controller::accountCount() const
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
return Accounts.count();
|
||||
#else
|
||||
return AccountRegistry::instance().count();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Controller::setQuitOnLastWindowClosed()
|
||||
@@ -548,7 +488,7 @@ void Controller::setActiveConnection(Connection *connection)
|
||||
m_isOnline = true;
|
||||
Q_EMIT isOnlineChanged(true);
|
||||
});
|
||||
connect(connection, &Connection::requestFailed, this, [=](BaseJob *job) {
|
||||
connect(connection, &Connection::requestFailed, this, [](BaseJob *job) {
|
||||
if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"].toString() == "M_TOO_LARGE"_ls) {
|
||||
RoomManager::instance().warning(i18n("File too large to download."), i18n("Contact your matrix server administrator for support."));
|
||||
}
|
||||
@@ -588,16 +528,30 @@ void Controller::createRoom(const QString &name, const QString &topic)
|
||||
{
|
||||
auto createRoomJob = m_connection->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
|
||||
connect(createRoomJob, &CreateRoomJob::failure, this, [this, createRoomJob] {
|
||||
Q_EMIT errorOccured(i18n("Room creation failed: \"%1\"", createRoomJob->errorString()));
|
||||
Q_EMIT errorOccured(i18n("Room creation failed: %1", createRoomJob->errorString()));
|
||||
});
|
||||
connectSingleShot(
|
||||
this,
|
||||
&Controller::roomAdded,
|
||||
this,
|
||||
[this](NeoChatRoom *room) {
|
||||
RoomManager::instance().enterRoom(room);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
connectSingleShot(this, &Controller::roomAdded, &RoomManager::instance(), &RoomManager::enterRoom, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void Controller::createSpace(const QString &name, const QString &topic)
|
||||
{
|
||||
auto createRoomJob = m_connection->createRoom(Connection::UnpublishRoom,
|
||||
{},
|
||||
name,
|
||||
topic,
|
||||
QStringList(),
|
||||
{},
|
||||
{},
|
||||
false,
|
||||
{},
|
||||
{},
|
||||
QJsonObject{
|
||||
{"type"_ls, "m.space"_ls},
|
||||
});
|
||||
connect(createRoomJob, &CreateRoomJob::failure, this, [this, createRoomJob] {
|
||||
Q_EMIT errorOccured(i18n("Space creation failed: %1", createRoomJob->errorString()));
|
||||
});
|
||||
connectSingleShot(this, &Controller::roomAdded, &RoomManager::instance(), &RoomManager::enterRoom, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
bool Controller::isOnline() const
|
||||
@@ -782,3 +736,12 @@ QVariantList Controller::getSupportedRoomVersions(Quotient::Connection *connecti
|
||||
|
||||
return supportedRoomVersions;
|
||||
}
|
||||
|
||||
bool Controller::callsSupported() const
|
||||
{
|
||||
#ifdef GSTREAMER_AVAILABLE
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ class Controller : public QObject
|
||||
* This is the only way to gate NeoChat features in flatpaks in QML.
|
||||
*/
|
||||
Q_PROPERTY(bool isFlatpak READ isFlatpak CONSTANT)
|
||||
Q_PROPERTY(bool callsSupported READ callsSupported CONSTANT)
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -159,6 +160,11 @@ public:
|
||||
*/
|
||||
Q_INVOKABLE void createRoom(const QString &name, const QString &topic);
|
||||
|
||||
/**
|
||||
* @brief Create new space.
|
||||
*/
|
||||
Q_INVOKABLE void createSpace(const QString &name, const QString &topic);
|
||||
|
||||
/**
|
||||
* @brief Join a room.
|
||||
*/
|
||||
@@ -192,6 +198,7 @@ public:
|
||||
int quotientMinorVersion() const;
|
||||
|
||||
bool isFlatpak() const;
|
||||
bool callsSupported() const;
|
||||
|
||||
/**
|
||||
* @brief Return a string for the input timestamp.
|
||||
@@ -230,9 +237,6 @@ private:
|
||||
QMap<Quotient::Room *, int> m_notificationCounts;
|
||||
|
||||
bool hasWindowSystem() const;
|
||||
#ifdef QUOTIENT_07
|
||||
void handleNotifications(QPointer<Quotient::Connection> connection);
|
||||
#endif
|
||||
|
||||
private Q_SLOTS:
|
||||
void invokeLogin();
|
||||
|
||||
153
src/delegatesizehelper.cpp
Normal file
153
src/delegatesizehelper.cpp
Normal file
@@ -0,0 +1,153 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "delegatesizehelper.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
DelegateSizeHelper::DelegateSizeHelper(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
qreal DelegateSizeHelper::parentWidth() const
|
||||
{
|
||||
return m_parentWidth;
|
||||
}
|
||||
|
||||
void DelegateSizeHelper::setParentWidth(qreal parentWidth)
|
||||
{
|
||||
if (parentWidth == m_parentWidth) {
|
||||
return;
|
||||
}
|
||||
m_parentWidth = parentWidth;
|
||||
Q_EMIT parentWidthChanged();
|
||||
Q_EMIT currentPercentageWidthChanged();
|
||||
Q_EMIT currentWidthChanged();
|
||||
}
|
||||
|
||||
qreal DelegateSizeHelper::startBreakpoint() const
|
||||
{
|
||||
return m_startBreakpoint;
|
||||
}
|
||||
|
||||
void DelegateSizeHelper::setStartBreakpoint(qreal startBreakpoint)
|
||||
{
|
||||
if (startBreakpoint == m_startBreakpoint) {
|
||||
return;
|
||||
}
|
||||
m_startBreakpoint = startBreakpoint;
|
||||
Q_EMIT startBreakpointChanged();
|
||||
}
|
||||
|
||||
qreal DelegateSizeHelper::endBreakpoint() const
|
||||
{
|
||||
return m_endBreakpoint;
|
||||
}
|
||||
|
||||
void DelegateSizeHelper::setEndBreakpoint(qreal endBreakpoint)
|
||||
{
|
||||
if (endBreakpoint == m_endBreakpoint) {
|
||||
return;
|
||||
}
|
||||
m_endBreakpoint = endBreakpoint;
|
||||
Q_EMIT endBreakpointChanged();
|
||||
}
|
||||
|
||||
int DelegateSizeHelper::startPercentWidth() const
|
||||
{
|
||||
return m_startPercentWidth;
|
||||
}
|
||||
|
||||
void DelegateSizeHelper::setStartPercentWidth(int startPercentWidth)
|
||||
{
|
||||
if (startPercentWidth == m_startPercentWidth) {
|
||||
return;
|
||||
}
|
||||
m_startPercentWidth = startPercentWidth;
|
||||
Q_EMIT startPercentWidthChanged();
|
||||
}
|
||||
|
||||
int DelegateSizeHelper::endPercentWidth() const
|
||||
{
|
||||
return m_endPercentWidth;
|
||||
}
|
||||
|
||||
void DelegateSizeHelper::setEndPercentWidth(int endPercentWidth)
|
||||
{
|
||||
if (endPercentWidth == m_endPercentWidth) {
|
||||
return;
|
||||
}
|
||||
m_endPercentWidth = endPercentWidth;
|
||||
Q_EMIT endPercentWidthChanged();
|
||||
}
|
||||
|
||||
qreal DelegateSizeHelper::maxWidth() const
|
||||
{
|
||||
return m_maxWidth;
|
||||
}
|
||||
|
||||
void DelegateSizeHelper::setMaxWidth(qreal maxWidth)
|
||||
{
|
||||
if (maxWidth == m_maxWidth) {
|
||||
return;
|
||||
}
|
||||
m_maxWidth = maxWidth;
|
||||
Q_EMIT maxWidthChanged();
|
||||
}
|
||||
|
||||
int DelegateSizeHelper::calculateCurrentPercentageWidth() const
|
||||
{
|
||||
// Don't do anything if m_parentWidth hasn't been set yet.
|
||||
if (m_parentWidth < 0) {
|
||||
return -1;
|
||||
}
|
||||
// Don't bother with calculations for a horizontal line.
|
||||
if (m_startPercentWidth == m_endPercentWidth) {
|
||||
return m_startPercentWidth;
|
||||
}
|
||||
// Dividing by zero is a bad idea.
|
||||
if (m_startBreakpoint == m_endBreakpoint) {
|
||||
qWarning() << "DelegateSizeHelper::calculateCurrentPercentageWidth() - m_startBreakpoint is equal to m_endBreakpoint this would lead to divide by "
|
||||
"zero, aborting";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Fit to y = mx + c
|
||||
qreal m = (m_endPercentWidth - m_startPercentWidth) / (m_endBreakpoint - m_startBreakpoint);
|
||||
qreal c = m_startPercentWidth - m * m_startBreakpoint;
|
||||
|
||||
// This allows us to clamp correctly if the start or end width is bigger.
|
||||
bool endPercentBigger = m_endPercentWidth > m_startPercentWidth;
|
||||
int maxPercentWidth = endPercentBigger ? m_endPercentWidth : m_startPercentWidth;
|
||||
int minPercentWidth = endPercentBigger ? m_startPercentWidth : m_endPercentWidth;
|
||||
|
||||
int calcPercentWidth = std::ceil(m * m_parentWidth + c);
|
||||
return std::clamp(calcPercentWidth, minPercentWidth, maxPercentWidth);
|
||||
}
|
||||
|
||||
int DelegateSizeHelper::currentPercentageWidth() const
|
||||
{
|
||||
return calculateCurrentPercentageWidth();
|
||||
}
|
||||
|
||||
qreal DelegateSizeHelper::currentWidth() const
|
||||
{
|
||||
if (m_parentWidth < 0) {
|
||||
return 0.0;
|
||||
}
|
||||
int percentWidth = calculateCurrentPercentageWidth();
|
||||
// - 1 means bad input values so don't try to calculate.
|
||||
if (percentWidth == -1) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
qreal absoluteWidth = m_parentWidth * percentWidth * 0.01;
|
||||
if (m_maxWidth < 0.0) {
|
||||
return std::ceil(absoluteWidth);
|
||||
} else {
|
||||
return std::ceil(std::min(absoluteWidth, m_maxWidth));
|
||||
}
|
||||
}
|
||||
123
src/delegatesizehelper.h
Normal file
123
src/delegatesizehelper.h
Normal file
@@ -0,0 +1,123 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
/**
|
||||
* @class DelegateSizeHelper
|
||||
*
|
||||
* A class to help calculate the current width of a chat delegate/bar.
|
||||
*
|
||||
* The aim is to support a dynamic sizing based upon the width of the page. There is
|
||||
* a built in curve that allows the width to transition between two percentages based
|
||||
* upon a start and finish break point. This is to provide better convergence where
|
||||
* generally the delegate will need to fill all or most the screen when thin but
|
||||
* should max out in size and only fill a lower percentage of the screen when wide.
|
||||
*
|
||||
* @note While the main intended usage is to start with a high percentage when the parentWidth
|
||||
* is small and transition to a lower one when large, the math is setup for the
|
||||
* general case so any combination of parameters works.
|
||||
*/
|
||||
class DelegateSizeHelper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The width of the component's parent.
|
||||
*/
|
||||
Q_PROPERTY(qreal parentWidth READ parentWidth WRITE setParentWidth NOTIFY parentWidthChanged)
|
||||
|
||||
/**
|
||||
* @brief The width (in px) when the width percentage should start to transition.
|
||||
*/
|
||||
Q_PROPERTY(qreal startBreakpoint READ startBreakpoint WRITE setStartBreakpoint NOTIFY startBreakpointChanged)
|
||||
|
||||
/**
|
||||
* @brief The width (in px) when the width percentage should finish transitioning.
|
||||
*/
|
||||
Q_PROPERTY(qreal endBreakpoint READ endBreakpoint WRITE setEndBreakpoint NOTIFY endBreakpointChanged)
|
||||
|
||||
/**
|
||||
* @brief The width (in %) of the component at or before the startBreakpoint.
|
||||
*
|
||||
* @sa startBreakpoint
|
||||
*/
|
||||
Q_PROPERTY(int startPercentWidth READ startPercentWidth WRITE setStartPercentWidth NOTIFY startPercentWidthChanged)
|
||||
|
||||
/**
|
||||
* @brief The width (in %) of the component at or after the endBreakpoint.
|
||||
*
|
||||
* @sa endBreakpoint
|
||||
*/
|
||||
Q_PROPERTY(int endPercentWidth READ endPercentWidth WRITE setEndPercentWidth NOTIFY endPercentWidthChanged)
|
||||
|
||||
/**
|
||||
* @brief The absolute maximum width (in px) the component can be.
|
||||
*/
|
||||
Q_PROPERTY(qreal maxWidth READ maxWidth WRITE setMaxWidth NOTIFY maxWidthChanged)
|
||||
|
||||
/**
|
||||
* @brief The width (in %) of the component at the current parentWidth.
|
||||
*
|
||||
* Will return -1 if no parentWidth is set or startBreakpoint == endBreakpoint.
|
||||
*
|
||||
* @sa parentWidth, startBreakpoint, endBreakpoint
|
||||
*/
|
||||
Q_PROPERTY(int currentPercentageWidth READ currentPercentageWidth NOTIFY currentPercentageWidthChanged)
|
||||
|
||||
/**
|
||||
* @brief The width (in px) of the component at the current parentWidth.
|
||||
*
|
||||
* Will return 0.0 if no parentWidth is set.
|
||||
*
|
||||
* @sa parentWidth
|
||||
*/
|
||||
Q_PROPERTY(qreal currentWidth READ currentWidth NOTIFY currentWidthChanged)
|
||||
|
||||
public:
|
||||
DelegateSizeHelper(QObject *parent = nullptr);
|
||||
|
||||
qreal parentWidth() const;
|
||||
void setParentWidth(qreal parentWidth);
|
||||
|
||||
qreal startBreakpoint() const;
|
||||
void setStartBreakpoint(qreal startBreakpoint);
|
||||
|
||||
qreal endBreakpoint() const;
|
||||
void setEndBreakpoint(qreal endBreakpoint);
|
||||
|
||||
int startPercentWidth() const;
|
||||
void setStartPercentWidth(int startPercentWidth);
|
||||
|
||||
int endPercentWidth() const;
|
||||
void setEndPercentWidth(int endPercentWidth);
|
||||
|
||||
qreal maxWidth() const;
|
||||
void setMaxWidth(qreal maxWidth);
|
||||
|
||||
int currentPercentageWidth() const;
|
||||
|
||||
qreal currentWidth() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void parentWidthChanged();
|
||||
void startBreakpointChanged();
|
||||
void endBreakpointChanged();
|
||||
void startPercentWidthChanged();
|
||||
void endPercentWidthChanged();
|
||||
void maxWidthChanged();
|
||||
void currentPercentageWidthChanged();
|
||||
void currentWidthChanged();
|
||||
|
||||
private:
|
||||
qreal m_parentWidth = -1.0;
|
||||
qreal m_startBreakpoint;
|
||||
qreal m_endBreakpoint;
|
||||
int m_startPercentWidth;
|
||||
int m_endPercentWidth;
|
||||
qreal m_maxWidth = -1.0;
|
||||
|
||||
int calculateCurrentPercentageWidth() const;
|
||||
};
|
||||
@@ -51,5 +51,42 @@ ImagePackEventContent::ImagePackEventContent(const QJsonObject &json)
|
||||
|
||||
void ImagePackEventContent::fillJson(QJsonObject *o) const
|
||||
{
|
||||
// TODO
|
||||
if (pack) {
|
||||
QJsonObject packJson;
|
||||
if (pack->displayName) {
|
||||
packJson["display_name"] = *pack->displayName;
|
||||
}
|
||||
if (pack->usage) {
|
||||
QJsonArray usageJson;
|
||||
for (const auto &usage : *pack->usage) {
|
||||
usageJson += usage;
|
||||
}
|
||||
packJson["usage"] = usageJson;
|
||||
}
|
||||
if (pack->avatarUrl) {
|
||||
packJson["avatar_url"] = pack->avatarUrl->toString();
|
||||
}
|
||||
if (pack->attribution) {
|
||||
packJson["attribution"] = *pack->attribution;
|
||||
}
|
||||
(*o)["pack"_ls] = packJson;
|
||||
}
|
||||
|
||||
QJsonObject imagesJson;
|
||||
for (const auto &image : images) {
|
||||
QJsonObject imageJson;
|
||||
imageJson["url"] = image.url.toString();
|
||||
if (image.body) {
|
||||
imageJson["body"] = *image.body;
|
||||
}
|
||||
if (image.usage) {
|
||||
QJsonArray usageJson;
|
||||
for (const auto &usage : *image.usage) {
|
||||
usageJson += usage;
|
||||
}
|
||||
imageJson["usage"] = usageJson;
|
||||
}
|
||||
imagesJson[image.shortcode] = imageJson;
|
||||
}
|
||||
(*o)["images"_ls] = imagesJson;
|
||||
}
|
||||
|
||||
@@ -131,22 +131,36 @@ public:
|
||||
}
|
||||
auto filePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QDir::separator() + appName;
|
||||
|
||||
QFileInfo infoOld(filePath + QLatin1String(".old"));
|
||||
if (infoOld.exists()) {
|
||||
QFile fileOld(infoOld.absoluteFilePath());
|
||||
const bool success = fileOld.remove();
|
||||
if (!success) {
|
||||
qFatal("Cannot remove old log file '%s': %s", qUtf8Printable(fileOld.fileName()), qUtf8Printable(fileOld.errorString()));
|
||||
QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QDir::separator());
|
||||
auto entryList = dir.entryList({appName + QStringLiteral(".*")});
|
||||
std::sort(entryList.begin(), entryList.end(), [](const auto &left, const auto &right) {
|
||||
auto leftIndex = left.split(".").last().toInt();
|
||||
auto rightIndex = right.split(".").last().toInt();
|
||||
return leftIndex > rightIndex;
|
||||
});
|
||||
for (const auto &entry : entryList) {
|
||||
bool ok = false;
|
||||
const auto index = entry.split(".").last().toInt(&ok);
|
||||
if (!ok) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
QFileInfo info(filePath);
|
||||
if (info.exists()) {
|
||||
QFile file(info.absoluteFilePath());
|
||||
const QString oldName = filePath + QLatin1String(".old");
|
||||
const bool success = file.copy(oldName);
|
||||
if (!success) {
|
||||
qFatal("Cannot rename log file '%s' to '%s': %s", qUtf8Printable(file.fileName()), qUtf8Printable(oldName), qUtf8Printable(file.errorString()));
|
||||
QFileInfo info(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QDir::separator() + entry);
|
||||
if (info.exists()) {
|
||||
QFile file(info.absoluteFilePath());
|
||||
if (index > 50) {
|
||||
file.remove();
|
||||
continue;
|
||||
}
|
||||
const QString newName = filePath + QStringLiteral(".%1").arg(index + 1);
|
||||
const bool success = file.copy(newName);
|
||||
if (success) {
|
||||
file.remove();
|
||||
} else {
|
||||
qFatal("Cannot rename log file '%s' to '%s': %s",
|
||||
qUtf8Printable(file.fileName()),
|
||||
qUtf8Printable(newName),
|
||||
qUtf8Printable(file.errorString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +168,7 @@ public:
|
||||
if (!finfo.absoluteDir().exists()) {
|
||||
QDir().mkpath(finfo.absolutePath());
|
||||
}
|
||||
file.setFileName(filePath);
|
||||
file.setFileName(filePath + QStringLiteral(".0"));
|
||||
file.open(QIODevice::WriteOnly | QIODevice::Unbuffered);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,12 +38,20 @@ void Login::init()
|
||||
|
||||
connect(this, &Login::matrixIdChanged, this, [this]() {
|
||||
setHomeserverReachable(false);
|
||||
QRegularExpression validator("^\\@?[a-zA-Z0-9\\._=\\-/]+\\:[a-zA-Z0-9\\-]+(\\.[a-zA-Z0-9\\-]+)*(\\:[0-9]+)?$");
|
||||
if (!validator.match(m_matrixId).hasMatch()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_matrixId == "@") {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
m_isLoggedIn = Accounts.isLoggedIn(m_matrixId);
|
||||
#else
|
||||
m_isLoggedIn = AccountRegistry::instance().isLoggedIn(m_matrixId);
|
||||
#endif
|
||||
Q_EMIT isLoggedInChanged();
|
||||
if (m_isLoggedIn) {
|
||||
return;
|
||||
|
||||
49
src/main.cpp
49
src/main.cpp
@@ -43,20 +43,24 @@
|
||||
#include "chatdocumenthandler.h"
|
||||
#include "clipboard.h"
|
||||
#include "controller.h"
|
||||
#include "delegatesizehelper.h"
|
||||
#include "filetypesingleton.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "logger.h"
|
||||
#include "login.h"
|
||||
#include "matriximageprovider.h"
|
||||
#include "models/accountemoticonmodel.h"
|
||||
#include "models/collapsestateproxymodel.h"
|
||||
#include "models/customemojimodel.h"
|
||||
#include "models/devicesmodel.h"
|
||||
#include "models/emojimodel.h"
|
||||
#include "models/emoticonfiltermodel.h"
|
||||
#include "models/imagepacksmodel.h"
|
||||
#include "models/keywordnotificationrulemodel.h"
|
||||
#include "models/messageeventmodel.h"
|
||||
#include "models/messagefiltermodel.h"
|
||||
#include "models/publicroomlistmodel.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "models/roomlistmodel.h"
|
||||
#include "models/searchmodel.h"
|
||||
#include "models/serverlistmodel.h"
|
||||
@@ -82,6 +86,8 @@
|
||||
#ifdef QUOTIENT_07
|
||||
#include <keyverificationsession.h>
|
||||
#endif
|
||||
#include <room.h>
|
||||
|
||||
#ifdef HAVE_COLORSCHEME
|
||||
#include "colorschemer.h"
|
||||
#endif
|
||||
@@ -89,6 +95,14 @@
|
||||
#include "models/statemodel.h"
|
||||
#include "neochatuser.h"
|
||||
|
||||
#ifdef GSTREAMER_AVAILABLE
|
||||
#include "call/audiosources.h"
|
||||
#include "call/callmanager.h"
|
||||
#include "call/callparticipant.h"
|
||||
#include "call/videosources.h"
|
||||
#include "models/callparticipantsmodel.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_RUNNER
|
||||
#include "runner.h"
|
||||
#include <QDBusConnection>
|
||||
@@ -158,7 +172,7 @@ int main(int argc, char *argv[])
|
||||
QStringLiteral(NEOCHAT_VERSION_STRING),
|
||||
i18n("Matrix client"),
|
||||
KAboutLicense::GPL_V3,
|
||||
i18n("© 2018-2020 Black Hat, 2020-2022 KDE Community"));
|
||||
i18n("© 2018-2020 Black Hat, 2020-2023 KDE Community"));
|
||||
about.addAuthor(i18n("Carl Schwan"), i18n("Maintainer"), QStringLiteral("carl@carlschwan.eu"), QStringLiteral("https://carlschwan.eu"));
|
||||
about.addAuthor(i18n("Tobias Fella"), i18n("Maintainer"), QStringLiteral("tobias.fella@kde.org"), QStringLiteral("https://tobiasfella.de"));
|
||||
about.addAuthor(i18n("James Graham"), i18n("Maintainer"), QStringLiteral("james.h.graham@protonmail.com"));
|
||||
@@ -179,6 +193,22 @@ int main(int argc, char *argv[])
|
||||
#endif
|
||||
QStringLiteral("https://github.com/quotient-im/libquotient"),
|
||||
KAboutLicense::LGPL_V2_1);
|
||||
#ifdef GSTREAMER_AVAILABLE
|
||||
guint major, minor, micro, nano;
|
||||
gst_version(&major, &minor, µ, &nano);
|
||||
about.addComponent(QStringLiteral("GStreamer"),
|
||||
i18nc("Description of GStreamer", "Open Source Multimedia Framework"),
|
||||
i18nc("<version number> (built against <possibly different version number>)",
|
||||
"%1.%2.%3.%4 (built against %5.%6.%7.%8)",
|
||||
major,
|
||||
minor,
|
||||
micro,
|
||||
nano,
|
||||
GST_VERSION_MAJOR,
|
||||
GST_VERSION_MINOR,
|
||||
GST_VERSION_MICRO,
|
||||
GST_VERSION_NANO));
|
||||
#endif
|
||||
|
||||
KAboutData::setApplicationData(about);
|
||||
QGuiApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("org.kde.neochat")));
|
||||
@@ -228,6 +258,7 @@ int main(int argc, char *argv[])
|
||||
qmlRegisterType<KWebShortcutModel>("org.kde.neochat", 1, 0, "WebShortcutModel");
|
||||
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
|
||||
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
|
||||
qmlRegisterType<ReactionModel>("org.kde.neochat", 1, 0, "ReactionModel");
|
||||
qmlRegisterType<CollapseStateProxyModel>("org.kde.neochat", 1, 0, "CollapseStateProxyModel");
|
||||
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
|
||||
qmlRegisterType<UserFilterModel>("org.kde.neochat", 1, 0, "UserFilterModel");
|
||||
@@ -248,12 +279,20 @@ int main(int argc, char *argv[])
|
||||
qmlRegisterType<KeywordNotificationRuleModel>("org.kde.neochat", 1, 0, "KeywordNotificationRuleModel");
|
||||
qmlRegisterType<StickerModel>("org.kde.neochat", 1, 0, "StickerModel");
|
||||
qmlRegisterType<ImagePacksModel>("org.kde.neochat", 1, 0, "ImagePacksModel");
|
||||
qmlRegisterType<AccountEmoticonModel>("org.kde.neochat", 1, 0, "AccountEmoticonModel");
|
||||
qmlRegisterType<EmoticonFilterModel>("org.kde.neochat", 1, 0, "EmoticonFilterModel");
|
||||
qmlRegisterType<DelegateSizeHelper>("org.kde.neochat", 1, 0, "DelegateSizeHelper");
|
||||
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM");
|
||||
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM");
|
||||
qmlRegisterUncreatableType<PushNotificationAction>("org.kde.neochat", 1, 0, "PushNotificationAction", "ENUM");
|
||||
qmlRegisterUncreatableType<NeoChatRoomType>("org.kde.neochat", 1, 0, "NeoChatRoomType", "ENUM");
|
||||
qmlRegisterUncreatableType<NeoChatUser>("org.kde.neochat", 1, 0, "NeoChatUser", {});
|
||||
qmlRegisterUncreatableType<NeoChatRoom>("org.kde.neochat", 1, 0, "NeoChatRoom", {});
|
||||
|
||||
#ifdef GSTREAMER_AVAILABLE
|
||||
qmlRegisterUncreatableType<CallParticipantsModel>("org.kde.neochat", 1, 0, "CallParticipantsModel", "Get through CallManager");
|
||||
qmlRegisterUncreatableType<CallParticipant>("org.kde.neochat", 1, 0, "CallParticipant", "Get through model");
|
||||
#endif
|
||||
qRegisterMetaType<User *>("User*");
|
||||
qRegisterMetaType<User *>("const User*");
|
||||
qRegisterMetaType<User *>("const Quotient::User*");
|
||||
@@ -270,10 +309,18 @@ int main(int argc, char *argv[])
|
||||
qmlRegisterUncreatableType<KeyVerificationSession>("org.kde.neochat", 1, 0, "KeyVerificationSession", {});
|
||||
qRegisterMetaType<QVector<EmojiEntry>>("QVector<EmojiEntry>");
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef GSTREAMER_AVAILABLE
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AudioSources", &AudioSources::instance());
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "VideoSources", &VideoSources::instance());
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CallManager", &CallManager::instance());
|
||||
qmlRegisterUncreatableType<CallSession>("org.kde.neochat", 1, 0, "CallSession", "ENUM");
|
||||
#endif
|
||||
qmlRegisterSingletonType("org.kde.neochat", 1, 0, "About", [](QQmlEngine *engine, QJSEngine *) -> QJSValue {
|
||||
return engine->toScriptValue(KAboutData::applicationData());
|
||||
});
|
||||
qmlRegisterSingletonType(QUrl("qrc:/OsmLocationPlugin.qml"), "org.kde.neochat", 1, 0, "OsmLocationPlugin");
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
qRegisterMetaTypeStreamOperators<Emoji>();
|
||||
|
||||
178
src/models/accountemoticonmodel.cpp
Normal file
178
src/models/accountemoticonmodel.cpp
Normal file
@@ -0,0 +1,178 @@
|
||||
// SPDX-FileCopyrightText: 2021-2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "accountemoticonmodel.h"
|
||||
|
||||
#include <csapi/content-repo.h>
|
||||
#include <qcoro/qcorosignal.h>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
AccountEmoticonModel::AccountEmoticonModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
int AccountEmoticonModel::rowCount(const QModelIndex &index) const
|
||||
{
|
||||
Q_UNUSED(index);
|
||||
if (!m_images) {
|
||||
return 0;
|
||||
}
|
||||
return m_images->images.size();
|
||||
}
|
||||
|
||||
QVariant AccountEmoticonModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
const auto &row = index.row();
|
||||
const auto &image = m_images->images[row];
|
||||
if (role == UrlRole) {
|
||||
#ifdef QUOTIENT_07
|
||||
return m_connection->makeMediaUrl(image.url);
|
||||
#else
|
||||
return QUrl();
|
||||
#endif
|
||||
}
|
||||
if (role == BodyRole) {
|
||||
if (image.body) {
|
||||
return *image.body;
|
||||
}
|
||||
}
|
||||
if (role == ShortCodeRole) {
|
||||
return image.shortcode;
|
||||
}
|
||||
if (role == IsStickerRole) {
|
||||
if (image.usage) {
|
||||
return image.usage->isEmpty() || image.usage->contains("sticker"_ls);
|
||||
}
|
||||
if (m_images->pack && m_images->pack->usage) {
|
||||
return m_images->pack->usage->isEmpty() || m_images->pack->usage->contains("sticker"_ls);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (role == IsEmojiRole) {
|
||||
if (image.usage) {
|
||||
return image.usage->isEmpty() || image.usage->contains("emoticon"_ls);
|
||||
}
|
||||
if (m_images->pack && m_images->pack->usage) {
|
||||
return m_images->pack->usage->isEmpty() || m_images->pack->usage->contains("emoticon"_ls);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> AccountEmoticonModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{AccountEmoticonModel::UrlRole, "url"},
|
||||
{AccountEmoticonModel::BodyRole, "body"},
|
||||
{AccountEmoticonModel::ShortCodeRole, "shortcode"},
|
||||
{AccountEmoticonModel::IsStickerRole, "isSticker"},
|
||||
{AccountEmoticonModel::IsEmojiRole, "isEmoji"},
|
||||
};
|
||||
}
|
||||
|
||||
Connection *AccountEmoticonModel::connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
|
||||
void AccountEmoticonModel::setConnection(Connection *connection)
|
||||
{
|
||||
if (m_connection) {
|
||||
disconnect(m_connection, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
m_connection = connection;
|
||||
Q_EMIT connectionChanged();
|
||||
connect(m_connection, &Connection::accountDataChanged, this, [this](QString type) {
|
||||
if (type == QStringLiteral("im.ponies.user_emotes")) {
|
||||
reloadEmoticons();
|
||||
}
|
||||
});
|
||||
reloadEmoticons();
|
||||
}
|
||||
|
||||
void AccountEmoticonModel::reloadEmoticons()
|
||||
{
|
||||
QJsonObject json;
|
||||
if (m_connection->hasAccountData("im.ponies.user_emotes"_ls)) {
|
||||
json = m_connection->accountData("im.ponies.user_emotes"_ls)->contentJson();
|
||||
}
|
||||
const auto &content = ImagePackEventContent(json);
|
||||
beginResetModel();
|
||||
m_images = content;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void AccountEmoticonModel::deleteEmoticon(int index)
|
||||
{
|
||||
QJsonObject data;
|
||||
m_images->images.removeAt(index);
|
||||
m_images->fillJson(&data);
|
||||
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
|
||||
}
|
||||
|
||||
void AccountEmoticonModel::setEmoticonBody(int index, const QString &text)
|
||||
{
|
||||
m_images->images[index].body = text;
|
||||
QJsonObject data;
|
||||
m_images->fillJson(&data);
|
||||
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
|
||||
}
|
||||
|
||||
void AccountEmoticonModel::setEmoticonShortcode(int index, const QString &shortcode)
|
||||
{
|
||||
m_images->images[index].shortcode = shortcode;
|
||||
QJsonObject data;
|
||||
m_images->fillJson(&data);
|
||||
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
|
||||
}
|
||||
|
||||
void AccountEmoticonModel::setEmoticonImage(int index, const QUrl &source)
|
||||
{
|
||||
doSetEmoticonImage(index, source);
|
||||
}
|
||||
|
||||
QCoro::Task<void> AccountEmoticonModel::doSetEmoticonImage(int index, QUrl source)
|
||||
{
|
||||
auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString());
|
||||
co_await qCoro(job, &BaseJob::finished);
|
||||
if (job->error() != BaseJob::NoError) {
|
||||
co_return;
|
||||
}
|
||||
#ifdef QUOTIENT_07
|
||||
m_images->images[index].url = job->contentUri().toString();
|
||||
#else
|
||||
m_images->images[index].url = job->contentUri();
|
||||
#endif
|
||||
m_images->images[index].info = none;
|
||||
QJsonObject data;
|
||||
m_images->fillJson(&data);
|
||||
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
|
||||
}
|
||||
|
||||
QCoro::Task<void> AccountEmoticonModel::doAddEmoticon(QUrl source, QString shortcode, QString description, QString type)
|
||||
{
|
||||
auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString());
|
||||
co_await qCoro(job, &BaseJob::finished);
|
||||
if (job->error() != BaseJob::NoError) {
|
||||
co_return;
|
||||
}
|
||||
m_images->images.append(ImagePackEventContent::ImagePackImage{
|
||||
shortcode,
|
||||
job->contentUri(),
|
||||
description,
|
||||
none,
|
||||
QStringList{type},
|
||||
});
|
||||
QJsonObject data;
|
||||
m_images->fillJson(&data);
|
||||
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
|
||||
}
|
||||
|
||||
void AccountEmoticonModel::addEmoticon(const QUrl &source, const QString &shortcode, const QString &description, const QString &type)
|
||||
{
|
||||
doAddEmoticon(source, shortcode, description, type);
|
||||
}
|
||||
101
src/models/accountemoticonmodel.h
Normal file
101
src/models/accountemoticonmodel.h
Normal file
@@ -0,0 +1,101 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "events/imagepackevent.h"
|
||||
#include <QAbstractListModel>
|
||||
#include <QCoroTask>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QVector>
|
||||
#include <connection.h>
|
||||
|
||||
class ImagePacksModel;
|
||||
|
||||
/**
|
||||
* @class AccountEmoticonModel
|
||||
*
|
||||
* This class defines the model for visualising the account stickers and emojis.
|
||||
*
|
||||
* This is based upon the im.ponies.user_emotes spec (MSC2545).
|
||||
*/
|
||||
class AccountEmoticonModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
/**
|
||||
* @brief The connection to get emoticons from.
|
||||
*/
|
||||
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
UrlRole = Qt::UserRole + 1, /**< The URL for the emoticon. */
|
||||
ShortCodeRole, /**< The shortcode for the emoticon. */
|
||||
BodyRole, //**< A textual description of the emoticon */
|
||||
IsStickerRole, //**< Whether this emoticon is a sticker */
|
||||
IsEmojiRole, //**< Whether this emoticon is an emoji */
|
||||
};
|
||||
|
||||
explicit AccountEmoticonModel(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
[[nodiscard]] int rowCount(const QModelIndex &index) const override;
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
[[nodiscard]] Quotient::Connection *connection() const;
|
||||
void setConnection(Quotient::Connection *connection);
|
||||
|
||||
/**
|
||||
* @brief Deletes the emoticon at the given index.
|
||||
*/
|
||||
Q_INVOKABLE void deleteEmoticon(int index);
|
||||
|
||||
/**
|
||||
* @brief Changes the description for the emoticon at the given index.
|
||||
*/
|
||||
Q_INVOKABLE void setEmoticonBody(int index, const QString &text);
|
||||
|
||||
/**
|
||||
* @brief Changes the shortcode for the emoticon at the given index.
|
||||
*/
|
||||
Q_INVOKABLE void setEmoticonShortcode(int index, const QString &shortCode);
|
||||
|
||||
/**
|
||||
* @brief Changes the image for the emoticon at the given index.
|
||||
*/
|
||||
Q_INVOKABLE void setEmoticonImage(int index, const QUrl &source);
|
||||
|
||||
/**
|
||||
* @brief Add an emoticon with the given parameters.
|
||||
*/
|
||||
Q_INVOKABLE void addEmoticon(const QUrl &source, const QString &shortcode, const QString &description, const QString &type);
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectionChanged();
|
||||
|
||||
private:
|
||||
std::optional<Quotient::ImagePackEventContent> m_images;
|
||||
QPointer<Quotient::Connection> m_connection;
|
||||
QCoro::Task<void> doSetEmoticonImage(int index, QUrl source);
|
||||
QCoro::Task<void> doAddEmoticon(QUrl source, QString shortcode, QString description, QString type);
|
||||
|
||||
void reloadEmoticons();
|
||||
};
|
||||
58
src/models/callparticipantsmodel.cpp
Normal file
58
src/models/callparticipantsmodel.cpp
Normal file
@@ -0,0 +1,58 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "callparticipantsmodel.h"
|
||||
|
||||
QVariant CallParticipantsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (role == ObjectRole) {
|
||||
return QVariant::fromValue(m_callParticipants[index.row()]);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
int CallParticipantsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return m_callParticipants.size();
|
||||
}
|
||||
|
||||
void CallParticipantsModel::clear()
|
||||
{
|
||||
beginRemoveRows(QModelIndex(), 0, m_callParticipants.size() - 1);
|
||||
m_callParticipants.clear();
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
CallParticipant *CallParticipantsModel::callParticipantForUser(NeoChatUser *user)
|
||||
{
|
||||
for (const auto &callParticipant : m_callParticipants) {
|
||||
if (callParticipant->m_user == user) {
|
||||
return callParticipant;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> CallParticipantsModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{WidthRole, "width"},
|
||||
{HeightRole, "height"},
|
||||
{PadRole, "pad"},
|
||||
{ObjectRole, "object"},
|
||||
};
|
||||
}
|
||||
|
||||
void CallParticipantsModel::addParticipant(CallParticipant *callParticipant)
|
||||
{
|
||||
beginInsertRows(QModelIndex(), m_callParticipants.size(), m_callParticipants.size());
|
||||
m_callParticipants += callParticipant;
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void CallParticipantsModel::setHasCamera(NeoChatUser *user, bool hasCamera)
|
||||
{
|
||||
callParticipantForUser(user)->m_hasCamera = hasCamera;
|
||||
Q_EMIT callParticipantForUser(user)->hasCameraChanged();
|
||||
}
|
||||
38
src/models/callparticipantsmodel.h
Normal file
38
src/models/callparticipantsmodel.h
Normal file
@@ -0,0 +1,38 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QVector>
|
||||
|
||||
#include "call/callparticipant.h"
|
||||
#include "neochatuser.h"
|
||||
|
||||
class CallParticipantsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
WidthRole,
|
||||
HeightRole,
|
||||
PadRole,
|
||||
ObjectRole,
|
||||
};
|
||||
Q_ENUM(Roles);
|
||||
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
;
|
||||
|
||||
void addParticipant(CallParticipant *callParticipant);
|
||||
CallParticipant *callParticipantForUser(NeoChatUser *user);
|
||||
|
||||
void setHasCamera(NeoChatUser *user, bool hasCamera);
|
||||
void clear();
|
||||
|
||||
private:
|
||||
QVector<CallParticipant *> m_callParticipants;
|
||||
};
|
||||
@@ -11,8 +11,9 @@ bool CollapseStateProxyModel::filterAcceptsRow(int source_row, const QModelIndex
|
||||
Q_UNUSED(source_parent);
|
||||
return sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If this is not a state, show it
|
||||
|| sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If this is the first state in a block, show it. TODO hidden events?
|
||||
|| (source_row < sourceModel()->rowCount() - 1
|
||||
&& sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State) // If this is the first state in a block, show it. TODO hidden events?
|
||||
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::ShowSectionRole).toBool(); // If it's a new day, show it
|
||||
}
|
||||
|
||||
@@ -50,10 +51,11 @@ QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
|
||||
if (!uniqueAuthors.contains(nextAuthor)) {
|
||||
uniqueAuthors.append(nextAuthor);
|
||||
}
|
||||
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
) {
|
||||
if (i > 0
|
||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -108,10 +110,11 @@ QVariantList CollapseStateProxyModel::stateEventsList(int sourceRow) const
|
||||
{"text", sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()},
|
||||
};
|
||||
stateEvents.append(nextState);
|
||||
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
) {
|
||||
if (i > 0
|
||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -126,10 +129,11 @@ QVariantList CollapseStateProxyModel::authorList(int sourceRow) const
|
||||
if (!uniqueAuthors.contains(nextAvatar)) {
|
||||
uniqueAuthors.append(nextAvatar);
|
||||
}
|
||||
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
) {
|
||||
if (i > 0
|
||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -148,10 +152,11 @@ QString CollapseStateProxyModel::excessAuthors(int row) const
|
||||
if (!uniqueAuthors.contains(nextAvatar)) {
|
||||
uniqueAuthors.append(nextAvatar);
|
||||
}
|
||||
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
) {
|
||||
if (i > 0
|
||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ QVariant CompletionModel::data(const QModelIndex &index, int role) const
|
||||
auto filterIndex = m_filterModel->index(index.row(), 0);
|
||||
if (m_autoCompletionType == User) {
|
||||
if (role == Text) {
|
||||
return m_filterModel->data(filterIndex, UserListModel::NameRole);
|
||||
return m_filterModel->data(filterIndex, UserListModel::DisplayNameRole);
|
||||
}
|
||||
if (role == Subtitle) {
|
||||
return m_filterModel->data(filterIndex, UserListModel::UserIdRole);
|
||||
@@ -123,7 +123,7 @@ void CompletionModel::updateCompletion()
|
||||
if (text().startsWith(QLatin1Char('@'))) {
|
||||
m_filterModel->setSourceModel(m_userListModel);
|
||||
m_filterModel->setFilterRole(UserListModel::UserIdRole);
|
||||
m_filterModel->setSecondaryFilterRole(UserListModel::NameRole);
|
||||
m_filterModel->setSecondaryFilterRole(UserListModel::DisplayNameRole);
|
||||
m_filterModel->setFullText(m_fullText);
|
||||
m_filterModel->setFilterText(m_text);
|
||||
m_autoCompletionType = User;
|
||||
|
||||
@@ -16,12 +16,6 @@
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
#define running isJobPending
|
||||
#else
|
||||
#define running isJobRunning
|
||||
#endif
|
||||
|
||||
void CustomEmojiModel::fetchEmojis()
|
||||
{
|
||||
if (!Controller::instance().activeConnection()) {
|
||||
@@ -62,36 +56,35 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
|
||||
|
||||
auto job = Controller::instance().activeConnection()->uploadFile(location.toLocalFile());
|
||||
|
||||
if (running(job)) {
|
||||
connect(job, &BaseJob::success, this, [name, location, job] {
|
||||
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes");
|
||||
auto json = data != nullptr ? data->contentJson() : QJsonObject();
|
||||
auto emojiData = json["images"].toObject();
|
||||
connect(job, &BaseJob::success, this, [name, location, job] {
|
||||
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes");
|
||||
auto json = data != nullptr ? data->contentJson() : QJsonObject();
|
||||
auto emojiData = json["images"].toObject();
|
||||
|
||||
QString url;
|
||||
QString url;
|
||||
#ifdef QUOTIENT_07
|
||||
url = job->contentUri().toString();
|
||||
url = job->contentUri().toString();
|
||||
#else
|
||||
url = job->contentUri();
|
||||
url = job->contentUri();
|
||||
#endif
|
||||
|
||||
QImage image(location.toLocalFile());
|
||||
QJsonObject imageInfo;
|
||||
imageInfo["w"] = image.width();
|
||||
imageInfo["h"] = image.height();
|
||||
imageInfo["mimetype"] = QMimeDatabase().mimeTypeForFile(location.toLocalFile()).name();
|
||||
imageInfo["size"] = image.sizeInBytes();
|
||||
QImage image(location.toLocalFile());
|
||||
QJsonObject imageInfo;
|
||||
imageInfo["w"] = image.width();
|
||||
imageInfo["h"] = image.height();
|
||||
imageInfo["mimetype"] = QMimeDatabase().mimeTypeForFile(location.toLocalFile()).name();
|
||||
imageInfo["size"] = image.sizeInBytes();
|
||||
|
||||
emojiData[QStringLiteral("%1").arg(name)] = QJsonObject({
|
||||
{QStringLiteral("url"), url},
|
||||
{QStringLiteral("info"), imageInfo},
|
||||
{QStringLiteral("body"), location.fileName()},
|
||||
});
|
||||
|
||||
json["images"] = emojiData;
|
||||
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes", json);
|
||||
emojiData[QStringLiteral("%1").arg(name)] = QJsonObject({
|
||||
{QStringLiteral("url"), url},
|
||||
{QStringLiteral("info"), imageInfo},
|
||||
{QStringLiteral("body"), location.fileName()},
|
||||
{"usage"_ls, "emoticon"_ls},
|
||||
});
|
||||
}
|
||||
|
||||
json["images"] = emojiData;
|
||||
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes", json);
|
||||
});
|
||||
}
|
||||
|
||||
void CustomEmojiModel::removeEmoji(const QString &name)
|
||||
|
||||
55
src/models/emoticonfiltermodel.cpp
Normal file
55
src/models/emoticonfiltermodel.cpp
Normal file
@@ -0,0 +1,55 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "emoticonfiltermodel.h"
|
||||
|
||||
#include "accountemoticonmodel.h"
|
||||
#include "stickermodel.h"
|
||||
|
||||
EmoticonFilterModel::EmoticonFilterModel(QObject *parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
connect(this, &EmoticonFilterModel::sourceModelChanged, this, [this]() {
|
||||
if (dynamic_cast<StickerModel *>(sourceModel())) {
|
||||
m_stickerRole = StickerModel::IsStickerRole;
|
||||
m_emojiRole = StickerModel::IsEmojiRole;
|
||||
} else {
|
||||
m_stickerRole = AccountEmoticonModel::IsStickerRole;
|
||||
m_emojiRole = AccountEmoticonModel::IsEmojiRole;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool EmoticonFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||
{
|
||||
Q_UNUSED(sourceParent);
|
||||
auto stickerUsage = sourceModel()->data(sourceModel()->index(sourceRow, 0), m_stickerRole).toBool();
|
||||
auto emojiUsage = sourceModel()->data(sourceModel()->index(sourceRow, 0), m_emojiRole).toBool();
|
||||
return (stickerUsage && m_showStickers) || (emojiUsage && m_showEmojis);
|
||||
}
|
||||
|
||||
bool EmoticonFilterModel::showStickers() const
|
||||
{
|
||||
return m_showStickers;
|
||||
}
|
||||
|
||||
void EmoticonFilterModel::setShowStickers(bool showStickers)
|
||||
{
|
||||
beginResetModel();
|
||||
m_showStickers = showStickers;
|
||||
endResetModel();
|
||||
Q_EMIT showStickersChanged();
|
||||
}
|
||||
|
||||
bool EmoticonFilterModel::showEmojis() const
|
||||
{
|
||||
return m_showEmojis;
|
||||
}
|
||||
|
||||
void EmoticonFilterModel::setShowEmojis(bool showEmojis)
|
||||
{
|
||||
beginResetModel();
|
||||
m_showEmojis = showEmojis;
|
||||
endResetModel();
|
||||
Q_EMIT showEmojisChanged();
|
||||
}
|
||||
51
src/models/emoticonfiltermodel.h
Normal file
51
src/models/emoticonfiltermodel.h
Normal file
@@ -0,0 +1,51 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
/**
|
||||
* @class EmoticonFilterModel
|
||||
*
|
||||
* This class creates a custom QSortFilterProxyModel for filtering a emoticon by type
|
||||
* (Sticker or Emoji).
|
||||
*/
|
||||
class EmoticonFilterModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief Whether stickers should be shown
|
||||
*/
|
||||
Q_PROPERTY(bool showStickers READ showStickers WRITE setShowStickers NOTIFY showStickersChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether emojis show be shown
|
||||
*/
|
||||
Q_PROPERTY(bool showEmojis READ showEmojis WRITE setShowEmojis NOTIFY showEmojisChanged)
|
||||
|
||||
public:
|
||||
explicit EmoticonFilterModel(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Custom filter function checking the type of emoticon
|
||||
*
|
||||
* @note The filter cannot be modified and will always use the same filter properties.
|
||||
*/
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
||||
|
||||
[[nodiscard]] bool showStickers() const;
|
||||
void setShowStickers(bool showStickers);
|
||||
|
||||
[[nodiscard]] bool showEmojis() const;
|
||||
void setShowEmojis(bool showEmojis);
|
||||
|
||||
Q_SIGNALS:
|
||||
void showStickersChanged();
|
||||
void showEmojisChanged();
|
||||
|
||||
private:
|
||||
bool m_showStickers = false;
|
||||
bool m_showEmojis = false;
|
||||
int m_stickerRole = 0;
|
||||
int m_emojiRole = 0;
|
||||
};
|
||||
@@ -15,12 +15,17 @@ ImagePacksModel::ImagePacksModel(QObject *parent)
|
||||
|
||||
int ImagePacksModel::rowCount(const QModelIndex &index) const
|
||||
{
|
||||
Q_UNUSED(index);
|
||||
return m_events.count();
|
||||
}
|
||||
|
||||
QVariant ImagePacksModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
const auto &event = m_events[index.row()];
|
||||
const auto row = index.row();
|
||||
if (row < 0 || row >= m_events.size()) {
|
||||
return {};
|
||||
}
|
||||
const auto &event = m_events[row];
|
||||
if (role == DisplayNameRole) {
|
||||
if (event.pack->displayName) {
|
||||
return *event.pack->displayName;
|
||||
@@ -59,24 +64,38 @@ void ImagePacksModel::setRoom(NeoChatRoom *room)
|
||||
{
|
||||
if (m_room) {
|
||||
disconnect(m_room, nullptr, this, nullptr);
|
||||
disconnect(m_room->connection(), nullptr, this, nullptr);
|
||||
}
|
||||
m_room = room;
|
||||
|
||||
connect(m_room->connection(), &Connection::accountDataChanged, this, [this](const QString &type) {
|
||||
if (type == "im.ponies.user_emotes"_ls) {
|
||||
reloadImages();
|
||||
}
|
||||
});
|
||||
// TODO listen to packs changing
|
||||
reloadImages();
|
||||
Q_EMIT roomChanged();
|
||||
}
|
||||
|
||||
void ImagePacksModel::reloadImages()
|
||||
{
|
||||
beginResetModel();
|
||||
m_events.clear();
|
||||
|
||||
// TODO listen to account data changing
|
||||
// TODO listen to packs changing
|
||||
// Load emoticons from the account data
|
||||
if (m_room->connection()->hasAccountData("im.ponies.user_emotes"_ls)) {
|
||||
auto json = m_room->connection()->accountData("im.ponies.user_emotes"_ls)->contentJson();
|
||||
json["pack"] = QJsonObject{
|
||||
{"display_name", i18n("Own Stickers")},
|
||||
{"display_name", m_showStickers ? i18nc("As in 'The user's own Stickers'", "Own Stickers") : i18nc("As in 'The user's own emojis", "Own Emojis")},
|
||||
};
|
||||
const auto &content = ImagePackEventContent(json);
|
||||
if (!content.images.isEmpty()) {
|
||||
m_events += ImagePackEventContent(json);
|
||||
}
|
||||
}
|
||||
|
||||
// Load emoticons from the saved rooms
|
||||
const auto &accountData = m_room->connection()->accountData("im.ponies.emote_rooms"_ls);
|
||||
if (accountData) {
|
||||
const auto &rooms = accountData->contentJson()["rooms"_ls].toObject();
|
||||
@@ -88,11 +107,11 @@ void ImagePacksModel::setRoom(NeoChatRoom *room)
|
||||
const auto &stickerRoom = m_room->connection()->room(roomId);
|
||||
for (const auto &packKey : packs.keys()) {
|
||||
#ifdef QUOTIENT_07
|
||||
const auto &pack = stickerRoom->currentState().get<ImagePackEvent>(packKey);
|
||||
if (pack) {
|
||||
if (const auto &pack = stickerRoom->currentState().get<ImagePackEvent>(packKey)) {
|
||||
const auto packContent = pack->content();
|
||||
if (!packContent.pack->usage || (packContent.pack->usage->contains("emoticon") && showEmoticons())
|
||||
|| (packContent.pack->usage->contains("sticker") && showStickers())) {
|
||||
if ((!packContent.pack || !packContent.pack->usage || (packContent.pack->usage->contains("emoticon") && showEmoticons())
|
||||
|| (packContent.pack->usage->contains("sticker") && showStickers()))
|
||||
&& !packContent.images.isEmpty()) {
|
||||
m_events += packContent;
|
||||
}
|
||||
}
|
||||
@@ -101,17 +120,21 @@ void ImagePacksModel::setRoom(NeoChatRoom *room)
|
||||
}
|
||||
}
|
||||
#ifdef QUOTIENT_07
|
||||
|
||||
// Load emoticons from the current room
|
||||
auto events = m_room->currentState().eventsOfType("im.ponies.room_emotes");
|
||||
for (const auto &event : events) {
|
||||
auto packContent = eventCast<const ImagePackEvent>(event)->content();
|
||||
if (!packContent.pack->usage || (packContent.pack->usage->contains("emoticon") && showEmoticons())
|
||||
|| (packContent.pack->usage->contains("sticker") && showStickers())) {
|
||||
m_events += packContent;
|
||||
if (packContent.pack.has_value()) {
|
||||
if (!packContent.pack->usage || (packContent.pack->usage->contains("emoticon") && showEmoticons())
|
||||
|| (packContent.pack->usage->contains("sticker") && showStickers())) {
|
||||
m_events += packContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Q_EMIT imagesLoaded();
|
||||
endResetModel();
|
||||
Q_EMIT roomChanged();
|
||||
}
|
||||
|
||||
bool ImagePacksModel::showStickers() const
|
||||
|
||||
@@ -90,10 +90,12 @@ Q_SIGNALS:
|
||||
void roomChanged();
|
||||
void showStickersChanged();
|
||||
void showEmoticonsChanged();
|
||||
void imagesLoaded();
|
||||
|
||||
private:
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
QVector<Quotient::ImagePackEventContent> m_events;
|
||||
bool m_showStickers = true;
|
||||
bool m_showEmoticons = true;
|
||||
void reloadImages();
|
||||
};
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
|
||||
#include "neochatconfig.h"
|
||||
#include <connection.h>
|
||||
#include <csapi/rooms.h>
|
||||
#include <events/reactionevent.h>
|
||||
#include <events/redactionevent.h>
|
||||
#include <events/roomavatarevent.h>
|
||||
#include <events/roommemberevent.h>
|
||||
#include <events/simplestateevents.h>
|
||||
#include <qt_connection_util.h>
|
||||
#include <user.h>
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
@@ -21,12 +21,12 @@
|
||||
|
||||
#include <QDebug>
|
||||
#include <QGuiApplication>
|
||||
#include <QQmlEngine> // for qmlRegisterType()
|
||||
#include <QTimeZone>
|
||||
|
||||
#include <KFormat>
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "models/reactionmodel.h"
|
||||
#include "neochatuser.h"
|
||||
#include "texthandler.h"
|
||||
|
||||
@@ -44,8 +44,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[ContentRole] = "content";
|
||||
roles[HighlightRole] = "isHighlighted";
|
||||
roles[SpecialMarksRole] = "marks";
|
||||
roles[LongOperationRole] = "progressInfo";
|
||||
roles[EventResolvedTypeRole] = "eventResolvedType";
|
||||
roles[ProgressInfoRole] = "progressInfo";
|
||||
roles[ShowLinkPreviewRole] = "showLinkPreview";
|
||||
roles[LinkPreviewRole] = "linkPreview";
|
||||
roles[MediaInfoRole] = "mediaInfo";
|
||||
@@ -61,9 +60,9 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[ReadMarkersStringRole] = "readMarkersString";
|
||||
roles[ShowReadMarkersRole] = "showReadMarkers";
|
||||
roles[ReactionRole] = "reaction";
|
||||
roles[ShowReactionsRole] = "showReactions";
|
||||
roles[SourceRole] = "source";
|
||||
roles[MimeTypeRole] = "mimeType";
|
||||
roles[FormattedBodyRole] = "formattedBody";
|
||||
roles[AuthorIdRole] = "authorId";
|
||||
roles[VerifiedRole] = "verified";
|
||||
roles[DisplayNameForInitialsRole] = "displayNameForInitials";
|
||||
@@ -80,17 +79,11 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
MessageEventModel::MessageEventModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
using namespace Quotient;
|
||||
qmlRegisterAnonymousType<FileTransferInfo>("org.kde.neochat", 1);
|
||||
qRegisterMetaType<FileTransferInfo>();
|
||||
|
||||
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, [this] {
|
||||
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReplyRole});
|
||||
});
|
||||
}
|
||||
|
||||
MessageEventModel::~MessageEventModel() = default;
|
||||
|
||||
NeoChatRoom *MessageEventModel::room() const
|
||||
{
|
||||
return m_currentRoom;
|
||||
@@ -106,6 +99,8 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
if (m_currentRoom) {
|
||||
m_currentRoom->disconnect(this);
|
||||
m_linkPreviewers.clear();
|
||||
qDeleteAll(m_reactionModels);
|
||||
m_reactionModels.clear();
|
||||
}
|
||||
|
||||
m_currentRoom = room;
|
||||
@@ -116,6 +111,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) {
|
||||
if (auto e = &*event->viewAs<RoomMessageEvent>()) {
|
||||
createLinkPreviewerForEvent(e);
|
||||
createReactionModelForEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,13 +123,21 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
#else
|
||||
lastReadEventId = room->readMarkerEventId();
|
||||
#endif
|
||||
connect(m_currentRoom, &NeoChatRoom::replyLoaded, this, [this](const auto &eventId, const auto &replyId) {
|
||||
Q_UNUSED(replyId);
|
||||
auto row = eventIdToRow(eventId);
|
||||
if (row == -1) {
|
||||
return;
|
||||
}
|
||||
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyRole, ReplyMediaInfoRole, ReplyAuthor});
|
||||
});
|
||||
|
||||
using namespace Quotient;
|
||||
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
|
||||
for (auto &&event : events) {
|
||||
const RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
|
||||
if (message != nullptr) {
|
||||
createLinkPreviewerForEvent(message);
|
||||
createReactionModelForEvent(message);
|
||||
|
||||
if (NeoChatConfig::self()->showFancyEffects()) {
|
||||
QString planBody = message->plainBody();
|
||||
@@ -173,6 +177,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
|
||||
if (message) {
|
||||
createLinkPreviewerForEvent(message);
|
||||
createReactionModelForEvent(message);
|
||||
}
|
||||
}
|
||||
if (rowCount() > 0) {
|
||||
@@ -244,7 +249,11 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
if (eventId.isEmpty()) { // How did we get here?
|
||||
return;
|
||||
}
|
||||
refreshEventRoles(eventId, {ReactionRole, Qt::DisplayRole});
|
||||
const auto eventIt = m_currentRoom->findInTimeline(eventId);
|
||||
if (eventIt != m_currentRoom->historyEdge()) {
|
||||
createReactionModelForEvent(static_cast<const RoomMessageEvent *>(&**eventIt));
|
||||
}
|
||||
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole, Qt::DisplayRole});
|
||||
});
|
||||
connect(m_currentRoom, &Room::changed, this, [this]() {
|
||||
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
|
||||
@@ -382,25 +391,6 @@ QDateTime MessageEventModel::makeMessageTimestamp(const Quotient::Room::rev_iter
|
||||
return {};
|
||||
}
|
||||
|
||||
QString MessageEventModel::renderDate(const QDateTime ×tamp)
|
||||
{
|
||||
auto date = timestamp.toLocalTime().date();
|
||||
if (date == QDate::currentDate()) {
|
||||
return i18n("Today");
|
||||
}
|
||||
if (date == QDate::currentDate().addDays(-1)) {
|
||||
return i18n("Yesterday");
|
||||
}
|
||||
if (date == QDate::currentDate().addDays(-2)) {
|
||||
return i18n("The day before yesterday");
|
||||
}
|
||||
if (date > QDate::currentDate().addDays(-7)) {
|
||||
return date.toString("dddd");
|
||||
}
|
||||
|
||||
return QLocale::system().toString(date, QLocale::ShortFormat);
|
||||
}
|
||||
|
||||
void MessageEventModel::refreshLastUserEvents(int baseTimelineRow)
|
||||
{
|
||||
if (!m_currentRoom || m_currentRoom->timelineSize() <= baseTimelineRow) {
|
||||
@@ -448,37 +438,13 @@ void MessageEventModel::fetchMore(const QModelIndex &parent)
|
||||
}
|
||||
}
|
||||
|
||||
static const QVariantMap emptyUser = {
|
||||
{"isLocalUser", false},
|
||||
{"id", QString()},
|
||||
{"avatarSource", QUrl()},
|
||||
{"avatarMediaId", QString()},
|
||||
{"avatarUrl", QString()},
|
||||
{"displayName", QString()},
|
||||
{"display", QString()},
|
||||
{"color", QColor()},
|
||||
{"object", QVariant()},
|
||||
};
|
||||
|
||||
inline QVariantMap userInContext(NeoChatUser *user, NeoChatRoom *room)
|
||||
{
|
||||
return QVariantMap{
|
||||
{"isLocalUser", user->id() == room->localUser()->id()},
|
||||
{"id", user->id()},
|
||||
{"avatarSource", room->avatarForMember(user)},
|
||||
{"avatarMediaId", user->avatarMediaId(room)},
|
||||
{"avatarUrl", user->avatarUrl(room)},
|
||||
{"displayName", user->displayname(room)},
|
||||
{"display", user->name()},
|
||||
{"color", user->color()},
|
||||
{"object", QVariant::fromValue(user)},
|
||||
};
|
||||
}
|
||||
|
||||
static LinkPreviewer *emptyLinkPreview = new LinkPreviewer;
|
||||
|
||||
QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
{
|
||||
if (!checkIndex(idx, QAbstractItemModel::CheckIndexOption::IndexIsValid)) {
|
||||
return {};
|
||||
}
|
||||
const auto row = idx.row();
|
||||
|
||||
if (!m_currentRoom || row < 0 || row >= int(m_currentRoom->pendingEvents().size()) + m_currentRoom->timelineSize()) {
|
||||
@@ -523,16 +489,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return m_currentRoom->eventToGenericString(evt);
|
||||
}
|
||||
|
||||
if (role == FormattedBodyRole) {
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||
if (e->hasTextContent() && e->mimeType().name() != "text/plain") {
|
||||
return static_cast<const Quotient::EventContent::TextContent *>(e->content())->body;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
if (role == PlainText) {
|
||||
return m_currentRoom->eventToString(evt);
|
||||
}
|
||||
@@ -565,6 +521,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
|
||||
return DelegateType::Message;
|
||||
}
|
||||
if (evt.matrixType() == "m.call.invite") {
|
||||
return DelegateType::CallInvite;
|
||||
}
|
||||
if (is<const StickerEvent>(evt)) {
|
||||
return DelegateType::Sticker;
|
||||
}
|
||||
@@ -586,13 +545,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return DelegateType::Other;
|
||||
}
|
||||
|
||||
if (role == EventResolvedTypeRole) {
|
||||
return EventTypeRegistry::getMatrixType(evt.type());
|
||||
}
|
||||
|
||||
if (role == AuthorRole) {
|
||||
auto author = static_cast<NeoChatUser *>(isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId()));
|
||||
return userInContext(author, m_currentRoom);
|
||||
return m_currentRoom->getUser(author);
|
||||
}
|
||||
|
||||
if (role == ContentRole) {
|
||||
@@ -690,7 +645,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return !evt.id().isEmpty() ? evt.id() : evt.transactionId();
|
||||
}
|
||||
|
||||
if (role == LongOperationRole) {
|
||||
if (role == ProgressInfoRole) {
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||
if (e->hasFileContent()) {
|
||||
return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id()));
|
||||
@@ -703,7 +658,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
|
||||
if (role == TimeRole || role == SectionRole) {
|
||||
auto ts = isPending ? pendingIt->lastUpdated() : makeMessageTimestamp(timelineIt);
|
||||
return role == TimeRole ? QVariant(ts) : renderDate(ts);
|
||||
return role == TimeRole ? QVariant(ts) : m_format.formatRelativeDate(ts.toLocalTime().date(), QLocale::ShortFormat);
|
||||
}
|
||||
|
||||
if (role == ShowLinkPreviewRole) {
|
||||
@@ -731,18 +686,18 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == ReplyAuthor) {
|
||||
auto replyPtr = getReplyForEvent(evt);
|
||||
auto replyPtr = m_currentRoom->getReplyForEvent(evt);
|
||||
|
||||
if (replyPtr) {
|
||||
auto replyUser = static_cast<NeoChatUser *>(m_currentRoom->user(replyPtr->senderId()));
|
||||
return userInContext(replyUser, m_currentRoom);
|
||||
return m_currentRoom->getUser(replyUser);
|
||||
} else {
|
||||
return emptyUser;
|
||||
return m_currentRoom->getUser(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
if (role == ReplyMediaInfoRole) {
|
||||
auto replyPtr = getReplyForEvent(evt);
|
||||
auto replyPtr = m_currentRoom->getReplyForEvent(evt);
|
||||
if (!replyPtr) {
|
||||
return {};
|
||||
}
|
||||
@@ -750,7 +705,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == ReplyRole) {
|
||||
auto replyPtr = getReplyForEvent(evt);
|
||||
auto replyPtr = m_currentRoom->getReplyForEvent(evt);
|
||||
if (!replyPtr) {
|
||||
return {};
|
||||
}
|
||||
@@ -873,7 +828,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
#else
|
||||
auto user = static_cast<NeoChatUser *>(userId);
|
||||
#endif
|
||||
users += userInContext(user, m_currentRoom);
|
||||
users += m_currentRoom->getUser(user);
|
||||
}
|
||||
|
||||
return users;
|
||||
@@ -932,38 +887,17 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == ReactionRole) {
|
||||
const auto &annotations = m_currentRoom->relatedEvents(evt, EventRelation::Annotation());
|
||||
if (annotations.isEmpty()) {
|
||||
return {};
|
||||
};
|
||||
QMap<QString, QList<NeoChatUser *>> reactions = {};
|
||||
for (const auto &a : annotations) {
|
||||
if (a->isRedacted()) { // Just in case?
|
||||
continue;
|
||||
}
|
||||
if (auto e = eventCast<const ReactionEvent>(a)) {
|
||||
reactions[e->relation().key].append(static_cast<NeoChatUser *>(m_currentRoom->user(e->senderId())));
|
||||
}
|
||||
if (m_reactionModels.contains(evt.id())) {
|
||||
return QVariant::fromValue<ReactionModel *>(m_reactionModels[evt.id()]);
|
||||
} else {
|
||||
return QVariantList();
|
||||
}
|
||||
|
||||
if (reactions.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QVariantList res = {};
|
||||
auto i = reactions.constBegin();
|
||||
while (i != reactions.constEnd()) {
|
||||
QVariantList authors;
|
||||
for (auto author : i.value()) {
|
||||
authors.append(userInContext(author, m_currentRoom));
|
||||
}
|
||||
bool hasLocalUser = i.value().contains(static_cast<NeoChatUser *>(m_currentRoom->localUser()));
|
||||
res.append(QVariantMap{{"reaction", i.key()}, {"count", i.value().count()}, {"authors", authors}, {"hasLocalUser", hasLocalUser}});
|
||||
++i;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
if (role == ShowReactionsRole) {
|
||||
return m_reactionModels.contains(evt.id());
|
||||
}
|
||||
|
||||
if (role == AuthorIdRole) {
|
||||
return evt.senderId();
|
||||
}
|
||||
@@ -1020,117 +954,6 @@ int MessageEventModel::eventIdToRow(const QString &eventID) const
|
||||
return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex();
|
||||
}
|
||||
|
||||
QVariant MessageEventModel::getLastLocalUserMessageEventId()
|
||||
{
|
||||
QVariantMap targetMessage;
|
||||
const auto &timelineBottom = m_currentRoom->messageEvents().rbegin();
|
||||
|
||||
// set a cap limit of 35 messages, to prevent loading a lot of messages
|
||||
// in rooms where the user has not sent many messages
|
||||
const auto limit = timelineBottom + std::min(35, m_currentRoom->timelineSize());
|
||||
|
||||
for (auto it = timelineBottom; it != limit; ++it) {
|
||||
auto evt = it->event();
|
||||
auto e = eventCast<const RoomMessageEvent>(evt);
|
||||
if (!e) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// check if the current message's sender's id is same as the user's id
|
||||
if ((*it)->senderId() == m_currentRoom->localUser()->id()) {
|
||||
auto content = (*it)->contentJson();
|
||||
|
||||
if (e->msgtype() != MessageEventType::Unknown) {
|
||||
QString eventId;
|
||||
if (content.contains("m.new_content")) {
|
||||
// The message has been edited so we have to return the id of the original message instead of the replacement
|
||||
eventId = content["m.relates_to"].toObject()["event_id"].toString();
|
||||
} else {
|
||||
// For any message that isn't an edit return the id of the current message
|
||||
eventId = (*it)->id();
|
||||
}
|
||||
targetMessage.insert("event_id", eventId);
|
||||
targetMessage.insert("formattedBody", content["formatted_body"].toString());
|
||||
// Need to get the message from the original eventId or body will have * on the front
|
||||
QModelIndex idx = index(eventIdToRow(eventId), 0);
|
||||
targetMessage.insert("message", idx.data(Qt::UserRole + 2));
|
||||
|
||||
return targetMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
return targetMessage;
|
||||
}
|
||||
|
||||
QVariant MessageEventModel::getLatestMessageFromRow(const int startRow)
|
||||
{
|
||||
QVariantMap replyResponse;
|
||||
const auto &timelineBottom = m_currentRoom->messageEvents().rbegin() + startRow;
|
||||
|
||||
// set a cap limit of startRow + 35 messages, to prevent loading a lot of messages
|
||||
// in rooms where the user has not sent many messages
|
||||
const auto limit = timelineBottom + std::min(startRow + 35, m_currentRoom->timelineSize());
|
||||
|
||||
for (auto it = timelineBottom; it != limit; ++it) {
|
||||
auto evt = it->event();
|
||||
auto e = eventCast<const RoomMessageEvent>(evt);
|
||||
if (!e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto content = (*it)->contentJson();
|
||||
|
||||
if (e->msgtype() != MessageEventType::Unknown) {
|
||||
QString eventId;
|
||||
if (content.contains("m.new_content")) {
|
||||
// The message has been edited so we have to return the id of the original message instead of the replacement
|
||||
eventId = content["m.relates_to"].toObject()["event_id"].toString();
|
||||
} else {
|
||||
// For any message that isn't an edit return the id of the current message
|
||||
eventId = (*it)->id();
|
||||
}
|
||||
replyResponse.insert("event_id", eventId);
|
||||
// Need to get the message from the original eventId or body will have * on the front
|
||||
QModelIndex idx = index(eventIdToRow(eventId), 0);
|
||||
replyResponse.insert("message", idx.data(Qt::UserRole + 2));
|
||||
replyResponse.insert("sender_id", QVariant::fromValue(m_currentRoom->getUser((*it)->senderId())));
|
||||
replyResponse.insert("at", -it->index());
|
||||
return replyResponse;
|
||||
}
|
||||
}
|
||||
return replyResponse;
|
||||
}
|
||||
|
||||
void MessageEventModel::loadReply(const QModelIndex &index)
|
||||
{
|
||||
auto job = m_currentRoom->connection()->callApi<GetOneRoomEventJob>(m_currentRoom->id(), data(index, ReplyIdRole).toString());
|
||||
QPersistentModelIndex persistentIndex(index);
|
||||
connect(job, &BaseJob::success, this, [this, job, persistentIndex] {
|
||||
m_extraEvents.push_back(fromJson<event_ptr_tt<RoomEvent>>(job->jsonData()));
|
||||
Q_EMIT dataChanged(persistentIndex, persistentIndex, {ReplyRole, ReplyMediaInfoRole, ReplyAuthor});
|
||||
});
|
||||
}
|
||||
|
||||
const RoomEvent *MessageEventModel::getReplyForEvent(const RoomEvent &event) const
|
||||
{
|
||||
const QString &replyEventId = event.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString();
|
||||
if (replyEventId.isEmpty()) {
|
||||
return {};
|
||||
};
|
||||
|
||||
const auto replyIt = m_currentRoom->findInTimeline(replyEventId);
|
||||
const RoomEvent *replyPtr = replyIt != m_currentRoom->historyEdge() ? &**replyIt : nullptr;
|
||||
if (!replyPtr) {
|
||||
for (const auto &e : m_extraEvents) {
|
||||
if (e->id() == replyEventId) {
|
||||
replyPtr = e.get();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return replyPtr;
|
||||
}
|
||||
|
||||
QVariantMap MessageEventModel::getMediaInfoForEvent(const RoomEvent &event) const
|
||||
{
|
||||
QVariantMap mediaInfo;
|
||||
@@ -1278,3 +1101,60 @@ void MessageEventModel::createLinkPreviewerForEvent(const Quotient::RoomMessageE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MessageEventModel::createReactionModelForEvent(const Quotient::RoomMessageEvent *event)
|
||||
{
|
||||
if (event == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto eventId = event->id();
|
||||
const auto &annotations = m_currentRoom->relatedEvents(eventId, EventRelation::Annotation());
|
||||
if (annotations.isEmpty()) {
|
||||
if (m_reactionModels.contains(eventId)) {
|
||||
delete m_reactionModels[eventId];
|
||||
m_reactionModels.remove(eventId);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
QMap<QString, QList<NeoChatUser *>> reactions = {};
|
||||
for (const auto &a : annotations) {
|
||||
if (a->isRedacted()) { // Just in case?
|
||||
continue;
|
||||
}
|
||||
if (const auto &e = eventCast<const ReactionEvent>(a)) {
|
||||
reactions[e->relation().key].append(static_cast<NeoChatUser *>(m_currentRoom->user(e->senderId())));
|
||||
}
|
||||
}
|
||||
|
||||
if (reactions.isEmpty()) {
|
||||
if (m_reactionModels.contains(eventId)) {
|
||||
delete m_reactionModels[eventId];
|
||||
m_reactionModels.remove(eventId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
QList<ReactionModel::Reaction> res;
|
||||
auto i = reactions.constBegin();
|
||||
while (i != reactions.constEnd()) {
|
||||
QVariantList authors;
|
||||
for (const auto &author : i.value()) {
|
||||
authors.append(m_currentRoom->getUser(author));
|
||||
}
|
||||
|
||||
res.append(ReactionModel::Reaction{i.key(), authors});
|
||||
++i;
|
||||
}
|
||||
|
||||
if (m_reactionModels.contains(eventId)) {
|
||||
m_reactionModels[eventId]->setReactions(res);
|
||||
} else if (res.size() > 0) {
|
||||
m_reactionModels[eventId] = new ReactionModel(this, res, static_cast<NeoChatUser *>(m_currentRoom->localUser()));
|
||||
} else {
|
||||
if (m_reactionModels.contains(eventId)) {
|
||||
delete m_reactionModels[eventId];
|
||||
m_reactionModels.remove(eventId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <KFormat>
|
||||
#include <QAbstractListModel>
|
||||
|
||||
#include "linkpreviewer.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
class ReactionModel;
|
||||
|
||||
/**
|
||||
* @class MessageEventModel
|
||||
*
|
||||
@@ -50,6 +53,7 @@ public:
|
||||
ReadMarker, /**< The local user read marker. */
|
||||
Poll, /**< The initial event for a poll. */
|
||||
Location, /**< A location event. */
|
||||
CallInvite, /**< An invitation to a call. */
|
||||
Other, /**< Anything that cannot be classified as another type. */
|
||||
};
|
||||
Q_ENUM(DelegateType);
|
||||
@@ -67,8 +71,7 @@ public:
|
||||
ContentRole, /**< The full message content. */
|
||||
HighlightRole, /**< Whether the event should be highlighted. */
|
||||
SpecialMarksRole, /**< Whether the event is hidden or not. */
|
||||
LongOperationRole, /**< Progress info when downloading files. */
|
||||
FormattedBodyRole, /**< The formatted body of a rich message. */
|
||||
ProgressInfoRole, /**< Progress info when downloading files. */
|
||||
GenericDisplayRole, /**< A generic string based upon the message type. */
|
||||
|
||||
ShowLinkPreviewRole, /**< Whether a link preview should be shown. */
|
||||
@@ -90,11 +93,10 @@ public:
|
||||
ExcessReadMarkersRole, /**< The number of other users at the event after the first 5. */
|
||||
ReadMarkersStringRole, /**< String with the display name and mxID of the users at the event. */
|
||||
ShowReadMarkersRole, /**< Whether there are any other user read markers to be shown. */
|
||||
ReactionRole, /**< List of reactions to this event. */
|
||||
ReactionRole, /**< List model for this event. */
|
||||
ShowReactionsRole, /**< Whether there are any reactions to be shown. */
|
||||
SourceRole, /**< The full message source JSON. */
|
||||
|
||||
// For debugging
|
||||
EventResolvedTypeRole, /**< The event type the message. */
|
||||
AuthorIdRole, /**< Matrix ID of the message author. */
|
||||
|
||||
VerifiedRole, /**< Whether an encrypted message is sent in a verified session. */
|
||||
@@ -110,7 +112,6 @@ public:
|
||||
Q_ENUM(EventRoles)
|
||||
|
||||
explicit MessageEventModel(QObject *parent = nullptr);
|
||||
~MessageEventModel() override;
|
||||
|
||||
[[nodiscard]] NeoChatRoom *room() const;
|
||||
void setRoom(NeoChatRoom *room);
|
||||
@@ -141,39 +142,6 @@ public:
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] int eventIdToRow(const QString &eventID) const;
|
||||
|
||||
/**
|
||||
* @brief Get the last message sent by the local user.
|
||||
*
|
||||
* @note This checks a maximum of the previous 35 message for performance reasons.
|
||||
*
|
||||
* @return a QVariantMap for the event with the following parameters:
|
||||
* - eventId - The event ID.
|
||||
* - formattedBody - The message text formatted as Qt::RichText.
|
||||
* - message - The message text formatted as Qt::PlainText.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] QVariant getLastLocalUserMessageEventId();
|
||||
|
||||
/**
|
||||
* @brief Get the last message sent earlier than the given row.
|
||||
*
|
||||
* @note This checks a maximum of the previous 35 message for performance reasons.
|
||||
*
|
||||
* @return a QVariantMap for the event with the following parameters:
|
||||
* - eventId - The event ID.
|
||||
* - message - The message text formatted as Qt::PlainText.
|
||||
* - sender_id - The matrix ID of the sender.
|
||||
* - at - The QModelIndex of the message.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] QVariant getLatestMessageFromRow(const int startRow);
|
||||
|
||||
/**
|
||||
* @brief Load the event that the item at the given index replied to.
|
||||
*
|
||||
* This is used to ensure that the reply data is available when the message that
|
||||
* was replied to is outside the currently loaded timeline.
|
||||
*/
|
||||
Q_INVOKABLE void loadReply(const QModelIndex &index);
|
||||
|
||||
private Q_SLOTS:
|
||||
int refreshEvent(const QString &eventId);
|
||||
void refreshRow(int row);
|
||||
@@ -184,12 +152,13 @@ private:
|
||||
QPersistentModelIndex m_lastReadEventIndex;
|
||||
int rowBelowInserted = -1;
|
||||
bool movingEvent = false;
|
||||
KFormat m_format;
|
||||
|
||||
QMap<QString, LinkPreviewer *> m_linkPreviewers;
|
||||
QMap<QString, ReactionModel *> m_reactionModels;
|
||||
|
||||
[[nodiscard]] int timelineBaseIndex() const;
|
||||
[[nodiscard]] QDateTime makeMessageTimestamp(const Quotient::Room::rev_iter_t &baseIt) const;
|
||||
[[nodiscard]] static QString renderDate(const QDateTime ×tamp);
|
||||
|
||||
bool canFetchMore(const QModelIndex &parent) const override;
|
||||
void fetchMore(const QModelIndex &parent) override;
|
||||
@@ -199,12 +168,10 @@ private:
|
||||
int refreshEventRoles(const QString &eventId, const QVector<int> &roles = {});
|
||||
void moveReadMarker(const QString &toEventId);
|
||||
|
||||
const Quotient::RoomEvent *getReplyForEvent(const Quotient::RoomEvent &event) const;
|
||||
QVariantMap getMediaInfoForEvent(const Quotient::RoomEvent &event) const;
|
||||
QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const;
|
||||
void createLinkPreviewerForEvent(const Quotient::RoomMessageEvent *event);
|
||||
|
||||
std::vector<Quotient::event_ptr_tt<Quotient::RoomEvent>> m_extraEvents;
|
||||
void createReactionModelForEvent(const Quotient::RoomMessageEvent *event);
|
||||
// Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows
|
||||
bool m_initialized = false;
|
||||
|
||||
|
||||
110
src/models/reactionmodel.cpp
Normal file
110
src/models/reactionmodel.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "reactionmodel.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "neochatuser.h"
|
||||
|
||||
ReactionModel::ReactionModel(QObject *parent, QList<Reaction> reactions, NeoChatUser *localUser)
|
||||
: QAbstractListModel(parent)
|
||||
, m_localUser(localUser)
|
||||
{
|
||||
setReactions(reactions);
|
||||
}
|
||||
|
||||
QVariant ReactionModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (index.row() >= rowCount()) {
|
||||
qDebug() << "ReactionModel, something's wrong: index.row() >= rowCount()";
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto &reaction = m_reactions.at(index.row());
|
||||
|
||||
if (role == TextRole) {
|
||||
if (reaction.authors.count() > 1) {
|
||||
return reaction.reaction + QStringLiteral(" %1").arg(reaction.authors.count());
|
||||
} else {
|
||||
return reaction.reaction;
|
||||
}
|
||||
}
|
||||
|
||||
if (role == ReactionRole) {
|
||||
return reaction.reaction;
|
||||
}
|
||||
|
||||
if (role == ToolTipRole) {
|
||||
QString text;
|
||||
|
||||
for (int i = 0; i < reaction.authors.count() && i < 3; i++) {
|
||||
if (i != 0) {
|
||||
if (i < reaction.authors.count() - 1) {
|
||||
text += QStringLiteral(", ");
|
||||
} else {
|
||||
text += i18nc("Separate the usernames of users", " and ");
|
||||
}
|
||||
}
|
||||
text += reaction.authors.at(i).toMap()["displayName"].toString();
|
||||
}
|
||||
|
||||
if (reaction.authors.count() > 3) {
|
||||
text += i18ncp("%1 is the number of other users", " and %1 other", " and %1 others", reaction.authors.count() - 3);
|
||||
}
|
||||
|
||||
text = i18ncp("%2 is the users who reacted and %3 the emoji that was given",
|
||||
"%2 reacted with %3",
|
||||
"%2 reacted with %3",
|
||||
reaction.authors.count(),
|
||||
text,
|
||||
reaction.reaction);
|
||||
return text;
|
||||
}
|
||||
|
||||
if (role == AuthorsRole) {
|
||||
return reaction.authors;
|
||||
}
|
||||
|
||||
if (role == HasLocalUser) {
|
||||
for (auto author : reaction.authors) {
|
||||
if (author.toMap()["id"] == m_localUser->id()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
int ReactionModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return m_reactions.count();
|
||||
}
|
||||
|
||||
void ReactionModel::setReactions(QList<Reaction> reactions)
|
||||
{
|
||||
beginResetModel();
|
||||
m_reactions.clear();
|
||||
m_reactions = reactions;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ReactionModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{TextRole, "text"},
|
||||
{ReactionRole, "reaction"},
|
||||
{ToolTipRole, "toolTip"},
|
||||
{AuthorsRole, "authors"},
|
||||
{HasLocalUser, "hasLocalUser"},
|
||||
};
|
||||
}
|
||||
72
src/models/reactionmodel.h
Normal file
72
src/models/reactionmodel.h
Normal file
@@ -0,0 +1,72 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
class NeoChatUser;
|
||||
|
||||
/**
|
||||
* @class ReactionModel
|
||||
*
|
||||
* This class defines the model for visualising a list of reactions to an event.
|
||||
*/
|
||||
class ReactionModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Definition of an reaction.
|
||||
*/
|
||||
struct Reaction {
|
||||
QString reaction; /**< The reaction emoji. */
|
||||
QVariantList authors; /**< The list of authors who sent the given reaction. */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
TextRole = Qt::DisplayRole, /**< The text to show in the reaction. */
|
||||
ReactionRole, /**< The reaction emoji. */
|
||||
ToolTipRole, /**< The tool tip to show for the reaction. */
|
||||
AuthorsRole, /**< The list of authors who sent the given reaction. */
|
||||
HasLocalUser, /**< Whether the local user is in the list of authors. */
|
||||
};
|
||||
|
||||
ReactionModel(QObject *parent = nullptr, QList<Reaction> reactions = {}, NeoChatUser *localUser = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Set the reactions data in the model.
|
||||
*/
|
||||
void setReactions(QList<Reaction> reactions);
|
||||
|
||||
private:
|
||||
QList<Reaction> m_reactions;
|
||||
|
||||
NeoChatUser *m_localUser;
|
||||
};
|
||||
Q_DECLARE_METATYPE(ReactionModel *)
|
||||
@@ -158,7 +158,7 @@ void RoomListModel::doAddRoom(Room *r)
|
||||
void RoomListModel::connectRoomSignals(NeoChatRoom *room)
|
||||
{
|
||||
connect(room, &Room::displaynameChanged, this, [this, room] {
|
||||
refresh(room, {DisplayNameRole, NameRole});
|
||||
refresh(room, {DisplayNameRole});
|
||||
});
|
||||
connect(room, &Room::unreadMessagesChanged, this, [this, room] {
|
||||
refresh(room, {NotificationCountRole, HighlightCountRole});
|
||||
@@ -234,12 +234,23 @@ void RoomListModel::handleNotifications()
|
||||
} else {
|
||||
avatar_image = room->avatar(128);
|
||||
}
|
||||
NotificationsManager::instance().postNotification(dynamic_cast<NeoChatRoom *>(room),
|
||||
sender->displayname(room),
|
||||
notification["event"].toObject()["content"].toObject()["body"].toString(),
|
||||
avatar_image,
|
||||
notification["event"].toObject()["event_id"].toString(),
|
||||
true);
|
||||
if (notification["event"]["type"].toString() == QStringLiteral("m.call.invite")) {
|
||||
#ifdef GSTREAMER_AVAILABLE
|
||||
NotificationsManager::instance().postCallInviteNotification(
|
||||
dynamic_cast<NeoChatRoom *>(room),
|
||||
room->displayName(),
|
||||
sender->displayname(room),
|
||||
avatar_image,
|
||||
notification["event"]["content"]["offer"]["sdp"].toString().contains(QStringLiteral("video")));
|
||||
#endif
|
||||
} else {
|
||||
NotificationsManager::instance().postNotification(dynamic_cast<NeoChatRoom *>(room),
|
||||
sender->displayname(room),
|
||||
notification["event"].toObject()["content"].toObject()["body"].toString(),
|
||||
avatar_image,
|
||||
notification["event"].toObject()["event_id"].toString(),
|
||||
true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -281,7 +292,7 @@ void RoomListModel::updateRoom(Room *room, Room *prev)
|
||||
}
|
||||
// Ok, we're through with pre-checks, now for the real thing.
|
||||
auto newRoom = static_cast<NeoChatRoom *>(room);
|
||||
const auto it = std::find_if(m_rooms.begin(), m_rooms.end(), [=](const NeoChatRoom *r) {
|
||||
const auto it = std::find_if(m_rooms.begin(), m_rooms.end(), [prev, newRoom](const NeoChatRoom *r) {
|
||||
return r == prev || r == newRoom;
|
||||
});
|
||||
if (it != m_rooms.end()) {
|
||||
@@ -333,9 +344,6 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
||||
return QVariant();
|
||||
}
|
||||
NeoChatRoom *room = m_rooms.at(index.row());
|
||||
if (role == NameRole) {
|
||||
return !room->name().isEmpty() ? room->name() : room->displayName();
|
||||
}
|
||||
if (role == DisplayNameRole) {
|
||||
return room->displayName();
|
||||
}
|
||||
@@ -434,7 +442,6 @@ void RoomListModel::refresh(NeoChatRoom *room, const QVector<int> &roles)
|
||||
QHash<int, QByteArray> RoomListModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[NameRole] = "name";
|
||||
roles[DisplayNameRole] = "displayName";
|
||||
roles[AvatarRole] = "avatar";
|
||||
roles[CanonicalAliasRole] = "canonicalAlias";
|
||||
|
||||
@@ -58,8 +58,7 @@ public:
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum EventRoles {
|
||||
NameRole = Qt::UserRole + 1, /**< The name of the room. */
|
||||
DisplayNameRole, /**< The display name of the room. */
|
||||
DisplayNameRole = Qt::DisplayRole, /**< The display name of the room. */
|
||||
AvatarRole, /**< The source URL for the room's avatar. */
|
||||
CanonicalAliasRole, /**< The room canonical alias. */
|
||||
TopicRole, /**< The room topic. */
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "searchmodel.h"
|
||||
#include "events/stickerevent.h"
|
||||
#include "messageeventmodel.h"
|
||||
#include "neochatroom.h"
|
||||
#include "neochatuser.h"
|
||||
@@ -42,18 +43,18 @@ void SearchModel::search()
|
||||
m_job = nullptr;
|
||||
}
|
||||
|
||||
RoomEventFilter filter;
|
||||
filter.unreadThreadNotifications = none;
|
||||
filter.lazyLoadMembers = true;
|
||||
filter.includeRedundantMembers = false;
|
||||
filter.notRooms = QStringList();
|
||||
filter.rooms = QStringList{m_room->id()};
|
||||
filter.containsUrl = false;
|
||||
|
||||
SearchJob::RoomEventsCriteria criteria{
|
||||
.searchTerm = m_searchText,
|
||||
.keys = {},
|
||||
.filter =
|
||||
RoomEventFilter{
|
||||
.unreadThreadNotifications = none,
|
||||
.lazyLoadMembers = true,
|
||||
.includeRedundantMembers = false,
|
||||
.notRooms = {},
|
||||
.rooms = {m_room->id()},
|
||||
.containsUrl = false,
|
||||
},
|
||||
.filter = filter,
|
||||
.orderBy = "recent",
|
||||
.eventContext = SearchJob::IncludeEventContext{3, 3, true},
|
||||
.includeState = false,
|
||||
@@ -96,16 +97,7 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
||||
case ShowAuthorRole:
|
||||
return true;
|
||||
case AuthorRole:
|
||||
return QVariantMap{
|
||||
{"isLocalUser", event.senderId() == m_room->localUser()->id()},
|
||||
{"id", event.senderId()},
|
||||
{"avatarMediaId", m_connection->user(event.senderId())->avatarMediaId(m_room)},
|
||||
{"avatarUrl", m_connection->user(event.senderId())->avatarUrl(m_room)},
|
||||
{"displayName", m_connection->user(event.senderId())->displayname(m_room)},
|
||||
{"display", m_connection->user(event.senderId())->name()},
|
||||
{"color", dynamic_cast<NeoChatUser *>(m_connection->user(event.senderId()))->color()},
|
||||
{"object", QVariant::fromValue(m_connection->user(event.senderId()))},
|
||||
};
|
||||
return m_room->getUser(event.senderId());
|
||||
case ShowSectionRole:
|
||||
if (row == 0) {
|
||||
return true;
|
||||
@@ -115,6 +107,72 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
||||
return renderDate(event.originTimestamp());
|
||||
case TimeRole:
|
||||
return event.originTimestamp();
|
||||
case ShowReactionsRole:
|
||||
return false;
|
||||
case ShowReadMarkersRole:
|
||||
return false;
|
||||
case ReplyAuthorRole:
|
||||
if (const auto &replyPtr = m_room->getReplyForEvent(event)) {
|
||||
return m_room->getUser(static_cast<NeoChatUser *>(m_room->user(replyPtr->senderId())));
|
||||
} else {
|
||||
return m_room->getUser(nullptr);
|
||||
}
|
||||
case ReplyRole:
|
||||
if (role == ReplyRole) {
|
||||
auto replyPtr = m_room->getReplyForEvent(event);
|
||||
if (!replyPtr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
MessageEventModel::DelegateType type;
|
||||
if (auto e = eventCast<const RoomMessageEvent>(replyPtr)) {
|
||||
switch (e->msgtype()) {
|
||||
case MessageEventType::Emote:
|
||||
type = MessageEventModel::DelegateType::Emote;
|
||||
break;
|
||||
case MessageEventType::Notice:
|
||||
type = MessageEventModel::DelegateType::Notice;
|
||||
break;
|
||||
case MessageEventType::Image:
|
||||
type = MessageEventModel::DelegateType::Image;
|
||||
break;
|
||||
case MessageEventType::Audio:
|
||||
type = MessageEventModel::DelegateType::Audio;
|
||||
break;
|
||||
case MessageEventType::Video:
|
||||
type = MessageEventModel::DelegateType::Video;
|
||||
break;
|
||||
default:
|
||||
if (e->hasFileContent()) {
|
||||
type = MessageEventModel::DelegateType::File;
|
||||
break;
|
||||
}
|
||||
type = MessageEventModel::DelegateType::Message;
|
||||
}
|
||||
|
||||
} else if (is<const StickerEvent>(*replyPtr)) {
|
||||
type = MessageEventModel::DelegateType::Sticker;
|
||||
} else {
|
||||
type = MessageEventModel::DelegateType::Other;
|
||||
}
|
||||
|
||||
return QVariantMap{
|
||||
{"display", m_room->eventToString(*replyPtr, Qt::RichText)},
|
||||
{"type", type},
|
||||
};
|
||||
}
|
||||
case IsPendingRole:
|
||||
return false;
|
||||
case ShowLinkPreviewRole:
|
||||
return false;
|
||||
case IsReplyRole:
|
||||
return !event.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString().isEmpty();
|
||||
case HighlightRole:
|
||||
return !m_room->isDirectChat() && m_room->isEventHighlighted(&event);
|
||||
case EventIdRole:
|
||||
return event.id();
|
||||
case ReplyIdRole:
|
||||
return event.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString();
|
||||
}
|
||||
return MessageEventModel::DelegateType::Message;
|
||||
#endif
|
||||
@@ -142,6 +200,27 @@ QHash<int, QByteArray> SearchModel::roleNames() const
|
||||
{SectionRole, "section"},
|
||||
{TimeRole, "time"},
|
||||
{ShowAuthorRole, "showAuthor"},
|
||||
{EventIdRole, "eventId"},
|
||||
{ExcessReadMarkersRole, "excessReadMarkers"},
|
||||
{HighlightRole, "isHighlighted"},
|
||||
{ReadMarkersString, "readMarkersString"},
|
||||
{PlainTextRole, "plainText"},
|
||||
{VerifiedRole, "verified"},
|
||||
{ReplyAuthorRole, "replyAuthor"},
|
||||
{ProgressInfoRole, "progressInfo"},
|
||||
{IsReplyRole, "isReply"},
|
||||
{ShowReactionsRole, "showReactions"},
|
||||
{ReplyRole, "reply"},
|
||||
{ReactionRole, "reaction"},
|
||||
{ReplyMediaInfoRole, "replyMediaInfo"},
|
||||
{ReadMarkersRole, "readMarkers"},
|
||||
{IsPendingRole, "isPending"},
|
||||
{ShowReadMarkersRole, "showReadMarkers"},
|
||||
{ReplyIdRole, "replyId"},
|
||||
{MimeTypeRole, "mimeType"},
|
||||
{ShowLinkPreviewRole, "showLinkPreview"},
|
||||
{LinkPreviewRole, "linkPreview"},
|
||||
{SourceRole, "source"},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -152,8 +231,26 @@ NeoChatRoom *SearchModel::room() const
|
||||
|
||||
void SearchModel::setRoom(NeoChatRoom *room)
|
||||
{
|
||||
if (m_room) {
|
||||
disconnect(m_room, nullptr, this, nullptr);
|
||||
}
|
||||
m_room = room;
|
||||
Q_EMIT roomChanged();
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
connect(m_room, &NeoChatRoom::replyLoaded, this, [this](const auto &eventId, const auto &replyId) {
|
||||
Q_UNUSED(replyId);
|
||||
const auto &results = m_result->results;
|
||||
auto it = std::find_if(results.begin(), results.end(), [eventId](const auto &event) {
|
||||
return event.result->id() == eventId;
|
||||
});
|
||||
if (it == results.end()) {
|
||||
return;
|
||||
}
|
||||
auto row = it - results.begin();
|
||||
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyRole, ReplyMediaInfoRole, ReplyAuthorRole});
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
// TODO deduplicate with messageeventmodel
|
||||
|
||||
@@ -50,17 +50,40 @@ public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*
|
||||
* For documentation of the roles, see MessageEventModel.
|
||||
*
|
||||
* Some of the roles exist only for compatibility with the MessageEventModel,
|
||||
* since the same delegates are used.
|
||||
*/
|
||||
enum Roles {
|
||||
DisplayRole = Qt::DisplayRole, /**< The message string. */
|
||||
DelegateTypeRole, /**< The type of the event. */
|
||||
ShowAuthorRole, /**< Whether the author should be shown (always true). */
|
||||
AuthorRole, /**< The author of the event. */
|
||||
ShowSectionRole, /**< Whether the section header should be shown. */
|
||||
SectionRole, /**< The date of the event as a string. */
|
||||
TimeRole, /**< The timestamp for when the event was sent. */
|
||||
DisplayRole = Qt::DisplayRole,
|
||||
DelegateTypeRole,
|
||||
ShowAuthorRole,
|
||||
AuthorRole,
|
||||
ShowSectionRole,
|
||||
SectionRole,
|
||||
TimeRole,
|
||||
EventIdRole,
|
||||
ExcessReadMarkersRole,
|
||||
HighlightRole,
|
||||
ReadMarkersString,
|
||||
PlainTextRole,
|
||||
VerifiedRole,
|
||||
ReplyAuthorRole,
|
||||
ProgressInfoRole,
|
||||
IsReplyRole,
|
||||
ShowReactionsRole,
|
||||
ReplyRole,
|
||||
ReactionRole,
|
||||
ReplyMediaInfoRole,
|
||||
ReadMarkersRole,
|
||||
IsPendingRole,
|
||||
ShowReadMarkersRole,
|
||||
ReplyIdRole,
|
||||
MimeTypeRole,
|
||||
ShowLinkPreviewRole,
|
||||
LinkPreviewRole,
|
||||
SourceRole,
|
||||
};
|
||||
Q_ENUM(Roles);
|
||||
SearchModel(QObject *parent = nullptr);
|
||||
|
||||
@@ -21,7 +21,7 @@ void SortFilterRoomListModel::setRoomSortOrder(SortFilterRoomListModel::RoomSort
|
||||
m_sortOrder = sortOrder;
|
||||
Q_EMIT roomSortOrderChanged();
|
||||
if (sortOrder == SortFilterRoomListModel::Alphabetical) {
|
||||
setSortRole(RoomListModel::NameRole);
|
||||
setSortRole(RoomListModel::DisplayNameRole);
|
||||
} else if (sortOrder == SortFilterRoomListModel::LastActivity) {
|
||||
setSortRole(RoomListModel::LastActiveTimeRole);
|
||||
}
|
||||
@@ -78,7 +78,8 @@ bool SortFilterRoomListModel::filterAcceptsRow(int source_row, const QModelIndex
|
||||
{
|
||||
Q_UNUSED(source_parent);
|
||||
|
||||
bool acceptRoom = sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::NameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
|
||||
bool acceptRoom =
|
||||
sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
|
||||
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() != "upgraded"
|
||||
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsSpaceRole).toBool() == false;
|
||||
|
||||
|
||||
@@ -14,30 +14,45 @@ StickerModel::StickerModel(QObject *parent)
|
||||
|
||||
int StickerModel::rowCount(const QModelIndex &index) const
|
||||
{
|
||||
Q_UNUSED(index);
|
||||
return m_images.size();
|
||||
}
|
||||
QVariant StickerModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
const auto &row = index.row();
|
||||
const auto &image = m_images[row];
|
||||
if (role == Url) {
|
||||
if (role == UrlRole) {
|
||||
#ifdef QUOTIENT_07
|
||||
return m_room->connection()->makeMediaUrl(image.url);
|
||||
#endif
|
||||
}
|
||||
if (role == Body) {
|
||||
if (role == BodyRole) {
|
||||
if (image.body) {
|
||||
return *image.body;
|
||||
}
|
||||
}
|
||||
if (role == IsStickerRole) {
|
||||
if (image.usage) {
|
||||
return image.usage->isEmpty() || image.usage->contains("sticker"_ls);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (role == IsEmojiRole) {
|
||||
if (image.usage) {
|
||||
return image.usage->isEmpty() || image.usage->contains("emoticon"_ls);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> StickerModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{StickerModel::Url, "url"},
|
||||
{StickerModel::Body, "body"},
|
||||
{UrlRole, "url"},
|
||||
{BodyRole, "body"},
|
||||
{IsStickerRole, "isSticker"},
|
||||
{IsEmojiRole, "isEmoji"},
|
||||
};
|
||||
}
|
||||
ImagePacksModel *StickerModel::model() const
|
||||
@@ -51,14 +66,11 @@ void StickerModel::setModel(ImagePacksModel *model)
|
||||
disconnect(m_model, nullptr, this, nullptr);
|
||||
}
|
||||
connect(model, &ImagePacksModel::roomChanged, this, [this]() {
|
||||
beginResetModel();
|
||||
m_images = m_model->images(m_index);
|
||||
endResetModel();
|
||||
reloadImages();
|
||||
});
|
||||
beginResetModel();
|
||||
connect(model, &ImagePacksModel::imagesLoaded, this, &StickerModel::reloadImages);
|
||||
m_model = model;
|
||||
m_images = m_model->images(m_index);
|
||||
endResetModel();
|
||||
reloadImages();
|
||||
Q_EMIT modelChanged();
|
||||
}
|
||||
|
||||
@@ -68,13 +80,18 @@ int StickerModel::packIndex() const
|
||||
}
|
||||
void StickerModel::setPackIndex(int index)
|
||||
{
|
||||
beginResetModel();
|
||||
m_index = index;
|
||||
Q_EMIT packIndexChanged();
|
||||
reloadImages();
|
||||
}
|
||||
|
||||
void StickerModel::reloadImages()
|
||||
{
|
||||
beginResetModel();
|
||||
if (m_model) {
|
||||
m_images = m_model->images(m_index);
|
||||
}
|
||||
endResetModel();
|
||||
Q_EMIT packIndexChanged();
|
||||
}
|
||||
|
||||
NeoChatRoom *StickerModel::room() const
|
||||
@@ -84,6 +101,9 @@ NeoChatRoom *StickerModel::room() const
|
||||
|
||||
void StickerModel::setRoom(NeoChatRoom *room)
|
||||
{
|
||||
if (room) {
|
||||
disconnect(room->connection(), nullptr, this, nullptr);
|
||||
}
|
||||
m_room = room;
|
||||
Q_EMIT roomChanged();
|
||||
}
|
||||
|
||||
@@ -47,8 +47,10 @@ public:
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
Url = Qt::UserRole + 1, /**< The source mxc URL for the image. */
|
||||
Body, /**< The image caption, if any. */
|
||||
UrlRole = Qt::UserRole + 1, /**< The source mxc URL for the image. */
|
||||
BodyRole, /**< The image caption, if any. */
|
||||
IsStickerRole, /**< Whether this emoticon is a sticker. */
|
||||
IsEmojiRole, /**< Whether this emoticon is an emoji. */
|
||||
};
|
||||
|
||||
explicit StickerModel(QObject *parent = nullptr);
|
||||
@@ -98,4 +100,5 @@ private:
|
||||
int m_index = 0;
|
||||
QVector<Quotient::ImagePackEventContent::ImagePackImage> m_images;
|
||||
NeoChatRoom *m_room;
|
||||
void reloadImages();
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ bool UserFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceP
|
||||
if (m_filterText.length() < 1) {
|
||||
return false;
|
||||
}
|
||||
return sourceModel()->data(sourceModel()->index(sourceRow, 0), UserListModel::NameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
|
||||
return sourceModel()->data(sourceModel()->index(sourceRow, 0), UserListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
|
||||
|| sourceModel()->data(sourceModel()->index(sourceRow, 0), UserListModel::UserIdRole).toString().contains(m_filterText, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ QVariant UserListModel::data(const QModelIndex &index, int role) const
|
||||
return {};
|
||||
}
|
||||
auto user = m_users.at(index.row());
|
||||
if (role == NameRole) {
|
||||
if (role == DisplayNameRole) {
|
||||
return user->displayname(m_currentRoom);
|
||||
}
|
||||
if (role == UserIdRole) {
|
||||
@@ -204,7 +204,7 @@ QHash<int, QByteArray> UserListModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
|
||||
roles[NameRole] = "name";
|
||||
roles[DisplayNameRole] = "name";
|
||||
roles[UserIdRole] = "userId";
|
||||
roles[AvatarRole] = "avatar";
|
||||
roles[ObjectRole] = "user";
|
||||
|
||||
@@ -38,7 +38,7 @@ public:
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum EventRoles {
|
||||
NameRole = Qt::UserRole + 1, /**< The user's display name in the current room. */
|
||||
DisplayNameRole = Qt::DisplayRole, /**< The user's display name in the current room. */
|
||||
UserIdRole, /**< Matrix ID of the user. */
|
||||
AvatarRole, /**< The source URL for the user's avatar in the current room. */
|
||||
ObjectRole, /**< The QObject for the user. */
|
||||
@@ -61,7 +61,7 @@ public:
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = NameRole) const override;
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
|
||||
@@ -13,6 +13,7 @@ Name[es]=NeoChat
|
||||
Name[eu]=NeoChat
|
||||
Name[fi]=NeoChat
|
||||
Name[fr]=NeoChat
|
||||
Name[gl]=NeoChat
|
||||
Name[hu]=NeoChat
|
||||
Name[ia]=Neochat
|
||||
Name[id]=NeoChat
|
||||
@@ -51,6 +52,7 @@ Comment[es]=Un cliente para Matrix, el protocolo de comunicaciones descentraliza
|
||||
Comment[eu]=Matrix, deszentralizatutako komunikazio protokolorako, bezero bat
|
||||
Comment[fi]=Hajautetun Matrix-viestintäyhteyskäytännön asiakasohjelma
|
||||
Comment[fr]=Un client pour « Matrix », le protocole décentralisé de communications.
|
||||
Comment[gl]=Un cliente para Matrix, o protocolo de comunicación descentralizada
|
||||
Comment[hu]=Kliens a matrixhoz, a decentralizált kommunikációs protokollhoz
|
||||
Comment[ia]=Un cliente per Matrix, le protocollo de communication decentralisate
|
||||
Comment[id]=Sebuah klien untuk matrix, protokol komunikasi terdecentralisasi
|
||||
@@ -90,6 +92,7 @@ Name[es]=Nuevo mensaje
|
||||
Name[eu]=Mezu berria
|
||||
Name[fi]=Uusi viesti
|
||||
Name[fr]=Nouveau message
|
||||
Name[gl]=Nova mensaxe
|
||||
Name[hu]=Új üzenet
|
||||
Name[ia]=Nove message
|
||||
Name[id]=Pesan baru
|
||||
@@ -126,6 +129,7 @@ Comment[es]=Hay un mensaje nuevo
|
||||
Comment[eu]=Mezu berri bat dago
|
||||
Comment[fi]=Saapui uusi viesti
|
||||
Comment[fr]=Il y a un nouveau message
|
||||
Comment[gl]=Hai unha nova mensaxe
|
||||
Comment[hu]=Új üzenet érkezett
|
||||
Comment[ia]=Il ha un nove message
|
||||
Comment[id]=Ada pesan baru
|
||||
@@ -166,6 +170,7 @@ Name[es]=Nueva invitación
|
||||
Name[eu]=Gonbidapen berria
|
||||
Name[fi]=Uusi kutsu
|
||||
Name[fr]=Nouvelle invitation
|
||||
Name[gl]=Novo convite
|
||||
Name[ia]=Nove invitation
|
||||
Name[id]=Undangan Baru
|
||||
Name[ie]=Nov invitation
|
||||
@@ -197,6 +202,7 @@ Comment[es]=Hay una nueva invitación a una sala
|
||||
Comment[eu]=Gela baterako gonbidapen berri bat dago
|
||||
Comment[fi]=Uusi kutsu huoneeseen
|
||||
Comment[fr]=Il y a une nouvelle invitation dans un salon.
|
||||
Comment[gl]=Tes un novo convite para unha sala
|
||||
Comment[ia]=Il ha un nove invitation a un sala
|
||||
Comment[id]=Ada undangan baru ke sebuah ruangan
|
||||
Comment[ie]=Vu have un nov invitation a un chambre
|
||||
|
||||
@@ -54,12 +54,12 @@ QVariant AccountRegistry::data(const QModelIndex &index, int role) const
|
||||
const auto account = m_accounts[index.row()];
|
||||
|
||||
switch (role) {
|
||||
case ConnectionRole:
|
||||
return QVariant::fromValue(account);
|
||||
case UserIdRole:
|
||||
return QVariant::fromValue(account->userId());
|
||||
default:
|
||||
return {};
|
||||
case ConnectionRole:
|
||||
return QVariant::fromValue(account);
|
||||
case UserIdRole:
|
||||
return QVariant::fromValue(account->userId());
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
|
||||
return {};
|
||||
|
||||
@@ -147,5 +147,23 @@
|
||||
<default></default>
|
||||
</entry>
|
||||
</group>
|
||||
<group name="Voip">
|
||||
<entry name="Microphone" type="string">
|
||||
<label>Name of the microphone</label>
|
||||
</entry>
|
||||
<entry name="Camera" type="string">
|
||||
<label>Name of the camera</label>
|
||||
</entry>
|
||||
<entry name="CameraCaps" type="int">
|
||||
<label>Index of the camera caps</label>
|
||||
</entry>
|
||||
<entry name="ScreenShareFrameRate" type="int">
|
||||
<label>Frame rate of the screenshare</label>
|
||||
</entry>
|
||||
<entry name="Ringtone" type="String">
|
||||
<label>Ringtone</label>
|
||||
<default>/usr/share/sounds/plasma-mobile/stereo/ringtones/Spatial.oga</default>
|
||||
</entry>
|
||||
</group>
|
||||
</kcfg>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user