Compare commits

..

1 Commits

Author SHA1 Message Date
Joshua Goins
e055992943 Don't erase existing reply-to relationships when editing messages
Signed-off-by: Joshua Goins <josh@redstrate.com>
2023-01-04 23:42:46 -05:00
332 changed files with 54250 additions and 96270 deletions

View File

@@ -2,7 +2,7 @@
"id": "org.kde.neochat",
"branch": "master",
"runtime": "org.kde.Platform",
"runtime-version": "5.15-22.08",
"runtime-version": "5.15-21.08",
"sdk": "org.kde.Sdk",
"command": "neochat",
"tags": [
@@ -41,7 +41,6 @@
{
"name": "olm",
"buildsystem": "cmake-ninja",
"config-opts": [ "-DOLM_TESTS=OFF" ],
"sources": [
{
"type": "git",
@@ -112,14 +111,12 @@
}
],
"config-opts": [
"-DQuotient_ENABLE_E2EE=ON",
"-DBUILD_TESTING=OFF"
"-DQuotient_ENABLE_E2EE=ON"
]
},
{
"name": "cmark",
"buildsystem": "cmake-ninja",
"config-opts": [ "-DCMARK_TESTS=OFF" ],
"sources": [
{
"type": "git",
@@ -136,7 +133,6 @@
{
"name": "qcoro",
"buildsystem": "cmake-ninja",
"config-opts": [ "-DQCORO_BUILD_EXAMPLES=OFF", "-DBUILD_TESTING=OFF" ],
"sources": [
{
"type": "archive",

2
.gitignore vendored
View File

@@ -9,5 +9,3 @@ compile_commands.json
kate.project.ctags.*
*.user
.flatpak-builder/
.idea/
cmake-build-*

View File

@@ -4,11 +4,9 @@
include:
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
# TODO enable once we can have qt6 libQuotient on the CI
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.yml

View File

@@ -1,8 +1,8 @@
# SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
# SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
# SPDX-License-Identifier: BSD-2-Clause
Dependencies:
- 'on': ['Linux/Qt5', 'Android/Qt5', 'FreeBSD/Qt5', 'Windows/Qt5']
- 'on': ['@all']
'require':
'frameworks/extra-cmake-modules': '@stable'
'frameworks/kcoreaddons': '@stable'
@@ -11,7 +11,6 @@ Dependencies:
'frameworks/kconfig': '@stable'
'frameworks/syntax-highlighting': '@stable'
'frameworks/kitemmodels': '@stable'
'frameworks/kquickcharts': '@stable'
'frameworks/knotifications': '@stable'
'libraries/kquickimageeditor': '@stable'
'frameworks/sonnet': '@stable'
@@ -20,43 +19,15 @@ Dependencies:
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- 'on': ['Windows/Qt5', 'Linux/Qt5', 'FreeBSD/Qt5']
- 'on': ['Windows', 'Linux', 'FreeBSD']
'require':
'frameworks/qqc2-desktop-style': '@stable'
'frameworks/kio': '@stable'
'frameworks/kwindowsystem': '@stable'
'frameworks/kconfigwidgets': '@stable'
- 'on': ['Linux/Qt5', 'FreeBSD/Qt5']
- 'on': ['Linux', 'FreeBSD']
'require':
'frameworks/kdbusaddons': '@stable'
- 'on': ['Linux/Qt6', 'Android/Qt6', 'FreeBSD/Qt6', 'Windows/Qt6']
'require':
'frameworks/extra-cmake-modules': '@latest-kf6'
'frameworks/kcoreaddons': '@latest-kf6'
'frameworks/kirigami': '@latest-kf6'
'frameworks/ki18n': '@latest-kf6'
'frameworks/kconfig': '@latest-kf6'
'frameworks/syntax-highlighting': '@latest-kf6'
'frameworks/kitemmodels': '@latest-kf6'
'frameworks/kquickcharts': '@latest-kf6'
'frameworks/knotifications': '@latest-kf6'
'libraries/kquickimageeditor': '@latest-kf6'
'frameworks/sonnet': '@latest-kf6'
'libraries/kirigami-addons': '@latest-kf6'
'third-party/libquotient': '@latest'
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- 'on': ['Windows/Qt6', 'Linux/Qt6', 'FreeBSD/Qt6']
'require':
'frameworks/qqc2-desktop-style': '@latest-kf6'
'frameworks/kio': '@latest-kf6'
'frameworks/kwindowsystem': '@latest-kf6'
'frameworks/kconfigwidgets': '@latest-kf6'
- 'on': ['Linux/Qt6', 'FreeBSD/Qt6']
'require':
'frameworks/kdbusaddons': '@latest-kf6'
Options:
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD' ]
require-passing-tests-on: [ 'Linux', 'FreeBSD', 'Windows' ]

View File

@@ -7,7 +7,7 @@ Copyright: 2020 Carson Black <uhhadd@gmail.com>
License: LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
Files: android/res/drawable/splash.xml
Copyright: 2020 Tobias Fella <tobias.fella@kde.org>
Copyright: 2020 Tobias Fella <fella@posteo.de>
License: BSD-2-Clause
Files: .gitignore
@@ -27,11 +27,11 @@ Copyright: 2021 Carl Schwan <carlschwan@kde.org>
License: BSD-2-Clause
Files: src/neochatconfig.kcfg
Copyright: 2020-2021 Carl Schwan <carlschwan@kde.org>, Tobias Fella <tobias.fella@kde.org>
Copyright: 2020-2021 Carl Schwan <carlschwan@kde.org>, Tobias Fella <fella@posteo.de>
License: BSD-2-Clause
Files: src/neochat.notifyrc
Copyright: 2020 Tobias Fella <tobias.fella@kde.org>
Copyright: 2020 Tobias Fella <fella@posteo.de>
License: BSD-2-Clause
Files: src/qml/Component/confetti.png src/qml/Component/glowdot.png
@@ -39,5 +39,5 @@ Copyright: 2021 Alexey Andreyev <aa13q@ya.ru>
License: CC0-1.0
Files: .flatpak-manifest.json
Copyright: 2020-2022 Tobias Fella <tobias.fella@kde.org>
Copyright: 2020-2022 Tobias Fella <fella@posteo.de>
License: BSD-2-Clause

View File

@@ -1,26 +1,18 @@
# SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carl@carlschwan.eu>
# SPDX-FileCopyrightText: 2020-2021 Nicolas Fella <nicolas.fella@gmx.de>
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
# SPDX-FileCopyrightText: 2021 Adriaan de Groot <groot@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "23")
set(RELEASE_SERVICE_VERSION_MINOR "07")
set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat)
set(PROJECT_VERSION "22.11")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
set(KF_MIN_VERSION "5.105.0")
set(KF5_MIN_VERSION "5.91.0")
set(QT_MIN_VERSION "5.15.2")
if (ANDROID)
set(QT_MIN_VERSION "5.15.8")
endif()
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
@@ -39,7 +31,6 @@ include(KDECompilerSettings NO_POLICY_SCOPE)
include(ECMAddAppIcon)
include(KDEGitCommitHooks)
include(ECMCheckOutboundLicense)
include(ECMQtDeclareLoggingCategory)
if (NOT ANDROID)
include(KDEClangFormat)
endif()
@@ -58,16 +49,16 @@ set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
set_package_properties(KF${QT_MAJOR_VERSION} PROPERTIES
find_package(KF5 ${KF5_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
set_package_properties(KF5 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
set_package_properties(KF${QT_MAJOR_VERSION}Kirigami2 PROPERTIES
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
find_package(KF${QT_MAJOR_VERSION}KirigamiAddons 0.7.2 REQUIRED)
set_package_properties(KF5Kirigami2 PROPERTIES
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
find_package(KF5KirigamiAddons 0.6 REQUIRED)
find_package(Qt${QT_MAJOR_VERSION}Keychain)
set_package_properties(Qt${QT_MAJOR_VERSION}Keychain PROPERTIES
@@ -83,23 +74,19 @@ if(ANDROID)
)
else()
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem)
set_package_properties(KF${QT_MAJOR_VERSION}QQC2DesktopStyle PROPERTIES
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem)
set_package_properties(KF5QQC2DesktopStyle PROPERTIES
TYPE RUNTIME
)
ecm_find_qmlmodule(org.kde.syntaxhighlighting 1.0)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
find_package(KF${QT_MAJOR_VERSION}DBusAddons ${KF_MIN_VERSION} REQUIRED)
find_package(KF5DBusAddons ${KF5_MIN_VERSION} REQUIRED)
endif()
if(QT_MAJOR_VERSION STREQUAL "6" AND NOT ANDROID AND NOT WIN32)
set(QUOTIENT_SUFFIX "Qt6")
endif()
find_package(Quotient${QUOTIENT_SUFFIX} 0.6)
set_package_properties(Quotient${QUOTIENT_SUFFIX} PROPERTIES
find_package(Quotient 0.6)
set_package_properties(Quotient PROPERTIES
TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API"
URL "https://github.com/quotient-im/libQuotient/"
@@ -116,7 +103,6 @@ set_package_properties(cmark PROPERTIES
ecm_find_qmlmodule(org.kde.kquickimageeditor 1.0)
ecm_find_qmlmodule(org.kde.kitemmodels 1.0)
ecm_find_qmlmodule(org.kde.quickcharts 1.0)
find_package(KQuickImageEditor COMPONENTS)
set_package_properties(KQuickImageEditor PROPERTIES
@@ -130,29 +116,20 @@ find_package(QCoro${QT_MAJOR_VERSION} 0.4 COMPONENTS Core REQUIRED)
qcoro_enable_coroutines()
find_package(KF${QT_MAJOR_VERSION}DocTools ${KF_MIN_VERSION})
set_package_properties(KF${QT_MAJOR_VERSION}DocTools PROPERTIES DESCRIPTION
find_package(KF5DocTools ${KF5_MIN_VERSION})
set_package_properties(KF5DocTools PROPERTIES DESCRIPTION
"Tools to generate documentation"
TYPE OPTIONAL
)
if(NOT Quotient${QUOTIENT_SUFFIX}_VERSION_MINOR GREATER 6)
if(NOT Quotient_VERSION_MINOR GREATER 6)
cmake_policy(SET CMP0063 OLD)
endif()
if(ANDROID)
find_package(Sqlite3)
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})
@@ -163,12 +140,12 @@ install(FILES org.kde.neochat.tray.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/h
add_definitions(-DQT_NO_FOREACH)
add_subdirectory(src)
if (BUILD_TESTING AND Quotient${QUOTIENT_SUFFIX}_VERSION_MINOR GREATER 6)
if (BUILD_TESTING AND Quotient_VERSION_MINOR GREATER 6)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
add_subdirectory(autotests)
endif()
if(KF${QT_MAJOR_VERSION}DocTools_FOUND)
if(KF5DocTools_FOUND)
kdoctools_install(po)
add_subdirectory(doc)
endif()

0
LICENSES/MIT.txt Normal file → Executable file
View File

102
README.md
View File

@@ -1,51 +1,24 @@
<!--
SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carlschwan@kde.org>
SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
SPDX-License-Identifier: CC0-1.0
-->
# NeoChat
A Qt/QML based Matrix client.
NeoChat is a client for Matrix, the decentralized communication protocol for instant
messaging. It is a fork of Spectral, using KDE frameworks, most notably Kirigami,
KConfig and KI18n.
<a href='https://matrix.org'><img src='https://matrix.org/docs/projects/images/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a>
<a href='https://flathub.org/apps/details/org.kde.neochat'><img width='190px' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-i-en.png'/></a>
## Introduction
NeoChat is a client for [Matrix](https://matrix.org), the decentralized communication protocol for instant
messaging. It is a fork of Spectral, using KDE frameworks, most notably [Kirigami](https://invent.kde.org/frameworks/kirigami)
to provide a convergent experience across multiple platforms.
NeoChat also make use of other KDE Frameworks as well as [libQuotient](https://github.com/quotient-im/libQuotient), a
Qt-based SDK for the [Matrix Protocol](https://spec.matrix.org/).
![Timeline](https://cdn.kde.org/screenshots/neochat/application.png)
## Features
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.
Due to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:
- Polls - MSC3381
- Sticker Packs - MSC2545
- Location Events - MSC3488
## Get it
Details where to find stable releases for NeoChat can be found on its [homepage](https://apps.kde.org/neochat).
A stable release [is available](https://apps.kde.org/neochat) for download for Linux distributions.
In addition to the stable builds, unstable nightly builds are available for all platforms. These can be downloaded
from the [binary factory](https://binary-factory.kde.org/). There are unstable versions for the following platforms
in addition to stable ones:
- Android
- MacOS
- Windows
Additionally the nightly Flatpak version can be obtained from the nightly Flatpak repo using the following commands in your terminal:
Along with the stable release, a Flatpak version is available for the nightly
version:
```
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
@@ -53,53 +26,42 @@ flatpak remote-add --if-not-exists kdeapps --from https://distribute.kde.org/kde
flatpak install kdeapps org.kde.neochat
```
The unstable Android version can also be obtained from the [KDE nightly F-Droid repo](https://community.kde.org/Android/FDroid).
A nightly build is also available for Android in the [KDE nightly F-Droid repo](https://community.kde.org/Android/FDroid)
and can also directly be downloaded from the [binary factory](https://binary-factory.kde.org/view/Android/job/NeoChat_Nightly_android-arm64/).
## Running
Nightly builds for [Windows](https://binary-factory.kde.org/job/NeoChat_Nightly_win64/), [MacOS](https://binary-factory.kde.org/job/NeoChat_Nightly_macos/) and [AppImages](https://binary-factory.kde.org/job/NeoChat_Nightly_appimage/) can also be downloaded from the [binary factory](https://binary-factory.kde.org/search/?q=neochat).
Just start the executable in your preferred way - either from the build directory or from the installed location.
![Timeline](https://cdn.kde.org/screenshots/neochat/application.png)
## Building NeoChat
## Features
The best way to build KDE apps during development is to use `kdesrc-build`. The full instructions for this can be found on
the KDE community website's get involved section under [development](https://community.kde.org/Get_Involved/development). This
is primarily aimed at Linux development.
* Sending messages
* Sending files from clipboard and filesystem
* Reply to message (right-click on a message to access menu)
* Start a private chat (but not encrypted)
* Show notifications, for the moment there is only a global switch
to disable it. We plan to implement the configuration part of the
specification soon.
* Autocompletion of usernames in chat
* Emoji picker
* Basic room setting page
* Send and accept invitations
* /rainbow <message> (very important)
* /me <message>
For Windows and Android [Craft](https://invent.kde.org/packaging/craft) is the primary choice. There are guides for setting up
development environments for [Windows](https://community.kde.org/Get_Involved/development/Windows) and [Android](https://develop.kde.org/docs/packaging/android/building_applications/).
## Tests
Tests are in the repository under [autotests](autotests) and should all pass for any contribution.
The project has CI setup to test new commits to the repository. All tests are expected to pass for a merge request to
be complete.
Current build status
![coverage](https://invent.kde.org/network/neochat/badges/master/pipeline.svg)
Currently the number of tests is limited, but growing. If anyone wants to help improve this, those
contributions would be especially welcome.
## Contributing
As is the case throughout the KDE ecosystem contributions are welcome from all. The code base is managed in the
[NeoChat repository](https://invent.kde.org/network/neochat) of the KDE Gitlab instance.
- [Code of Conduct](https://kde.org/code-of-conduct)
- [Report a Bug](https://bugs.kde.org/enter_bug.cgi?format=guided&product=neochat)
- [Feature Request](https://community.kde.org/Infrastructure/GitLab#Submitting_a_merge_request)
- [Create a Merge Request](https://community.kde.org/Infrastructure/GitLab#Submitting_a_merge_request)
- [Translation](https://community.kde.org/Get_Involved/translation)
NeoChat is still missing a few features to become a full-featured
Matrix client (most notably encryption support and video chat support).
We welcome contributions in this direction.
## Contact
The best place to reach the maintainers is on the KDE Matrix instance in the NeoChat channel, [#neochat:kde.org](https://matrix.to/#/#neochat:kde.org).
You can reach the maintainers at [#neochat:kde.org](https://matrix.to/#/#neochat:kde.org), if you are already on Matrix.
Development happens in http://invent.kde.org/network/neochat (not in GitHub).
## Acknowledgement
This program utilizes [libQuotient](https://github.com/quotient-im/libQuotient/) as its Matrix SDK.
This program utilizes [libQuotient](https://github.com/quotient-im/libQuotient/)
library and some C++ models from [Quaternion](https://github.com/quotient-im/Quaternion/).
This program is a fork of [Spectral](https://gitlab.com/spectral-im/spectral/).

View File

@@ -14,8 +14,7 @@
android:name="org.qtproject.qt5.android.bindings.QtActivity"
android:label="NeoChat"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTop"
android:exported="true">
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -23,6 +22,7 @@
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="neochat-app"/>
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
<meta-data android:name="android.app.repository" android:value="default"/>
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
@@ -38,6 +38,8 @@
<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
<!-- Messages maps -->
<meta-data android:value="@string/ministro_not_found_msg" android:name="android.app.ministro_not_found_msg"/>
<meta-data android:value="@string/ministro_needed_msg" android:name="android.app.ministro_needed_msg"/>
<meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/>
<!-- Splash screen -->

View File

@@ -12,7 +12,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.2'
classpath 'com.android.tools.build:gradle:3.6.4'
}
}
@@ -73,10 +73,6 @@ android {
defaultConfig {
minSdkVersion qtMinSdkVersion
targetSdkVersion qtTargetSdkVersion
applicationId "org.kde.neochat"
namespace "org.kde.neochat"
versionCode timestamp
versionName projectVersionFull
manifestPlaceholders = [versionName: projectVersionFull, versionCode: timestamp]
}

View File

@@ -5,18 +5,6 @@ enable_testing()
ecm_add_test(
neochatroomtest.cpp
LINK_LIBRARIES neochat Qt::Test
LINK_LIBRARIES neochat Qt::Test Quotient
TEST_NAME neochatroomtest
)
ecm_add_test(
texthandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME texthandlertest
)
ecm_add_test(
delegatesizehelpertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME delegatesizehelpertest
)

View File

@@ -1,156 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QTest>
#include "delegatesizehelper.h"
class DelegateSizeHelperTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void risingPercentage_data();
void risingPercentage();
void fallingPercentage_data();
void fallingPercentage();
void equalPercentage_data();
void equalPercentage();
void equalBreakpoint_data();
void equalBreakpoint();
};
void DelegateSizeHelperTest::risingPercentage_data()
{
QTest::addColumn<qreal>("parentWidth");
QTest::addColumn<int>("currentPercentageWidth");
QTest::addColumn<qreal>("currentWidth");
QTest::newRow("zero") << qreal(0) << int(0) << qreal(0);
QTest::newRow("one hundred") << qreal(100) << int(0) << qreal(0);
QTest::newRow("one fifty") << qreal(150) << int(50) << qreal(75);
QTest::newRow("two hundred") << qreal(200) << int(100) << qreal(200);
QTest::newRow("three hundred") << qreal(300) << int(100) << qreal(300);
}
void DelegateSizeHelperTest::risingPercentage()
{
QFETCH(qreal, parentWidth);
QFETCH(int, currentPercentageWidth);
QFETCH(qreal, currentWidth);
DelegateSizeHelper delegateSizeHelper;
delegateSizeHelper.setStartBreakpoint(100);
delegateSizeHelper.setEndBreakpoint(200);
delegateSizeHelper.setStartPercentWidth(0);
delegateSizeHelper.setEndPercentWidth(100);
delegateSizeHelper.setParentWidth(parentWidth);
QCOMPARE(delegateSizeHelper.currentPercentageWidth(), currentPercentageWidth);
QCOMPARE(delegateSizeHelper.currentWidth(), currentWidth);
}
void DelegateSizeHelperTest::fallingPercentage_data()
{
QTest::addColumn<qreal>("parentWidth");
QTest::addColumn<int>("currentPercentageWidth");
QTest::addColumn<qreal>("currentWidth");
QTest::newRow("zero") << qreal(0) << int(100) << qreal(0);
QTest::newRow("one hundred") << qreal(100) << int(100) << qreal(100);
QTest::newRow("one fifty") << qreal(150) << int(50) << qreal(75);
QTest::newRow("two hundred") << qreal(200) << int(0) << qreal(0);
QTest::newRow("three hundred") << qreal(300) << int(0) << qreal(0);
}
void DelegateSizeHelperTest::fallingPercentage()
{
QFETCH(qreal, parentWidth);
QFETCH(int, currentPercentageWidth);
QFETCH(qreal, currentWidth);
DelegateSizeHelper delegateSizeHelper;
delegateSizeHelper.setStartBreakpoint(100);
delegateSizeHelper.setEndBreakpoint(200);
delegateSizeHelper.setStartPercentWidth(100);
delegateSizeHelper.setEndPercentWidth(0);
delegateSizeHelper.setParentWidth(parentWidth);
QCOMPARE(delegateSizeHelper.currentPercentageWidth(), currentPercentageWidth);
QCOMPARE(delegateSizeHelper.currentWidth(), currentWidth);
}
void DelegateSizeHelperTest::equalPercentage_data()
{
QTest::addColumn<qreal>("parentWidth");
QTest::addColumn<int>("currentPercentageWidth");
QTest::addColumn<qreal>("currentWidth");
QTest::newRow("zero") << qreal(0) << int(50) << qreal(0);
QTest::newRow("one hundred") << qreal(100) << int(50) << qreal(50);
QTest::newRow("one fifty") << qreal(150) << int(50) << qreal(75);
QTest::newRow("two hundred") << qreal(200) << int(50) << qreal(100);
QTest::newRow("three hundred") << qreal(300) << int(50) << qreal(150);
}
void DelegateSizeHelperTest::equalPercentage()
{
QFETCH(qreal, parentWidth);
QFETCH(int, currentPercentageWidth);
QFETCH(qreal, currentWidth);
DelegateSizeHelper delegateSizeHelper;
delegateSizeHelper.setStartBreakpoint(100);
delegateSizeHelper.setEndBreakpoint(200);
delegateSizeHelper.setStartPercentWidth(50);
delegateSizeHelper.setEndPercentWidth(50);
delegateSizeHelper.setParentWidth(parentWidth);
QCOMPARE(delegateSizeHelper.currentPercentageWidth(), currentPercentageWidth);
QCOMPARE(delegateSizeHelper.currentWidth(), currentWidth);
}
void DelegateSizeHelperTest::equalBreakpoint_data()
{
QTest::addColumn<int>("startPercentageWidth");
QTest::addColumn<int>("endPercentageWidth");
QTest::addColumn<int>("currentPercentageWidth");
QTest::addColumn<qreal>("currentWidth");
QTest::newRow("start higher") << int(100) << int(0) << int(-1) << qreal(0);
QTest::newRow("equal") << int(50) << int(50) << int(50) << qreal(500);
QTest::newRow("end higher") << int(0) << int(100) << int(-1) << qreal(0);
}
/**
* We expect a default return except in the case where the the two percentages are
* equal as that case can be calculated without dividing by zero.
*/
void DelegateSizeHelperTest::equalBreakpoint()
{
QFETCH(int, startPercentageWidth);
QFETCH(int, endPercentageWidth);
QFETCH(int, currentPercentageWidth);
QFETCH(qreal, currentWidth);
DelegateSizeHelper delegateSizeHelper;
delegateSizeHelper.setStartBreakpoint(100);
delegateSizeHelper.setEndBreakpoint(100);
delegateSizeHelper.setStartPercentWidth(startPercentageWidth);
delegateSizeHelper.setEndPercentWidth(endPercentageWidth);
delegateSizeHelper.setParentWidth(1000);
QCOMPARE(delegateSizeHelper.currentPercentageWidth(), currentPercentageWidth);
QCOMPARE(delegateSizeHelper.currentWidth(), currentWidth);
}
QTEST_GUILESS_MAIN(DelegateSizeHelperTest)
#include "delegatesizehelpertest.moc"

View File

@@ -136,7 +136,7 @@ void NeoChatRoomTest::initTestCase()
void NeoChatRoomTest::subtitleTextTest()
{
QCOMPARE(room->timelineSize(), 1);
QCOMPARE(room->lastEventToString(), QStringLiteral("@example:example.org: This is an example text message"));
QCOMPARE(room->subtitleText(), QStringLiteral("@example:example.org: This is an example text message"));
}
void NeoChatRoomTest::eventTest()
@@ -144,5 +144,5 @@ void NeoChatRoomTest::eventTest()
QCOMPARE(room->timelineSize(), 1);
}
QTEST_GUILESS_MAIN(NeoChatRoomTest)
QTEST_MAIN(NeoChatRoomTest)
#include "neochatroomtest.moc"

View File

@@ -1,652 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QTest>
#include "texthandler.h"
#include <qnamespace.h>
#include <quotient_common.h>
#include <syncdata.h>
using namespace Quotient;
class TestRoom : public NeoChatRoom
{
public:
using NeoChatRoom::NeoChatRoom;
void update(SyncRoomData &&data, bool fromCache = false)
{
Room::updateData(std::move(data), fromCache);
}
};
class TextHandlerTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void allowedAttributes();
void stripDisallowedTags();
void stripDisallowedAttributes();
void emptyCodeTags();
void sendSimpleStringCase();
void sendSingleParaMarkup();
void sendMultipleSectionMarkup();
void sendBadLinks();
void sendEscapeCode();
void sendCodeClass();
void receiveStripReply();
void receivePlainTextIn();
void receiveRichInPlainOut_data();
void receiveRichInPlainOut();
void receivePlainStripHtml();
void receivePlainStripMarkup();
void receiveStripNewlines();
void receiveRichUserPill();
void receiveRichStrikethrough();
void receiveRichtextIn();
void receiveRichMxcUrl();
void receiveRichPlainUrl();
void receiveRichEmote();
void receiveRichEdited_data();
void receiveRichEdited();
void receiveLineSeparator();
void linkPreviewsMatch_data();
void linkPreviewsMatch();
void linkPreviewsReject_data();
void linkPreviewsReject();
};
#ifdef QUOTIENT_07
void TextHandlerTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
const auto json = QJsonDocument::fromJson(R"EVENT({
"account_data": {
"events": [
{
"content": {
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
},
{
"content": {
"custom_config_key": "custom_config_value"
},
"type": "org.example.custom.room.config"
}
]
},
"ephemeral": {
"events": [
{
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.typing"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an **example** text message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>",
"msgtype": "m.text"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "/me This is an emote.",
"format": "org.matrix.custom.html",
"formatted_body": "This is an emote.",
"msgtype": "m.emote"
},
"event_id": "$153273582443PhrSn:example.org",
"origin_server_ts": 1532735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1231
}
},
{
"content": {
"body": "tested",
"msgtype": "m.text"
},
"event_id": "$zrCiBxBnqqTn0Z5FY78qSZAszno_w8nJJXzfBULG-3E",
"origin_server_ts": 1680948575928,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1747776,
"m.relations": {
"m.replace": {
"event_id": "$UX0PlpyI7vYO32iHMuuYEP7ECMh4sX3XLGiB2SwM4mQ",
"origin_server_ts": 1680948580992,
"sender": "@example:example.org"
}
}
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
})EVENT");
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
room->update(std::move(roomData));
}
#endif
void TextHandlerTest::allowedAttributes()
{
const QString testInputString1 = QStringLiteral("<p><span data-mx-spoiler><font color=#FFFFFF>Test</font><span></p>");
const QString testOutputString1 = QStringLiteral("<p><span data-mx-spoiler><font color=#FFFFFF>Test</font><span></p>");
// Handle urls where the href has either single (') or double (") quotes.
const QString testInputString2 = QStringLiteral("<p><a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a></p>");
const QString testOutputString2 = QStringLiteral("<p><a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a></p>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString1);
QCOMPARE(testTextHandler.handleSendText(), testOutputString1);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString1);
testTextHandler.setData(testInputString2);
QCOMPARE(testTextHandler.handleSendText(), testOutputString2);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString2);
}
void TextHandlerTest::stripDisallowedTags()
{
const QString testInputString = QStringLiteral("<p>Allowed</p> <span>Allowed</span> <body>Disallowed</body>");
const QString testOutputString = QStringLiteral("<p>Allowed</p> <span>Allowed</span> Disallowed");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::stripDisallowedAttributes()
{
const QString testInputString = QStringLiteral("<p style=\"font-size:50px;\" color=#FFFFFF>Test</p>");
const QString testOutputString = QStringLiteral("<p>Test</p>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
/**
* Make sure that empty code tags are handled.
* (this was a bug during development hence the test)
*/
void TextHandlerTest::emptyCodeTags()
{
const QString testInputString = QStringLiteral("<pre><code></code></pre>");
const QString testOutputString = QStringLiteral("<pre><code></code></pre>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::sendSimpleStringCase()
{
const QString testInputString = QStringLiteral("This data should just be put in a paragraph.");
const QString testOutputString = QStringLiteral("<p>This data should just be put in a paragraph.</p>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendSingleParaMarkup()
{
const QString testInputString = QStringLiteral(
"Text para with **bold**, *italic*, [link](https://kde.org), ![image](mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e), `inline code`.");
const QString testOutputString = QStringLiteral(
"<p>Text para with <strong>bold</strong>, <em>italic</em>, <a href=\"https://kde.org\">link</a>, <img "
"src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e\" alt=\"image\">, <code>inline code</code>.</p>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendMultipleSectionMarkup()
{
const QString testInputString =
QStringLiteral("Text para\n> blockquote\n* List 1\n* List 2\n1. one\n2. two\n# Heading 1\n## Heading 2\nhorizontal rule\n\n---\n```\ncodeblock\n```");
const QString testOutputString = QStringLiteral(
"<p>Text para</p>\n<blockquote>\n<p>blockquote</p>\n</blockquote>\n<ul>\n<li>List 1</li>\n<li>List "
"2</li>\n</ul>\n<ol>\n<li>one</li>\n<li>two</li>\n</ol>\n<h1>Heading 1</h1>\n<h2>Heading 2</h2>\n<p>horizontal "
"rule</p>\n<hr>\n<pre><code>codeblock\n</code></pre>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendBadLinks()
{
const QString testInputString = QStringLiteral("[link](kde.org), ![image](https://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e)");
const QString testOutputString = QStringLiteral("<p><a>link</a>, <img alt=\"image\"></p>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
/**
* All text between code tags is treated as plain so it should get escaped.
*/
void TextHandlerTest::sendEscapeCode()
{
const QString testInputString = QStringLiteral("```\n<p>Test <span style=\"font-size:50px;\">some</span> code</p>\n```");
const QString testOutputString =
QStringLiteral("<pre><code>&lt;p&gt;Test &lt;span style=&quot;font-size:50px;&quot;&gt;some&lt;/span&gt; code&lt;/p&gt;\n</code></pre>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendCodeClass()
{
const QString testInputString = QStringLiteral("```html\nsome code\n```\n<pre><code class=\"code-underline\">some more code</code></pre>");
const QString testOutputString = QStringLiteral("<pre><code class=\"language-html\">some code\n</code></pre>\n<pre><code>some more code</code></pre>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::receiveStripReply()
{
const QString testInputString = QStringLiteral(
"<mx-reply><blockquote><a href=\"https://matrix.to/#/!somewhere:example.org/$event:example.org\">In reply to</a><a "
"href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a><br />Message replied to.</blockquote></mx-reply>Reply message.");
const QString testOutputString = QStringLiteral("Reply message.");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString);
}
void TextHandlerTest::receiveRichInPlainOut_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::newRow("ampersand") << QStringLiteral("a &amp; b") << QStringLiteral("a & b");
QTest::newRow("quote") << QStringLiteral("&quot;a and b&quot;") << QStringLiteral("\"a and b\"");
}
void TextHandlerTest::receiveRichInPlainOut()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testOutputString);
}
void TextHandlerTest::receivePlainTextIn()
{
const QString testInputString = QStringLiteral("<plain text in tag bracket>\nTest link https://kde.org.");
const QString testOutputStringRich = QStringLiteral("&lt;plain text in tag bracket&gt;<br>Test link <a href=\"https://kde.org\">https://kde.org</a>.");
QString testOutputStringPlain = QStringLiteral("<plain text in tag bracket>\nTest link https://kde.org.");
// Make sure quotes are maintained in a plain string.
const QString testInputString2 = QStringLiteral("last line is \"Time to switch to a new topic.\"");
const QString testOutputString2 = QStringLiteral("last line is \"Time to switch to a new topic.\"");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText), testOutputStringRich);
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputStringPlain);
testTextHandler.setData(testInputString2);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText), testOutputString2);
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString2);
}
void TextHandlerTest::receiveStripNewlines()
{
const QString testInputStringPlain = QStringLiteral("Test\nmany\nnew\nlines.");
const QString testInputStringRich = QStringLiteral("Test<br>many<br />new<br>lines.");
const QString testOutputString = QStringLiteral("Test many new lines.");
const QString testInputStringPlain2 = QStringLiteral("* List\n* Items");
const QString testOutputString2 = QStringLiteral("List Items");
TextHandler testTextHandler;
testTextHandler.setData(testInputStringPlain);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::PlainText, true), testOutputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText, nullptr, nullptr, true), testOutputString);
testTextHandler.setData(testInputStringRich);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText, true), testOutputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, nullptr, nullptr, true), testOutputString);
testTextHandler.setData(testInputStringPlain2);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText, true), testOutputString2);
}
/**
* For a plain text output of a received string all html is stripped except for
* code which is unescaped if it's html.
*/
void TextHandlerTest::receivePlainStripHtml()
{
const QString testInputString = QStringLiteral("<p>Test</p> <pre><code>Some code <strong>with tags</strong></code></pre>");
const QString testOutputString = QStringLiteral("Test Some code <strong>with tags</strong>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testOutputString);
}
void TextHandlerTest::receivePlainStripMarkup()
{
const QString testInputString = QStringLiteral("**bold** `<p>inline code</p>` *italic*");
const QString testOutputString = QStringLiteral("bold <p>inline code</p> italic");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString);
}
void TextHandlerTest::receiveRichUserPill()
{
const QString testInputString = QStringLiteral("<p><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></p>");
const QString testOutputString = QStringLiteral("<p><b><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></b></p>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::receiveRichStrikethrough()
{
const QString testInputString = QStringLiteral("<p><del>Test</del></p>");
const QString testOutputString = QStringLiteral("<p><s>Test</s></p>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::receiveRichtextIn()
{
const QString testInputString = QStringLiteral("<p>Test</p> <pre><code>Some code <strong>with tags</strong></code></pre>");
const QString testOutputString = QStringLiteral("<p>Test</p> <pre><code>Some code &lt;strong&gt;with tags&lt;/strong&gt;</code></pre>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
#ifdef QUOTIENT_07
void TextHandlerTest::receiveRichMxcUrl()
{
const QString testInputString = QStringLiteral(
"<img src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e\" alt=\"image\"><img src=\"mxc://kde.org/34c3464b3a1bd7f55af2d559e07d2c773c430e73\" "
"alt=\"image\">");
const QString testOutputString = QStringLiteral(
"<img "
"src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e?user_id=@bob:kde.org&room_id=%23myroom:kde.org&event_id=$143273582443PhrSn:example.org\" "
"alt=\"image\"><img "
"src=\"mxc://kde.org/34c3464b3a1bd7f55af2d559e07d2c773c430e73?user_id=@bob:kde.org&room_id=%23myroom:kde.org&event_id=$143273582443PhrSn:example.org\" "
"alt=\"image\">");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(0).get()), testOutputString);
}
#endif
/**
* For when your rich input string has a plain text url left in.
*
* This test is to show that a url that is already rich will be left alone but a
* plain one will be linkified.
*/
void TextHandlerTest::receiveRichPlainUrl()
{
// This is an actual link that caused trouble which is why it's so long. Keeping
// so we can confirm consistent behaviour for complex urls.
const QString testInputStringLink1 = QStringLiteral(
"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im "
"<a "
"href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/"
"$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">Link already rich</a>");
const QString testOutputStringLink1 = QStringLiteral(
"<a "
"href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/"
"$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">https://matrix.to/#/"
"!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im</a> <a "
"href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/"
"$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">Link already rich</a>");
// Another real case. The linkification wasn't handling it when a single link
// contains what looks like and email. It was been broken into 3 but needs to
// be just single link.
const QString testInputStringLink2 = QStringLiteral("https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/");
const QString testOutputStringLink2 = QStringLiteral(
"<a "
"href=\"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/\">https://lore.kernel.org/lkml/"
"CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/</a>");
QString testInputStringEmail = QStringLiteral(R"(email@example.com <a href="mailto:email@example.com">Link already rich</a>)");
QString testOutputStringEmail =
QStringLiteral(R"(<a href="mailto:email@example.com">email@example.com</a> <a href="mailto:email@example.com">Link already rich</a>)");
QString testInputStringMxId = QStringLiteral("@user:kde.org <a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a>");
QString testOutputStringMxId = QStringLiteral(
"<b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a></b>");
TextHandler testTextHandler;
testTextHandler.setData(testInputStringLink1);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringLink1);
testTextHandler.setData(testInputStringLink2);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringLink2);
testTextHandler.setData(testInputStringEmail);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringEmail);
testTextHandler.setData(testInputStringMxId);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxId);
}
// Test that user pill is add to an emote message.
// N.B. The second message in the test timeline is marked as an emote.
void TextHandlerTest::receiveRichEmote()
{
auto event = room->messageEvents().at(1).get();
auto author = static_cast<NeoChatUser *>(room->user(event->senderId()));
const QString testInputString = QStringLiteral("This is an emote.");
const QString testOutputString = QStringLiteral("* <a href=\"https://matrix.to/#/@example:example.org\" style=\"color:") + author->color().name()
+ QStringLiteral("\">@example:example.org</a> This is an emote.");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, event), testOutputString);
}
void TextHandlerTest::receiveRichEdited_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::newRow("basic") << QStringLiteral("Edited") << QStringLiteral("Edited <span style=\"color:#000000\">(edited)</span>");
QTest::newRow("multiple paragraphs") << QStringLiteral("<p>Edited</p>\n<p>Edited</p>")
<< QStringLiteral("<p>Edited</p>\n<p>Edited <span style=\"color:#000000\">(edited)</span></p>");
QTest::newRow("blockquote") << QStringLiteral("<blockquote>Edited</blockquote>")
<< QStringLiteral("<blockquote>Edited</blockquote><p> <span style=\"color:#000000\">(edited)</span></p>");
}
void TextHandlerTest::receiveRichEdited()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(2).get()), testOutputString);
}
void TextHandlerTest::receiveLineSeparator()
{
auto text = QStringLiteral("foo\u2028bar");
TextHandler textHandler;
textHandler.setData(text);
QCOMPARE(textHandler.handleRecievePlainText(Qt::PlainText, true), QStringLiteral("foo bar"));
}
void TextHandlerTest::linkPreviewsMatch_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QList<QUrl>>("testOutputLinks");
QTest::newRow("plainHttps") << QStringLiteral("https://kde.org") << QList<QUrl>({QUrl("https://kde.org")});
QTest::newRow("richHttps") << QStringLiteral("<a href=\"https://kde.org\">Rich Link</a>") << QList<QUrl>({QUrl("https://kde.org")});
QTest::newRow("plainWww") << QStringLiteral("www.example.org") << QList<QUrl>({QUrl("www.example.org")});
QTest::newRow("multipleHttps") << QStringLiteral("https://kde.org www.example.org")
<< QList<QUrl>({
QUrl("https://kde.org"),
QUrl("www.example.org"),
});
}
void TextHandlerTest::linkPreviewsMatch()
{
QFETCH(QString, testInputString);
QFETCH(QList<QUrl>, testOutputLinks);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.getLinkPreviews(), testOutputLinks);
}
void TextHandlerTest::linkPreviewsReject_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QList<QUrl>>("testOutputLinks");
QTest::newRow("mxc") << QStringLiteral("mxc://example.org/SEsfnsuifSDFSSEF") << QList<QUrl>();
QTest::newRow("matrixTo") << QStringLiteral("https://matrix.to/#/@alice:example.org") << QList<QUrl>();
QTest::newRow("noSpace") << QStringLiteral("testhttps://kde.org") << QList<QUrl>();
}
void TextHandlerTest::linkPreviewsReject()
{
QFETCH(QString, testInputString);
QFETCH(QList<QUrl>, testOutputLinks);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.getLinkPreviews(), testOutputLinks);
}
QTEST_GUILESS_MAIN(TextHandlerTest)
#include "texthandlertest.moc"

View File

@@ -2,7 +2,7 @@
<!--
- SPDX-License-Identifier: CC0-1.0
- SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carlschwan@kde.org>
- SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
- SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
-->
<component type="desktop">
<id>org.kde.neochat</id>
@@ -22,7 +22,6 @@
<name xml:lang="eu">NeoChat</name>
<name xml:lang="fi">NeoChat</name>
<name xml:lang="fr">NeoChat</name>
<name xml:lang="gl">NeoChat</name>
<name xml:lang="hu">NeoChat</name>
<name xml:lang="ia">Neochat</name>
<name xml:lang="id">NeoChat</name>
@@ -48,8 +47,8 @@
<summary>A client for matrix, the decentralized communication protocol</summary>
<summary xml:lang="ar">عميل لماتركس، ميفاق الاتصال اللامركزي</summary>
<summary xml:lang="az">Matrix üçün müştəri, mərkəzləşməmiş kommunikasiya protokolu</summary>
<summary xml:lang="ca">Un client de Matrix, el protocol de comunicacions descentralitzat</summary>
<summary xml:lang="ca-valencia">Un client de Matrix, el protocol de comunicacions descentralitzat</summary>
<summary xml:lang="ca">Un client per al Matrix, el protocol de comunicacions descentralitzat</summary>
<summary xml:lang="ca-valencia">Un client per a Matrix, el protocol de comunicacions descentralitzat</summary>
<summary xml:lang="cs">Klient pro decentralizovaný komunikační protokol matrix</summary>
<summary xml:lang="de">Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll</summary>
<summary xml:lang="el">Ένας πελάτης για το Matrix, το αποκεντρωμένο πρωτόκολλο επικοινωνίας</summary>
@@ -58,7 +57,6 @@
<summary xml:lang="eu">Matrix, deszentralizatutako komunikazio protokolorako bezero bat</summary>
<summary xml:lang="fi">Asiakas Matrixille, hajautetulle viestintäyhteyskäytännölle</summary>
<summary xml:lang="fr">Un client pour « Matrix », le protocole décentralisé de communications.</summary>
<summary xml:lang="gl">Un cliente para Matrix, o protocolo de comunicación descentralizada</summary>
<summary xml:lang="hu">Kliens a matrixhoz, a decentralizált kommunikációs protokollhoz</summary>
<summary xml:lang="ia">Un cliente per Matrix, le protocollo de communication decentralisate</summary>
<summary xml:lang="id">Klien untuk matrix, protokol komunikasi terdesentralisasi</summary>
@@ -76,123 +74,104 @@
<summary xml:lang="sk">Klient pre matrix, decentralizovaný komunikačný protokol</summary>
<summary xml:lang="sl">Odjemalec za matrix, decentralizirani komunikacijski protokol</summary>
<summary xml:lang="sv">En klient för Matrix, det decentraliserade kommunikationsprotokollet</summary>
<summary xml:lang="ta">மையமில்லா தகவல் பரிமாற்ற நெறிமுறையான மேட்ரிக்ஸுக்கான செயலி</summary>
<summary xml:lang="tr">Merkezi olmayan iletişim protokolü Matrix için bir istemci</summary>
<summary xml:lang="uk">Клієнт matrix, децентралізованого протоколу обміну даними</summary>
<summary xml:lang="x-test">xxA client for matrix, the decentralized communication protocolxx</summary>
<summary xml:lang="zh-CN">分布式通讯协议 Matrix 的客户端</summary>
<description>
<p>NeoChat is a client for Matrix, the decentralized 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="ar">نيوتشات هو عميل ماتركس Matrix، (ميفاق الاتصال اللامركزي للمراسلة الفورية). يتيح لك نيوتشات إرسال رسائل نصية ومقاطع فيديو وملفات صوتية إلى عائلتك وزملائك وأصدقائك. يستخدم أطر عمل كيدي وأبرزها Kirigami لتوفير تجربة متقاربة عبر منصات متعددة.</p>
<p xml:lang="ca">El NeoChat és un client de Matrix, el protocol descentralitzat de comunicacions de missatgeria instantània. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics. Fa servir els Frameworks de KDE i, sobretot, el Kirigami per a proporcionar una experiència convergent a través de diverses plataformes.</p>
<p xml:lang="ca-valencia">NeoChat és un client de Matrix, el protocol descentralitzat de comunicacions de missatgeria instantània. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics. Utilitza els Frameworks de KDE i, sobretot, Kirigami per a proporcionar una experiència convergent a través de diverses plataformes.</p>
<p xml:lang="en-GB">NeoChat is a client for Matrix, the decentralised communication protocol for instant messaging. It allows you to send text messages, videos and audio files to your family, colleagues and friends. It uses KDE frameworks and most notably Kirigami to provide a convergent experience across multiple platforms.</p>
<p xml:lang="es">NeoChat es un cliente para Matrix, el protocolo de comunicaciones descentralizado para mensajería instantánea. Le permite enviar mensajes de texto, vídeos y archivos de sonido a su familia, compañeros de trabajo y amigos. Usa la infraestructura de KDE y, en particular, Kirigami para proporcionar una experiencia convergente en muchas plataformas.</p>
<p xml:lang="fr">NeoChat est un client pour le protocole Matrix, un protocole décentralisé de communications pour messagerie instantané. Il vous permet d'envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos amis. Il utilise les environnements de développement et plus précisément Kirigami pour fournir une expérience convergente sur plusieurs plate-formes. </p>
<p xml:lang="gl">NeoChat é un cliente para Matrix, o protocolo de comunicación descentralizada para mensaxería instantánea. Podes enviar mensaxes de texto, vídeos e ficheiros de son á túa familia, colegas e amizades. Usas infraestruturas de KDE e principalmente Kirigami para proporcionar unha experiencia de uso converxente para varias plataformas.</p>
<p xml:lang="ia">NeoChat es un cliente per Matrix, le protocollo de communication decentralisate per messager instantanee. Illo te permitte inviar messager de texto, files de video e audio a tu familia, collegas e amicos usante. Illo usa KDE frameworks e super toto Kirigamii forni un experientia convergente trans platteforme multiple.</p>
<p xml:lang="it">NeoChat è un client per Matrix, il protocollo di comunicazione decentralizzato per la messaggistica istantanea. Ti consente di inviare messaggi di testo, video e file audio a familiari, colleghi e amici. Utilizza i framework KDE e in particolare Kirigami per fornire un'esperienza convergente su più piattaforme.</p>
<p>NeoChat is a Matrix client. It allows you to send text messages, videos and audio files to your family, colleagues and friends using the Matrix protocol.</p>
<p xml:lang="ar">نيوتشات هو عميل ماتركس Matrix. يتيح لك إرسال رسائل نصية ومقاطع فيديو وملفات صوتية إلى عائلتك وزملائك وأصدقائك باستخدام بروتوكول ماتركس</p>
<p xml:lang="az">NeoChat Mtrix müştərisidir. O, Matrix protokolundan istifadə edərək, ailənizə, dostlarınıza, iş yoldaşlarınıza mətn, səsli və görüntülü ismarıclar göndərməyə imkan verir.</p>
<p xml:lang="ca">El NeoChat és un client de Matrix. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics usant el protocol Matrix.</p>
<p xml:lang="ca-valencia">NeoChat és un client de Matrix. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics utilitzant el protocol Matrix.</p>
<p xml:lang="de">NeoChat ist ein Matrix-Client. Er ermöglicht Ihnen das Senden von Textnachrichten, Videos und Audiodateien an Ihre Familie, Kollegen und Freunde unter Verwendung des Matrix-Protokolls.</p>
<p xml:lang="el">Το NeoChat είναι μια εφαρμογή του Matrix. Σας επιτρέπει να στέλνετε μηνύματα κειμένου, βίντεο και ήχο στην οικογένειά σας, σε συναδέλφους και φίλους με το πρωτόκολλο Matrix.</p>
<p xml:lang="en-GB">NeoChat is a Matrix client. It allows you to send text messages, videos and audio files to your family, colleagues and friends using the Matrix protocol.</p>
<p xml:lang="es">NeoChat es un cliente para Matrix. Le permite enviar mensajes de texto, vídeos y archivos de sonido a su familia, compañeros de trabajo y amigos usando el protocolo Matrix.</p>
<p xml:lang="eu">NeoChat Matrix bezero bat da. Familiari, lankideei eta lagunei testu-mezuak, bideoak eta audio-fitxategiak bidaltzeko aukera ematen du, Matrix protokoloa erabiliz.</p>
<p xml:lang="fi">NeoChat on Matrix-asiakas. Sillä voi lähettää perheelle, tuttaville ja kavereille tekstiviestejä sekä video- ja äänitiedostoja Matrix-yhteyskäytännöllä.</p>
<p xml:lang="fr">NeoChat est un client Matrix. Il vous permet d'envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos amis en utilisant le protocole Matrix.</p>
<p xml:lang="hu">A NeoChat egy Matrix kliens. Szöveges üzeneteket, videókat ésaudio fájlokat küldhet családjának, kollégáinak és barátainak a Matrix protokoll használatával.</p>
<p xml:lang="ia">NeoChat es un cliente de Matrix. Illo te permitte inviar messager de texto, files de video e audio a tu familia, collegas e amicos usante le protocollo de Matrix.</p>
<p xml:lang="id">NeoChat adalah sebuah klien Matrix. Memungkinkan Anda untuk mengirim pesan teks, file video dan audio ke keluarga, kolega dan teman Anda menggunakan protokol Matrix.</p>
<p xml:lang="ie">NeoChat es un cliente de Matrix. On posse usar it por inviar missages textual, video- e audiofiles a su familie, colegos e amics med li protocol Matrix.</p>
<p xml:lang="it">NeoChat è un client Matrix. Ti consente di inviare messaggi di testo, file video e audio a familiari, colleghi e amici utilizzando il protocollo Matrix.</p>
<p xml:lang="ka">NeoChat არის Matrix კლიენტი. ის საშუალებას გაძლევთ გაგზავნოთ ტექსტური შეტყობინებები, ვიდეოები და აუდიო ფაილები თქვენს ოჯახს, კოლეგებსა და მეგობრებს მატრიქსის პროტოკოლის გამოყენებით.</p>
<p xml:lang="nl">NeoChat is een client voor Matrix, het gedecentraliseerde communicatieprotocol voor instant messages. Het biedt u het verzenden van tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden. Het gebruik KDE frameworks en het meest opmerkelijk Kirigami om een convergente ervaring te leveren op meerdere platforms.</p>
<p xml:lang="pl">NeoChat jest programem do Matriksa, protokołu rozproszonego porozumiewania się w czasie rzeczywistym. Umożliwia wysyłanie wiadomości tekstowych, filmów oraz dźwięku do twojej rodziny, znajomych oraz przyjaciół. Używa szkieletów KDE i głównie Kirigami, aby zapewnić spójne wrażenia na wielu platformach</p>
<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>
<p xml:lang="tr">NeoChat, anlık iletileşme için merkezi olmayan iletişim protokolü olan Matrix için bir istemcidir. Ailenize, iş arkadaşlarınıza ve arkadaşlarınıza metin iletiler, videolar ve ses dosyaları göndermenize olanak tanır. Birden çok platformda yakınsak bir deneyim sağlamak için KDE Frameworks ve en önemlilerinden Kirigami'yi kullanır.</p>
<p xml:lang="uk">NeoChat — клієнт Matrix, децентралізованого протоколу спілкування для миттєвого обміну повідомленнями. За його допомогою ви можете надсилати текстові повідомлення, відео та звукові файли вашій родин, колегами та друзям. У програмі використано бібліотеки KDE, зокрема Kirigami, для надання однорідного середовища на декількох програмних та апаратних платформах.</p>
<p xml:lang="x-test">xxNeoChat is a client for Matrix, the decentralized 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.xx</p>
<p>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="ar">يهدف نيوتشات إلى أن يكون تطبيقًا كامل الميزات لمواصفات ماتركس. على هذا النحو يتم دعم كل شيء في المواصفات المستقرة الحالية مع الاستثناءات الملحوظة لـ VoIP والخيوط وبعض جوانب التشفير من طرف إلى طرف. هناك عدد قليل من الإغفالات الصغيرة الأخرى بسبب حقيقة أن مواصفات ماتركس تتطور باستمرار ، ولكن يبقى الهدف توفير الدعم النهائي للمواصفات بأكملها.</p>
<p xml:lang="ca">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de la VoIP, fils i alguns aspectes de l'encriptatge d'extrem a extrem. Hi ha algunes altres omissions més petites a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu segueix sent proporcionar suport eventual per a tota l'especificació.</p>
<p xml:lang="ca-valencia">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de VoIP, fils i alguns aspectes de l'encriptació d'extrem a extrem. Hi ha algunes altres omissions més xicotetes a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu seguix sent proporcionar suport eventual per a tota l'especificació.</p>
<p xml:lang="en-GB">NeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.</p>
<p xml:lang="es">NeoChat pretende ser una aplicación con todas las funciones para la especificación de Matrix. Como tal, admite todo en la especificación estable actual, con las notables excepciones de VoIP, subprocesos y algunas funciones de cifrado de extremo a extremo. Existen algunas omisiones menos importantes debido al hecho de que la especificación de Matrix está en constante evolución, pero el objetivo sigue siendo brindar compatibilidad final con toda la especificación.</p>
<p xml:lang="fr">L'objectif de NeoChat est d'être une application complète pour le protocole Matrix. En tant que tel, tout dans la spécification stable actuelle avec les exceptions notables de VoIP, les processus et certains aspects du chiffrement de bout en bout sont pris en charge. Il y a quelques autres petites omissions en raison du fait que la spécification du protocole Matrix est en constante évolution. Cependant, l'objectif reste de fournir un soutien éventuel pour l'ensemble de la spécification.</p>
<p xml:lang="gl">NeoChat pretende ser unha aplicación completa para a especificación de Matrix. Coas excepcións de VoIP, conversas fiadas e algúns aspectos da cifraxe de extremo a extremo, a versión estábel segue as especificacións. Existen algunhas outras pequenas omisións debido ao feito de que Matrix está en continua evolución pero a intención é implementar a especificación completa.</p>
<p xml:lang="ia">NeoChat aspira a esser un application plenemente eminente per le specification de Matrix. Tal como omne cosas in le specification currentemente stabile con le exceptiones notabile de VOIP, threads e alcun aspectos del cryptation End-to-End es supportate. Il ha ltere pauc omissiones, debite al facto que le specification de Matrix es in evolution constante ma le aspiration remane a fornir supporto eventual per le integre specification.</p>
<p xml:lang="it">NeoChat mira ad essere un'applicazione completa per le specifiche Matrix. Pertanto, sono supportati tutti gli elementi dell'attuale specifica stabile con le notevoli eccezioni di VoIP, conversazioni e alcuni aspetti della cifratura end-to-end. Ci sono alcune altre piccole omissioni dovute al fatto che le specifiche Matrix sono in continua evoluzione, ma l'obiettivo rimane quello di fornire un eventuale supporto per l'intera specifica.</p>
<p xml:lang="ka">NeoChat-ი მიზნად ისახავს Matrix სპეციფიკაციის სრული განხორციელება ჰქონდეს. როგორც ასეთი, ყველაფერი მიმდინარე სპეციფიკაციიდან, VoIP-ის, ძაფებისა და გამჭოლი დაშიფვრის ზოგიერთი ასპექტის გარდა, მხარდაჭერილია. შეძლება ასევე იყოს მცირე ლაფსუსებიც იმის გამო, რომ Matrix-ის სპეციფიკაცია მუდმივად ვითარგდება, მაგრამ ჩვენი მიზანი მისი სრული მხარდაჭერაა.</p>
<p xml:lang="nl">NeoChat richt zich op het volledig bieden van alle mogelijkheden van de Matrix-specificatie. Alles in de huidige stabiele specificatie met merkbare uitzondering van VoIP, gekoppelde discussies en sommige aspecten van eind-tot-eind versleuteling worden ondersteund. Er zijn een paar andere kleinere omissies vanwege het feit dat de Matrix specificatie constant evolueert maar het doel blijft het eventueel bieden van ondersteuning van de gehele specificatie.</p>
<p xml:lang="pl">NeoChat w zamyśle ma być pełnowartościową aplikacją wg wytycznych Matriksa. Z tego powodu, wszystko, co jest obecnie w stabilnych wytycznych z pominięciem VoIP, wątków i niektórych części szyfrowania Użytkownik-do-Użytkownika są obecnie obsługiwane. Pominięto też kilka mniejszych rzeczy ze względu na ciągły rozwój wytycznych Matriksa, lecz celem nadal jest zapewnienie obsługi wszystkich wytycznych.</p>
<p xml:lang="pt">O NeoChat pretende ser uma aplicação completa para a especificação do Matrix. Como tal, tudo o que existe na especificação estável actual, com as notáveis excepções do VoIP, tópicos e alguns aspectos da Encriptação Ponto-a-Ponto, são suportados. Existem mais algumas omissões, devido ao facto que a norma do Matrix está em constante evolução, mas o objectivo continua a ser oferecer o suporte eventual para a norma por inteiro.</p>
<p xml:lang="sl">Neochat cilja, da bi bila popolna aplikacija po specifikaciji Matrixa. Kot takšna vsebuje vse v trenutni stabilni specifikaciji z pomembnimi izjemami pri VoIP, nitih in nekaterih vidikov šifriranja od konca do konca. Obstaja nekaj drugih manjših opustitev zaradi dejstva, da se specifikacija Matrix nenehno razvija, vendar cilj ostaja zagotoviti morebitno podporo celotni specifikaciji.</p>
<p xml:lang="tr">NeoChat, Matrix belirtimi için tam özellikli bir uygulama olmayı hedefler. Bu nedenle; VoIP, ileti zincirleri ve Uçtan Uca Şifreleme'nin bazı yönleri gibi dikkate değer istisnalar dışında var olan kararlı belirtimdeki her şey desteklenir. Matrix belirtiminin sürekli gelişmesi nedeniyle birkaç küçük eksiklik daha var; ancak amaç tüm belirtim için nihai destek sağlamak olmayı sürdürüyor.</p>
<p xml:lang="uk">Метою створення NeoChat є повноцінна реалізація програми для специфікації Matrix. Як наслідок, реалізовано усе у поточній стабільній специфікації, окрім голосового інтернет-зв'язку, потоків та деяких аспектів міжвузлового шифрування. Є також декілька інших незначних прогалин через те, що специфікація Matrix постійно змінюється, але метою лишається повна підтримка специфікації.</p>
<p xml:lang="x-test">xxNeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.xx</p>
<p>Due to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:</p>
<p xml:lang="ar">نظرًا لطبيعة تطوير مواصفات ماتركس، يدعم نيوتشات أيضًا العديد من الميزات غير المستقرة وهي:</p>
<p xml:lang="ca">A causa de la naturalesa del desenvolupament de l'especificació de Matrix, el NeoChat també implementa nombroses característiques inestables. Actualment són:</p>
<p xml:lang="ca-valencia">A causa de la naturalea del desenvolupament de l'especificació de Matrix, NeoChat també implementa nombroses característiques inestables. Actualment són:</p>
<p xml:lang="en-GB">Due to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:</p>
<p xml:lang="es">Debido a la naturaleza del desarrollo de la especificación de Matrix, NeoChat también permite numerosas funciones no estables, como:</p>
<p xml:lang="fr">En raison de la nature du développement des spécifications du protocole Matrix, NeoChat prend également en charge de nombreuses fonctionnalités instables. Actuellement, ce sont :</p>
<p xml:lang="gl">Debido á natureza do desenvolvemento da especificación de Matrix, NeoChat tamén inclúe varias funcionalidades non estábeis:</p>
<p xml:lang="ia">Debite al natura del disveloppamento de specification de Matrix NeoChat tamben supporta numerose characteristicas instabile. Currentemente istes es:</p>
<p xml:lang="it">A causa della natura dello sviluppo delle specifiche Matrix, NeoChat supporta anche numerose funzionalità instabili. Attualmente queste sono:</p>
<p xml:lang="ka">Matrix-ის სპეციფიკაციის განვითარების ბუნების გამო NeoChat-ს ასევე აქვს უამრავი არასტაბილური ფუნქციაც. ახლა ისინია:</p>
<p xml:lang="nl">Vanwege de aard van de ontwikkeling van de Matrix specificatie ondersteunt NeoChat ook talloze onstabiele mogelijkheden. Dit zijn nu:</p>
<p xml:lang="pl">Ze względu na sposób rozwoju Matriksa, NeoChat obsługuje także kilka niestabilnych możliwości. Obecnie są to:</p>
<p xml:lang="pt">Devido à natureza do desenvolvimento da especificação do Matrix, o NeoChat também suporta diversas funcionalidades instáveis. De momento são:</p>
<p xml:lang="sl">Zaradi narave razvoja specifikacije Matrixa NeoChat podpira tudi številne nestabilne zmožnosti. Trenutno so to:</p>
<p xml:lang="tr">NeoChat, Matrix belirtimi geliştirmesinin doğası gereği çok sayıda kararsız özelliği de destekler. Şu anda bunlar:</p>
<p xml:lang="uk">Через природу розробки специфікації Matrix, у NeoChat також передбачено підтримку численних нестабільних можливостей. У поточній версії цими можливостями є:</p>
<p xml:lang="x-test">xxDue to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:xx</p>
<ul>
<li>Polls - MSC3381</li>
<li xml:lang="ar">التصويت - MSC3381</li>
<li xml:lang="ca">Enquestes - MSC3381</li>
<li xml:lang="ca-valencia">Enquestes - MSC3381</li>
<li xml:lang="en-GB">Polls - MSC3381</li>
<li xml:lang="es">Encuestas - MSC3381</li>
<li xml:lang="fr">Sondages - MSC3381</li>
<li xml:lang="gl">Enquisas - MSC3381</li>
<li xml:lang="ia">Inquestas - MSC3381</li>
<li xml:lang="it">Sondaggi - MSC3381</li>
<li xml:lang="ka">Polls - MSC3381</li>
<li xml:lang="nl">Polls - MSC3381</li>
<li xml:lang="pt">Inquéritos - MSC3381</li>
<li xml:lang="sl">Polls - MSC3381</li>
<li xml:lang="ta">வாக்கெடுப்புகள் - MSC3381</li>
<li xml:lang="tr">Anketler - MSC3381</li>
<li xml:lang="uk">Опитування - MSC3381</li>
<li xml:lang="x-test">xxPolls - MSC3381xx</li>
<li>Sticker Packs - MSC2545</li>
<li xml:lang="ar">حزم الملصقات - MSC2545</li>
<li xml:lang="ca">Paquets d'adhesius - MSC2545</li>
<li xml:lang="ca-valencia">Paquets d'adhesius - MSC2545</li>
<li xml:lang="en-GB">Sticker Packs - MSC2545</li>
<li xml:lang="es">Paquetes de pegatinas - MSC2545</li>
<li xml:lang="fr">Paquets d'auto-collants - MSC2545</li>
<li xml:lang="gl">Paquetes de adhesivos - MSC2545</li>
<li xml:lang="ia">Etiquetta gummate (sticker) -MSC2545</li>
<li xml:lang="it">Pacchetti di adesivi - MSC2545</li>
<li xml:lang="ka">სტიკერების პაკეტები - MSC2545</li>
<li xml:lang="nl">Sticker Packs - MSC2545</li>
<li xml:lang="pt">Pacotes de Autocolantes - MSC2545</li>
<li xml:lang="sl">Sticker Packs - MSC2545</li>
<li xml:lang="ta">ஒட்டி தொகுப்புகள் - MSC2545</li>
<li xml:lang="tr">Yapışkan Paketleri - MSC2545</li>
<li xml:lang="uk">Пакунки наліпок - MSC2545</li>
<li xml:lang="x-test">xxSticker Packs - MSC2545xx</li>
<li>Location Events - MSC3488</li>
<li xml:lang="ar">موقع الأحداث - MSC3488</li>
<li xml:lang="ca">Esdeveniments d'ubicació - MSC3488</li>
<li xml:lang="ca-valencia">Esdeveniments d'ubicació - MSC3488</li>
<li xml:lang="en-GB">Location Events - MSC3488</li>
<li xml:lang="es">Eventos de ubicación - MSC3488</li>
<li xml:lang="fr">Événements de lieu - MSC3488</li>
<li xml:lang="gl">Localización de eventos - MSC3488</li>
<li xml:lang="ia">Eventos de Location - MSC3488</li>
<li xml:lang="it">Località eventi - MSC3488</li>
<li xml:lang="ka">მდებარეობის მოვლენები - MSC3488</li>
<li xml:lang="nl">Locatie gebeurtenissen - MSC3488</li>
<li xml:lang="pt">Eventos com Localizações - MSC3488</li>
<li xml:lang="sl">Location Events - MSC3488</li>
<li xml:lang="ta">இட நிகழ்வுகள் - MSC3488</li>
<li xml:lang="tr">Konum Etkinlikleri - MSC3488</li>
<li xml:lang="uk">Місцеві зустрічі - MSC3488</li>
<li xml:lang="x-test">xxLocation Events - MSC3488xx</li>
</ul>
<p xml:lang="ko">NeoChat은 Matrix 클라이언트입니다. Matrix 프로토콜을 사용하여 가족, 동료, 친구에게 텍스트 메시지, 동영상, 오디오 파일을 전송할 수 있습니다.</p>
<p xml:lang="nl">NeoChat is een Matrix-client. Het biedt u het verzenden van tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden met het Matrix-protocol.</p>
<p xml:lang="pl">NeoChat jest programem do Matrisa. Umożliwia wysyłanie wiadomości tekstowych, filmów oraz dźwięku do twojej rodziny, znajomych oraz przyjaciół poprzez protokół Matriksa.</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.</p>
<p xml:lang="pt-BR">O NeoChat é um cliente Matrix. Ele permite a você enviar mensagens de texto, arquivos de vídeo e áudio para seus familiares, colegas e amigos usando o protocolo Matrix.</p>
<p xml:lang="ru">NeoChat — это клиент, поддерживающий работу с протоколом Matrix. Он позволяет отправлять текстовые сообщения, видео и аудиофайлы.</p>
<p xml:lang="sk">NeoChat je Matrix klient. Umožňuje vám posielať textové správy, videá a zvukové súbory rodine, kolegom a priateľom pomocou protokolu Matrix.</p>
<p xml:lang="sl">NeoChat je odjemalec Matrixa. Dovoljuje vam pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek vaši družini, kolegom in prijateljem z uporabo protokola Matrix.</p>
<p xml:lang="sv">NeoChat är en Matrix-klient. Den låter dig skicka textmeddelanden, videor och ljudfiler till din familj, kollegor och vänner med användning av Matrix-protokollet.</p>
<p xml:lang="tr">NeoChat, bir Matrix istemcisidir. Matrix protokolünü kullanarak ailenize, iş arkadaşlarınıza, arkadaşlarınıza ve başkalarına metin iletileri, video ve ses dosyaların göndermenize olanak verir.</p>
<p xml:lang="uk">NeoChat — клієнт мережі обміну повідомленнями Matrix. За допомогою цієї програми ви зможете надсилати текстові повідомлення, відео та звукові файли вашій родині, колегам та друзям за допомогою протоколу Matrix.</p>
<p xml:lang="x-test">xxNeoChat is a Matrix client. It allows you to send text messages, videos and audio files to your family, colleagues and friends using the Matrix protocol.xx</p>
<p xml:lang="zh-CN">NeoChat 是一个 Matrix 客户端。 它允许您使用 Matrix 协议向您的家人、同事和朋友发送文本消息、视频和音频文件。</p>
<p>Matrix is a decentralized communication protocol, putting the user back in control. Currently NeoChat implements large part of the protocol with the exception of encrypted chats and video chat.</p>
<p xml:lang="ar">ماتريكس هو بروتوكول اتصال لامركزي ، يعيد المستخدم إلى السيطرة. يطبق نيوتشات حاليًا جزءًا كبيرًا من الميفاق باستثناء الدردشات المشفرة ودردشة الفيديو.</p>
<p xml:lang="az">Matrix, istifadəçini nəzarətdə saxlayan, mərkəzləşməmişi rabitə protokoludur. NeoChat, söhbətin və video əlaqəsinin şifrələnməsindən başqa bir çox protokolları həyata keçirə bilir.</p>
<p xml:lang="ca">Matrix és un protocol de comunicacions descentralitzat, que retorna el control a l'usuari. Actualment el NeoChat implementa una gran part del protocol amb l'excepció dels xats encriptats i els xats de vídeo.</p>
<p xml:lang="ca-valencia">Matrix és un protocol de comunicacions descentralitzat, que retorna el control a l'usuari. Actualment NeoChat implementa una gran part del protocol amb l'excepció dels xats encriptats i els xats de vídeo.</p>
<p xml:lang="de">Matrix ist ein dezentralisiertes Kommunikationsprotokoll, das dem Benutzer wieder die Kontrolle zurückgibt. Derzeit implementiert NeoChat einen großen Teil des Protokolls mit der Ausnahme von verschlüsselten Chats und Video-Chat.</p>
<p xml:lang="el">Το Matrix είναι ένα αποκεντρωμένο πρωτόκολλο επικοινωνίας, δίνοντας πίσω στον χρήστη τον έλεγχο. Προς το παρόν το NeoChat υλοποιεί ένα μεγάλο μέρος του πρωτοκόλλου με εξαίρεση τις κρυπτογραφημένες συνομιλίες και τη συνομιλία με βίντεο.</p>
<p xml:lang="en-GB">Matrix is a decentralised communication protocol, putting the user back in control. Currently NeoChat implements large part of the protocol with the exception of encrypted chats and video chat.</p>
<p xml:lang="es">Matrix es un protocolo de comunicaciones descentralizado, que devuelve el control al usuario. En la actualidad, NeoChat implementa gran parte del protocolo con la excepción de chats cifrados y chats de vídeo.</p>
<p xml:lang="eu">Matrix komunikazio-protokolo deszentralizatu bat da, erabiltzaileari kontrola itzultzen diona. Gaur egun, NeoChat-ek protokoloaren zati handi bat inplementatzen du, berriketa zifratuak eta bideo berriketak izan ezik.</p>
<p xml:lang="fi">Matrix on hajautettu viestintäyhteyskäytäntö, joka antaa hallinnan takaisin käyttäjille. NeoChat tarjoaa nykyisellään valtaosan yhteyskäytännöstä salattuja keskustelu- ja videokeskusteluja lukuun ottamatta.</p>
<p xml:lang="fr">Matrix est un protocole de communication décentralisé, donnant le contrôle à l'utilisateur. Actuellement, NeoChat met en œuvre une grande partie du protocole, à l'exception des discussions chiffrées et du chat vidéo.</p>
<p xml:lang="hu">A Matrix egy decentralizált kommunikációs protokoll, amely a felhasználók kezébe adja az irányítást.</p>
<p xml:lang="ia">Matrix es un protocollo de communication decentrate, ponente le usator in le controlo. Currentemente NeoChat implementa un grande parte del protocollo con le exception de conversationes cryptate e conversationes video.</p>
<p xml:lang="id">Matrix adalah protokol komunikasi terdesentralisasi, menempatkan pengguna kembali dalam kendali. Saat ini NeoChat mengimplementasikan sebagian besar protokol dengan pengecualian obrolan terenkripsi dan obrolan video.</p>
<p xml:lang="ie">Matrix es un decentralisat protocol de communication quel retroda li control a un usator. Actualmen, NeoChat implementa un grand parte del protocol except ciffrat conversationes e videotelefonada.</p>
<p xml:lang="it">Matrix è un protocollo di comunicazione decentralizzato, che restituisce all'utente il controllo. Attualmente NeoChat implementa gran parte del protocollo ad eccezione delle chat cifrate e delle chat video.</p>
<p xml:lang="ka">Matrix -ი დეცენტრალიზებული კომუნიკაციის პროტოკოლია, რომელიც მომხმარებელს კონტროლის უფლებას უბრუნებს. ამჟამად NeoChat-ს პროტოკოლის ძირითადი ნაწილის მხარდაჭერა გააჩნია, დაშიფვრისა და ვიდეოჩატის გამოკლებით.</p>
<p xml:lang="ko">Matrix는 사용자에게 제어권을 돌려 주는 분산 통신 프로토콜입니다. NeoChat은 암호화된 대화 및 영상 통화를 제외한 프로토콜의 대부분 기능을 구현합니다.</p>
<p xml:lang="nl">Matrix is een gedecentraliseerd communicatieprotocol, dat de gebruiker de controle teruggeeft. Op dit moment implementeert NeoChat grote delen van het protocol met de uitzondering van versleutelde chats en video-chat.</p>
<p xml:lang="pl">Matrix jest protokołem rozproszonego porozumiewania się oddający użytkownikowi jego władzę. Obecnie NeoChat obsługuje dużą część protokołu poza szyfrowanymi rozmowami tekstowymi i z obrazem.</p>
<p xml:lang="pt">O Matrix é um protocolo de comunicações descentralizado, colocando de novo o utilizador no poder. De momento, o NeoChat implementa uma boa parte do protocolo, com a excepção das conversas encriptadas e as conversas de vídeo.</p>
<p xml:lang="pt-BR">O Matrix é um protocolo de comunicação descentralizado, colocando o usuário de volta no controle. Atualmente o NeoChat implementa grande parte do protocolo com a exceção de bate-papos criptografados e bate-papo por vídeo.</p>
<p xml:lang="ru">Matrix — это децентрализованный коммуникационный протокол, возвращающий пользователю контроль над своими данными. В настоящее время в приложении NeoChat реализована поддержка большей части протокола, за исключением зашифрованных чатов и видеочата.</p>
<p xml:lang="sk">Matrix je decentralizovaný komunikačný protokol, ktorý používateľovi vracia kontrolu. V súčasnosti NeoChat implementuje veľkú časť protokolu s výnimkou šifrovaných chatov a videohovorov.</p>
<p xml:lang="sl">Matrix je decentraliziran komunikacijski protokol, kjer ima uporabnik uporabnik kontrolo rabe. Trenutno ima NeoChat izveden velik del protokola z izjemo šifriranih klepetov in video klepetov.</p>
<p xml:lang="sv">Matrix är ett decentraliserat kommunikationsprotokoll, som ger tillbaka kontrollen till användaren. För närvarande implementerar NeoChat en stor del av protokollet, med undantag för krypterad chatt och videochatt.</p>
<p xml:lang="tr">Matrix; tam denetimi kullanıcıya bırakan, merkezi olmayan bir iletişim protokolüdür. Şu anda NeoChat, uçtan uca şifrelenmiş metin ve video sohbetleri dışında protokolün büyük bir bölümünü bünyesinde bulundurur.</p>
<p xml:lang="uk">Matrix — протокол децентралізованого спілкування, який передає контроль над даними користувачеві. У поточній версії NeoChat реалізовано більшу частину протоколу, окрім зашифрованого спілкування та відеоспілкування.</p>
<p xml:lang="x-test">xxMatrix is a decentralized communication protocol, putting the user back in control. Currently NeoChat implements large part of the protocol with the exception of encrypted chats and video chat.xx</p>
<p xml:lang="zh-CN">Matrix 是一个分布式通讯协议,使用户重新得到控制权。 目前NeoChat 实现了协议的大部分,除了加密聊天和视频聊天。</p>
<p>NeoChat works both on mobile and desktop while providing a consistent user experience.</p>
<p xml:lang="ar">يعمل نيوتشات على كل من الأجهزة المحمولة وسطح المكتب مع توفير تجربة مستخدم متسقة.</p>
<p xml:lang="az">Vahid istifadəçi interfeysi ilə təmin olunan NeoChat, həm mobil telefonda həm də kompyuterlərdə işləyir.</p>
<p xml:lang="ca">El NeoChat funciona en els mòbils i a l'escriptori, proporcionant una experiència d'usuari coherent.</p>
<p xml:lang="ca-valencia">NeoChat funciona en els mòbils i en l'escriptori, proporcionant una experiència d'usuari coherent.</p>
<p xml:lang="de">NeoChat funktioniert sowohl auf Mobilgeräten als auch auf dem PC und bietet ein einheitliches Benutzererlebnis.</p>
<p xml:lang="el">Το NeoChat λειτουργεί και στα κινητά και στους υπολογιστές γραφείου παρέχοντας μια αδιάλειπτη εμπειρία χρήσης.</p>
<p xml:lang="en-GB">NeoChat works both on mobile and desktop while providing a consistent user experience.</p>
<p xml:lang="es">NeoChat funciona en móviles y en el escritorio a la vez que proporciona una experiencia de usuario consistente.</p>
<p xml:lang="eu">NeoChat mugikorretan eta mahaigainean dabil, erabiltzaile esperientzia koherentea eskainiz.</p>
<p xml:lang="fi">NeoChat toimii sekä mobiili- että työpöytäalustoilla tarjoten yhdenmukaisen käyttökokemuksen.</p>
<p xml:lang="fr">NeoChat fonctionne aussi bien sur les mobiles que sur les ordinateurs de bureau, tout en offrant une expérience utilisateur cohérente.</p>
<p xml:lang="hu">A NeoChat mobilon és asztali számítógépen is működik, egységes felhasználói élményt nyújtva.</p>
<p xml:lang="ia">NeoChat functiona sia sur mobile que ur scriptorio durante que forni un experientia de usator consistente.</p>
<p xml:lang="id">NeoChat berfungsi baik di ponsel dan desktop sambil memberikan pengalaman pengguna yang konsisten.</p>
<p xml:lang="ie">NeoChat supporta amb computatores e aparates mobil, ancor providente un consistent experientie de usator.</p>
<p xml:lang="it">NeoChat funziona sia su dispositivi mobili che desktop, fornendo un'esperienza utente coerente.</p>
<p xml:lang="ka">NeoChat მუშაობს როგორც მობილურ, ისე ჩვეულებრივ კომპიუტერებზე იმ დროს, როცა მომხმარებლის ინტერფეისი არ იცვლება.</p>
<p xml:lang="ko">NeoChat은 모바일과 데스크톱 모두에서 일관된 사용자 경험을 제공합니다.</p>
<p xml:lang="nl">NeoChat werkt zowel op de mobiel en het bureaublad met het leveren van een consistente gebruikerservaring.</p>
<p xml:lang="pl">NeoChat działa zarówno na urządzeniach przenośnych jak i biurkowych, zapewniając spójne wrażenia użytkownika</p>
<p xml:lang="pt">O NeoChat funciona tanto em dispositivos móveis como no computador, fornecendo uma experiência de utilizador consistente.</p>
<p xml:lang="pt-BR">O NeoChat funciona tanto no celular como no computador enquanto fornece uma experiência consistente ao usuário.</p>
<p xml:lang="ru">NeoChat работает как на мобильных устройствах, так и на настольных компьютерах, обеспечивая единый пользовательский интерфейс.</p>
<p xml:lang="sk">NeoChat funguje na mobilných aj stolových počítačoch a poskytuje konzistentný používateľský zážitok.</p>
<p xml:lang="sl">NeoChat deluje tako na mobilnih kot na namiznih platformah z zagotavljanjem konsistentne uporabniške izkušnje.</p>
<p xml:lang="sv">NeoChat fungerar både på mobil och skrivbord och tillhandahåller en konsekvent användarupplevelse.</p>
<p xml:lang="tr">NeoChat, hem masaüstü hem de taşınabilir ortamlarda çalışarak tutarlı bir kullanıcı deneyimi sunar.</p>
<p xml:lang="uk">NeoChat працює на мобільних пристроях та звичайних комп'ютерах, маючи однорідний інтерфейс на усіх підтримуваних пристроях.</p>
<p xml:lang="x-test">xxNeoChat works both on mobile and desktop while providing a consistent user experience.xx</p>
<p xml:lang="zh-CN">NeoChat 在移动设备和桌面上均可用,并提供一致的用户体验。</p>
</description>
<url type="homepage">https://apps.kde.org/neochat/</url>
<url type="bugtracker">https://bugs.kde.org/buglist.cgi?component=General&amp;product=NeoChat</url>
@@ -200,7 +179,7 @@ to provide a convergent experience across multiple platforms.</p>
<category>Network</category>
</categories>
<developer_name>The KDE Community</developer_name>
<developer_name xml:lang="ar">مجتمع كِيدِي</developer_name>
<developer_name xml:lang="ar">مجتمع كدي</developer_name>
<developer_name xml:lang="az">KDE Cəmiyyəti</developer_name>
<developer_name xml:lang="ca">La comunitat KDE</developer_name>
<developer_name xml:lang="ca-valencia">La comunitat KDE</developer_name>
@@ -212,7 +191,6 @@ to provide a convergent experience across multiple platforms.</p>
<developer_name xml:lang="eu">KDE komunitatea</developer_name>
<developer_name xml:lang="fi">KDE-yhteisö</developer_name>
<developer_name xml:lang="fr">La communauté de KDE</developer_name>
<developer_name xml:lang="gl">A comunidade KDE</developer_name>
<developer_name xml:lang="hu">A KDE Közösség</developer_name>
<developer_name xml:lang="ia">Le communitate de KDE</developer_name>
<developer_name xml:lang="id">Komunitas KDE</developer_name>
@@ -230,7 +208,6 @@ to provide a convergent experience across multiple platforms.</p>
<developer_name xml:lang="sk">KDE Komunita</developer_name>
<developer_name xml:lang="sl">Skupnost KDE</developer_name>
<developer_name xml:lang="sv">KDE-gemenskapen</developer_name>
<developer_name xml:lang="ta">கே.டீ.யீ. சமூகம்</developer_name>
<developer_name xml:lang="tr">KDE Topluluğu</developer_name>
<developer_name xml:lang="uk">Спільнота KDE</developer_name>
<developer_name xml:lang="x-test">xxThe KDE Communityxx</developer_name>
@@ -240,7 +217,6 @@ to provide a convergent experience across multiple platforms.</p>
<custom>
<value key="KDE::matrix">#neochat:kde.org</value>
<value key="KDE::windows_store">https://www.microsoft.com/store/apps/9PNXWVNRC29H</value>
<value key="KDE::mastodon">https://kde.social/@neochat</value>
</custom>
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
<screenshots>
@@ -255,31 +231,6 @@ to provide a convergent experience across multiple platforms.</p>
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="23.04.2" date="2023-06-08"/>
<release version="23.04.1" date="2023-05-11"/>
<release version="23.04.0" date="2023-04-20">
<artifacts>
<artifact type="binary" platform="x86_64-windows-msvc">
<location>https://download.kde.org/stable/release-service/23.04.0/windows/neochat-23.04.0-512-windows-cl-msvc2019-x86_64.exe</location>
<checksum type="sha256">85e17fdd9b128bfdd171a93b11f7a95febeff0a00df9261df11f6ff67637686d</checksum>
<size type="download">58032464</size>
</artifact>
</artifacts>
</release>
<release version="23.01" date="2023-01-30">
<url>https://plasma-mobile.org/2023/01/30/january-blog-post/</url>
<description>
<p>New features and bugfixes:</p>
<ul>
<li>Notifications will now be shown for all accounts, not just the active one</li>
<li>There is a new "compact" mode for the room list</li>
<li>You can now search in the room history</li>
<li>Emojis and Reactions have been significantly improved</li>
<li>Fixed several crashes around user invitations</li>
<li>Room permission settings can now be configured</li>
</ul>
</description>
</release>
<release version="22.11" date="2022-11-30">
<url>https://plasma-mobile.org/2022/11/30/plasma-mobile-gear-22-11/</url>
</release>

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: CC0-1.0
# SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
# SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
[Desktop Entry]
Version=1.5
Name=NeoChat
@@ -15,7 +15,6 @@ Name[es]=NeoChat
Name[eu]=NeoChat
Name[fi]=NeoChat
Name[fr]=NeoChat
Name[gl]=NeoChat
Name[hu]=NeoChat
Name[ia]=Neochat
Name[id]=NeoChat
@@ -53,7 +52,6 @@ GenericName[es]=Cliente para Matrix
GenericName[eu]=Matrix bezeroa
GenericName[fi]=Matrix-asiakas
GenericName[fr]=Client « Matrix »
GenericName[gl]=Cliente de Matrix
GenericName[hu]=Matrix kliens
GenericName[ia]=Cliente de Matrice
GenericName[id]=Klien Matrix
@@ -90,7 +88,6 @@ Comment[es]=Cliente para el protocolo Matrix
Comment[eu]=Matrix protokolorako bezeroa
Comment[fi]=Asiakas Matrix-yhteyskäytännölle
Comment[fr]=Client pour le protocole « Matrix »
Comment[gl]=Cliente para o protocolo Matrix
Comment[hu]=Kliens a Matrix protokollhoz
Comment[ia]=Cliente per le protocollo de Matrix
Comment[id]=Klien untuk protokol Matrix

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

@@ -1,5 +0,0 @@
# SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
#
# SPDX-License-Identifier: GPL-3.0-or-later
kdoctools_create_manpage(man-neochat.1.docbook 1 INSTALL_DESTINATION ${MAN_INSTALL_DIR})

View File

@@ -1,122 +0,0 @@
<?xml version="1.0" ?>
<!DOCTYPE refentry PUBLIC "-//KDE//DTD DocBook XML V4.5-Based Variant V1.1//EN" "dtd/kdedbx45.dtd" [
<!ENTITY % Turkish "INCLUDE">
]>
<!--
SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
SPDX-License-Identifier: CC-BY-SA-4.0
-->
<refentry lang="&language;">
<refentryinfo>
<title
>NeoChat Kullanıcı Kılavuzu</title>
<author
><firstname
>Carl</firstname
><surname
>Schwan</surname
> <contrib
>NeoChat man page.</contrib
> <email
>carl@carlschwan.eu</email
></author>
<date
>2022-11-01</date>
<releaseinfo
>22.09</releaseinfo>
<productname
>NeoChat</productname>
</refentryinfo>
<refmeta>
<refentrytitle>
<command
>neochat</command>
</refentrytitle>
<manvolnum
>1</manvolnum>
</refmeta>
<refnamediv>
<refname
>neochat</refname>
<refpurpose
>Matrix iletileşme protokolü ile etkileşim için istemci</refpurpose>
</refnamediv>
<!-- body begins here -->
<refsynopsisdiv id='synopsis'>
<cmdsynopsis
><command
>neochat</command
> <arg choice="opt"
><replaceable
>URI</replaceable
></arg
> </cmdsynopsis>
</refsynopsisdiv>
<refsect1 id="description">
<title
>Açıklama</title>
<para
><command
>neochat</command
>, Matrix protokolü için hem masaüstünde hem de taşınabilir aygıtlarda çalışan bir sohbet uygulamasıdır. </para>
</refsect1>
<refsect1 id="options"
><title
>Seçenekler</title>
<variablelist>
<varlistentry>
<term
><option
>URI</option
></term>
<listitem>
<para
>Bir kullanıcı veya oda için matrix URI'si; örneğin, matrix:u/kullanıcı:örnek.org ve matrix:r/kök:örnek.org. Bu, NeoChat'in verilen odayı veya konuşmayı açmayı denemesini sağlar. </para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1 id="bug">
<title
>Hata Bildirme</title>
<para
>Hataları veya özellik isteklerini <ulink url="https://bugs.kde.org/enter_bug.cgi?product=NeoChat&amp;component=General"
>https://bugs.kde.org/enter_bug.cgi?product=NeoChat&amp;component=General</ulink
> bağlantısından bildirebilirsiniz</para>
</refsect1>
<refsect1>
<title
>Ayrıca</title>
<simplelist>
<member
>Matrix üzerine sıkça sorulan soruların bir listesi: <ulink url="https://matrix.org/faq/"
>https://matrix.org/faq/</ulink
> </member>
<member
>kf5options(7)</member>
<member
>qt5options(7)</member>
</simplelist>
</refsect1>
<refsect1 id="copyright"
><title
>Telif Hakkı</title>
<para
>Telif hakkı &copy; 2020-2022 Tobias Fella </para>
<para
>Telif hakkı &copy; 2020-2022 Carl Schwan </para>
<para
>Lisans: GNU Genel Kamu Lisansa, 3. sürüm veya sonrası &lt;<ulink url="https://www.gnu.org/licenses/gpl-3.0.html"
>https://www.gnu.org/licenses/gpl-3.0.html</ulink
>&gt;</para>
</refsect1>
</refentry>

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

@@ -1,74 +1,52 @@
# SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carl@carlschwan.eu>
# SPDX-FileCopyrightText: 2020-2021 Nicolas Fella <nicolas.fella@gmx.de>
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
# SPDX-License-Identifier: BSD-2-Clause
add_library(neochat STATIC
controller.cpp
actionshandler.cpp
models/emojimodel.cpp
emojimodel.cpp
emojitones.cpp
models/customemojimodel.cpp
customemojimodel.cpp
clipboard.cpp
matriximageprovider.cpp
models/messageeventmodel.cpp
models/messagefiltermodel.cpp
models/roomlistmodel.cpp
models/sortfilterspacelistmodel.cpp
models/accountemoticonmodel.cpp
messageeventmodel.cpp
messagefiltermodel.cpp
roomlistmodel.cpp
sortfilterspacelistmodel.cpp
spacehierarchycache.cpp
roommanager.cpp
neochatroom.cpp
neochatuser.cpp
models/userlistmodel.cpp
models/userfiltermodel.cpp
models/publicroomlistmodel.cpp
models/userdirectorylistmodel.cpp
models/keywordnotificationrulemodel.cpp
models/emoticonfiltermodel.cpp
userlistmodel.cpp
userfiltermodel.cpp
publicroomlistmodel.cpp
userdirectorylistmodel.cpp
keywordnotificationrulemodel.cpp
utils.cpp
notificationsmanager.cpp
models/sortfilterroomlistmodel.cpp
sortfilterroomlistmodel.cpp
chatdocumenthandler.cpp
models/devicesmodel.cpp
devicesmodel.cpp
filetypesingleton.cpp
login.cpp
models/webshortcutmodel.cpp
stickerevent.cpp
webshortcutmodel.cpp
blurhash.cpp
blurhashimageprovider.cpp
models/collapsestateproxymodel.cpp
joinrulesevent.cpp
collapsestateproxymodel.cpp
urlhelper.cpp
windowcontroller.cpp
linkpreviewer.cpp
models/completionmodel.cpp
models/completionproxymodel.cpp
models/actionsmodel.cpp
models/serverlistmodel.cpp
models/statemodel.cpp
models/statefiltermodel.cpp
completionmodel.cpp
completionproxymodel.cpp
actionsmodel.cpp
serverlistmodel.cpp
statemodel.cpp
filetransferpseudojob.cpp
models/searchmodel.cpp
texthandler.cpp
logger.cpp
models/stickermodel.cpp
models/imagepacksmodel.cpp
events/imagepackevent.cpp
events/joinrulesevent.cpp
events/stickerevent.cpp
models/reactionmodel.cpp
delegatesizehelper.cpp
)
ecm_qt_declare_logging_category(neochat
HEADER "messageeventmodel_logging.h"
IDENTIFIER "MessageEvent"
CATEGORY_NAME "org.kde.neochat.messageeventmodel"
DEFAULT_SEVERITY Info
)
ecm_qt_declare_logging_category(neochat
HEADER "voip_logging.h"
IDENTIFIER "voip"
CATEGORY_NAME "org.kde.neochat.voip"
searchmodel.cpp
)
add_executable(neochat-app
@@ -82,11 +60,11 @@ target_link_libraries(neochat-app PRIVATE
neochat
)
if(Quotient${QUOTIENT_SUFFIX}_VERSION_MINOR GREATER 6)
if(Quotient_VERSION_MINOR GREATER 6)
target_compile_definitions(neochat PUBLIC QUOTIENT_07)
target_sources(neochat PRIVATE events/pollevent.cpp pollhandler.cpp)
target_sources(neochat PRIVATE pollevent.cpp pollhandler.cpp)
else()
target_compile_definitions(neochat PUBLIC QUOTIENT_VERSION=\"${Quotient${QUOTIENT_SUFFIX}_VERSION}\")
target_compile_definitions(neochat PUBLIC QUOTIENT_VERSION=\"${Quotient_VERSION}\")
target_sources(neochat PRIVATE neochataccountregistry.cpp)
endif()
@@ -101,7 +79,7 @@ if(NOT ANDROID)
else()
target_sources(neochat PRIVATE trayicon.cpp)
endif()
target_link_libraries(neochat PUBLIC KF${QT_MAJOR_VERSION}::ConfigWidgets KF${QT_MAJOR_VERSION}::WindowSystem)
target_link_libraries(neochat PUBLIC KF5::ConfigWidgets KF5::WindowSystem)
target_compile_definitions(neochat PUBLIC -DHAVE_COLORSCHEME)
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
endif()
@@ -109,28 +87,13 @@ endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
target_sources(neochat-app PRIVATE res_desktop.qrc)
target_compile_definitions(neochat PUBLIC -DHAVE_RUNNER)
target_compile_definitions(neochat PUBLIC -DHAVE_X11)
target_sources(neochat PRIVATE runner.cpp)
else()
target_sources(neochat-app PRIVATE res_android.qrc)
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()
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF5::I18n KF5::Kirigami2 KF5::Notifications KF5::ConfigCore KF5::ConfigGui KF5::CoreAddons KF5::SonnetCore KF5::ItemModels Quotient cmark::cmark ${QTKEYCHAIN_LIBRARIES} QCoro::Core)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
if(NEOCHAT_FLATPAK)
@@ -140,9 +103,6 @@ endif()
if(ANDROID)
target_sources(neochat PRIVATE notifyrc.qrc)
target_link_libraries(neochat PRIVATE Qt::Svg OpenSSL::SSL)
if(SQLite3_FOUND)
target_link_libraries(neochat-app PRIVATE SQLite::SQLite3)
endif()
target_sources(neochat-app PRIVATE notifyrc.qrc)
target_link_libraries(neochat PUBLIC Qt::Svg OpenSSL::SSL)
kirigami_package_breeze_icons(ICONS
@@ -199,7 +159,7 @@ if(ANDROID)
"zoom-out"
"image-rotate-left-symbolic"
"image-rotate-right-symbolic"
"channel-secure-symbolic"
"channel-insecure-symbolic"
"download"
"smiley"
"tools-check-spelling"
@@ -211,13 +171,9 @@ if(ANDROID)
"window-new"
"globe"
"visibility"
"home"
"preferences-desktop-notification"
"computer-symbolic"
"gps"
)
else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF${QT_MAJOR_VERSION}::KIOWidgets)
target_link_libraries(neochat PUBLIC Qt::Widgets KF5::KIOWidgets)
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
endif()
@@ -225,12 +181,12 @@ if(NOT ANDROID)
set_target_properties(neochat-app PROPERTIES OUTPUT_NAME "neochat")
endif()
if(TARGET KF${QT_MAJOR_VERSION}::DBusAddons)
target_link_libraries(neochat PUBLIC KF${QT_MAJOR_VERSION}::DBusAddons)
if(TARGET KF5::DBusAddons)
target_link_libraries(neochat PUBLIC KF5::DBusAddons)
target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS)
endif()
if (TARGET KF${QT_MAJOR_VERSION}::KIOWidgets)
if (TARGET KF5::KIOWidgets)
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
endif()

View File

@@ -3,6 +3,8 @@
#include "actionshandler.h"
#include "controller.h"
#include <csapi/joining.h>
#include <events/roommemberevent.h>
@@ -11,14 +13,31 @@
#include <KLocalizedString>
#include <QStringBuilder>
#include "models/actionsmodel.h"
#include "models/customemojimodel.h"
#include "actionsmodel.h"
#include "controller.h"
#include "customemojimodel.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "texthandler.h"
#include "neochatuser.h"
#include "roommanager.h"
using namespace Quotient;
QString markdownToHTML(const QString &markdown)
{
const auto str = markdown.toUtf8();
char *tmp_buf = cmark_markdown_to_html(str.constData(), str.size(), CMARK_OPT_HARDBREAKS | CMARK_OPT_UNSAFE);
const std::string html(tmp_buf);
free(tmp_buf);
auto result = QString::fromStdString(html).trimmed();
result.replace("<!-- raw HTML omitted -->", "");
return result;
}
ActionsHandler::ActionsHandler(QObject *parent)
: QObject(parent)
{
@@ -39,9 +58,9 @@ void ActionsHandler::setRoom(NeoChatRoom *room)
Q_EMIT roomChanged();
}
void ActionsHandler::handleNewMessage()
void ActionsHandler::handleMessage()
{
checkEffects(m_room->chatBoxText());
checkEffects();
if (!m_room->chatBoxAttachmentPath().isEmpty()) {
QUrl url(m_room->chatBoxAttachmentPath());
auto path = url.isLocalFile() ? url.toLocalFile() : url.toString();
@@ -50,39 +69,13 @@ void ActionsHandler::handleNewMessage()
m_room->setChatBoxText({});
return;
}
QString handledText = m_room->chatBoxText();
handledText = handleMentions(handledText);
handleMessage(m_room->chatBoxText(), handledText);
}
void ActionsHandler::handleEdit()
{
checkEffects(m_room->editText());
QString handledText = m_room->editText();
handledText = handleMentions(handledText, true);
handleMessage(m_room->editText(), handledText, true);
}
QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit)
{
if (!m_room) {
return QString();
}
QVector<Mention> *mentions;
if (isEdit) {
mentions = m_room->editMentions();
} else {
mentions = m_room->mentions();
}
std::sort(mentions->begin(), mentions->end(), [](const auto &a, const auto &b) -> bool {
std::sort(m_room->mentions()->begin(), m_room->mentions()->end(), [](const auto &a, const auto &b) -> bool {
return a.cursor.anchor() > b.cursor.anchor();
});
for (const auto &mention : *mentions) {
for (const auto &mention : *m_room->mentions()) {
if (mention.text.isEmpty() || mention.id.isEmpty()) {
continue;
}
@@ -90,16 +83,11 @@ QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit)
mention.cursor.position() - mention.cursor.anchor(),
QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text, mention.id));
}
mentions->clear();
m_room->mentions()->clear();
return handledText;
}
void ActionsHandler::handleMessage(const QString &text, QString handledText, const bool &isEdit)
{
if (NeoChatConfig::allowQuickEdit()) {
QRegularExpression sed("^s/([^/]*)/([^/]*)(/g)?$");
auto match = sed.match(text);
auto match = sed.match(m_room->chatBoxText());
if (match.hasMatch()) {
const QString regex = match.captured(1);
const QString replacement = match.captured(2).toHtmlEscaped();
@@ -149,10 +137,7 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
}
handledText = CustomEmojiModel::instance().preprocessText(handledText);
TextHandler textHandler;
textHandler.setData(handledText);
handledText = textHandler.handleSendText();
handledText = markdownToHTML(handledText);
if (handledText.count("<p>") == 1 && handledText.count("</p>") == 1) {
handledText.remove("<p>");
handledText.remove("</p>");
@@ -161,13 +146,13 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
if (handledText.length() == 0) {
return;
}
m_room->postMessage(text, handledText, messageType, m_room->chatBoxReplyId(), isEdit ? m_room->chatBoxEditId() : "");
m_room->postMessage(m_room->chatBoxText(), handledText, messageType, m_room->chatBoxReplyId(), m_room->chatBoxEditId());
}
void ActionsHandler::checkEffects(const QString &text)
void ActionsHandler::checkEffects()
{
std::optional<QString> effect = std::nullopt;
const auto &text = m_room->chatBoxText();
if (text.contains("\u2744")) {
effect = QLatin1String("snowflake");
} else if (text.contains("\u1F386")) {

View File

@@ -12,31 +12,13 @@
class CustomEmojiModel;
class NeoChatRoom;
/**
* @class ActionsHandler
*
* This class handles chat messages ready for posting to a room.
*
* Everything that needs to be done to prepare the message for posting in a room
* including:
* - File handling
* - User mentions
* - Quick edits
* - Chat actions
* - Custom emojis
*
* @note A chat action is a message starting with /, resulting in something other
* than a normal message being sent (e.g. /me, /join).
*
* @sa ActionsModel, NeoChatRoom
*/
/// \brief Handles user interactions with NeoChat (joining room, creating room,
/// sending message). Account management is handled by Controller.
class ActionsHandler : public QObject
{
Q_OBJECT
/**
* @brief The room that messages will be sent to.
*/
/// \brief The room that messages will be sent to.
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
public:
@@ -51,20 +33,14 @@ Q_SIGNALS:
public Q_SLOTS:
/**
* @brief Pre-process text and send message.
*/
void handleNewMessage();
/**
* @brief Pre-process text and send edit.
*/
void handleEdit();
/// \brief Post a message.
///
/// This also interprets commands if any.
void handleMessage();
private:
NeoChatRoom *m_room = nullptr;
void checkEffects(const QString &text);
QString handleMentions(QString handledText, const bool &isEdit = false);
void handleMessage(const QString &text, QString handledText, const bool &isEdit = false);
void checkEffects();
};
QString markdownToHTML(const QString &markdown);

View File

@@ -1,10 +1,11 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "actionsmodel.h"
#include "controller.h"
#include "neochatroom.h"
#include "neochatuser.h"
#include "roommanager.h"
#include <events/roommemberevent.h>
#include <events/roompowerlevelsevent.h>
@@ -18,41 +19,6 @@ QStringList rainbowColors{"#ff2b00", "#ff5500", "#ff8000", "#ffaa00", "#ffd500",
"#00ff2b", "#00ff55", "#00ff80", "#00ffaa", "#00ffd5", "#00ffff", "#00d4ff", "#00aaff", "#007fff", "#0055ff", "#002bff", "#0000ff",
"#2a00ff", "#5500ff", "#7f00ff", "#aa00ff", "#d400ff", "#ff00ff", "#ff00d4", "#ff00aa", "#ff0080", "#ff0055", "#ff002b", "#ff0000"};
auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18n("Leaving this room."));
room->connection()->leaveRoom(room);
} else {
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(NeoChatRoom::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
auto leaving = room->connection()->room(text);
if (!leaving) {
leaving = room->connection()->roomByAlias(text);
}
if (leaving) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
room->connection()->leaveRoom(leaving);
} else {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Room <roomname> not found", "Room %1 not found.", text));
}
}
return QString();
};
auto roomNickLambda = [](const QString &text, NeoChatRoom *room) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen."));
} else {
room->connection()->user()->rename(text, room);
}
return QString();
};
QVector<ActionsModel::Action> actions{
Action{
QStringLiteral("shrug"),
@@ -149,7 +115,7 @@ QVector<ActionsModel::Action> actions{
QStringLiteral("spoiler"),
[](const QString &text, NeoChatRoom *room) {
// Ideally, we would just return rainbowText and let that do the rest, but the colors don't survive markdownToHTML.
room->postMessage(QStringLiteral("/spoiler %1").arg(text),
room->postMessage(QStringLiteral("/rainbow %1").arg(text),
QStringLiteral("<span data-mx-spoiler>%1</span>").arg(text),
RoomMessageEvent::MsgType::Text,
room->chatBoxReplyId(),
@@ -192,12 +158,11 @@ QVector<ActionsModel::Action> actions{
return QString();
}
#ifdef QUOTIENT_07
const RoomMemberEvent *roomMemberEvent = room->currentState().get<RoomMemberEvent>(text);
if (roomMemberEvent && roomMemberEvent->membership() == Membership::Invite) {
if (room->currentState().get<RoomMemberEvent>(text)->membership() == Membership::Invite) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already invited to this room.", "%1 is already invited to this room.", text));
return QString();
}
if (roomMemberEvent && roomMemberEvent->membership() == Membership::Ban) {
if (room->currentState().get<RoomMemberEvent>(text)->membership() == Membership::Ban) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is banned from this room.", "%1 is banned from this room.", text));
return QString();
}
@@ -243,40 +208,6 @@ QVector<ActionsModel::Action> actions{
kli18n("<room alias or id>"),
kli18n("Joins the given room"),
},
#ifdef QUOTIENT_07
Action{
QStringLiteral("knock"),
[](const QString &text, NeoChatRoom *room) {
auto parts = text.split(QLatin1String(" "));
QString roomName = parts[0];
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(roomName);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(NeoChatRoom::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text);
if (targetRoom) {
RoomManager::instance().enterRoom(dynamic_cast<NeoChatRoom *>(targetRoom));
return QString();
}
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Knocking room <roomname>.", "Knocking room %1.", text));
auto connection = Controller::instance().activeConnection();
const auto knownServer = roomName.mid(roomName.indexOf(":") + 1);
if (parts.length() >= 2) {
RoomManager::instance().knockRoom(connection, roomName, parts[1], QStringList{knownServer});
} else {
RoomManager::instance().knockRoom(connection, roomName, QString(), QStringList{knownServer});
}
return QString();
},
false,
std::nullopt,
kli18n("<room alias or id> [<reason>]"),
kli18n("Requests to join the given room"),
},
#endif
Action{
QStringLiteral("j"),
[](const QString &text, NeoChatRoom *room) {
@@ -302,7 +233,31 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("part"),
leaveRoomLambda,
[](const QString &text, NeoChatRoom *room) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18n("Leaving this room."));
room->connection()->leaveRoom(room);
} else {
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(NeoChatRoom::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
auto leaving = room->connection()->room(text);
if (!leaving) {
leaving = room->connection()->roomByAlias(text);
}
if (leaving) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
room->connection()->leaveRoom(leaving);
} else {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Room <roomname> not found", "Room %1 not found.", text));
}
}
return QString();
},
false,
std::nullopt,
kli18n("[<room alias or id>]"),
@@ -310,7 +265,31 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("leave"),
leaveRoomLambda,
[](const QString &text, NeoChatRoom *room) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18n("Leaving this room."));
room->connection()->leaveRoom(room);
} else {
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(NeoChatRoom::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
auto leaving = room->connection()->room(text);
if (!leaving) {
leaving = room->connection()->roomByAlias(text);
}
if (leaving) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
room->connection()->leaveRoom(leaving);
} else {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Room <roomname> not found", "Room %1 not found.", text));
}
}
return QString();
},
false,
std::nullopt,
kli18n("[<room alias or id>]"),
@@ -333,15 +312,14 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("roomnick"),
roomNickLambda,
false,
std::nullopt,
kli18n("<display name>"),
kli18n("Changes your display name in this room"),
},
Action{
QStringLiteral("myroomnick"),
roomNickLambda,
[](const QString &text, NeoChatRoom *room) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen."));
} else {
room->connection()->user()->rename(text, room);
}
return QString();
},
false,
std::nullopt,
kli18n("<display name>"),

49
src/actionsmodel.h Normal file
View File

@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <KLazyLocalizedString>
#include <QAbstractListModel>
#include <events/roommessageevent.h>
class NeoChatRoom;
class ActionsModel : public QAbstractListModel
{
public:
struct Action {
// The prefix, without '/' and space after the word
QString prefix;
std::function<QString(const QString &, NeoChatRoom *)> handle;
// If this is true, this action transforms a message to a different message and it will be sent.
// If this is false, this message does some action on the client and should not be sent as a message.
bool messageAction;
// If this action changes the message type, this is the new message type. Otherwise it's nullopt
std::optional<Quotient::RoomMessageEvent::MsgType> messageType = std::nullopt;
KLazyLocalizedString parameters;
KLazyLocalizedString description;
};
static ActionsModel &instance()
{
static ActionsModel _instance;
return _instance;
}
enum Roles {
Prefix = Qt::DisplayRole,
Description,
CompletionType,
Parameters,
};
Q_ENUM(Roles);
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;
QVector<Action> &allActions() const;
private:
ActionsModel() = default;
};

View File

@@ -5,24 +5,6 @@
#include <stdint.h>
/**
* @brief Returns the pixel array of the result image given the blurhash string.
*
* @param blurhash a string representing the blurhash to be decoded.
* @param width the width of the resulting image.
* @param height the height of the resulting image.
* @param punch the factor to improve the contrast, default = 1.
* @param nChannels the number of channels in the resulting image array, 3 = RGB, 4 = RGBA.
*
* @return A pointer to memory region where pixels are stored in (H, W, C) format.
*/
uint8_t *decode(const char *blurhash, int width, int height, int punch, int nChannels);
/**
* @brief Checks if the Blurhash is valid or not.
*
* @param blurhash a string representing the blurhash.
*
* @return A bool (true if it is a valid blurhash, else false).
*/
bool isValidBlurhash(const char *blurhash);

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "blurhashimageprovider.h"

View File

@@ -1,26 +1,13 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QQuickImageProvider>
/**
* @class BlurhashImageProvider
*
* A QQuickImageProvider for blurhashes.
*
* @sa QQuickImageProvider
*/
class BlurhashImageProvider : public QQuickImageProvider
{
public:
BlurhashImageProvider();
/**
* @brief Return an image for a given ID.
*
* @sa QQuickImageProvider::requestImage
*/
QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override;
};
};

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

@@ -14,7 +14,9 @@
#include <Sonnet/BackgroundChecker>
#include <Sonnet/Settings>
#include "actionsmodel.h"
#include "neochatroom.h"
#include "roomlistmodel.h"
class SyntaxHighlighter : public QSyntaxHighlighter
{
@@ -103,19 +105,14 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
{
connect(this, &ChatDocumentHandler::roomChanged, this, [this]() {
m_completionModel->setRoom(m_room);
static QPointer<NeoChatRoom> previousRoom = nullptr;
static NeoChatRoom *previousRoom = nullptr;
if (previousRoom) {
disconnect(previousRoom, &NeoChatRoom::chatBoxTextChanged, this, nullptr);
disconnect(previousRoom, &NeoChatRoom::editTextChanged, this, nullptr);
}
previousRoom = m_room;
connect(m_room, &NeoChatRoom::chatBoxTextChanged, this, [this]() {
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
});
connect(m_room, &NeoChatRoom::editTextChanged, this, [this]() {
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
m_completionModel->setText(m_room->chatBoxText().mid(start, cursorPosition() - start), m_room->chatBoxText().mid(start));
});
});
connect(this, &ChatDocumentHandler::documentChanged, this, [this]() {
@@ -126,7 +123,7 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
return;
}
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
m_completionModel->setText(m_room->chatBoxText().mid(start, cursorPosition() - start), m_room->chatBoxText().mid(start));
});
}
@@ -136,13 +133,12 @@ int ChatDocumentHandler::completionStartIndex() const
return 0;
}
#if !defined(Q_OS_ANDROID) && QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
const long long cursor = cursorPosition();
#else
const auto cursor = cursorPosition();
#endif
const auto &text = getText();
const auto &text = m_room->chatBoxText();
auto start = std::min(cursor, text.size()) - 1;
while (start > -1) {
if (text.at(start) == QLatin1Char(' ')) {
@@ -154,20 +150,6 @@ int ChatDocumentHandler::completionStartIndex() const
return start;
}
bool ChatDocumentHandler::isEdit() const
{
return m_isEdit;
}
void ChatDocumentHandler::setIsEdit(bool edit)
{
if (edit == m_isEdit) {
return;
}
m_isEdit = edit;
Q_EMIT isEditChanged();
}
QQuickTextDocument *ChatDocumentHandler::document() const
{
return m_document;
@@ -222,7 +204,7 @@ void ChatDocumentHandler::complete(int index)
if (m_completionModel->autoCompletionType() == CompletionModel::User) {
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Text).toString();
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
auto text = getText();
auto text = m_room->chatBoxText();
auto at = text.lastIndexOf(QLatin1Char('@'), cursorPosition() - 1);
QTextCursor cursor(document()->textDocument());
cursor.setPosition(at);
@@ -231,11 +213,11 @@ void ChatDocumentHandler::complete(int index)
cursor.setPosition(at);
cursor.setPosition(cursor.position() + name.size(), QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true);
pushMention({cursor, name, 0, 0, id});
m_room->mentions()->push_back({cursor, name, 0, 0, id});
m_highlighter->rehighlight();
} else if (m_completionModel->autoCompletionType() == CompletionModel::Command) {
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString();
auto text = getText();
auto text = m_room->chatBoxText();
auto at = text.lastIndexOf(QLatin1Char('/'));
QTextCursor cursor(document()->textDocument());
cursor.setPosition(at);
@@ -243,7 +225,7 @@ void ChatDocumentHandler::complete(int index)
cursor.insertText(QStringLiteral("/%1 ").arg(command));
} else if (m_completionModel->autoCompletionType() == CompletionModel::Room) {
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
auto text = getText();
auto text = m_room->chatBoxText();
auto at = text.lastIndexOf(QLatin1Char('#'), cursorPosition() - 1);
QTextCursor cursor(document()->textDocument());
cursor.setPosition(at);
@@ -252,11 +234,11 @@ void ChatDocumentHandler::complete(int index)
cursor.setPosition(at);
cursor.setPosition(cursor.position() + alias.size(), QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true);
pushMention({cursor, alias, 0, 0, alias});
m_room->mentions()->push_back({cursor, alias, 0, 0, alias});
m_highlighter->rehighlight();
} else if (m_completionModel->autoCompletionType() == CompletionModel::Emoji) {
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString();
auto text = getText();
auto text = m_room->chatBoxText();
auto at = text.lastIndexOf(QLatin1Char(':'));
QTextCursor cursor(document()->textDocument());
cursor.setPosition(at);
@@ -299,59 +281,3 @@ void ChatDocumentHandler::setSelectionEnd(int position)
m_selectionEnd = position;
Q_EMIT selectionEndChanged();
}
QString ChatDocumentHandler::getText() const
{
if (!m_room) {
return QString();
}
if (m_isEdit) {
return m_room->editText();
} else {
return m_room->chatBoxText();
}
}
void ChatDocumentHandler::pushMention(const Mention mention) const
{
if (!m_room) {
return;
}
if (m_isEdit) {
m_room->editMentions()->push_back(mention);
} else {
m_room->mentions()->push_back(mention);
}
}
QColor ChatDocumentHandler::mentionColor() const
{
return m_mentionColor;
}
void ChatDocumentHandler::setMentionColor(const QColor &color)
{
if (m_mentionColor == color) {
return;
}
m_mentionColor = color;
m_highlighter->mentionFormat.setForeground(m_mentionColor);
m_highlighter->rehighlight();
Q_EMIT mentionColorChanged();
}
QColor ChatDocumentHandler::errorColor() const
{
return m_errorColor;
}
void ChatDocumentHandler::setErrorColor(const QColor &color)
{
if (m_errorColor == color) {
return;
}
m_errorColor = color;
m_highlighter->errorFormat.setForeground(m_errorColor);
m_highlighter->rehighlight();
Q_EMIT errorColorChanged();
}

View File

@@ -7,116 +7,28 @@
#include <QQuickTextDocument>
#include <QTextCursor>
#include "models/completionmodel.h"
#include "neochatroom.h"
#include "completionmodel.h"
#include "userlistmodel.h"
class QTextDocument;
class NeoChatRoom;
class SyntaxHighlighter;
/**
* @class ChatDocumentHandler
*
* Handle the QQuickTextDocument of a qml text item.
*
* The class provides functionality to highlight text in the text document as well
* as providing completion functionality via a CompletionModel.
*
* The ChatDocumentHandler is also linked to a NeoChatRoom to provide functionality
* to save the chat document text when switching between rooms.
*
* To get the full functionality the cursor position and text selection information
* need to be passed in. For example:
*
* @code{.qml}
* import QtQuick 2.0
* import QtQuick.Controls 2.15 as QQC2
*
* import org.kde.kirigami 2.12 as Kirigami
* import org.kde.neochat 1.0
*
* QQC2.TextArea {
* id: textField
*
* // Set this to a NeoChatRoom object.
* property var room
*
* ChatDocumentHandler {
* id: documentHandler
* document: textField.textDocument
* cursorPosition: textField.cursorPosition
* selectionStart: textField.selectionStart
* selectionEnd: textField.selectionEnd
* mentionColor: Kirigami.Theme.linkColor
* errorColor: Kirigami.Theme.negativeTextColor
* room: textField.room
* }
* }
* @endcode
*
* @sa QQuickTextDocument, CompletionModel, NeoChatRoom
*/
class ChatDocumentHandler : public QObject
{
Q_OBJECT
/**
* @brief Is the instance being used to handle an edit message.
*
* This is needed to ensure that the text and mentions are saved and retrieved
* from the correct parameters in the assigned room.
*/
Q_PROPERTY(bool isEdit READ isEdit WRITE setIsEdit NOTIFY isEditChanged)
/**
* @brief The QQuickTextDocument that is being handled.
*/
Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
/**
* @brief The current saved cursor position.
*/
Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
/**
* @brief The start position of any currently selected text.
*/
Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged)
/**
* @brief The end position of any currently selected text.
*/
Q_PROPERTY(int selectionEnd READ selectionEnd WRITE setSelectionEnd NOTIFY selectionEndChanged)
/**
* @brief The current CompletionModel.
*
* This is typically provided to a qml component to visualise the current
* completion results.
*/
Q_PROPERTY(CompletionModel *completionModel READ completionModel NOTIFY completionModelChanged)
/**
* @brief The current room that the the text document is being handled for.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/**
* @brief The color to highlight user mentions.
*/
Q_PROPERTY(QColor mentionColor READ mentionColor WRITE setMentionColor NOTIFY mentionColorChanged);
/**
* @brief The color to highlight spelling errors.
*/
Q_PROPERTY(QColor errorColor READ errorColor WRITE setErrorColor NOTIFY errorColorChanged);
Q_PROPERTY(NeoChatRoom *room READ room NOTIFY roomChanged)
public:
explicit ChatDocumentHandler(QObject *parent = nullptr);
[[nodiscard]] bool isEdit() const;
void setIsEdit(bool edit);
[[nodiscard]] QQuickTextDocument *document() const;
void setDocument(QQuickTextDocument *document);
@@ -136,44 +48,26 @@ public:
void updateCompletions();
CompletionModel *completionModel() const;
[[nodiscard]] QColor mentionColor() const;
void setMentionColor(const QColor &color);
[[nodiscard]] QColor errorColor() const;
void setErrorColor(const QColor &color);
Q_SIGNALS:
void isEditChanged();
void documentChanged();
void cursorPositionChanged();
void roomChanged();
void completionModelChanged();
void selectionStartChanged();
void selectionEndChanged();
void errorColorChanged();
void mentionColorChanged();
private:
int completionStartIndex() const;
bool m_isEdit = false;
QQuickTextDocument *m_document;
QPointer<QQuickTextDocument> m_document;
QPointer<NeoChatRoom> m_room;
NeoChatRoom *m_room = nullptr;
bool completionVisible = false;
QColor m_mentionColor;
QColor m_errorColor;
int m_cursorPosition;
int m_selectionStart;
int m_selectionEnd;
QString getText() const;
void pushMention(const Mention mention) const;
SyntaxHighlighter *m_highlighter = nullptr;
CompletionModel::AutoCompletionType m_completionType = CompletionModel::None;

View File

@@ -33,14 +33,13 @@ QImage Clipboard::image() const
QString Clipboard::saveImage(QString localPath) const
{
QString imageDir(QStringLiteral("%1/screenshots").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)));
if (!QDir().exists(imageDir)) {
QDir().mkdir(imageDir);
if (!QDir().exists(QStringLiteral("%1/screenshots").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)))) {
QDir().mkdir(QStringLiteral("%1/screenshots").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)));
}
if (localPath.isEmpty()) {
localPath = QStringLiteral("file://%1/%2.png").arg(imageDir, QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd-hh-mm-ss")));
localPath = QStringLiteral("file://%1/screenshots/%2.png")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation),
QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd-hh-mm-ss")));
}
QUrl url(localPath);
if (!url.isLocalFile()) {
@@ -52,30 +51,21 @@ QString Clipboard::saveImage(QString localPath) const
return {};
}
if (image.save(url.toLocalFile())) {
return localPath;
} else {
return {};
QDir dir;
if (!dir.exists(localPath)) {
dir.mkpath(localPath);
}
image.save(url.toLocalFile());
return localPath;
}
void Clipboard::saveText(QString message)
{
static QRegularExpression re(QStringLiteral("<[^>]*>"));
auto *mimeData = new QMimeData; // ownership is transferred to clipboard
mimeData->setHtml(message);
mimeData->setText(message.replace(re, QString()));
m_clipboard->setMimeData(mimeData);
}
void Clipboard::setImage(const QUrl &url)
{
if (url.isLocalFile()) {
QImage img(url.path());
auto *mimeData = new QMimeData;
mimeData->setImageData(img);
if (!img.isNull()) {
m_clipboard->setMimeData(mimeData);
}
}
QRegularExpression re("<[^>]*>");
auto *mineData = new QMimeData; // ownership is transferred to clipboard
mineData->setHtml(message);
mineData->setText(message.replace(re, ""));
m_clipboard->setMimeData(mineData);
}

View File

@@ -9,62 +9,24 @@ class QClipboard;
class QImage;
/**
* @class Clipboard
*
* Clipboard proxy
*
* Used to set and retrieve content from the clipboard.
*/
class Clipboard : public QObject
{
Q_OBJECT
/**
* @brief Whether the current clipboard content is an image.
*/
Q_PROPERTY(bool hasImage READ hasImage NOTIFY imageChanged)
/**
* @brief Return the current clipboard content image.
*
* Returns a null image if the clipboard does not contain an image or if it
* contains an image in an unsupported image format.
*/
Q_PROPERTY(QImage image READ image NOTIFY imageChanged)
public:
explicit Clipboard(QObject *parent = nullptr);
[[nodiscard]] bool hasImage() const;
[[nodiscard]] QImage image() const;
/**
* @brief Save the current clipboard image to file.
*
* If the clipboard does not contain an image or if it contains an image in an
* unsupported image format nothing happens.
*
* The given file path must be both valid and local or nothing happens.
*
* @param localPath the path to save the image. A default path for the app cache
* will be used if available and this is empty.
*
* @return A QString with the path that the image was saved to. The string will
* be empty if nothing was saved.
*/
Q_INVOKABLE QString saveImage(QString localPath = {}) const;
/**
* @brief Set the clipboard content to the input message.
*/
Q_INVOKABLE void saveText(QString message);
/**
* @brief Set the clipboard content to the input image.
*/
Q_INVOKABLE void setImage(const QUrl &image);
private:
QClipboard *m_clipboard;

View File

@@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "collapsestateproxymodel.h"
#include <KLocalizedString>
bool CollapseStateProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
Q_UNUSED(source_parent);
return sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::EventTypeRole)
!= MessageEventModel::DelegateType::State // If this is not a state, show it
|| sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::EventTypeRole)
!= MessageEventModel::DelegateType::State // If this is the first state in a block, show it. TODO hidden events?
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::ShowSectionRole).toBool() // If it's a new day, show it
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::EventResolvedTypeRole)
!= sourceModel()->data(sourceModel()->index(source_row + 1, 0),
MessageEventModel::EventResolvedTypeRole) // Also show it if it's of a different type than the one before TODO improve in
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::AuthorIdRole)
!= sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::AuthorIdRole); // Also show it if it's a different author
}
QVariant CollapseStateProxyModel::data(const QModelIndex &index, int role) const
{
if (role == AggregateDisplayRole) {
return aggregateEventToString(mapToSource(index).row());
}
return sourceModel()->data(mapToSource(index), role);
}
QHash<int, QByteArray> CollapseStateProxyModel::roleNames() const
{
auto roles = sourceModel()->roleNames();
roles[AggregateDisplayRole] = "aggregateDisplay";
return roles;
}
QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
{
QStringList parts;
for (int i = sourceRow; i >= 0; i--) {
parts += sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString();
if (sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::EventTypeRole)
!= MessageEventModel::DelegateType::State // If it's not a state event
|| (i > 0
&& sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::EventResolvedTypeRole)
!= sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventResolvedTypeRole)) // or of a different type
|| (i > 0
&& sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorIdRole)
!= sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::AuthorIdRole)) // or by a different author
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
) {
break;
}
}
if (!parts.isEmpty()) {
QStringList chunks;
while (!parts.isEmpty()) {
chunks += QString();
int count = 1;
auto part = parts.takeFirst();
chunks.last() += part;
while (!parts.isEmpty() && parts.first() == part) {
parts.removeFirst();
count++;
}
if (count > 1) {
chunks.last() += i18ncp("[user did something] n times", " %1 time", " %1 times", count);
}
}
QString text = chunks.takeFirst();
if (chunks.size() > 0) {
while (chunks.size() > 1) {
text += i18nc("[action 1], [action 2 and action 3]", ", ");
text += chunks.takeFirst();
}
text += i18nc("[action 1, action 2] and [action 3]", " and ");
text += chunks.takeFirst();
}
return text;
} else {
return {};
}
}

View File

@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include "messageeventmodel.h"
#include <QSortFilterProxyModel>
class CollapseStateProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
enum Roles {
AggregateDisplayRole = MessageEventModel::LastRole + 1,
};
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
[[nodiscard]] QString aggregateEventToString(int row) const;
};

View File

@@ -8,53 +8,18 @@
class QAbstractItemModel;
class KColorSchemeManager;
/**
* @class ColorSchemer
*
* A class to provide a wrapper around KColorSchemeManager to make it available in
* QML.
*
* @sa KColorSchemeManager
*/
class ColorSchemer : public QObject
{
Q_OBJECT
/**
* @brief A QAbstractItemModel of all available color schemes.
*
* @sa QAbstractItemModel
*/
Q_PROPERTY(QAbstractItemModel *model READ model CONSTANT)
public:
ColorSchemer(QObject *parent = nullptr);
~ColorSchemer();
QAbstractItemModel *model() const;
/**
* @brief Activates the KColorScheme identified by the provided index.
*
* @sa KColorScheme
*/
Q_INVOKABLE void apply(int idx);
/**
* @brief Activates the KColorScheme with the given name.
*
* @sa KColorScheme
*/
Q_INVOKABLE void apply(const QString &name);
/**
* @brief Returns the index for the scheme with the given name.
*/
Q_INVOKABLE int indexForScheme(const QString &name) const;
/**
* @brief Returns the name for the scheme with the given index.
*/
Q_INVOKABLE QString nameForIndex(int index) const;
private:

View File

@@ -1,10 +1,11 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "completionmodel.h"
#include <QDebug>
#include "actionsmodel.h"
#include "chatdocumenthandler.h"
#include "completionproxymodel.h"
#include "customemojimodel.h"
#include "emojimodel.h"
@@ -15,7 +16,7 @@ CompletionModel::CompletionModel(QObject *parent)
: QAbstractListModel(parent)
, m_filterModel(new CompletionProxyModel())
, m_userListModel(new UserListModel(this))
, m_emojiModel(new QConcatenateTablesProxyModel(this))
, m_emojiModel(new KConcatenateRowsProxyModel(this))
{
connect(this, &CompletionModel::textChanged, this, &CompletionModel::updateCompletion);
connect(this, &CompletionModel::roomChanged, this, [this]() {
@@ -54,7 +55,7 @@ QVariant CompletionModel::data(const QModelIndex &index, int role) const
auto filterIndex = m_filterModel->index(index.row(), 0);
if (m_autoCompletionType == User) {
if (role == Text) {
return m_filterModel->data(filterIndex, UserListModel::DisplayNameRole);
return m_filterModel->data(filterIndex, UserListModel::NameRole);
}
if (role == Subtitle) {
return m_filterModel->data(filterIndex, UserListModel::UserIdRole);
@@ -123,7 +124,7 @@ void CompletionModel::updateCompletion()
if (text().startsWith(QLatin1Char('@'))) {
m_filterModel->setSourceModel(m_userListModel);
m_filterModel->setFilterRole(UserListModel::UserIdRole);
m_filterModel->setSecondaryFilterRole(UserListModel::DisplayNameRole);
m_filterModel->setSecondaryFilterRole(UserListModel::NameRole);
m_filterModel->setFullText(m_fullText);
m_filterModel->setFilterText(m_text);
m_autoCompletionType = User;
@@ -144,7 +145,7 @@ void CompletionModel::updateCompletion()
m_filterModel->setFullText(m_fullText);
m_filterModel->setFilterText(m_text);
m_filterModel->invalidate();
} else if (text().startsWith(QLatin1Char(':')) && !text()[1].isUpper()
} else if (text().startsWith(QLatin1Char(':'))
&& (m_fullText.indexOf(QLatin1Char(':'), 1) == -1
|| (m_fullText.indexOf(QLatin1Char(' ')) != -1 && m_fullText.indexOf(QLatin1Char(':'), 1) > m_fullText.indexOf(QLatin1Char(' '), 1)))) {
m_filterModel->setSourceModel(m_emojiModel);

View File

@@ -1,11 +1,12 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QConcatenateTablesProxyModel>
#include <QSortFilterProxyModel>
#include <KConcatenateRowsProxyModel>
#include "roomlistmodel.h"
class CompletionProxyModel;
@@ -13,89 +14,41 @@ class UserListModel;
class NeoChatRoom;
class RoomListModel;
/**
* @class CompletionModel
*
* This class defines the model for suggesting completions in chat text.
*
* This model is able to select the appropriate completion type for the input text
* and present a list of options that can be presented to the user.
*/
class CompletionModel : public QAbstractListModel
{
Q_OBJECT
/**
* @brief The current text to search for completions.
*/
Q_PROPERTY(QString text READ text NOTIFY textChanged)
/**
* @brief The current room that the model is getting completions for.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/**
* @brief The current type of completion being done on the entered text.
*
* @sa AutoCompletionType
*/
Q_PROPERTY(AutoCompletionType autoCompletionType READ autoCompletionType NOTIFY autoCompletionTypeChanged);
/**
* @brief The RoomListModel to be used for room completions.
*/
Q_PROPERTY(RoomListModel *roomListModel READ roomListModel WRITE setRoomListModel NOTIFY roomListModelChanged);
public:
/**
* @brief Defines the different types of completion available.
*/
enum AutoCompletionType {
User, /**< A user in the current room. */
Room, /**< A matrix room. */
Emoji, /**< An emoji. */
Command, /**< A / command. */
None, /**< No available completion for the current text. */
User,
Room,
Emoji,
Command,
None,
};
Q_ENUM(AutoCompletionType)
/**
* @brief Defines the model roles.
*/
enum Roles {
Text = Qt::DisplayRole, /**< The main text to show. */
Subtitle, /**< The subtitle text to show. */
Icon, /**< The icon to show. */
ReplacedText, /**< The text to replace the input text with for the completion. */
Text = Qt::DisplayRole,
Subtitle,
Icon,
ReplacedText,
};
Q_ENUM(Roles);
CompletionModel(QObject *parent = nullptr);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
QString text() const;
void setText(const QString &text, const QString &fullText);
void updateCompletion();
NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
@@ -104,7 +57,6 @@ public:
void setRoomListModel(RoomListModel *roomListModel);
AutoCompletionType autoCompletionType() const;
void setAutoCompletionType(AutoCompletionType autoCompletionType);
Q_SIGNALS:
void textChanged();
@@ -119,10 +71,10 @@ private:
NeoChatRoom *m_room = nullptr;
AutoCompletionType m_autoCompletionType = None;
void updateCompletion();
void setAutoCompletionType(AutoCompletionType autoCompletionType);
UserListModel *m_userListModel;
RoomListModel *m_roomListModel;
QConcatenateTablesProxyModel *m_emojiModel;
KConcatenateRowsProxyModel *m_emojiModel;
};
Q_DECLARE_METATYPE(CompletionModel::AutoCompletionType);

View File

@@ -1,10 +1,11 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "completionproxymodel.h"
#include <QDebug>
#include "neochatroom.h"
bool CompletionProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
Q_UNUSED(sourceParent);
@@ -43,6 +44,7 @@ int CompletionProxyModel::secondaryFilterRole() const
void CompletionProxyModel::setSecondaryFilterRole(int role)
{
m_secondaryFilterRole = role;
Q_EMIT secondaryFilterRoleChanged();
}
QString CompletionProxyModel::filterText() const
@@ -53,6 +55,7 @@ QString CompletionProxyModel::filterText() const
void CompletionProxyModel::setFilterText(const QString &filterText)
{
m_filterText = filterText;
Q_EMIT filterTextChanged();
}
void CompletionProxyModel::setFullText(const QString &fullText)

View File

@@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QSortFilterProxyModel>
class CompletionProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
Q_PROPERTY(int secondaryFilterRole READ secondaryFilterRole WRITE setSecondaryFilterRole NOTIFY secondaryFilterRoleChanged)
Q_PROPERTY(QString filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
public:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
int secondaryFilterRole() const;
void setSecondaryFilterRole(int role);
QString filterText() const;
void setFilterText(const QString &filterText);
void setFullText(const QString &fullText);
Q_SIGNALS:
void secondaryFilterRoleChanged();
void filterTextChanged();
private:
int m_secondaryFilterRole = -1;
QString m_filterText;
QString m_fullText;
};

View File

@@ -1,14 +1,10 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-3.0-only
#include "controller.h"
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <qt5keychain/keychain.h>
#else
#include <qt6keychain/keychain.h>
#endif
#include <KConfig>
#include <KConfigGroup>
@@ -41,10 +37,10 @@
#include <csapi/content-repo.h>
#include <csapi/logout.h>
#include <csapi/profile.h>
#include <jobs/downloadfilejob.h>
#include <qt_connection_util.h>
#ifdef QUOTIENT_07
#include <csapi/notifications.h>
#include <eventstats.h>
#endif
@@ -72,8 +68,24 @@ Controller::Controller(QObject *parent)
setApplicationProxy();
#ifndef Q_OS_ANDROID
setQuitOnLastWindowClosed();
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, &Controller::setQuitOnLastWindowClosed);
if (NeoChatConfig::self()->systemTray()) {
m_trayIcon = new TrayIcon(this);
m_trayIcon->show();
connect(m_trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
QGuiApplication::setQuitOnLastWindowClosed(false);
}
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, [this]() {
if (NeoChatConfig::self()->systemTray()) {
m_trayIcon = new TrayIcon(this);
m_trayIcon->show();
connect(m_trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
} else {
disconnect(m_trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
delete m_trayIcon;
m_trayIcon = nullptr;
}
QGuiApplication::setQuitOnLastWindowClosed(!NeoChatConfig::self()->systemTray());
});
#endif
QTimer::singleShot(0, this, [this] {
@@ -108,26 +120,108 @@ Controller::Controller(QObject *parent)
}
#endif
#ifdef QUOTIENT_07
connect(&Accounts, &AccountRegistry::accountCountChanged, this, &Controller::activeConnectionIndexChanged);
#else
connect(&AccountRegistry::instance(), &AccountRegistry::accountCountChanged, this, &Controller::activeConnectionIndexChanged);
#endif
#ifdef QUOTIENT_07
static int oldAccountCount = 0;
connect(&Accounts, &AccountRegistry::accountCountChanged, this, [this]() {
if (Accounts.size() > oldAccountCount) {
auto connection = Accounts.accounts()[Accounts.size() - 1];
connect(connection, &Connection::syncDone, this, [connection]() {
NotificationsManager::instance().handleNotifications(connection);
connect(&AccountRegistry::instance(), &AccountRegistry::accountCountChanged, this, [=]() {
if (AccountRegistry::instance().size() > oldAccountCount) {
auto connection = AccountRegistry::instance().accounts()[AccountRegistry::instance().size() - 1];
connect(connection, &Connection::syncDone, this, [=]() {
bool changes = false;
for (const auto &room : connection->allRooms()) {
if (m_notificationCounts[room] != room->unreadStats().notableCount) {
m_notificationCounts[room] = room->unreadStats().notableCount;
changes = true;
}
}
if (changes) {
handleNotifications();
}
});
}
oldAccountCount = Accounts.size();
oldAccountCount = AccountRegistry::instance().size();
});
#endif
}
#ifdef QUOTIENT_07
void Controller::handleNotifications()
{
static bool initial = true;
static QStringList oldNotifications;
if (!m_connection) {
return;
}
auto job = m_connection->callApi<GetNotificationsJob>();
connect(job, &BaseJob::success, this, [this, job]() {
const auto notifications = job->jsonData()["notifications"].toArray();
if (initial) {
initial = false;
for (const auto &n : notifications) {
oldNotifications += n.toObject()["event"].toObject()["event_id"].toString();
}
return;
}
for (const auto &n : notifications) {
const auto notification = n.toObject();
if (notification["read"].toBool()) {
oldNotifications.removeOne(notification["event"].toObject()["event_id"].toString());
continue;
}
if (oldNotifications.contains(notification["event"].toObject()["event_id"].toString())) {
continue;
}
oldNotifications += notification["event"].toObject()["event_id"].toString();
auto room = m_connection->room(notification["room_id"].toString());
// If room exists, room is NOT active OR the application is NOT active, show notification
if (room && !(room->id() == RoomManager::instance().currentRoom()->id() && QGuiApplication::applicationState() == Qt::ApplicationActive)) {
// The room might have been deleted (for example rejected invitation).
auto sender = room->user(notification["event"].toObject()["sender"].toString());
QString body;
if (notification["event"].toObject()["type"].toString() == "org.matrix.msc3381.poll.start") {
body = notification["event"]
.toObject()["content"]
.toObject()["org.matrix.msc3381.poll.start"]
.toObject()["question"]
.toObject()["body"]
.toString();
} else {
body = notification["event"].toObject()["content"].toObject()["body"].toString();
}
if (notification["event"]["type"] == "m.room.encrypted") {
#ifdef Quotient_E2EE_ENABLED
auto decrypted = m_connection->decryptNotification(notification);
body = decrypted["content"].toObject()["body"].toString();
#endif
if (body.isEmpty()) {
body = i18n("Encrypted Message");
}
}
QImage avatar_image;
if (!sender->avatarUrl(room).isEmpty()) {
avatar_image = sender->avatar(128, room);
} else {
avatar_image = room->avatar(128);
}
NotificationsManager::instance().postNotification(dynamic_cast<NeoChatRoom *>(room),
sender->displayname(room),
body,
avatar_image,
notification["event"].toObject()["event_id"].toString(),
true);
}
}
});
}
#endif
Controller &Controller::instance()
{
static Controller _instance;
@@ -139,6 +233,38 @@ void Controller::showWindow()
WindowController::instance().showAndRaiseWindow(QString());
}
void Controller::loginWithAccessToken(const QString &serverAddr, const QString &user, const QString &token, const QString &deviceName)
{
if (user.isEmpty() || token.isEmpty()) {
return;
}
QUrl serverUrl(serverAddr);
auto conn = new Connection();
if (serverUrl.isValid()) {
conn->setHomeserver(serverUrl);
}
connect(conn, &Connection::connected, this, [this, conn, deviceName] {
AccountSettings account(conn->userId());
account.setKeepLoggedIn(true);
account.setHomeserver(conn->homeserver());
account.setDeviceId(conn->deviceId());
account.setDeviceName(deviceName);
if (!saveAccessTokenToKeyChain(account, conn->accessToken())) {
qWarning() << "Couldn't save access token";
}
account.sync();
addConnection(conn);
setActiveConnection(conn);
});
connect(conn, &Connection::networkError, this, [this](QString error, const QString &, int, int) {
Q_EMIT errorOccured(i18n("Network Error: %1", error));
});
conn->assumeIdentity(user, token, deviceName);
}
void Controller::logout(Connection *conn, bool serverSideLogout)
{
if (!conn) {
@@ -156,18 +282,10 @@ void Controller::logout(Connection *conn, bool serverSideLogout)
job.start();
loop.exec();
#ifdef QUOTIENT_07
if (Accounts.count() > 1) {
#else
if (AccountRegistry::instance().count() > 1) {
#endif
// Only set the connection if the the account being logged out is currently active
if (conn == activeConnection()) {
#ifdef QUOTIENT_07
setActiveConnection(Accounts.accounts()[0]);
#else
setActiveConnection(AccountRegistry::instance().accounts()[0]);
#endif
}
} else {
setActiveConnection(nullptr);
@@ -182,15 +300,15 @@ void Controller::addConnection(Connection *c)
{
Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection");
#ifdef QUOTIENT_07
Accounts.add(c);
#else
#ifndef QUOTIENT_07
AccountRegistry::instance().add(c);
#endif
c->setLazyLoading(true);
connect(c, &Connection::syncDone, this, [this, c] {
setBusy(false);
Q_EMIT syncDone();
c->sync(30000);
@@ -206,6 +324,8 @@ void Controller::addConnection(Connection *c)
}
});
setBusy(true);
c->sync();
Q_EMIT connectionAdded(c);
@@ -261,11 +381,6 @@ void Controller::invokeLogin()
if (error == "Unrecognised access token") {
Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked"));
logout(connection, false);
} else if (error == "Connection closed") {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
// Failed due to network connection issue. This might happen when the homeserver is
// temporary down, or the user trying to re-launch NeoChat in a network that cannot
// connect to the homeserver. In this case, we don't want to do logout().
} else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
logout(connection, true);
@@ -341,9 +456,15 @@ bool Controller::saveAccessTokenToKeyChain(const AccountSettings &account, const
void Controller::changeAvatar(Connection *conn, const QUrl &localFile)
{
auto job = conn->uploadFile(localFile.toLocalFile());
connect(job, &BaseJob::success, this, [conn, job] {
conn->callApi<SetAvatarUrlJob>(conn->userId(), job->contentUri());
});
#ifdef QUOTIENT_07
if (isJobPending(job)) {
#else
if (isJobRunning(job)) {
#endif
connect(job, &BaseJob::success, this, [conn, job] {
conn->callApi<SetAvatarUrlJob>(conn->userId(), job->contentUri());
});
}
}
void Controller::markAllMessagesAsRead(Connection *conn)
@@ -427,31 +548,34 @@ NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, b
int Controller::accountCount() const
{
#ifdef QUOTIENT_07
return Accounts.count();
#else
return AccountRegistry::instance().count();
#endif
}
void Controller::setQuitOnLastWindowClosed()
bool Controller::quitOnLastWindowClosed()
{
#ifndef Q_OS_ANDROID
if (NeoChatConfig::self()->systemTray()) {
m_trayIcon = new TrayIcon(this);
m_trayIcon->show();
connect(m_trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
} else {
if (m_trayIcon) {
disconnect(m_trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
delete m_trayIcon;
m_trayIcon = nullptr;
}
return QGuiApplication::quitOnLastWindowClosed();
}
void Controller::setQuitOnLastWindowClosed(bool value)
{
if (quitOnLastWindowClosed() != value) {
QGuiApplication::setQuitOnLastWindowClosed(value);
Q_EMIT quitOnLastWindowClosedChanged();
}
QGuiApplication::setQuitOnLastWindowClosed(!NeoChatConfig::self()->systemTray());
#else
return;
#endif
}
bool Controller::busy() const
{
return m_busy;
}
void Controller::setBusy(bool busy)
{
if (m_busy == busy) {
return;
}
m_busy = busy;
Q_EMIT busyChanged();
}
Connection *Controller::activeConnection() const
@@ -469,7 +593,6 @@ void Controller::setActiveConnection(Connection *connection)
}
if (m_connection != nullptr) {
disconnect(m_connection, &Connection::syncError, this, nullptr);
disconnect(m_connection, &Connection::accountDataChanged, this, nullptr);
}
m_connection = connection;
if (connection != nullptr) {
@@ -488,23 +611,12 @@ void Controller::setActiveConnection(Connection *connection)
m_isOnline = true;
Q_EMIT isOnlineChanged(true);
});
connect(connection, &Connection::requestFailed, this, [](BaseJob *job) {
if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"].toString() == "M_TOO_LARGE"_ls) {
RoomManager::instance().warning(i18n("File too large to download."), i18n("Contact your matrix server administrator for support."));
}
});
connect(connection, &Connection::accountDataChanged, this, [this](const QString &type) {
if (type == QLatin1String("org.kde.neochat.account_label")) {
Q_EMIT activeAccountLabelChanged();
}
});
} else {
NeoChatConfig::self()->setActiveConnection(QString());
}
NeoChatConfig::self()->save();
Q_EMIT activeConnectionChanged();
Q_EMIT activeConnectionIndexChanged();
Q_EMIT activeAccountLabelChanged();
}
void Controller::saveWindowGeometry()
@@ -527,31 +639,9 @@ NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Om
void Controller::createRoom(const QString &name, const QString &topic)
{
auto createRoomJob = m_connection->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
connect(createRoomJob, &CreateRoomJob::failure, this, [this, createRoomJob] {
Q_EMIT errorOccured(i18n("Room creation failed: %1", createRoomJob->errorString()));
Quotient::CreateRoomJob::connect(createRoomJob, &CreateRoomJob::failure, [this, createRoomJob] {
Q_EMIT errorOccured(i18n("Room creation failed: \"%1\"", createRoomJob->errorString()));
});
connectSingleShot(this, &Controller::roomAdded, &RoomManager::instance(), &RoomManager::enterRoom, Qt::QueuedConnection);
}
void Controller::createSpace(const QString &name, const QString &topic)
{
auto createRoomJob = m_connection->createRoom(Connection::UnpublishRoom,
{},
name,
topic,
QStringList(),
{},
{},
false,
{},
{},
QJsonObject{
{"type"_ls, "m.space"_ls},
});
connect(createRoomJob, &CreateRoomJob::failure, this, [this, createRoomJob] {
Q_EMIT errorOccured(i18n("Space creation failed: %1", createRoomJob->errorString()));
});
connectSingleShot(this, &Controller::roomAdded, &RoomManager::instance(), &RoomManager::enterRoom, Qt::QueuedConnection);
}
bool Controller::isOnline() const
@@ -621,6 +711,11 @@ bool Controller::hasWindowSystem() const
#endif
}
QString Controller::plainText(QQuickTextDocument *document) const
{
return document->textDocument()->toPlainText();
}
bool Controller::encryptionSupported() const
{
#ifdef QUOTIENT_07
@@ -701,47 +796,3 @@ bool Controller::isFlatpak() const
return false;
#endif
}
void Controller::setActiveAccountLabel(const QString &label)
{
if (!m_connection) {
return;
}
QJsonObject json{
{"account_label", label},
};
m_connection->setAccountData("org.kde.neochat.account_label", json);
}
QString Controller::activeAccountLabel() const
{
if (!m_connection) {
return {};
}
return m_connection->accountDataJson("org.kde.neochat.account_label")["account_label"].toString();
}
QVariantList Controller::getSupportedRoomVersions(Quotient::Connection *connection)
{
auto roomVersions = connection->availableRoomVersions();
QVariantList supportedRoomVersions;
for (const Quotient::Connection::SupportedRoomVersion &v : roomVersions) {
QVariantMap roomVersionMap;
roomVersionMap.insert("id", v.id);
roomVersionMap.insert("status", v.status);
roomVersionMap.insert("isStable", v.isStable());
supportedRoomVersions.append(roomVersionMap);
}
return supportedRoomVersions;
}
bool Controller::callsSupported() const
{
#ifdef GSTREAMER_AVAILABLE
return true;
#else
return false;
#endif
}

View File

@@ -28,205 +28,86 @@ namespace QKeychain
class ReadPasswordJob;
}
/**
* @class Controller
*
* A singleton class designed to help manage the application.
*
* There are also a bunch of helper functions that currently don't fit anywhere
* else.
*/
class Controller : public QObject
{
Q_OBJECT
/**
* @brief The number of logged in accounts.
*/
Q_PROPERTY(int accountCount READ accountCount NOTIFY accountCountChanged)
/**
* @brief The current connection for the rest of NeoChat to use.
*/
Q_PROPERTY(bool quitOnLastWindowClosed READ quitOnLastWindowClosed WRITE setQuitOnLastWindowClosed NOTIFY quitOnLastWindowClosedChanged)
Q_PROPERTY(Quotient::Connection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
/**
* @brief The row number in the accounts directory of the active connection.
*/
Q_PROPERTY(int activeConnectionIndex READ activeConnectionIndex NOTIFY activeConnectionIndexChanged)
/**
* @brief The account label for the active account.
*
* Account labels are a concept specific to NeoChat, allowing accounts to be
* labelled, e.g. for "Work", "Private", etc.
*
* Set to an empty string to remove the label.
*/
Q_PROPERTY(QString activeAccountLabel READ activeAccountLabel WRITE setActiveAccountLabel NOTIFY activeAccountLabelChanged)
/**
* @brief Whether the OS NeoChat is running on supports sytem tray icons.
*/
Q_PROPERTY(bool busy READ busy WRITE setBusy NOTIFY busyChanged)
Q_PROPERTY(bool supportSystemTray READ supportSystemTray CONSTANT)
/**
* @brief Whether KWindowSystem specific features are available.
*/
Q_PROPERTY(bool hasWindowSystem READ hasWindowSystem CONSTANT)
/**
* @brief Whether NeoChat is currently able to connect to the server.
*/
Q_PROPERTY(bool isOnline READ isOnline NOTIFY isOnlineChanged)
/**
* @brief Whether the ecryption support has been enabled.
*/
Q_PROPERTY(bool encryptionSupported READ encryptionSupported CONSTANT)
/**
* @brief The current minor version number of libQuotient being used.
*
* This is the only way to gate NeoChat features by libQuotient version in QML.
*
* @note No major version because libQuotient doesn't have any; All are 0.x.
*/
Q_PROPERTY(int activeConnectionIndex READ activeConnectionIndex NOTIFY activeConnectionIndexChanged)
Q_PROPERTY(int quotientMinorVersion READ quotientMinorVersion CONSTANT)
/**
* @brief Whether NeoChat is running as a flatpak.
*
* 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:
/**
* @brief Defines the status after an attempt to change the password on an account.
*/
enum PasswordStatus {
Success, /**< The password was successfully changed. */
Wrong, /**< The current password entered was wrong. */
Other, /**< An unknown problem occurred. */
};
Q_ENUM(PasswordStatus);
static Controller &instance();
[[nodiscard]] int accountCount() const;
void setActiveConnection(Quotient::Connection *connection);
[[nodiscard]] Quotient::Connection *activeConnection() const;
/**
* @brief Add a new connection to the account registry.
*/
void addConnection(Quotient::Connection *c);
/**
* @brief Drop a connection from the account registry.
*/
void dropConnection(Quotient::Connection *c);
int activeConnectionIndex() const;
Q_INVOKABLE void loginWithAccessToken(const QString &, const QString &, const QString &, const QString &);
[[nodiscard]] QString activeAccountLabel() const;
void setActiveAccountLabel(const QString &label);
/**
* @brief Save an access token to the keychain for the given account.
*/
bool saveAccessTokenToKeyChain(const Quotient::AccountSettings &account, const QByteArray &accessToken);
/**
* @brief Change the password for an account.
*
* The function emits a passwordStatus signal with a PasswordStatus value when
* complete.
*
* @sa PasswordStatus, passwordStatus
*/
Q_INVOKABLE void changePassword(Quotient::Connection *connection, const QString &currentPassword, const QString &newPassword);
/**
* @brief Change the avatar for an account.
*/
Q_INVOKABLE bool setAvatar(Quotient::Connection *connection, const QUrl &avatarSource);
/**
* @brief Create new room for a group chat.
*/
Q_INVOKABLE void createRoom(const QString &name, const QString &topic);
[[nodiscard]] int accountCount() const;
/**
* @brief Create new space.
*/
Q_INVOKABLE void createSpace(const QString &name, const QString &topic);
[[nodiscard]] static bool quitOnLastWindowClosed();
void setQuitOnLastWindowClosed(bool value);
/**
* @brief Join a room.
*/
Q_INVOKABLE void joinRoom(const QString &alias);
/**
* @brief Join a direct chat with the given user.
*
* If a direct chat with the user doesn't exist one is created and then joined.
*/
Q_INVOKABLE void openOrCreateDirectChat(NeoChatUser *user);
[[nodiscard]] bool busy() const;
void setBusy(bool busy);
[[nodiscard]] bool supportSystemTray() const;
/**
* @brief Set the background blur status of the given item.
*/
Q_INVOKABLE void setBlur(QQuickItem *item, bool blur);
bool saveAccessTokenToKeyChain(const Quotient::AccountSettings &account, const QByteArray &accessToken);
int activeConnectionIndex() const;
enum PasswordStatus {
Success,
Wrong,
Other,
};
Q_ENUM(PasswordStatus);
/// \brief Create new room for a group chat.
Q_INVOKABLE void createRoom(const QString &name, const QString &topic);
/// \brief Join a room.
Q_INVOKABLE void joinRoom(const QString &alias);
bool isOnline() const;
Q_INVOKABLE QString formatDuration(quint64 msecs, KFormat::DurationFormatOptions options = KFormat::DefaultDuration) const;
Q_INVOKABLE QString formatByteSize(double size, int precision = 1) const;
Q_INVOKABLE void openOrCreateDirectChat(NeoChatUser *user);
Q_INVOKABLE void setBlur(QQuickItem *item, bool blur);
Q_INVOKABLE QString plainText(QQuickTextDocument *document) const;
bool encryptionSupported() const;
/**
* @brief Sets the QNetworkProxy for the application.
*
* @sa QNetworkProxy::setApplicationProxy
*/
Q_INVOKABLE void forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item);
Q_INVOKABLE void setApplicationProxy();
int quotientMinorVersion() const;
bool isFlatpak() const;
bool callsSupported() const;
/**
* @brief Return a string for the input timestamp.
*
* The output format depends on the KFormat::DurationFormatOptions chosen.
*
* @sa KFormat::DurationFormatOptions
*/
Q_INVOKABLE QString formatDuration(quint64 msecs, KFormat::DurationFormatOptions options = KFormat::DefaultDuration) const;
/**
* @brief Return a human readable string for a given input number of bytes.
*/
Q_INVOKABLE QString formatByteSize(double size, int precision = 1) const;
/**
* @brief Force a QQuickTextDocument to refresh when images are loaded.
*
* HACK: This is a workaround for QTBUG 93281.
*/
Q_INVOKABLE void forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item);
Q_INVOKABLE QVariantList getSupportedRoomVersions(Quotient::Connection *connection);
private:
explicit Controller(QObject *parent = nullptr);
QPointer<Quotient::Connection> m_connection;
bool m_busy = false;
TrayIcon *m_trayIcon = nullptr;
QKeychain::ReadPasswordJob *loadAccessTokenFromKeyChain(const Quotient::AccountSettings &account);
@@ -237,13 +118,16 @@ private:
QMap<Quotient::Room *, int> m_notificationCounts;
bool hasWindowSystem() const;
#ifdef QUOTIENT_07
void handleNotifications();
#endif
private Q_SLOTS:
void invokeLogin();
void showWindow();
void setQuitOnLastWindowClosed();
Q_SIGNALS:
void busyChanged();
/// Error occurred because of user inputs
void errorOccured(const QString &error);
@@ -267,8 +151,6 @@ Q_SIGNALS:
void keyVerificationAccept(const QString &commitment);
void keyVerificationKey(const QString &sas);
void activeConnectionIndexChanged();
void roomAdded(NeoChatRoom *room);
void activeAccountLabelChanged();
public Q_SLOTS:
void logout(Quotient::Connection *conn, bool serverSideLogout);

View File

@@ -2,20 +2,21 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "customemojimodel.h"
#include <QImage>
#include <QMimeDatabase>
#include "controller.h"
#include "emojimodel.h"
#include <connection.h>
#include <csapi/account-data.h>
#include <csapi/content-repo.h>
#include <events/eventcontent.h>
using namespace Quotient;
#ifdef QUOTIENT_07
#define running isJobPending
#else
#define running isJobRunning
#endif
void CustomEmojiModel::fetchEmojis()
{
if (!Controller::instance().activeConnection()) {
@@ -56,35 +57,22 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
auto job = Controller::instance().activeConnection()->uploadFile(location.toLocalFile());
connect(job, &BaseJob::success, this, [name, location, job] {
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes");
auto json = data != nullptr ? data->contentJson() : QJsonObject();
auto emojiData = json["images"].toObject();
QString url;
if (running(job)) {
connect(job, &BaseJob::success, this, [this, name, job] {
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes");
auto json = data != nullptr ? data->contentJson() : QJsonObject();
auto emojiData = json["images"].toObject();
emojiData[QStringLiteral("%1").arg(name)] = QJsonObject({
#ifdef QUOTIENT_07
url = job->contentUri().toString();
{QStringLiteral("url"), job->contentUri().toString()}
#else
url = job->contentUri();
{QStringLiteral("url"), job->contentUri()}
#endif
QImage image(location.toLocalFile());
QJsonObject imageInfo;
imageInfo["w"] = image.width();
imageInfo["h"] = image.height();
imageInfo["mimetype"] = QMimeDatabase().mimeTypeForFile(location.toLocalFile()).name();
imageInfo["size"] = image.sizeInBytes();
emojiData[QStringLiteral("%1").arg(name)] = QJsonObject({
{QStringLiteral("url"), url},
{QStringLiteral("info"), imageInfo},
{QStringLiteral("body"), location.fileName()},
{"usage"_ls, "emoticon"_ls},
});
json["images"] = emojiData;
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes", json);
});
json["images"] = emojiData;
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes", json);
});
}
}
void CustomEmojiModel::removeEmoji(const QString &name)
@@ -153,8 +141,6 @@ QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
return QUrl(QStringLiteral("image://mxc/") + data.url.mid(6));
case Roles::MxcUrl:
return data.url.mid(6);
default:
return {};
}
return QVariant();

57
src/customemojimodel.h Normal file
View File

@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <memory>
#include <QRegularExpression>
struct CustomEmoji {
QString name; // with :semicolons:
QString url; // mxc://
QRegularExpression regexp;
Q_GADGET
Q_PROPERTY(QString unicode MEMBER url)
Q_PROPERTY(QString name MEMBER name)
};
class CustomEmojiModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
Name = Qt::DisplayRole,
ImageURL,
ModelData, // for emulating the regular emoji model's usage, otherwise the UI code would get too complicated
MxcUrl = 50,
DisplayRole = 51,
ReplacedTextRole = 52,
DescriptionRole = 53, // also invalid, reserved
};
Q_ENUM(Roles);
static CustomEmojiModel &instance()
{
static CustomEmojiModel _instance;
return _instance;
}
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE QString preprocessText(const QString &it);
Q_INVOKABLE QVariantList filterModel(const QString &filter);
Q_INVOKABLE void addEmoji(const QString &name, const QUrl &location);
Q_INVOKABLE void removeEmoji(const QString &name);
private:
explicit CustomEmojiModel(QObject *parent = nullptr);
QList<CustomEmoji> m_emojis;
void fetchEmojis();
};

View File

@@ -1,153 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "delegatesizehelper.h"
#include <cmath>
#include <QDebug>
DelegateSizeHelper::DelegateSizeHelper(QObject *parent)
: QObject(parent)
{
}
qreal DelegateSizeHelper::parentWidth() const
{
return m_parentWidth;
}
void DelegateSizeHelper::setParentWidth(qreal parentWidth)
{
if (parentWidth == m_parentWidth) {
return;
}
m_parentWidth = parentWidth;
Q_EMIT parentWidthChanged();
Q_EMIT currentPercentageWidthChanged();
Q_EMIT currentWidthChanged();
}
qreal DelegateSizeHelper::startBreakpoint() const
{
return m_startBreakpoint;
}
void DelegateSizeHelper::setStartBreakpoint(qreal startBreakpoint)
{
if (startBreakpoint == m_startBreakpoint) {
return;
}
m_startBreakpoint = startBreakpoint;
Q_EMIT startBreakpointChanged();
}
qreal DelegateSizeHelper::endBreakpoint() const
{
return m_endBreakpoint;
}
void DelegateSizeHelper::setEndBreakpoint(qreal endBreakpoint)
{
if (endBreakpoint == m_endBreakpoint) {
return;
}
m_endBreakpoint = endBreakpoint;
Q_EMIT endBreakpointChanged();
}
int DelegateSizeHelper::startPercentWidth() const
{
return m_startPercentWidth;
}
void DelegateSizeHelper::setStartPercentWidth(int startPercentWidth)
{
if (startPercentWidth == m_startPercentWidth) {
return;
}
m_startPercentWidth = startPercentWidth;
Q_EMIT startPercentWidthChanged();
}
int DelegateSizeHelper::endPercentWidth() const
{
return m_endPercentWidth;
}
void DelegateSizeHelper::setEndPercentWidth(int endPercentWidth)
{
if (endPercentWidth == m_endPercentWidth) {
return;
}
m_endPercentWidth = endPercentWidth;
Q_EMIT endPercentWidthChanged();
}
qreal DelegateSizeHelper::maxWidth() const
{
return m_maxWidth;
}
void DelegateSizeHelper::setMaxWidth(qreal maxWidth)
{
if (maxWidth == m_maxWidth) {
return;
}
m_maxWidth = maxWidth;
Q_EMIT maxWidthChanged();
}
int DelegateSizeHelper::calculateCurrentPercentageWidth() const
{
// Don't do anything if m_parentWidth hasn't been set yet.
if (m_parentWidth < 0) {
return -1;
}
// Don't bother with calculations for a horizontal line.
if (m_startPercentWidth == m_endPercentWidth) {
return m_startPercentWidth;
}
// Dividing by zero is a bad idea.
if (m_startBreakpoint == m_endBreakpoint) {
qWarning() << "DelegateSizeHelper::calculateCurrentPercentageWidth() - m_startBreakpoint is equal to m_endBreakpoint this would lead to divide by "
"zero, aborting";
return -1;
}
// Fit to y = mx + c
qreal m = (m_endPercentWidth - m_startPercentWidth) / (m_endBreakpoint - m_startBreakpoint);
qreal c = m_startPercentWidth - m * m_startBreakpoint;
// This allows us to clamp correctly if the start or end width is bigger.
bool endPercentBigger = m_endPercentWidth > m_startPercentWidth;
int maxPercentWidth = endPercentBigger ? m_endPercentWidth : m_startPercentWidth;
int minPercentWidth = endPercentBigger ? m_startPercentWidth : m_endPercentWidth;
int calcPercentWidth = std::ceil(m * m_parentWidth + c);
return std::clamp(calcPercentWidth, minPercentWidth, maxPercentWidth);
}
int DelegateSizeHelper::currentPercentageWidth() const
{
return calculateCurrentPercentageWidth();
}
qreal DelegateSizeHelper::currentWidth() const
{
if (m_parentWidth < 0) {
return 0.0;
}
int percentWidth = calculateCurrentPercentageWidth();
// - 1 means bad input values so don't try to calculate.
if (percentWidth == -1) {
return 0.0;
}
qreal absoluteWidth = m_parentWidth * percentWidth * 0.01;
if (m_maxWidth < 0.0) {
return std::ceil(absoluteWidth);
} else {
return std::ceil(std::min(absoluteWidth, m_maxWidth));
}
}

View File

@@ -1,123 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
/**
* @class DelegateSizeHelper
*
* A class to help calculate the current width of a chat delegate/bar.
*
* The aim is to support a dynamic sizing based upon the width of the page. There is
* a built in curve that allows the width to transition between two percentages based
* upon a start and finish break point. This is to provide better convergence where
* generally the delegate will need to fill all or most the screen when thin but
* should max out in size and only fill a lower percentage of the screen when wide.
*
* @note While the main intended usage is to start with a high percentage when the parentWidth
* is small and transition to a lower one when large, the math is setup for the
* general case so any combination of parameters works.
*/
class DelegateSizeHelper : public QObject
{
Q_OBJECT
/**
* @brief The width of the component's parent.
*/
Q_PROPERTY(qreal parentWidth READ parentWidth WRITE setParentWidth NOTIFY parentWidthChanged)
/**
* @brief The width (in px) when the width percentage should start to transition.
*/
Q_PROPERTY(qreal startBreakpoint READ startBreakpoint WRITE setStartBreakpoint NOTIFY startBreakpointChanged)
/**
* @brief The width (in px) when the width percentage should finish transitioning.
*/
Q_PROPERTY(qreal endBreakpoint READ endBreakpoint WRITE setEndBreakpoint NOTIFY endBreakpointChanged)
/**
* @brief The width (in %) of the component at or before the startBreakpoint.
*
* @sa startBreakpoint
*/
Q_PROPERTY(int startPercentWidth READ startPercentWidth WRITE setStartPercentWidth NOTIFY startPercentWidthChanged)
/**
* @brief The width (in %) of the component at or after the endBreakpoint.
*
* @sa endBreakpoint
*/
Q_PROPERTY(int endPercentWidth READ endPercentWidth WRITE setEndPercentWidth NOTIFY endPercentWidthChanged)
/**
* @brief The absolute maximum width (in px) the component can be.
*/
Q_PROPERTY(qreal maxWidth READ maxWidth WRITE setMaxWidth NOTIFY maxWidthChanged)
/**
* @brief The width (in %) of the component at the current parentWidth.
*
* Will return -1 if no parentWidth is set or startBreakpoint == endBreakpoint.
*
* @sa parentWidth, startBreakpoint, endBreakpoint
*/
Q_PROPERTY(int currentPercentageWidth READ currentPercentageWidth NOTIFY currentPercentageWidthChanged)
/**
* @brief The width (in px) of the component at the current parentWidth.
*
* Will return 0.0 if no parentWidth is set.
*
* @sa parentWidth
*/
Q_PROPERTY(qreal currentWidth READ currentWidth NOTIFY currentWidthChanged)
public:
DelegateSizeHelper(QObject *parent = nullptr);
qreal parentWidth() const;
void setParentWidth(qreal parentWidth);
qreal startBreakpoint() const;
void setStartBreakpoint(qreal startBreakpoint);
qreal endBreakpoint() const;
void setEndBreakpoint(qreal endBreakpoint);
int startPercentWidth() const;
void setStartPercentWidth(int startPercentWidth);
int endPercentWidth() const;
void setEndPercentWidth(int endPercentWidth);
qreal maxWidth() const;
void setMaxWidth(qreal maxWidth);
int currentPercentageWidth() const;
qreal currentWidth() const;
Q_SIGNALS:
void parentWidthChanged();
void startBreakpointChanged();
void endBreakpointChanged();
void startPercentWidthChanged();
void endPercentWidthChanged();
void maxWidthChanged();
void currentPercentageWidthChanged();
void currentWidthChanged();
private:
qreal m_parentWidth = -1.0;
qreal m_startBreakpoint;
qreal m_endBreakpoint;
int m_startPercentWidth;
int m_endPercentWidth;
qreal m_maxWidth = -1.0;
int calculateCurrentPercentageWidth() const;
};

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "devicesmodel.h"
@@ -70,12 +70,7 @@ void DevicesModel::logout(int index, const QString &password)
auto job = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId);
connect(job, &BaseJob::result, this, [this, job, password, index] {
auto onSuccess = [this, index]() {
beginRemoveRows(QModelIndex(), index, index);
m_devices.remove(index);
endRemoveRows();
};
if (job->error() != BaseJob::Success) {
if (job->error() != 0) {
QJsonObject replyData = job->jsonData();
QJsonObject authData;
authData["session"] = replyData["session"];
@@ -84,9 +79,11 @@ void DevicesModel::logout(int index, const QString &password)
QJsonObject identifier = {{"type", "m.id.user"}, {"user", Controller::instance().activeConnection()->user()->id()}};
authData["identifier"] = identifier;
auto *innerJob = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId, authData);
connect(innerJob, &BaseJob::success, this, onSuccess);
} else {
onSuccess();
connect(innerJob, &BaseJob::success, this, [this, index]() {
beginRemoveRows(QModelIndex(), index, index);
m_devices.remove(index);
endRemoveRows();
});
}
});
}

47
src/devicesmodel.h Normal file
View File

@@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <csapi/definitions/client_device.h>
namespace Quotient
{
class Connection;
}
class DevicesModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(Quotient::Connection *connection READ connection NOTIFY connectionChanged)
public:
enum Roles {
Id,
DisplayName,
LastIp,
LastTimestamp,
};
Q_ENUM(Roles);
DevicesModel(QObject *parent = nullptr);
QVariant data(const QModelIndex &index, int role) const override;
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent) const override;
Q_INVOKABLE void logout(int index, const QString &password);
Q_INVOKABLE void setName(int index, const QString &name);
Quotient::Connection *connection() const;
Q_SIGNALS:
void connectionChanged();
private:
void fetchDevices();
QVector<Quotient::Device> m_devices;
};

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