Compare commits

..

3 Commits

Author SHA1 Message Date
Carl Schwan
8177c1f1bc Port About page to FormCard 2023-08-22 22:03:35 +00:00
Carl Schwan
80171748d8 Port more MobileForm to FormCard
- DevTools
- Network settings
- Push notification
- Permissions
2023-08-22 22:03:35 +00:00
Carl Schwan
6aa2e586de Port to form card 2023-08-22 22:03:35 +00:00
229 changed files with 43962 additions and 56395 deletions

View File

@@ -3,8 +3,12 @@
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
- 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-qt6.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.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

@@ -2,7 +2,35 @@
# SPDX-License-Identifier: BSD-2-Clause
Dependencies:
- 'on': ['Linux', 'Android', 'FreeBSD', 'Windows']
- 'on': ['Linux/Qt5', 'Android/Qt5', 'FreeBSD/Qt5', 'Windows/Qt5']
'require':
'frameworks/extra-cmake-modules': '@stable'
'frameworks/kcoreaddons': '@stable'
'frameworks/kirigami': '@stable'
'frameworks/ki18n': '@stable'
'frameworks/kconfig': '@stable'
'frameworks/syntax-highlighting': '@stable'
'frameworks/kitemmodels': '@stable'
'frameworks/kquickcharts': '@stable'
'frameworks/knotifications': '@stable'
'libraries/kquickimageeditor': '@stable'
'frameworks/sonnet': '@stable'
'libraries/kirigami-addons': '@latest'
'third-party/libquotient': '@latest'
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- 'on': ['Windows/Qt5', 'Linux/Qt5', 'FreeBSD/Qt5']
'require':
'frameworks/qqc2-desktop-style': '@stable'
'frameworks/kio': '@stable'
'frameworks/kwindowsystem': '@stable'
'frameworks/kconfigwidgets': '@stable'
- 'on': ['Linux/Qt5', 'FreeBSD/Qt5']
'require':
'frameworks/kdbusaddons': '@stable'
- 'on': ['Linux/Qt6', 'Android/Qt6', 'FreeBSD/Qt6', 'Windows/Qt6']
'require':
'frameworks/extra-cmake-modules': '@latest-kf6'
'frameworks/kcoreaddons': '@latest-kf6'
@@ -20,21 +48,20 @@ Dependencies:
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- 'on': ['Windows', 'Linux', 'FreeBSD']
- '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', 'FreeBSD']
- 'on': ['Linux/Qt6', 'FreeBSD/Qt6']
'require':
'frameworks/kdbusaddons': '@latest-kf6'
'frameworks/kstatusnotifieritem': '@latest-kf6'
- 'on': ['Linux']
- 'on': ['Linux/Qt6', 'Linux/Qt5']
'require':
'sdk/selenium-webdriver-at-spi': '@latest-kf6'
Options:
per-test-timeout: 90
require-passing-tests-on: [ 'FreeBSD', 'Windows' ]
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD', 'Windows' ]

View File

@@ -14,8 +14,11 @@ set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
set(KF_MIN_VERSION "5.240.0")
set(QT_MIN_VERSION "6.5")
set(KF_MIN_VERSION "5.105.0")
set(QT_MIN_VERSION "5.15.2")
if (ANDROID)
set(QT_MIN_VERSION "5.15.10")
endif()
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
@@ -45,6 +48,28 @@ if(NEOCHAT_FLATPAK)
include(cmake/Flatpak.cmake)
endif()
if(QT_MAJOR_VERSION STREQUAL "6")
set(BASICLISTITEM_BOLD "font.bold")
set(OVERLAYSHEET_OPEN "onOpened")
set(QTQUICK_MODULE_QML_VERSION "")
set(QTLOCATION_MODULE_QML_VERSION "")
set(QTMULTIMEDIA_MODULE_QML_VERSION "")
set(QTMULTIMEDIA_AUDIO "MediaPlayer")
# in Audio qt6 we don't have it but we disable it in qt5 => it seems ok
set(QTMULTIMEDIA_AUDIO_AUTOLOAD "")
# In Video qml qt6 we don't have it.
set(QTMULTIMEDIA_VIDEO_FLUSHMODE "")
else()
set(BASICLISTITEM_BOLD "bold")
set(OVERLAYSHEET_OPEN "onSheetOpenChanged")
set(QTQUICK_MODULE_QML_VERSION "2.15")
set(QTLOCATION_MODULE_QML_VERSION "5.15")
set(QTMULTIMEDIA_MODULE_QML_VERSION "5.15")
set(QTMULTIMEDIA_AUDIO "Audio")
set(QTMULTIMEDIA_AUDIO_AUTOLOAD "autoLoad: false")
set(QTMULTIMEDIA_VIDEO_FLUSHMODE "flushMode: VideoOutput.FirstFrame")
endif()
set(QUOTIENT_FORCE_NAMESPACED_INCLUDES TRUE)
ecm_setup_version(${PROJECT_VERSION}
@@ -52,21 +77,25 @@ ecm_setup_version(${PROJECT_VERSION}
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
)
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg WebView)
set_package_properties(Qt6 PROPERTIES
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg)
set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
set_package_properties(KF6 PROPERTIES
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
TYPE REQUIRED
PURPOSE "Basic application components"
)
set_package_properties(KF6Kirigami2 PROPERTIES
set_package_properties(KF${QT_MAJOR_VERSION}Kirigami2 PROPERTIES
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
find_package(KF6KirigamiAddons 0.7.2 REQUIRED)
find_package(KF${QT_MAJOR_VERSION}KirigamiAddons 0.7.2 REQUIRED)
if(QT_MAJOR_VERSION STREQUAL "6")
find_package(KF6StatusNotifierItem ${KF_MIN_VERSION} REQUIRED)
endif()
if(ANDROID)
find_package(OpenSSL)
@@ -75,20 +104,24 @@ if(ANDROID)
PURPOSE "Encrypted communications"
)
else()
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem StatusNotifierItem)
set_package_properties(KF6QQC2DesktopStyle PROPERTIES
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
TYPE RUNTIME
)
ecm_find_qmlmodule(org.kde.syntaxhighlighting 1.0)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
find_package(KF${QT_MAJOR_VERSION}DBusAddons ${KF_MIN_VERSION} REQUIRED)
endif()
find_package(QuotientQt6 0.7)
set_package_properties(QuotientQt6 PROPERTIES
if(QT_MAJOR_VERSION STREQUAL "6" AND NOT ANDROID AND NOT WIN32)
set(QUOTIENT_SUFFIX "Qt6")
endif()
find_package(Quotient${QUOTIENT_SUFFIX} 0.7)
set_package_properties(Quotient${QUOTIENT_SUFFIX} PROPERTIES
TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API"
URL "https://github.com/quotient-im/libQuotient/"
@@ -111,7 +144,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)
ecm_find_qmlmodule(QtLocation ${QTLOCATION_MODULE_QML_VERSION})
find_package(KQuickImageEditor COMPONENTS)
set_package_properties(KQuickImageEditor PROPERTIES
@@ -121,12 +153,12 @@ set_package_properties(KQuickImageEditor PROPERTIES
PURPOSE "Add image editing capability to image attachments"
)
find_package(QCoro6 0.4 COMPONENTS Core REQUIRED)
find_package(QCoro${QT_MAJOR_VERSION} 0.4 COMPONENTS Core REQUIRED)
qcoro_enable_coroutines()
find_package(KF6DocTools ${KF_MIN_VERSION})
set_package_properties(KF6DocTools PROPERTIES DESCRIPTION
find_package(KF${QT_MAJOR_VERSION}DocTools ${KF_MIN_VERSION})
set_package_properties(KF${QT_MAJOR_VERSION}DocTools PROPERTIES DESCRIPTION
"Tools to generate documentation"
TYPE OPTIONAL
)
@@ -148,12 +180,12 @@ add_definitions(-DQT_NO_FOREACH)
add_subdirectory(src)
if (BUILD_TESTING)
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
add_subdirectory(autotests)
add_subdirectory(appiumtests)
endif()
if(KF6DocTools_FOUND)
if(KF${QT_MAJOR_VERSION}DocTools_FOUND)
kdoctools_install(po)
add_subdirectory(doc)
endif()

View File

@@ -9,7 +9,7 @@
android:versionName="${versionName}"
android:versionCode="${versionCode}"
android:installLocation="auto">
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat" android:usesCleartextTraffic="true">
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation"
android:name="org.qtproject.qt5.android.bindings.QtActivity"
android:label="NeoChat"

View File

@@ -18,6 +18,6 @@ if(NOT SeleniumWebDriverATSPI_FOUND)
endif()
add_test(
NAME logintest
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/logintest.py
NAME logintest
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/logintest.py
)

View File

@@ -8,10 +8,8 @@ import unittest
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
import os
import time
import subprocess
import sys
class LoginTest(unittest.TestCase):
@classmethod
@@ -22,7 +20,7 @@ class LoginTest(unittest.TestCase):
self.driver = webdriver.Remote(
command_executor='http://127.0.0.1:4723',
desired_capabilities=desired_caps)
self.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "login-server.py")])
subprocess.Popen(["python","login-server.py"])
def setUp(self):
pass
@@ -33,12 +31,9 @@ class LoginTest(unittest.TestCase):
@classmethod
def tearDownClass(self):
self.mockServerProcess.terminate()
self.driver.quit()
def test_login(self):
self.driver.find_element(by=AppiumBy.NAME, value="Login").click()
self.driver.find_element(by=AppiumBy.NAME, value="Matrix ID").send_keys("@user:localhost:1234")
self.driver.find_element(by=AppiumBy.NAME, value="Continue").click()
self.driver.find_element(by=AppiumBy.NAME, value="Password").send_keys("1234")

View File

@@ -20,15 +20,3 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test
TEST_NAME delegatesizehelpertest
)
ecm_add_test(
mediasizehelpertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME mediasizehelpertest
)
ecm_add_test(
eventhandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME eventhandlertest
)

View File

@@ -1,729 +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 "eventhandler.h"
#include <KFormat>
#include <Quotient/connection.h>
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include "enums/delegatetype.h"
#include "linkpreviewer.h"
#include "models/reactionmodel.h"
#include "neochatroom.h"
#include "utils.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 EventHandlerTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestRoom *room = nullptr;
EventHandler eventHandler;
private Q_SLOTS:
void initTestCase();
void eventId();
void delegateType_data();
void delegateType();
void author();
void authorDisplayName();
void time();
void timeString();
void highlighted();
void hidden();
void body();
void genericBody_data();
void genericBody();
void mediaInfo();
void linkPreviewer();
void reactions();
void hasReply();
void replyId();
void replyDelegateType();
void replyAuthor();
void replyBody();
void replyMediaInfo();
void location();
void readMarkers();
};
void EventHandlerTest::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"
},
{
"content": {
"$153456789:example.org": {
"m.read": {
"@alice:matrix.org": {
"ts": 1436451550453
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@bob:example.com": {
"ts": 1436451550453
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@tim:example.com": {
"ts": 1436451550454
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@jeff:example.com": {
"ts": 1436451550455
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@tina:example.com": {
"ts": 1436451550456
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@sally:example.com": {
"ts": 1436451550457
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@fred:example.com": {
"ts": 1436451550458
}
}
}
},
"type": "m.receipt"
}
]
},
"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\ntext message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example<br>text message</b>",
"msgtype": "m.text"
},
"event_id": "$153456789:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"avatar_url": "mxc://kde.org/123456",
"displayname": "after",
"membership": "join"
},
"origin_server_ts": 1690651134736,
"sender": "@example:example.org",
"state_key": "@example:example.org",
"type": "m.room.member",
"unsigned": {
"replaces_state": "$1234567890:example.org",
"prev_content": {
"avatar_url": "mxc://kde.org/12345",
"displayname": "before",
"membership": "join"
},
"prev_sender": "@example:example.orgg",
"age": 1234
},
"event_id": "$143273583553PhrSn:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"content": {
"body": "This is a highlight @bob:kde.org and this is a link https://kde.org",
"format": "org.matrix.custom.html",
"msgtype": "m.text"
},
"event_id": "$1532735824654:example.org",
"origin_server_ts": 1532735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"m.relates_to": {
"event_id": "$153456789:example.org",
"key": "👍",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1690322545182,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@alice:matrix.org",
"type": "m.reaction",
"unsigned": {
"age": 390159120
},
"event_id": "$163456789:example.org",
"age": 390159120
},
{
"age": 4926305285,
"content": {
"body": "video caption",
"filename": "video.mp4",
"info": {
"duration": 10,
"h": 1080,
"mimetype": "video/mp4",
"size": 62650636,
"w": 1920,
"thumbnail_info": {
"h": 450,
"mimetype": "image/jpeg",
"size": 382249,
"w": 800
},
"thumbnail_url": "mxc://kde.org/2234567"
},
"msgtype": "m.video",
"url": "mxc://kde.org/1234567"
},
"event_id": "$263456789:example.org",
"origin_server_ts": 1685793783330,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 4926305285
},
"user_id": "@example:example.org"
},
{
"content": {
"body": "> <@example:example.org> This is an example\ntext message\n\nreply",
"format": "org.matrix.custom.html",
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!jEsUZKDJdhlrceRyVU:example.org/$153456789:example.org?via=kde.org&via=matrix.org\">In reply to</a> <a href=\"https://matrix.to/#/@example:example.org\">@example:example.org</a><br><b>This is an example<br>text message</b></blockquote></mx-reply>reply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$153456789:example.org"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1690725965572,
"sender": "@alice:matrix.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": "$154456789:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"content": {
"body": "> <@example:example.org> video caption\n\nreply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$263456789:example.org"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1690725965573,
"sender": "@alice:matrix.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": "$154456799:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"age": 96845207,
"content": {
"body": "Lat: 51.7035, Lon: -1.14394",
"geo_uri": "geo:51.7035,-1.14394",
"msgtype": "m.location",
"org.matrix.msc1767.text": "Lat: 51.7035, Lon: -1.14394",
"org.matrix.msc3488.asset": {
"type": "m.pin"
},
"org.matrix.msc3488.location": {
"uri": "geo:51.7035,-1.14394"
}
},
"event_id": "$1544567999:example.org",
"origin_server_ts": 1690821582876,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 96845207
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
})EVENT");
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
room->update(std::move(roomData));
eventHandler.setRoom(room);
}
void EventHandlerTest::eventId()
{
eventHandler.setEvent(room->messageEvents().at(0).get());
QCOMPARE(eventHandler.getId(), QStringLiteral("$153456789:example.org"));
}
void EventHandlerTest::delegateType_data()
{
QTest::addColumn<int>("eventNum");
QTest::addColumn<DelegateType::Type>("delegateType");
QTest::newRow("message") << 0 << DelegateType::Message;
QTest::newRow("state") << 1 << DelegateType::State;
QTest::newRow("message 2") << 2 << DelegateType::Message;
QTest::newRow("reaction") << 3 << DelegateType::Other;
QTest::newRow("video") << 4 << DelegateType::Video;
QTest::newRow("location") << 7 << DelegateType::Location;
}
void EventHandlerTest::delegateType()
{
QFETCH(int, eventNum);
QFETCH(DelegateType::Type, delegateType);
eventHandler.setEvent(room->messageEvents().at(eventNum).get());
QCOMPARE(eventHandler.getDelegateType(), delegateType);
}
void EventHandlerTest::author()
{
auto event = room->messageEvents().at(0).get();
auto author = room->user(event->senderId());
eventHandler.setEvent(event);
auto eventHandlerAuthor = eventHandler.getAuthor();
QCOMPARE(eventHandlerAuthor["isLocalUser"_ls], author->id() == room->localUser()->id());
QCOMPARE(eventHandlerAuthor["id"_ls], author->id());
QCOMPARE(eventHandlerAuthor["displayName"_ls], author->displayname(room));
QCOMPARE(eventHandlerAuthor["avatarSource"_ls], room->avatarForMember(author));
QCOMPARE(eventHandlerAuthor["avatarMediaId"_ls], author->avatarMediaId(room));
QCOMPARE(eventHandlerAuthor["color"_ls], Utils::getUserColor(author->hueF()));
QCOMPARE(eventHandlerAuthor["object"_ls], QVariant::fromValue(author));
}
void EventHandlerTest::authorDisplayName()
{
auto event = room->messageEvents().at(1).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getAuthorDisplayName(), QStringLiteral("before"));
}
void EventHandlerTest::time()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getTime(), QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC));
QCOMPARE(eventHandler.getTime(true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC));
}
void EventHandlerTest::timeString()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
KFormat format;
QCOMPARE(eventHandler.getTimeString(false),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(eventHandler.getTimeString(true),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(eventHandler.getTimeString(false, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(eventHandler.getTimeString(true, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(eventHandler.getTimeString(false, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::LongFormat));
QCOMPARE(eventHandler.getTimeString(true, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::LongFormat));
}
void EventHandlerTest::highlighted()
{
auto event = room->messageEvents().at(2).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isHighlighted(), true);
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isHighlighted(), false);
}
void EventHandlerTest::hidden()
{
auto event = room->messageEvents().at(3).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isHidden(), true);
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isHidden(), false);
}
void EventHandlerTest::body()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
QCOMPARE(eventHandler.getRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
QCOMPARE(eventHandler.getPlainBody(), QStringLiteral("This is an example\ntext message"));
QCOMPARE(eventHandler.getPlainBody(true), QStringLiteral("This is an example text message"));
}
void EventHandlerTest::genericBody_data()
{
QTest::addColumn<int>("eventNum");
QTest::addColumn<QString>("output");
QTest::newRow("message") << 0 << QStringLiteral("sent a message");
QTest::newRow("member") << 1 << QStringLiteral("changed their display name and updated their avatar");
QTest::newRow("message 2") << 2 << QStringLiteral("sent a message");
QTest::newRow("reaction") << 3 << QStringLiteral("Unknown event");
QTest::newRow("video") << 4 << QStringLiteral("sent a message");
}
void EventHandlerTest::genericBody()
{
QFETCH(int, eventNum);
QFETCH(QString, output);
eventHandler.setEvent(room->messageEvents().at(eventNum).get());
QCOMPARE(eventHandler.getGenericBody(), output);
}
void EventHandlerTest::mediaInfo()
{
auto event = room->messageEvents().at(4).get();
eventHandler.setEvent(event);
auto mediaInfo = eventHandler.getMediaInfo();
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
QCOMPARE(mediaInfo["source"_ls], room->makeMediaUrl(event->id(), QUrl("mxc://kde.org/1234567"_ls)));
QCOMPARE(mediaInfo["mimeType"_ls], QStringLiteral("video/mp4"));
QCOMPARE(mediaInfo["mimeIcon"_ls], QStringLiteral("video-mp4"));
QCOMPARE(mediaInfo["size"_ls], 62650636);
QCOMPARE(mediaInfo["duration"_ls], 10);
QCOMPARE(mediaInfo["width"_ls], 1920);
QCOMPARE(mediaInfo["height"_ls], 1080);
QCOMPARE(thumbnailInfo["source"_ls], room->makeMediaUrl(event->id(), QUrl("mxc://kde.org/2234567"_ls)));
QCOMPARE(thumbnailInfo["mimeType"_ls], QStringLiteral("image/jpeg"));
QCOMPARE(thumbnailInfo["mimeIcon"_ls], QStringLiteral("image-jpeg"));
QCOMPARE(thumbnailInfo["size"_ls], 382249);
QCOMPARE(thumbnailInfo["width"_ls], 800);
QCOMPARE(thumbnailInfo["height"_ls], 450);
}
void EventHandlerTest::linkPreviewer()
{
auto event = room->messageEvents().at(2).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getLinkPreviewer()->url(), QUrl("https://kde.org"_ls));
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getLinkPreviewer(), nullptr);
}
void EventHandlerTest::reactions()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReactions()->rowCount(), 1);
}
void EventHandlerTest::hasReply()
{
auto event = room->messageEvents().at(5).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.hasReply(), true);
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.hasReply(), false);
}
void EventHandlerTest::replyId()
{
auto event = room->messageEvents().at(5).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$153456789:example.org"));
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyId(), QStringLiteral(""));
}
void EventHandlerTest::replyDelegateType()
{
auto event = room->messageEvents().at(5).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Message);
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Other);
}
void EventHandlerTest::replyAuthor()
{
auto event = room->messageEvents().at(5).get();
auto replyEvent = room->messageEvents().at(0).get();
auto replyAuthor = room->user(replyEvent->senderId());
eventHandler.setEvent(event);
auto eventHandlerReplyAuthor = eventHandler.getReplyAuthor();
QCOMPARE(eventHandlerReplyAuthor["isLocalUser"_ls], replyAuthor->id() == room->localUser()->id());
QCOMPARE(eventHandlerReplyAuthor["id"_ls], replyAuthor->id());
QCOMPARE(eventHandlerReplyAuthor["displayName"_ls], replyAuthor->displayname(room));
QCOMPARE(eventHandlerReplyAuthor["avatarSource"_ls], room->avatarForMember(replyAuthor));
QCOMPARE(eventHandlerReplyAuthor["avatarMediaId"_ls], replyAuthor->avatarMediaId(room));
QCOMPARE(eventHandlerReplyAuthor["color"_ls], Utils::getUserColor(replyAuthor->hueF()));
QCOMPARE(eventHandlerReplyAuthor["object"_ls], QVariant::fromValue(replyAuthor));
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyAuthor(), room->getUser(nullptr));
}
void EventHandlerTest::replyBody()
{
auto event = room->messageEvents().at(5).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
QCOMPARE(eventHandler.getReplyRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
QCOMPARE(eventHandler.getReplyPlainBody(), QStringLiteral("This is an example\ntext message"));
QCOMPARE(eventHandler.getReplyPlainBody(true), QStringLiteral("This is an example text message"));
}
void EventHandlerTest::replyMediaInfo()
{
auto event = room->messageEvents().at(6).get();
auto replyEvent = room->messageEvents().at(4).get();
eventHandler.setEvent(event);
auto mediaInfo = eventHandler.getReplyMediaInfo();
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
QCOMPARE(mediaInfo["source"_ls], room->makeMediaUrl(replyEvent->id(), QUrl("mxc://kde.org/1234567"_ls)));
QCOMPARE(mediaInfo["mimeType"_ls], QStringLiteral("video/mp4"));
QCOMPARE(mediaInfo["mimeIcon"_ls], QStringLiteral("video-mp4"));
QCOMPARE(mediaInfo["size"_ls], 62650636);
QCOMPARE(mediaInfo["duration"_ls], 10);
QCOMPARE(mediaInfo["width"_ls], 1920);
QCOMPARE(mediaInfo["height"_ls], 1080);
QCOMPARE(thumbnailInfo["source"_ls], room->makeMediaUrl(replyEvent->id(), QUrl("mxc://kde.org/2234567"_ls)));
QCOMPARE(thumbnailInfo["mimeType"_ls], QStringLiteral("image/jpeg"));
QCOMPARE(thumbnailInfo["mimeIcon"_ls], QStringLiteral("image-jpeg"));
QCOMPARE(thumbnailInfo["size"_ls], 382249);
QCOMPARE(thumbnailInfo["width"_ls], 800);
QCOMPARE(thumbnailInfo["height"_ls], 450);
}
void EventHandlerTest::location()
{
auto event = room->messageEvents().at(7).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getLatitude(), QStringLiteral("51.7035").toFloat());
QCOMPARE(eventHandler.getLongitude(), QStringLiteral("-1.14394").toFloat());
QCOMPARE(eventHandler.getLocationAssetType(), QStringLiteral("m.pin"));
}
void EventHandlerTest::readMarkers()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.hasReadMarkers(), true);
auto readMarkers = eventHandler.getReadMarkers();
QCOMPARE(readMarkers.size(), 1);
QCOMPARE(readMarkers[0].toMap()["id"_ls], QStringLiteral("@alice:matrix.org"));
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QString());
QCOMPARE(eventHandler.getReadMarkersString(), QStringLiteral("1 user: @alice:matrix.org"));
event = room->messageEvents().at(2).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.hasReadMarkers(), true);
readMarkers = eventHandler.getReadMarkers();
QCOMPARE(readMarkers.size(), 5);
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QStringLiteral("+ 1"));
// There are no guarantees on the order of the users it will be different every time so don't match the whole string.
QCOMPARE(eventHandler.getReadMarkersString().startsWith(QStringLiteral("6 users:")), true);
}
QTEST_MAIN(EventHandlerTest)
#include "eventhandlertest.moc"

View File

@@ -1,72 +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 <qglobal.h>
#include <qtestcase.h>
#include <cmath>
#include "mediasizehelper.h"
#include "neochatconfig.h"
class MediaSizeHelperTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void uninitialized();
void limits_data();
void limits();
};
void MediaSizeHelperTest::uninitialized()
{
MediaSizeHelper mediasizehelper;
QCOMPARE(mediasizehelper.currentSize(), QSize(540, qRound(qreal(NeoChatConfig::self()->mediaMaxWidth()) / qreal(16.0) * qreal(9.0))));
}
void MediaSizeHelperTest::limits_data()
{
QTest::addColumn<qreal>("mediaWidth");
QTest::addColumn<qreal>("mediaHeight");
QTest::addColumn<qreal>("contentMaxWidth");
QTest::addColumn<qreal>("contentMaxHeight");
QTest::addColumn<QSize>("currentSize");
QTest::newRow("media smaller than content limits") << qreal(200) << qreal(150) << qreal(400) << qreal(900) << QSize(200, 150);
QTest::newRow("media smaller than max limits") << qreal(200) << qreal(150) << qreal(-1) << qreal(-1) << QSize(200, 150);
QTest::newRow("limit by max width") << qreal(600) << qreal(50) << qreal(-1) << qreal(-1) << QSize(540, qRound(qreal(540) / (qreal(600) / qreal(50))));
QTest::newRow("limit by max height") << qreal(50) << qreal(600) << qreal(-1) << qreal(-1) << QSize(qRound(qreal(540) * (qreal(50) / qreal(600))), 540);
QTest::newRow("limit by content width") << qreal(600) << qreal(50) << qreal(300) << qreal(-1) << QSize(300, qRound(qreal(300) / (qreal(600) / qreal(50))));
QTest::newRow("limit by content height") << qreal(50) << qreal(600) << qreal(-1) << qreal(300) << QSize(qRound(qreal(300) * (qreal(50) / qreal(600))), 300);
QTest::newRow("limit by content width tall media")
<< qreal(400) << qreal(600) << qreal(100) << qreal(400) << QSize(100, qRound(qreal(100) / (qreal(400) / qreal(600))));
QTest::newRow("limit by content height wide media")
<< qreal(1000) << qreal(600) << qreal(400) << qreal(100) << QSize(qRound(qreal(100) * (qreal(1000) / qreal(600))), 100);
}
void MediaSizeHelperTest::limits()
{
QFETCH(qreal, mediaWidth);
QFETCH(qreal, mediaHeight);
QFETCH(qreal, contentMaxWidth);
QFETCH(qreal, contentMaxHeight);
QFETCH(QSize, currentSize);
MediaSizeHelper mediasizehelper;
mediasizehelper.setMediaWidth(mediaWidth);
mediasizehelper.setMediaHeight(mediaHeight);
mediasizehelper.setContentMaxWidth(contentMaxWidth);
mediasizehelper.setContentMaxHeight(contentMaxHeight);
QCOMPARE(mediasizehelper.currentSize(), currentSize);
}
QTEST_GUILESS_MAIN(MediaSizeHelperTest)
#include "mediasizehelpertest.moc"

View File

@@ -363,7 +363,6 @@ void TextHandlerTest::receiveRichInPlainOut_data()
QTest::newRow("ampersand") << QStringLiteral("a &amp; b") << QStringLiteral("a & b");
QTest::newRow("quote") << QStringLiteral("&quot;a and b&quot;") << QStringLiteral("\"a and b\"");
QTest::newRow("new line") << QStringLiteral("new<br>line") << QStringLiteral("new\nline");
}
void TextHandlerTest::receiveRichInPlainOut()

View File

@@ -57,7 +57,6 @@
<summary xml:lang="gl">Charle coas súas amizades en Matrix.</summary>
<summary xml:lang="it">Conversa con i tuoi contatti su matrix</summary>
<summary xml:lang="ka">ესაუბრეთ მეგობრებს Matrix-ზე</summary>
<summary xml:lang="ko">Matrix를 사용하여 친구들과 대화하기</summary>
<summary xml:lang="nl">Met uw vrienden chatten op matrix</summary>
<summary xml:lang="nn">Prat med vennar på Matrix</summary>
<summary xml:lang="sl">Klepet z vašimi prijatelji na matrixu</summary>
@@ -159,7 +158,6 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="ko">투표 - MSC3381</li>
<li xml:lang="nl">Polls - MSC3381</li>
<li xml:lang="nn">Avstemmingar  MSC3381</li>
<li xml:lang="pl">Ankiety - MSC3381</li>
<li xml:lang="pt">Inquéritos - MSC3381</li>
<li xml:lang="sl">Polls - MSC3381</li>
<li xml:lang="sv">Polls - MSC3381</li>
@@ -184,7 +182,6 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="ko">스티커 팩 - MSC2545</li>
<li xml:lang="nl">Sticker Packs - MSC2545</li>
<li xml:lang="nn">Klistremerke-pakkar  MSC2545</li>
<li xml:lang="pl">Paczki naklejek - MSC2545</li>
<li xml:lang="pt">Pacotes de Autocolantes - MSC2545</li>
<li xml:lang="sl">Sticker Packs - MSC2545</li>
<li xml:lang="sv">Sticker Packs - MSC2545</li>
@@ -209,7 +206,6 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="ko">위치 이벤트 - MSC3488</li>
<li xml:lang="nl">Locatie gebeurtenissen - MSC3488</li>
<li xml:lang="nn">Posisjonshendingar  MSC3488</li>
<li xml:lang="pl">Wydarzenia w miejscach - MSC3488</li>
<li xml:lang="pt">Eventos com Localizações - MSC3488</li>
<li xml:lang="sl">Location Events - MSC3488</li>
<li xml:lang="sv">Location Events - MSC3488</li>
@@ -271,6 +267,52 @@ to provide a convergent experience across multiple platforms.</p>
<value key="KDE::windows_store::StoreLogoSquare">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/storelogo-1080x1080.png</value>
<value key="KDE::windows_store::Icon">https://invent.kde.org/network/neochat/-/raw/master/icons/300-apps-neochat.png</value>
<value key="KDE::windows_store::PromotionalArt16x9">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/promoimage-1920x1080.png</value>
<value key="KDE::windows_store::screenshots::1::image">https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Timeline.png</value>
<value key="KDE::windows_store::screenshots::1::caption">Main view with room list, chat, and room information</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ar">العرض الرئيسة مع قائمة الغرف والدردشات و معلومات الغرفة</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ca">Vista principal amb la llista de sales, xats i informació de les sales</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ca-valencia">Vista principal amb la llista de sales, xats i informació de les sales</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="eo">Ĉefa vido kun ĉambra listo, babilejo kaj ĉambra informo</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="es">Vista principal con la lista de salas, chat e información de la sala</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="eu">Ikuspegi nagusia gela-zerrenda, berriketa, eta gelako informazioarekin</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ka">მთავარი ხედი სურათების სიით, ჩატით და ოთახის ინფორმაციით</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ko">대화방 목록, 채팅, 대화방 정보가 표시된 주 보기</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="nl">Hoofdweergave met lijst met rooms, chat en roominformatie</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="nn">Hovudvising med romliste, pratevindauge og rominformasjon</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="sv">Huvudvy med rumslista, chatt, och rumsinformation</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ta">அரங்குப்பட்டியல், உரையாடல், மற்றும் அரங்குவிவரங்களைக் கொண்டுள்ள பிரதான காட்சி</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="tr">Oda listesini, sohbet penceresini ve oda bilgisini gösteren ana görünüm</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="uk">Головна панель із списком кімнат, спілкуванням та даними щодо кімнати</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="x-test">xxMain view with room list, chat, and room informationxx</value>
<value key="KDE::windows_store::screenshots::2::image">https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Login.png</value>
<value key="KDE::windows_store::screenshots::2::caption">Login screen</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ar">شاشة الدخول</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ca">Pantalla d'inici de sessió</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ca-valencia">Pantalla d'inici de sessió</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="eo">Ensaluta ekrano</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="es">Pantalla de inicio de sesión</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="eu">Saio-hasteko pantaila</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="fi">Kirjautumisnäkymä</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="fr">Écran de connexion</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="gl">Pantalla de identificación.</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="it">Schermata di accesso</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ka">შესვლის ეკრანი</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ko">로그인 화면</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="nl">Aanmeldscherm</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="nn">Innloggingsbilete</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="pt">Ecrã de autenticação</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="sl">Prijavni zaslon</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="sv">Inloggningsfönster</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ta">நுழைவுத் திரை</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="tr">Oturum açma ekranı</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="uk">Вікно входу</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="x-test">xxLogin screenxx</value>
</custom>
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
<screenshots>
@@ -280,79 +322,16 @@ to provide a convergent experience across multiple platforms.</p>
<screenshot type="default">
<image>https://cdn.kde.org/screenshots/neochat/application.png</image>
</screenshot>
<screenshot x-kde-os="windows">
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Timeline.png</image>
<caption>Main view with room list, chat, and room information</caption>
<caption xml:lang="ar">العرض الرئيسة مع قائمة الغرف والدردشات و معلومات الغرفة</caption>
<caption xml:lang="ca">Vista principal amb la llista de sales, xats i informació de les sales</caption>
<caption xml:lang="ca-valencia">Vista principal amb la llista de sales, xats i informació de les sales</caption>
<caption xml:lang="eo">Ĉefa vido kun ĉambra listo, babilejo kaj ĉambra informo</caption>
<caption xml:lang="es">Vista principal con la lista de salas, chat e información de la sala</caption>
<caption xml:lang="eu">Ikuspegi nagusia gela-zerrenda, berriketa, eta gelako informazioarekin</caption>
<caption xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</caption>
<caption xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</caption>
<caption xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</caption>
<caption xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption>
<caption xml:lang="ka">მთავარი ხედი სურათების სიით, ჩატით და ოთახის ინფორმაციით</caption>
<caption xml:lang="ko">대화방 목록, 채팅, 대화방 정보가 표시된 주 보기</caption>
<caption xml:lang="nl">Hoofdweergave met lijst met rooms, chat en roominformatie</caption>
<caption xml:lang="nn">Hovudvising med romliste, pratevindauge og rominformasjon</caption>
<caption xml:lang="pl">Główny widok z wykazem pokojów, rozmowami i szczegółami pokojów</caption>
<caption xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</caption>
<caption xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</caption>
<caption xml:lang="sv">Huvudvy med rumslista, chatt, och rumsinformation</caption>
<caption xml:lang="ta">அரங்குப்பட்டியல், உரையாடல், மற்றும் அரங்குவிவரங்களைக் கொண்டுள்ள பிரதான காட்சி</caption>
<caption xml:lang="tr">Oda listesini, sohbet penceresini ve oda bilgisini gösteren ana görünüm</caption>
<caption xml:lang="uk">Головна панель із списком кімнат, спілкуванням та даними щодо кімнати</caption>
<caption xml:lang="x-test">xxMain view with room list, chat, and room informationxx</caption>
</screenshot>
<screenshot x-kde-os="windows">
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Login.png</image>
<caption>Login screen</caption>
<caption xml:lang="ar">شاشة الدخول</caption>
<caption xml:lang="ca">Pantalla d'inici de sessió</caption>
<caption xml:lang="ca-valencia">Pantalla d'inici de sessió</caption>
<caption xml:lang="eo">Ensaluta ekrano</caption>
<caption xml:lang="es">Pantalla de inicio de sesión</caption>
<caption xml:lang="eu">Saio-hasteko pantaila</caption>
<caption xml:lang="fi">Kirjautumisnäkymä</caption>
<caption xml:lang="fr">Écran de connexion</caption>
<caption xml:lang="gl">Pantalla de identificación.</caption>
<caption xml:lang="it">Schermata di accesso</caption>
<caption xml:lang="ka">შესვლის ეკრანი</caption>
<caption xml:lang="ko">로그인 화면</caption>
<caption xml:lang="nl">Aanmeldscherm</caption>
<caption xml:lang="nn">Innloggingsbilete</caption>
<caption xml:lang="pl">Ekran logowania</caption>
<caption xml:lang="pt">Ecrã de autenticação</caption>
<caption xml:lang="sl">Prijavni zaslon</caption>
<caption xml:lang="sv">Inloggningsfönster</caption>
<caption xml:lang="ta">நுழைவுத் திரை</caption>
<caption xml:lang="tr">Oturum açma ekranı</caption>
<caption xml:lang="uk">Вікно входу</caption>
<caption xml:lang="x-test">xxLogin screenxx</caption>
</screenshot>
</screenshots>
<content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="23.08.0" date="2023-08-24">
<url>https://kde.org/announcements/gear/23.08.0/#neochathttpsappskdeorgneochat</url>
<description>
<p>Apart from a visual overhaul, NeoChat can now display location events and also a map with the location of all the users currently broadcasting their location using Itineray's Matrix integration. Great for locating where your friends are.</p>
</description>
</release>
<release version="23.08.0" date="2023-08-24"/>
<release version="23.04.3" date="2023-07-06"/>
<release version="23.04.2" date="2023-06-08"/>
<release version="23.04.1" date="2023-05-11"/>
<release version="23.04.0" date="2023-04-20">
<url>https://kde.org/announcements/gear/23.04.0/#neochathttpsappskdeorgneochat</url>
<description>
<p>NeoChat improves its design with tweaks that provide a more compact layout and a simpler menu which works better for the collapsed room list.</p>
<p>We have also improved the video controls, added a new command /knock &lt;room-id&gt; to send a knock event to a room, and you can now edit a prior message inline, within the chat pane.</p>
<p>Other usability improvements include an overhaul of the keyboard navigation and shortcuts like Ctrl+PgUp/PgDn that allow you to skip from room to room.</p>
</description>
<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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,15 @@
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
configure_file(qml/Page/RoomList/RoomDelegate.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Page/RoomList/RoomDelegate.qml)
configure_file(qml/Component/QuickSwitcher.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/QuickSwitcher.qml)
configure_file(qml/Dialog/PowerLevelDialog.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Dialog/PowerLevelDialog.qml)
configure_file(qml/Component/Timeline/AudioDelegate.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/Timeline/AudioDelegate.qml)
configure_file(qml/Component/Timeline/VideoDelegate.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/Timeline/VideoDelegate.qml)
configure_file(qml/Component/Timeline/OsmLocationPlugin.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/Timeline/OsmLocationPlugin.qml)
configure_file(res.qrc ${CMAKE_CURRENT_SOURCE_DIR}/res.generated.qrc)
add_library(neochat STATIC
controller.cpp
controller.h
@@ -65,6 +74,8 @@ add_library(neochat STATIC
blurhash.h
blurhashimageprovider.cpp
blurhashimageprovider.h
models/collapsestateproxymodel.cpp
models/collapsestateproxymodel.h
models/mediamessagefiltermodel.cpp
models/mediamessagefiltermodel.h
urlhelper.cpp
@@ -114,19 +125,6 @@ add_library(neochat STATIC
events/pollevent.cpp
pollhandler.cpp
utils.h
registration.cpp
neochatconnection.cpp
neochatconnection.h
jobs/neochatdeactivateaccountjob.cpp
jobs/neochatdeactivateaccountjob.h
jobs/neochatdeletedevicejob.cpp
jobs/neochatdeletedevicejob.h
jobs/neochatchangepasswordjob.cpp
jobs/neochatchangepasswordjob.h
mediasizehelper.cpp
mediasizehelper.h
eventhandler.cpp
enums/delegatetype.h
)
ecm_qt_declare_logging_category(neochat
@@ -138,23 +136,11 @@ ecm_qt_declare_logging_category(neochat
EXPORT NEOCHAT
)
ecm_qt_declare_logging_category(neochat
HEADER "eventhandler_logging.h"
IDENTIFIER "EventHandling"
CATEGORY_NAME "org.kde.neochat.eventhandler"
DEFAULT_SEVERITY Info
)
add_executable(neochat-app
main.cpp
res.qrc
${CMAKE_CURRENT_SOURCE_DIR}/res.generated.qrc
)
if(TARGET Qt::WebView)
target_link_libraries(neochat-app PUBLIC Qt::WebView)
target_compile_definitions(neochat-app PUBLIC -DHAVE_WEBVIEW)
endif()
target_include_directories(neochat-app PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat-app PRIVATE
@@ -169,11 +155,13 @@ if(NOT ANDROID)
target_sources(neochat PRIVATE colorschemer.cpp colorschemer.h)
if (NOT WIN32 AND NOT APPLE)
target_sources(neochat PRIVATE trayicon_sni.cpp trayicon_sni.h)
target_link_libraries(neochat PRIVATE KF6::StatusNotifierItem)
if(QT_MAJOR_VERSION STREQUAL "6")
target_link_libraries(neochat PRIVATE KF6::StatusNotifierItem)
endif()
else()
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
endif()
target_link_libraries(neochat PUBLIC KF6::ConfigWidgets KF6::WindowSystem)
target_link_libraries(neochat PUBLIC KF${QT_MAJOR_VERSION}::ConfigWidgets KF${QT_MAJOR_VERSION}::WindowSystem)
target_compile_definitions(neochat PUBLIC -DHAVE_COLORSCHEME)
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
endif()
@@ -188,8 +176,7 @@ else()
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 KF6::I18n KF6::Kirigami2 KF6::Notifications KF6::ConfigCore KF6::ConfigGui KF6::CoreAddons KF6::SonnetCore KF6::ItemModels QuotientQt6 cmark::cmark QCoro::Core)
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 QCoro::Core)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
if(NEOCHAT_FLATPAK)
@@ -276,7 +263,7 @@ if(ANDROID)
"gps"
)
else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets)
target_link_libraries(neochat PUBLIC Qt::Widgets KF${QT_MAJOR_VERSION}::KIOWidgets)
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
endif()
@@ -284,12 +271,12 @@ if(NOT ANDROID)
set_target_properties(neochat-app PROPERTIES OUTPUT_NAME "neochat")
endif()
if(TARGET KF6::DBusAddons)
target_link_libraries(neochat PUBLIC KF6::DBusAddons)
if(TARGET KF${QT_MAJOR_VERSION}::DBusAddons)
target_link_libraries(neochat PUBLIC KF${QT_MAJOR_VERSION}::DBusAddons)
target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS)
endif()
if (TARGET KF6::KIOWidgets)
if (TARGET KF${QT_MAJOR_VERSION}::KIOWidgets)
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
endif()

View File

@@ -9,6 +9,7 @@
#include "neochatroom.h"
class CustomEmojiModel;
class NeoChatRoom;
/**

View File

@@ -90,11 +90,6 @@ public:
}),
mentions->end());
}
QStringList suggestions(const QString &word) const
{
return checker->suggest(word);
}
};
ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
@@ -139,7 +134,7 @@ int ChatDocumentHandler::completionStartIndex() const
return 0;
}
#if !defined(Q_OS_ANDROID)
#if !defined(Q_OS_ANDROID) && QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
const long long cursor = cursorPosition();
#else
const auto cursor = cursorPosition();
@@ -268,47 +263,6 @@ void ChatDocumentHandler::complete(int index)
}
}
QStringList ChatDocumentHandler::getSuggestions(int mousePosition)
{
QTextCursor cursorAtMouse(document()->textDocument());
cursorAtMouse.setPosition(mousePosition);
// Get the word under the (mouse-)cursor and see if it is misspelled.
// Don't include apostrophes at the start/end of the word in the selection.
QTextCursor wordSelectCursor(cursorAtMouse);
wordSelectCursor.clearSelection();
wordSelectCursor.select(QTextCursor::WordUnderCursor);
m_selectedWord = wordSelectCursor.selectedText();
return m_highlighter->suggestions(m_selectedWord);
}
bool ChatDocumentHandler::getActive() const
{
return m_highlighter->settings.checkerEnabledByDefault();
}
bool ChatDocumentHandler::getIsWordIsMisspelled() const
{
return !m_highlighter->errors.isEmpty();
}
QString ChatDocumentHandler::getWordUnderMouse() const
{
return m_selectedWord;
}
void ChatDocumentHandler::replaceWord(const QString &word)
{
QTextCursor cursor(document()->textDocument());
const auto &text = m_room->chatBoxText();
auto at = text.indexOf(m_highlighter->previousText);
cursor.setPosition(at);
cursor.setPosition(at + m_highlighter->previousText.length(), QTextCursor::KeepAnchor);
cursor.insertText(word);
}
CompletionModel *ChatDocumentHandler::completionModel() const
{
return m_completionModel;

View File

@@ -10,6 +10,7 @@
#include "models/completionmodel.h"
#include "neochatroom.h"
class QTextDocument;
class NeoChatRoom;
class SyntaxHighlighter;
@@ -87,10 +88,6 @@ class ChatDocumentHandler : public QObject
*/
Q_PROPERTY(int selectionEnd READ selectionEnd WRITE setSelectionEnd NOTIFY selectionEndChanged)
Q_PROPERTY(bool active READ getActive NOTIFY cursorPositionChanged)
Q_PROPERTY(bool wordIsMisspelled READ getIsWordIsMisspelled NOTIFY cursorPositionChanged)
Q_PROPERTY(QString wordUnderMouse READ getWordUnderMouse NOTIFY cursorPositionChanged)
/**
* @brief The current CompletionModel.
*
@@ -137,12 +134,6 @@ public:
Q_INVOKABLE void complete(int index);
Q_INVOKABLE void replaceWord(const QString &word);
Q_INVOKABLE QStringList getSuggestions(int mousePosition);
bool getActive() const;
bool getIsWordIsMisspelled() const;
QString getWordUnderMouse() const;
void updateCompletions();
CompletionModel *completionModel() const;
@@ -188,6 +179,4 @@ private:
CompletionModel::AutoCompletionType m_completionType = CompletionModel::None;
CompletionModel *m_completionModel = nullptr;
QString m_selectedWord;
};

View File

@@ -4,7 +4,11 @@
#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>
@@ -17,6 +21,7 @@
#include <QFile>
#include <QFileInfo>
#include <QGuiApplication>
#include <QImageReader>
#include <QNetworkProxy>
#include <QQuickTextDocument>
#include <QQuickWindow>
@@ -28,8 +33,10 @@
#include <Quotient/accountregistry.h>
#include <Quotient/connection.h>
#include <Quotient/csapi/content-repo.h>
#include <Quotient/csapi/logout.h>
#include <Quotient/csapi/notifications.h>
#include <Quotient/csapi/profile.h>
#include <Quotient/eventstats.h>
#include <Quotient/jobs/downloadfilejob.h>
#include <Quotient/qt_connection_util.h>
@@ -93,11 +100,13 @@ Controller::Controller(QObject *parent)
}
#endif
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, &Controller::activeConnectionIndexChanged);
static int oldAccountCount = 0;
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
if (m_accountRegistry.size() > oldAccountCount) {
auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]);
connect(connection, &NeoChatConnection::syncDone, this, [connection]() {
auto connection = m_accountRegistry.accounts()[m_accountRegistry.size() - 1];
connect(connection, &Connection::syncDone, this, [connection]() {
NotificationsManager::instance().handleNotifications(connection);
});
}
@@ -132,7 +141,38 @@ void Controller::toggleWindow()
}
}
void Controller::addConnection(NeoChatConnection *c)
void Controller::logout(Connection *conn, bool serverSideLogout)
{
if (!conn) {
qCritical() << "Attempt to logout null connection";
return;
}
SettingsGroup("Accounts"_ls).remove(conn->userId());
QKeychain::DeletePasswordJob job(qAppName());
job.setAutoDelete(true);
job.setKey(conn->userId());
QEventLoop loop;
QKeychain::DeletePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
if (m_accountRegistry.count() > 1) {
// Only set the connection if the the account being logged out is currently active
if (conn == activeConnection()) {
setActiveConnection(m_accountRegistry.accounts()[0]);
}
} else {
setActiveConnection(nullptr);
}
if (!serverSideLogout) {
return;
}
conn->logout();
}
void Controller::addConnection(Connection *c)
{
Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection");
@@ -140,17 +180,17 @@ void Controller::addConnection(NeoChatConnection *c)
c->setLazyLoading(true);
connect(c, &NeoChatConnection::syncDone, this, [this, c] {
connect(c, &Connection::syncDone, this, [this, c] {
Q_EMIT syncDone();
c->sync(30000);
c->saveState();
});
connect(c, &NeoChatConnection::loggedOut, this, [this, c] {
connect(c, &Connection::loggedOut, this, [this, c] {
dropConnection(c);
});
connect(c, &NeoChatConnection::requestFailed, this, [this](BaseJob *job) {
connect(c, &Connection::requestFailed, this, [this](BaseJob *job) {
if (job->error() == BaseJob::UserConsentRequired) {
Q_EMIT userConsentRequired(job->errorUrl());
}
@@ -159,14 +199,15 @@ void Controller::addConnection(NeoChatConnection *c)
c->sync();
Q_EMIT connectionAdded(c);
Q_EMIT accountCountChanged();
}
void Controller::dropConnection(NeoChatConnection *c)
void Controller::dropConnection(Connection *c)
{
Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection");
m_accountRegistry.drop(c);
Q_EMIT connectionDropped(c);
Q_EMIT accountCountChanged();
}
void Controller::invokeLogin()
@@ -190,19 +231,19 @@ void Controller::invokeLogin()
return;
}
auto connection = new NeoChatConnection(account.homeserver());
connect(connection, &NeoChatConnection::connected, this, [this, connection, id] {
auto connection = new Connection(account.homeserver());
connect(connection, &Connection::connected, this, [this, connection, id] {
connection->loadState();
addConnection(connection);
if (connection->userId() == id) {
setActiveConnection(connection);
connectSingleShot(connection, &NeoChatConnection::syncDone, this, &Controller::initiated);
connectSingleShot(connection, &Connection::syncDone, this, &Controller::initiated);
}
});
connect(connection, &NeoChatConnection::loginError, this, [this, connection](const QString &error, const QString &) {
connect(connection, &Connection::loginError, this, [this, connection](const QString &error, const QString &) {
if (error == "Unrecognised access token"_ls) {
Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked"));
connection->logout(false);
logout(connection, false);
} else if (error == "Connection closed"_ls) {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
// Failed due to network connection issue. This might happen when the homeserver is
@@ -210,11 +251,11 @@ void Controller::invokeLogin()
// connect to the homeserver. In this case, we don't want to do logout().
} else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
connection->logout(true);
logout(connection, true);
}
Q_EMIT initiated();
});
connect(connection, &NeoChatConnection::networkError, this, [this](const QString &error, const QString &, int, int) {
connect(connection, &Connection::networkError, this, [this](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured(i18n("Network Error: %1", error));
});
connection->assumeIdentity(account.userId(), accessToken);
@@ -280,6 +321,22 @@ bool Controller::saveAccessTokenToKeyChain(const AccountSettings &account, const
return true;
}
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());
});
}
void Controller::markAllMessagesAsRead(Connection *conn)
{
const auto rooms = conn->allRooms();
for (auto room : rooms) {
room->markAllMessagesAsRead();
}
}
bool Controller::supportSystemTray() const
{
#ifdef Q_OS_ANDROID
@@ -290,6 +347,64 @@ bool Controller::supportSystemTray() const
#endif
}
void Controller::changePassword(Connection *connection, const QString &currentPassword, const QString &newPassword)
{
NeochatChangePasswordJob *job = connection->callApi<NeochatChangePasswordJob>(newPassword, false);
connect(job, &BaseJob::result, this, [this, job, currentPassword, newPassword, connection] {
if (job->error() == 103) {
QJsonObject replyData = job->jsonData();
QJsonObject authData;
authData["session"_ls] = replyData["session"_ls];
authData["password"_ls] = currentPassword;
authData["type"_ls] = "m.login.password"_ls;
authData["user"_ls] = connection->user()->id();
QJsonObject identifier = {{"type"_ls, "m.id.user"_ls}, {"user"_ls, connection->user()->id()}};
authData["identifier"_ls] = identifier;
NeochatChangePasswordJob *innerJob = connection->callApi<NeochatChangePasswordJob>(newPassword, false, authData);
connect(innerJob, &BaseJob::success, this, [this]() {
Q_EMIT passwordStatus(PasswordStatus::Success);
});
connect(innerJob, &BaseJob::failure, this, [innerJob, this]() {
if (innerJob->jsonData()["errcode"_ls] == "M_FORBIDDEN"_ls) {
Q_EMIT passwordStatus(PasswordStatus::Wrong);
} else {
Q_EMIT passwordStatus(PasswordStatus::Other);
}
});
}
});
}
bool Controller::setAvatar(Connection *connection, const QUrl &avatarSource)
{
User *localUser = connection->user();
QString decoded = avatarSource.path();
if (decoded.isEmpty()) {
connection->callApi<SetAvatarUrlJob>(localUser->id(), avatarSource);
return true;
}
if (QImageReader(decoded).read().isNull()) {
return false;
} else {
return localUser->setAvatar(decoded);
}
}
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("new_password"), newPassword);
addParam<IfNotEmpty>(_data, QStringLiteral("logout_devices"), logoutDevices);
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(_data);
}
int Controller::accountCount() const
{
return m_accountRegistry.count();
}
void Controller::setQuitOnLastWindowClosed()
{
#ifndef Q_OS_ANDROID
@@ -310,7 +425,7 @@ void Controller::setQuitOnLastWindowClosed()
#endif
}
NeoChatConnection *Controller::activeConnection() const
Connection *Controller::activeConnection() const
{
if (m_connection.isNull()) {
return nullptr;
@@ -318,43 +433,49 @@ NeoChatConnection *Controller::activeConnection() const
return m_connection;
}
void Controller::setActiveConnection(NeoChatConnection *connection)
void Controller::setActiveConnection(Connection *connection)
{
if (connection == m_connection) {
return;
}
if (m_connection != nullptr) {
disconnect(m_connection, &NeoChatConnection::syncError, this, nullptr);
disconnect(m_connection, &NeoChatConnection::accountDataChanged, this, nullptr);
disconnect(m_connection, &Connection::syncError, this, nullptr);
disconnect(m_connection, &Connection::accountDataChanged, this, nullptr);
}
m_connection = connection;
if (connection != nullptr) {
NeoChatConfig::self()->setActiveConnection(connection->userId());
connect(connection, &NeoChatConnection::networkError, this, [this]() {
connect(connection, &Connection::networkError, this, [this]() {
if (!m_isOnline) {
return;
}
m_isOnline = false;
Q_EMIT isOnlineChanged(false);
});
connect(connection, &NeoChatConnection::syncDone, this, [this] {
connect(connection, &Connection::syncDone, this, [this] {
if (m_isOnline) {
return;
}
m_isOnline = true;
Q_EMIT isOnlineChanged(true);
});
connect(connection, &NeoChatConnection::requestFailed, this, [](BaseJob *job) {
connect(connection, &Connection::requestFailed, this, [](BaseJob *job) {
if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"_ls].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();
}
PushRuleModel *Controller::pushRuleModel() const
@@ -367,6 +488,44 @@ void Controller::saveWindowGeometry()
WindowController::instance().saveGeometry();
}
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
: Quotient::BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(std::move(_data));
}
void Controller::createRoom(const QString &name, const QString &topic)
{
auto createRoomJob = m_connection->createRoom(Connection::PublishRoom, QString(), name, topic, QStringList());
connect(createRoomJob, &CreateRoomJob::failure, this, [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
{
return m_isOnline;
@@ -487,6 +646,41 @@ bool Controller::isFlatpak() const
#endif
}
void Controller::setActiveAccountLabel(const QString &label)
{
if (!m_connection) {
return;
}
QJsonObject json{
{"account_label"_ls, label},
};
m_connection->setAccountData("org.kde.neochat.account_label"_ls, json);
}
QString Controller::activeAccountLabel() const
{
if (!m_connection) {
return {};
}
return m_connection->accountDataJson("org.kde.neochat.account_label"_ls)["account_label"_ls].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"_ls, v.id);
roomVersionMap.insert("status"_ls, v.status);
roomVersionMap.insert("isStable"_ls, v.isStable());
supportedRoomVersions.append(roomVersionMap);
}
return supportedRoomVersions;
}
AccountRegistry &Controller::accounts()
{
return m_accountRegistry;

View File

@@ -9,17 +9,18 @@
#include <KFormat>
#include "neochatconnection.h"
#include <Quotient/accountregistry.h>
#include <Quotient/jobs/basejob.h>
#include <Quotient/settings.h>
class NeoChatRoom;
class TrayIcon;
class QWindow;
class QQuickTextDocument;
namespace Quotient
{
class Connection;
class Room;
class User;
}
@@ -41,10 +42,15 @@ 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(NeoChatConnection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
Q_PROPERTY(Quotient::Connection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
/**
* @brief The PushRuleModel that has the active connection's push rules.
@@ -56,6 +62,16 @@ class Controller : public QObject
*/
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.
*/
@@ -91,28 +107,58 @@ public:
static Controller &instance();
void setActiveConnection(NeoChatConnection *connection);
[[nodiscard]] NeoChatConnection *activeConnection() const;
[[nodiscard]] int accountCount() const;
void setActiveConnection(Quotient::Connection *connection);
[[nodiscard]] Quotient::Connection *activeConnection() const;
[[nodiscard]] PushRuleModel *pushRuleModel() const;
/**
* @brief Add a new connection to the account registry.
*/
void addConnection(NeoChatConnection *c);
void addConnection(Quotient::Connection *c);
/**
* @brief Drop a connection from the account registry.
*/
void dropConnection(NeoChatConnection *c);
void dropConnection(Quotient::Connection *c);
int activeConnectionIndex() const;
[[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);
/**
* @brief Create new space.
*/
Q_INVOKABLE void createSpace(const QString &name, const QString &topic);
/**
* @brief Join a room.
*/
@@ -164,12 +210,14 @@ public:
*/
Q_INVOKABLE void forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item);
Q_INVOKABLE QVariantList getSupportedRoomVersions(Quotient::Connection *connection);
Quotient::AccountRegistry &accounts();
private:
explicit Controller(QObject *parent = nullptr);
QPointer<NeoChatConnection> m_connection;
QPointer<Quotient::Connection> m_connection;
TrayIcon *m_trayIcon = nullptr;
QKeychain::ReadPasswordJob *loadAccessTokenFromKeyChain(const Quotient::AccountSettings &account);
@@ -196,17 +244,42 @@ Q_SIGNALS:
/// Error occurred because of server or bug in NeoChat
void globalErrorOccured(QString error, QString detail);
void syncDone();
void connectionAdded(NeoChatConnection *connection);
void connectionDropped(NeoChatConnection *connection);
void connectionAdded(Quotient::Connection *_t1);
void connectionDropped(Quotient::Connection *_t1);
void accountCountChanged();
void initiated();
void notificationClicked(const QString &_t1, const QString &_t2);
void quitOnLastWindowClosedChanged();
void unreadCountChanged();
void activeConnectionChanged();
void passwordStatus(Controller::PasswordStatus status);
void passwordStatus(Controller::PasswordStatus _t1);
void userConsentRequired(QUrl url);
void testConnectionResult(const QString &connection, bool usable);
void isOnlineChanged(bool isOnline);
void keyVerificationRequest(int timeLeft, Quotient::Connection *connection, const QString &transactionId, const QString &deviceId);
void keyVerificationStart();
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);
void changeAvatar(Quotient::Connection *conn, const QUrl &localFile);
static void markAllMessagesAsRead(Quotient::Connection *conn);
void saveWindowGeometry();
};
// TODO libQuotient 0.7: Drop
class NeochatChangePasswordJob : public Quotient::BaseJob
{
public:
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};
class NeochatDeleteDeviceJob : public Quotient::BaseJob
{
public:
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};

View File

@@ -124,7 +124,7 @@ int DelegateSizeHelper::calculateCurrentPercentageWidth() const
int maxPercentWidth = endPercentBigger ? m_endPercentWidth : m_startPercentWidth;
int minPercentWidth = endPercentBigger ? m_startPercentWidth : m_endPercentWidth;
int calcPercentWidth = std::round(m * m_parentWidth + c);
int calcPercentWidth = std::ceil(m * m_parentWidth + c);
return std::clamp(calcPercentWidth, minPercentWidth, maxPercentWidth);
}
@@ -146,9 +146,9 @@ qreal DelegateSizeHelper::currentWidth() const
qreal absoluteWidth = m_parentWidth * percentWidth * 0.01;
if (m_maxWidth < 0.0) {
return std::round(absoluteWidth);
return std::ceil(absoluteWidth);
} else {
return std::round(std::min(absoluteWidth, m_maxWidth));
return std::ceil(std::min(absoluteWidth, m_maxWidth));
}
}

View File

@@ -1,43 +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 DelegateType
*
* This class is designed to define the DelegateType enumeration.
*/
class DelegateType
{
Q_GADGET
public:
/**
* @brief The type of delegate that is needed for the event.
*
* @note While similar this is not the matrix event or message type. This is
* to tell a QML ListView what delegate to show for each event. So while
* similar to the spec it is not the same.
*/
enum Type {
Emote, /**< A message that begins with /me. */
Notice, /**< A notice event. */
Image, /**< A message that is an image. */
Audio, /**< A message that is an audio recording. */
Video, /**< A message that is a video. */
File, /**< A message that is a file. */
Message, /**< A text message. */
Sticker, /**< A message that is a sticker. */
State, /**< A state event in the room. */
Encrypted, /**< An encrypted message that cannot be decrypted. */
ReadMarker, /**< The local user read marker. */
Poll, /**< The initial event for a poll. */
Location, /**< A location event. */
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
Other, /**< Anything that cannot be classified as another type. */
};
Q_ENUM(Type);
};

View File

@@ -1,988 +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 "eventhandler.h"
#include <QMovie>
#include <KLocalizedString>
#include <Quotient/eventitem.h>
#include <Quotient/events/encryptionevent.h>
#include <Quotient/events/reactionevent.h>
#include <Quotient/events/redactionevent.h>
#include <Quotient/events/roomavatarevent.h>
#include <Quotient/events/roomcanonicalaliasevent.h>
#include <Quotient/events/roommemberevent.h>
#include <Quotient/events/roompowerlevelsevent.h>
#include <Quotient/events/simplestateevents.h>
#include <Quotient/events/stickerevent.h>
#include <Quotient/quotient_common.h>
#include "eventhandler_logging.h"
#include "events/pollevent.h"
#include "linkpreviewer.h"
#include "models/reactionmodel.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "texthandler.h"
#include "utils.h"
using namespace Quotient;
const NeoChatRoom *EventHandler::getRoom() const
{
return m_room;
}
void EventHandler::setRoom(const NeoChatRoom *room)
{
if (room == m_room) {
return;
}
m_room = room;
}
const Quotient::Event *EventHandler::getEvent() const
{
return m_event;
}
void EventHandler::setEvent(const Quotient::RoomEvent *event)
{
if (event == m_event) {
return;
}
m_event = event;
}
QString EventHandler::getId() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getId called with m_event set to nullptr.";
return {};
}
return !m_event->id().isEmpty() ? m_event->id() : m_event->transactionId();
}
DelegateType::Type EventHandler::getDelegateTypeForEvent(const Quotient::RoomEvent *event) const
{
if (auto e = eventCast<const RoomMessageEvent>(event)) {
switch (e->msgtype()) {
case MessageEventType::Emote:
return DelegateType::Emote;
case MessageEventType::Notice:
return DelegateType::Notice;
case MessageEventType::Image:
return DelegateType::Image;
case MessageEventType::Audio:
return DelegateType::Audio;
case MessageEventType::Video:
return DelegateType::Video;
case MessageEventType::Location:
return DelegateType::Location;
default:
break;
}
if (e->hasFileContent()) {
return DelegateType::File;
}
return DelegateType::Message;
}
if (is<const StickerEvent>(*event)) {
return DelegateType::Sticker;
}
if (event->isStateEvent()) {
if (event->matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
return DelegateType::LiveLocation;
}
return DelegateType::State;
}
if (is<const EncryptedEvent>(*event)) {
return DelegateType::Encrypted;
}
if (is<PollStartEvent>(*event)) {
const auto pollEvent = eventCast<const PollStartEvent>(event);
if (pollEvent->isRedacted()) {
return DelegateType::Message;
}
return DelegateType::Poll;
}
return DelegateType::Other;
}
DelegateType::Type EventHandler::getDelegateType() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getDelegateType called with m_event set to nullptr.";
return DelegateType::Other;
}
return getDelegateTypeForEvent(m_event);
}
QVariantMap EventHandler::getAuthor(bool isPending) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getAuthor called with m_room set to nullptr.";
return {};
}
// If we have a room we can return an empty user by handing nullptr to m_room->getUser.
if (m_event == nullptr) {
qCWarning(EventHandling) << "getAuthor called with m_event set to nullptr. Returning empty user.";
return m_room->getUser(nullptr);
}
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
return m_room->getUser(author);
}
QString EventHandler::getAuthorDisplayName(bool isPending) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getAuthorDisplayName called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getAuthorDisplayName called with m_event set to nullptr.";
return {};
}
if (is<RoomMemberEvent>(*m_event) && !m_event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].isNull()
&& m_event->stateKey() == m_event->senderId()) {
auto previousDisplayName = m_event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].toString().toHtmlEscaped();
if (previousDisplayName.isEmpty()) {
previousDisplayName = m_event->senderId();
}
return previousDisplayName;
} else {
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
return m_room->htmlSafeMemberName(author->id());
}
}
QDateTime EventHandler::getTime(bool isPending, QDateTime lastUpdated) const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getTime called with m_event set to nullptr.";
return {};
}
if (isPending && lastUpdated == QDateTime()) {
qCWarning(EventHandling) << "a value must be provided for lastUpdated for a pending event.";
return {};
}
return isPending ? lastUpdated : m_event->originTimestamp();
}
QString EventHandler::getTimeString(bool relative, QLocale::FormatType format, bool isPending, QDateTime lastUpdated) const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getTime called with m_event set to nullptr.";
return {};
}
if (isPending && lastUpdated == QDateTime()) {
qCWarning(EventHandling) << "a value must be provided for lastUpdated for a pending event.";
return {};
}
auto ts = getTime(isPending, lastUpdated);
if (ts.isValid()) {
if (relative) {
return m_format.formatRelativeDate(ts.toLocalTime().date(), format);
} else {
return QLocale().toString(ts.toLocalTime().time(), format);
}
}
return {};
}
bool EventHandler::isHighlighted()
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "isHighlighted called with m_room set to nullptr.";
return false;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "isHighlighted called with m_event set to nullptr.";
return false;
}
return !m_room->isDirectChat() && m_room->isEventHighlighted(m_event);
}
bool EventHandler::isHidden()
{
if (m_event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
return true;
}
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(m_event)) {
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
return true;
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
return true;
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
&& !NeoChatConfig::self()->showAvatarUpdate()) {
return true;
}
}
if (m_event->isStateEvent() && eventCast<const StateEvent>(m_event)->repeatsState()) {
return true;
}
// isReplacement?
if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
if (!e->replacedEvent().isEmpty()) {
return true;
}
}
if (is<RedactionEvent>(*m_event) || is<ReactionEvent>(*m_event)) {
return true;
}
if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
if (!e->replacedEvent().isEmpty() && e->replacedEvent() != e->id()) {
return true;
}
}
if (m_room->connection()->isIgnored(m_room->user(m_event->senderId()))) {
return true;
}
// hide ending live location beacons
if (m_event->isStateEvent() && m_event->matrixType() == "org.matrix.msc3672.beacon_info"_ls && !m_event->contentJson()["live"_ls].toBool()) {
return true;
}
return false;
}
QString EventHandler::getRichBody(bool stripNewlines) const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getRichBody called with m_event set to nullptr.";
return {};
}
return getBody(m_event, Qt::RichText, stripNewlines);
}
QString EventHandler::getPlainBody(bool stripNewlines) const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getPlainBody called with m_event set to nullptr.";
return {};
}
return getBody(m_event, Qt::PlainText, stripNewlines);
}
QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const
{
if (event->isRedacted()) {
auto reason = event->redactedBecause()->reason();
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>") : i18n("<i>[This message was deleted: %1]</i>", reason);
}
const bool prettyPrint = (format == Qt::RichText);
return switchOnType(
*event,
[this, format, stripNewlines](const RoomMessageEvent &event) {
return getMessageBody(event, format, stripNewlines);
},
[](const StickerEvent &e) {
return e.body();
},
[this, prettyPrint](const RoomMemberEvent &e) {
// FIXME: Rewind to the name that was at the time of this event
auto subjectName = m_room->htmlSafeMemberName(e.userId());
if (e.membership() == Membership::Leave) {
if (e.prevContent() && e.prevContent()->displayName) {
subjectName = sanitized(*e.prevContent()->displayName).toHtmlEscaped();
}
}
if (prettyPrint) {
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
.arg(e.userId(), Utils::getUserColor(m_room->user(e.userId())->hueF()).name(), subjectName);
}
// The below code assumes senderName output in AuthorRole
switch (e.membership()) {
case Membership::Invite:
if (e.repeatsState()) {
auto text = i18n("reinvited %1 to the room", subjectName);
if (!e.reason().isEmpty()) {
text += i18nc("Optional reason for an invitation", ": %1") + e.reason().toHtmlEscaped();
}
return text;
}
Q_FALLTHROUGH();
case Membership::Join: {
QString text{};
// Part 1: invites and joins
if (e.repeatsState()) {
text = i18n("joined the room (repeated)");
} else if (e.changesMembership()) {
text = e.membership() == Membership::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
}
if (!text.isEmpty()) {
if (!e.reason().isEmpty()) {
text += i18n(": %1", e.reason().toHtmlEscaped());
}
return text;
}
// Part 2: profile changes of joined members
if (e.isRename()) {
if (!e.newDisplayName()) {
text = i18nc("their refers to a singular user", "cleared their display name");
} else {
text = i18nc("their refers to a singular user", "changed their display name to %1", e.newDisplayName()->toHtmlEscaped());
}
}
if (e.isAvatarUpdate()) {
if (!text.isEmpty()) {
text += i18n(" and ");
}
if (!e.newAvatarUrl()) {
text += i18nc("their refers to a singular user", "cleared their avatar");
} else if (!e.prevContent()->avatarUrl) {
text += i18n("set an avatar");
} else {
text += i18nc("their refers to a singular user", "updated their avatar");
}
}
if (text.isEmpty()) {
text = i18nc("<user> changed nothing", "changed nothing");
}
return text;
}
case Membership::Leave:
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
return (e.senderId() != e.userId()) ? i18n("withdrew %1's invitation", subjectName) : i18n("rejected the invitation");
}
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
return (e.senderId() != e.userId()) ? i18n("unbanned %1", subjectName) : i18n("self-unbanned");
}
return (e.senderId() != e.userId())
? i18n("has put %1 out of the room: %2", subjectName, e.contentJson()["reason"_ls].toString().toHtmlEscaped())
: i18n("left the room");
case Membership::Ban:
if (e.senderId() != e.userId()) {
if (e.reason().isEmpty()) {
return i18n("banned %1 from the room", subjectName);
} else {
return i18n("banned %1 from the room: %2", subjectName, e.reason().toHtmlEscaped());
}
} else {
return i18n("self-banned from the room");
}
case Membership::Knock: {
QString reason(e.contentJson()["reason"_ls].toString().toHtmlEscaped());
return reason.isEmpty() ? i18n("requested an invite") : i18n("requested an invite with reason: %1", reason);
}
default:;
}
return i18n("made something unknown");
},
[](const RoomCanonicalAliasEvent &e) {
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias to: %1", e.alias());
},
[](const RoomNameEvent &e) {
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name to: %1", e.name().toHtmlEscaped());
},
[prettyPrint, stripNewlines](const RoomTopicEvent &e) {
return (e.topic().isEmpty()) ? i18n("cleared the topic")
: i18n("set the topic to: %1",
prettyPrint ? Quotient::prettyPrint(e.topic())
: stripNewlines ? e.topic().replace(u'\n', u' ')
: e.topic());
},
[](const RoomAvatarEvent &) {
return i18n("changed the room avatar");
},
[](const EncryptionEvent &) {
return i18n("activated End-to-End Encryption");
},
[](const RoomCreateEvent &e) {
return e.isUpgrade() ? i18n("upgraded the room to version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped())
: i18n("created the room, version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped());
},
[](const RoomPowerLevelsEvent &) {
return i18nc("'power level' means permission level", "changed the power levels for this room");
},
[](const StateEvent &e) {
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
return i18n("changed the server access control lists for this room");
}
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
return i18nc("[User] added <name> widget", "added %1 widget", e.contentJson()["name"_ls].toString());
}
if (e.contentJson().isEmpty()) {
return i18nc("[User] removed <name> widget", "removed %1 widget", e.fullJson()["unsigned"_ls]["prev_content"_ls]["name"_ls].toString());
}
return i18nc("[User] configured <name> widget", "configured %1 widget", e.contentJson()["name"_ls].toString());
}
if (e.matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
return e.contentJson()["description"_ls].toString();
}
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
: i18n("updated %1 state for %2", e.matrixType(), e.stateKey().toHtmlEscaped());
},
[](const PollStartEvent &e) {
return e.question();
},
i18n("Unknown event"));
}
QString EventHandler::getMessageBody(const RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const
{
TextHandler textHandler;
if (event.hasFileContent()) {
auto fileCaption = event.content()->fileInfo()->originalName;
if (fileCaption.isEmpty()) {
fileCaption = event.plainBody();
} else if (event.content()->fileInfo()->originalName != event.plainBody()) {
fileCaption = event.plainBody() + " | "_ls + fileCaption;
}
textHandler.setData(fileCaption);
return !fileCaption.isEmpty() ? textHandler.handleRecievePlainText(Qt::PlainText, stripNewlines) : i18n("a file");
}
QString body;
if (event.hasTextContent() && event.content()) {
body = static_cast<const MessageEventContent::TextContent *>(event.content())->body;
} else {
body = event.plainBody();
}
textHandler.setData(body);
Qt::TextFormat inputFormat;
if (event.mimeType().name() == "text/plain"_ls) {
inputFormat = Qt::PlainText;
} else {
inputFormat = Qt::RichText;
}
if (format == Qt::RichText) {
return textHandler.handleRecieveRichText(inputFormat, m_room, &event, stripNewlines);
} else {
return textHandler.handleRecievePlainText(inputFormat, stripNewlines);
}
}
QString EventHandler::getGenericBody() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getGenericBody called with m_event set to nullptr.";
return {};
}
if (m_event->isRedacted()) {
return i18n("<i>[This message was deleted]</i>");
}
return switchOnType(
*m_event,
[](const RoomMessageEvent &e) {
Q_UNUSED(e)
return i18n("sent a message");
},
[](const StickerEvent &e) {
Q_UNUSED(e)
return i18n("sent a sticker");
},
[](const RoomMemberEvent &e) {
switch (e.membership()) {
case Membership::Invite:
if (e.repeatsState()) {
return i18n("reinvited someone to the room");
}
Q_FALLTHROUGH();
case Membership::Join: {
QString text{};
// Part 1: invites and joins
if (e.repeatsState()) {
text = i18n("joined the room (repeated)");
} else if (e.changesMembership()) {
text = e.membership() == Membership::Invite ? i18n("invited someone to the room") : i18n("joined the room");
}
if (!text.isEmpty()) {
return text;
}
// Part 2: profile changes of joined members
if (e.isRename()) {
if (!e.newDisplayName()) {
text = i18nc("their refers to a singular user", "cleared their display name");
} else {
text = i18nc("their refers to a singular user", "changed their display name");
}
}
if (e.isAvatarUpdate()) {
if (!text.isEmpty()) {
text += i18n(" and ");
}
if (!e.newAvatarUrl()) {
text += i18nc("their refers to a singular user", "cleared their avatar");
} else if (!e.prevContent()->avatarUrl) {
text += i18n("set an avatar");
} else {
text += i18nc("their refers to a singular user", "updated their avatar");
}
}
if (text.isEmpty()) {
text = i18nc("<user> changed nothing", "changed nothing");
}
return text;
}
case Membership::Leave:
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
return (e.senderId() != e.userId()) ? i18n("withdrew a user's invitation") : i18n("rejected the invitation");
}
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
return (e.senderId() != e.userId()) ? i18n("unbanned a user") : i18n("self-unbanned");
}
return (e.senderId() != e.userId()) ? i18n("put a user out of the room") : i18n("left the room");
case Membership::Ban:
if (e.senderId() != e.userId()) {
return i18n("banned a user from the room");
} else {
return i18n("self-banned from the room");
}
case Membership::Knock: {
return i18n("requested an invite");
}
default:;
}
return i18n("made something unknown");
},
[](const RoomCanonicalAliasEvent &e) {
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias");
},
[](const RoomNameEvent &e) {
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name");
},
[](const RoomTopicEvent &e) {
return (e.topic().isEmpty()) ? i18n("cleared the topic") : i18n("set the topic");
},
[](const RoomAvatarEvent &) {
return i18n("changed the room avatar");
},
[](const EncryptionEvent &) {
return i18n("activated End-to-End Encryption");
},
[](const RoomCreateEvent &e) {
return e.isUpgrade() ? i18n("upgraded the room version") : i18n("created the room");
},
[](const RoomPowerLevelsEvent &) {
return i18nc("'power level' means permission level", "changed the power levels for this room");
},
[](const StateEvent &e) {
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
return i18n("changed the server access control lists for this room");
}
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
return i18n("added a widget");
}
if (e.contentJson().isEmpty()) {
return i18n("removed a widget");
}
return i18n("configured a widget");
}
return i18n("updated the state");
},
[](const PollStartEvent &e) {
Q_UNUSED(e);
return i18n("started a poll");
},
i18n("Unknown event"));
}
QVariantMap EventHandler::getMediaInfo() const
{
return getMediaInfoForEvent(m_event);
}
QVariantMap EventHandler::getMediaInfoForEvent(const Quotient::RoomEvent *event) const
{
QString eventId = event->id();
// Get the file info for the event.
const EventContent::FileInfo *fileInfo;
if (event->is<RoomMessageEvent>()) {
auto roomMessageEvent = eventCast<const RoomMessageEvent>(event);
if (!roomMessageEvent->hasFileContent()) {
return {};
}
fileInfo = roomMessageEvent->content()->fileInfo();
} else if (event->is<StickerEvent>()) {
auto stickerEvent = eventCast<const StickerEvent>(event);
fileInfo = &stickerEvent->image();
} else {
return {};
}
return getMediaInfoFromFileInfo(fileInfo, eventId);
}
QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail) const
{
QVariantMap mediaInfo;
// Get the mxc URL for the media.
if (!fileInfo->url().isValid() || eventId.isEmpty()) {
mediaInfo["source"_ls] = QUrl();
} else {
QUrl source = m_room->makeMediaUrl(eventId, fileInfo->url());
if (source.isValid() && source.scheme() == QStringLiteral("mxc")) {
mediaInfo["source"_ls] = source;
} else {
mediaInfo["source"_ls] = QUrl();
}
}
auto mimeType = fileInfo->mimeType;
// Add the MIME type for the media if available.
mediaInfo["mimeType"_ls] = mimeType.name();
// Add the MIME type icon if available.
mediaInfo["mimeIcon"_ls] = mimeType.iconName();
// Add media size if available.
mediaInfo["size"_ls] = fileInfo->payloadSize;
// Add parameter depending on media type.
if (mimeType.name().contains(QStringLiteral("image"))) {
if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileInfo)) {
mediaInfo["width"_ls] = castInfo->imageSize.width();
mediaInfo["height"_ls] = castInfo->imageSize.height();
// TODO: Images in certain formats (e.g. WebP) will be erroneously marked as animated, even if they are static.
mediaInfo["animated"_ls] = QMovie::supportedFormats().contains(mimeType.preferredSuffix().toUtf8());
if (!isThumbnail) {
QVariantMap tempInfo;
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
tempInfo = thumbnailInfo;
} else {
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
if (blurhash.isEmpty()) {
tempInfo["source"_ls] = QUrl();
} else {
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
}
}
mediaInfo["tempInfo"_ls] = tempInfo;
}
}
}
if (mimeType.name().contains(QStringLiteral("video"))) {
if (auto castInfo = static_cast<const EventContent::VideoContent *>(fileInfo)) {
mediaInfo["width"_ls] = castInfo->imageSize.width();
mediaInfo["height"_ls] = castInfo->imageSize.height();
mediaInfo["duration"_ls] = castInfo->duration;
if (!isThumbnail) {
QVariantMap tempInfo;
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
tempInfo = thumbnailInfo;
} else {
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
if (blurhash.isEmpty()) {
tempInfo["source"_ls] = QUrl();
} else {
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
}
}
mediaInfo["tempInfo"_ls] = tempInfo;
}
}
}
if (mimeType.name().contains(QStringLiteral("audio"))) {
if (auto castInfo = static_cast<const EventContent::AudioContent *>(fileInfo)) {
mediaInfo["duration"_ls] = castInfo->duration;
}
}
return mediaInfo;
}
QSharedPointer<LinkPreviewer> EventHandler::getLinkPreviewer() const
{
if (!m_event->is<RoomMessageEvent>()) {
return nullptr;
}
QString text;
auto event = eventCast<const RoomMessageEvent>(m_event);
if (event->hasTextContent()) {
auto textContent = static_cast<const EventContent::TextContent *>(event->content());
if (textContent) {
text = textContent->body;
} else {
text = event->plainBody();
}
} else {
text = event->plainBody();
}
TextHandler textHandler;
textHandler.setData(text);
QList<QUrl> links = textHandler.getLinkPreviews();
if (links.size() > 0) {
return QSharedPointer<LinkPreviewer>(new LinkPreviewer(nullptr, m_room, links.size() > 0 ? links[0] : QUrl()));
} else {
return nullptr;
}
}
QSharedPointer<ReactionModel> EventHandler::getReactions() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReactions called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReactions called with m_event set to nullptr.";
return nullptr;
}
if (!m_event->is<RoomMessageEvent>()) {
qCWarning(EventHandling) << "getReactions called with on a non-message event.";
return nullptr;
}
auto eventId = m_event->id();
const auto &annotations = m_room->relatedEvents(eventId, EventRelation::AnnotationType);
if (annotations.isEmpty()) {
return nullptr;
};
QMap<QString, QList<User *>> reactions = {};
for (const auto &a : annotations) {
if (a->isRedacted()) { // Just in case?
continue;
}
if (const auto &e = eventCast<const ReactionEvent>(a)) {
reactions[e->key()].append(m_room->user(e->senderId()));
}
}
if (reactions.isEmpty()) {
return nullptr;
}
QList<ReactionModel::Reaction> res;
auto i = reactions.constBegin();
while (i != reactions.constEnd()) {
QVariantList authors;
for (const auto &author : i.value()) {
authors.append(m_room->getUser(author));
}
res.append(ReactionModel::Reaction{i.key(), authors});
++i;
}
if (res.size() > 0) {
return QSharedPointer<ReactionModel>(new ReactionModel(nullptr, res, m_room->localUser()));
} else {
return nullptr;
}
}
bool EventHandler::hasReply() const
{
return !m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
}
QString EventHandler::getReplyId() const
{
return m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
}
DelegateType::Type EventHandler::getReplyDelegateType() const
{
auto replyEvent = m_room->getReplyForEvent(*m_event);
if (replyEvent == nullptr) {
return DelegateType::Other;
}
return getDelegateTypeForEvent(replyEvent);
}
QVariantMap EventHandler::getReplyAuthor() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyAuthor called with m_room set to nullptr.";
return {};
}
// If we have a room we can return an empty user by handing nullptr to m_room->getUser.
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyAuthor called with m_event set to nullptr. Returning empty user.";
return m_room->getUser(nullptr);
}
auto replyPtr = m_room->getReplyForEvent(*m_event);
if (replyPtr) {
auto replyUser = m_room->user(replyPtr->senderId());
return m_room->getUser(replyUser);
} else {
return m_room->getUser(nullptr);
}
}
QString EventHandler::getReplyRichBody(bool stripNewlines) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyRichBody called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyRichBody called with m_event set to nullptr.";
return {};
}
auto replyEvent = m_room->getReplyForEvent(*m_event);
if (replyEvent == nullptr) {
return {};
}
return getBody(replyEvent, Qt::RichText, stripNewlines);
}
QString EventHandler::getReplyPlainBody(bool stripNewlines) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyPlainBody called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyPlainBody called with m_event set to nullptr.";
return {};
}
auto replyEvent = m_room->getReplyForEvent(*m_event);
if (replyEvent == nullptr) {
return {};
}
return getBody(replyEvent, Qt::PlainText, stripNewlines);
}
QVariantMap EventHandler::getReplyMediaInfo() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyMediaInfo called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyMediaInfo called with m_event set to nullptr.";
return {};
}
auto replyPtr = m_room->getReplyForEvent(*m_event);
if (!replyPtr) {
return {};
}
return getMediaInfoForEvent(replyPtr);
}
float EventHandler::getLatitude() const
{
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) {
return -100.0; // latitude runs from -90deg to +90deg so -100 is out of range.
}
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[0];
return latitude.toFloat();
}
float EventHandler::getLongitude() const
{
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) {
return -200.0; // longitude runs from -180deg to +180deg so -200 is out of range.
}
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[1];
return latitude.toFloat();
}
QString EventHandler::getLocationAssetType() const
{
const auto assetType = m_event->contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
if (assetType.isEmpty()) {
return {};
}
return assetType;
}
bool EventHandler::hasReadMarkers() const
{
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
return userIds.size() > 0;
}
QVariantList EventHandler::getReadMarkers(int maxMarkers) const
{
auto userIds_temp = m_room->userIdsAtEvent(m_event->id());
userIds_temp.remove(m_room->localUser()->id());
auto userIds = userIds_temp.values();
if (userIds.count() > maxMarkers) {
userIds = userIds.mid(0, maxMarkers);
}
QVariantList users;
users.reserve(userIds.size());
for (const auto &userId : userIds) {
auto user = m_room->user(userId);
users += m_room->getUser(user);
}
return users;
}
QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const
{
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
if (userIds.count() > maxMarkers) {
return QStringLiteral("+ ") + QString::number(userIds.count() - maxMarkers);
} else {
return QString();
}
}
QString EventHandler::getReadMarkersString() const
{
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
/**
* The string ends up in the form
* "x users: user1DisplayName, user2DisplayName, etc."
*/
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
for (const auto &userId : userIds) {
auto user = m_room->user(userId);
readMarkersString += user->displayname(m_room) + i18nc("list separator", ", ");
}
readMarkersString.chop(2);
return readMarkersString;
}

View File

@@ -1,400 +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>
#include <KFormat>
#include <Quotient/eventitem.h>
#include <Quotient/events/roomevent.h>
#include <Quotient/events/roommessageevent.h>
#include "enums/delegatetype.h"
class LinkPreviewer;
class NeoChatRoom;
class ReactionModel;
/**
* @class EventHandler
*
* This class is designed to handle a Quotient::RoomEvent allowing data to be extracted
* in a form ready for the NeoChat UI.
*
* To use this properly both the room and the event should be set (and the event should
* be from the given room).
*
* @note EventHandler will always try to return something even when not properly
* initialised, this is usually the best empty value it can create with available
* information. This is to minimize warnings from QML especially during startup
* and room changes.
*/
class EventHandler : public QObject
{
Q_OBJECT
public:
/**
* @brief Return the current room the EventHandler is using.
*/
const NeoChatRoom *getRoom() const;
/**
* @brief Set the current room the EventHandler to using.
*/
void setRoom(const NeoChatRoom *room);
/**
* @brief Return the current event the EventHandler is using.
*/
const Quotient::Event *getEvent() const;
/**
* @brief Set the current event the EventHandler to using.
*/
void setEvent(const Quotient::RoomEvent *event);
/**
* @brief Return the Matrix ID of the event.
*/
QString getId() const;
/**
* @brief Return the DelegateType of the event.
*
* @note While similar this is not the matrix event or message type. This is
* to tell a QML ListView what delegate to show for each event. So while
* similar to the spec it is not the same.
*/
DelegateType::Type getDelegateType() const;
/**
* @brief Get the author of the event in context of the room.
*
* This is different to getting a Quotient::User object
* as neither of those can provide details like the displayName or avatarMediaId
* without the room context as these can vary from room to room. This function
* uses the room context and outputs the result as QVariantMap.
*
* An empty QVariantMap will be returned if the EventHandler hasn't had the room
* intialised. An empty user (i.e. a QVariantMap with all the correct keys
* but empty values) will be returned if the room has been set but not an event.
*
* @param isPending if the event is pending, i.e. has not been confirmed by
* the server.
*
* @return a QVariantMap for the user with the following properties:
* - isLocalUser - Whether the user is the local user.
* - id - The matrix ID of the user.
* - displayName - Display name in the context of this room.
* - avatarSource - The mxc URL for the user's avatar in the current room.
* - avatarMediaId - Avatar id in the context of this room.
* - color - Color for the user.
* - object - The Quotient::User object for the user.
*
* @sa Quotient::User
*/
QVariantMap getAuthor(bool isPending = false) const;
/**
* @brief Get the display name of the event author.
*
* This method is separate from getAuthor() and special in that it will return
* the old display name of the author if the current event is one that caused it
* to change. This allows for scenarios where the UI wishes to notify that a
* user's display name has changed and what it changed from.
*
* @param isPending whether the event is pending as this cannot be derived from
* just the event object.
*/
QString getAuthorDisplayName(bool isPending = false) const;
/**
* @brief Return a QDateTime object for the event timestamp.
*/
QDateTime getTime(bool isPending = false, QDateTime lastUpdated = {}) const;
/**
* @brief Return a QString for the event timestamp.
*
* This is intended to return a string that is read for display in the UI without
* any further manipulation required.
*
* @param relative whether the string is realtive to the current date, i.e.
* Yesterday or Wednesday, etc.
* @param format the QLocale::FormatType to use.
* @param isPending whether the event is pending as this cannot be derived from
* just the event object.
* @param lastUpdated the time the event was last updated locally as this cannot be
* obtained from the event.
*/
QString getTimeString(bool relative, QLocale::FormatType format = QLocale::ShortFormat, bool isPending = false, QDateTime lastUpdated = {}) const;
/**
* @brief Whether the event should be highlighted in the timeline.
*
* @note Messages in direct chats are never highlighted.
*/
bool isHighlighted();
/**
* @brief Whether the event should be hidden in the timeline.
*
* This could be for numerous reasons, e.g. if it's a replacement event, if the
* user has hidden all state events or if the sender has been ignored by the local
* user.
*/
bool isHidden();
/**
* @brief Output a string for the message content ready for display in a rich text field.
*
* The output string is dependant upon the event type and the desired output format.
*
* For most messages this is the body content of the message. For media messages
* this will be the caption and for state events it will be a string specific
* to that event with some dynamic details about the event added.
*
* E.g. For a room topic state event the text will be:
* "set the topic to: <new topic text>"
*
* @param stripNewlines whether the output should have new lines in it.
*/
QString getRichBody(bool stripNewlines = false) const;
/**
* @brief Output a string for the message content ready for display in a plain text field.
*
* The output string is dependant upon the event type and the desired output format.
*
* For most messages this is the body content of the message. For media messages
* this will be the caption and for state events it will be a string specific
* to that event with some dynamic details about the event added.
*
* E.g. For a room topic state event the text will be:
* "set the topic to: <new topic text>"
*
* @param stripNewlines whether the output should have new lines in it.
*/
QString getPlainBody(bool stripNewlines = false) const;
/**
* @brief Output a generic string for the message content ready for display.
*
* The output string is dependant upon the event type.
*
* Unlike EventHandler::getRichBody or EventHandler::getPlainBody the string
* is the same for all events of the same type.
*
* E.g. For a message the text will be:
* "sent a message"
*
* @sa getRichBody(), getPlainBody()
*/
QString getGenericBody() const;
/**
* @brief Return the media info for the event.
*
* An empty QVariantMap will be returned for any event that doesn't have any
* media info.
*
* @return This should consist of the following:
* - source - The mxc URL for the media.
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
* - mimeIcon - The MIME icon name (should be image-xxx).
* - size - The file size in bytes.
* - width - The width in pixels of the audio media.
* - height - The height in pixels of the audio media.
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
*/
QVariantMap getMediaInfo() const;
/**
* @brief Return a LinkPreviewer object for the event.
*
* A nullptr will be returned for any event that doesn't have any links so the
* return should be null checked and an empty LinkPreviewer provided if null.
*
* @sa LinkPreviewer
*/
QSharedPointer<LinkPreviewer> getLinkPreviewer() const;
/**
* @brief Return a ReactionModel object for the event.
*
* A nullptr will be returned for any event that doesn't have any links so the
* return should be null checked and an empty QVariantList (or other suitable
* empty mode) provided if null.
*/
QSharedPointer<ReactionModel> getReactions() const;
/**
* @brief Whether the event is a reply to another in the timeline.
*/
bool hasReply() const;
/**
* @brief Return the Matrix ID of the event replied to.
*/
QString getReplyId() const;
/**
* @brief Return the DelegateType of the event replied to.
*
* @note While similar this is not the matrix event or message type. This is
* to tell a QML ListView what delegate to show for each event. So while
* similar to the spec it is not the same.
*/
DelegateType::Type getReplyDelegateType() const;
/**
* @brief Get the author of the event replied to in context of the room.
*
* This is different to getting a Quotient::User object
* as neither of those can provide details like the displayName or avatarMediaId
* without the room context as these can vary from room to room. This function
* uses the room context and outputs the result as QVariantMap.
*
* An empty QVariantMap will be returned if the EventHandler hasn't had the room
* intialised. An empty user (i.e. a QVariantMap with all the correct keys
* but empty values) will be returned if the room has been set but not an event.
*
* @return a QVariantMap for the user with the following properties:
* - isLocalUser - Whether the user is the local user.
* - id - The matrix ID of the user.
* - displayName - Display name in the context of this room.
* - avatarSource - The mxc URL for the user's avatar in the current room.
* - avatarMediaId - Avatar id in the context of this room.
* - color - Color for the user.
* - object - The Quotient::User object for the user.
*
* @sa Quotient::User
*/
QVariantMap getReplyAuthor() const;
/**
* @brief Output a string for the message content of the event replied to ready
* for display in a rich text field.
*
* The output string is dependant upon the event type and the desired output format.
*
* For most messages this is the body content of the message. For media messages
* this will be the caption and for state events it will be a string specific
* to that event with some dynamic details about the event added.
*
* E.g. For a room topic state event the text will be:
* "set the topic to: <new topic text>"
*
* @param stripNewlines whether the output should have new lines in it.
*/
QString getReplyRichBody(bool stripNewlines = false) const;
/**
* @brief Output a string for the message content of the event replied to ready
* for display in a plain text field.
*
* The output string is dependant upon the event type and the desired output format.
*
* For most messages this is the body content of the message. For media messages
* this will be the caption and for state events it will be a string specific
* to that event with some dynamic details about the event added.
*
* E.g. For a room topic state event the text will be:
* "set the topic to: <new topic text>"
*
* @param stripNewlines whether the output should have new lines in it.
*/
QString getReplyPlainBody(bool stripNewlines = false) const;
/**
* @brief Return the media info for the event replied to.
*
* An empty QVariantMap will be returned for any event that doesn't have any
* media info.
*
* @return This should consist of the following:
* - source - The mxc URL for the media.
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
* - mimeIcon - The MIME icon name (should be image-xxx).
* - size - The file size in bytes.
* - width - The width in pixels of the audio media.
* - height - The height in pixels of the audio media.
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
*/
QVariantMap getReplyMediaInfo() const;
/**
* @brief Return the latitude for the event.
*
* Returns -100.0 if the event doesn't have a location (latitudes are in the
* range -90deg to +90deg so -100 is out of range).
*/
float getLatitude() const;
/**
* @brief Return the longitude for the event.
*
* Returns -200.0 if the event doesn't have a location (latitudes are in the
* range -180deg to +180deg so -200 is out of range).
*/
float getLongitude() const;
/**
* @brief Return the type of location marker for the event.
*/
QString getLocationAssetType() const;
/**
* @brief Whether the event has any read marker for other users.
*/
bool hasReadMarkers() const;
/**
* @brief Returns a list of user read marker for the event.
*
* @param maxMarkers the maximum number of users to return. Usually the number
* of user read makers shown is limited to not clutter the UI.
* This needs to be the same as used in getNumberExcessReadMarkers
* so that the markers line up with the number displayed, i.e.
* the number of users shown plus the excess number will be
* the total number of other user read markers at an event.
*/
QVariantList getReadMarkers(int maxMarkers = 5) const;
/**
* @brief Returns the number of excess user read markers for the event.
*
* This returns a string in the form "+ x" ready for use in the UI.
*
* @param maxMarkers the maximum number of markers shown in the UI. This needs to
* be the same as used in getReadMarkers so that the value lines
* up with the number displayed, i.e. the number of users shown
* plus the excess number will be the total number of other user
* read markers at an event.
*/
QString getNumberExcessReadMarkers(int maxMarkers = 5) const;
/**
* @brief Returns a string with the names of the read markers at the event.
*
* This is in the form "x users: name 1, name 2, ...".
*/
QString getReadMarkersString() const;
private:
const NeoChatRoom *m_room = nullptr;
const Quotient::RoomEvent *m_event = nullptr;
KFormat m_format;
DelegateType::Type getDelegateTypeForEvent(const Quotient::RoomEvent *event) const;
QString getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const;
QString getMessageBody(const Quotient::RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const;
QVariantMap getMediaInfoForEvent(const Quotient::RoomEvent *event) const;
QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const;
};

View File

@@ -1,16 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatchangepasswordjob.h"
using namespace Quotient;
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("new_password"), newPassword);
addParam<IfNotEmpty>(_data, QStringLiteral("logout_devices"), logoutDevices);
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(_data);
}

View File

@@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatChangePasswordJob : public Quotient::BaseJob
{
public:
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};

View File

@@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatdeactivateaccountjob.h"
using namespace Quotient;
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("DisableDeviceJob"), "_matrix/client/v3/account/deactivate")
{
QJsonObject data;
addParam<IfNotEmpty>(data, QStringLiteral("auth"), auth);
setRequestData(data);
}

View File

@@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeoChatDeactivateAccountJob : public Quotient::BaseJob
{
public:
explicit NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};

View File

@@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatdeletedevicejob.h"
using namespace Quotient;
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(std::move(_data));
}

View File

@@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatDeleteDeviceJob : public Quotient::BaseJob
{
public:
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};

View File

@@ -8,22 +8,17 @@
#include <Quotient/connection.h>
#include <Quotient/csapi/content-repo.h>
#include "neochatconfig.h"
#include "neochatroom.h"
using namespace Quotient;
LinkPreviewer::LinkPreviewer(QObject *parent, const NeoChatRoom *room, const QUrl &url)
LinkPreviewer::LinkPreviewer(QObject *parent, NeoChatRoom *room, const QUrl &url)
: QObject(parent)
, m_currentRoom(room)
, m_loaded(false)
, m_url(url)
{
loadUrlPreview();
if (m_currentRoom) {
connect(m_currentRoom, &NeoChatRoom::urlPreviewEnabledChanged, this, &LinkPreviewer::loadUrlPreview);
}
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, &LinkPreviewer::loadUrlPreview);
}
bool LinkPreviewer::loaded() const
@@ -62,9 +57,6 @@ void LinkPreviewer::setUrl(QUrl url)
void LinkPreviewer::loadUrlPreview()
{
if (!m_currentRoom || !NeoChatConfig::showLinkPreview() || !m_currentRoom->urlPreviewEnabled()) {
return;
}
if (m_url.scheme() == QStringLiteral("https")) {
m_loaded = false;
Q_EMIT loadedChanged();
@@ -93,9 +85,4 @@ void LinkPreviewer::loadUrlPreview()
}
}
bool LinkPreviewer::empty() const
{
return m_url.isEmpty();
}
#include "moc_linkpreviewer.cpp"

View File

@@ -44,15 +44,8 @@ class LinkPreviewer : public QObject
*/
Q_PROPERTY(QUrl imageSource READ imageSource NOTIFY imageSourceChanged)
/**
* @brief Whether the there is a link to preview.
*
* A linkPreviwer is empty if the URL is empty.
*/
Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged)
public:
explicit LinkPreviewer(QObject *parent = nullptr, const NeoChatRoom *room = nullptr, const QUrl &url = {});
explicit LinkPreviewer(QObject *parent = nullptr, NeoChatRoom *room = nullptr, const QUrl &url = {});
[[nodiscard]] QUrl url() const;
void setUrl(QUrl);
@@ -60,10 +53,9 @@ public:
[[nodiscard]] QString title() const;
[[nodiscard]] QString description() const;
[[nodiscard]] QUrl imageSource() const;
[[nodiscard]] bool empty() const;
private:
const NeoChatRoom *m_currentRoom = nullptr;
NeoChatRoom *m_currentRoom = nullptr;
bool m_loaded;
QString m_title = QString();
@@ -79,6 +71,5 @@ Q_SIGNALS:
void descriptionChanged();
void imageSourceChanged();
void urlChanged();
void emptyChanged();
};
Q_DECLARE_METATYPE(LinkPreviewer *)

View File

@@ -22,7 +22,7 @@ Login::Login(QObject *parent)
void Login::init()
{
m_homeserverReachable = false;
m_connection = new NeoChatConnection();
m_connection = new Connection();
m_matrixId = QString();
m_password = QString();
m_deviceName = QStringLiteral("NeoChat %1 %2 %3 %4")
@@ -51,7 +51,7 @@ void Login::init()
m_testing = true;
Q_EMIT testingChanged();
if (!m_connection) {
m_connection = new NeoChatConnection();
m_connection = new Connection();
}
m_connection->resolveServer(m_matrixId);
connectSingleShot(m_connection, &Connection::loginFlowsChanged, this, [this]() {
@@ -87,11 +87,7 @@ void Login::init()
Q_EMIT isLoggingInChanged();
});
connect(m_connection, &Connection::loginError, this, [this](QString error, const QString &) {
if (error == QStringLiteral("Invalid username or password")) {
setInvalidPassword(true);
} else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
}
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
m_isLoggingIn = false;
Q_EMIT isLoggingInChanged();
});
@@ -137,7 +133,6 @@ QString Login::password() const
void Login::setPassword(const QString &password)
{
setInvalidPassword(false);
m_password = password;
Q_EMIT passwordChanged();
}
@@ -204,15 +199,4 @@ bool Login::isLoggedIn() const
return m_isLoggedIn;
}
void Login::setInvalidPassword(bool invalid)
{
m_invalidPassword = invalid;
Q_EMIT isInvalidPasswordChanged();
}
bool Login::isInvalidPassword() const
{
return m_invalidPassword;
}
#include "moc_login.cpp"

View File

@@ -6,7 +6,10 @@
#include <QObject>
#include <QUrl>
class NeoChatConnection;
namespace Quotient
{
class Connection;
}
/**
* @class Login
@@ -70,11 +73,6 @@ class Login : public QObject
*/
Q_PROPERTY(bool isLoggedIn READ isLoggedIn NOTIFY isLoggedInChanged)
/**
* @brief Whether the password (or the username) is invalid.
*/
Q_PROPERTY(bool isInvalidPassword READ isInvalidPassword NOTIFY isInvalidPasswordChanged)
public:
explicit Login(QObject *parent = nullptr);
@@ -102,9 +100,6 @@ public:
bool isLoggedIn() const;
bool isInvalidPassword() const;
void setInvalidPassword(bool invalid);
Q_INVOKABLE void login();
Q_INVOKABLE void loginWithSso();
@@ -121,7 +116,6 @@ Q_SIGNALS:
void testingChanged();
void isLoggingInChanged();
void isLoggedInChanged();
void isInvalidPasswordChanged();
private:
void setHomeserverReachable(bool reachable);
@@ -132,10 +126,9 @@ private:
QString m_deviceName;
bool m_supportsSso = false;
bool m_supportsPassword = false;
NeoChatConnection *m_connection = nullptr;
Quotient::Connection *m_connection = nullptr;
QUrl m_ssoUrl;
bool m_testing = false;
bool m_isLoggingIn = false;
bool m_isLoggedIn = false;
bool m_invalidPassword = false;
};

View File

@@ -18,10 +18,6 @@
#include <QApplication>
#endif
#ifdef HAVE_WEBVIEW
#include <QtWebView>
#endif
#include <KAboutData>
#ifdef HAVE_KDBUSADDONS
#include <KDBusService>
@@ -47,15 +43,14 @@
#include "clipboard.h"
#include "controller.h"
#include "delegatesizehelper.h"
#include "enums/delegatetype.h"
#include "filetypesingleton.h"
#include "linkpreviewer.h"
#include "locationhelper.h"
#include "logger.h"
#include "login.h"
#include "matriximageprovider.h"
#include "mediasizehelper.h"
#include "models/accountemoticonmodel.h"
#include "models/collapsestateproxymodel.h"
#include "models/customemojimodel.h"
#include "models/devicesmodel.h"
#include "models/devicesproxymodel.h"
@@ -82,7 +77,6 @@
#include "models/userlistmodel.h"
#include "models/webshortcutmodel.h"
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include "notificationsmanager.h"
#include "pollhandler.h"
@@ -101,7 +95,6 @@
#include "runner.h"
#include <QDBusConnection>
#endif
#include "registration.h"
#ifdef Q_OS_WINDOWS
#include <Windows.h>
@@ -140,14 +133,12 @@ Q_DECL_EXPORT
#endif
int main(int argc, char *argv[])
{
QNetworkProxyFactory::setUseSystemConfiguration(true);
#ifdef HAVE_WEBVIEW
QtWebView::initialize();
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
QNetworkProxyFactory::setUseSystemConfiguration(true);
#ifdef Q_OS_ANDROID
QGuiApplication app(argc, argv);
QQuickStyle::setStyle(QStringLiteral("org.kde.breeze"));
@@ -242,7 +233,6 @@ int main(int argc, char *argv[])
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Controller::instance().accounts());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "SpaceHierarchyCache", &SpaceHierarchyCache::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CustomEmojiModel", &CustomEmojiModel::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Registration", &Registration::instance());
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");
@@ -250,6 +240,7 @@ int main(int argc, char *argv[])
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
qmlRegisterType<ReactionModel>("org.kde.neochat", 1, 0, "ReactionModel");
qmlRegisterType<CollapseStateProxyModel>("org.kde.neochat", 1, 0, "CollapseStateProxyModel");
qmlRegisterType<MediaMessageFilterModel>("org.kde.neochat", 1, 0, "MediaMessageFilterModel");
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
qmlRegisterType<UserFilterModel>("org.kde.neochat", 1, 0, "UserFilterModel");
@@ -274,8 +265,7 @@ int main(int argc, char *argv[])
qmlRegisterType<AccountEmoticonModel>("org.kde.neochat", 1, 0, "AccountEmoticonModel");
qmlRegisterType<EmoticonFilterModel>("org.kde.neochat", 1, 0, "EmoticonFilterModel");
qmlRegisterType<DelegateSizeHelper>("org.kde.neochat", 1, 0, "DelegateSizeHelper");
qmlRegisterType<MediaSizeHelper>("org.kde.neochat", 1, 0, "MediaSizeHelper");
qmlRegisterUncreatableType<DelegateType>("org.kde.neochat", 1, 0, "DelegateType", "ENUM"_ls);
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM"_ls);
qmlRegisterUncreatableType<PushNotificationKind>("org.kde.neochat", 1, 0, "PushNotificationKind", "ENUM"_ls);
qmlRegisterUncreatableType<PushNotificationSection>("org.kde.neochat", 1, 0, "PushNotificationSection", "ENUM"_ls);
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM"_ls);
@@ -283,13 +273,12 @@ int main(int argc, char *argv[])
qmlRegisterUncreatableType<NeoChatRoomType>("org.kde.neochat", 1, 0, "NeoChatRoomType", "ENUM"_ls);
qmlRegisterUncreatableType<User>("org.kde.neochat", 1, 0, "User", {});
qmlRegisterUncreatableType<NeoChatRoom>("org.kde.neochat", 1, 0, "NeoChatRoom", {});
qmlRegisterUncreatableType<NeoChatConnection>("org.kde.neochat", 1, 0, "NeoChatConnection", {});
qmlRegisterSingletonType(QUrl("qrc:/ContextMenu.qml"), "org.kde.neochat", 1, 0, "ContextMenu");
qRegisterMetaType<User *>("User*");
qRegisterMetaType<User *>("const User*");
qRegisterMetaType<User *>("const Quotient::User*");
qRegisterMetaType<Room *>("Room*");
qRegisterMetaType<Connection *>("Connection*");
qRegisterMetaType<MessageEventType>("MessageEventType");
qRegisterMetaType<NeoChatRoom *>("NeoChatRoom*");
qRegisterMetaType<User *>("User*");
@@ -306,6 +295,10 @@ int main(int argc, char *argv[])
return engine->toScriptValue(LocationHelper());
});
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
qRegisterMetaTypeStreamOperators<Emoji>();
#endif
QQmlApplicationEngine engine;
#ifdef HAVE_KDBUSADDONS
@@ -360,7 +353,7 @@ int main(int argc, char *argv[])
return -1;
}
if (!parser.positionalArguments().isEmpty()) {
if (parser.positionalArguments().length() > 0) {
RoomManager::instance().setUrlArgument(parser.positionalArguments()[0]);
}

View File

@@ -10,6 +10,11 @@
#include <QReadWriteLock>
namespace Quotient
{
class Connection;
}
/**
* @class ThumbnailResponse
*

View File

@@ -1,163 +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 "mediasizehelper.h"
#include "neochatconfig.h"
MediaSizeHelper::MediaSizeHelper(QObject *parent)
: QObject(parent)
{
}
qreal MediaSizeHelper::contentMaxWidth() const
{
return m_contentMaxWidth;
}
void MediaSizeHelper::setContentMaxWidth(qreal contentMaxWidth)
{
if (contentMaxWidth < 0.0 || qFuzzyCompare(contentMaxWidth, 0.0)) {
m_contentMaxWidth = -1.0;
Q_EMIT contentMaxWidthChanged();
Q_EMIT currentSizeChanged();
return;
}
if (qFuzzyCompare(contentMaxWidth, m_contentMaxWidth)) {
return;
}
m_contentMaxWidth = contentMaxWidth;
Q_EMIT contentMaxWidthChanged();
Q_EMIT currentSizeChanged();
}
qreal MediaSizeHelper::contentMaxHeight() const
{
return m_contentMaxHeight;
}
void MediaSizeHelper::setContentMaxHeight(qreal contentMaxHeight)
{
if (contentMaxHeight < 0.0 || qFuzzyCompare(contentMaxHeight, 0.0)) {
m_contentMaxHeight = -1.0;
Q_EMIT contentMaxHeightChanged();
Q_EMIT currentSizeChanged();
return;
}
if (qFuzzyCompare(contentMaxHeight, m_contentMaxHeight)) {
return;
}
m_contentMaxHeight = contentMaxHeight;
Q_EMIT contentMaxHeightChanged();
Q_EMIT currentSizeChanged();
}
qreal MediaSizeHelper::mediaWidth() const
{
return m_mediaWidth;
}
void MediaSizeHelper::setMediaWidth(qreal mediaWidth)
{
if (mediaWidth < 0.0 || qFuzzyCompare(mediaWidth, 0.0)) {
m_mediaWidth = -1.0;
Q_EMIT mediaWidthChanged();
Q_EMIT currentSizeChanged();
return;
}
if (qFuzzyCompare(mediaWidth, m_mediaWidth)) {
return;
}
m_mediaWidth = mediaWidth;
Q_EMIT mediaWidthChanged();
Q_EMIT currentSizeChanged();
}
qreal MediaSizeHelper::mediaHeight() const
{
return m_mediaHeight;
}
void MediaSizeHelper::setMediaHeight(qreal mediaHeight)
{
if (mediaHeight < 0.0 || qFuzzyCompare(mediaHeight, 0.0)) {
m_mediaHeight = -1.0;
Q_EMIT mediaHeightChanged();
Q_EMIT currentSizeChanged();
return;
}
if (qFuzzyCompare(mediaHeight, m_mediaHeight)) {
return;
}
m_mediaHeight = mediaHeight;
Q_EMIT mediaHeightChanged();
Q_EMIT currentSizeChanged();
}
qreal MediaSizeHelper::resolvedMediaWidth() const
{
if (m_mediaWidth > 0.0) {
return m_mediaWidth;
}
return widthLimit();
}
qreal MediaSizeHelper::resolvedMediaHeight() const
{
if (m_mediaHeight > 0.0) {
return m_mediaHeight;
}
return widthLimit() / 16.0 * 9.0;
}
qreal MediaSizeHelper::aspectRatio() const
{
return resolvedMediaWidth() / resolvedMediaHeight();
}
bool MediaSizeHelper::limitWidth() const
{
// If actual data isn't available we'll be using a placeholder that is width
// limited so return true.
if (m_mediaWidth < 0.0 || m_mediaHeight < 0.0) {
return true;
}
return m_mediaWidth >= m_mediaHeight;
}
qreal MediaSizeHelper::widthLimit() const
{
if (m_contentMaxWidth < 0.0) {
return NeoChatConfig::self()->mediaMaxWidth();
}
return std::min(m_contentMaxWidth, qreal(NeoChatConfig::self()->mediaMaxWidth()));
}
qreal MediaSizeHelper::heightLimit() const
{
if (m_contentMaxHeight < 0.0) {
return NeoChatConfig::self()->mediaMaxHeight();
}
return std::min(m_contentMaxHeight, qreal(NeoChatConfig::self()->mediaMaxHeight()));
}
QSize MediaSizeHelper::currentSize() const
{
if (limitWidth()) {
qreal width = std::min(widthLimit(), resolvedMediaWidth());
qreal height = width / aspectRatio();
if (height > heightLimit()) {
return QSize(qRound(heightLimit() * aspectRatio()), qRound(heightLimit()));
}
return QSize(qRound(width), qRound(height));
} else {
qreal height = std::min(heightLimit(), resolvedMediaHeight());
qreal width = height * aspectRatio();
if (width > widthLimit()) {
return QSize(qRound(widthLimit()), qRound(widthLimit() / aspectRatio()));
}
return QSize(qRound(width), qRound(height));
}
}
#include "moc_mediasizehelper.cpp"

View File

@@ -1,103 +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>
#include <QSize>
/**
* @class MediaSizeHelper
*
* A class to help calculate the current width of a media item within a chat delegate.
*
* The only realistic way to guarantee that a media item (e.g. an image or video)
* is the correct size in QML is to calculate the size manually.
*
* The rules for this component work as follows:
* - The output will always try to keep the media size if no limits are breached.
* - If no media width is set, the current size will be a placeholder at a 16:9 ratio
* calcualated from either the configured max width or the contentMaxWidth, whichever
* is smaller (if the contentMaxWidth isn't set, the configured max width is used).
* - The aspect ratio of the media will always be maintained if set (otherwise 16:9).
* - The current size will never be larger than any of the limits in either direction.
* - If any limit is breached the image size will be reduced while maintaining aspect
* ration, i.e. no stretching or squashing. This can mean that the width or height
* is reduced even if that parameter doesn't breach the limit itself.
*/
class MediaSizeHelper : public QObject
{
Q_OBJECT
/**
* @brief The maximum width (in px) the media can be.
*
* This is the upper limit placed upon the media by the delegate.
*/
Q_PROPERTY(qreal contentMaxWidth READ contentMaxWidth WRITE setContentMaxWidth NOTIFY contentMaxWidthChanged)
/**
* @brief The maximum height (in px) the media can be.
*
* This is the upper limit placed upon the media by the delegate.
*/
Q_PROPERTY(qreal contentMaxHeight READ contentMaxHeight WRITE setContentMaxHeight NOTIFY contentMaxHeightChanged)
/**
* @brief The base width (in px) of the media.
*/
Q_PROPERTY(qreal mediaWidth READ mediaWidth WRITE setMediaWidth NOTIFY mediaWidthChanged)
/**
* @brief The base height (in px) of the media.
*/
Q_PROPERTY(qreal mediaHeight READ mediaHeight WRITE setMediaHeight NOTIFY mediaHeightChanged)
/**
* @brief The size (in px) of the component based on the current input.
*
* Will always try to return a value even if some of the inputs are not set to
* account for being called before the parameters are intialised. For any parameters
* not set these will just be left out of the calcs.
*
* If no input values are provided a default placeholder value will be returned.
*/
Q_PROPERTY(QSize currentSize READ currentSize NOTIFY currentSizeChanged)
public:
explicit MediaSizeHelper(QObject *parent = nullptr);
qreal contentMaxWidth() const;
void setContentMaxWidth(qreal contentMaxWidth);
qreal contentMaxHeight() const;
void setContentMaxHeight(qreal contentMaxHeight);
qreal mediaWidth() const;
void setMediaWidth(qreal mediaWidth);
qreal mediaHeight() const;
void setMediaHeight(qreal mediaHeight);
QSize currentSize() const;
Q_SIGNALS:
void contentMaxWidthChanged();
void contentMaxHeightChanged();
void mediaWidthChanged();
void mediaHeightChanged();
void currentSizeChanged();
private:
qreal m_contentMaxWidth = -1.0;
qreal m_contentMaxHeight = -1.0;
qreal m_mediaWidth = -1.0;
qreal m_mediaHeight = -1.0;
qreal resolvedMediaWidth() const;
qreal resolvedMediaHeight() const;
qreal aspectRatio() const;
bool limitWidth() const;
qreal widthLimit() const;
qreal heightLimit() const;
};

View File

@@ -13,6 +13,8 @@
#include <Quotient/connection.h>
class ImagePacksModel;
/**
* @class AccountEmoticonModel
*

View File

@@ -0,0 +1,177 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// 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::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If this is not a state, show it
|| (source_row < sourceModel()->rowCount() - 1
&& sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State) // If this is the first state in a block, show it. TODO hidden events?
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::ShowSectionRole).toBool(); // If it's a new day, show it
}
QVariant CollapseStateProxyModel::data(const QModelIndex &index, int role) const
{
if (role == AggregateDisplayRole) {
return aggregateEventToString(mapToSource(index).row());
} else if (role == StateEventsRole) {
return stateEventsList(mapToSource(index).row());
} else if (role == AuthorListRole) {
return authorList(mapToSource(index).row());
} else if (role == ExcessAuthorsRole) {
return excessAuthors(mapToSource(index).row());
}
return sourceModel()->data(mapToSource(index), role);
}
QHash<int, QByteArray> CollapseStateProxyModel::roleNames() const
{
auto roles = sourceModel()->roleNames();
roles[AggregateDisplayRole] = "aggregateDisplay";
roles[StateEventsRole] = "stateEvents";
roles[AuthorListRole] = "authorList";
roles[ExcessAuthorsRole] = "excessAuthors";
return roles;
}
QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
{
QStringList parts;
QVariantList uniqueAuthors;
for (int i = sourceRow; i >= 0; i--) {
parts += sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::GenericDisplayRole).toString();
QVariant nextAuthor = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAuthor)) {
uniqueAuthors.append(nextAuthor);
}
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
)) {
break;
}
}
parts.sort(); // Sort them so that all identical events can be collected.
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 && uniqueAuthors.length() == 1) {
chunks.last() += i18ncp("n times", " %1 time ", " %1 times ", count);
}
}
chunks.removeDuplicates();
QString text = QStringLiteral("<style>a {text-decoration: none;}</style>"); // There can be links in the event text so make sure all are styled.
// The author text is either "n users" if > 1 user or the matrix.to link to a single user.
QString userText = uniqueAuthors.length() > 1 ? i18ncp("n users", " %1 user ", " %1 users ", uniqueAuthors.length())
: QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a> ")
.arg(uniqueAuthors[0].toMap()[QStringLiteral("id")].toString(),
uniqueAuthors[0].toMap()[QStringLiteral("color")].toString(),
uniqueAuthors[0].toMap()[QStringLiteral("displayName")].toString().toHtmlEscaped());
text += userText;
text += chunks.takeFirst();
if (chunks.size() > 0) {
while (chunks.size() > 1) {
text += i18nc("[action 1], [action 2 and/or action 3]", ", ");
text += chunks.takeFirst();
}
text += uniqueAuthors.length() > 1 ? i18nc("[action 1, action 2] or [action 3]", " or ") : i18nc("[action 1, action 2] and [action 3]", " and ");
text += chunks.takeFirst();
}
return text;
} else {
return {};
}
}
QVariantList CollapseStateProxyModel::stateEventsList(int sourceRow) const
{
QVariantList stateEvents;
for (int i = sourceRow; i >= 0; i--) {
auto nextState = QVariantMap{
{QStringLiteral("author"), sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole)},
{QStringLiteral("authorDisplayName"), sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorDisplayNameRole).toString()},
{QStringLiteral("text"), sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()},
};
stateEvents.append(nextState);
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
)) {
break;
}
}
return stateEvents;
}
QVariantList CollapseStateProxyModel::authorList(int sourceRow) const
{
QVariantList uniqueAuthors;
for (int i = sourceRow; i >= 0; i--) {
QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAvatar)) {
uniqueAuthors.append(nextAvatar);
}
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
)) {
break;
}
}
if (uniqueAuthors.count() > 5) {
uniqueAuthors = uniqueAuthors.mid(0, 5);
}
return uniqueAuthors;
}
QString CollapseStateProxyModel::excessAuthors(int row) const
{
QVariantList uniqueAuthors;
for (int i = row; i >= 0; i--) {
QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAvatar)) {
uniqueAuthors.append(nextAvatar);
}
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
)) {
break;
}
}
int excessAuthors;
if (uniqueAuthors.count() > 5) {
excessAuthors = uniqueAuthors.count() - 5;
} else {
excessAuthors = 0;
}
QString excessAuthorsString;
if (excessAuthors == 0) {
return QString();
} else {
return QStringLiteral("+ %1").arg(excessAuthors);
}
}
#include "moc_collapsestateproxymodel.cpp"

View File

@@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include "messageeventmodel.h"
#include <QSortFilterProxyModel>
/**
* @class CollapseStateProxyModel
*
* This model aggregates multiple sequential state events into a single entry.
*
* Events are only aggregated if they happened on the same day.
*
* @sa MessageEventModel
*/
class CollapseStateProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
AggregateDisplayRole = MessageEventModel::LastRole + 1, /**< Single line aggregation of all the state events. */
StateEventsRole, /**< List of state events in the aggregated state. */
AuthorListRole, /**< List of the first 5 unique authors of the aggregated state event. */
ExcessAuthorsRole, /**< The number of unique authors beyond the first 5. */
LastRole, // Keep this last
};
/**
* @brief Whether a row should be shown out or not.
*
* @sa QSortFilterProxyModel::filterAcceptsRow
*/
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
/**
* @brief Get the given role value at the given index.
*
* @sa QSortFilterProxyModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractProxyModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
/**
* @brief Aggregation of the text of consecutive state events starting at row.
*
* If state events happen on different days they will be split into two aggregate
* events.
*/
[[nodiscard]] QString aggregateEventToString(int row) const;
/**
* @brief Return a list of consecutive state events starting at row.
*
* If state events happen on different days they will be split into two aggregate
* events.
*/
[[nodiscard]] QVariantList stateEventsList(int row) const;
/**
* @brief List of the first 5 unique authors for the aggregate state events starting at row.
*/
[[nodiscard]] QVariantList authorList(int row) const;
/**
* @brief The number of unique authors beyond the first 5 for the aggregate state events starting at row.
*/
[[nodiscard]] QString excessAuthors(int row) const;
};

View File

@@ -145,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().size() > 1 && !text()[1].isUpper()
} else if (text().startsWith(QLatin1Char(':')) && !text()[1].isUpper()
&& (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

@@ -22,7 +22,11 @@ bool CompletionProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &so
&& sourceModel()
->data(sourceModel()->index(sourceRow, 0), secondaryFilterRole())
.toString()
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
.startsWith(QStringView(m_filterText).sliced(1), Qt::CaseInsensitive));
#else
.startsWith(m_filterText.midRef(1), Qt::CaseInsensitive));
#endif
}
bool CompletionProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const

View File

@@ -4,7 +4,6 @@
#include "devicesmodel.h"
#include "controller.h"
#include "jobs/neochatdeletedevicejob.h"
#include <QDateTime>
#include <QLocale>

View File

@@ -10,6 +10,11 @@
#include <QPointer>
#include <QRectF>
namespace Quotient
{
class RoomMessageEvent;
}
struct LiveLocationData {
QString eventId;
QString senderId;

View File

@@ -2,26 +2,20 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
#include "mediamessagefiltermodel.h"
#include "models/messageeventmodel.h"
#include <Quotient/room.h>
#include "enums/delegatetype.h"
#include "messageeventmodel.h"
#include "messagefiltermodel.h"
MediaMessageFilterModel::MediaMessageFilterModel(QObject *parent, MessageFilterModel *sourceMediaModel)
MediaMessageFilterModel::MediaMessageFilterModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
Q_ASSERT(sourceMediaModel);
setSourceModel(sourceMediaModel);
}
bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
if (index.data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image
|| index.data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Video) {
if (index.data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image
|| index.data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Video) {
return true;
}
return false;
@@ -30,9 +24,9 @@ bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex
QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
{
if (role == SourceRole) {
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image) {
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("source")].toUrl();
} else if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Video) {
} else if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Video) {
auto progressInfo = mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>();
if (progressInfo.completed()) {
@@ -48,10 +42,10 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("tempInfo")].toMap()[QStringLiteral("source")].toUrl();
}
if (role == TypeRole) {
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image) {
return MediaType::Image;
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image) {
return 0;
} else {
return MediaType::Video;
return 1;
}
}
if (role == CaptionRole) {
@@ -63,13 +57,6 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
if (role == SourceHeightRole) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("height")].toFloat();
}
// We need to catch this one and return true if the next media object was
// on a different day.
if (role == MessageEventModel::ShowSectionRole) {
const auto day = mapToSource(index).data(MessageEventModel::TimeRole).toDateTime().toLocalTime().date();
const auto previousEventDay = mapToSource(this->index(index.row() + 1, 0)).data(MessageEventModel::TimeRole).toDateTime().toLocalTime().date();
return day != previousEventDay;
}
return sourceModel()->data(mapToSource(index), role);
}

View File

@@ -4,11 +4,8 @@
#pragma once
#include <QSortFilterProxyModel>
#include <qobjectdefs.h>
#include "models/messagefiltermodel.h"
class MessageFilterModel;
#include "models/collapsestateproxymodel.h"
/**
* @class MediaMessageFilterModel
@@ -21,17 +18,11 @@ class MediaMessageFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
enum MediaType {
Image = 0,
Video,
};
Q_ENUM(MediaType)
/**
* @brief Defines the model roles.
*/
enum Roles {
SourceRole = MessageFilterModel::LastRole + 1, /**< The mxc source URL for the item. */
SourceRole = CollapseStateProxyModel::LastRole + 1, /**< The mxc source URL for the item. */
TempSourceRole, /**< Source for the temporary content (either blurhash or mxc URL). */
TypeRole, /**< The type of the media (image or video). */
CaptionRole, /**< The caption for the item. */
@@ -40,7 +31,7 @@ public:
};
Q_ENUM(Roles)
explicit MediaMessageFilterModel(QObject *parent = nullptr, MessageFilterModel *sourceMediaModel = nullptr);
explicit MediaMessageFilterModel(QObject *parent = nullptr);
/**
* @brief Custom filter to show only image and video messages.
@@ -52,7 +43,7 @@ public:
*
* @sa QSortFilterProxyModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief Returns a mapping from Role enum values to role names.

View File

@@ -8,19 +8,25 @@
#include <Quotient/connection.h>
#include <Quotient/csapi/rooms.h>
#include <Quotient/events/reactionevent.h>
#include <Quotient/events/redactionevent.h>
#include <Quotient/events/stickerevent.h>
#include <Quotient/events/roomavatarevent.h>
#include <Quotient/events/roommemberevent.h>
#include <Quotient/events/simplestateevents.h>
#include <Quotient/user.h>
#include "events/pollevent.h"
#include <Quotient/events/stickerevent.h>
#include <QDebug>
#include <QGuiApplication>
#include <QMovie>
#include <QTimeZone>
#include <KLocalizedString>
#include "enums/delegatetype.h"
#include "eventhandler.h"
#include "models/reactionmodel.h"
#include "texthandler.h"
using namespace Quotient;
@@ -31,7 +37,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[PlainText] = "plainText";
roles[EventIdRole] = "eventId";
roles[TimeRole] = "time";
roles[TimeStringRole] = "timeString";
roles[SectionRole] = "section";
roles[AuthorRole] = "author";
roles[ContentRole] = "content";
@@ -43,9 +48,8 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[MediaInfoRole] = "mediaInfo";
roles[IsReplyRole] = "isReply";
roles[ReplyAuthor] = "replyAuthor";
roles[ReplyRole] = "reply";
roles[ReplyIdRole] = "replyId";
roles[ReplyDelegateTypeRole] = "replyDelegateType";
roles[ReplyDisplayRole] = "replyDisplay";
roles[ReplyMediaInfoRole] = "replyMediaInfo";
roles[ShowAuthorRole] = "showAuthor";
roles[ShowSectionRole] = "showSection";
@@ -56,8 +60,10 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[ReactionRole] = "reaction";
roles[ShowReactionsRole] = "showReactions";
roles[SourceRole] = "jsonSource";
roles[MimeTypeRole] = "mimeType";
roles[AuthorIdRole] = "authorId";
roles[VerifiedRole] = "verified";
roles[DisplayNameForInitialsRole] = "displayNameForInitials";
roles[AuthorDisplayNameRole] = "authorDisplayName";
roles[IsRedactedRole] = "isRedacted";
roles[GenericDisplayRole] = "genericDisplay";
@@ -91,6 +97,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
if (m_currentRoom) {
m_currentRoom->disconnect(this);
m_linkPreviewers.clear();
qDeleteAll(m_reactionModels);
m_reactionModels.clear();
}
@@ -100,7 +107,10 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
room->setDisplayed();
for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) {
createEventObjects(&*event->viewAs<RoomMessageEvent>());
if (auto e = &*event->viewAs<RoomMessageEvent>()) {
createLinkPreviewerForEvent(e);
createReactionModelForEvent(e);
}
}
if (m_currentRoom->timelineSize() < 10 && !room->allHistoryLoaded()) {
@@ -113,16 +123,16 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
if (row == -1) {
return;
}
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyDelegateTypeRole, ReplyDisplayRole, ReplyMediaInfoRole, ReplyAuthor});
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyRole, ReplyMediaInfoRole, ReplyAuthor});
});
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
for (auto &&event : events) {
const RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
createEventObjects(message);
if (message != nullptr) {
createLinkPreviewerForEvent(message);
createReactionModelForEvent(message);
if (NeoChatConfig::self()->showFancyEffects()) {
QString planBody = message->plainBody();
// snowflake
@@ -159,7 +169,10 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
for (auto &event : events) {
RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
createEventObjects(message);
if (message) {
createLinkPreviewerForEvent(message);
createReactionModelForEvent(message);
}
}
if (rowCount() > 0) {
rowBelowInserted = rowCount() - 1; // See #312
@@ -228,7 +241,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
}
const auto eventIt = m_currentRoom->findInTimeline(eventId);
if (eventIt != m_currentRoom->historyEdge()) {
createEventObjects(static_cast<const RoomMessageEvent *>(&**eventIt));
createReactionModelForEvent(static_cast<const RoomMessageEvent *>(&**eventIt));
}
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole, Qt::DisplayRole});
});
@@ -327,7 +340,7 @@ int MessageEventModel::refreshEventRoles(const QString &id, const QVector<int> &
return -1;
}
row = int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex();
if (data(index(row, 0), DelegateTypeRole).toInt() == DelegateType::ReadMarker || data(index(row, 0), DelegateTypeRole).toInt() == DelegateType::Other) {
if (data(index(row, 0), DelegateTypeRole).toInt() == ReadMarker || data(index(row, 0), DelegateTypeRole).toInt() == Other) {
row++;
}
}
@@ -445,10 +458,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
const auto pendingIt = m_currentRoom->pendingEvents().crbegin() + std::min(row, timelineBaseIndex());
const auto &evt = isPending ? **pendingIt : **timelineIt;
EventHandler eventHandler;
eventHandler.setRoom(m_currentRoom);
eventHandler.setEvent(&evt);
if (role == Qt::DisplayRole) {
if (evt.isRedacted()) {
auto reason = evt.redactedBecause()->reason();
@@ -456,15 +465,19 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
: i18n("<i>[This message was deleted: %1]</i>", evt.redactedBecause()->reason());
}
return eventHandler.getRichBody();
return m_currentRoom->eventToString(evt, Qt::RichText);
}
if (role == GenericDisplayRole) {
return eventHandler.getGenericBody();
if (evt.isRedacted()) {
return i18n("<i>[This message was deleted]</i>");
}
return m_currentRoom->eventToGenericString(evt);
}
if (role == PlainText) {
return eventHandler.getPlainBody();
return m_currentRoom->eventToString(evt);
}
if (role == SourceRole) {
@@ -472,11 +485,54 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == DelegateTypeRole) {
return eventHandler.getDelegateType();
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
switch (e->msgtype()) {
case MessageEventType::Emote:
return DelegateType::Emote;
case MessageEventType::Notice:
return DelegateType::Notice;
case MessageEventType::Image:
return DelegateType::Image;
case MessageEventType::Audio:
return DelegateType::Audio;
case MessageEventType::Video:
return DelegateType::Video;
case MessageEventType::Location:
return DelegateType::Location;
default:
break;
}
if (e->hasFileContent()) {
return DelegateType::File;
}
return DelegateType::Message;
}
if (is<const StickerEvent>(evt)) {
return DelegateType::Sticker;
}
if (evt.isStateEvent()) {
if (evt.matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
return DelegateType::LiveLocation;
}
return DelegateType::State;
}
if (is<const EncryptedEvent>(evt)) {
return DelegateType::Encrypted;
}
if (is<PollStartEvent>(evt)) {
if (evt.isRedacted()) {
return DelegateType::Message;
}
return DelegateType::Poll;
}
return DelegateType::Other;
}
if (role == AuthorRole) {
return eventHandler.getAuthor(isPending);
auto author = isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId());
return m_currentRoom->getUser(author);
}
if (role == ContentRole) {
@@ -502,7 +558,21 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == HighlightRole) {
return eventHandler.isHighlighted();
return !m_currentRoom->isDirectChat() && m_currentRoom->isEventHighlighted(&evt);
}
if (role == MimeTypeRole) {
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if (!e || !e->hasFileContent()) {
return QVariant();
}
return e->content()->fileInfo()->mimeType.name();
}
if (auto e = eventCast<const StickerEvent>(&evt)) {
return e->image().mimeType.name();
}
}
if (role == SpecialMarksRole) {
@@ -515,7 +585,46 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return pendingIt->deliveryStatus();
}
if (eventHandler.isHidden()) {
if (evt.isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
return EventStatus::Hidden;
}
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(&evt)) {
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
return EventStatus::Hidden;
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
return EventStatus::Hidden;
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
&& !NeoChatConfig::self()->showAvatarUpdate()) {
return EventStatus::Hidden;
}
}
// isReplacement?
if (auto e = eventCast<const RoomMessageEvent>(&evt))
if (!e->replacedEvent().isEmpty())
return EventStatus::Hidden;
if (is<RedactionEvent>(evt) || is<ReactionEvent>(evt)) {
return EventStatus::Hidden;
}
if (evt.isStateEvent() && static_cast<const StateEvent &>(evt).repeatsState()) {
return EventStatus::Hidden;
}
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if (!e->replacedEvent().isEmpty() && e->replacedEvent() != e->id()) {
return EventStatus::Hidden;
}
}
if (m_currentRoom->connection()->isIgnored(m_currentRoom->user(evt.senderId()))) {
return EventStatus::Hidden;
}
// hide ending live location beacons
if (evt.isStateEvent() && evt.matrixType() == "org.matrix.msc3672.beacon_info"_ls && !evt.contentJson()["live"_ls].toBool()) {
return EventStatus::Hidden;
}
@@ -523,7 +632,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == EventIdRole) {
return eventHandler.getId();
return !evt.id().isEmpty() ? evt.id() : evt.transactionId();
}
if (role == ProgressInfoRole) {
@@ -537,19 +646,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
}
if (role == TimeRole) {
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
return eventHandler.getTime(isPending, lastUpdated);
}
if (role == TimeStringRole) {
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
return eventHandler.getTimeString(false, QLocale::ShortFormat, isPending, lastUpdated);
}
if (role == SectionRole) {
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
return eventHandler.getTimeString(true, QLocale::ShortFormat, isPending, lastUpdated);
if (role == TimeRole || role == SectionRole) {
auto ts = isPending ? pendingIt->lastUpdated() : makeMessageTimestamp(timelineIt);
return role == TimeRole ? QVariant(ts) : m_format.formatRelativeDate(ts.toLocalTime().date(), QLocale::ShortFormat);
}
if (role == ShowLinkPreviewRole) {
@@ -558,38 +657,85 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
if (role == LinkPreviewRole) {
if (m_linkPreviewers.contains(evt.id())) {
return QVariant::fromValue<LinkPreviewer *>(m_linkPreviewers[evt.id()].data());
return QVariant::fromValue<LinkPreviewer *>(m_linkPreviewers[evt.id()]);
} else {
return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview);
}
}
if (role == MediaInfoRole) {
return eventHandler.getMediaInfo();
return getMediaInfoForEvent(evt);
}
if (role == IsReplyRole) {
return eventHandler.hasReply();
return !evt.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
}
if (role == ReplyIdRole) {
return eventHandler.getReplyId();
}
if (role == ReplyDelegateTypeRole) {
return eventHandler.getReplyDelegateType();
return evt.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
}
if (role == ReplyAuthor) {
return eventHandler.getReplyAuthor();
}
auto replyPtr = m_currentRoom->getReplyForEvent(evt);
if (role == ReplyDisplayRole) {
return eventHandler.getReplyRichBody();
if (replyPtr) {
auto replyUser = m_currentRoom->user(replyPtr->senderId());
return m_currentRoom->getUser(replyUser);
} else {
return m_currentRoom->getUser(nullptr);
}
}
if (role == ReplyMediaInfoRole) {
return eventHandler.getReplyMediaInfo();
auto replyPtr = m_currentRoom->getReplyForEvent(evt);
if (!replyPtr) {
return {};
}
return getMediaInfoForEvent(*replyPtr);
}
if (role == ReplyRole) {
auto replyPtr = m_currentRoom->getReplyForEvent(evt);
if (!replyPtr) {
return {};
}
DelegateType type;
if (auto e = eventCast<const RoomMessageEvent>(replyPtr)) {
switch (e->msgtype()) {
case MessageEventType::Emote:
type = DelegateType::Emote;
break;
case MessageEventType::Notice:
type = DelegateType::Notice;
break;
case MessageEventType::Image:
type = DelegateType::Image;
break;
case MessageEventType::Audio:
type = DelegateType::Audio;
break;
case MessageEventType::Video:
type = DelegateType::Video;
break;
default:
if (e->hasFileContent()) {
type = DelegateType::File;
break;
}
type = DelegateType::Message;
}
} else if (is<const StickerEvent>(*replyPtr)) {
type = DelegateType::Sticker;
} else {
type = DelegateType::Other;
}
return QVariantMap{
{"display"_ls, m_currentRoom->eventToString(*replyPtr, Qt::RichText)},
{"type"_ls, type},
};
}
if (role == ShowAuthorRole) {
@@ -599,7 +745,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
// While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index.
// See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows
if (data(i, SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) {
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, DelegateTypeRole) == DelegateType::State
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, DelegateTypeRole) == MessageEventModel::State
|| data(i, TimeRole).toDateTime().msecsTo(data(idx, TimeRole).toDateTime()) > 600000
|| data(i, TimeRole).toDateTime().toLocalTime().date().day() != data(idx, TimeRole).toDateTime().toLocalTime().date().day();
}
@@ -625,36 +771,87 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == LatitudeRole) {
return eventHandler.getLatitude();
const auto geoUri = evt.contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) {
return {};
}
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[0];
return latitude.toFloat();
}
if (role == LongitudeRole) {
return eventHandler.getLongitude();
const auto geoUri = evt.contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) {
return {};
}
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[1];
return latitude.toFloat();
}
if (role == AssetRole) {
return eventHandler.getLocationAssetType();
const auto assetType = evt.contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
if (assetType.isEmpty()) {
return {};
}
return assetType;
}
if (role == ReadMarkersRole) {
return eventHandler.getReadMarkers();
auto userIds_temp = room()->userIdsAtEvent(evt.id());
userIds_temp.remove(m_currentRoom->localUser()->id());
auto userIds = userIds_temp.values();
if (userIds.count() > 5) {
userIds = userIds.mid(0, 5);
}
QVariantList users;
users.reserve(userIds.size());
for (const auto &userId : userIds) {
auto user = m_currentRoom->user(userId);
users += m_currentRoom->getUser(user);
}
return users;
}
if (role == ExcessReadMarkersRole) {
return eventHandler.getNumberExcessReadMarkers();
auto userIds = room()->userIdsAtEvent(evt.id());
userIds.remove(m_currentRoom->localUser()->id());
if (userIds.count() > 5) {
return QStringLiteral("+ %1").arg(userIds.count() - 5);
} else {
return QString();
}
}
if (role == ReadMarkersStringRole) {
return eventHandler.getReadMarkersString();
auto userIds = room()->userIdsAtEvent(evt.id());
userIds.remove(m_currentRoom->localUser()->id());
/**
* The string ends up in the form
* "x users: user1DisplayName, user2DisplayName, etc."
*/
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
for (const auto &userId : userIds) {
auto user = m_currentRoom->user(userId);
readMarkersString += user->displayname(m_currentRoom) + i18nc("list separator", ", ");
}
readMarkersString.chop(2);
return readMarkersString;
}
if (role == ShowReadMarkersRole) {
return eventHandler.hasReadMarkers();
auto userIds = room()->userIdsAtEvent(evt.id());
userIds.remove(m_currentRoom->localUser()->id());
return userIds.size() > 0;
}
if (role == ReactionRole) {
if (m_reactionModels.contains(evt.id())) {
return QVariant::fromValue<ReactionModel *>(m_reactionModels[evt.id()].data());
return QVariant::fromValue<ReactionModel *>(m_reactionModels[evt.id()]);
} else {
return QVariantList();
}
@@ -677,8 +874,22 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return false;
}
if (role == DisplayNameForInitialsRole) {
auto user = isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId());
return user->displayname(m_currentRoom).remove(QStringLiteral(" (%1)").arg(user->id()));
}
if (role == AuthorDisplayNameRole) {
return eventHandler.getAuthorDisplayName(isPending);
if (is<RoomMemberEvent>(evt) && !evt.unsignedJson()["prev_content"_ls]["displayname"_ls].isNull() && evt.stateKey() == evt.senderId()) {
auto previousDisplayName = evt.unsignedJson()["prev_content"_ls]["displayname"_ls].toString().toHtmlEscaped();
if (previousDisplayName.isEmpty()) {
previousDisplayName = evt.senderId();
}
return previousDisplayName;
} else {
auto author = isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId());
return m_currentRoom->htmlSafeMemberName(author->id());
}
}
if (role == IsRedactedRole) {
@@ -702,27 +913,195 @@ int MessageEventModel::eventIdToRow(const QString &eventID) const
return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex();
}
void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *event)
QVariantMap MessageEventModel::getMediaInfoForEvent(const RoomEvent &event) const
{
QVariantMap mediaInfo;
QString eventId = event.id();
// Get the file info for the event.
const EventContent::FileInfo *fileInfo;
if (event.is<RoomMessageEvent>()) {
auto roomMessageEvent = eventCast<const RoomMessageEvent>(&event);
if (!roomMessageEvent->hasFileContent()) {
return {};
}
fileInfo = roomMessageEvent->content()->fileInfo();
} else if (event.is<StickerEvent>()) {
auto stickerEvent = eventCast<const StickerEvent>(&event);
fileInfo = &stickerEvent->image();
} else {
return {};
}
return getMediaInfoFromFileInfo(fileInfo, eventId);
}
QVariantMap MessageEventModel::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail) const
{
QVariantMap mediaInfo;
// Get the mxc URL for the media.
if (!fileInfo->url().isValid() || eventId.isEmpty()) {
mediaInfo["source"_ls] = QUrl();
} else {
QUrl source = m_currentRoom->makeMediaUrl(eventId, fileInfo->url());
if (source.isValid() && source.scheme() == QStringLiteral("mxc")) {
mediaInfo["source"_ls] = source;
} else {
mediaInfo["source"_ls] = QUrl();
}
}
auto mimeType = fileInfo->mimeType;
// Add the MIME type for the media if available.
mediaInfo["mimeType"_ls] = mimeType.name();
// Add the MIME type icon if available.
mediaInfo["mimeIcon"_ls] = mimeType.iconName();
// Add media size if available.
mediaInfo["size"_ls] = fileInfo->payloadSize;
// Add parameter depending on media type.
if (mimeType.name().contains(QStringLiteral("image"))) {
if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileInfo)) {
mediaInfo["width"_ls] = castInfo->imageSize.width();
mediaInfo["height"_ls] = castInfo->imageSize.height();
// TODO: Images in certain formats (e.g. WebP) will be erroneously marked as animated, even if they are static.
mediaInfo["animated"_ls] = QMovie::supportedFormats().contains(mimeType.preferredSuffix().toUtf8());
if (!isThumbnail) {
QVariantMap tempInfo;
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
tempInfo = thumbnailInfo;
} else {
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
if (blurhash.isEmpty()) {
tempInfo["source"_ls] = QUrl();
} else {
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
}
}
mediaInfo["tempInfo"_ls] = tempInfo;
}
}
}
if (mimeType.name().contains(QStringLiteral("video"))) {
if (auto castInfo = static_cast<const EventContent::VideoContent *>(fileInfo)) {
mediaInfo["width"_ls] = castInfo->imageSize.width();
mediaInfo["height"_ls] = castInfo->imageSize.height();
mediaInfo["duration"_ls] = castInfo->duration;
if (!isThumbnail) {
QVariantMap tempInfo;
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
tempInfo = thumbnailInfo;
} else {
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
if (blurhash.isEmpty()) {
tempInfo["source"_ls] = QUrl();
} else {
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
}
}
mediaInfo["tempInfo"_ls] = tempInfo;
}
}
}
if (mimeType.name().contains(QStringLiteral("audio"))) {
if (auto castInfo = static_cast<const EventContent::AudioContent *>(fileInfo)) {
mediaInfo["duration"_ls] = castInfo->duration;
}
}
return mediaInfo;
}
void MessageEventModel::createLinkPreviewerForEvent(const Quotient::RoomMessageEvent *event)
{
if (m_linkPreviewers.contains(event->id())) {
return;
} else {
QString text;
if (event->hasTextContent()) {
auto textContent = static_cast<const EventContent::TextContent *>(event->content());
if (textContent) {
text = textContent->body;
} else {
text = event->plainBody();
}
} else {
text = event->plainBody();
}
TextHandler textHandler;
textHandler.setData(text);
QList<QUrl> links = textHandler.getLinkPreviews();
if (links.size() > 0) {
m_linkPreviewers[event->id()] = new LinkPreviewer(nullptr, m_currentRoom, links.size() > 0 ? links[0] : QUrl());
}
}
}
void MessageEventModel::createReactionModelForEvent(const Quotient::RoomMessageEvent *event)
{
if (event == nullptr) {
return;
}
auto eventId = event->id();
const auto &annotations = m_currentRoom->relatedEvents(eventId, EventRelation::AnnotationType);
if (annotations.isEmpty()) {
if (m_reactionModels.contains(eventId)) {
delete m_reactionModels[eventId];
m_reactionModels.remove(eventId);
}
return;
};
EventHandler eventHandler;
eventHandler.setRoom(m_currentRoom);
eventHandler.setEvent(event);
if (auto linkPreviewer = eventHandler.getLinkPreviewer()) {
m_linkPreviewers[eventId] = linkPreviewer;
} else {
m_linkPreviewers.remove(eventId);
QMap<QString, QList<Quotient::User *>> reactions = {};
for (const auto &a : annotations) {
if (a->isRedacted()) { // Just in case?
continue;
}
if (const auto &e = eventCast<const ReactionEvent>(a)) {
reactions[e->key()].append(m_currentRoom->user(e->senderId()));
}
}
if (auto reactionModel = eventHandler.getReactions()) {
m_reactionModels[eventId] = reactionModel;
if (reactions.isEmpty()) {
if (m_reactionModels.contains(eventId)) {
delete m_reactionModels[eventId];
m_reactionModels.remove(eventId);
}
return;
}
QList<ReactionModel::Reaction> res;
auto i = reactions.constBegin();
while (i != reactions.constEnd()) {
QVariantList authors;
for (const auto &author : i.value()) {
authors.append(m_currentRoom->getUser(author));
}
res.append(ReactionModel::Reaction{i.key(), authors});
++i;
}
if (m_reactionModels.contains(eventId)) {
m_reactionModels[eventId]->setReactions(res);
} else if (res.size() > 0) {
m_reactionModels[eventId] = new ReactionModel(this, res, m_currentRoom->localUser());
} else {
m_reactionModels.remove(eventId);
if (m_reactionModels.contains(eventId)) {
delete m_reactionModels[eventId];
m_reactionModels.remove(eventId);
}
}
}

View File

@@ -32,6 +32,32 @@ class MessageEventModel : public QAbstractListModel
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
public:
/**
* @brief The type of delegate that is needed for the event.
*
* @note While similar this is not the matrix event or message type. This is
* to tell a QML ListView what delegate to show for each event. So while
* similar to the spec it is not the same.
*/
enum DelegateType {
Emote, /**< A message that begins with /me. */
Notice, /**< A notice event. */
Image, /**< A message that is an image. */
Audio, /**< A message that is an audio recording. */
Video, /**< A message that is a video. */
File, /**< A message that is a file. */
Message, /**< A text message. */
Sticker, /**< A message that is a sticker. */
State, /**< A state event in the room. */
Encrypted, /**< An encrypted message that cannot be decrypted. */
ReadMarker, /**< The local user read marker. */
Poll, /**< The initial event for a poll. */
Location, /**< A location event. */
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
Other, /**< Anything that cannot be classified as another type. */
};
Q_ENUM(DelegateType)
/**
* @brief Defines the model roles.
*/
@@ -39,8 +65,7 @@ public:
DelegateTypeRole = Qt::UserRole + 1, /**< The delegate type of the message. */
PlainText, /**< Plain text representation of the message. */
EventIdRole, /**< The matrix event ID of the event. */
TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */
TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */
TimeRole, /**< The timestamp for when the event was sent. */
SectionRole, /**< The date of the event as a string. */
AuthorRole, /**< The author of the event. */
ContentRole, /**< The full message content. */
@@ -53,13 +78,13 @@ public:
LinkPreviewRole, /**< The link preview details. */
MediaInfoRole, /**< The media info for the event. */
MimeTypeRole, /**< The mime type of the message's file or media. */
IsReplyRole, /**< Is the message a reply to another event. */
ReplyAuthor, /**< The author of the event that was replied to. */
ReplyIdRole, /**< The matrix ID of the message that was replied to. */
ReplyDelegateTypeRole, /**< The delegate type of the message that was replied to. */
ReplyDisplayRole, /**< The body of the message that was replied to. */
ReplyMediaInfoRole, /**< The media info of the message that was replied to. */
ReplyRole, /**< The content data of the message that was replied to. */
ShowAuthorRole, /**< Whether the author's name should be shown. */
ShowSectionRole, /**< Whether the section header should be shown. */
@@ -75,6 +100,7 @@ public:
AuthorIdRole, /**< Matrix ID of the message author. */
VerifiedRole, /**< Whether an encrypted message is sent in a verified session. */
DisplayNameForInitialsRole, /**< Sender's displayname, always without the matrix id. */
AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */
IsRedactedRole, /**< Whether an event has been deleted. */
IsPendingRole, /**< Whether an event is waiting to be accepted by the server. */
@@ -128,8 +154,8 @@ private:
bool movingEvent = false;
KFormat m_format;
QMap<QString, QSharedPointer<LinkPreviewer>> m_linkPreviewers;
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
QMap<QString, LinkPreviewer *> m_linkPreviewers;
QMap<QString, ReactionModel *> m_reactionModels;
[[nodiscard]] int timelineBaseIndex() const;
[[nodiscard]] QDateTime makeMessageTimestamp(const Quotient::Room::rev_iter_t &baseIt) const;
@@ -142,7 +168,10 @@ private:
int refreshEventRoles(const QString &eventId, const QVector<int> &roles = {});
void moveReadMarker(const QString &toEventId);
void createEventObjects(const Quotient::RoomMessageEvent *event);
QVariantMap getMediaInfoForEvent(const Quotient::RoomEvent &event) const;
QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const;
void createLinkPreviewerForEvent(const Quotient::RoomMessageEvent *event);
void createReactionModelForEvent(const Quotient::RoomMessageEvent *event);
// Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows
bool m_initialized = false;

View File

@@ -3,20 +3,14 @@
#include "messagefiltermodel.h"
#include <KLocalizedString>
#include "enums/delegatetype.h"
#include "messageeventmodel.h"
#include "neochatconfig.h"
using namespace Quotient;
MessageFilterModel::MessageFilterModel(QObject *parent, MessageEventModel *sourceMessageModel)
MessageFilterModel::MessageFilterModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
Q_ASSERT(sourceMessageModel);
setSourceModel(sourceMessageModel);
connect(NeoChatConfig::self(), &NeoChatConfig::ShowStateEventChanged, this, [this] {
invalidateFilter();
});
@@ -38,189 +32,22 @@ bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour
{
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
// Don't show redacted (i.e. deleted) messages.
if (index.data(MessageEventModel::IsRedactedRole).toBool() && !NeoChatConfig::self()->showDeletedMessages()) {
return false;
}
// Don't show hidden or replaced messages.
const int specialMarks = index.data(MessageEventModel::SpecialMarksRole).toInt();
if (specialMarks == EventStatus::Hidden || specialMarks == EventStatus::Replaced) {
return false;
}
// Don't show events with an unknown type.
const auto eventType = index.data(MessageEventModel::DelegateTypeRole).toInt();
if (eventType == DelegateType::Other) {
return false;
}
// Don't show state events that are not the first in a consecutive group on the
// same day as they will be grouped as a single delegate.
const bool notLastRow = sourceRow < sourceModel()->rowCount() - 1;
const bool previousEventIsState = notLastRow
? sourceModel()->data(sourceModel()->index(sourceRow + 1, 0), MessageEventModel::DelegateTypeRole) == DelegateType::State
: false;
const bool newDay = sourceModel()->data(sourceModel()->index(sourceRow, 0), MessageEventModel::ShowSectionRole).toBool();
if (eventType == DelegateType::State && notLastRow && previousEventIsState && !newDay) {
if (eventType == MessageEventModel::Other) {
return false;
}
return true;
}
QVariant MessageFilterModel::data(const QModelIndex &index, int role) const
{
if (role == AggregateDisplayRole) {
return aggregateEventToString(mapToSource(index).row());
} else if (role == StateEventsRole) {
return stateEventsList(mapToSource(index).row());
} else if (role == AuthorListRole) {
return authorList(mapToSource(index).row());
} else if (role == ExcessAuthorsRole) {
return excessAuthors(mapToSource(index).row());
}
return sourceModel()->data(mapToSource(index), role);
}
QHash<int, QByteArray> MessageFilterModel::roleNames() const
{
auto roles = sourceModel() ? sourceModel()->roleNames() : QHash<int, QByteArray>();
roles[AggregateDisplayRole] = "aggregateDisplay";
roles[StateEventsRole] = "stateEvents";
roles[AuthorListRole] = "authorList";
roles[ExcessAuthorsRole] = "excessAuthors";
return roles;
}
QString MessageFilterModel::aggregateEventToString(int sourceRow) const
{
QStringList parts;
QVariantList uniqueAuthors;
for (int i = sourceRow; i >= 0; i--) {
parts += sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::GenericDisplayRole).toString();
QVariant nextAuthor = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAuthor)) {
uniqueAuthors.append(nextAuthor);
}
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
)) {
break;
}
}
parts.sort(); // Sort them so that all identical events can be collected.
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 && uniqueAuthors.length() == 1) {
chunks.last() += i18ncp("n times", " %1 time ", " %1 times ", count);
}
}
chunks.removeDuplicates();
QString text = QStringLiteral("<style>a {text-decoration: none;}</style>"); // There can be links in the event text so make sure all are styled.
// The author text is either "n users" if > 1 user or the matrix.to link to a single user.
QString userText = uniqueAuthors.length() > 1 ? i18ncp("n users", " %1 user ", " %1 users ", uniqueAuthors.length())
: QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a> ")
.arg(uniqueAuthors[0].toMap()[QStringLiteral("id")].toString(),
uniqueAuthors[0].toMap()[QStringLiteral("color")].toString(),
uniqueAuthors[0].toMap()[QStringLiteral("displayName")].toString().toHtmlEscaped());
text += userText;
text += chunks.takeFirst();
if (chunks.size() > 0) {
while (chunks.size() > 1) {
text += i18nc("[action 1], [action 2 and/or action 3]", ", ");
text += chunks.takeFirst();
}
text += uniqueAuthors.length() > 1 ? i18nc("[action 1, action 2] or [action 3]", " or ") : i18nc("[action 1, action 2] and [action 3]", " and ");
text += chunks.takeFirst();
}
return text;
} else {
return {};
}
}
QVariantList MessageFilterModel::stateEventsList(int sourceRow) const
{
QVariantList stateEvents;
for (int i = sourceRow; i >= 0; i--) {
auto nextState = QVariantMap{
{QStringLiteral("author"), sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole)},
{QStringLiteral("authorDisplayName"), sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorDisplayNameRole).toString()},
{QStringLiteral("text"), sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()},
};
stateEvents.append(nextState);
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
)) {
break;
}
}
return stateEvents;
}
QVariantList MessageFilterModel::authorList(int sourceRow) const
{
QVariantList uniqueAuthors;
for (int i = sourceRow; i >= 0; i--) {
QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAvatar)) {
uniqueAuthors.append(nextAvatar);
}
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
)) {
break;
}
}
if (uniqueAuthors.count() > 5) {
uniqueAuthors = uniqueAuthors.mid(0, 5);
}
return uniqueAuthors;
}
QString MessageFilterModel::excessAuthors(int row) const
{
QVariantList uniqueAuthors;
for (int i = row; i >= 0; i--) {
QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAvatar)) {
uniqueAuthors.append(nextAvatar);
}
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
)) {
break;
}
}
int excessAuthors;
if (uniqueAuthors.count() > 5) {
excessAuthors = uniqueAuthors.count() - 5;
} else {
excessAuthors = 0;
}
QString excessAuthorsString;
if (excessAuthors == 0) {
return QString();
} else {
return QStringLiteral("+ %1").arg(excessAuthors);
}
}
#include "moc_messagefiltermodel.cpp"

View File

@@ -5,79 +5,21 @@
#include <QSortFilterProxyModel>
#include "messageeventmodel.h"
/**
* @class MessageFilterModel
*
* This model filters out any messages that should be hidden.
*
* Deleted messages are only hidden if the user hasn't set them to be shown.
*
* The model also contains the roles and functions to support aggregating multiple
* consecutive state events into a single delegate. The state events must all happen
* on the same day to be aggregated.
*/
class MessageFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
AggregateDisplayRole = MessageEventModel::LastRole + 1, /**< Single line aggregation of all the state events. */
StateEventsRole, /**< List of state events in the aggregated state. */
AuthorListRole, /**< List of the first 5 unique authors of the aggregated state event. */
ExcessAuthorsRole, /**< The number of unique authors beyond the first 5. */
LastRole, // Keep this last
};
explicit MessageFilterModel(QObject *parent = nullptr, MessageEventModel *sourceMessageModel = nullptr);
explicit MessageFilterModel(QObject *parent = nullptr);
/**
* @brief Custom filter function to remove hidden messages.
*/
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
/**
* @brief Get the given role value at the given index.
*
* @sa QSortFilterProxyModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractProxyModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
/**
* @brief Aggregation of the text of consecutive state events starting at row.
*
* If state events happen on different days they will be split into two aggregate
* events.
*/
[[nodiscard]] QString aggregateEventToString(int row) const;
/**
* @brief Return a list of consecutive state events starting at row.
*
* If state events happen on different days they will be split into two aggregate
* events.
*/
[[nodiscard]] QVariantList stateEventsList(int row) const;
/**
* @brief List of the first 5 unique authors for the aggregate state events starting at row.
*/
[[nodiscard]] QVariantList authorList(int row) const;
/**
* @brief The number of unique authors beyond the first 5 for the aggregate state events starting at row.
*/
[[nodiscard]] QString excessAuthors(int row) const;
};

View File

@@ -171,7 +171,7 @@ QVariant PublicRoomListModel::data(const QModelIndex &index, int role) const
return {};
}
auto room = rooms.at(index.row());
if (role == DisplayNameRole) {
if (role == NameRole) {
auto displayName = room.name;
if (!displayName.isEmpty()) {
return displayName;
@@ -188,17 +188,18 @@ QVariant PublicRoomListModel::data(const QModelIndex &index, int role) const
return room.roomId;
}
if (role == AvatarUrlRole) {
if (role == AvatarRole) {
auto avatarUrl = room.avatarUrl;
if (avatarUrl.isEmpty() || !m_connection) {
return QUrl();
if (avatarUrl.isEmpty()) {
return {};
}
return m_connection->makeMediaUrl(avatarUrl);
return avatarUrl.url().remove(0, 6);
}
if (role == TopicRole) {
return room.topic;
}
if (role == RoomIdRole) {
if (role == RoomIDRole) {
return room.roomId;
}
if (role == AliasRole) {
@@ -231,10 +232,10 @@ QHash<int, QByteArray> PublicRoomListModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[DisplayNameRole] = "displayName";
roles[AvatarUrlRole] = "avatarUrl";
roles[NameRole] = "name";
roles[AvatarRole] = "avatar";
roles[TopicRole] = "topic";
roles[RoomIdRole] = "roomId";
roles[RoomIDRole] = "roomID";
roles[MemberCountRole] = "memberCount";
roles[AllowGuestsRole] = "allowGuests";
roles[WorldReadableRole] = "worldReadable";

View File

@@ -58,10 +58,10 @@ public:
* @brief Defines the model roles.
*/
enum EventRoles {
DisplayNameRole = Qt::DisplayRole + 1, /**< The name of the room. */
AvatarUrlRole, /**< The source URL for the room's avatar. */
NameRole = Qt::DisplayRole + 1, /**< The name of the room. */
AvatarRole, /**< The source URL for the room's avatar. */
TopicRole, /**< The room topic. */
RoomIdRole, /**< The room matrix ID. */
RoomIDRole, /**< The room matrix ID. */
AliasRole, /**< The room canonical alias. */
MemberCountRole, /**< The number of members in the room. */
AllowGuestsRole, /**< Whether the room allows guest users. */
@@ -76,7 +76,7 @@ public:
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role = DisplayNameRole) const override;
[[nodiscard]] QVariant data(const QModelIndex &index, int role = NameRole) const override;
/**
* @brief Number of rows in the model.

View File

@@ -23,9 +23,6 @@ static const QHash<QString, QString> defaultRuleNames = {
{QStringLiteral(".m.rule.encrypted"), QStringLiteral("Messages in encrypted group chats")},
{QStringLiteral(".m.rule.tombstone"), QStringLiteral("Room upgrade messages")},
{QStringLiteral(".m.rule.contains_display_name"), QStringLiteral("Messages containing my display name")},
{QStringLiteral(".m.rule.is_user_mention"), QStringLiteral("Messages which mention my Matrix user ID.")},
{QStringLiteral(".m.rule.is_room_mention"), QStringLiteral("Messages which mention a room.")},
{QStringLiteral(".m.rule.contains_user_name"), QStringLiteral("Messages containing the local part of my Matrix ID.")},
{QStringLiteral(".m.rule.roomnotif"), QStringLiteral("Whole room (@room) notifications")},
{QStringLiteral(".m.rule.invite_for_me"), QStringLiteral("Invites to a room")},
{QStringLiteral(".m.rule.call"), QStringLiteral("Call invitation")},
@@ -40,9 +37,6 @@ static const QHash<QString, PushNotificationSection::Section> defaultSections =
{QStringLiteral(".m.rule.encrypted"), PushNotificationSection::Room},
{QStringLiteral(".m.rule.tombstone"), PushNotificationSection::Room},
{QStringLiteral(".m.rule.contains_display_name"), PushNotificationSection::Mentions},
{QStringLiteral(".m.rule.is_user_mention"), PushNotificationSection::Mentions},
{QStringLiteral(".m.rule.is_room_mention"), PushNotificationSection::Mentions},
{QStringLiteral(".m.rule.contains_user_name"), PushNotificationSection::Mentions},
{QStringLiteral(".m.rule.roomnotif"), PushNotificationSection::Mentions},
{QStringLiteral(".m.rule.invite_for_me"), PushNotificationSection::Invites},
{QStringLiteral(".m.rule.call"), PushNotificationSection::Undefined}, // TODO: make invites when VOIP added.
@@ -146,9 +140,6 @@ PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule ru
if (defaultSections.contains(ruleId)) {
return defaultSections.value(ruleId);
} else {
if (rule.ruleId.startsWith(u'.')) {
return PushNotificationSection::Unknown;
}
/**
* If the rule name resolves to a matrix id for a room that the user is part
* of it shouldn't appear in the global list as it's overriding the global

View File

@@ -83,16 +83,12 @@ public:
Keywords, /**< Global Keyword push rules. */
RoomKeywords, /**< Keyword push rules that only apply to a specific room. */
Invites, /**< Push rules relating to invites. */
Unknown, /**< New default push rules that have not been added to the model yet. */
/**
* @brief Push rules that should never be shown.
*
* There are numerous rules that get set that shouldn't be shown in the general
* list e.g. The array of rules used to override global settings in individual
* rooms.
*
* This is specifically different to unknown which are just new default push
* rule that haven't been added to the model yet.
*/
Undefined,
};

View File

@@ -31,7 +31,7 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
if (role == TextRole) {
if (reaction.authors.count() > 1) {
return QStringLiteral("%1 %2").arg(reaction.reaction, QString::number(reaction.authors.count()));
return QStringLiteral("%1 %2").arg(reaction.reaction, reaction.authors.count());
} else {
return reaction.reaction;
}

View File

@@ -36,13 +36,13 @@ RoomListModel::RoomListModel(QObject *parent)
m_categoryVisibility[collapsedSection] = false;
}
connect(this, &RoomListModel::highlightCountChanged, this, [this]() {
connect(this, &RoomListModel::notificationCountChanged, this, [this]() {
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
#ifndef Q_OS_ANDROID
// copied from Telegram desktop
const auto launcherUrl = "application://org.kde.neochat.desktop"_ls;
// Gnome requires that count is a 64bit integer
const qint64 counterSlice = std::min(m_highlightCount, 9999);
const qint64 counterSlice = std::min(m_notificationCount, 9999);
QVariantMap dbusUnityProperties;
if (counterSlice > 0) {
@@ -59,7 +59,7 @@ RoomListModel::RoomListModel(QObject *parent)
QDBusConnection::sessionBus().send(signal);
#endif // Q_OS_ANDROID
#else
qGuiApp->setBadgeNumber(m_highlightCount);
qGuiApp->setBadgeNumber(m_notificationCount);
#endif // QT_VERSION_CHECK(6, 6, 0)
});
connect(&SpaceHierarchyCache::instance(), &SpaceHierarchyCache::spaceHierarchyChanged, this, [this]() {
@@ -144,6 +144,7 @@ void RoomListModel::doAddRoom(Room *r)
m_rooms.append(room);
connectRoomSignals(room);
Q_EMIT roomAdded(room);
Q_EMIT Controller::instance().roomAdded(room);
} else {
qCritical() << "Attempt to add nullptr to the room list";
Q_ASSERT(false);
@@ -161,9 +162,6 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
connect(room, &Room::notificationCountChanged, this, [this, room] {
refresh(room);
});
connect(room, &Room::highlightCountChanged, this, [this, room] {
refresh(room);
});
connect(room, &Room::avatarChanged, this, [this, room] {
refresh(room, {AvatarRole});
});
@@ -180,7 +178,6 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
refresh(room, {LastEventRole, SubtitleTextRole});
});
connect(room, &Room::unreadStatsChanged, this, &RoomListModel::refreshNotificationCount);
connect(room, &Room::highlightCountChanged, this, &RoomListModel::refreshHighlightCount);
}
int RoomListModel::notificationCount() const
@@ -188,11 +185,6 @@ int RoomListModel::notificationCount() const
return m_notificationCount;
}
int RoomListModel::highlightCount() const
{
return m_highlightCount;
}
void RoomListModel::refreshNotificationCount()
{
int count = 0;
@@ -206,19 +198,6 @@ void RoomListModel::refreshNotificationCount()
Q_EMIT notificationCountChanged();
}
void RoomListModel::refreshHighlightCount()
{
int count = 0;
for (auto room : std::as_const(m_rooms)) {
count += room->highlightCount();
}
if (m_highlightCount == count) {
return;
}
m_highlightCount = count;
Q_EMIT highlightCountChanged();
}
void RoomListModel::updateRoom(Room *room, Room *prev)
{
// There are two cases when this method is called:

View File

@@ -85,7 +85,6 @@ public:
void setConnection(Quotient::Connection *connection);
[[nodiscard]] int notificationCount() const;
[[nodiscard]] int highlightCount() const;
/**
* @brief Get the given role value at the given index.
@@ -156,7 +155,6 @@ private Q_SLOTS:
void deleteRoom(Quotient::Room *room);
void refresh(NeoChatRoom *room, const QVector<int> &roles = {});
void refreshNotificationCount();
void refreshHighlightCount();
private:
Quotient::Connection *m_connection = nullptr;
@@ -165,7 +163,6 @@ private:
QMap<int, bool> m_categoryVisibility;
int m_notificationCount = 0;
int m_highlightCount = 0;
QString m_activeSpaceId;
void connectRoomSignals(NeoChatRoom *room);
@@ -173,7 +170,6 @@ private:
Q_SIGNALS:
void connectionChanged();
void notificationCountChanged();
void highlightCountChanged();
void roomAdded(NeoChatRoom *_t1);
};

View File

@@ -3,7 +3,6 @@
#include "searchmodel.h"
#include "eventhandler.h"
#include "messageeventmodel.h"
#include "neochatroom.h"
@@ -92,55 +91,90 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
{
auto row = index.row();
const auto &event = *m_result->results[row].result;
EventHandler eventHandler;
eventHandler.setRoom(m_room);
eventHandler.setEvent(&event);
switch (role) {
case DisplayRole:
return eventHandler.getRichBody();
return m_room->eventToString(*m_result->results[row].result);
case ShowAuthorRole:
return true;
case AuthorRole:
return eventHandler.getAuthor();
return m_room->getUser(event.senderId());
case ShowSectionRole:
if (row == 0) {
return true;
}
return event.originTimestamp().date() != m_result->results[row - 1].result->originTimestamp().date();
case SectionRole:
return eventHandler.getTimeString(true);
return renderDate(event.originTimestamp());
case TimeRole:
return eventHandler.getTime();
case TimeStringRole:
return eventHandler.getTimeString(false);
return event.originTimestamp();
case ShowReactionsRole:
return false;
case ShowReadMarkersRole:
return false;
case IsReplyRole:
return eventHandler.hasReply();
case ReplyIdRole:
return eventHandler.hasReply();
case ReplyAuthorRole:
return eventHandler.getReplyAuthor();
case ReplyDelegateTypeRole:
return eventHandler.getReplyDelegateType();
case ReplyDisplayRole:
return eventHandler.getReplyRichBody();
case ReplyMediaInfoRole:
return eventHandler.getReplyMediaInfo();
if (const auto &replyPtr = m_room->getReplyForEvent(event)) {
return m_room->getUser(m_room->user(replyPtr->senderId()));
} else {
return m_room->getUser(nullptr);
}
case ReplyRole:
if (role == ReplyRole) {
auto replyPtr = m_room->getReplyForEvent(event);
if (!replyPtr) {
return {};
}
MessageEventModel::DelegateType type;
if (auto e = eventCast<const RoomMessageEvent>(replyPtr)) {
switch (e->msgtype()) {
case MessageEventType::Emote:
type = MessageEventModel::DelegateType::Emote;
break;
case MessageEventType::Notice:
type = MessageEventModel::DelegateType::Notice;
break;
case MessageEventType::Image:
type = MessageEventModel::DelegateType::Image;
break;
case MessageEventType::Audio:
type = MessageEventModel::DelegateType::Audio;
break;
case MessageEventType::Video:
type = MessageEventModel::DelegateType::Video;
break;
default:
if (e->hasFileContent()) {
type = MessageEventModel::DelegateType::File;
break;
}
type = MessageEventModel::DelegateType::Message;
}
} else if (is<const StickerEvent>(*replyPtr)) {
type = MessageEventModel::DelegateType::Sticker;
} else {
type = MessageEventModel::DelegateType::Other;
}
return QVariantMap{
{"display"_ls, m_room->eventToString(*replyPtr, Qt::RichText)},
{"type"_ls, type},
};
}
case IsPendingRole:
return false;
case ShowLinkPreviewRole:
return false;
case IsReplyRole:
return !event.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
case HighlightRole:
return eventHandler.isHighlighted();
return !m_room->isDirectChat() && m_room->isEventHighlighted(&event);
case EventIdRole:
return eventHandler.getId();
return event.id();
case ReplyIdRole:
return event.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
}
return DelegateType::Message;
return MessageEventModel::DelegateType::Message;
}
int SearchModel::rowCount(const QModelIndex &parent) const
@@ -161,7 +195,6 @@ QHash<int, QByteArray> SearchModel::roleNames() const
{ShowSectionRole, "showSection"},
{SectionRole, "section"},
{TimeRole, "time"},
{TimeStringRole, "timeString"},
{ShowAuthorRole, "showAuthor"},
{EventIdRole, "eventId"},
{ExcessReadMarkersRole, "excessReadMarkers"},
@@ -169,22 +202,21 @@ QHash<int, QByteArray> SearchModel::roleNames() const
{ReadMarkersString, "readMarkersString"},
{PlainTextRole, "plainText"},
{VerifiedRole, "verified"},
{ProgressInfoRole, "progressInfo"},
{ShowReactionsRole, "showReactions"},
{IsReplyRole, "isReply"},
{ReplyAuthorRole, "replyAuthor"},
{ReplyIdRole, "replyId"},
{ReplyDelegateTypeRole, "replyDelegateType"},
{ReplyDisplayRole, "replyDisplay"},
{ReplyMediaInfoRole, "replyMediaInfo"},
{ProgressInfoRole, "progressInfo"},
{IsReplyRole, "isReply"},
{ShowReactionsRole, "showReactions"},
{ReplyRole, "reply"},
{ReactionRole, "reaction"},
{ReplyMediaInfoRole, "replyMediaInfo"},
{ReadMarkersRole, "readMarkers"},
{IsPendingRole, "isPending"},
{ShowReadMarkersRole, "showReadMarkers"},
{ReplyIdRole, "replyId"},
{MimeTypeRole, "mimeType"},
{ShowLinkPreviewRole, "showLinkPreview"},
{LinkPreviewRole, "linkPreview"},
{SourceRole, "jsonSource"},
{SourceRole, "source"},
};
}
@@ -211,10 +243,30 @@ void SearchModel::setRoom(NeoChatRoom *room)
return;
}
auto row = it - results.begin();
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyDelegateTypeRole, ReplyDisplayRole, ReplyMediaInfoRole, ReplyAuthorRole});
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyRole, ReplyMediaInfoRole, ReplyAuthorRole});
});
}
// TODO deduplicate with messageeventmodel
QString renderDate(const QDateTime &timestamp)
{
auto date = timestamp.toLocalTime().date();
if (date == QDate::currentDate()) {
return i18n("Today");
}
if (date == QDate::currentDate().addDays(-1)) {
return i18n("Yesterday");
}
if (date == QDate::currentDate().addDays(-2)) {
return i18n("The day before yesterday");
}
if (date > QDate::currentDate().addDays(-7)) {
return date.toString("dddd"_ls);
}
return QLocale::system().toString(date, QLocale::ShortFormat);
}
bool SearchModel::searching() const
{
return m_searching;

View File

@@ -61,25 +61,23 @@ public:
ShowSectionRole,
SectionRole,
TimeRole,
TimeStringRole,
EventIdRole,
ExcessReadMarkersRole,
HighlightRole,
ReadMarkersString,
PlainTextRole,
VerifiedRole,
ProgressInfoRole,
ShowReactionsRole,
IsReplyRole,
ReplyAuthorRole,
ReplyIdRole,
ReplyDelegateTypeRole,
ReplyDisplayRole,
ReplyMediaInfoRole,
ProgressInfoRole,
IsReplyRole,
ShowReactionsRole,
ReplyRole,
ReactionRole,
ReplyMediaInfoRole,
ReadMarkersRole,
IsPendingRole,
ShowReadMarkersRole,
ReplyIdRole,
MimeTypeRole,
ShowLinkPreviewRole,
LinkPreviewRole,
@@ -141,3 +139,5 @@ private:
Quotient::SearchJob *m_job = nullptr;
bool m_searching = false;
};
QString renderDate(const QDateTime &dateTime);

View File

@@ -26,8 +26,7 @@ QVariant StateModel::data(const QModelIndex &index, int role) const
int StateModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_room->currentState().events().size();
return !m_room || parent.isValid() ? 0 : m_room->currentState().events().size();
}
NeoChatRoom *StateModel::room() const
@@ -37,8 +36,12 @@ NeoChatRoom *StateModel::room() const
void StateModel::setRoom(NeoChatRoom *room)
{
if (m_room == room) {
return;
}
m_room = room;
Q_EMIT roomChanged();
beginResetModel();
m_stateEvents.clear();
m_stateEvents = m_room->currentState().events().keys();

View File

@@ -109,7 +109,7 @@ void StickerModel::setRoom(NeoChatRoom *room)
void StickerModel::postSticker(int index)
{
const auto &image = m_images[index];
const auto &body = image.body ? *image.body : image.shortcode;
const auto &body = image.body ? *image.body : QString();
QJsonObject infoJson;
if (image.info) {
infoJson["w"_ls] = image.info->imageSize.width();

View File

@@ -107,14 +107,6 @@
<label>Show Fancy Effects</label>
<default>true</default>
</entry>
<entry name="MediaMaxWidth" type="int">
<label>The maximum width any media item in the timeline can be.</label>
<default>540</default>
</entry>
<entry name="MediaMaxHeight" type="int">
<label>The maximum height any media item in the timeline can be.</label>
<default>540</default>
</entry>
</group>
<group name="RoomDrawer">
<entry name="ShowAvatarInRoomDrawer" type="bool">

View File

@@ -1,181 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatconnection.h"
#include <QImageReader>
#include "controller.h"
#include "jobs/neochatchangepasswordjob.h"
#include "jobs/neochatdeactivateaccountjob.h"
#include "roommanager.h"
#include <qt6keychain/keychain.h>
#include <KLocalizedString>
#include <Quotient/csapi/content-repo.h>
#include <Quotient/csapi/profile.h>
#include <Quotient/qt_connection_util.h>
#include <Quotient/settings.h>
#include <Quotient/user.h>
using namespace Quotient;
NeoChatConnection::NeoChatConnection(QObject *parent)
: Connection(parent)
{
connect(this, &NeoChatConnection::accountDataChanged, this, [this](const QString &type) {
if (type == QLatin1String("org.kde.neochat.account_label")) {
Q_EMIT labelChanged();
}
});
}
NeoChatConnection::NeoChatConnection(const QUrl &server, QObject *parent)
: Connection(server, parent)
{
connect(this, &NeoChatConnection::accountDataChanged, this, [this](const QString &type) {
if (type == QLatin1String("org.kde.neochat.account_label")) {
Q_EMIT labelChanged();
}
});
}
void NeoChatConnection::logout(bool serverSideLogout)
{
SettingsGroup(QStringLiteral("Accounts")).remove(userId());
QKeychain::DeletePasswordJob job(qAppName());
job.setAutoDelete(true);
job.setKey(userId());
QEventLoop loop;
QKeychain::DeletePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
if (Controller::instance().accounts().count() > 1) {
// Only set the connection if the the account being logged out is currently active
if (this == Controller::instance().activeConnection()) {
Controller::instance().setActiveConnection(dynamic_cast<NeoChatConnection *>(Controller::instance().accounts().accounts()[0]));
}
} else {
Controller::instance().setActiveConnection(nullptr);
}
if (!serverSideLogout) {
return;
}
Connection::logout();
}
bool NeoChatConnection::setAvatar(const QUrl &avatarSource)
{
QString decoded = avatarSource.path();
if (decoded.isEmpty()) {
callApi<SetAvatarUrlJob>(user()->id(), avatarSource);
return true;
}
if (QImageReader(decoded).read().isNull()) {
return false;
} else {
return user()->setAvatar(decoded);
}
}
QVariantList NeoChatConnection::getSupportedRoomVersions() const
{
const auto &roomVersions = availableRoomVersions();
QVariantList supportedRoomVersions;
for (const auto &v : roomVersions) {
QVariantMap roomVersionMap;
roomVersionMap.insert("id"_ls, v.id);
roomVersionMap.insert("status"_ls, v.status);
roomVersionMap.insert("isStable"_ls, v.isStable());
supportedRoomVersions.append(roomVersionMap);
}
return supportedRoomVersions;
}
void NeoChatConnection::changePassword(const QString &currentPassword, const QString &newPassword)
{
auto job = callApi<NeochatChangePasswordJob>(newPassword, false);
connect(job, &BaseJob::result, this, [this, job, currentPassword, newPassword] {
if (job->error() == 103) {
QJsonObject replyData = job->jsonData();
QJsonObject authData;
authData["session"_ls] = replyData["session"_ls];
authData["password"_ls] = currentPassword;
authData["type"_ls] = "m.login.password"_ls;
authData["user"_ls] = user()->id();
QJsonObject identifier = {{"type"_ls, "m.id.user"_ls}, {"user"_ls, user()->id()}};
authData["identifier"_ls] = identifier;
NeochatChangePasswordJob *innerJob = callApi<NeochatChangePasswordJob>(newPassword, false, authData);
connect(innerJob, &BaseJob::success, this, []() {
Q_EMIT Controller::instance().passwordStatus(Controller::PasswordStatus::Success);
});
connect(innerJob, &BaseJob::failure, this, [innerJob]() {
Q_EMIT Controller::instance().passwordStatus(innerJob->jsonData()["errcode"_ls] == "M_FORBIDDEN"_ls ? Controller::PasswordStatus::Wrong
: Controller::PasswordStatus::Other);
});
}
});
}
void NeoChatConnection::setLabel(const QString &label)
{
QJsonObject json{
{"account_label"_ls, label},
};
setAccountData("org.kde.neochat.account_label"_ls, json);
Q_EMIT labelChanged();
}
QString NeoChatConnection::label() const
{
return accountDataJson("org.kde.neochat.account_label"_ls)["account_label"_ls].toString();
}
void NeoChatConnection::deactivateAccount(const QString &password)
{
auto job = callApi<NeoChatDeactivateAccountJob>();
connect(job, &BaseJob::result, this, [this, job, password] {
if (job->error() == 103) {
QJsonObject replyData = job->jsonData();
QJsonObject authData;
authData["session"_ls] = replyData["session"_ls];
authData["password"_ls] = password;
authData["type"_ls] = "m.login.password"_ls;
authData["user"_ls] = user()->id();
QJsonObject identifier = {{"type"_ls, "m.id.user"_ls}, {"user"_ls, user()->id()}};
authData["identifier"_ls] = identifier;
auto innerJob = callApi<NeoChatDeactivateAccountJob>(authData);
connect(innerJob, &BaseJob::success, this, [this]() {
logout(false);
});
}
});
}
void NeoChatConnection::createRoom(const QString &name, const QString &topic)
{
const auto job = Connection::createRoom(Connection::PublishRoom, {}, name, topic, {});
connect(job, &CreateRoomJob::failure, this, [this, job] {
Q_EMIT Controller::instance().errorOccured(i18n("Room creation failed: %1", job->errorString()));
});
connectSingleShot(this, &Connection::newRoom, this, [](Room *room) {
RoomManager::instance().enterRoom(dynamic_cast<NeoChatRoom *>(room));
});
}
void NeoChatConnection::createSpace(const QString &name, const QString &topic)
{
const auto job = Connection::createRoom(Connection::UnpublishRoom, {}, name, topic, {}, {}, {}, false, {}, {}, QJsonObject{{"type"_ls, "m.space"_ls}});
connect(job, &CreateRoomJob::failure, this, [this, job] {
Q_EMIT Controller::instance().errorOccured(i18n("Space creation failed: %1", job->errorString()));
});
connectSingleShot(this, &Connection::newRoom, this, [](Room *room) {
RoomManager::instance().enterRoom(dynamic_cast<NeoChatRoom *>(room));
});
}
#include "moc_neochatconnection.cpp"

View File

@@ -1,63 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
#include <Quotient/connection.h>
class NeoChatConnection : public Quotient::Connection
{
Q_OBJECT
/**
* @brief The account label for this 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 label READ label WRITE setLabel NOTIFY labelChanged)
public:
NeoChatConnection(QObject *parent = nullptr);
NeoChatConnection(const QUrl &server, QObject *parent = nullptr);
Q_INVOKABLE void logout(bool serverSideLogout);
Q_INVOKABLE QVariantList getSupportedRoomVersions() const;
/**
* @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(const QString &currentPassword, const QString &newPassword);
/**
* @brief Change the avatar for an account.
*/
Q_INVOKABLE bool setAvatar(const QUrl &avatarSource);
[[nodiscard]] QString label() const;
void setLabel(const QString &label);
Q_INVOKABLE void deactivateAccount(const QString &password);
/**
* @brief Create new room for a group chat.
*/
Q_INVOKABLE void createRoom(const QString &name, const QString &topic);
/**
* @brief Create new space.
*/
Q_INVOKABLE void createSpace(const QString &name, const QString &topic);
Q_SIGNALS:
void labelChanged();
};

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