Compare commits

..

59 Commits

Author SHA1 Message Date
Laurent Montel
4f3553a59d [QT6] Use icon.name 2023-07-12 09:51:40 +00:00
Laurent Montel
882a0d4901 Autogenerate + Install debug category file 2023-07-12 09:50:58 +00:00
Laurent Montel
506d31f53f "This" is not used 2023-07-12 09:50:41 +00:00
l10n daemon script
110f007b41 GIT_SILENT made messages (after extraction) 2023-07-12 00:47:42 +00:00
l10n daemon script
a90943d9ac GIT_SILENT Sync po/docbooks with svn 2023-07-11 01:50:22 +00:00
l10n daemon script
cbe7d8c2c2 GIT_SILENT made messages (after extraction) 2023-07-11 00:47:06 +00:00
James Graham
ae4943dd71 Use new mobileform heading
Does what is says on the tin. Use the recently merged new `MobileForm.FormHeader` component in all settings pages.
2023-07-10 16:46:35 +00:00
James Graham
7bd84bf51e Push Rule Model Rework
This is a significant rework of the handling of push rules. Rather than using a lot of boilerplate code for the default models `KeywordNotificationModel` has been converted to `PushRuleModel` and now handles all push rules.

The new model has the following features:
- Handles all push rules
- Has special handling for the names of default keywords (i.e. it still gives the same text as previously for showing in the settings menus)
- Push rules for blocking individuals or room overrides are still there but hidden so will be available for developer tools (to follow)
- Room specific keywords are now supported.

The notification settings pages have also been refactored to take advantage of the new models. Each section is now just a repeater with a filter for the rules that it should contain. The push rule delegate has now been cleaned up and uses required properties.

Implements network/neochat#574
2023-07-10 16:17:17 +00:00
l10n daemon script
a6ce44eb24 GIT_SILENT Sync po/docbooks with svn 2023-07-10 01:56:36 +00:00
Carl Schwan
b1c42c3d3d Make the whole right sidebar scrollable
The sidebar grew a lot and now contains some tools. This makes the space
left for the room members very small. Particuliary on mobile.

So make the whole sidebar contained inside the ScrollView

This also refactor a few things (e.g. use a plain ToolBar for the header)
2023-07-09 22:41:28 +00:00
Carl Schwan
213aaf3ac4 Ensure chat box and userInfo have the same height 2023-07-09 22:32:46 +00:00
James Graham
c55b40c9c6 Media Model
Create a media model for all the media message in the timeline and then setup `NeoChatMaximizeComponent` so that it can use the media model to scroll through all loaded images and video in the current room.

Depends upon libraries/kirigami-addons!105

FEATURE: 467411
2023-07-08 11:07:04 +00:00
Volker Krause
81928d8b93 Fix binary stripping of APKs with Qt 5.15.10
This fixes APKs on binary factory having exploded in size 3x-5x after
the Qt 5.15.10 update. This also affects release packages and will need
to be backported to those (in Craft).
2023-07-08 07:51:24 +02:00
Ingo Klöcker
b7bddba053 Add Windows-specific screenshots as custom values for Microsoft Store
This is an interim solution until AppStream (hopefully) gets support
for platform-specific screenshots. The captions will be extracted for
translation.
2023-07-07 15:55:11 +02:00
l10n daemon script
307a9370db GIT_SILENT Sync po/docbooks with svn 2023-07-07 01:54:27 +00:00
Volker Krause
2f65cbeb36 Show location descriptions in the timeline when available 2023-07-06 15:08:42 +00:00
l10n daemon script
e0c0b1f0e8 GIT_SILENT Sync po/docbooks with svn 2023-07-06 02:28:47 +00:00
l10n daemon script
74f767aa82 GIT_SILENT Sync po/docbooks with svn 2023-07-04 02:36:05 +00:00
l10n daemon script
7176dd4476 GIT_SILENT Sync po/docbooks with svn 2023-07-03 02:15:16 +00:00
l10n daemon script
f75fe31571 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2023-07-03 01:58:53 +00:00
l10n daemon script
61bdb1ed5f GIT_SILENT made messages (after extraction) 2023-07-03 00:55:03 +00:00
l10n daemon script
527e9d93a5 GIT_SILENT Sync po/docbooks with svn 2023-07-02 02:21:48 +00:00
l10n daemon script
7d22b30217 GIT_SILENT Sync po/docbooks with svn 2023-06-30 01:54:09 +00:00
Heiko Becker
c0d2333a3d GIT_SILENT Update Appstream for new release
(cherry picked from commit c72cf64369)
2023-06-30 01:16:59 +02:00
l10n daemon script
4a7e1d058c GIT_SILENT Sync po/docbooks with svn 2023-06-29 01:52:16 +00:00
l10n daemon script
66974615f6 GIT_SILENT Sync po/docbooks with svn 2023-06-28 01:55:40 +00:00
Ingo Klöcker
116c888686 Add "16:9 Super hero art" for Microsoft Store 2023-06-27 12:29:01 +02:00
Ingo Klöcker
f6d6a804d2 Add some logos in different sizes for the Microsoft Store 2023-06-27 12:28:06 +02:00
l10n daemon script
6d6d702b97 GIT_SILENT Sync po/docbooks with svn 2023-06-27 01:59:23 +00:00
l10n daemon script
9be04c1272 GIT_SILENT Sync po/docbooks with svn 2023-06-26 01:59:28 +00:00
l10n daemon script
6f5d88cf63 GIT_SILENT Sync po/docbooks with svn 2023-06-25 02:12:18 +00:00
l10n daemon script
1103e80191 GIT_SILENT made messages (after extraction) 2023-06-25 00:52:33 +00:00
l10n daemon script
a5320397c2 GIT_SILENT Sync po/docbooks with svn 2023-06-24 02:10:00 +00:00
Volker Krause
53719b971d Show a placeholder message if there are no shared locations 2023-06-22 15:17:40 +00:00
Volker Krause
64a1316f9b Zoom/center the page map so that all locations are in view 2023-06-22 15:17:40 +00:00
Volker Krause
a3b8168744 Add bounding box property for LocationsModel as well
Needed to property position the room map.
2023-06-22 15:17:40 +00:00
Volker Krause
c06e69931a Add fullscreen map to live location delegates as well 2023-06-22 15:17:40 +00:00
Volker Krause
30f8573dfc Show live location shares on the room map as well 2023-06-22 15:17:40 +00:00
Volker Krause
7f067b698e Unify map markers on the full screen and room maps as well 2023-06-22 15:17:40 +00:00
Volker Krause
b56ebdf149 Don't assert on invalid/empty geo: URIs 2023-06-22 15:17:40 +00:00
Volker Krause
a02dd4ab87 Extend LocationsModel to include asset and auther roles
Needed for compatibility with the standard map marker.
2023-06-22 15:17:40 +00:00
Volker Krause
898f0c962a Factor out map marker into its own QML element for reuse 2023-06-22 15:17:40 +00:00
Tobias Fella
5b4ae764cf Show a map for location events 2023-06-22 15:17:40 +00:00
l10n daemon script
d14db326bb GIT_SILENT Sync po/docbooks with svn 2023-06-22 01:53:18 +00:00
l10n daemon script
5c51e0d0fc GIT_SILENT Sync po/docbooks with svn 2023-06-21 01:49:24 +00:00
l10n daemon script
f78b4af692 GIT_SILENT Sync po/docbooks with svn 2023-06-19 02:05:24 +00:00
Carl Schwan
bd5ed0f46c Fix toolbar of RoomList when using font size 11px
We don't need to use a ToolBar for the titleDelegate since the
titleDelegate is already included inside a toolbar. This work magically
when using the default font size of 10px Noto Sans but breaks with 11px

BUG: 467242
2023-06-18 07:00:47 +00:00
l10n daemon script
529cfa8f7d GIT_SILENT Sync po/docbooks with svn 2023-06-18 02:44:28 +00:00
Carl Schwan
5552cd60f6 Group pageStack properties of the ApplicationWindow together 2023-06-17 21:29:15 +02:00
Tobias Fella
c51a1f4851 Fix another missing import 2023-06-17 14:32:03 +02:00
James Graham
a27f4765e4 Fix permission model
Make sure `Permissions.qml` uses the right role.
2023-06-17 09:34:42 +00:00
James Graham
402f99923c Fix NeoChatRoom type QML
Make sure import `org.kde.neochat 1.0` is included where `NeoChatRoom` is used as a type
2023-06-17 09:26:22 +00:00
l10n daemon script
2afda78912 GIT_SILENT Sync po/docbooks with svn 2023-06-17 01:53:16 +00:00
Volker Krause
25f9c7e125 Compute live location bounding box and center the map accordingly 2023-06-16 14:12:15 +00:00
Volker Krause
05082cb2bb Consider live location beacon timeouts for their 2023-06-16 14:12:15 +00:00
Volker Krause
59495a1452 Show starting live location beacons as location delegates in the timeline
Also, hide ending live location beacon state changes.
2023-06-16 14:12:15 +00:00
Volker Krause
d10460c45b Add live location tracking model
This can either watch a single live location beacon or all of those in
a given room.
2023-06-16 14:12:15 +00:00
l10n daemon script
b968c85de2 GIT_SILENT Sync po/docbooks with svn 2023-06-16 01:49:40 +00:00
l10n daemon script
13efc08b07 GIT_SILENT Sync po/docbooks with svn 2023-06-14 01:51:18 +00:00
138 changed files with 18236 additions and 20739 deletions

View File

@@ -17,7 +17,7 @@ project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
set(KF_MIN_VERSION "5.105.0")
set(QT_MIN_VERSION "5.15.2")
if (ANDROID)
set(QT_MIN_VERSION "5.15.8")
set(QT_MIN_VERSION "5.15.10")
endif()
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
@@ -145,14 +145,6 @@ 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})
@@ -186,3 +178,9 @@ file(GLOB_RECURSE ALL_SOURCE_FILES *.cpp *.h *.qml)
# Fixes the test by excluding this directory
list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX [[_(install|build)/.*]])
ecm_check_outbound_license(LICENSES GPL-3.0-only FILES ${ALL_SOURCE_FILES})
ecm_qt_install_logging_categories(
EXPORT NEOCHAT
FILE neochat.categories
DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}
)

View File

@@ -47,6 +47,7 @@ android {
compileSdkVersion androidCompileSdkVersion.toInteger()
buildToolsVersion androidBuildToolsVersion
ndkVersion androidNdkVersion
sourceSets {
main {

BIN
icons/300-apps-neochat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -67,7 +67,7 @@
<summary xml:lang="ka">კლიენტი Matrix-სთვის, დეცენტრალიზებული კომუნიკაციის პროტოკოლისთვის</summary>
<summary xml:lang="ko">Matrix, 분산 대화 프로토콜 클라이언트</summary>
<summary xml:lang="nl">Een client voor matrix, het gedecentraliseerde communicatieprotocol</summary>
<summary xml:lang="nn">Ein klient for Matrix, den desentraliserte lynmeldings­protokollen</summary>
<summary xml:lang="nn">Ein klient for Matrix  protokollen for desentralisert kommunikasjon</summary>
<summary xml:lang="pa">ਮੈਟਰਿਕਸ, ਸਰਬ-ਸਾਂਝੇ ਸੰਚਾਰ ਪਰੋਟੋਕਾਲ, ਲਈ ਕਲਾਈਂਟ ਹੈ</summary>
<summary xml:lang="pl">Program do obsługi matriksa, rozproszonego protokołu porozumiewania się</summary>
<summary xml:lang="pt">Um cliente para o Matrix, o protocolo de comunicação descentralizado</summary>
@@ -89,12 +89,14 @@ to provide a convergent experience across multiple platforms.</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="fi">NeoChat on asiakassovellus Matrixille, hajautetulle pikaviestinyhteyskäytännölle. Sillä voi lähettää teksti-, video- ja ääniviestejä perheelle, tutuille ja ystäville. Se käyttää KDE-kehystä ja erityisesti Kirigamia tuottaakseen mukautuvan monialustaisen käyttökokemuksen.</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="nn">NeoChat er ein klient for Matrix, ein protokoll for desentralisert kommunikasjon. Du kan utveksla tekst, lyd og videoar med kollegaar, vennar og familie. Programmet brukar KDE Frameworks og Kirigami for å gje ei brukarflate tilpassa ulike plattformer.</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>
<p xml:lang="pt">O NeoChat é um cliente do Matrix. O mesmo permite-lhe enviar mensagens de texto, ficheiros de vídeo e áudio para a sua família, colegas e amigos com o protocolo Matrix. Usa as plataformas do KDE, e principalmente o Kirigami, para oferecer uma experiência convergente entre várias plataformas.</p>
<p xml:lang="sl">Neochat je odjemalec za Matrix, decentralizirani komunikacijski protokol za takojšnje sporočanje. Omogoča vam pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek svoji družini, sodelavcem in prijateljem. Uporablja okvire ogrodje KDE frameworks in predvsem Kirigami za zagotavljanje konvergentne izkušnje na več platformah.</p>
@@ -107,12 +109,14 @@ to provide a convergent experience across multiple platforms.</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="fi">NeoChat pyrkii olemaan Matrix-määritelmän täysominaisuuksinen sovellus, joten se tukee kaikkea nykyisessä vakaassa määritelmässä muutamaa huomattavaa poikkeusta lukuun ottamatta (VoIP, säikeet ja jotkin piirteet päästä päähän -salauksessa). Joitakin pienempiäkin puutteita on Matrix-määritelmän jatkuvan kehityksen vuoksi, mutta lopputavoitteena on tarjota määritelmän täysi tuki.</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="nn">NeoChat har som mål å støtta all funksjonalitet i Matrix-spesifikasjonen. Førebels er alt i den gjeldande stabile spesifikasjonen støtta, med unntak av VoIP, trådar og nokre delar av ende-til-kryptering. Det finst òg andre småting som ikkje er støtta, sidan Matrix-spesifikasjon er i stadig endring, men målet er altså støtte for alt.</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>
@@ -125,12 +129,14 @@ to provide a convergent experience across multiple platforms.</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="fi">Matrix-määritelmän kehittyessä NeoChat tukee myös monia epävakaita ominaisuuksia. Tällä hetkellä näitä ovat:</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="nn">På grunn av måten Matrix-spesifikasjonen vert utvikla på, støttar NeoChat òg nokre uferdige funksjonar:</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>
@@ -144,12 +150,14 @@ to provide a convergent experience across multiple platforms.</p>
<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="fi">Kyselyt 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="nn">Avstemmingar  MSC3381</li>
<li xml:lang="pt">Inquéritos - MSC3381</li>
<li xml:lang="sl">Polls - MSC3381</li>
<li xml:lang="ta">வாக்கெடுப்புகள் - MSC3381</li>
@@ -162,12 +170,14 @@ to provide a convergent experience across multiple platforms.</p>
<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="fi">Tarrapakkaukset 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="nn">Klistremerke-pakkar  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>
@@ -180,12 +190,14 @@ to provide a convergent experience across multiple platforms.</p>
<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="fi">Sijaintitapahtumat 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="nn">Posisjonshendingar  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>
@@ -241,6 +253,32 @@ to provide a convergent experience across multiple platforms.</p>
<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>
<value key="KDE::windows_store::StoreLogo9x16">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/storelogo-720x1080.png</value>
<value key="KDE::windows_store::StoreLogoSquare">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/storelogo-1080x1080.png</value>
<value key="KDE::windows_store::Icon">https://invent.kde.org/network/neochat/-/raw/master/icons/300-apps-neochat.png</value>
<value key="KDE::windows_store::PromotionalArt16x9">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/promoimage-1920x1080.png</value>
<value key="KDE::windows_store::screenshots::1::image">https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Timeline.png</value>
<value key="KDE::windows_store::screenshots::1::caption">Main view with room list, chat, and room information</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ca">Vista principal amb la llista de sales, xats i informació de les sales</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ca-valencia">Vista principal amb la llista de sales, xats i informació de les sales</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="es">Vista principal con la lista de salas, chat e información de la sala</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ka">მთავარი ხედი სურათების სიით, ჩატით და ოთახის ინფორმაციით</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="nl">Hoofdweergave met lijst met rooms, chat en roominformatie</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="tr">Oda listesini, sohbet penceresini ve oda bilgisini gösteren ana görünüm</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="uk">Головна панель із списком кімнат, спілкуванням та даними щодо кімнати</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="x-test">xxMain view with room list, chat, and room informationxx</value>
<value key="KDE::windows_store::screenshots::2::image">https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Login.png</value>
<value key="KDE::windows_store::screenshots::2::caption">Login screen</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ca">Pantalla d'inici de sessió</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ca-valencia">Pantalla d'inici de sessió</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="es">Pantalla de inicio de sesión</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ka">შესვლის ეკრანი</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="nl">Aanmeldscherm</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="sl">Prijavni zaslon</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="tr">Oturum açma ekranı</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="uk">Вікно входу</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="x-test">xxLogin screenxx</value>
</custom>
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
<screenshots>
@@ -255,6 +293,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.3" date="2023-07-06"/>
<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">

View File

@@ -100,7 +100,7 @@ Comment[ka]=კლიენტი Matrix-ის პროტოკოლის
Comment[ko]=Matrix 프로토콜용 클라이언트
Comment[lt]=Matrix protokolo kliento programa
Comment[nl]=Client voor het Matrix-protocol
Comment[nn]=Lynmeldings­klient for Matrix-protokollen
Comment[nn]=Klient for Matrix-protokollen
Comment[pa]=ਮੈਟਰਿਕਸ ਪਰੋਟੋਕਾਲ ਲਈ ਕਲਾਈਂਟ ਹੈ
Comment[pl]=Program obsługi protokołu Matriksa
Comment[pt]=Cliente para o protocolo Matrix

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

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

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

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

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

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ add_library(neochat STATIC
models/userfiltermodel.cpp
models/publicroomlistmodel.cpp
models/userdirectorylistmodel.cpp
models/keywordnotificationrulemodel.cpp
models/pushrulemodel.cpp
models/emoticonfiltermodel.cpp
notificationsmanager.cpp
models/sortfilterroomlistmodel.cpp
@@ -36,6 +36,7 @@ add_library(neochat STATIC
blurhash.cpp
blurhashimageprovider.cpp
models/collapsestateproxymodel.cpp
models/mediamessagefiltermodel.cpp
urlhelper.cpp
windowcontroller.cpp
linkpreviewer.cpp
@@ -56,19 +57,18 @@ add_library(neochat STATIC
events/stickerevent.cpp
models/reactionmodel.cpp
delegatesizehelper.cpp
models/livelocationsmodel.cpp
models/locationsmodel.cpp
locationhelper.cpp
)
ecm_qt_declare_logging_category(neochat
HEADER "messageeventmodel_logging.h"
IDENTIFIER "MessageEvent"
CATEGORY_NAME "org.kde.neochat.messageeventmodel"
DESCRIPTION "Neochat: messageeventmodel"
DEFAULT_SEVERITY Info
)
ecm_qt_declare_logging_category(neochat
HEADER "voip_logging.h"
IDENTIFIER "voip"
CATEGORY_NAME "org.kde.neochat.voip"
EXPORT NEOCHAT
)
add_executable(neochat-app
@@ -117,20 +117,6 @@ 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)

View File

@@ -1,99 +0,0 @@
// 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;
}

View File

@@ -1,41 +0,0 @@
// 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;
};

View File

@@ -1,199 +0,0 @@
// 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

View File

@@ -1,64 +0,0 @@
// 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;
};

View File

@@ -1,617 +0,0 @@
// 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();
}

View File

@@ -1,159 +0,0 @@
// 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;
};

View File

@@ -1,51 +0,0 @@
// 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();
}

View File

@@ -1,30 +0,0 @@
// 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;
};
}

View File

@@ -1,26 +0,0 @@
// 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);
});
}

View File

@@ -1,36 +0,0 @@
// 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();
};

View File

@@ -1,916 +0,0 @@
// 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;
}

View File

@@ -1,113 +0,0 @@
// 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;
};

View File

@@ -1,165 +0,0 @@
// 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;
}

View File

@@ -1,59 +0,0 @@
// 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);
};

View File

@@ -1,142 +0,0 @@
// 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();
}

View File

@@ -1,51 +0,0 @@
// 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();
};

View File

@@ -3,6 +3,7 @@
// SPDX-License-Identifier: GPL-3.0-only
#include "controller.h"
#include "models/pushrulemodel.h"
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <qt5keychain/keychain.h>
@@ -126,6 +127,10 @@ Controller::Controller(QObject *parent)
oldAccountCount = Accounts.size();
});
#endif
QTimer::singleShot(0, this, [this] {
m_pushRuleModel = new PushRuleModel;
});
}
Controller &Controller::instance()
@@ -507,6 +512,11 @@ void Controller::setActiveConnection(Connection *connection)
Q_EMIT activeAccountLabelChanged();
}
PushRuleModel *Controller::pushRuleModel() const
{
return m_pushRuleModel;
}
void Controller::saveWindowGeometry()
{
WindowController::instance().saveGeometry();
@@ -736,12 +746,3 @@ QVariantList Controller::getSupportedRoomVersions(Quotient::Connection *connecti
return supportedRoomVersions;
}
bool Controller::callsSupported() const
{
#ifdef GSTREAMER_AVAILABLE
return true;
#else
return false;
#endif
}

View File

@@ -3,6 +3,7 @@
#pragma once
#include "models/pushrulemodel.h"
#include <QObject>
#include <QQuickItem>
@@ -50,6 +51,11 @@ class Controller : public QObject
*/
Q_PROPERTY(Quotient::Connection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
/**
* @brief The PushRuleModel that has the active connection's push rules.
*/
Q_PROPERTY(PushRuleModel *pushRuleModel READ pushRuleModel CONSTANT)
/**
* @brief The row number in the accounts directory of the active connection.
*/
@@ -100,7 +106,6 @@ 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:
/**
@@ -120,6 +125,8 @@ public:
void setActiveConnection(Quotient::Connection *connection);
[[nodiscard]] Quotient::Connection *activeConnection() const;
[[nodiscard]] PushRuleModel *pushRuleModel() const;
/**
* @brief Add a new connection to the account registry.
*/
@@ -198,7 +205,6 @@ public:
int quotientMinorVersion() const;
bool isFlatpak() const;
bool callsSupported() const;
/**
* @brief Return a string for the input timestamp.
@@ -238,6 +244,8 @@ private:
bool hasWindowSystem() const;
QPointer<PushRuleModel> m_pushRuleModel;
private Q_SLOTS:
void invokeLogin();
void showWindow();

44
src/locationhelper.cpp Normal file
View File

@@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "locationhelper.h"
#include <cmath>
QRectF LocationHelper::unite(const QRectF &r1, const QRectF &r2)
{
// this looks weird but is actually intentional as we need to handle point-like "rects" as well
if ((!r1.isEmpty() || r1.isNull()) && (!r2.isEmpty() || r2.isNull())) {
return r1 | r2;
}
return (!r1.isEmpty() || r1.isNull()) ? r1 : r2;
}
QPointF LocationHelper::center(const QRectF &r)
{
return r.center();
}
constexpr inline double degToRad(double deg)
{
return deg / 180.0 * M_PI;
}
static QPointF mercatorProject(double lat, double lon, double zoom)
{
const auto x = (256.0 / (2.0 * M_PI)) * std::pow(2.0, zoom) * (degToRad(lon) + M_PI);
const auto y = (256.0 / (2.0 * M_PI)) * std::pow(2.0, zoom) * (M_PI - std::log(std::tan(M_PI / 4.0 + degToRad(lat) / 2.0)));
return QPointF(x, y);
}
float LocationHelper::zoomToFit(const QRectF &r, float mapWidth, float mapHeight)
{
const auto p1 = mercatorProject(r.bottomLeft().y(), r.bottomLeft().x(), 1.0);
const auto p2 = mercatorProject(r.topRight().y(), r.topRight().x(), 1.0);
const auto zx = std::log2((mapWidth / (p2.x() - p1.x())));
const auto zy = std::log2((mapHeight / (p2.y() - p1.y())));
const auto z = std::min(zx, zy);
return std::clamp(z, 5.0, 18.0);
}

22
src/locationhelper.h Normal file
View File

@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "linkpreviewer.h"
#include <QMetaType>
#include <QRectF>
/** Location related helper functions for QML. */
class LocationHelper
{
Q_GADGET
public:
/** Unite two rectanlges. */
Q_INVOKABLE static QRectF unite(const QRectF &r1, const QRectF &r2);
/** Returns the center of @p r. */
Q_INVOKABLE static QPointF center(const QRectF &r);
/** Returns the highest zoom level to fit @r into a map of size @p mapWidth x @p mapHeight. */
Q_INVOKABLE static float zoomToFit(const QRectF &r, float mapWidth, float mapHeight);
};
Q_DECLARE_METATYPE(LocationHelper)

View File

@@ -105,7 +105,7 @@ void Login::init()
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
});
connectSingleShot(m_connection, &Connection::syncDone, this, [this]() {
connectSingleShot(m_connection, &Connection::syncDone, this, []() {
Q_EMIT Controller::instance().initiated();
});
}

View File

@@ -46,6 +46,7 @@
#include "delegatesizehelper.h"
#include "filetypesingleton.h"
#include "linkpreviewer.h"
#include "locationhelper.h"
#include "logger.h"
#include "login.h"
#include "matriximageprovider.h"
@@ -56,10 +57,13 @@
#include "models/emojimodel.h"
#include "models/emoticonfiltermodel.h"
#include "models/imagepacksmodel.h"
#include "models/keywordnotificationrulemodel.h"
#include "models/livelocationsmodel.h"
#include "models/locationsmodel.h"
#include "models/mediamessagefiltermodel.h"
#include "models/messageeventmodel.h"
#include "models/messagefiltermodel.h"
#include "models/publicroomlistmodel.h"
#include "models/pushrulemodel.h"
#include "models/reactionmodel.h"
#include "models/roomlistmodel.h"
#include "models/searchmodel.h"
@@ -86,8 +90,6 @@
#ifdef QUOTIENT_07
#include <keyverificationsession.h>
#endif
#include <room.h>
#ifdef HAVE_COLORSCHEME
#include "colorschemer.h"
#endif
@@ -95,14 +97,6 @@
#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>
@@ -193,22 +187,6 @@ 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, &micro, &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")));
@@ -260,6 +238,7 @@ int main(int argc, char *argv[])
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<MediaMessageFilterModel>("org.kde.neochat", 1, 0, "MediaMessageFilterModel");
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
qmlRegisterType<UserFilterModel>("org.kde.neochat", 1, 0, "UserFilterModel");
qmlRegisterType<PublicRoomListModel>("org.kde.neochat", 1, 0, "PublicRoomListModel");
@@ -273,26 +252,26 @@ int main(int argc, char *argv[])
qmlRegisterType<StateModel>("org.kde.neochat", 1, 0, "StateModel");
qmlRegisterType<StateFilterModel>("org.kde.neochat", 1, 0, "StateFilterModel");
qmlRegisterType<SearchModel>("org.kde.neochat", 1, 0, "SearchModel");
qmlRegisterType<LiveLocationsModel>("org.kde.neochat", 1, 0, "LiveLocationsModel");
qmlRegisterType<LocationsModel>("org.kde.neochat", 1, 0, "LocationsModel");
#ifdef QUOTIENT_07
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");
#endif
qmlRegisterType<KeywordNotificationRuleModel>("org.kde.neochat", 1, 0, "KeywordNotificationRuleModel");
qmlRegisterType<PushRuleModel>("org.kde.neochat", 1, 0, "PushRuleModel");
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<PushNotificationKind>("org.kde.neochat", 1, 0, "PushNotificationKind", "ENUM");
qmlRegisterUncreatableType<PushNotificationSection>("org.kde.neochat", 1, 0, "PushNotificationSection", "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*");
@@ -309,18 +288,14 @@ 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");
qmlRegisterSingletonType("org.kde.neochat", 1, 0, "LocationHelper", [](QQmlEngine *engine, QJSEngine *) -> QJSValue {
return engine->toScriptValue(LocationHelper());
});
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
qRegisterMetaTypeStreamOperators<Emoji>();

View File

@@ -1,58 +0,0 @@
// 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();
}

View File

@@ -1,38 +0,0 @@
// 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;
};

View File

@@ -27,6 +27,7 @@ public:
StateEventsRole, /**< List of state events in the aggregated state. */
AuthorListRole, /**< List of the first 5 unique authors of the aggregated state event. */
ExcessAuthorsRole, /**< The number of unique authors beyond the first 5. */
LastRole, // Keep this last
};
/**

View File

@@ -1,110 +0,0 @@
// SPDX-FileCopyrightText: 2022 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 "keywordnotificationrulemodel.h"
#include "controller.h"
#include "notificationsmanager.h"
#include <QDebug>
#include <connection.h>
#include <converters.h>
#include <csapi/definitions/push_ruleset.h>
#include <csapi/pushrules.h>
#include <jobs/basejob.h>
KeywordNotificationRuleModel::KeywordNotificationRuleModel(QObject *parent)
: QAbstractListModel(parent)
{
if (Controller::instance().activeConnection()) {
controllerConnectionChanged();
}
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, &KeywordNotificationRuleModel::controllerConnectionChanged);
}
void KeywordNotificationRuleModel::controllerConnectionChanged()
{
connect(Controller::instance().activeConnection(), &Quotient::Connection::accountDataChanged, this, &KeywordNotificationRuleModel::updateNotificationRules);
updateNotificationRules("m.push_rules");
}
void KeywordNotificationRuleModel::updateNotificationRules(const QString &type)
{
if (type != "m.push_rules") {
return;
}
const QJsonObject ruleDataJson = Controller::instance().activeConnection()->accountDataJson("m.push_rules");
const Quotient::PushRuleset ruleData = Quotient::fromJson<Quotient::PushRuleset>(ruleDataJson["global"].toObject());
const QVector<Quotient::PushRule> contentRules = ruleData.content;
beginResetModel();
m_notificationRules.clear();
for (const auto &i : contentRules) {
if (!m_notificationRules.contains(i.ruleId) && i.ruleId[0] != '.') {
m_notificationRules.append(i.ruleId);
}
}
endResetModel();
}
QVariant KeywordNotificationRuleModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
if (index.row() >= m_notificationRules.count()) {
qDebug() << "KeywordNotificationRuleModel, something's wrong: index.row() >= m_notificationRules.count()";
return {};
}
if (role == NameRole) {
return m_notificationRules.at(index.row());
}
return {};
}
int KeywordNotificationRuleModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_notificationRules.count();
}
void KeywordNotificationRuleModel::addKeyword(const QString &keyword)
{
if (m_notificationRules.count() == 0) {
NotificationsManager::instance().initializeKeywordNotificationAction();
}
const QVector<QVariant> actions = NotificationsManager::instance().getKeywordNotificationActions();
auto job = Controller::instance()
.activeConnection()
->callApi<Quotient::SetPushRuleJob>("global", "content", keyword, actions, "", "", QVector<Quotient::PushCondition>(), keyword);
connect(job, &Quotient::BaseJob::success, this, [this, keyword]() {
beginInsertRows(QModelIndex(), m_notificationRules.count(), m_notificationRules.count());
m_notificationRules.append(keyword);
endInsertRows();
});
}
void KeywordNotificationRuleModel::removeKeywordAtIndex(int index)
{
auto job = Controller::instance().activeConnection()->callApi<Quotient::DeletePushRuleJob>("global", "content", m_notificationRules[index]);
connect(job, &Quotient::BaseJob::success, this, [this, index]() {
beginRemoveRows(QModelIndex(), index, index);
m_notificationRules.removeAt(index);
endRemoveRows();
if (m_notificationRules.count() == 0) {
NotificationsManager::instance().deactivateKeywordNotificationAction();
}
});
}
QHash<int, QByteArray> KeywordNotificationRuleModel::roleNames() const
{
return {{NameRole, QByteArrayLiteral("name")}};
}

View File

@@ -1,66 +0,0 @@
// SPDX-FileCopyrightText: 2022 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 <csapi/definitions/push_rule.h>
#include <QAbstractListModel>
/**
* @class KeywordNotificationRuleModel
*
* This class defines the model for managing notification push rule keywords.
*/
class KeywordNotificationRuleModel : public QAbstractListModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum EventRoles {
NameRole = Qt::DisplayRole, /**< The push rule keyword. */
};
KeywordNotificationRuleModel(QObject *parent = 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 EventRoles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Add a new keyword to the model.
*/
Q_INVOKABLE void addKeyword(const QString &keyword);
/**
* @brief Remove a keyword from the model.
*/
Q_INVOKABLE void removeKeywordAtIndex(int index);
private Q_SLOTS:
void controllerConnectionChanged();
void updateNotificationRules(const QString &type);
private:
QList<QString> m_notificationRules;
};

View File

@@ -0,0 +1,171 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "livelocationsmodel.h"
#include <events/roommessageevent.h>
#include <QDebug>
using namespace Quotient;
bool operator<(const LiveLocationData &lhs, const LiveLocationData &rhs)
{
return lhs.eventId < rhs.eventId;
}
LiveLocationsModel::LiveLocationsModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(
this,
&LiveLocationsModel::roomChanged,
this,
[this]() {
for (const auto &event : m_room->messageEvents()) {
addEvent(event.get());
}
connect(m_room, &NeoChatRoom::aboutToAddHistoricalMessages, this, [this](const auto &events) {
for (const auto &event : events) {
addEvent(event.get());
}
});
connect(m_room, &NeoChatRoom::aboutToAddNewMessages, this, [this](const auto &events) {
for (const auto &event : events) {
addEvent(event.get());
}
});
},
Qt::QueuedConnection); // deferred so we are sure the eventId filter is set
connect(this, &LiveLocationsModel::dataChanged, this, &LiveLocationsModel::boundingBoxChanged);
connect(this, &LiveLocationsModel::rowsInserted, this, &LiveLocationsModel::boundingBoxChanged);
}
int LiveLocationsModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_locations.size();
}
QVariant LiveLocationsModel::data(const QModelIndex &index, int roleName) const
{
if (!checkIndex(index)) {
return {};
}
const auto &data = m_locations.at(index.row());
switch (roleName) {
case LatitudeRole: {
const auto geoUri = data.beacon["org.matrix.msc3488.location"_ls].toObject()["uri"_ls].toString();
if (geoUri.isEmpty()) {
return {};
}
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[0];
return latitude.toFloat();
}
case LongitudeRole: {
const auto geoUri = data.beacon["org.matrix.msc3488.location"_ls].toObject()["uri"_ls].toString();
if (geoUri.isEmpty()) {
return {};
}
const auto longitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[1];
return longitude.toFloat();
}
case AssetRole:
return data.beaconInfo["org.matrix.msc3488.asset"_ls].toObject()["type"].toString();
case AuthorRole:
return m_room->getUser(data.senderId);
case IsLiveRole: {
if (!data.beaconInfo["live"_ls].toBool()) {
return false;
}
// TODO Qt6: port to toInteger(), timestamps are in ms since epoch, ie. 64 bit values
const auto lastTs = std::max(data.beaconInfo.value("org.matrix.msc3488.ts"_ls).toDouble(), data.beacon.value("org.matrix.msc3488.ts"_ls).toDouble());
const auto timeout = data.beaconInfo.value("timeout"_ls).toDouble(600000);
return lastTs + timeout >= QDateTime::currentDateTime().toMSecsSinceEpoch();
}
}
return {};
}
QHash<int, QByteArray> LiveLocationsModel::roleNames() const
{
auto r = QAbstractListModel::roleNames();
r.insert(LatitudeRole, "latitude");
r.insert(LongitudeRole, "longitude");
r.insert(AssetRole, "asset");
r.insert(AuthorRole, "author");
r.insert(IsLiveRole, "isLive");
return r;
}
QRectF LiveLocationsModel::boundingBox() const
{
QRectF bbox(QPointF(180.0, 90.0), QPointF(-180.0, -90.0));
for (auto i = 0; i < rowCount(); ++i) {
const auto lat = data(index(i, 0), LatitudeRole).toDouble();
const auto lon = data(index(i, 0), LongitudeRole).toDouble();
bbox.setLeft(std::min(bbox.left(), lon));
bbox.setRight(std::max(bbox.right(), lon));
bbox.setTop(std::min(bbox.top(), lat));
bbox.setBottom(std::max(bbox.bottom(), lat));
}
return bbox;
}
void LiveLocationsModel::addEvent(const Quotient::RoomEvent *event)
{
if (event->isStateEvent() && event->matrixType() == "org.matrix.msc3672.beacon_info") {
LiveLocationData data;
data.senderId = event->senderId();
data.beaconInfo = event->contentJson();
if (event->contentJson()["live"_ls].toBool()) {
data.eventId = event->id();
} else {
data.eventId = event->fullJson()["replaces_state"_ls].toString();
}
updateLocationData(std::move(data));
}
if (event->matrixType() == "org.matrix.msc3672.beacon"_ls) {
LiveLocationData data;
data.eventId = event->contentJson()["m.relates_to"_ls].toObject()["event_id"_ls].toString();
data.senderId = event->senderId();
data.beacon = event->contentJson();
updateLocationData(std::move(data));
}
}
void LiveLocationsModel::updateLocationData(LiveLocationData &&data)
{
if (!m_eventId.isEmpty() && data.eventId != m_eventId) {
return;
}
auto it = std::lower_bound(m_locations.begin(), m_locations.end(), data);
if (it == m_locations.end() || it->eventId != data.eventId) {
const auto row = std::distance(m_locations.begin(), it);
beginInsertRows({}, row, row);
m_locations.insert(it, std::move(data));
endInsertRows();
return;
}
const auto idx = index(std::distance(m_locations.begin(), it), 0);
// TODO Qt6: port to toInteger(), timestamps are in ms since epoch, ie. 64 bit values
if (it->beacon.isEmpty() || it->beacon.value("org.matrix.msc3488.ts"_ls).toDouble() < data.beacon.value("org.matrix.msc3488.ts"_ls).toDouble()) {
it->beacon = std::move(data.beacon);
}
if (it->beaconInfo.isEmpty()
|| it->beaconInfo.value("org.matrix.msc3488.ts"_ls).toDouble() < data.beaconInfo.value("org.matrix.msc3488.ts"_ls).toDouble()) {
it->beaconInfo = std::move(data.beaconInfo);
}
Q_EMIT dataChanged(idx, idx);
}

View File

@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "neochatroom.h"
#include <QAbstractListModel>
#include <QPointer>
#include <QRectF>
namespace Quotient
{
class RoomMessageEvent;
}
struct LiveLocationData {
QString eventId;
QString senderId;
QJsonObject beaconInfo;
QJsonObject beacon;
};
bool operator<(const LiveLocationData &lhs, const LiveLocationData &rhs);
/** Accumulates live location beacon events in a given room
* and provides the last known state for one or more live location beacons.
*/
class LiveLocationsModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(NeoChatRoom *room MEMBER m_room NOTIFY roomChanged)
/** The event id of the beacon start event, ie. the one all suspequent
* events use to relate to the same beacon.
* If this is set only this specific beacon will be coverd by this model,
* if it is empty, all beacons in the room will be covered.
*/
Q_PROPERTY(QString eventId MEMBER m_eventId NOTIFY eventIdChanged)
/** Bounding box of all live location beacons covered by this model. */
Q_PROPERTY(QRectF boundingBox READ boundingBox NOTIFY boundingBoxChanged)
public:
explicit LiveLocationsModel(QObject *parent = nullptr);
enum Roles {
LatitudeRole, /**< Latest latitude of a live locaction beacon. */
LongitudeRole, /**< Latest longitude of a live locaction beacon. */
AssetRole, /**< Type of location event, e.g. self pin of the user location. */
AuthorRole, /**< The author of the event. */
IsLiveRole, /**< Boolean that indicates whether a live location beacon is still live. */
};
Q_ENUM(Roles)
int rowCount(const QModelIndex &parent = {}) const override;
QVariant data(const QModelIndex &index, int roleName) const override;
QHash<int, QByteArray> roleNames() const override;
QRectF boundingBox() const;
Q_SIGNALS:
void roomChanged();
void eventIdChanged();
void boundingBoxChanged();
private:
void addEvent(const Quotient::RoomEvent *event);
void updateLocationData(LiveLocationData &&data);
QPointer<NeoChatRoom> m_room;
QString m_eventId;
QList<LiveLocationData> m_locations;
};

View File

@@ -0,0 +1,129 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "locationsmodel.h"
using namespace Quotient;
LocationsModel::LocationsModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(this, &LocationsModel::roomChanged, this, [=]() {
for (const auto &event : m_room->messageEvents()) {
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"] == "m.location") {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
connect(m_room, &NeoChatRoom::aboutToAddHistoricalMessages, this, [=](const auto &events) {
for (const auto &event : events) {
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"] == "m.location") {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
});
connect(m_room, &NeoChatRoom::aboutToAddNewMessages, this, [=](const auto &events) {
for (const auto &event : events) {
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"] == "m.location") {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
});
});
connect(this, &LocationsModel::rowsInserted, this, &LocationsModel::boundingBoxChanged);
}
void LocationsModel::addLocation(const RoomMessageEvent *event)
{
const auto uri = event->contentJson()["org.matrix.msc3488.location"]["uri"].toString();
const auto parts = uri.mid(4).split(QLatin1Char(','));
if (parts.size() < 2) {
qWarning() << "invalid geo: URI" << uri;
return;
}
const auto latitude = parts[0].toFloat();
const auto longitude = parts[1].toFloat();
beginInsertRows(QModelIndex(), m_locations.size(), m_locations.size() + 1);
m_locations += LocationData{
.eventId = event->id(),
.latitude = latitude,
.longitude = longitude,
.content = event->contentJson(),
.author = dynamic_cast<NeoChatUser *>(m_room->user(event->senderId())),
};
endInsertRows();
}
NeoChatRoom *LocationsModel::room() const
{
return m_room;
}
void LocationsModel::setRoom(NeoChatRoom *room)
{
if (m_room) {
disconnect(this, nullptr, m_room, nullptr);
}
m_room = room;
Q_EMIT roomChanged();
}
QHash<int, QByteArray> LocationsModel::roleNames() const
{
return {
{LongitudeRole, "longitude"},
{LatitudeRole, "latitude"},
{TextRole, "text"},
{AssetRole, "asset"},
{AuthorRole, "author"},
};
}
QVariant LocationsModel::data(const QModelIndex &index, int roleName) const
{
auto row = index.row();
if (roleName == LongitudeRole) {
return m_locations[row].longitude;
} else if (roleName == LatitudeRole) {
return m_locations[row].latitude;
} else if (roleName == TextRole) {
return m_locations[row].content["body"_ls].toString();
} else if (roleName == AssetRole) {
return m_locations[row].content["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
} else if (roleName == AuthorRole) {
return m_room->getUser(m_locations[row].author);
}
return {};
}
int LocationsModel::rowCount(const QModelIndex &parent) const
{
return m_locations.size();
}
QRectF LocationsModel::boundingBox() const
{
QRectF bbox(QPointF(180.0, 90.0), QPointF(-180.0, -90.0));
for (auto i = 0; i < rowCount(); ++i) {
const auto lat = data(index(i, 0), LatitudeRole).toDouble();
const auto lon = data(index(i, 0), LongitudeRole).toDouble();
bbox.setLeft(std::min(bbox.left(), lon));
bbox.setRight(std::max(bbox.right(), lon));
bbox.setTop(std::min(bbox.top(), lat));
bbox.setBottom(std::max(bbox.bottom(), lat));
}
return bbox;
}

View File

@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QPointer>
#include <QRectF>
#include "neochatroom.h"
#include <events/roommessageevent.h>
class LocationsModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
TextRole = Qt::DisplayRole,
LongitudeRole,
LatitudeRole,
AssetRole,
AuthorRole,
};
Q_ENUM(Roles)
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/** Bounding box of all locations covered by this model. */
Q_PROPERTY(QRectF boundingBox READ boundingBox NOTIFY boundingBoxChanged)
explicit LocationsModel(QObject *parent = nullptr);
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
QRectF boundingBox() const;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] QVariant data(const QModelIndex &index, int roleName) const override;
[[nodiscard]] int rowCount(const QModelIndex &parent = {}) const override;
Q_SIGNALS:
void roomChanged();
void boundingBoxChanged();
private:
QPointer<NeoChatRoom> m_room;
struct LocationData {
QString eventId;
float latitude;
float longitude;
QJsonObject content;
NeoChatUser *author;
};
QList<LocationData> m_locations;
void addLocation(const Quotient::RoomMessageEvent *event);
};

View File

@@ -0,0 +1,79 @@
// 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-LGPL
#include "mediamessagefiltermodel.h"
#include "models/messageeventmodel.h"
#include <room.h>
MediaMessageFilterModel::MediaMessageFilterModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
}
bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
if (index.data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image
|| index.data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Video) {
return true;
}
return false;
}
QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
{
if (role == SourceRole) {
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()["source"].toUrl();
} else if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Video) {
auto progressInfo = mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>();
if (progressInfo.completed()) {
return mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>().localPath;
} else {
return QUrl();
}
} else {
return QUrl();
}
}
if (role == TempSourceRole) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()["tempInfo"].toMap()["source"].toUrl();
}
if (role == TypeRole) {
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image) {
return 0;
} else {
return 1;
}
}
if (role == CaptionRole) {
return mapToSource(index).data(Qt::DisplayRole);
}
if (role == SourceWidthRole) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()["width"].toFloat();
}
if (role == SourceHeightRole) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()["height"].toFloat();
}
return sourceModel()->data(mapToSource(index), role);
}
QHash<int, QByteArray> MediaMessageFilterModel::roleNames() const
{
auto roles = sourceModel()->roleNames();
roles[SourceRole] = "source";
roles[TempSourceRole] = "tempSource";
roles[TypeRole] = "type";
roles[CaptionRole] = "caption";
roles[SourceWidthRole] = "sourceWidth";
roles[SourceHeightRole] = "sourceHeight";
return roles;
}
int MediaMessageFilterModel::getRowForSourceItem(int sourceRow) const
{
return mapFromSource(sourceModel()->index(sourceRow, 0)).row();
}

View File

@@ -0,0 +1,56 @@
// 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-LGPL
#pragma once
#include <QSortFilterProxyModel>
#include "models/collapsestateproxymodel.h"
/**
* @class MediaMessageFilterModel
*
* This model filters a MessageEventModel for image and video messages.
*
* @sa MessageEventModel
*/
class MediaMessageFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
SourceRole = CollapseStateProxyModel::LastRole + 1, /**< The mxc source URL for the item. */
TempSourceRole, /**< Source for the temporary content (either blurhash or mxc URL). */
TypeRole, /**< The type of the media (image or video). */
CaptionRole, /**< The caption for the item. */
SourceWidthRole, /**< The width of the source item. */
SourceHeightRole, /**< The height of the source item. */
};
Q_ENUM(Roles)
MediaMessageFilterModel(QObject *parent = nullptr);
/**
* @brief Custom filter to show only image and video messages.
*/
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
/**
* @brief Get the given role value at the given index.
*
* @sa QSortFilterProxyModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractProxyModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE int getRowForSourceItem(int sourceRow) const;
};

View File

@@ -61,7 +61,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[ShowReadMarkersRole] = "showReadMarkers";
roles[ReactionRole] = "reaction";
roles[ShowReactionsRole] = "showReactions";
roles[SourceRole] = "source";
roles[SourceRole] = "jsonSource";
roles[MimeTypeRole] = "mimeType";
roles[AuthorIdRole] = "authorId";
roles[VerifiedRole] = "verified";
@@ -521,13 +521,13 @@ 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;
}
if (evt.isStateEvent()) {
if (evt.matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
return DelegateType::LiveLocation;
}
return DelegateType::State;
}
if (is<const EncryptedEvent>(evt)) {
@@ -638,6 +638,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return EventStatus::Hidden;
}
// hide ending live location beacons
if (evt.isStateEvent() && evt.matrixType() == "org.matrix.msc3672.beacon_info"_ls && !evt.contentJson()["live"_ls].toBool()) {
return EventStatus::Hidden;
}
return EventStatus::Normal;
}

View File

@@ -53,7 +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. */
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
Other, /**< Anything that cannot be classified as another type. */
};
Q_ENUM(DelegateType);

View File

@@ -0,0 +1,445 @@
// SPDX-FileCopyrightText: 2022 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 "pushrulemodel.h"
#include <QDebug>
#include <connection.h>
#include <converters.h>
#include <csapi/definitions/push_ruleset.h>
#include <csapi/pushrules.h>
#include <jobs/basejob.h>
#include <qobjectdefs.h>
#include "controller.h"
#include "neochatconfig.h"
#include "notificationsmanager.h"
// Alternate name text for default rules.
static const QHash<QString, QString> defaultRuleNames = {
{QStringLiteral(".m.rule.master"), QStringLiteral("Enable notifications for this account")},
{QStringLiteral(".m.rule.room_one_to_one"), QStringLiteral("Messages in one-to-one chats")},
{QStringLiteral(".m.rule.encrypted_room_one_to_one"), QStringLiteral("Encrypted messages in one-to-one chats")},
{QStringLiteral(".m.rule.message"), QStringLiteral("Messages in group chats")},
{QStringLiteral(".m.rule.encrypted"), QStringLiteral("Messages in encrypted group chats")},
{QStringLiteral(".m.rule.tombstone"), QStringLiteral("Room upgrade messages")},
{QStringLiteral(".m.rule.contains_display_name"), QStringLiteral("Messages containing my display name")},
{QStringLiteral(".m.rule.roomnotif"), QStringLiteral("Whole room (@room) notifications")},
{QStringLiteral(".m.rule.invite_for_me"), QStringLiteral("Invites to a room")},
{QStringLiteral(".m.rule.call"), QStringLiteral("Call invitation")},
};
// Sections for default rules.
static const QHash<QString, PushNotificationSection::Section> defaultSections = {
{QStringLiteral(".m.rule.master"), PushNotificationSection::Master},
{QStringLiteral(".m.rule.room_one_to_one"), PushNotificationSection::Room},
{QStringLiteral(".m.rule.encrypted_room_one_to_one"), PushNotificationSection::Room},
{QStringLiteral(".m.rule.message"), PushNotificationSection::Room},
{QStringLiteral(".m.rule.encrypted"), PushNotificationSection::Room},
{QStringLiteral(".m.rule.tombstone"), PushNotificationSection::Room},
{QStringLiteral(".m.rule.contains_display_name"), PushNotificationSection::Mentions},
{QStringLiteral(".m.rule.roomnotif"), PushNotificationSection::Mentions},
{QStringLiteral(".m.rule.invite_for_me"), PushNotificationSection::Invites},
{QStringLiteral(".m.rule.call"), PushNotificationSection::Undefined}, // TODO: make invites when VOIP added.
{QStringLiteral(".m.rule.suppress_notices"), PushNotificationSection::Undefined},
{QStringLiteral(".m.rule.member_event"), PushNotificationSection::Undefined},
{QStringLiteral(".m.rule.reaction"), PushNotificationSection::Undefined},
{QStringLiteral(".m.rule.room.server_acl"), PushNotificationSection::Undefined},
{QStringLiteral(".im.vector.jitsi"), PushNotificationSection::Undefined},
};
// Default rules that don't have a highlight option as it would lead to all messages
// in a room being highlighted.
static const QStringList noHighlight = {
QStringLiteral(".m.rule.room_one_to_one"),
QStringLiteral(".m.rule.encrypted_room_one_to_one"),
QStringLiteral(".m.rule.message"),
QStringLiteral(".m.rule.encrypted"),
};
PushRuleModel::PushRuleModel(QObject *parent)
: QAbstractListModel(parent)
{
m_defaultKeywordAction = static_cast<PushNotificationAction::Action>(NeoChatConfig::self()->keywordPushRuleDefault());
if (Controller::instance().activeConnection()) {
controllerConnectionChanged();
}
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, &PushRuleModel::controllerConnectionChanged);
}
void PushRuleModel::controllerConnectionChanged()
{
connect(Controller::instance().activeConnection(), &Quotient::Connection::accountDataChanged, this, &PushRuleModel::updateNotificationRules);
updateNotificationRules("m.push_rules");
}
void PushRuleModel::updateNotificationRules(const QString &type)
{
if (type != "m.push_rules") {
return;
}
const QJsonObject ruleDataJson = Controller::instance().activeConnection()->accountDataJson("m.push_rules");
const Quotient::PushRuleset ruleData = Quotient::fromJson<Quotient::PushRuleset>(ruleDataJson["global"].toObject());
beginResetModel();
m_rules.clear();
// Doing this 5 times because PushRuleset is a struct.
setRules(ruleData.override, PushNotificationKind::Override);
setRules(ruleData.content, PushNotificationKind::Content);
setRules(ruleData.room, PushNotificationKind::Room);
setRules(ruleData.sender, PushNotificationKind::Sender);
setRules(ruleData.underride, PushNotificationKind::Underride);
Q_EMIT globalNotificationsEnabledChanged();
Q_EMIT globalNotificationsSetChanged();
endResetModel();
}
void PushRuleModel::setRules(QVector<Quotient::PushRule> rules, PushNotificationKind::Kind kind)
{
for (const auto &rule : rules) {
QString roomId;
if (rule.conditions.size() > 0) {
for (const auto &condition : rule.conditions) {
if (condition.key == QStringLiteral("room_id")) {
roomId = condition.pattern;
}
}
}
m_rules.append(Rule{
rule.ruleId,
kind,
variantToAction(rule.actions, rule.enabled),
getSection(rule),
rule.enabled,
roomId,
});
}
}
int PushRuleModel::getRuleIndex(const QString &ruleId) const
{
for (auto i = 0; i < m_rules.count(); i++) {
if (m_rules[i].id == ruleId) {
return i;
}
}
return -1;
}
PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule rule)
{
auto ruleId = rule.ruleId;
if (defaultSections.contains(ruleId)) {
return defaultSections.value(ruleId);
} else {
/**
* If the rule name resolves to a matrix id for a room that the user is part
* of it shouldn't appear in the global list as it's overriding the global
* state for that room.
*
* Rooms that the user hasn't joined shouldn't have a rule.
*/
auto connection = Controller::instance().activeConnection();
if (connection->room(ruleId) != nullptr) {
return PushNotificationSection::Undefined;
}
/**
* If the rule name resolves to a matrix id for a user it shouldn't appear
* in the global list as it's a rule to block notifications from a user and
* is handled elsewhere.
*/
auto testUserId = ruleId;
// Rules for user matrix IDs often don't have the @ on the beginning so add
// if not there to avoid malformed ID.
if (!testUserId.startsWith(u'@')) {
testUserId.prepend(u'@');
}
if (connection->user(testUserId) != nullptr) {
return PushNotificationSection::Undefined;
}
// If the rule has push conditions and one is a room ID it is a room only keyword.
if (!rule.conditions.isEmpty()) {
for (auto condition : rule.conditions) {
if (condition.key == QStringLiteral("room_id")) {
return PushNotificationSection::RoomKeywords;
}
}
}
return PushNotificationSection::Keywords;
}
}
PushNotificationAction::Action PushRuleModel::defaultState() const
{
return m_defaultKeywordAction;
}
void PushRuleModel::setDefaultState(PushNotificationAction::Action defaultState)
{
if (defaultState == m_defaultKeywordAction) {
return;
}
m_defaultKeywordAction = defaultState;
NeoChatConfig::setKeywordPushRuleDefault(m_defaultKeywordAction);
Q_EMIT defaultStateChanged();
}
bool PushRuleModel::globalNotificationsEnabled() const
{
auto masterIndex = getRuleIndex(QStringLiteral(".m.rule.master"));
if (masterIndex > -1) {
return !m_rules[masterIndex].enabled;
}
return false;
}
void PushRuleModel::setGlobalNotificationsEnabled(bool enabled)
{
setNotificationRuleEnabled("override", ".m.rule.master", !enabled);
}
bool PushRuleModel::globalNotificationsSet() const
{
return getRuleIndex(QStringLiteral(".m.rule.master")) > -1;
}
QVariant PushRuleModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
if (index.row() >= rowCount()) {
qDebug() << "PushRuleModel, something's wrong: index.row() >= m_rules.count()";
return {};
}
if (role == NameRole) {
auto ruleId = m_rules.at(index.row()).id;
if (defaultRuleNames.contains(ruleId)) {
return defaultRuleNames.value(ruleId);
} else {
return ruleId;
}
}
if (role == IdRole) {
return m_rules.at(index.row()).id;
}
if (role == KindRole) {
return m_rules.at(index.row()).kind;
}
if (role == ActionRole) {
return m_rules.at(index.row()).action;
}
if (role == HighlightableRole) {
return !noHighlight.contains(m_rules.at(index.row()).id);
}
if (role == DeletableRole) {
return !m_rules.at(index.row()).id.startsWith(QStringLiteral("."));
}
if (role == SectionRole) {
return m_rules.at(index.row()).section;
}
if (role == RoomIdRole) {
return m_rules.at(index.row()).roomId;
}
return {};
}
int PushRuleModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_rules.count();
}
QHash<int, QByteArray> PushRuleModel::roleNames() const
{
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
roles[NameRole] = "name";
roles[IdRole] = "id";
roles[KindRole] = "kind";
roles[ActionRole] = "ruleAction";
roles[HighlightableRole] = "highlightable";
roles[DeletableRole] = "deletable";
roles[SectionRole] = "section";
roles[RoomIdRole] = "roomId";
return roles;
}
void PushRuleModel::setPushRuleAction(const QString &id, PushNotificationAction::Action action)
{
int index = getRuleIndex(id);
if (index == -1) {
return;
}
auto rule = m_rules[index];
// Override rules need to be disabled when off so that other rules can match the message if they apply.
if (action == PushNotificationAction::Off && rule.kind == PushNotificationKind::Override) {
setNotificationRuleEnabled(PushNotificationKind::kindString(rule.kind), rule.id, false);
} else if (rule.kind == PushNotificationKind::Override) {
setNotificationRuleEnabled(PushNotificationKind::kindString(rule.kind), rule.id, true);
}
setNotificationRuleActions(PushNotificationKind::kindString(rule.kind), rule.id, action);
}
void PushRuleModel::addKeyword(const QString &keyword, const QString &roomId)
{
PushNotificationKind::Kind kind = PushNotificationKind::Content;
const QVector<QVariant> actions = actionToVariant(m_defaultKeywordAction);
QVector<Quotient::PushCondition> pushConditions;
if (!roomId.isEmpty()) {
kind = PushNotificationKind::Override;
Quotient::PushCondition roomCondition;
roomCondition.kind = "event_match";
roomCondition.key = "room_id";
roomCondition.pattern = roomId;
pushConditions.append(roomCondition);
Quotient::PushCondition keywordCondition;
keywordCondition.kind = "event_match";
keywordCondition.key = "content.body";
keywordCondition.pattern = keyword;
pushConditions.append(keywordCondition);
}
auto job = Controller::instance().activeConnection()->callApi<Quotient::SetPushRuleJob>("global",
PushNotificationKind::kindString(kind),
keyword,
actions,
QLatin1String(""),
QLatin1String(""),
pushConditions,
roomId.isEmpty() ? keyword : QLatin1String(""));
connect(job, &Quotient::BaseJob::failure, this, [job, keyword]() {
qWarning() << QLatin1String("Unable to set push rule for keyword %1: ").arg(keyword) << job->errorString();
});
}
/**
* The rule never being removed from the list by this function is intentional. When
* the server is updated the new push rule account data will be synced and it will
* be removed when the model is updated then.
*/
void PushRuleModel::removeKeyword(const QString &keyword)
{
int index = getRuleIndex(keyword);
if (index == -1) {
return;
}
auto kind = PushNotificationKind::kindString(m_rules[index].kind);
auto job = Controller::instance().activeConnection()->callApi<Quotient::DeletePushRuleJob>("global", kind, m_rules[index].id);
connect(job, &Quotient::BaseJob::failure, this, [this, job, index]() {
qWarning() << QLatin1String("Unable to remove push rule for keyword %1: ").arg(m_rules[index].id) << job->errorString();
});
}
void PushRuleModel::setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled)
{
auto job = Controller::instance().activeConnection()->callApi<Quotient::IsPushRuleEnabledJob>("global", kind, ruleId);
connect(job, &Quotient::BaseJob::success, this, [job, kind, ruleId, enabled]() {
if (job->enabled() != enabled) {
Controller::instance().activeConnection()->callApi<Quotient::SetPushRuleEnabledJob>("global", kind, ruleId, enabled);
}
});
}
void PushRuleModel::setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action)
{
QVector<QVariant> actions;
if (ruleId == ".m.rule.call") {
actions = actionToVariant(action, "ring");
} else {
actions = actionToVariant(action);
}
Controller::instance().activeConnection()->callApi<Quotient::SetPushRuleActionsJob>("global", kind, ruleId, actions);
}
PushNotificationAction::Action PushRuleModel::variantToAction(const QVector<QVariant> &actions, bool enabled)
{
bool notify = false;
bool isNoisy = false;
bool highlightEnabled = false;
for (const auto &i : actions) {
auto actionString = i.toString();
if (!actionString.isEmpty()) {
if (actionString == QLatin1String("notify")) {
notify = true;
}
continue;
}
QJsonObject action = i.toJsonObject();
if (action["set_tweak"].toString() == "sound") {
isNoisy = true;
} else if (action["set_tweak"].toString() == "highlight") {
if (action["value"].toString() != "false") {
highlightEnabled = true;
}
}
}
if (!enabled) {
return PushNotificationAction::Off;
}
if (notify) {
if (isNoisy && highlightEnabled) {
return PushNotificationAction::NoisyHighlight;
} else if (isNoisy) {
return PushNotificationAction::Noisy;
} else if (highlightEnabled) {
return PushNotificationAction::Highlight;
} else {
return PushNotificationAction::On;
}
} else {
return PushNotificationAction::Off;
}
}
QVector<QVariant> PushRuleModel::actionToVariant(PushNotificationAction::Action action, const QString &sound)
{
// The caller should never try to set the state to unknown.
// It exists only as a default state to diable the settings options until the actual state is retrieved from the server.
if (action == PushNotificationAction::Unknown) {
Q_ASSERT(false);
return QVector<QVariant>();
}
QVector<QVariant> actions;
if (action != PushNotificationAction::Off) {
actions.append("notify");
} else {
actions.append("dont_notify");
}
if (action == PushNotificationAction::Noisy || action == PushNotificationAction::NoisyHighlight) {
QJsonObject soundTweak;
soundTweak.insert("set_tweak", "sound");
soundTweak.insert("value", sound);
actions.append(soundTweak);
}
if (action == PushNotificationAction::Highlight || action == PushNotificationAction::NoisyHighlight) {
QJsonObject highlightTweak;
highlightTweak.insert("set_tweak", "highlight");
actions.append(highlightTweak);
}
return actions;
}

241
src/models/pushrulemodel.h Normal file
View File

@@ -0,0 +1,241 @@
// SPDX-FileCopyrightText: 2022 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>
#include <csapi/definitions/push_rule.h>
#include "notificationsmanager.h"
/**
* @class PushNotificationKind
*
* A class with the Kind enum for push notifications and helper functions.
*
* The kind relates to the kinds of push rule definied in the matrix spec, see
* https://spec.matrix.org/v1.7/client-server-api/#push-rules for full details.
*/
class PushNotificationKind : public QObject
{
Q_OBJECT
public:
/**
* @brief Defines the different kinds of push rule.
*/
enum Kind {
Override = 0, /**< The highest priority rules. */
Content, /**< These configure behaviour for messages that match certain patterns. */
Room, /**< These rules change the behaviour of all messages for a given room. */
Sender, /**< These rules configure notification behaviour for messages from a specific Matrix user ID. */
Underride, /**< These are identical to override rules, but have a lower priority than content, room and sender rules. */
};
Q_ENUM(Kind);
/**
* @brief Translate the Kind enum value to a human readable string.
*
* @sa Kind
*/
static QString kindString(Kind kind)
{
switch (kind) {
case Kind::Override:
return QLatin1String("override");
case Kind::Content:
return QLatin1String("content");
case Kind::Room:
return QLatin1String("room");
case Kind::Sender:
return QLatin1String("sender");
case Kind::Underride:
return QLatin1String("underride");
default:
return {};
}
};
};
/**
* @class PushNotificationSection
*
* A class with the Section enum for push notifications and helper functions.
*
* @note This is different from the PushNotificationKind and instead is used for sorting
* in the settings page which is not necessarily by Kind.
*
* @sa PushNotificationKind
*/
class PushNotificationSection : public QObject
{
Q_OBJECT
public:
/**
* @brief Defines the sections to sort push rules into.
*/
enum Section {
Master = 0, /**< The master push rule */
Room, /**< Push rules relating to all rooms. */
Mentions, /**< Push rules relating to user mentions. */
Keywords, /**< Global Keyword push rules. */
RoomKeywords, /**< Keyword push rules that only apply to a specific room. */
Invites, /**< Push rules relating to invites. */
/**
* @brief Push rules that should never be shown.
*
* There are numerous rules that get set that shouldn't be shown in the general
* list e.g. The array of rules used to override global settings in individual
* rooms.
*/
Undefined,
};
Q_ENUM(Section);
/**
* @brief Translate the Section enum value to a human readable string.
*
* @sa Section
*/
static QString sectionString(Section section)
{
switch (section) {
case Section::Master:
return QLatin1String("Master");
case Section::Room:
return QLatin1String("Room Notifications");
case Section::Mentions:
return QLatin1String("@Mentions");
case Section::Keywords:
return QLatin1String("Keywords");
case Section::Invites:
return QLatin1String("Invites");
default:
return {};
}
};
};
/**
* @class PushRuleModel
*
* This class defines the model for managing notification push rule keywords.
*/
class PushRuleModel : public QAbstractListModel
{
Q_OBJECT
/**
* @brief The default state for any newly created keyword rule.
*/
Q_PROPERTY(PushNotificationAction::Action defaultState READ defaultState WRITE setDefaultState NOTIFY defaultStateChanged)
/**
* @brief The global notification state.
*
* If this rule is set to off all push notifications are disabled regardless
* of other settings.
*/
Q_PROPERTY(bool globalNotificationsEnabled READ globalNotificationsEnabled WRITE setGlobalNotificationsEnabled NOTIFY globalNotificationsEnabledChanged)
/**
* @brief Whether the global notification state has been retrieved from the server.
*
* @sa globalNotificationsEnabled, PushNotificationAction::Action
*/
Q_PROPERTY(bool globalNotificationsSet READ globalNotificationsSet NOTIFY globalNotificationsSetChanged)
public:
struct Rule {
QString id;
PushNotificationKind::Kind kind;
PushNotificationAction::Action action;
PushNotificationSection::Section section;
bool enabled;
QString roomId;
};
/**
* @brief Defines the model roles.
*/
enum EventRoles {
NameRole = Qt::DisplayRole, /**< The push rule name. */
IdRole, /**< The push rule ID. */
KindRole, /**< The kind of notification rule; override, content, etc. */
ActionRole, /**< The PushNotificationAction for the rule. */
HighlightableRole, /**< Whether the rule can have a highlight action. */
DeletableRole, /**< Whether the rule can be deleted the rule. */
SectionRole, /**< The section to sort into in the settings page. */
RoomIdRole, /**< The room the rule applies to (blank if global). */
};
Q_ENUM(EventRoles)
PushRuleModel(QObject *parent = nullptr);
[[nodiscard]] PushNotificationAction::Action defaultState() const;
void setDefaultState(PushNotificationAction::Action defaultState);
[[nodiscard]] bool globalNotificationsEnabled() const;
void setGlobalNotificationsEnabled(bool enabled);
[[nodiscard]] bool globalNotificationsSet() const;
/**
* @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 EventRoles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE void setPushRuleAction(const QString &id, PushNotificationAction::Action action);
/**
* @brief Add a new keyword to the model.
*/
Q_INVOKABLE void addKeyword(const QString &keyword, const QString &roomId = {});
/**
* @brief Remove a keyword from the model.
*/
Q_INVOKABLE void removeKeyword(const QString &keyword);
Q_SIGNALS:
void defaultStateChanged();
void globalNotificationsEnabledChanged();
void globalNotificationsSetChanged();
private Q_SLOTS:
void controllerConnectionChanged();
void updateNotificationRules(const QString &type);
private:
PushNotificationAction::Action m_defaultKeywordAction;
QList<Rule> m_rules;
void setRules(QVector<Quotient::PushRule> rules, PushNotificationKind::Kind kind);
int getRuleIndex(const QString &ruleId) const;
PushNotificationSection::Section getSection(Quotient::PushRule rule);
void setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled);
void setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action);
PushNotificationAction::Action variantToAction(const QVector<QVariant> &actions, bool enabled);
QVector<QVariant> actionToVariant(PushNotificationAction::Action action, const QString &sound = "default");
};
Q_DECLARE_METATYPE(PushRuleModel *)

View File

@@ -234,23 +234,12 @@ void RoomListModel::handleNotifications()
} else {
avatar_image = room->avatar(128);
}
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);
}
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);
}
}
});

View File

@@ -45,6 +45,7 @@ public:
PowerLevelRole, /**< The user's power level in the current room. */
PowerLevelStringRole, /**< The name of the user's power level in the current room. */
};
Q_ENUM(EventRoles)
UserListModel(QObject *parent = nullptr);

View File

@@ -62,7 +62,7 @@ Comment[ka]=კლიენტი Matrix-სთვის, დეცენტრ
Comment[ko]=Matrix, 분산 대화 프로토콜 클라이언트
Comment[lt]=Matrix decentralizuoto bendravimo protokolo kliento programa
Comment[nl]=Een client voor matrix, het gedecentraliseerde communicatieprotocol
Comment[nn]=Klient for Matrix, den desentraliserte lynmeldings­protokollen.
Comment[nn]=Ein klient for Matrix  protokollen for desentralisert kommunikasjon
Comment[pa]=ਮੈਟਰਿਕਸ, ਸਰਬ-ਸਾਂਝੇ ਸੰਚਾਰ ਪਰੋਟੋਕਾਲ, ਲਈ ਕਲਾਈਂਟ ਹੈ
Comment[pl]=Program do obsługi matriksa, rozproszonego protokołu porozumiewania się
Comment[pt]=Um cliente para o Matrix, o protocolo descentralizado de comunicações
@@ -178,6 +178,7 @@ Name[it]=Nuovo invito
Name[ka]=ახალი მოსაწვევი
Name[ko]=새 초대장
Name[nl]=Nieuwe uitnodiging
Name[nn]=Ny invitasjon
Name[pa]=ਨਵਾਂ ਸੱਦਾ
Name[pl]=Nowe zaproszenie
Name[pt]=Novo Convite
@@ -210,6 +211,7 @@ Comment[it]=È presente un nuovo invito a una stanza
Comment[ka]=გაქვთ ახალი ოთახის მოსაწვევი
Comment[ko]=새로운 대화방 초대장을 받음
Comment[nl]=Er is een nieuwe uitnodiging naar een room
Comment[nn]=Du har ein ny invitasjon til eit rom
Comment[pa]=ਰੂਮ ਲਈ ਨਵਾਂ ਸੱਦਾ ਹੈ
Comment[pl]=Dostępna jest nowe zaproszenie do pokoju
Comment[pt]=Existe um novo convite para uma sala

View File

@@ -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 {};

View File

@@ -54,6 +54,10 @@
<entry name="LastSaveDirectory" type="String">
<label>Directory last used for saving a file</label>
</entry>
<entry name="KeywordPushRuleDefault" type="int">
<label>The default setting for a new keyword push rule.</label>
<default>4</default>
</entry>
</group>
<group name="Timeline">
<entry name="ShowAvatarInTimeline" type="bool">
@@ -147,23 +151,5 @@
<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>

View File

@@ -42,9 +42,6 @@
#endif
#include <qt_connection_util.h>
#ifdef GSTREAMER_AVAILABLE
#include "call/callmanager.h"
#endif
#include "controller.h"
#include "events/joinrulesevent.h"
#include "neochatconfig.h"
@@ -125,13 +122,6 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
Q_EMIT canEncryptRoomChanged();
});
connect(connection, &Connection::capabilitiesLoaded, this, &NeoChatRoom::maxRoomVersionChanged);
#ifdef GSTREAMER_AVAILABLE
connect(this, &Room::callEvent, this, [=](Room *room, const RoomEvent *event) {
CallManager::instance().handleCallEvent(static_cast<NeoChatRoom *>(room), event);
});
#endif
connect(this, &Room::changed, this, [this]() {
Q_EMIT defaultUrlPreviewStateChanged();
});
@@ -706,6 +696,9 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
}
return i18nc("[User] configured <name> widget", "configured %1 widget", e.contentJson()["name"].toString());
}
if (e.matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
return e.contentJson()["description"_ls].toString();
}
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
: i18n("updated %1 state for %2", e.matrixType(), e.stateKey().toHtmlEscaped());
},
@@ -1687,6 +1680,7 @@ void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
m_currentPushNotificationState = state;
Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
}
void NeoChatRoom::updatePushNotificationState(QString type)

View File

@@ -22,9 +22,6 @@
#include <csapi/pushrules.h>
#include <jobs/basejob.h>
#include <user.h>
#ifdef GSTREAMER_AVAILABLE
#include "call/callmanager.h"
#endif
#include "controller.h"
#include "neochatconfig.h"
@@ -44,12 +41,6 @@ NotificationsManager &NotificationsManager::instance()
NotificationsManager::NotificationsManager(QObject *parent)
: QObject(parent)
{
// Can't connect the signal up until the active connection has been established by the controller
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this]() {
connect(Controller::instance().activeConnection(), &Connection::accountDataChanged, this, &NotificationsManager::updateNotificationRules);
// Ensure that the push rule states are retrieved after the connection is changed
updateNotificationRules("m.push_rules");
});
}
#ifdef QUOTIENT_07
@@ -303,341 +294,3 @@ void NotificationsManager::clearInvitationNotification(const QString &roomId)
m_invitations[roomId]->close();
}
}
/**
* The master push rule sets all notifications to off when enabled
* see https://spec.matrix.org/v1.3/client-server-api/#default-override-rules
* therefore to enable push rules the master rule needs to be disabled and vice versa
*/
void NotificationsManager::setGlobalNotificationsEnabled(bool enabled)
{
setNotificationRuleEnabled("override", ".m.rule.master", !enabled);
}
void NotificationsManager::setOneToOneNotificationAction(PushNotificationAction::Action action)
{
setNotificationRuleActions("underride", ".m.rule.room_one_to_one", action);
}
void NotificationsManager::setEncryptedOneToOneNotificationAction(PushNotificationAction::Action action)
{
setNotificationRuleActions("underride", ".m.rule.encrypted_room_one_to_one", action);
}
void NotificationsManager::setGroupChatNotificationAction(PushNotificationAction::Action action)
{
setNotificationRuleActions("underride", ".m.rule.message", action);
}
void NotificationsManager::setEncryptedGroupChatNotificationAction(PushNotificationAction::Action action)
{
setNotificationRuleActions("underride", ".m.rule.encrypted", action);
}
/*
* .m.rule.contains_display_name is an override rule so it needs to be disabled when off
* so that other rules can match the message if they apply.
*/
void NotificationsManager::setDisplayNameNotificationAction(PushNotificationAction::Action action)
{
if (action == PushNotificationAction::Off) {
setNotificationRuleEnabled("override", ".m.rule.contains_display_name", false);
} else {
setNotificationRuleActions("override", ".m.rule.contains_display_name", action);
setNotificationRuleEnabled("override", ".m.rule.contains_display_name", true);
}
}
/*
* .m.rule.roomnotif is an override rule so it needs to be disabled when off
* so that other rules can match the message if they apply.
*/
void NotificationsManager::setRoomNotificationAction(PushNotificationAction::Action action)
{
if (action == PushNotificationAction::Off) {
setNotificationRuleEnabled("override", ".m.rule.roomnotif", false);
} else {
setNotificationRuleActions("override", ".m.rule.roomnotif", action);
setNotificationRuleEnabled("override", ".m.rule.roomnotif", true);
}
}
void NotificationsManager::initializeKeywordNotificationAction()
{
m_keywordNotificationAction = PushNotificationAction::Highlight;
Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction);
}
void NotificationsManager::deactivateKeywordNotificationAction()
{
m_keywordNotificationAction = PushNotificationAction::Off;
Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction);
}
QVector<QVariant> NotificationsManager::getKeywordNotificationActions()
{
return toActions(m_keywordNotificationAction);
}
void NotificationsManager::setKeywordNotificationAction(PushNotificationAction::Action action)
{
// Unlike the other rules this needs to be set here for the case where there are no keyords.
m_keywordNotificationAction = action;
Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction);
const QJsonObject accountData = Controller::instance().activeConnection()->accountDataJson("m.push_rules");
const QJsonArray contentRuleArray = accountData["global"].toObject()["content"].toArray();
for (const auto &i : contentRuleArray) {
const QJsonObject contentRule = i.toObject();
if (contentRule["rule_id"].toString()[0] != '.') {
setNotificationRuleActions("content", contentRule["rule_id"].toString(), action);
}
}
}
/*
* .m.rule.invite_for_me is an override rule so it needs to be disabled when off
* so that other rules can match the message if they apply.
*/
void NotificationsManager::setInviteNotificationAction(PushNotificationAction::Action action)
{
if (action == PushNotificationAction::Off) {
setNotificationRuleEnabled("override", ".m.rule.invite_for_me", false);
} else {
setNotificationRuleActions("override", ".m.rule.invite_for_me", action);
setNotificationRuleEnabled("override", ".m.rule.invite_for_me", true);
}
}
void NotificationsManager::setCallInviteNotificationAction(PushNotificationAction::Action action)
{
setNotificationRuleActions("underride", ".m.rule.call", action);
}
/*
* .m.rule.tombstone is an override rule so it needs to be disabled when off
* so that other rules can match the message if they apply.
*/
void NotificationsManager::setTombstoneNotificationAction(PushNotificationAction::Action action)
{
if (action == PushNotificationAction::Off) {
setNotificationRuleEnabled("override", ".m.rule.tombstone", false);
} else {
setNotificationRuleActions("override", ".m.rule.tombstone", action);
setNotificationRuleEnabled("override", ".m.rule.tombstone", true);
}
}
void NotificationsManager::updateNotificationRules(const QString &type)
{
if (type != "m.push_rules") {
return;
}
if (!Controller::instance().activeConnection()) {
return;
}
const QJsonObject accountData = Controller::instance().activeConnection()->accountDataJson("m.push_rules");
// Update override rules
const QJsonArray overrideRuleArray = accountData["global"].toObject()["override"].toArray();
for (const auto &i : overrideRuleArray) {
const QJsonObject overrideRule = i.toObject();
if (overrideRule["rule_id"] == ".m.rule.master") {
bool ruleEnabled = overrideRule["enabled"].toBool();
m_globalNotificationsEnabled = !ruleEnabled;
if (!m_globalNotificationsSet) {
m_globalNotificationsSet = true;
}
Q_EMIT globalNotificationsEnabledChanged(m_globalNotificationsEnabled);
}
const PushNotificationAction::Action action = toAction(overrideRule);
if (overrideRule["rule_id"] == ".m.rule.contains_display_name") {
m_displayNameNotificationAction = action;
Q_EMIT displayNameNotificationActionChanged(m_displayNameNotificationAction);
} else if (overrideRule["rule_id"] == ".m.rule.roomnotif") {
m_roomNotificationAction = action;
Q_EMIT roomNotificationActionChanged(m_roomNotificationAction);
} else if (overrideRule["rule_id"] == ".m.rule.invite_for_me") {
m_inviteNotificationAction = action;
Q_EMIT inviteNotificationActionChanged(m_inviteNotificationAction);
} else if (overrideRule["rule_id"] == ".m.rule.tombstone") {
m_tombstoneNotificationAction = action;
Q_EMIT tombstoneNotificationActionChanged(m_tombstoneNotificationAction);
}
}
// Update content rules
const QJsonArray contentRuleArray = accountData["global"].toObject()["content"].toArray();
PushNotificationAction::Action keywordAction = PushNotificationAction::Unknown;
for (const auto &i : contentRuleArray) {
const QJsonObject contentRule = i.toObject();
const PushNotificationAction::Action action = toAction(contentRule);
bool actionMismatch = false;
if (contentRule["rule_id"].toString()[0] != '.' && !actionMismatch) {
if (keywordAction == PushNotificationAction::Unknown) {
keywordAction = action;
m_keywordNotificationAction = action;
Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction);
} else if (action != keywordAction) {
actionMismatch = true;
m_keywordNotificationAction = PushNotificationAction::On;
Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction);
}
}
}
// If there are no keywords set the state to off, this is the only time it'll be in the off state
if (keywordAction == PushNotificationAction::Unknown) {
m_keywordNotificationAction = PushNotificationAction::Off;
Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction);
}
// Update underride rules
const QJsonArray underrideRuleArray = accountData["global"].toObject()["underride"].toArray();
for (const auto &i : underrideRuleArray) {
const QJsonObject underrideRule = i.toObject();
const PushNotificationAction::Action action = toAction(underrideRule);
if (underrideRule["rule_id"] == ".m.rule.room_one_to_one") {
m_oneToOneNotificationAction = action;
Q_EMIT oneToOneNotificationActionChanged(m_oneToOneNotificationAction);
} else if (underrideRule["rule_id"] == ".m.rule.encrypted_room_one_to_one") {
m_encryptedOneToOneNotificationAction = action;
Q_EMIT encryptedOneToOneNotificationActionChanged(m_encryptedOneToOneNotificationAction);
} else if (underrideRule["rule_id"] == ".m.rule.message") {
m_groupChatNotificationAction = action;
Q_EMIT groupChatNotificationActionChanged(m_groupChatNotificationAction);
} else if (underrideRule["rule_id"] == ".m.rule.encrypted") {
m_encryptedGroupChatNotificationAction = action;
Q_EMIT encryptedGroupChatNotificationActionChanged(m_encryptedGroupChatNotificationAction);
} else if (underrideRule["rule_id"] == ".m.rule.call") {
m_callInviteNotificationAction = action;
Q_EMIT callInviteNotificationActionChanged(m_callInviteNotificationAction);
}
}
}
void NotificationsManager::setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled)
{
auto job = Controller::instance().activeConnection()->callApi<IsPushRuleEnabledJob>("global", kind, ruleId);
connect(job, &BaseJob::success, this, [job, kind, ruleId, enabled]() {
if (job->enabled() != enabled) {
Controller::instance().activeConnection()->callApi<SetPushRuleEnabledJob>("global", kind, ruleId, enabled);
}
});
}
void NotificationsManager::setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action)
{
QVector<QVariant> actions;
if (ruleId == ".m.rule.call") {
actions = toActions(action, "ring");
} else {
actions = toActions(action);
}
Controller::instance().activeConnection()->callApi<SetPushRuleActionsJob>("global", kind, ruleId, actions);
}
PushNotificationAction::Action NotificationsManager::toAction(const QJsonObject &rule)
{
const QJsonArray actions = rule["actions"].toArray();
bool isNoisy = false;
bool highlightEnabled = false;
const bool enabled = rule["enabled"].toBool();
for (const auto &i : actions) {
QJsonObject action = i.toObject();
if (action["set_tweak"].toString() == "sound") {
isNoisy = true;
} else if (action["set_tweak"].toString() == "highlight") {
if (action["value"].toString() != "false") {
highlightEnabled = true;
}
}
}
if (!enabled) {
return PushNotificationAction::Off;
}
if (actions[0] == "notify") {
if (isNoisy && highlightEnabled) {
return PushNotificationAction::NoisyHighlight;
} else if (isNoisy) {
return PushNotificationAction::Noisy;
} else if (highlightEnabled) {
return PushNotificationAction::Highlight;
} else {
return PushNotificationAction::On;
}
} else {
return PushNotificationAction::Off;
}
}
QVector<QVariant> NotificationsManager::toActions(PushNotificationAction::Action action, const QString &sound)
{
// The caller should never try to set the state to unknown.
// It exists only as a default state to diable the settings options until the actual state is retrieved from the server.
if (action == PushNotificationAction::Unknown) {
Q_ASSERT(false);
return QVector<QVariant>();
}
QVector<QVariant> actions;
if (action != PushNotificationAction::Off) {
actions.append("notify");
} else {
actions.append("dont_notify");
}
if (action == PushNotificationAction::Noisy || action == PushNotificationAction::NoisyHighlight) {
QJsonObject soundTweak;
soundTweak.insert("set_tweak", "sound");
soundTweak.insert("value", sound);
actions.append(soundTweak);
}
if (action == PushNotificationAction::Highlight || action == PushNotificationAction::NoisyHighlight) {
QJsonObject highlightTweak;
highlightTweak.insert("set_tweak", "highlight");
actions.append(highlightTweak);
}
return actions;
}
#ifdef GSTREAMER_AVAILABLE
void NotificationsManager::postCallInviteNotification(NeoChatRoom *room, const QString &roomName, const QString &sender, const QImage &icon, bool video)
{
QPixmap img;
img.convertFromImage(icon);
KNotification *notification = new KNotification("message");
if (sender == roomName) {
notification->setTitle(sender);
} else {
notification->setTitle(i18n("%1 (%2)", sender, roomName));
}
notification->setText(video ? i18n("%1 is inviting you to a video call", sender) : i18n("%1 is inviting you to a voice call", sender));
notification->setPixmap(img);
notification->setDefaultAction(i18n("Open NeoChat in this room"));
connect(notification, &KNotification::defaultActivated, this, [=]() {
RoomManager::instance().enterRoom(room);
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
});
notification->setActions({i18n("Accept"), i18n("Decline")});
connect(notification, &KNotification::action1Activated, this, [=]() {
CallManager::instance().acceptCall();
});
connect(notification, &KNotification::action2Activated, this, [=]() {
CallManager::instance().hangupCall();
});
notification->sendEvent();
m_notifications.insert(room->id(), notification);
}
#endif

View File

@@ -56,93 +56,6 @@ class NotificationsManager : public QObject
{
Q_OBJECT
/**
* @brief The global notification state.
*
* If this rule is set to off all push notifications are disabled regardless
* of other settings.
*/
Q_PROPERTY(bool globalNotificationsEnabled MEMBER m_globalNotificationsEnabled WRITE setGlobalNotificationsEnabled NOTIFY globalNotificationsEnabledChanged)
/**
* @brief Whether the global notification state has been retrieved from the server.
*
* This is different to the others below as globalNotificationsEnabled is only
* a bool rather than a PushNotificationAction::Action so a separate property
* is required to track if the state has been retrieved from the server.
*
* @sa globalNotificationsEnabled, PushNotificationAction::Action
*/
Q_PROPERTY(bool globalNotificationsSet MEMBER m_globalNotificationsSet NOTIFY globalNotificationsSetChanged)
/**
* @brief The notification action for direct chats.
*
* @note Encrypted direct chats have a separate setting.
*
* @sa encryptedOneToOneNotificationAction
*/
Q_PROPERTY(PushNotificationAction::Action oneToOneNotificationAction MEMBER m_oneToOneNotificationAction WRITE setOneToOneNotificationAction NOTIFY
oneToOneNotificationActionChanged)
/**
* @brief The notification action for encrypted direct chats.
*/
Q_PROPERTY(PushNotificationAction::Action encryptedOneToOneNotificationAction MEMBER m_encryptedOneToOneNotificationAction WRITE
setEncryptedOneToOneNotificationAction NOTIFY encryptedOneToOneNotificationActionChanged)
/**
* @brief The notification action for group chats.
*
* @note Encrypted group chats have a separate setting.
*
* @sa encryptedGroupChatNotificationAction
*/
Q_PROPERTY(PushNotificationAction::Action groupChatNotificationAction MEMBER m_groupChatNotificationAction WRITE setGroupChatNotificationAction NOTIFY
groupChatNotificationActionChanged)
/**
* @brief The notification action for encrypted group chats.
*/
Q_PROPERTY(PushNotificationAction::Action encryptedGroupChatNotificationAction MEMBER m_encryptedGroupChatNotificationAction WRITE
setEncryptedGroupChatNotificationAction NOTIFY encryptedGroupChatNotificationActionChanged)
/**
* @brief The notification action for events containing the local user's display name.
*/
Q_PROPERTY(PushNotificationAction::Action displayNameNotificationAction MEMBER m_displayNameNotificationAction WRITE setDisplayNameNotificationAction NOTIFY
displayNameNotificationActionChanged)
/**
* @brief The notification action for room events.
*/
Q_PROPERTY(PushNotificationAction::Action roomNotificationAction MEMBER m_roomNotificationAction WRITE setRoomNotificationAction NOTIFY
roomNotificationActionChanged)
/**
* @brief The notification action for keyword push rules.
*/
Q_PROPERTY(PushNotificationAction::Action keywordNotificationAction MEMBER m_keywordNotificationAction WRITE setKeywordNotificationAction NOTIFY
keywordNotificationActionChanged)
/**
* @brief The notification action for invites to chats.
*/
Q_PROPERTY(PushNotificationAction::Action inviteNotificationAction MEMBER m_inviteNotificationAction WRITE setInviteNotificationAction NOTIFY
inviteNotificationActionChanged)
/**
* @brief The notification action for voice calls.
*/
Q_PROPERTY(PushNotificationAction::Action callInviteNotificationAction MEMBER m_callInviteNotificationAction WRITE setCallInviteNotificationAction NOTIFY
callInviteNotificationActionChanged)
/**
* @brief The notification action for room upgrade events.
*/
Q_PROPERTY(PushNotificationAction::Action tombstoneNotificationAction MEMBER m_tombstoneNotificationAction WRITE setTombstoneNotificationAction NOTIFY
tombstoneNotificationActionChanged)
public:
static NotificationsManager &instance();
@@ -156,7 +69,6 @@ public:
* @brief Display a native notification for an invite.
*/
void postInviteNotification(NeoChatRoom *room, const QString &title, const QString &sender, const QImage &icon);
void postCallInviteNotification(NeoChatRoom *room, const QString &roomName, const QString &sender, const QImage &icon, bool video);
/**
* @brief Clear an existing invite notification for the given room.
@@ -165,30 +77,6 @@ public:
*/
void clearInvitationNotification(const QString &roomId);
/**
* @brief Set the initial keyword notification action.
*
* This is only required if no keyword rules exist and one is created. The default
* action is PushNotificationAction::Action::Highlight.
*
* @sa PushNotificationAction::Action
*/
void initializeKeywordNotificationAction();
/**
* @brief Set the keyword notification action to PushNotificationAction::Action::Off.
*
* This is only required the last keyword rule is deleted.
*
* @sa PushNotificationAction::Action
*/
void deactivateKeywordNotificationAction();
/**
* @brief Return the keyword notification action in a form required for the server push rule.
*/
QVector<QVariant> getKeywordNotificationActions();
#ifdef QUOTIENT_07
/**
* @brief Handle the notifications for the given connection.
@@ -209,52 +97,6 @@ private:
QHash<QString, KNotification *> m_notifications;
QHash<QString, QPointer<KNotification>> m_invitations;
bool m_globalNotificationsEnabled = false;
bool m_globalNotificationsSet = false;
PushNotificationAction::Action m_oneToOneNotificationAction = PushNotificationAction::Unknown;
PushNotificationAction::Action m_encryptedOneToOneNotificationAction = PushNotificationAction::Unknown;
PushNotificationAction::Action m_groupChatNotificationAction = PushNotificationAction::Unknown;
PushNotificationAction::Action m_encryptedGroupChatNotificationAction = PushNotificationAction::Unknown;
PushNotificationAction::Action m_displayNameNotificationAction = PushNotificationAction::Unknown;
PushNotificationAction::Action m_roomNotificationAction = PushNotificationAction::Unknown;
PushNotificationAction::Action m_keywordNotificationAction = PushNotificationAction::Unknown;
PushNotificationAction::Action m_inviteNotificationAction = PushNotificationAction::Unknown;
PushNotificationAction::Action m_callInviteNotificationAction = PushNotificationAction::Unknown;
PushNotificationAction::Action m_tombstoneNotificationAction = PushNotificationAction::Unknown;
void setGlobalNotificationsEnabled(bool enabled);
void setOneToOneNotificationAction(PushNotificationAction::Action action);
void setEncryptedOneToOneNotificationAction(PushNotificationAction::Action action);
void setGroupChatNotificationAction(PushNotificationAction::Action action);
void setEncryptedGroupChatNotificationAction(PushNotificationAction::Action action);
void setDisplayNameNotificationAction(PushNotificationAction::Action action);
void setRoomNotificationAction(PushNotificationAction::Action action);
void setKeywordNotificationAction(PushNotificationAction::Action action);
void setInviteNotificationAction(PushNotificationAction::Action action);
void setCallInviteNotificationAction(PushNotificationAction::Action action);
void setTombstoneNotificationAction(PushNotificationAction::Action action);
void setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled);
void setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action);
PushNotificationAction::Action toAction(const QJsonObject &rule);
QVector<QVariant> toActions(PushNotificationAction::Action action, const QString &sound = "default");
private Q_SLOTS:
void processNotificationJob(QPointer<Quotient::Connection> connection, Quotient::GetNotificationsJob *job, bool initialization);
void updateNotificationRules(const QString &type);
Q_SIGNALS:
void globalNotificationsEnabledChanged(bool newState);
void globalNotificationsSetChanged(bool newState);
void oneToOneNotificationActionChanged(PushNotificationAction::Action action);
void encryptedOneToOneNotificationActionChanged(PushNotificationAction::Action action);
void groupChatNotificationActionChanged(PushNotificationAction::Action action);
void encryptedGroupChatNotificationActionChanged(PushNotificationAction::Action action);
void displayNameNotificationActionChanged(PushNotificationAction::Action action);
void roomNotificationActionChanged(PushNotificationAction::Action action);
void keywordNotificationActionChanged(PushNotificationAction::Action action);
void inviteNotificationActionChanged(PushNotificationAction::Action action);
void callInviteNotificationActionChanged(PushNotificationAction::Action action);
void tombstoneNotificationActionChanged(PushNotificationAction::Action action);
};

View File

@@ -59,6 +59,7 @@ Comment[it]=Trova stanze in NeoChat
Comment[ka]=იპოვე ოთახები NeoChat-ში
Comment[ko]=NeoChat에서 대화방 찾기
Comment[nl]=Rooms zoeken in NeoChat
Comment[nn]=Finn rom i NeoChat
Comment[pl]=Znajdź pokoje w NeoChat
Comment[pt]=Procurar salas no NeoChat
Comment[pt_BR]=Encontrar salas no NeoChat

View File

@@ -1,86 +0,0 @@
// SPDX-FileCopyrightText: 2022 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.0
import QtQuick.Controls 2.7 as QQC2
import QtQuick.Layouts 1.1
import org.kde.kirigami 2.13 as Kirigami
QQC2.AbstractButton {
id: control
property int temprament: CallPageButton.Neutral
property bool shimmering: false
enum Temprament {
Neutral,
Constructive,
Destructive
}
padding: Kirigami.Units.largeSpacing
contentItem: ColumnLayout {
QQC2.Control {
padding: Kirigami.Units.gridUnit
Kirigami.Theme.colorSet: Kirigami.Theme.Button
Layout.alignment: Qt.AlignHCenter
contentItem: Kirigami.Icon {
implicitHeight: Kirigami.Units.iconSizes.medium
implicitWidth: Kirigami.Units.iconSizes.medium
source: control.icon.name
}
background: Rectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.Button
ShimmerGradient {
id: shimmerGradient
color: {
switch (control.temprament) {
case CallPageButton.Neutral:
return Kirigami.Theme.textColor
case CallPageButton.Constructive:
return Kirigami.Theme.positiveTextColor
case CallPageButton.Destructive:
return Kirigami.Theme.negativeTextColor
}
}
}
color: {
if (control.checked) {
return Kirigami.Theme.focusColor
}
switch (control.temprament) {
case CallPageButton.Neutral:
return Kirigami.Theme.backgroundColor
case CallPageButton.Constructive:
return Kirigami.Theme.positiveBackgroundColor
case CallPageButton.Destructive:
return Kirigami.Theme.negativeBackgroundColor
}
}
border.color: Kirigami.Theme.focusColor
border.width: control.visualFocus ? 2 : 0
radius: height/2
Rectangle {
visible: control.shimmering
anchors.fill: parent
radius: height/2
gradient: control.shimmering ? shimmerGradient : null
}
}
}
QQC2.Label {
text: control.text
font: Kirigami.Theme.smallFont
horizontalAlignment: Qt.AlignHCenter
Layout.fillWidth: true
}
}
}

View File

@@ -1,67 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.freedesktop.gstreamer.GLVideoItem 1.0
import org.kde.neochat 1.0
Rectangle {
id: videoStreamDelegate
implicitWidth: height / 9 * 16
implicitHeight: 300
color: "black"
radius: 10
QQC2.Label {
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
color: "white"
text: model.object.user.id
}
RowLayout {
anchors.fill: parent
Loader {
active: model.object.hasCamera
Layout.maximumWidth: parent.width
Layout.maximumHeight: parent.height
Layout.preferredHeight: parent.height
Layout.preferredWidth: parent.width
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
onActiveChanged: {
if (active) {
model.object.initCamera(camera)
}
}
Component.onCompleted: if (active) model.object.initCamera(camera)
GstGLVideoItem {
id: camera
width: parent.width
height: parent.height
}
}
Loader {
active: false
Layout.maximumWidth: parent.width
Layout.maximumHeight: parent.height
Layout.preferredHeight: parent.height
Layout.preferredWidth: parent.width
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
GstGLVideoItem {
id: screenCast
width: parent.width
height: parent.height
Component.onCompleted: {
model.object.initCamera(this)
}
}
}
}
}

View File

@@ -75,7 +75,7 @@ ColumnLayout {
visible: currentRoom.canSendEvent("m.room.message")
Layout.fillWidth: true
Layout.minimumHeight: implicitHeight + Kirigami.Units.largeSpacing
Layout.minimumHeight: Math.max(Kirigami.Units.gridUnit * 2, implicitHeight + Kirigami.Units.largeSpacing)
// lineSpacing is height+leading, so subtract leading once since leading only exists between lines.
Layout.maximumHeight: chatBarFontMetrics.lineSpacing * 8 - chatBarFontMetrics.leading + textField.topPadding + textField.bottomPadding

View File

@@ -0,0 +1,73 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtLocation 5.15
import QtPositioning 5.15
import org.kde.kirigami 2.15 as Kirigami
ApplicationWindow {
id: root
property real latitude: NaN
property real longitude: NaN
property string asset
property var author
property QtObject liveLocationModel: null
flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground
visibility: Qt.WindowFullScreen
title: i18n("View Location")
Shortcut {
sequence: "Escape"
onActivated: root.destroy()
}
color: Kirigami.Theme.backgroundColor
background: AbstractButton {
onClicked: root.destroy()
}
Map {
id: map
anchors.fill: parent
center: root.liveLocationModel ? QtPositioning.coordinate(root.liveLocationModel.boundingBox.y, root.liveLocationModel.boundingBox.x)
: QtPositioning.coordinate(root.latitude, root.longitude)
zoomLevel: 15
plugin: OsmLocationPlugin.plugin
LocationMapItem {
latitude: root.latitude
longitude: root.longitude
asset: root.asset
author: root.author
isLive: true
visible: !isNaN(root.latitude) && !isNaN(root.longitude)
}
MapItemView {
model: root.liveLocationModel
delegate: LocationMapItem {}
}
onCopyrightLinkActivated: {
Qt.openUrlExternally(link)
}
}
Button {
anchors.top: parent.top
anchors.right: parent.right
text: i18n("Close")
icon.name: "dialog-close"
display: AbstractButton.IconOnly
width: Kirigami.Units.gridUnit * 2
height: Kirigami.Units.gridUnit * 2
onClicked: root.destroy()
}
}

View File

@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtLocation 5.15
import QtPositioning 5.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
/** Location marker for any of the shared location maps. */
MapQuickItem {
id: root
required property real latitude
required property real longitude
required property string asset
required property var author
required property bool isLive
anchorPoint.x: sourceItem.width / 2
anchorPoint.y: sourceItem.height
coordinate: QtPositioning.coordinate(root.latitude, root.longitude)
autoFadeIn: false
sourceItem: Kirigami.Icon {
width: height
height: Kirigami.Units.iconSizes.huge
source: "gps"
isMask: true
color: root.isLive ? Kirigami.Theme.highlightColor : Kirigami.Theme.disabledTextColor
Kirigami.Icon {
anchors.centerIn: parent
anchors.verticalCenterOffset: -parent.height / 8
visible: root.asset === "m.pin"
width: height
height: parent.height / 3 + 1
source: "pin"
isMask: true
color: parent.color
}
Kirigami.Avatar {
anchors.centerIn: parent
anchors.verticalCenterOffset: -parent.height / 8
visible: root.asset === "m.self"
width: height
height: parent.height / 3 + 1
name: root.author.displayName
source: root.author.avatarSource
color: root.author.color
}
}
}

View File

@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtLocation 5.15
import QtPositioning 5.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.neochat 1.0
Kirigami.Page {
id: locationsPage
required property var room
title: i18nc("Locations on a map", "Locations")
padding: 0
Map {
id: map
anchors.fill: parent
plugin: OsmLocationPlugin.plugin
center: {
let c = LocationHelper.center(LocationHelper.unite(locationsModel.boundingBox, liveLocationsModel.boundingBox));
return QtPositioning.coordinate(c.y, c.x);
}
zoomLevel: LocationHelper.zoomToFit(LocationHelper.unite(locationsModel.boundingBox, liveLocationsModel.boundingBox), map.width, map.height)
MapItemView {
model: LocationsModel {
id: locationsModel
room: locationsPage.room
}
delegate: LocationMapItem {
isLive: true
}
}
MapItemView {
model: LiveLocationsModel {
id: liveLocationsModel
room: locationsPage.room
}
delegate: LocationMapItem {}
}
Kirigami.PlaceholderMessage {
text: i18n("There are no locations shared in this room.")
visible: map.mapItems.length === 0
anchors.centerIn: parent
}
}
}

View File

@@ -4,7 +4,7 @@
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import Qt.labs.platform 1.1
import Qt.labs.platform 1.1 as Platform
import org.kde.kirigami 2.13 as Kirigami
import org.kde.kirigamiaddons.labs.components 1.0 as Components
@@ -14,39 +14,46 @@ import org.kde.neochat 1.0
Components.AlbumMaximizeComponent {
id: root
required property string eventId
readonly property string currentEventId: model.data(model.index(content.currentIndex, 0), MessageEventModel.EventIdRole)
required property var time
readonly property var currentAuthor: model.data(model.index(content.currentIndex, 0), MessageEventModel.AuthorRole)
required property var author
readonly property var currentTime: model.data(model.index(content.currentIndex, 0), MessageEventModel.TimeRole)
required property int delegateType
readonly property string currentPlainText: model.data(model.index(content.currentIndex, 0), MessageEventModel.PlainText)
required property string plainText
readonly property var currentMimeType: model.data(model.index(content.currentIndex, 0), MessageEventModel.MimeTypeRole)
required property string caption
readonly property var currentProgressInfo: model.data(model.index(content.currentIndex, 0), MessageEventModel.ProgressInfoRole)
required property var mediaInfo
readonly property var currentJsonSource: model.data(model.index(content.currentIndex, 0), MessageEventModel.SourceRole)
required property var progressInfo
autoLoad: false
required property var mimeType
required property var source
property list<Components.AlbumModelItem> items: [
Components.AlbumModelItem {
type: root.delegateType === MessageEventModel.Image || root.delegateType === MessageEventModel.Sticker ? Components.AlbumModelItem.Image : Components.AlbumModelItem.Video
source: root.delegateType === MessageEventModel.Video ? root.progressInfo.localPath : root.mediaInfo.source
tempSource: root.mediaInfo.tempInfo.source
caption: root.caption
sourceWidth: root.mediaInfo.width
sourceHeight: root.mediaInfo.height
downloadAction: Components.DownloadAction {
id: downloadAction
onTriggered: {
currentRoom.downloadFile(root.currentEventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + root.currentEventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(root.currentEventId))
}
]
}
model: items
initialIndex: 0
Connections {
target: currentRoom
function onFileTransferProgress(id, progress, total) {
if (id == root.currentEventId) {
downloadAction.progress = progress / total * 100.0
}
}
}
Connections {
target: content
function onCurrentIndexChanged() {
downloadAction.progress = currentProgressInfo.progress / currentProgressInfo.total * 100.0
}
}
leading: RowLayout {
Kirigami.Avatar {
@@ -54,22 +61,23 @@ Components.AlbumMaximizeComponent {
implicitWidth: Kirigami.Units.iconSizes.medium
implicitHeight: Kirigami.Units.iconSizes.medium
name: root.author.displayName
source: root.author.avatarSource
color: root.author.color
name: root.currentAuthor.name ?? root.currentAuthor.displayName
source: root.currentAuthor.avatarSource
color: root.currentAuthor.color
}
ColumnLayout {
spacing: 0
QQC2.Label {
id: userLabel
text: root.author.displayName
color: root.author.color
text: root.currentAuthor.name ?? root.currentAuthor.displayName
color: root.currentAuthor.color
font.weight: Font.Bold
elide: Text.ElideRight
}
QQC2.Label {
id: dateTimeLabel
text: root.time.toLocaleString(Qt.locale(), Locale.ShortFormat)
text: root.currentTime.toLocaleString(Qt.locale(), Locale.ShortFormat)
color: Kirigami.Theme.disabledTextColor
elide: Text.ElideRight
}
@@ -77,13 +85,13 @@ Components.AlbumMaximizeComponent {
}
onItemRightClicked: {
const contextMenu = fileDelegateContextMenu.createObject(parent, {
author: root.author,
eventId: root.eventId,
source: root.source,
author: root.currentAuthor,
eventId: root.currentEventId,
source: root.currentJsonSource,
file: parent,
mimeType: root.mimeType,
progressInfo: root.progressInfo,
plainText: root.plainText,
mimeType: root.currentMimeType,
progressInfo: root.currentProgressInfo,
plainMessage: root.currentPlainText
});
contextMenu.closeFullscreen.connect(root.close)
contextMenu.open();
@@ -91,12 +99,12 @@ Components.AlbumMaximizeComponent {
onSaveItem: {
var dialog = saveAsDialog.createObject(QQC2.ApplicationWindow.overlay)
dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(root.eventId)
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(root.currentEventId)
}
Component {
id: saveAsDialog
FileDialog {
Platform.FileDialog {
fileMode: FileDialog.SaveFile
folder: root.saveFolder
onAccepted: {

Some files were not shown because too many files have changed in this diff Show More