Compare commits

...

64 Commits

Author SHA1 Message Date
Tobias Fella
819d88e18c Assorted Qt6 android fixes 2023-11-07 23:17:38 +01:00
James Graham
dbbad2cf13 Restricted Room Security
Create the required ux to allow the restricted room security setting to be re-enabled

BUG: 471307
2023-11-07 20:43:49 +00:00
Albert Astals Cid
08b84c6592 GIT_SILENT Upgrade release service version to 24.01.75. 2023-11-07 21:11:26 +01:00
l10n daemon script
b8abf0540d GIT_SILENT Sync po/docbooks with svn 2023-11-07 03:22:56 +00:00
Tobias Fella
7f9e709559 Improve initial active connection handling
If the last active connection is not reachable (server down, keychain problems,
token revoked, etc.), NeoChat currently fails to load at all, with the only fix
being to delete a line from the config file. This is surprisingly hard to fix with
a nice UX as long as we stick to the principle of loading the user's last active
connection automatically.

This patch thus drops that principle; instead, the user is always asked to choose
the connection to continue with.
2023-11-06 12:26:25 +00:00
l10n daemon script
c4f6abee9d GIT_SILENT Sync po/docbooks with svn 2023-11-06 02:17:00 +00:00
Joshua Goins
1a4947b98a Set accessible press actions for RoomDelegate 2023-11-05 18:09:20 -05:00
Tobias Fella
6ba2b715c3 Port away from Controller::activeConnectionIndex 2023-11-05 21:03:04 +00:00
Joshua Goins
e9e1e223f7 Allow right-clicking on a room without selecting it
Same idea as the fix I did for spaces, we introduce a new signal called
selected instead of using ItemDelegate's button signals.
2023-11-05 20:43:08 +00:00
Joshua Goins
006da1fb16 Show color scheme option on Android
This works on Android now, and the rest of the code changed to enable it
anyway.
2023-11-05 20:38:33 +00:00
James Graham
3aff1795c8 Accounts Popup Close on Hide
Make sure that the accounts popup is closed when the UserInfo component is hidden
2023-11-05 19:57:15 +00:00
Joshua Goins
576b1f928f Set RoomManager connection when opening a room
This was only set for the Controller, but it needed to be set for the
RoomManager too before opening a room. This could cause NeoChat to crash
when activating a notification, for example.
2023-11-05 14:48:55 -05:00
l10n daemon script
fc3ab50701 GIT_SILENT Sync po/docbooks with svn 2023-11-05 14:37:01 +00:00
James Graham
e7fa3ad524 Explicit room parameter DirectChatDrawerHeader
Create explicit room parameter and reference it in DirectChatDrawerHeader
2023-11-05 13:49:13 +00:00
James Graham
5adffddbd8 Fix InviteUserPage
Port the InviteUserPage to Qt6 and fix close action

fixes network/neochat#614
2023-11-05 13:45:10 +00:00
Tobias Fella
1d95d5aa15 Fix Runner and MatrixImageProvider singletons 2023-11-05 14:35:28 +01:00
Carl Schwan
dabd6291a5 roomlist: Fix ListSectionDelegate usage
The default property of ListSectionDelegate is already an alias to the
RowLayout.data and trying to use RoomLayout.children didn't work as
expected.
2023-11-05 13:08:29 +00:00
Tobias Fella
0e55c3b38f Port some things away from Controller::activeConnection 2023-11-05 13:02:18 +00:00
Carl Schwan
ff4cf86ea5 Add profile picture in about data 2023-11-05 12:17:26 +01:00
l10n daemon script
f4d5ccbf12 GIT_SILENT Sync po/docbooks with svn 2023-11-05 11:14:20 +00:00
l10n daemon script
192601d358 GIT_SILENT Sync po/docbooks with svn 2023-11-05 01:41:24 +00:00
l10n daemon script
d7c432119e GIT_SILENT Sync po/docbooks with svn 2023-11-04 17:24:07 +00:00
Tobias Fella
eddb2b73c2 Use new notification action api 2023-11-04 16:18:00 +00:00
Tobias Fella
0c60bfdb83 Don't crash on quit
BUG: 474386
2023-11-04 16:03:54 +00:00
James Graham
2bfb2fa1f9 Improve ChatDocumentHandler handling of null properties
Improve ChatDocumentHandler handling of null properties
2023-11-04 14:43:31 +00:00
l10n daemon script
d1aac971bf GIT_SILENT Sync po/docbooks with svn 2023-11-04 12:51:19 +00:00
Heiko Becker
284cadf305 GIT_SILENT Update Appstream for new release
(cherry picked from commit 753e879556)
2023-11-04 12:33:19 +01:00
l10n daemon script
4697b7fcf1 GIT_SILENT Sync po/docbooks with svn 2023-11-04 02:10:15 +00:00
Tobias Fella
9f356912c9 Add placeholder message to devices page 2023-11-03 22:06:33 +00:00
Tobias Fella
1cf891f845 Fix device logout dialog 2023-11-03 22:06:14 +00:00
Joshua Goins
52e2d636b9 Adjust tests to blockquote formatting changes 2023-11-03 21:52:47 +00:00
Joshua Goins
dc6b539ddf Add somewhat better blockquote styling
Quotes are now "quoted" and also have a different background.
2023-11-03 21:52:47 +00:00
Joshua Goins
e3cf85aa8c Yank "Qt5" out of the description for libQuotient
We aren't using thr Qt5 version anymore, and it's not really a point in
describing which exact Qt version we use with libQuotient in a user
facing string like this anyway.
2023-11-03 17:39:02 -04:00
James Graham
2065eb6684 Mobile explore component
Create a mobile version of explore component and place it at the bottom for single handed use. This also refactors the UserInfo component so it can be at the top on mobile as well as the bottom on dektop.

This should have no effect on desktop and should be identical.

![image](/uploads/9b3133fbde74ca27069d6b039efb1079/image.png)
2023-11-03 17:22:57 +00:00
Tobias Fella
9cac2a8abd Delete KeywordNotificationRuleModel
It's unused
2023-11-03 17:39:10 +01:00
Tobias Fella
feb87e6f70 Show QR codes in UserDetailDialog 2023-11-03 16:23:54 +00:00
l10n daemon script
a0057b8a49 GIT_SILENT Sync po/docbooks with svn 2023-11-03 02:12:15 +00:00
Tobias Fella
dbb0269354 Fix sending locations 2023-11-02 19:46:06 +01:00
Tobias Fella
7fdb617b33 Port RoundButtons in TimelineView to FloatingButton
BUG: 476124
2023-11-02 18:15:57 +01:00
Tobias Fella
d798b0dec9 HTML-escape mentions 2023-11-02 17:04:26 +00:00
James Graham
95cf23eb5b Remove GridLayout ReplyComponent
Stop using GridLayout in ReplyComponent as they are notoriously terribleRemove
2023-11-02 17:04:18 +00:00
James Graham
7e3db20229 Remove isEdit from ChatDocumentHandler
Remove isEdit from ChatDocumentHandler as it was made redundant by the ChatCache rework.
2023-11-02 17:00:41 +00:00
Laurent Montel
12689babfb Add missing moc 2023-11-02 08:50:43 +01:00
l10n daemon script
211407da44 GIT_SILENT Sync po/docbooks with svn 2023-11-02 02:13:36 +00:00
l10n daemon script
400a84e48d GIT_SILENT Sync po/docbooks with svn 2023-11-01 02:15:14 +00:00
Tobias Fella
7a45640e5e Fix string 2023-10-31 17:13:05 +01:00
Tobias Fella
8af20885ab Add account / device security settings page 2023-10-31 14:59:08 +01:00
Tobias Fella
4033f07272 Port away from custom format* QML functions 2023-10-31 14:56:52 +01:00
Tobias Fella
5df4fa297d Move Controller::setBlur and Controller::hasWindowSystem to WindowController 2023-10-31 09:17:44 +00:00
Tobias Fella
33c5b418d2 Move Controller::openOrCreateDirectChat to NeoChatConnection 2023-10-31 09:16:59 +00:00
l10n daemon script
69d378a17b GIT_SILENT Sync po/docbooks with svn 2023-10-31 02:11:02 +00:00
Tobias Fella
f1b1b8ce53 Remove unused include 2023-10-30 21:29:44 +01:00
Tobias Fella
526d4748e0 Remove unused includes 2023-10-30 21:27:36 +01:00
Tobias Fella
c5f93adbf4 Don't percent encode job parameter 2023-10-30 20:51:44 +01:00
Tobias Fella
61630cbe90 Rename "Security.qml" to "RoomSecurity.qml" 2023-10-30 11:07:42 +01:00
l10n daemon script
8c435e9d6d GIT_SILENT Sync po/docbooks with svn 2023-10-30 02:13:41 +00:00
James Graham
57978b1a6e EventHandler Nullptr checks
So I got lazy halfway through doing this the first time.

Add missing checks for null m_room and m_event to EventHandler and add tests for them.
2023-10-29 14:51:25 +00:00
James Graham
f3c4d9449a Move remaining tests to external Json files
Title
2023-10-29 13:12:05 +00:00
James Graham
9b37777f20 Remove getUsers
Remove getUsers as it's unused
2023-10-29 13:07:45 +00:00
l10n daemon script
d6d6c161db GIT_SILENT Sync po/docbooks with svn 2023-10-29 02:23:11 +00:00
Tobias Fella
772bca5ba6 Fix sending messages that were typed before switching rooms 2023-10-28 17:21:12 +02:00
Tobias Fella
036a60a095 Don't linkify urls in code blocks
BUG: 475301
2023-10-28 16:19:32 +02:00
Tobias Fella
b3315e1ed4 Add test for bug 475301
BUG: 475301
2023-10-28 16:19:31 +02:00
Tobias Fella
d300e9cf52 Slightly reformat cmake 2023-10-28 16:18:21 +02:00
126 changed files with 20253 additions and 16479 deletions

View File

@@ -16,6 +16,7 @@ Dependencies:
'frameworks/kcolorscheme': '@latest-kf6' 'frameworks/kcolorscheme': '@latest-kf6'
'libraries/kquickimageeditor': '@latest-kf6' 'libraries/kquickimageeditor': '@latest-kf6'
'frameworks/sonnet': '@latest-kf6' 'frameworks/sonnet': '@latest-kf6'
'frameworks/prison': '@latest-kf6'
'libraries/kirigami-addons': '@latest-kf6' 'libraries/kirigami-addons': '@latest-kf6'
'third-party/libquotient': '@latest' 'third-party/libquotient': '@latest'
'third-party/qtkeychain': '@latest' 'third-party/qtkeychain': '@latest'

View File

@@ -7,9 +7,9 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script. # KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "23") set(RELEASE_SERVICE_VERSION_MAJOR "24")
set(RELEASE_SERVICE_VERSION_MINOR "11") set(RELEASE_SERVICE_VERSION_MINOR "01")
set(RELEASE_SERVICE_VERSION_MICRO "70") set(RELEASE_SERVICE_VERSION_MICRO "75")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}") set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION}) project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
@@ -37,6 +37,7 @@ include(ECMAddAppIcon)
include(KDEGitCommitHooks) include(KDEGitCommitHooks)
include(ECMCheckOutboundLicense) include(ECMCheckOutboundLicense)
include(ECMQtDeclareLoggingCategory) include(ECMQtDeclareLoggingCategory)
include(ECMAddAndroidApk)
if (NOT ANDROID) if (NOT ANDROID)
include(KDEClangFormat) include(KDEClangFormat)
endif() endif()
@@ -119,6 +120,7 @@ ecm_find_qmlmodule(org.kde.kquickimageeditor 1.0)
ecm_find_qmlmodule(org.kde.kitemmodels 1.0) ecm_find_qmlmodule(org.kde.kitemmodels 1.0)
ecm_find_qmlmodule(org.kde.quickcharts 1.0) ecm_find_qmlmodule(org.kde.quickcharts 1.0)
ecm_find_qmlmodule(QtLocation) ecm_find_qmlmodule(QtLocation)
ecm_find_qmlmodule(org.kde.prison)
find_package(KQuickImageEditor COMPONENTS) find_package(KQuickImageEditor COMPONENTS)
set_package_properties(KQuickImageEditor PROPERTIES set_package_properties(KQuickImageEditor PROPERTIES

View File

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

View File

@@ -12,7 +12,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.0.2' classpath 'com.android.tools.build:gradle:7.4.1'
} }
} }
@@ -35,7 +35,7 @@ android {
* The following variables: * The following variables:
* - androidBuildToolsVersion, * - androidBuildToolsVersion,
* - androidCompileSdkVersion * - androidCompileSdkVersion
* - qt5AndroidDir - holds the path to qt android files * - qtAndroidDir - holds the path to qt android files
* needed to build any Qt application * needed to build any Qt application
* on Android. * on Android.
* *
@@ -44,17 +44,20 @@ android {
* Changing them manually might break the compilation! * Changing them manually might break the compilation!
*******************************************************/ *******************************************************/
compileSdkVersion androidCompileSdkVersion.toInteger() compileSdkVersion androidCompileSdkVersion
buildToolsVersion androidBuildToolsVersion buildToolsVersion androidBuildToolsVersion
ndkVersion androidNdkVersion ndkVersion androidNdkVersion
// Extract native libraries from the APK
packagingOptions.jniLibs.useLegacyPackaging true
sourceSets { sourceSets {
main { main {
manifest.srcFile 'AndroidManifest.xml' manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java'] java.srcDirs = [qtAndroidDir + '/src', 'src', 'java']
aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl'] aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl']
res.srcDirs = [qt5AndroidDir + '/res', 'res'] res.srcDirs = [qtAndroidDir + '/res', 'res']
resources.srcDirs = ['src'] resources.srcDirs = ['src']
renderscript.srcDirs = ['src'] renderscript.srcDirs = ['src']
assets.srcDirs = ['assets'] assets.srcDirs = ['assets']

View File

@@ -40,3 +40,9 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test LINK_LIBRARIES neochat Qt::Test
TEST_NAME chatbarcachetest TEST_NAME chatbarcachetest
) )
ecm_add_test(
chatdocumenthandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME chatdocumenthandlertest
)

View File

@@ -0,0 +1,36 @@
// 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 "chatdocumenthandler.h"
#include "neochatconfig.h"
class ChatDocumentHandlerTest : public QObject
{
Q_OBJECT
private:
ChatDocumentHandler emptyHandler;
private Q_SLOTS:
void initTestCase();
void nullComplete();
};
void ChatDocumentHandlerTest::initTestCase()
{
// HACK: this is to stop KStatusNotifierItem SEGFAULTING on cleanup.
NeoChatConfig::self()->setSystemTray(false);
}
void ChatDocumentHandlerTest::nullComplete()
{
QTest::ignoreMessage(QtWarningMsg, "complete called with m_document set to nullptr.");
emptyHandler.complete(0);
}
QTEST_MAIN(ChatDocumentHandlerTest)
#include "chatdocumenthandlertest.moc"

View File

@@ -0,0 +1,124 @@
{
"account_data": {
"events": [
{
"content": {
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
},
{
"content": {
"custom_config_key": "custom_config_value"
},
"type": "org.example.custom.room.config"
}
]
},
"ephemeral": {
"events": [
{
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.typing"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an **example** text message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>",
"msgtype": "m.text"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "/me This is an emote.",
"format": "org.matrix.custom.html",
"formatted_body": "This is an emote.",
"msgtype": "m.emote"
},
"event_id": "$153273582443PhrSn:example.org",
"origin_server_ts": 1532735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1231
}
},
{
"content": {
"body": "tested",
"msgtype": "m.text"
},
"event_id": "$zrCiBxBnqqTn0Z5FY78qSZAszno_w8nJJXzfBULG-3E",
"origin_server_ts": 1680948575928,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1747776,
"m.relations": {
"m.replace": {
"event_id": "$UX0PlpyI7vYO32iHMuuYEP7ECMh4sX3XLGiB2SwM4mQ",
"origin_server_ts": 1680948580992,
"sender": "@example:example.org"
}
}
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -39,34 +39,61 @@ private:
Connection *connection = nullptr; Connection *connection = nullptr;
TestRoom *room = nullptr; TestRoom *room = nullptr;
EventHandler eventHandler; EventHandler eventHandler;
EventHandler emptyHandler;
EventHandler noEventHandler;
private Q_SLOTS: private Q_SLOTS:
void initTestCase(); void initTestCase();
void nullSetEvent();
void eventId(); void eventId();
void nullEventId();
void delegateType_data(); void delegateType_data();
void delegateType(); void delegateType();
void nullDelegateType();
void author(); void author();
void nullAuthor();
void authorDisplayName(); void authorDisplayName();
void nullAuthorDisplayName();
void time(); void time();
void nullTime();
void timeString(); void timeString();
void nullTimeString();
void highlighted(); void highlighted();
void nullHighlighted();
void hidden(); void hidden();
void nullHidden();
void body(); void body();
void nullBody();
void genericBody_data(); void genericBody_data();
void genericBody(); void genericBody();
void nullGenericBody();
void mediaInfo(); void mediaInfo();
void nullMediaInfo();
void linkPreviewer(); void linkPreviewer();
void nullLinkPreviewer();
void reactions(); void reactions();
void nullReactions();
void hasReply(); void hasReply();
void nullHasReply();
void replyId(); void replyId();
void nullReplyId();
void replyDelegateType(); void replyDelegateType();
void nullReplyDelegateType();
void replyAuthor(); void replyAuthor();
void nullReplyAuthor();
void replyBody(); void replyBody();
void nullReplyBody();
void replyMediaInfo(); void replyMediaInfo();
void nullReplyMediaInfo();
void thread(); void thread();
void nullThread();
void location(); void location();
void nullLocation();
void readMarkers(); void readMarkers();
void nullReadMarkers();
void cleanup();
}; };
void EventHandlerTest::initTestCase() void EventHandlerTest::initTestCase()
@@ -82,6 +109,13 @@ void EventHandlerTest::initTestCase()
room->update(std::move(roomData)); room->update(std::move(roomData));
eventHandler.setRoom(room); eventHandler.setRoom(room);
noEventHandler.setRoom(room);
}
void EventHandlerTest::nullSetEvent()
{
QTest::ignoreMessage(QtWarningMsg, "cannot setEvent when m_room is set to nullptr.");
emptyHandler.setEvent(room->messageEvents().at(0).get());
} }
void EventHandlerTest::eventId() void EventHandlerTest::eventId()
@@ -91,6 +125,12 @@ void EventHandlerTest::eventId()
QCOMPARE(eventHandler.getId(), QStringLiteral("$153456789:example.org")); QCOMPARE(eventHandler.getId(), QStringLiteral("$153456789:example.org"));
} }
void EventHandlerTest::nullEventId()
{
QTest::ignoreMessage(QtWarningMsg, "getId called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getId(), QString());
}
void EventHandlerTest::delegateType_data() void EventHandlerTest::delegateType_data()
{ {
QTest::addColumn<int>("eventNum"); QTest::addColumn<int>("eventNum");
@@ -114,6 +154,12 @@ void EventHandlerTest::delegateType()
QCOMPARE(eventHandler.getDelegateType(), delegateType); QCOMPARE(eventHandler.getDelegateType(), delegateType);
} }
void EventHandlerTest::nullDelegateType()
{
QTest::ignoreMessage(QtWarningMsg, "getDelegateType called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getDelegateType(), DelegateType::Other);
}
void EventHandlerTest::author() void EventHandlerTest::author()
{ {
auto event = room->messageEvents().at(0).get(); auto event = room->messageEvents().at(0).get();
@@ -131,6 +177,15 @@ void EventHandlerTest::author()
QCOMPARE(eventHandlerAuthor["object"_ls], QVariant::fromValue(author)); QCOMPARE(eventHandlerAuthor["object"_ls], QVariant::fromValue(author));
} }
void EventHandlerTest::nullAuthor()
{
QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getAuthor(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_event set to nullptr. Returning empty user.");
QCOMPARE(noEventHandler.getAuthor(), room->getUser(nullptr));
}
void EventHandlerTest::authorDisplayName() void EventHandlerTest::authorDisplayName()
{ {
auto event = room->messageEvents().at(1).get(); auto event = room->messageEvents().at(1).get();
@@ -139,6 +194,15 @@ void EventHandlerTest::authorDisplayName()
QCOMPARE(eventHandler.getAuthorDisplayName(), QStringLiteral("before")); QCOMPARE(eventHandler.getAuthorDisplayName(), QStringLiteral("before"));
} }
void EventHandlerTest::nullAuthorDisplayName()
{
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getAuthorDisplayName(), QString());
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getAuthorDisplayName(), QString());
}
void EventHandlerTest::time() void EventHandlerTest::time()
{ {
auto event = room->messageEvents().at(0).get(); auto event = room->messageEvents().at(0).get();
@@ -148,6 +212,16 @@ void EventHandlerTest::time()
QCOMPARE(eventHandler.getTime(true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)); QCOMPARE(eventHandler.getTime(true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC));
} }
void EventHandlerTest::nullTime()
{
QTest::ignoreMessage(QtWarningMsg, "getTime called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getTime(), QDateTime());
eventHandler.setEvent(room->messageEvents().at(0).get());
QTest::ignoreMessage(QtWarningMsg, "a value must be provided for lastUpdated for a pending event.");
QCOMPARE(eventHandler.getTime(true), QDateTime());
}
void EventHandlerTest::timeString() void EventHandlerTest::timeString()
{ {
auto event = room->messageEvents().at(0).get(); auto event = room->messageEvents().at(0).get();
@@ -169,6 +243,16 @@ void EventHandlerTest::timeString()
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::LongFormat)); format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::LongFormat));
} }
void EventHandlerTest::nullTimeString()
{
QTest::ignoreMessage(QtWarningMsg, "getTimeString called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getTimeString(false), QString());
eventHandler.setEvent(room->messageEvents().at(0).get());
QTest::ignoreMessage(QtWarningMsg, "a value must be provided for lastUpdated for a pending event.");
QCOMPARE(eventHandler.getTimeString(false, QLocale::ShortFormat, true), QString());
}
void EventHandlerTest::highlighted() void EventHandlerTest::highlighted()
{ {
auto event = room->messageEvents().at(2).get(); auto event = room->messageEvents().at(2).get();
@@ -182,6 +266,15 @@ void EventHandlerTest::highlighted()
QCOMPARE(eventHandler.isHighlighted(), false); QCOMPARE(eventHandler.isHighlighted(), false);
} }
void EventHandlerTest::nullHighlighted()
{
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with m_room set to nullptr.");
QCOMPARE(emptyHandler.isHighlighted(), false);
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with m_event set to nullptr.");
QCOMPARE(noEventHandler.isHighlighted(), false);
}
void EventHandlerTest::hidden() void EventHandlerTest::hidden()
{ {
auto event = room->messageEvents().at(3).get(); auto event = room->messageEvents().at(3).get();
@@ -195,6 +288,15 @@ void EventHandlerTest::hidden()
QCOMPARE(eventHandler.isHidden(), false); QCOMPARE(eventHandler.isHidden(), false);
} }
void EventHandlerTest::nullHidden()
{
QTest::ignoreMessage(QtWarningMsg, "isHidden called with m_room set to nullptr.");
QCOMPARE(emptyHandler.isHidden(), false);
QTest::ignoreMessage(QtWarningMsg, "isHidden called with m_event set to nullptr.");
QCOMPARE(noEventHandler.isHidden(), false);
}
void EventHandlerTest::body() void EventHandlerTest::body()
{ {
auto event = room->messageEvents().at(0).get(); auto event = room->messageEvents().at(0).get();
@@ -206,6 +308,15 @@ void EventHandlerTest::body()
QCOMPARE(eventHandler.getPlainBody(true), QStringLiteral("This is an example text message")); QCOMPARE(eventHandler.getPlainBody(true), QStringLiteral("This is an example text message"));
} }
void EventHandlerTest::nullBody()
{
QTest::ignoreMessage(QtWarningMsg, "getRichBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getRichBody(), QString());
QTest::ignoreMessage(QtWarningMsg, "getPlainBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getPlainBody(), QString());
}
void EventHandlerTest::genericBody_data() void EventHandlerTest::genericBody_data()
{ {
QTest::addColumn<int>("eventNum"); QTest::addColumn<int>("eventNum");
@@ -228,6 +339,12 @@ void EventHandlerTest::genericBody()
QCOMPARE(eventHandler.getGenericBody(), output); QCOMPARE(eventHandler.getGenericBody(), output);
} }
void EventHandlerTest::nullGenericBody()
{
QTest::ignoreMessage(QtWarningMsg, "getGenericBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getGenericBody(), QString());
}
void EventHandlerTest::mediaInfo() void EventHandlerTest::mediaInfo()
{ {
auto event = room->messageEvents().at(4).get(); auto event = room->messageEvents().at(4).get();
@@ -251,6 +368,15 @@ void EventHandlerTest::mediaInfo()
QCOMPARE(thumbnailInfo["height"_ls], 450); QCOMPARE(thumbnailInfo["height"_ls], 450);
} }
void EventHandlerTest::nullMediaInfo()
{
QTest::ignoreMessage(QtWarningMsg, "getMediaInfo called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getMediaInfo(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "getMediaInfo called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getMediaInfo(), QVariantMap());
}
void EventHandlerTest::linkPreviewer() void EventHandlerTest::linkPreviewer()
{ {
auto event = room->messageEvents().at(2).get(); auto event = room->messageEvents().at(2).get();
@@ -264,6 +390,15 @@ void EventHandlerTest::linkPreviewer()
QCOMPARE(eventHandler.getLinkPreviewer(), nullptr); QCOMPARE(eventHandler.getLinkPreviewer(), nullptr);
} }
void EventHandlerTest::nullLinkPreviewer()
{
QTest::ignoreMessage(QtWarningMsg, "getLinkPreviewer called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getLinkPreviewer(), nullptr);
QTest::ignoreMessage(QtWarningMsg, "getLinkPreviewer called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getLinkPreviewer(), nullptr);
}
void EventHandlerTest::reactions() void EventHandlerTest::reactions()
{ {
auto event = room->messageEvents().at(0).get(); auto event = room->messageEvents().at(0).get();
@@ -272,6 +407,15 @@ void EventHandlerTest::reactions()
QCOMPARE(eventHandler.getReactions()->rowCount(), 1); QCOMPARE(eventHandler.getReactions()->rowCount(), 1);
} }
void EventHandlerTest::nullReactions()
{
QTest::ignoreMessage(QtWarningMsg, "getReactions called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReactions(), nullptr);
QTest::ignoreMessage(QtWarningMsg, "getReactions called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReactions(), nullptr);
}
void EventHandlerTest::hasReply() void EventHandlerTest::hasReply()
{ {
auto event = room->messageEvents().at(5).get(); auto event = room->messageEvents().at(5).get();
@@ -285,6 +429,12 @@ void EventHandlerTest::hasReply()
QCOMPARE(eventHandler.hasReply(), false); QCOMPARE(eventHandler.hasReply(), false);
} }
void EventHandlerTest::nullHasReply()
{
QTest::ignoreMessage(QtWarningMsg, "hasReply called with m_event set to nullptr.");
QCOMPARE(noEventHandler.hasReply(), false);
}
void EventHandlerTest::replyId() void EventHandlerTest::replyId()
{ {
auto event = room->messageEvents().at(5).get(); auto event = room->messageEvents().at(5).get();
@@ -298,6 +448,12 @@ void EventHandlerTest::replyId()
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("")); QCOMPARE(eventHandler.getReplyId(), QStringLiteral(""));
} }
void EventHandlerTest::nullReplyId()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyId called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyId(), QString());
}
void EventHandlerTest::replyDelegateType() void EventHandlerTest::replyDelegateType()
{ {
auto event = room->messageEvents().at(5).get(); auto event = room->messageEvents().at(5).get();
@@ -311,6 +467,15 @@ void EventHandlerTest::replyDelegateType()
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Other); QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Other);
} }
void EventHandlerTest::nullReplyDelegateType()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyDelegateType called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyDelegateType(), DelegateType::Other);
QTest::ignoreMessage(QtWarningMsg, "getReplyDelegateType called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyDelegateType(), DelegateType::Other);
}
void EventHandlerTest::replyAuthor() void EventHandlerTest::replyAuthor()
{ {
auto event = room->messageEvents().at(5).get(); auto event = room->messageEvents().at(5).get();
@@ -334,6 +499,15 @@ void EventHandlerTest::replyAuthor()
QCOMPARE(eventHandler.getReplyAuthor(), room->getUser(nullptr)); QCOMPARE(eventHandler.getReplyAuthor(), room->getUser(nullptr));
} }
void EventHandlerTest::nullReplyAuthor()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyAuthor(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_event set to nullptr. Returning empty user.");
QCOMPARE(noEventHandler.getReplyAuthor(), room->getUser(nullptr));
}
void EventHandlerTest::replyBody() void EventHandlerTest::replyBody()
{ {
auto event = room->messageEvents().at(5).get(); auto event = room->messageEvents().at(5).get();
@@ -345,6 +519,15 @@ void EventHandlerTest::replyBody()
QCOMPARE(eventHandler.getReplyPlainBody(true), QStringLiteral("This is an example text message")); QCOMPARE(eventHandler.getReplyPlainBody(true), QStringLiteral("This is an example text message"));
} }
void EventHandlerTest::nullReplyBody()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyRichBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyRichBody(), QString());
QTest::ignoreMessage(QtWarningMsg, "getReplyPlainBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyPlainBody(), QString());
}
void EventHandlerTest::replyMediaInfo() void EventHandlerTest::replyMediaInfo()
{ {
auto event = room->messageEvents().at(6).get(); auto event = room->messageEvents().at(6).get();
@@ -369,6 +552,15 @@ void EventHandlerTest::replyMediaInfo()
QCOMPARE(thumbnailInfo["height"_ls], 450); QCOMPARE(thumbnailInfo["height"_ls], 450);
} }
void EventHandlerTest::nullReplyMediaInfo()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyMediaInfo called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyMediaInfo(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "getReplyMediaInfo called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyMediaInfo(), QVariantMap());
}
void EventHandlerTest::thread() void EventHandlerTest::thread()
{ {
auto event = room->messageEvents().at(0).get(); auto event = room->messageEvents().at(0).get();
@@ -392,6 +584,15 @@ void EventHandlerTest::thread()
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$threadmessage1:example.org")); QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$threadmessage1:example.org"));
} }
void EventHandlerTest::nullThread()
{
QTest::ignoreMessage(QtWarningMsg, "isThreaded called with m_event set to nullptr.");
QCOMPARE(emptyHandler.isThreaded(), false);
QTest::ignoreMessage(QtWarningMsg, "threadRoot called with m_event set to nullptr.");
QCOMPARE(noEventHandler.threadRoot(), QString());
}
void EventHandlerTest::location() void EventHandlerTest::location()
{ {
auto event = room->messageEvents().at(7).get(); auto event = room->messageEvents().at(7).get();
@@ -402,6 +603,18 @@ void EventHandlerTest::location()
QCOMPARE(eventHandler.getLocationAssetType(), QStringLiteral("m.pin")); QCOMPARE(eventHandler.getLocationAssetType(), QStringLiteral("m.pin"));
} }
void EventHandlerTest::nullLocation()
{
QTest::ignoreMessage(QtWarningMsg, "getLatitude called with m_event set to nullptr.");
QCOMPARE(emptyHandler.getLatitude(), -100.0);
QTest::ignoreMessage(QtWarningMsg, "getLongitude called with m_event set to nullptr.");
QCOMPARE(emptyHandler.getLongitude(), -200.0);
QTest::ignoreMessage(QtWarningMsg, "getLocationAssetType called with m_event set to nullptr.");
QCOMPARE(emptyHandler.getLocationAssetType(), QString());
}
void EventHandlerTest::readMarkers() void EventHandlerTest::readMarkers()
{ {
auto event = room->messageEvents().at(0).get(); auto event = room->messageEvents().at(0).get();
@@ -431,5 +644,37 @@ void EventHandlerTest::readMarkers()
QCOMPARE(eventHandler.getReadMarkersString().startsWith(QStringLiteral("6 users:")), true); QCOMPARE(eventHandler.getReadMarkersString().startsWith(QStringLiteral("6 users:")), true);
} }
void EventHandlerTest::nullReadMarkers()
{
QTest::ignoreMessage(QtWarningMsg, "hasReadMarkers called with m_room set to nullptr.");
QCOMPARE(emptyHandler.hasReadMarkers(), false);
QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReadMarkers(), QVariantList());
QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getNumberExcessReadMarkers(), QString());
QTest::ignoreMessage(QtWarningMsg, "getReadMarkersString called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReadMarkersString(), QString());
QTest::ignoreMessage(QtWarningMsg, "hasReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.hasReadMarkers(), false);
QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReadMarkers(), QVariantList());
QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getNumberExcessReadMarkers(), QString());
QTest::ignoreMessage(QtWarningMsg, "getReadMarkersString called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReadMarkersString(), QString());
}
void EventHandlerTest::cleanup()
{
eventHandler.setEvent(nullptr);
}
QTEST_MAIN(EventHandlerTest) QTEST_MAIN(EventHandlerTest)
#include "eventhandlertest.moc" #include "eventhandlertest.moc"

View File

@@ -42,101 +42,18 @@ void NeoChatRoomTest::initTestCase()
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org")); connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join); room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
auto json = QJsonDocument::fromJson(R"EVENT({ QFile testMinSyncFile;
"account_data": { testMinSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-min-sync.json"));
"events": [ testMinSyncFile.open(QIODevice::ReadOnly);
{ const auto testMinSyncJson = QJsonDocument::fromJson(testMinSyncFile.readAll());
"content": { SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testMinSyncJson.object());
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
},
{
"content": {
"custom_config_key": "custom_config_value"
},
"type": "org.example.custom.room.config"
}
]
},
"ephemeral": {
"events": [
{
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.typing"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an **example** text message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>",
"msgtype": "m.text"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1235
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
})EVENT");
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
room->update(std::move(roomData)); room->update(std::move(roomData));
} }
void NeoChatRoomTest::subtitleTextTest() void NeoChatRoomTest::subtitleTextTest()
{ {
QCOMPARE(room->timelineSize(), 1); QCOMPARE(room->timelineSize(), 1);
QCOMPARE(room->lastEventToString(), QStringLiteral("@example:example.org: This is an example text message")); QCOMPARE(room->lastEventToString(), QStringLiteral("@example:example.org: This is an example\ntext message"));
} }
void NeoChatRoomTest::eventTest() void NeoChatRoomTest::eventTest()

View File

@@ -40,6 +40,7 @@ private Q_SLOTS:
void stripDisallowedTags(); void stripDisallowedTags();
void stripDisallowedAttributes(); void stripDisallowedAttributes();
void emptyCodeTags(); void emptyCodeTags();
void formatBlockQuote();
void sendSimpleStringCase(); void sendSimpleStringCase();
void sendSingleParaMarkup(); void sendSingleParaMarkup();
@@ -66,6 +67,7 @@ private Q_SLOTS:
void receiveRichEdited_data(); void receiveRichEdited_data();
void receiveRichEdited(); void receiveRichEdited();
void receiveLineSeparator(); void receiveLineSeparator();
void receiveRichCodeUrl();
void linkPreviewsMatch_data(); void linkPreviewsMatch_data();
void linkPreviewsMatch(); void linkPreviewsMatch();
@@ -78,131 +80,11 @@ void TextHandlerTest::initTestCase()
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org")); connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join); room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
const auto json = QJsonDocument::fromJson(R"EVENT({ QFile testTextHandlerSyncFile;
"account_data": { testTextHandlerSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-texthandler-sync.json"));
"events": [ testTextHandlerSyncFile.open(QIODevice::ReadOnly);
{ const auto testTextHandlerSyncJson = QJsonDocument::fromJson(testTextHandlerSyncFile.readAll());
"content": { SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testTextHandlerSyncJson.object());
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
},
{
"content": {
"custom_config_key": "custom_config_value"
},
"type": "org.example.custom.room.config"
}
]
},
"ephemeral": {
"events": [
{
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.typing"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an **example** text message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>",
"msgtype": "m.text"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "/me This is an emote.",
"format": "org.matrix.custom.html",
"formatted_body": "This is an emote.",
"msgtype": "m.emote"
},
"event_id": "$153273582443PhrSn:example.org",
"origin_server_ts": 1532735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1231
}
},
{
"content": {
"body": "tested",
"msgtype": "m.text"
},
"event_id": "$zrCiBxBnqqTn0Z5FY78qSZAszno_w8nJJXzfBULG-3E",
"origin_server_ts": 1680948575928,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1747776,
"m.relations": {
"m.replace": {
"event_id": "$UX0PlpyI7vYO32iHMuuYEP7ECMh4sX3XLGiB2SwM4mQ",
"origin_server_ts": 1680948580992,
"sender": "@example:example.org"
}
}
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
})EVENT");
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
room->update(std::move(roomData)); room->update(std::move(roomData));
} }
@@ -265,6 +147,16 @@ void TextHandlerTest::emptyCodeTags()
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString); QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
} }
void TextHandlerTest::formatBlockQuote()
{
auto input = QStringLiteral("<blockquote>\n<p>Lorem Ispum</p>\n</blockquote>");
auto expectedOutput = QStringLiteral("<blockquote><table><tr><td>\u201CLorem Ispum\u201D</td></tr></table></blockquote>");
TextHandler testTextHandler;
testTextHandler.setData(input);
QCOMPARE(testTextHandler.handleRecieveRichText(), expectedOutput);
}
void TextHandlerTest::sendSimpleStringCase() void TextHandlerTest::sendSimpleStringCase()
{ {
const QString testInputString = QStringLiteral("This data should just be put in a paragraph."); const QString testInputString = QStringLiteral("This data should just be put in a paragraph.");
@@ -577,8 +469,9 @@ void TextHandlerTest::receiveRichEdited_data()
QTest::newRow("basic") << QStringLiteral("Edited") << QStringLiteral("Edited <span style=\"color:#000000\">(edited)</span>"); QTest::newRow("basic") << QStringLiteral("Edited") << QStringLiteral("Edited <span style=\"color:#000000\">(edited)</span>");
QTest::newRow("multiple paragraphs") << QStringLiteral("<p>Edited</p>\n<p>Edited</p>") QTest::newRow("multiple paragraphs") << QStringLiteral("<p>Edited</p>\n<p>Edited</p>")
<< QStringLiteral("<p>Edited</p>\n<p>Edited <span style=\"color:#000000\">(edited)</span></p>"); << QStringLiteral("<p>Edited</p>\n<p>Edited <span style=\"color:#000000\">(edited)</span></p>");
QTest::newRow("blockquote") << QStringLiteral("<blockquote>Edited</blockquote>") QTest::newRow("blockquote")
<< QStringLiteral("<blockquote>Edited</blockquote><p> <span style=\"color:#000000\">(edited)</span></p>"); << QStringLiteral("<blockquote>Edited</blockquote>")
<< QStringLiteral("<blockquote><table><tr><td>\u201CEdited\u201D</td></tr></table></blockquote><p> <span style=\"color:#000000\">(edited)</span></p>");
} }
void TextHandlerTest::receiveRichEdited() void TextHandlerTest::receiveRichEdited()
@@ -647,5 +540,13 @@ void TextHandlerTest::linkPreviewsReject()
QCOMPARE(testTextHandler.getLinkPreviews(), testOutputLinks); QCOMPARE(testTextHandler.getLinkPreviews(), testOutputLinks);
} }
void TextHandlerTest::receiveRichCodeUrl()
{
auto input = QStringLiteral("<code>https://kde.org</code>");
TextHandler testTextHandler;
testTextHandler.setData(input);
QCOMPARE(testTextHandler.handleRecieveRichText(), input);
}
QTEST_MAIN(TextHandlerTest) QTEST_MAIN(TextHandlerTest)
#include "texthandlertest.moc" #include "texthandlertest.moc"

View File

@@ -343,6 +343,7 @@ to provide a convergent experience across multiple platforms.</p>
<content_attribute id="social-chat">intense</content_attribute> <content_attribute id="social-chat">intense</content_attribute>
</content_rating> </content_rating>
<releases> <releases>
<release version="23.08.3" date="2023-11-09"/>
<release version="23.08.2" date="2023-10-12"/> <release version="23.08.2" date="2023-10-12"/>
<release version="23.08.0" date="2023-08-24"> <release version="23.08.0" date="2023-08-24">
<url>https://kde.org/announcements/gear/23.08.0/#neochathttpsappskdeorgneochat</url> <url>https://kde.org/announcements/gear/23.08.0/#neochathttpsappskdeorgneochat</url>

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -146,13 +146,14 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/main.qml qml/main.qml
qml/AccountMenu.qml qml/AccountMenu.qml
qml/ExploreComponent.qml qml/ExploreComponent.qml
qml/ExploreComponentMobile.qml
qml/ContextMenu.qml qml/ContextMenu.qml
qml/CollapsedRoomDelegate.qml qml/CollapsedRoomDelegate.qml
qml/RoomDelegate.qml qml/RoomDelegate.qml
qml/RoomListPage.qml qml/RoomListPage.qml
qml/SpaceListContextMenu.qml qml/SpaceListContextMenu.qml
qml/UserInfo.qml qml/UserInfo.qml
qml/LoadingPage.qml qml/UserInfoDesktop.qml
qml/RoomPage.qml qml/RoomPage.qml
qml/RoomWindow.qml qml/RoomWindow.qml
qml/JoinRoomPage.qml qml/JoinRoomPage.qml
@@ -163,7 +164,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/ImageEditorPage.qml qml/ImageEditorPage.qml
qml/WelcomePage.qml qml/WelcomePage.qml
qml/General.qml qml/General.qml
qml/Security.qml qml/RoomSecurity.qml
qml/PushNotification.qml qml/PushNotification.qml
qml/Categories.qml qml/Categories.qml
qml/Permissions.qml qml/Permissions.qml
@@ -286,6 +287,9 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/SpaceHierarchyDelegate.qml qml/SpaceHierarchyDelegate.qml
qml/RemoveChildDialog.qml qml/RemoveChildDialog.qml
qml/SelectParentDialog.qml qml/SelectParentDialog.qml
qml/Security.qml
qml/QrCodeMaximizeComponent.qml
qml/SelectSpacesDialog.qml
RESOURCES RESOURCES
qml/confetti.png qml/confetti.png
qml/glowdot.png qml/glowdot.png
@@ -307,6 +311,13 @@ ecm_qt_declare_logging_category(neochat
DEFAULT_SEVERITY Info DEFAULT_SEVERITY Info
) )
ecm_qt_declare_logging_category(neochat
HEADER "chatdocumenthandler_logging.h"
IDENTIFIER "ChatDocumentHandling"
CATEGORY_NAME "org.kde.neochat.chatdocumenthandler"
DEFAULT_SEVERITY Info
)
add_executable(neochat-app add_executable(neochat-app
main.cpp main.cpp
) )
@@ -345,7 +356,27 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
endif() endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models ${CMAKE_CURRENT_SOURCE_DIR}/enums) target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models ${CMAKE_CURRENT_SOURCE_DIR}/enums)
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::ColorScheme 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
KF6::I18n
KF6::Kirigami2
KF6::Notifications
KF6::ConfigCore
KF6::ConfigGui
KF6::CoreAddons
KF6::SonnetCore
KF6::ColorScheme
KF6::ItemModels
QuotientQt6
cmark::cmark
QCoro::Core
)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc) kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
@@ -433,6 +464,7 @@ if(ANDROID)
"gps" "gps"
"system-users-symbolic" "system-users-symbolic"
) )
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR/android})
else() else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets) target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets)
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR}) install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})

View File

@@ -75,7 +75,7 @@ QString ActionsHandler::handleMentions(QString handledText, QList<Mention> *ment
} }
handledText = handledText.replace(mention.cursor.anchor(), handledText = handledText.replace(mention.cursor.anchor(),
mention.cursor.position() - mention.cursor.anchor(), mention.cursor.position() - mention.cursor.anchor(),
QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text, mention.id)); QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text.toHtmlEscaped(), mention.id));
} }
mentions->clear(); mentions->clear();

View File

@@ -173,3 +173,5 @@ void ChatBarCache::setSavedText(const QString &savedText)
{ {
m_savedText = savedText; m_savedText = savedText;
} }
#include "moc_chatbarcache.cpp"

View File

@@ -14,6 +14,8 @@
#include <Sonnet/BackgroundChecker> #include <Sonnet/BackgroundChecker>
#include <Sonnet/Settings> #include <Sonnet/Settings>
#include "chatdocumenthandler_logging.h"
class SyntaxHighlighter : public QSyntaxHighlighter class SyntaxHighlighter : public QSyntaxHighlighter
{ {
public: public:
@@ -98,7 +100,7 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
, m_document(nullptr) , m_document(nullptr)
, m_cursorPosition(-1) , m_cursorPosition(-1)
, m_highlighter(new SyntaxHighlighter(this)) , m_highlighter(new SyntaxHighlighter(this))
, m_completionModel(new CompletionModel()) , m_completionModel(new CompletionModel(this))
{ {
connect(this, &ChatDocumentHandler::roomChanged, this, [this]() { connect(this, &ChatDocumentHandler::roomChanged, this, [this]() {
m_completionModel->setRoom(m_room); m_completionModel->setRoom(m_room);
@@ -148,20 +150,6 @@ int ChatDocumentHandler::completionStartIndex() const
return start; return start;
} }
bool ChatDocumentHandler::isEdit() const
{
return m_isEdit;
}
void ChatDocumentHandler::setIsEdit(bool edit)
{
if (edit == m_isEdit) {
return;
}
m_isEdit = edit;
Q_EMIT isEditChanged();
}
QQuickTextDocument *ChatDocumentHandler::document() const QQuickTextDocument *ChatDocumentHandler::document() const
{ {
return m_document; return m_document;
@@ -227,6 +215,15 @@ void ChatDocumentHandler::setChatBarCache(ChatBarCache *chatBarCache)
void ChatDocumentHandler::complete(int index) void ChatDocumentHandler::complete(int index)
{ {
if (m_document == nullptr) {
qCWarning(ChatDocumentHandling) << "complete called with m_document set to nullptr.";
return;
}
if (m_completionModel->autoCompletionType() == CompletionModel::None) {
qCWarning(ChatDocumentHandling) << "complete called with m_completionModel->autoCompletionType() == CompletionModel::None.";
return;
}
if (m_completionModel->autoCompletionType() == CompletionModel::User) { if (m_completionModel->autoCompletionType() == CompletionModel::User) {
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::DisplayNameRole).toString(); auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::DisplayNameRole).toString();
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString(); auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
@@ -310,15 +307,17 @@ void ChatDocumentHandler::setSelectionEnd(int position)
QString ChatDocumentHandler::getText() const QString ChatDocumentHandler::getText() const
{ {
if (!m_room) { if (!m_chatBarCache) {
return QString(); qCWarning(ChatDocumentHandling) << "getText called with m_chatBarCache set to nullptr.";
return {};
} }
return m_chatBarCache->text(); return m_chatBarCache->text();
} }
void ChatDocumentHandler::pushMention(const Mention mention) const void ChatDocumentHandler::pushMention(const Mention mention) const
{ {
if (!m_room) { if (!m_chatBarCache) {
qCWarning(ChatDocumentHandling) << "pushMention called with m_chatBarCache set to nullptr.";
return; return;
} }
m_chatBarCache->mentions()->push_back(mention); m_chatBarCache->mentions()->push_back(mention);

View File

@@ -62,14 +62,6 @@ class ChatDocumentHandler : public QObject
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
/**
* @brief Is the instance being used to handle an edit message.
*
* This is needed to ensure that the text and mentions are saved and retrieved
* from the correct parameters in the assigned room.
*/
Q_PROPERTY(bool isEdit READ isEdit WRITE setIsEdit NOTIFY isEditChanged)
/** /**
* @brief The QQuickTextDocument that is being handled. * @brief The QQuickTextDocument that is being handled.
*/ */
@@ -96,7 +88,7 @@ class ChatDocumentHandler : public QObject
* This is typically provided to a qml component to visualise the current * This is typically provided to a qml component to visualise the current
* completion results. * completion results.
*/ */
Q_PROPERTY(CompletionModel *completionModel READ completionModel NOTIFY completionModelChanged) Q_PROPERTY(CompletionModel *completionModel READ completionModel CONSTANT)
/** /**
* @brief The current room that the the text document is being handled for. * @brief The current room that the the text document is being handled for.
@@ -121,9 +113,6 @@ class ChatDocumentHandler : public QObject
public: public:
explicit ChatDocumentHandler(QObject *parent = nullptr); explicit ChatDocumentHandler(QObject *parent = nullptr);
[[nodiscard]] bool isEdit() const;
void setIsEdit(bool edit);
[[nodiscard]] QQuickTextDocument *document() const; [[nodiscard]] QQuickTextDocument *document() const;
void setDocument(QQuickTextDocument *document); void setDocument(QQuickTextDocument *document);
@@ -144,7 +133,6 @@ public:
Q_INVOKABLE void complete(int index); Q_INVOKABLE void complete(int index);
void updateCompletions();
CompletionModel *completionModel() const; CompletionModel *completionModel() const;
[[nodiscard]] QColor mentionColor() const; [[nodiscard]] QColor mentionColor() const;
@@ -154,12 +142,10 @@ public:
void setErrorColor(const QColor &color); void setErrorColor(const QColor &color);
Q_SIGNALS: Q_SIGNALS:
void isEditChanged();
void documentChanged(); void documentChanged();
void cursorPositionChanged(); void cursorPositionChanged();
void roomChanged(); void roomChanged();
void chatBarCacheChanged(); void chatBarCacheChanged();
void completionModelChanged();
void selectionStartChanged(); void selectionStartChanged();
void selectionEndChanged(); void selectionEndChanged();
void errorColorChanged(); void errorColorChanged();
@@ -168,8 +154,6 @@ Q_SIGNALS:
private: private:
int completionStartIndex() const; int completionStartIndex() const;
bool m_isEdit = false;
QPointer<QQuickTextDocument> m_document; QPointer<QQuickTextDocument> m_document;
QPointer<NeoChatRoom> m_room; QPointer<NeoChatRoom> m_room;

View File

@@ -6,13 +6,8 @@
#include <qt6keychain/keychain.h> #include <qt6keychain/keychain.h>
#include <KConfig>
#include <KConfigGroup>
#include <KLocalizedString> #include <KLocalizedString>
#include <KWindowConfig> #include <KWindowConfig>
#ifdef HAVE_WINDOWSYSTEM
#include <KWindowEffects>
#endif
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
@@ -65,7 +60,8 @@ Controller::Controller(QObject *parent)
invokeLogin(); invokeLogin();
}); });
QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [] { QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] {
delete m_trayIcon;
NeoChatConfig::self()->save(); NeoChatConfig::self()->save();
}); });
@@ -103,10 +99,6 @@ Controller::Controller(QObject *parent)
} }
oldAccountCount = m_accountRegistry.size(); oldAccountCount = m_accountRegistry.size();
}); });
QTimer::singleShot(0, this, [this] {
m_pushRuleModel = new PushRuleModel;
});
} }
Controller &Controller::instance() Controller &Controller::instance()
@@ -175,6 +167,8 @@ void Controller::invokeLogin()
QString id = NeoChatConfig::self()->activeConnection(); QString id = NeoChatConfig::self()->activeConnection();
for (const auto &accountId : accounts) { for (const auto &accountId : accounts) {
AccountSettings account{accountId}; AccountSettings account{accountId};
m_accountsLoading += accountId;
Q_EMIT accountsLoadingChanged();
if (id.isEmpty()) { if (id.isEmpty()) {
// handle case where the account config is empty // handle case where the account config is empty
id = accountId; id = accountId;
@@ -194,6 +188,8 @@ void Controller::invokeLogin()
connect(connection, &NeoChatConnection::connected, this, [this, connection, id] { connect(connection, &NeoChatConnection::connected, this, [this, connection, id] {
connection->loadState(); connection->loadState();
addConnection(connection); addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
Q_EMIT accountsLoadingChanged();
if (connection->userId() == id) { if (connection->userId() == id) {
setActiveConnection(connection); setActiveConnection(connection);
connectSingleShot(connection, &NeoChatConnection::syncDone, this, &Controller::initiated); connectSingleShot(connection, &NeoChatConnection::syncDone, this, &Controller::initiated);
@@ -354,12 +350,6 @@ void Controller::setActiveConnection(NeoChatConnection *connection)
} }
NeoChatConfig::self()->save(); NeoChatConfig::self()->save();
Q_EMIT activeConnectionChanged(); Q_EMIT activeConnectionChanged();
Q_EMIT activeConnectionIndexChanged();
}
PushRuleModel *Controller::pushRuleModel() const
{
return m_pushRuleModel;
} }
void Controller::saveWindowGeometry() void Controller::saveWindowGeometry()
@@ -384,56 +374,6 @@ void Controller::joinRoom(const QString &alias)
RoomManager::instance().joinRoom(m_connection, alias, QStringList{knownServer}); RoomManager::instance().joinRoom(m_connection, alias, QStringList{knownServer});
} }
void Controller::openOrCreateDirectChat(User *user)
{
const auto existing = activeConnection()->directChats();
if (existing.contains(user)) {
const auto &room = static_cast<NeoChatRoom *>(activeConnection()->room(existing.value(user)));
if (room) {
RoomManager::instance().enterRoom(room);
return;
}
}
activeConnection()->requestDirectChat(user);
}
QString Controller::formatByteSize(double size, int precision) const
{
return QLocale().formattedDataSize(size, precision);
}
QString Controller::formatDuration(quint64 msecs, KFormat::DurationFormatOptions options) const
{
return KFormat().formatDuration(msecs, options);
}
void Controller::setBlur(QQuickItem *item, bool blur)
{
#ifdef HAVE_WINDOWSYSTEM
auto setWindows = [item, blur]() {
auto reg = QRect(QPoint(0, 0), item->window()->size());
KWindowEffects::enableBackgroundContrast(item->window(), blur, 1, 1, 1, reg);
KWindowEffects::enableBlurBehind(item->window(), blur, reg);
};
disconnect(item->window(), &QQuickWindow::heightChanged, this, nullptr);
disconnect(item->window(), &QQuickWindow::widthChanged, this, nullptr);
connect(item->window(), &QQuickWindow::heightChanged, this, setWindows);
connect(item->window(), &QQuickWindow::widthChanged, this, setWindows);
setWindows();
#endif
}
bool Controller::hasWindowSystem() const
{
#ifdef HAVE_WINDOWSYSTEM
return true;
#else
return false;
#endif
}
void Controller::forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item) void Controller::forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item)
{ {
// HACK: Workaround bug QTBUG 93281 // HACK: Workaround bug QTBUG 93281
@@ -470,14 +410,6 @@ void Controller::setApplicationProxy()
QNetworkProxy::setApplicationProxy(proxy); QNetworkProxy::setApplicationProxy(proxy);
} }
int Controller::activeConnectionIndex() const
{
auto result = std::find_if(m_accountRegistry.accounts().begin(), m_accountRegistry.accounts().end(), [this](const auto &it) {
return it == m_connection;
});
return result - m_accountRegistry.accounts().begin();
}
bool Controller::isFlatpak() const bool Controller::isFlatpak() const
{ {
#ifdef NEOCHAT_FLATPAK #ifdef NEOCHAT_FLATPAK

View File

@@ -3,13 +3,10 @@
#pragma once #pragma once
#include "models/pushrulemodel.h"
#include <QObject> #include <QObject>
#include <QQmlEngine> #include <QQmlEngine>
#include <QQuickItem> #include <QQuickItem>
#include <KFormat>
#include "neochatconnection.h" #include "neochatconnection.h"
#include <Quotient/accountregistry.h> #include <Quotient/accountregistry.h>
#include <Quotient/jobs/basejob.h> #include <Quotient/jobs/basejob.h>
@@ -49,26 +46,11 @@ class Controller : public QObject
*/ */
Q_PROPERTY(NeoChatConnection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged) Q_PROPERTY(NeoChatConnection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
/**
* @brief The PushRuleModel that has the active connection's push rules.
*/
Q_PROPERTY(PushRuleModel *pushRuleModel READ pushRuleModel CONSTANT)
/**
* @brief The row number in the accounts directory of the active connection.
*/
Q_PROPERTY(int activeConnectionIndex READ activeConnectionIndex NOTIFY activeConnectionIndexChanged)
/** /**
* @brief Whether the OS NeoChat is running on supports sytem tray icons. * @brief Whether the OS NeoChat is running on supports sytem tray icons.
*/ */
Q_PROPERTY(bool supportSystemTray READ supportSystemTray CONSTANT) Q_PROPERTY(bool supportSystemTray READ supportSystemTray CONSTANT)
/**
* @brief Whether KWindowSystem specific features are available.
*/
Q_PROPERTY(bool hasWindowSystem READ hasWindowSystem CONSTANT)
/** /**
* @brief Whether NeoChat is currently able to connect to the server. * @brief Whether NeoChat is currently able to connect to the server.
*/ */
@@ -81,6 +63,8 @@ class Controller : public QObject
*/ */
Q_PROPERTY(bool isFlatpak READ isFlatpak CONSTANT) Q_PROPERTY(bool isFlatpak READ isFlatpak CONSTANT)
Q_PROPERTY(QStringList accountsLoading MEMBER m_accountsLoading NOTIFY accountsLoadingChanged)
public: public:
/** /**
* @brief Defines the status after an attempt to change the password on an account. * @brief Defines the status after an attempt to change the password on an account.
@@ -102,8 +86,6 @@ public:
void setActiveConnection(NeoChatConnection *connection); void setActiveConnection(NeoChatConnection *connection);
[[nodiscard]] NeoChatConnection *activeConnection() const; [[nodiscard]] NeoChatConnection *activeConnection() const;
[[nodiscard]] PushRuleModel *pushRuleModel() const;
/** /**
* @brief Add a new connection to the account registry. * @brief Add a new connection to the account registry.
*/ */
@@ -114,8 +96,6 @@ public:
*/ */
void dropConnection(NeoChatConnection *c); void dropConnection(NeoChatConnection *c);
int activeConnectionIndex() const;
/** /**
* @brief Save an access token to the keychain for the given account. * @brief Save an access token to the keychain for the given account.
*/ */
@@ -126,20 +106,8 @@ public:
*/ */
Q_INVOKABLE void joinRoom(const QString &alias); Q_INVOKABLE void joinRoom(const QString &alias);
/**
* @brief Join a direct chat with the given user.
*
* If a direct chat with the user doesn't exist one is created and then joined.
*/
Q_INVOKABLE void openOrCreateDirectChat(Quotient::User *user);
[[nodiscard]] bool supportSystemTray() const; [[nodiscard]] bool supportSystemTray() const;
/**
* @brief Set the background blur status of the given item.
*/
Q_INVOKABLE void setBlur(QQuickItem *item, bool blur);
bool isOnline() const; bool isOnline() const;
/** /**
@@ -151,20 +119,6 @@ public:
bool isFlatpak() const; bool isFlatpak() const;
/**
* @brief Return a string for the input timestamp.
*
* The output format depends on the KFormat::DurationFormatOptions chosen.
*
* @sa KFormat::DurationFormatOptions
*/
Q_INVOKABLE QString formatDuration(quint64 msecs, KFormat::DurationFormatOptions options = KFormat::DefaultDuration) const;
/**
* @brief Return a human readable string for a given input number of bytes.
*/
Q_INVOKABLE QString formatByteSize(double size, int precision = 1) const;
/** /**
* @brief Force a QQuickTextDocument to refresh when images are loaded. * @brief Force a QQuickTextDocument to refresh when images are loaded.
* *
@@ -187,10 +141,8 @@ private:
bool m_isOnline = true; bool m_isOnline = true;
QMap<Quotient::Room *, int> m_notificationCounts; QMap<Quotient::Room *, int> m_notificationCounts;
bool hasWindowSystem() const;
QPointer<PushRuleModel> m_pushRuleModel;
Quotient::AccountRegistry m_accountRegistry; Quotient::AccountRegistry m_accountRegistry;
QStringList m_accountsLoading;
private Q_SLOTS: private Q_SLOTS:
void invokeLogin(); void invokeLogin();
@@ -213,7 +165,7 @@ Q_SIGNALS:
void passwordStatus(Controller::PasswordStatus status); void passwordStatus(Controller::PasswordStatus status);
void userConsentRequired(QUrl url); void userConsentRequired(QUrl url);
void isOnlineChanged(bool isOnline); void isOnlineChanged(bool isOnline);
void activeConnectionIndexChanged(); void accountsLoadingChanged();
public Q_SLOTS: public Q_SLOTS:
void saveWindowGeometry(); void saveWindowGeometry();

View File

@@ -19,6 +19,7 @@
#include <Quotient/events/stickerevent.h> #include <Quotient/events/stickerevent.h>
#include <Quotient/quotient_common.h> #include <Quotient/quotient_common.h>
#include "delegatetype.h"
#include "eventhandler_logging.h" #include "eventhandler_logging.h"
#include "events/pollevent.h" #include "events/pollevent.h"
#include "linkpreviewer.h" #include "linkpreviewer.h"
@@ -50,6 +51,10 @@ const Quotient::Event *EventHandler::getEvent() const
void EventHandler::setEvent(const Quotient::RoomEvent *event) void EventHandler::setEvent(const Quotient::RoomEvent *event)
{ {
if (m_room == nullptr) {
qCWarning(EventHandling) << "cannot setEvent when m_room is set to nullptr.";
return;
}
if (event == m_event) { if (event == m_event) {
return; return;
} }
@@ -181,7 +186,7 @@ QDateTime EventHandler::getTime(bool isPending, QDateTime lastUpdated) const
QString EventHandler::getTimeString(bool relative, QLocale::FormatType format, bool isPending, QDateTime lastUpdated) const QString EventHandler::getTimeString(bool relative, QLocale::FormatType format, bool isPending, QDateTime lastUpdated) const
{ {
if (m_event == nullptr) { if (m_event == nullptr) {
qCWarning(EventHandling) << "getTime called with m_event set to nullptr."; qCWarning(EventHandling) << "getTimeString called with m_event set to nullptr.";
return {}; return {};
} }
if (isPending && lastUpdated == QDateTime()) { if (isPending && lastUpdated == QDateTime()) {
@@ -216,6 +221,15 @@ bool EventHandler::isHighlighted()
bool EventHandler::isHidden() bool EventHandler::isHidden()
{ {
if (m_room == nullptr) {
qCWarning(EventHandling) << "isHidden called with m_room set to nullptr.";
return false;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "isHidden called with m_event set to nullptr.";
return false;
}
if (m_event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) { if (m_event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
return true; return true;
} }
@@ -611,6 +625,14 @@ QString EventHandler::getGenericBody() const
QVariantMap EventHandler::getMediaInfo() const QVariantMap EventHandler::getMediaInfo() const
{ {
if (m_room == nullptr) {
qCWarning(EventHandling) << "getMediaInfo called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getMediaInfo called with m_event set to nullptr.";
return {};
}
return getMediaInfoForEvent(m_event); return getMediaInfoForEvent(m_event);
} }
@@ -723,6 +745,14 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
QSharedPointer<LinkPreviewer> EventHandler::getLinkPreviewer() const QSharedPointer<LinkPreviewer> EventHandler::getLinkPreviewer() const
{ {
if (m_room == nullptr) {
qCWarning(EventHandling) << "getLinkPreviewer called with m_room set to nullptr.";
return nullptr;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLinkPreviewer called with m_event set to nullptr.";
return nullptr;
}
if (!m_event->is<RoomMessageEvent>()) { if (!m_event->is<RoomMessageEvent>()) {
return nullptr; return nullptr;
} }
@@ -754,7 +784,7 @@ QSharedPointer<ReactionModel> EventHandler::getReactions() const
{ {
if (m_room == nullptr) { if (m_room == nullptr) {
qCWarning(EventHandling) << "getReactions called with m_room set to nullptr."; qCWarning(EventHandling) << "getReactions called with m_room set to nullptr.";
return {}; return nullptr;
} }
if (m_event == nullptr) { if (m_event == nullptr) {
qCWarning(EventHandling) << "getReactions called with m_event set to nullptr."; qCWarning(EventHandling) << "getReactions called with m_event set to nullptr.";
@@ -806,16 +836,33 @@ QSharedPointer<ReactionModel> EventHandler::getReactions() const
bool EventHandler::hasReply() const bool EventHandler::hasReply() const
{ {
if (m_event == nullptr) {
qCWarning(EventHandling) << "hasReply called with m_event set to nullptr.";
return false;
}
return !m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty(); return !m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
} }
QString EventHandler::getReplyId() const QString EventHandler::getReplyId() const
{ {
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyId called with m_event set to nullptr.";
return {};
}
return m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString(); return m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
} }
DelegateType::Type EventHandler::getReplyDelegateType() const DelegateType::Type EventHandler::getReplyDelegateType() const
{ {
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyDelegateType called with m_room set to nullptr.";
return DelegateType::Other;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyDelegateType called with m_event set to nullptr.";
return DelegateType::Other;
}
auto replyEvent = m_room->getReplyForEvent(*m_event); auto replyEvent = m_room->getReplyForEvent(*m_event);
if (replyEvent == nullptr) { if (replyEvent == nullptr) {
return DelegateType::Other; return DelegateType::Other;
@@ -903,6 +950,11 @@ QVariantMap EventHandler::getReplyMediaInfo() const
bool EventHandler::isThreaded() const bool EventHandler::isThreaded() const
{ {
if (m_event == nullptr) {
qCWarning(EventHandling) << "isThreaded called with m_event set to nullptr.";
return false;
}
return (m_event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls) return (m_event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& m_event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls) && m_event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls)
|| (!m_event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && m_event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls)); || (!m_event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && m_event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls));
@@ -910,6 +962,11 @@ bool EventHandler::isThreaded() const
QString EventHandler::threadRoot() const QString EventHandler::threadRoot() const
{ {
if (m_event == nullptr) {
qCWarning(EventHandling) << "threadRoot called with m_event set to nullptr.";
return {};
}
// Get the thread root ID from m.relates_to if it exists. // Get the thread root ID from m.relates_to if it exists.
if (m_event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls) if (m_event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& m_event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls) { && m_event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls) {
@@ -925,6 +982,11 @@ QString EventHandler::threadRoot() const
float EventHandler::getLatitude() const float EventHandler::getLatitude() const
{ {
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLatitude called with m_event set to nullptr.";
return -100.0;
}
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString(); const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) { if (geoUri.isEmpty()) {
return -100.0; // latitude runs from -90deg to +90deg so -100 is out of range. return -100.0; // latitude runs from -90deg to +90deg so -100 is out of range.
@@ -935,6 +997,11 @@ float EventHandler::getLatitude() const
float EventHandler::getLongitude() const float EventHandler::getLongitude() const
{ {
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLongitude called with m_event set to nullptr.";
return -200.0;
}
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString(); const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) { if (geoUri.isEmpty()) {
return -200.0; // longitude runs from -180deg to +180deg so -200 is out of range. return -200.0; // longitude runs from -180deg to +180deg so -200 is out of range.
@@ -945,6 +1012,11 @@ float EventHandler::getLongitude() const
QString EventHandler::getLocationAssetType() const QString EventHandler::getLocationAssetType() const
{ {
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLocationAssetType called with m_event set to nullptr.";
return {};
}
const auto assetType = m_event->contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString(); const auto assetType = m_event->contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
if (assetType.isEmpty()) { if (assetType.isEmpty()) {
return {}; return {};
@@ -954,6 +1026,15 @@ QString EventHandler::getLocationAssetType() const
bool EventHandler::hasReadMarkers() const bool EventHandler::hasReadMarkers() const
{ {
if (m_room == nullptr) {
qCWarning(EventHandling) << "hasReadMarkers called with m_room set to nullptr.";
return false;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "hasReadMarkers called with m_event set to nullptr.";
return false;
}
auto userIds = m_room->userIdsAtEvent(m_event->id()); auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id()); userIds.remove(m_room->localUser()->id());
return userIds.size() > 0; return userIds.size() > 0;
@@ -961,6 +1042,15 @@ bool EventHandler::hasReadMarkers() const
QVariantList EventHandler::getReadMarkers(int maxMarkers) const QVariantList EventHandler::getReadMarkers(int maxMarkers) const
{ {
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReadMarkers called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReadMarkers called with m_event set to nullptr.";
return {};
}
auto userIds_temp = m_room->userIdsAtEvent(m_event->id()); auto userIds_temp = m_room->userIdsAtEvent(m_event->id());
userIds_temp.remove(m_room->localUser()->id()); userIds_temp.remove(m_room->localUser()->id());
@@ -981,6 +1071,15 @@ QVariantList EventHandler::getReadMarkers(int maxMarkers) const
QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const
{ {
if (m_room == nullptr) {
qCWarning(EventHandling) << "getNumberExcessReadMarkers called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getNumberExcessReadMarkers called with m_event set to nullptr.";
return {};
}
auto userIds = m_room->userIdsAtEvent(m_event->id()); auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id()); userIds.remove(m_room->localUser()->id());
@@ -993,6 +1092,15 @@ QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const
QString EventHandler::getReadMarkersString() const QString EventHandler::getReadMarkersString() const
{ {
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReadMarkersString called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReadMarkersString called with m_event set to nullptr.";
return {};
}
auto userIds = m_room->userIdsAtEvent(m_event->id()); auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id()); userIds.remove(m_room->localUser()->id());

View File

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

View File

@@ -79,7 +79,17 @@ class LoginHelper : public QObject
Q_PROPERTY(bool isInvalidPassword READ isInvalidPassword NOTIFY isInvalidPasswordChanged) Q_PROPERTY(bool isInvalidPassword READ isInvalidPassword NOTIFY isInvalidPasswordChanged)
public: public:
explicit LoginHelper(QObject *parent = nullptr); static LoginHelper &instance()
{
static LoginHelper _instance;
return _instance;
}
static LoginHelper *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
Q_INVOKABLE void init(); Q_INVOKABLE void init();
@@ -125,6 +135,7 @@ Q_SIGNALS:
void isLoggingInChanged(); void isLoggingInChanged();
void isLoggedInChanged(); void isLoggedInChanged();
void isInvalidPasswordChanged(); void isInvalidPasswordChanged();
void loaded();
private: private:
void setHomeserverReachable(bool reachable); void setHomeserverReachable(bool reachable);
@@ -141,4 +152,5 @@ private:
bool m_isLoggingIn = false; bool m_isLoggingIn = false;
bool m_isLoggedIn = false; bool m_isLoggedIn = false;
bool m_invalidPassword = false; bool m_invalidPassword = false;
explicit LoginHelper(QObject *parent = nullptr);
}; };

View File

@@ -35,7 +35,6 @@
#include "neochat-version.h" #include "neochat-version.h"
#include <Quotient/networkaccessmanager.h> #include <Quotient/networkaccessmanager.h>
#include <Quotient/util.h>
#include "blurhashimageprovider.h" #include "blurhashimageprovider.h"
#include "colorschemer.h" #include "colorschemer.h"
@@ -132,7 +131,11 @@ int main(int argc, char *argv[])
i18n("Matrix client"), i18n("Matrix client"),
KAboutLicense::GPL_V3, KAboutLicense::GPL_V3,
i18n("© 2018-2020 Black Hat, 2020-2023 KDE Community")); i18n("© 2018-2020 Black Hat, 2020-2023 KDE Community"));
about.addAuthor(i18n("Carl Schwan"), i18n("Maintainer"), QStringLiteral("carl@carlschwan.eu"), QStringLiteral("https://carlschwan.eu")); about.addAuthor(i18n("Carl Schwan"),
i18n("Maintainer"),
QStringLiteral("carl@carlschwan.eu"),
QStringLiteral("https://carlschwan.eu"),
QStringLiteral("https://carlschwan.eu/avatar.png"));
about.addAuthor(i18n("Tobias Fella"), i18n("Maintainer"), QStringLiteral("tobias.fella@kde.org"), QStringLiteral("https://tobiasfella.de")); about.addAuthor(i18n("Tobias Fella"), i18n("Maintainer"), QStringLiteral("tobias.fella@kde.org"), QStringLiteral("https://tobiasfella.de"));
about.addAuthor(i18n("James Graham"), i18n("Maintainer"), QStringLiteral("james.h.graham@protonmail.com")); about.addAuthor(i18n("James Graham"), i18n("Maintainer"), QStringLiteral("james.h.graham@protonmail.com"));
about.addCredit(i18n("Black Hat"), i18n("Original author of Spectral"), QStringLiteral("bhat@encom.eu.org")); about.addCredit(i18n("Black Hat"), i18n("Original author of Spectral"), QStringLiteral("bhat@encom.eu.org"));
@@ -141,7 +144,7 @@ int main(int argc, char *argv[])
about.setOrganizationDomain("kde.org"); about.setOrganizationDomain("kde.org");
about.addComponent(QStringLiteral("libQuotient"), about.addComponent(QStringLiteral("libQuotient"),
i18n("A Qt5 library to write cross-platform clients for Matrix"), i18n("A Qt library to write cross-platform clients for Matrix"),
i18nc("<version number> (built against <possibly different version number>)", i18nc("<version number> (built against <possibly different version number>)",
"%1 (built against %2)", "%1 (built against %2)",
Quotient::versionString(), Quotient::versionString(),
@@ -220,7 +223,7 @@ int main(int argc, char *argv[])
}); });
} }
engine.addImageProvider(QLatin1String("mxc"), new MatrixImageProvider); engine.addImageProvider(QLatin1String("mxc"), MatrixImageProvider::create(&engine, &engine));
engine.addImageProvider(QLatin1String("blurhash"), new BlurhashImageProvider); engine.addImageProvider(QLatin1String("blurhash"), new BlurhashImageProvider);
engine.load(QUrl(QStringLiteral("qrc:/org/kde/neochat/qml/main.qml"))); engine.load(QUrl(QStringLiteral("qrc:/org/kde/neochat/qml/main.qml")));
@@ -233,8 +236,8 @@ int main(int argc, char *argv[])
} }
#ifdef HAVE_RUNNER #ifdef HAVE_RUNNER
Runner runner; auto runner = Runner::create(&engine, &engine);
QDBusConnection::sessionBus().registerObject("/RoomRunner"_ls, &runner, QDBusConnection::ExportScriptableContents); QDBusConnection::sessionBus().registerObject("/RoomRunner"_ls, runner, QDBusConnection::ExportScriptableContents);
#endif #endif
QWindow *window = windowFromEngine(&engine); QWindow *window = windowFromEngine(&engine);

View File

@@ -13,11 +13,13 @@
#include <KLocalizedString> #include <KLocalizedString>
#include "controller.h" #include "controller.h"
#include "neochatconnection.h"
#include <Quotient/connection.h> #include <Quotient/connection.h>
using namespace Quotient; using namespace Quotient;
ThumbnailResponse::ThumbnailResponse(QString id, QSize size) ThumbnailResponse::ThumbnailResponse(QString id, QSize size, NeoChatConnection *connection)
: mediaId(std::move(id)) : mediaId(std::move(id))
, requestedSize(size) , requestedSize(size)
, localFile(QStringLiteral("%1/image_provider/%2-%3x%4.png") , localFile(QStringLiteral("%1/image_provider/%2-%3x%4.png")
@@ -25,6 +27,7 @@ ThumbnailResponse::ThumbnailResponse(QString id, QSize size)
mediaId, mediaId,
QString::number(requestedSize.width()), QString::number(requestedSize.width()),
QString::number(requestedSize.height()))) QString::number(requestedSize.height())))
, m_connection(connection)
, errorStr("Image request hasn't started"_ls) , errorStr("Image request hasn't started"_ls)
{ {
if (requestedSize.isEmpty()) { if (requestedSize.isEmpty()) {
@@ -51,24 +54,24 @@ ThumbnailResponse::ThumbnailResponse(QString id, QSize size)
return; return;
} }
if (!Controller::instance().activeConnection()) { if (!m_connection) {
qWarning() << "Current connection is null"; qWarning() << "Current connection is null";
return; return;
} }
// Execute a request on the main thread asynchronously // Execute a request on the main thread asynchronously
moveToThread(Controller::instance().activeConnection()->thread()); moveToThread(m_connection->thread());
QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest, Qt::QueuedConnection); QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest, Qt::QueuedConnection);
} }
void ThumbnailResponse::startRequest() void ThumbnailResponse::startRequest()
{ {
if (!Controller::instance().activeConnection()) { if (!m_connection) {
return; return;
} }
// Runs in the main thread, not QML thread // Runs in the main thread, not QML thread
Q_ASSERT(QThread::currentThread() == Controller::instance().activeConnection()->thread()); Q_ASSERT(QThread::currentThread() == m_connection->thread());
job = Controller::instance().activeConnection()->getThumbnail(mediaId, requestedSize); job = m_connection->getThumbnail(mediaId, requestedSize);
// Connect to any possible outcome including abandonment // Connect to any possible outcome including abandonment
// to make sure the QML thread is not left stuck forever. // to make sure the QML thread is not left stuck forever.
connect(job, &BaseJob::finished, this, &ThumbnailResponse::prepareResult); connect(job, &BaseJob::finished, this, &ThumbnailResponse::prepareResult);
@@ -118,7 +121,7 @@ QString ThumbnailResponse::errorString() const
QQuickImageResponse *MatrixImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize) QQuickImageResponse *MatrixImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{ {
return new ThumbnailResponse(id, requestedSize); return new ThumbnailResponse(id, requestedSize, m_connection);
} }
#include "moc_matriximageprovider.cpp" #include "moc_matriximageprovider.cpp"

View File

@@ -10,6 +10,8 @@
#include <QReadWriteLock> #include <QReadWriteLock>
class NeoChatConnection;
/** /**
* @class ThumbnailResponse * @class ThumbnailResponse
* *
@@ -21,7 +23,7 @@ class ThumbnailResponse : public QQuickImageResponse
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit ThumbnailResponse(QString mediaId, QSize requestedSize); explicit ThumbnailResponse(QString mediaId, QSize requestedSize, NeoChatConnection *m_connection);
~ThumbnailResponse() override = default; ~ThumbnailResponse() override = default;
private Q_SLOTS: private Q_SLOTS:
@@ -33,6 +35,7 @@ private:
QSize requestedSize; QSize requestedSize;
const QString localFile; const QString localFile;
Quotient::MediaThumbnailJob *job = nullptr; Quotient::MediaThumbnailJob *job = nullptr;
NeoChatConnection *m_connection;
QImage image; QImage image;
QString errorStr; QString errorStr;
@@ -51,11 +54,27 @@ private:
*/ */
class MatrixImageProvider : public QQuickAsyncImageProvider class MatrixImageProvider : public QQuickAsyncImageProvider
{ {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(NeoChatConnection *connection MEMBER m_connection)
public: public:
static MatrixImageProvider *create(QQmlEngine *engine, QJSEngine *)
{
static MatrixImageProvider instance;
engine->setObjectOwnership(&instance, QQmlEngine::CppOwnership);
return &instance;
}
/** /**
* @brief Return a job to provide the image with the given ID. * @brief Return a job to provide the image with the given ID.
* *
* @sa QQuickAsyncImageProvider::requestImageResponse * @sa QQuickAsyncImageProvider::requestImageResponse
*/ */
QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override; QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
private:
NeoChatConnection *m_connection = nullptr;
MatrixImageProvider() = default;
}; };

View File

@@ -261,7 +261,7 @@ QList<ActionsModel::Action> actions{
return QString(); return QString();
} }
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Knocking room <roomname>.", "Knocking room %1.", text)); Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Knocking room <roomname>.", "Knocking room %1.", text));
auto connection = Controller::instance().activeConnection(); auto connection = room->connection();
const auto knownServer = roomName.mid(roomName.indexOf(":"_ls) + 1); const auto knownServer = roomName.mid(roomName.indexOf(":"_ls) + 1);
if (parts.length() >= 2) { if (parts.length() >= 2) {
RoomManager::instance().knockRoom(connection, roomName, parts[1], QStringList{knownServer}); RoomManager::instance().knockRoom(connection, roomName, parts[1], QStringList{knownServer});
@@ -285,7 +285,7 @@ QList<ActionsModel::Action> actions{
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text)); i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString(); return QString();
} }
if (Controller::instance().activeConnection()->room(text) || Controller::instance().activeConnection()->roomByAlias(text)) { if (room->connection()->room(text) || room->connection()->roomByAlias(text)) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("You are already in room <roomname>.", "You are already in room %1.", text)); Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("You are already in room <roomname>.", "You are already in room %1.", text));
return QString(); return QString();
} }

View File

@@ -15,13 +15,28 @@
using namespace Quotient; using namespace Quotient;
void CustomEmojiModel::setConnection(NeoChatConnection *connection)
{
if (connection == m_connection) {
return;
}
m_connection = connection;
Q_EMIT connectionChanged();
fetchEmojis();
}
NeoChatConnection *CustomEmojiModel::connection() const
{
return m_connection;
}
void CustomEmojiModel::fetchEmojis() void CustomEmojiModel::fetchEmojis()
{ {
if (!Controller::instance().activeConnection()) { if (!m_connection) {
return; return;
} }
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes"_ls); const auto &data = m_connection->accountData("im.ponies.user_emotes"_ls);
if (data == nullptr) { if (data == nullptr) {
return; return;
} }
@@ -53,10 +68,10 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
{ {
using namespace Quotient; using namespace Quotient;
auto job = Controller::instance().activeConnection()->uploadFile(location.toLocalFile()); auto job = m_connection->uploadFile(location.toLocalFile());
connect(job, &BaseJob::success, this, [name, location, job] { connect(job, &BaseJob::success, this, [name, location, job, this] {
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes"_ls); const auto &data = m_connection->accountData("im.ponies.user_emotes"_ls);
auto json = data != nullptr ? data->contentJson() : QJsonObject(); auto json = data != nullptr ? data->contentJson() : QJsonObject();
auto emojiData = json["images"_ls].toObject(); auto emojiData = json["images"_ls].toObject();
@@ -78,7 +93,7 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
}); });
json["images"_ls] = emojiData; json["images"_ls] = emojiData;
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes"_ls, json); m_connection->setAccountData("im.ponies.user_emotes"_ls, json);
}); });
} }
@@ -86,7 +101,7 @@ void CustomEmojiModel::removeEmoji(const QString &name)
{ {
using namespace Quotient; using namespace Quotient;
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes"_ls); const auto &data = m_connection->accountData("im.ponies.user_emotes"_ls);
Q_ASSERT(data); Q_ASSERT(data);
auto json = data->contentJson(); auto json = data->contentJson();
const QString _name = name.mid(1).chopped(1); const QString _name = name.mid(1).chopped(1);
@@ -109,18 +124,18 @@ void CustomEmojiModel::removeEmoji(const QString &name)
emojiData.remove(_name); emojiData.remove(_name);
json["emoticons"_ls] = emojiData; json["emoticons"_ls] = emojiData;
} }
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes"_ls, json); m_connection->setAccountData("im.ponies.user_emotes"_ls, json);
} }
CustomEmojiModel::CustomEmojiModel(QObject *parent) CustomEmojiModel::CustomEmojiModel(QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
{ {
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this]() { connect(this, &CustomEmojiModel::connectionChanged, this, [this]() {
if (!Controller::instance().activeConnection()) { if (!m_connection) {
return; return;
} }
CustomEmojiModel::fetchEmojis(); CustomEmojiModel::fetchEmojis();
connect(Controller::instance().activeConnection(), &Connection::accountDataChanged, this, [this](const QString &id) { connect(m_connection, &Connection::accountDataChanged, this, [this](const QString &id) {
if (id != QStringLiteral("im.ponies.user_emotes")) { if (id != QStringLiteral("im.ponies.user_emotes")) {
return; return;
} }

View File

@@ -8,6 +8,8 @@
#include <QRegularExpression> #include <QRegularExpression>
#include <memory> #include <memory>
class NeoChatConnection;
struct CustomEmoji { struct CustomEmoji {
QString name; // with :semicolons: QString name; // with :semicolons:
QString url; // mxc:// QString url; // mxc://
@@ -31,6 +33,8 @@ class CustomEmojiModel : public QAbstractListModel
QML_ELEMENT QML_ELEMENT
QML_SINGLETON QML_SINGLETON
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
public: public:
/** /**
* @brief Defines the model roles. * @brief Defines the model roles.
@@ -98,9 +102,16 @@ public:
*/ */
Q_INVOKABLE void removeEmoji(const QString &name); Q_INVOKABLE void removeEmoji(const QString &name);
void setConnection(NeoChatConnection *connection);
NeoChatConnection *connection() const;
Q_SIGNALS:
void connectionChanged();
private: private:
explicit CustomEmojiModel(QObject *parent = nullptr); explicit CustomEmojiModel(QObject *parent = nullptr);
QList<CustomEmoji> m_emojis; QList<CustomEmoji> m_emojis;
NeoChatConnection *m_connection = nullptr;
void fetchEmojis(); void fetchEmojis();
}; };

View File

@@ -24,8 +24,8 @@ DevicesModel::DevicesModel(QObject *parent)
void DevicesModel::fetchDevices() void DevicesModel::fetchDevices()
{ {
if (Controller::instance().activeConnection()) { if (m_connection) {
auto job = Controller::instance().activeConnection()->callApi<GetDevicesJob>(); auto job = m_connection->callApi<GetDevicesJob>();
connect(job, &BaseJob::success, this, [this, job]() { connect(job, &BaseJob::success, this, [this, job]() {
beginResetModel(); beginResetModel();
m_devices = job->devices(); m_devices = job->devices();
@@ -102,7 +102,7 @@ void DevicesModel::logout(const QString &deviceId, const QString &password)
for (index = 0; m_devices[index].deviceId != deviceId; index++) for (index = 0; m_devices[index].deviceId != deviceId; index++)
; ;
auto job = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId); auto job = m_connection->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId);
connect(job, &BaseJob::result, this, [this, job, password, index] { connect(job, &BaseJob::result, this, [this, job, password, index] {
auto onSuccess = [this, index]() { auto onSuccess = [this, index]() {
@@ -117,9 +117,9 @@ void DevicesModel::logout(const QString &deviceId, const QString &password)
authData["session"_ls] = replyData["session"_ls]; authData["session"_ls] = replyData["session"_ls];
authData["password"_ls] = password; authData["password"_ls] = password;
authData["type"_ls] = "m.login.password"_ls; authData["type"_ls] = "m.login.password"_ls;
QJsonObject identifier = {{"type"_ls, "m.id.user"_ls}, {"user"_ls, Controller::instance().activeConnection()->user()->id()}}; QJsonObject identifier = {{"type"_ls, "m.id.user"_ls}, {"user"_ls, m_connection->user()->id()}};
authData["identifier"_ls] = identifier; authData["identifier"_ls] = identifier;
auto *innerJob = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId, authData); auto *innerJob = m_connection->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId, authData);
connect(innerJob, &BaseJob::success, this, onSuccess); connect(innerJob, &BaseJob::success, this, onSuccess);
} else { } else {
onSuccess(); onSuccess();
@@ -132,7 +132,7 @@ void DevicesModel::setName(const QString &deviceId, const QString &name)
int index; int index;
for (index = 0; m_devices[index].deviceId != deviceId; index++); for (index = 0; m_devices[index].deviceId != deviceId; index++);
auto job = Controller::instance().activeConnection()->callApi<UpdateDeviceJob>(m_devices[index].deviceId, name); auto job = m_connection->callApi<UpdateDeviceJob>(m_devices[index].deviceId, name);
QString oldName = m_devices[index].displayName; QString oldName = m_devices[index].displayName;
beginResetModel(); beginResetModel();
m_devices[index].displayName = name; m_devices[index].displayName = name;
@@ -160,7 +160,7 @@ void DevicesModel::setConnection(Connection *connection)
connect(m_connection, &Connection::sessionVerified, this, [this](const QString &userId, const QString &deviceId) { connect(m_connection, &Connection::sessionVerified, this, [this](const QString &userId, const QString &deviceId) {
Q_UNUSED(deviceId); Q_UNUSED(deviceId);
if (userId == Controller::instance().activeConnection()->userId()) { if (userId == m_connection->userId()) {
fetchDevices(); fetchDevices();
} }
}); });

View File

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

View File

@@ -1,66 +0,0 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <Quotient/csapi/definitions/push_rule.h>
#include <QAbstractListModel>
/**
* @class KeywordNotificationRuleModel
*
* This class defines the model for managing notification push rule keywords.
*/
class KeywordNotificationRuleModel : public QAbstractListModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum EventRoles {
NameRole = Qt::DisplayRole, /**< The push rule keyword. */
};
KeywordNotificationRuleModel(QObject *parent = nullptr);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Add a new keyword to the model.
*/
Q_INVOKABLE void addKeyword(const QString &keyword);
/**
* @brief Remove a keyword from the model.
*/
Q_INVOKABLE void removeKeywordAtIndex(int index);
private Q_SLOTS:
void controllerConnectionChanged();
void updateNotificationRules(const QString &type);
private:
QList<QString> m_notificationRules;
};

View File

@@ -66,19 +66,6 @@ PushRuleModel::PushRuleModel(QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
{ {
m_defaultKeywordAction = static_cast<PushNotificationAction::Action>(NeoChatConfig::self()->keywordPushRuleDefault()); m_defaultKeywordAction = static_cast<PushNotificationAction::Action>(NeoChatConfig::self()->keywordPushRuleDefault());
if (Controller::instance().activeConnection()) {
controllerConnectionChanged();
}
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, &PushRuleModel::controllerConnectionChanged);
}
void PushRuleModel::controllerConnectionChanged()
{
if (Controller::instance().activeConnection()) {
connect(Controller::instance().activeConnection(), &Quotient::Connection::accountDataChanged, this, &PushRuleModel::updateNotificationRules);
updateNotificationRules(QStringLiteral("m.push_rules"));
}
} }
void PushRuleModel::updateNotificationRules(const QString &type) void PushRuleModel::updateNotificationRules(const QString &type)
@@ -87,7 +74,7 @@ void PushRuleModel::updateNotificationRules(const QString &type)
return; return;
} }
const QJsonObject ruleDataJson = Controller::instance().activeConnection()->accountDataJson(QStringLiteral("m.push_rules")); const QJsonObject ruleDataJson = m_connection->accountDataJson(QStringLiteral("m.push_rules"));
const Quotient::PushRuleset ruleData = Quotient::fromJson<Quotient::PushRuleset>(ruleDataJson[QStringLiteral("global")].toObject()); const Quotient::PushRuleset ruleData = Quotient::fromJson<Quotient::PushRuleset>(ruleDataJson[QStringLiteral("global")].toObject());
beginResetModel(); beginResetModel();
@@ -156,8 +143,7 @@ PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule ru
* *
* Rooms that the user hasn't joined shouldn't have a rule. * Rooms that the user hasn't joined shouldn't have a rule.
*/ */
auto connection = Controller::instance().activeConnection(); if (m_connection->room(ruleId) != nullptr) {
if (connection->room(ruleId) != nullptr) {
return PushNotificationSection::Undefined; return PushNotificationSection::Undefined;
} }
/** /**
@@ -171,7 +157,7 @@ PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule ru
if (!testUserId.startsWith(u'@')) { if (!testUserId.startsWith(u'@')) {
testUserId.prepend(u'@'); testUserId.prepend(u'@');
} }
if (testUserId.startsWith(u'@') && !Quotient::serverPart(testUserId).isEmpty() && connection->user(testUserId) != nullptr) { if (testUserId.startsWith(u'@') && !Quotient::serverPart(testUserId).isEmpty() && m_connection->user(testUserId) != nullptr) {
return PushNotificationSection::Undefined; return PushNotificationSection::Undefined;
} }
// If the rule has push conditions and one is a room ID it is a room only keyword. // If the rule has push conditions and one is a room ID it is a room only keyword.
@@ -325,14 +311,14 @@ void PushRuleModel::addKeyword(const QString &keyword, const QString &roomId)
pushConditions.append(keywordCondition); pushConditions.append(keywordCondition);
} }
auto job = Controller::instance().activeConnection()->callApi<Quotient::SetPushRuleJob>(QLatin1String("global"), auto job = m_connection->callApi<Quotient::SetPushRuleJob>(QLatin1String("global"),
PushNotificationKind::kindString(kind), PushNotificationKind::kindString(kind),
keyword, keyword,
actions, actions,
QString(), QString(),
QString(), QString(),
pushConditions, pushConditions,
roomId.isEmpty() ? keyword : QString()); roomId.isEmpty() ? keyword : QString());
connect(job, &Quotient::BaseJob::failure, this, [job, keyword]() { connect(job, &Quotient::BaseJob::failure, this, [job, keyword]() {
qWarning() << QLatin1String("Unable to set push rule for keyword %1: ").arg(keyword) << job->errorString(); qWarning() << QLatin1String("Unable to set push rule for keyword %1: ").arg(keyword) << job->errorString();
}); });
@@ -351,7 +337,7 @@ void PushRuleModel::removeKeyword(const QString &keyword)
} }
auto kind = PushNotificationKind::kindString(m_rules[index].kind); auto kind = PushNotificationKind::kindString(m_rules[index].kind);
auto job = Controller::instance().activeConnection()->callApi<Quotient::DeletePushRuleJob>(QStringLiteral("global"), kind, m_rules[index].id); auto job = m_connection->callApi<Quotient::DeletePushRuleJob>(QStringLiteral("global"), kind, m_rules[index].id);
connect(job, &Quotient::BaseJob::failure, this, [this, job, index]() { connect(job, &Quotient::BaseJob::failure, this, [this, job, index]() {
qWarning() << QLatin1String("Unable to remove push rule for keyword %1: ").arg(m_rules[index].id) << job->errorString(); qWarning() << QLatin1String("Unable to remove push rule for keyword %1: ").arg(m_rules[index].id) << job->errorString();
}); });
@@ -359,10 +345,10 @@ void PushRuleModel::removeKeyword(const QString &keyword)
void PushRuleModel::setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled) void PushRuleModel::setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled)
{ {
auto job = Controller::instance().activeConnection()->callApi<Quotient::IsPushRuleEnabledJob>(QStringLiteral("global"), kind, ruleId); auto job = m_connection->callApi<Quotient::IsPushRuleEnabledJob>(QStringLiteral("global"), kind, ruleId);
connect(job, &Quotient::BaseJob::success, this, [job, kind, ruleId, enabled]() { connect(job, &Quotient::BaseJob::success, this, [job, kind, ruleId, enabled, this]() {
if (job->enabled() != enabled) { if (job->enabled() != enabled) {
Controller::instance().activeConnection()->callApi<Quotient::SetPushRuleEnabledJob>(QStringLiteral("global"), kind, ruleId, enabled); m_connection->callApi<Quotient::SetPushRuleEnabledJob>(QStringLiteral("global"), kind, ruleId, enabled);
} }
}); });
} }
@@ -376,7 +362,7 @@ void PushRuleModel::setNotificationRuleActions(const QString &kind, const QStrin
actions = actionToVariant(action); actions = actionToVariant(action);
} }
Controller::instance().activeConnection()->callApi<Quotient::SetPushRuleActionsJob>(QStringLiteral("global"), kind, ruleId, actions); m_connection->callApi<Quotient::SetPushRuleActionsJob>(QStringLiteral("global"), kind, ruleId, actions);
} }
PushNotificationAction::Action PushRuleModel::variantToAction(const QList<QVariant> &actions, bool enabled) PushNotificationAction::Action PushRuleModel::variantToAction(const QList<QVariant> &actions, bool enabled)
@@ -453,4 +439,23 @@ QList<QVariant> PushRuleModel::actionToVariant(PushNotificationAction::Action ac
return actions; return actions;
} }
NeoChatConnection *PushRuleModel::connection() const
{
return m_connection;
}
void PushRuleModel::setConnection(NeoChatConnection *connection)
{
if (connection == m_connection) {
return;
}
m_connection = connection;
Q_EMIT connectionChanged();
if (m_connection) {
connect(m_connection, &Quotient::Connection::accountDataChanged, this, &PushRuleModel::updateNotificationRules);
updateNotificationRules(QStringLiteral("m.push_rules"));
}
}
#include "moc_pushrulemodel.cpp" #include "moc_pushrulemodel.cpp"

View File

@@ -157,6 +157,8 @@ class PushRuleModel : public QAbstractListModel
*/ */
Q_PROPERTY(bool globalNotificationsSet READ globalNotificationsSet NOTIFY globalNotificationsSetChanged) Q_PROPERTY(bool globalNotificationsSet READ globalNotificationsSet NOTIFY globalNotificationsSetChanged)
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
public: public:
struct Rule { struct Rule {
QString id; QString id;
@@ -225,18 +227,22 @@ public:
*/ */
Q_INVOKABLE void removeKeyword(const QString &keyword); Q_INVOKABLE void removeKeyword(const QString &keyword);
void setConnection(NeoChatConnection *connection);
NeoChatConnection *connection() const;
Q_SIGNALS: Q_SIGNALS:
void defaultStateChanged(); void defaultStateChanged();
void globalNotificationsEnabledChanged(); void globalNotificationsEnabledChanged();
void globalNotificationsSetChanged(); void globalNotificationsSetChanged();
void connectionChanged();
private Q_SLOTS: private Q_SLOTS:
void controllerConnectionChanged();
void updateNotificationRules(const QString &type); void updateNotificationRules(const QString &type);
private: private:
PushNotificationAction::Action m_defaultKeywordAction; PushNotificationAction::Action m_defaultKeywordAction;
QList<Rule> m_rules; QList<Rule> m_rules;
NeoChatConnection *m_connection;
void setRules(QList<Quotient::PushRule> rules, PushNotificationKind::Kind kind); void setRules(QList<Quotient::PushRule> rules, PushNotificationKind::Kind kind);

View File

@@ -19,7 +19,7 @@ ServerListModel::ServerListModel(QObject *parent)
const auto stateConfig = KSharedConfig::openStateConfig(); const auto stateConfig = KSharedConfig::openStateConfig();
const KConfigGroup serverGroup = stateConfig->group(QStringLiteral("Servers")); const KConfigGroup serverGroup = stateConfig->group(QStringLiteral("Servers"));
QString domain = Controller::instance().activeConnection()->domain(); QString domain = m_connection->domain();
// Add the user's homeserver // Add the user's homeserver
m_servers.append(Server{ m_servers.append(Server{
@@ -100,7 +100,7 @@ void ServerListModel::checkServer(const QString &url)
m_checkServerJob->abandon(); m_checkServerJob->abandon();
} }
m_checkServerJob = Controller::instance().activeConnection()->callApi<Quotient::QueryPublicRoomsJob>(url, 1); m_checkServerJob = m_connection->callApi<Quotient::QueryPublicRoomsJob>(url, 1);
connect(m_checkServerJob, &Quotient::BaseJob::success, this, [this, url] { connect(m_checkServerJob, &Quotient::BaseJob::success, this, [this, url] {
Q_EMIT serverCheckComplete(url, true); Q_EMIT serverCheckComplete(url, true);
}); });
@@ -153,4 +153,18 @@ QHash<int, QByteArray> ServerListModel::roleNames() const
}; };
} }
NeoChatConnection *ServerListModel::connection() const
{
return m_connection;
}
void ServerListModel::setConnection(NeoChatConnection *connection)
{
if (m_connection == connection) {
return;
}
m_connection = connection;
Q_EMIT connectionChanged();
}
#include "moc_serverlistmodel.cpp" #include "moc_serverlistmodel.cpp"

View File

@@ -10,6 +10,8 @@
#include <QQmlEngine> #include <QQmlEngine>
#include <QUrl> #include <QUrl>
class NeoChatConnection;
/** /**
* @class ServerListModel * @class ServerListModel
* *
@@ -27,6 +29,8 @@ class ServerListModel : public QAbstractListModel
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
public: public:
/** /**
* @brief Define the data required to represent a server. * @brief Define the data required to represent a server.
@@ -96,10 +100,15 @@ public:
*/ */
Q_INVOKABLE void removeServerAtIndex(int index); Q_INVOKABLE void removeServerAtIndex(int index);
NeoChatConnection *connection() const;
void setConnection(NeoChatConnection *connection);
Q_SIGNALS: Q_SIGNALS:
void serverCheckComplete(QString url, bool valid); void serverCheckComplete(QString url, bool valid);
void connectionChanged();
private: private:
QList<Server> m_servers; QList<Server> m_servers;
QPointer<Quotient::QueryPublicRoomsJob> m_checkServerJob = nullptr; QPointer<Quotient::QueryPublicRoomsJob> m_checkServerJob = nullptr;
NeoChatConnection *m_connection = nullptr;
}; };

View File

@@ -12,7 +12,7 @@
SpaceChildrenModel::SpaceChildrenModel(QObject *parent) SpaceChildrenModel::SpaceChildrenModel(QObject *parent)
: QAbstractItemModel(parent) : QAbstractItemModel(parent)
{ {
m_rootItem = new SpaceTreeItem(); m_rootItem = new SpaceTreeItem(nullptr);
} }
SpaceChildrenModel::~SpaceChildrenModel() SpaceChildrenModel::~SpaceChildrenModel()
@@ -71,7 +71,8 @@ void SpaceChildrenModel::refreshModel()
delete m_rootItem; delete m_rootItem;
m_loading = true; m_loading = true;
Q_EMIT loadingChanged(); Q_EMIT loadingChanged();
m_rootItem = new SpaceTreeItem(nullptr, m_space->id(), m_space->displayName(), m_space->canonicalAlias()); m_rootItem =
new SpaceTreeItem(dynamic_cast<NeoChatConnection *>(m_space->connection()), nullptr, m_space->id(), m_space->displayName(), m_space->canonicalAlias());
endResetModel(); endResetModel();
auto job = m_space->connection()->callApi<Quotient::GetSpaceHierarchyJob>(m_space->id(), Quotient::none, Quotient::none, 1); auto job = m_space->connection()->callApi<Quotient::GetSpaceHierarchyJob>(m_space->id(), Quotient::none, Quotient::none, 1);
m_currentJobs.append(job); m_currentJobs.append(job);
@@ -112,7 +113,8 @@ void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJ
} }
} }
parentItem->insertChild(insertRow, parentItem->insertChild(insertRow,
new SpaceTreeItem(parentItem, new SpaceTreeItem(dynamic_cast<NeoChatConnection *>(m_space->connection()),
parentItem,
children[i].roomId, children[i].roomId,
children[i].name, children[i].name,
children[i].canonicalAlias, children[i].canonicalAlias,

View File

@@ -5,7 +5,8 @@
#include "controller.h" #include "controller.h"
SpaceTreeItem::SpaceTreeItem(SpaceTreeItem *parent, SpaceTreeItem::SpaceTreeItem(NeoChatConnection *connection,
SpaceTreeItem *parent,
const QString &id, const QString &id,
const QString &name, const QString &name,
const QString &canonicalAlias, const QString &canonicalAlias,
@@ -15,7 +16,8 @@ SpaceTreeItem::SpaceTreeItem(SpaceTreeItem *parent,
bool allowGuests, bool allowGuests,
bool worldReadable, bool worldReadable,
bool isSpace) bool isSpace)
: m_parentItem(parent) : m_connection(connection)
, m_parentItem(parent)
, m_id(id) , m_id(id)
, m_name(name) , m_name(name)
, m_canonicalAlias(canonicalAlias) , m_canonicalAlias(canonicalAlias)
@@ -107,8 +109,7 @@ QUrl SpaceTreeItem::avatarUrl() const
if (m_avatarUrl.isEmpty() || m_avatarUrl.scheme() != QLatin1String("mxc")) { if (m_avatarUrl.isEmpty() || m_avatarUrl.scheme() != QLatin1String("mxc")) {
return {}; return {};
} }
auto connection = Controller::instance().activeConnection(); auto url = m_connection->makeMediaUrl(m_avatarUrl);
auto url = connection->makeMediaUrl(m_avatarUrl);
if (url.scheme() == QLatin1String("mxc")) { if (url.scheme() == QLatin1String("mxc")) {
return url; return url;
} }
@@ -127,11 +128,10 @@ bool SpaceTreeItem::worldReadable() const
bool SpaceTreeItem::isJoined() const bool SpaceTreeItem::isJoined() const
{ {
auto connection = Controller::instance().activeConnection(); if (!m_connection) {
if (!connection) {
return false; return false;
} }
return connection->room(id(), Quotient::JoinState::Join) != nullptr; return m_connection->room(id(), Quotient::JoinState::Join) != nullptr;
} }
bool SpaceTreeItem::isSpace() const bool SpaceTreeItem::isSpace() const

View File

@@ -3,6 +3,8 @@
#include <Quotient/csapi/space_hierarchy.h> #include <Quotient/csapi/space_hierarchy.h>
class NeoChatConnection;
/** /**
* @class SpaceTreeItem * @class SpaceTreeItem
* *
@@ -18,7 +20,8 @@
class SpaceTreeItem class SpaceTreeItem
{ {
public: public:
explicit SpaceTreeItem(SpaceTreeItem *parent = nullptr, explicit SpaceTreeItem(NeoChatConnection *connection,
SpaceTreeItem *parent = nullptr,
const QString &id = {}, const QString &id = {},
const QString &name = {}, const QString &name = {},
const QString &canonicalAlias = {}, const QString &canonicalAlias = {},
@@ -121,6 +124,7 @@ public:
bool isSpace() const; bool isSpace() const;
private: private:
NeoChatConnection *m_connection;
QList<SpaceTreeItem *> m_children; QList<SpaceTreeItem *> m_children;
SpaceTreeItem *m_parentItem; SpaceTreeItem *m_parentItem;

View File

@@ -16,6 +16,7 @@
#include <Quotient/csapi/content-repo.h> #include <Quotient/csapi/content-repo.h>
#include <Quotient/csapi/profile.h> #include <Quotient/csapi/profile.h>
#include <Quotient/database.h>
#include <Quotient/qt_connection_util.h> #include <Quotient/qt_connection_util.h>
#include <Quotient/settings.h> #include <Quotient/settings.h>
#include <Quotient/user.h> #include <Quotient/user.h>
@@ -220,4 +221,35 @@ void NeoChatConnection::createSpace(const QString &name, const QString &topic, c
}); });
} }
void NeoChatConnection::openOrCreateDirectChat(User *user)
{
const auto existing = directChats();
if (existing.contains(user)) {
const auto room = static_cast<NeoChatRoom *>(this->room(existing.value(user)));
if (room) {
RoomManager::instance().enterRoom(room);
return;
}
}
requestDirectChat(user);
}
QString NeoChatConnection::deviceKey() const
{
return edKeyForUserDevice(userId(), deviceId());
}
QString NeoChatConnection::encryptionKey() const
{
auto query = database()->prepareQuery(QStringLiteral("SELECT curveKey FROM tracked_devices WHERE matrixId=:matrixId AND deviceid=:deviceId LIMIT 1;"));
query.bindValue(QStringLiteral(":matrixId"), userId());
query.bindValue(QStringLiteral(":deviceId"), deviceId());
database()->execute(query);
if (!query.next()) {
return {};
}
return query.value(0).toString();
}
#include "moc_neochatconnection.cpp" #include "moc_neochatconnection.cpp"

View File

@@ -23,6 +23,8 @@ class NeoChatConnection : public Quotient::Connection
* Set to an empty string to remove the label. * Set to an empty string to remove the label.
*/ */
Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged) Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged)
Q_PROPERTY(QString deviceKey READ deviceKey CONSTANT)
Q_PROPERTY(QString encryptionKey READ encryptionKey CONSTANT)
public: public:
NeoChatConnection(QObject *parent = nullptr); NeoChatConnection(QObject *parent = nullptr);
@@ -61,6 +63,16 @@ public:
*/ */
Q_INVOKABLE void createSpace(const QString &name, const QString &topic, const QString &parent = {}, bool setChildParent = false); Q_INVOKABLE void createSpace(const QString &name, const QString &topic, const QString &parent = {}, bool setChildParent = false);
/**
* @brief Join a direct chat with the given user.
*
* If a direct chat with the user doesn't exist one is created and then joined.
*/
Q_INVOKABLE void openOrCreateDirectChat(Quotient::User *user);
QString deviceKey() const;
QString encryptionKey() const;
Q_SIGNALS: Q_SIGNALS:
void labelChanged(); void labelChanged();
}; };

View File

@@ -123,6 +123,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
Q_EMIT canEncryptRoomChanged(); Q_EMIT canEncryptRoomChanged();
Q_EMIT parentIdsChanged(); Q_EMIT parentIdsChanged();
Q_EMIT canonicalParentChanged(); Q_EMIT canonicalParentChanged();
Q_EMIT joinRuleChanged();
}); });
connect(connection, &Connection::capabilitiesLoaded, this, &NeoChatRoom::maxRoomVersionChanged); connect(connection, &Connection::capabilitiesLoaded, this, &NeoChatRoom::maxRoomVersionChanged);
connect(this, &Room::changed, this, [this]() { connect(this, &Room::changed, this, [this]() {
@@ -418,30 +419,6 @@ QDateTime NeoChatRoom::lastActiveTime()
return messageEvents().rbegin()->get()->originTimestamp(); return messageEvents().rbegin()->get()->originTimestamp();
} }
QVariantList NeoChatRoom::getUsers(const QString &keyword, int limit) const
{
const auto userList = users();
QVariantList matchedList;
int count = 0;
for (const auto u : userList) {
if (u->displayname(this).contains(keyword, Qt::CaseInsensitive)) {
Quotient::User user(u->id(), u->connection());
QVariantMap userVariant{{QStringLiteral("id"), user.id()},
{QStringLiteral("displayName"), user.displayname(this)},
{QStringLiteral("avatarMediaId"), user.avatarMediaId(this)},
{QStringLiteral("color"), Utils::getUserColor(user.hueF())}};
matchedList.append(QVariant::fromValue(userVariant));
count++;
if (count == limit) { // -1 is infinite
break;
}
}
}
return matchedList;
}
// An empty user is useful for returning as a model value to avoid properties being undefined. // An empty user is useful for returning as a model value to avoid properties being undefined.
static const QVariantMap emptyUser = { static const QVariantMap emptyUser = {
{"isLocalUser"_ls, false}, {"isLocalUser"_ls, false},
@@ -736,16 +713,51 @@ QString NeoChatRoom::joinRule() const
return joinRulesEvent->joinRule(); return joinRulesEvent->joinRule();
} }
void NeoChatRoom::setJoinRule(const QString &joinRule) void NeoChatRoom::setJoinRule(const QString &joinRule, const QList<QString> &allowedSpaces)
{ {
if (!canSendState("m.room.join_rules"_ls)) { if (!canSendState("m.room.join_rules"_ls)) {
qWarning() << "Power level too low to set join rules"; qWarning() << "Power level too low to set join rules";
return; return;
} }
setState("m.room.join_rules"_ls, {}, QJsonObject{{"join_rule"_ls, joinRule}}); auto actualRule = joinRule;
if (joinRule == "restricted"_ls && allowedSpaces.isEmpty()) {
actualRule = "private"_ls;
}
QJsonArray allowConditions;
if (actualRule == "restricted"_ls) {
for (auto allowedSpace : allowedSpaces) {
allowConditions += QJsonObject{{"type"_ls, "m.room_membership"_ls}, {"room_id"_ls, allowedSpace}};
}
}
QJsonObject content;
content.insert("join_rule"_ls, joinRule);
if (!allowConditions.isEmpty()) {
content.insert("allow"_ls, allowConditions);
}
qWarning() << content;
setState("m.room.join_rules"_ls, {}, content);
// Not emitting joinRuleChanged() here, since that would override the change in the UI with the *current* value, which is not the *new* value. // Not emitting joinRuleChanged() here, since that would override the change in the UI with the *current* value, which is not the *new* value.
} }
QList<QString> NeoChatRoom::restrictedIds() const
{
auto joinRulesEvent = currentState().get<JoinRulesEvent>();
if (!joinRulesEvent) {
return {};
}
if (joinRulesEvent->joinRule() != "restricted"_ls) {
return {};
}
QList<QString> roomIds;
for (auto allow : joinRulesEvent->allow()) {
roomIds += allow.toObject().value("room_id"_ls).toString();
}
return roomIds;
}
QString NeoChatRoom::historyVisibility() const QString NeoChatRoom::historyVisibility() const
{ {
return currentState().get("m.room.history_visibility"_ls)->contentJson()["history_visibility"_ls].toString(); return currentState().get("m.room.history_visibility"_ls)->contentJson()["history_visibility"_ls].toString();
@@ -1165,6 +1177,21 @@ QList<QString> NeoChatRoom::parentIds() const
return parentIds; return parentIds;
} }
QList<NeoChatRoom *> NeoChatRoom::parentObjects(bool multiLevel) const
{
QList<NeoChatRoom *> parentObjects;
QList<QString> parentIds = this->parentIds();
for (const auto &parentId : parentIds) {
if (auto parentObject = static_cast<NeoChatRoom *>(connection()->room(parentId))) {
parentObjects += parentObject;
if (multiLevel) {
parentObjects += parentObject->parentObjects(true);
}
}
}
return parentObjects;
}
QString NeoChatRoom::canonicalParent() const QString NeoChatRoom::canonicalParent() const
{ {
auto parentEvents = currentState().eventsOfType("m.space.parent"_ls); auto parentEvents = currentState().eventsOfType("m.space.parent"_ls);
@@ -1362,7 +1389,7 @@ void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
for (const auto &i : roomRuleArray) { for (const auto &i : roomRuleArray) {
QJsonObject roomRule = i.toObject(); QJsonObject roomRule = i.toObject();
if (roomRule["rule_id"_ls] == id()) { if (roomRule["rule_id"_ls] == id()) {
Controller::instance().activeConnection()->callApi<DeletePushRuleJob>("global"_ls, "room"_ls, id()); connection()->callApi<DeletePushRuleJob>("global"_ls, "room"_ls, id());
} }
} }
} }
@@ -1373,7 +1400,7 @@ void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
for (const auto &i : overrideRuleArray) { for (const auto &i : overrideRuleArray) {
QJsonObject overrideRule = i.toObject(); QJsonObject overrideRule = i.toObject();
if (overrideRule["rule_id"_ls] == id()) { if (overrideRule["rule_id"_ls] == id()) {
Controller::instance().activeConnection()->callApi<DeletePushRuleJob>("global"_ls, "override"_ls, id()); connection()->callApi<DeletePushRuleJob>("global"_ls, "override"_ls, id());
} }
} }
} }
@@ -1409,11 +1436,9 @@ void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
const QList<PushCondition> conditions = {pushCondition}; const QList<PushCondition> conditions = {pushCondition};
// Add new override rule and make sure it's enabled // Add new override rule and make sure it's enabled
auto job = Controller::instance() auto job = connection()->callApi<SetPushRuleJob>("global"_ls, "override"_ls, id(), actions, QString(), QString(), conditions, QString());
.activeConnection()
->callApi<SetPushRuleJob>("global"_ls, "override"_ls, id(), actions, QString(), QString(), conditions, QString());
connect(job, &BaseJob::success, this, [this]() { connect(job, &BaseJob::success, this, [this]() {
auto enableJob = Controller::instance().activeConnection()->callApi<SetPushRuleEnabledJob>("global"_ls, "override"_ls, id(), true); auto enableJob = connection()->callApi<SetPushRuleEnabledJob>("global"_ls, "override"_ls, id(), true);
connect(enableJob, &BaseJob::success, this, [this]() { connect(enableJob, &BaseJob::success, this, [this]() {
m_pushNotificationStateUpdating = false; m_pushNotificationStateUpdating = false;
}); });
@@ -1437,11 +1462,9 @@ void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
// No conditions for a room rule // No conditions for a room rule
const QList<PushCondition> conditions; const QList<PushCondition> conditions;
auto setJob = Controller::instance() auto setJob = connection()->callApi<SetPushRuleJob>("global"_ls, "room"_ls, id(), actions, QString(), QString(), conditions, QString());
.activeConnection()
->callApi<SetPushRuleJob>("global"_ls, "room"_ls, id(), actions, QString(), QString(), conditions, QString());
connect(setJob, &BaseJob::success, this, [this]() { connect(setJob, &BaseJob::success, this, [this]() {
auto enableJob = Controller::instance().activeConnection()->callApi<SetPushRuleEnabledJob>("global"_ls, "room"_ls, id(), true); auto enableJob = connection()->callApi<SetPushRuleEnabledJob>("global"_ls, "room"_ls, id(), true);
connect(enableJob, &BaseJob::success, this, [this]() { connect(enableJob, &BaseJob::success, this, [this]() {
m_pushNotificationStateUpdating = false; m_pushNotificationStateUpdating = false;
}); });
@@ -1470,11 +1493,9 @@ void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
const QList<PushCondition> conditions; const QList<PushCondition> conditions;
// Add new room rule and make sure enabled // Add new room rule and make sure enabled
auto setJob = Controller::instance() auto setJob = connection()->callApi<SetPushRuleJob>("global"_ls, "room"_ls, id(), actions, QString(), QString(), conditions, QString());
.activeConnection()
->callApi<SetPushRuleJob>("global"_ls, "room"_ls, id(), actions, QString(), QString(), conditions, QString());
connect(setJob, &BaseJob::success, this, [this]() { connect(setJob, &BaseJob::success, this, [this]() {
auto enableJob = Controller::instance().activeConnection()->callApi<SetPushRuleEnabledJob>("global"_ls, "room"_ls, id(), true); auto enableJob = connection()->callApi<SetPushRuleEnabledJob>("global"_ls, "room"_ls, id(), true);
connect(enableJob, &BaseJob::success, this, [this]() { connect(enableJob, &BaseJob::success, this, [this]() {
m_pushNotificationStateUpdating = false; m_pushNotificationStateUpdating = false;
}); });

View File

@@ -160,6 +160,13 @@ class NeoChatRoom : public Quotient::Room
*/ */
Q_PROPERTY(QString joinRule READ joinRule WRITE setJoinRule NOTIFY joinRuleChanged) Q_PROPERTY(QString joinRule READ joinRule WRITE setJoinRule NOTIFY joinRuleChanged)
/**
* @brief The space IDs that members of can join this room.
*
* Empty if the join rule is not restricted.
*/
Q_PROPERTY(QList<QString> restrictedIds READ restrictedIds NOTIFY joinRuleChanged)
/** /**
* @brief Get the maximum room version that the server supports. * @brief Get the maximum room version that the server supports.
* *
@@ -324,28 +331,6 @@ public:
explicit NeoChatRoom(Quotient::Connection *connection, QString roomId, Quotient::JoinState joinState = {}); explicit NeoChatRoom(Quotient::Connection *connection, QString roomId, Quotient::JoinState joinState = {});
/**
* @brief Get a list of users in the context of this room.
*
* This is different to getting a list of Quotient::User objects
* 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
* provides the room context and returns the result as a list of QVariantMap objects.
*
* @param keyword filters the users based on the displayname containing keyword.
* @param limit max number of user returned, -1 is infinite.
*
* @return a QVariantList containing a QVariantMap for each user with the following
* properties:
* - id - User ID.
* - displayName - Display name in the context of this room.
* - avatarMediaId - Avatar id in the context of this room.
* - color - Color for the user.
*
* @sa Quotient::User
*/
Q_INVOKABLE [[nodiscard]] QVariantList getUsers(const QString &keyword, int limit = -1) const;
/** /**
* @brief Get a user in the context of this room. * @brief Get a user in the context of this room.
* *
@@ -527,6 +512,17 @@ public:
QList<QString> parentIds() const; QList<QString> parentIds() const;
/**
* @brief Get a list of parent space objects for this room.
*
* Will only return retrun spaces that are know, i.e. the user has joined and
* a valid NeoChatRoom is available.
*
* @param multiLevel whether the function should recursively gather all levels
* of parents
*/
Q_INVOKABLE QList<NeoChatRoom *> parentObjects(bool multiLevel = false) const;
QString canonicalParent() const; QString canonicalParent() const;
void setCanonicalParent(const QString &parentId); void setCanonicalParent(const QString &parentId);
@@ -581,7 +577,23 @@ public:
Q_INVOKABLE void clearInvitationNotification(); Q_INVOKABLE void clearInvitationNotification();
[[nodiscard]] QString joinRule() const; [[nodiscard]] QString joinRule() const;
void setJoinRule(const QString &joinRule);
/**
* @brief Set the join rule for the room.
*
* Will fail if the user doesn't have the required privileges.
*
* @param joinRule the join rule [public, knock, invite, private, restricted].
* @param allowedSpaces only used when the join rule is restricted. This is a
* list of space Matrix IDs that members of can join without an invite.
* If the rule is restricted and this list is empty it is treated as a join
* rule of private instead.
*
* @sa https://spec.matrix.org/latest/client-server-api/#mroomjoin_rules
*/
Q_INVOKABLE void setJoinRule(const QString &joinRule, const QList<QString> &allowedSpaces = {});
QList<QString> restrictedIds() const;
int maxRoomVersion() const; int maxRoomVersion() const;

View File

@@ -204,15 +204,15 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
notification->setText(notification->text() + QLatin1Char('\n') + entry); notification->setText(notification->text() + QLatin1Char('\n') + entry);
notification->setPixmap(createNotificationImage(icon, room)); notification->setPixmap(createNotificationImage(icon, room));
notification->setDefaultAction(i18n("Open NeoChat in this room")); auto defaultAction = notification->addDefaultAction(i18n("Open NeoChat in this room"));
connect(notification, &KNotification::defaultActivated, this, [notification, room]() { connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken()); WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
if (!room) { if (!room) {
return; return;
} }
if (room->localUser()->id() != Controller::instance().activeConnection()->userId()) { auto connection = dynamic_cast<NeoChatConnection *>(Controller::instance().accounts().get(room->localUser()->id()));
Controller::instance().setActiveConnection(dynamic_cast<NeoChatConnection *>(Controller::instance().accounts().get(room->localUser()->id()))); Controller::instance().setActiveConnection(connection);
} RoomManager::instance().setConnection(connection);
RoomManager::instance().enterRoom(room); RoomManager::instance().enterRoom(room);
}); });
@@ -240,28 +240,31 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *room, const QStri
notification->setTitle(title); notification->setTitle(title);
notification->setPixmap(createNotificationImage(icon, nullptr)); notification->setPixmap(createNotificationImage(icon, nullptr));
notification->setFlags(KNotification::Persistent); notification->setFlags(KNotification::Persistent);
notification->setDefaultAction(i18n("Open this invitation in NeoChat")); auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat"));
connect(notification, &KNotification::defaultActivated, this, [notification, room]() { connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken()); WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
notification->close(); notification->close();
RoomManager::instance().enterRoom(room); RoomManager::instance().enterRoom(room);
}); });
notification->setActions({i18nc("@action:button The thing being accepted is an invitation to chat", "Accept"), i18nc("@action:button The thing being rejected is an invitation to chat", "Reject"), i18nc("@action:button The thing being rejected is an invitation to chat", "Reject and Ignore User")});
connect(notification, &KNotification::action1Activated, this, [room, notification]() { const auto acceptAction = notification->addAction(i18nc("@action:button The thing being accepted is an invitation to chat", "Accept"));
const auto rejectAction = notification->addAction(i18nc("@action:button The thing being rejected is an invitation to chat", "Reject"));
const auto rejectAndIgnoreAction = notification->addAction(i18nc("@action:button The thing being rejected is an invitation to chat", "Reject and Ignore User"));
connect(acceptAction, &KNotificationAction::activated, this, [room, notification]() {
if (!room) { if (!room) {
return; return;
} }
room->acceptInvitation(); room->acceptInvitation();
notification->close(); notification->close();
}); });
connect(notification, &KNotification::action2Activated, this, [room, notification]() { connect(rejectAction, &KNotificationAction::activated, this, [room, notification]() {
if (!room) { if (!room) {
return; return;
} }
RoomManager::instance().leaveRoom(room); RoomManager::instance().leaveRoom(room);
notification->close(); notification->close();
}); });
connect(notification, &KNotification::action3Activated, this, [room, notification]() { connect(rejectAndIgnoreAction, &KNotificationAction::activated, this, [room, notification]() {
if (!room) { if (!room) {
return; return;
} }

View File

@@ -212,7 +212,7 @@ FormCard.FormCardPage {
Loader { Loader {
id: colorSchemeDelegate id: colorSchemeDelegate
visible: item !== null && Qt.platform.os !== "android" visible: item !== null
source: "qrc:/org/kde/neochat/qml/ColorScheme.qml" source: "qrc:/org/kde/neochat/qml/ColorScheme.qml"
Layout.fillWidth: true Layout.fillWidth: true
} }
@@ -235,7 +235,7 @@ FormCard.FormCardPage {
FormCard.FormCheckDelegate { FormCard.FormCheckDelegate {
id: hasWindowSystemDelegate id: hasWindowSystemDelegate
visible: Controller.hasWindowSystem visible: WindowController.hasWindowSystem
text: i18n("Use transparent chat page") text: i18n("Use transparent chat page")
enabled: !Config.compactLayout && !Config.isBlurImmutable enabled: !Config.compactLayout && !Config.isBlurImmutable
checked: Config.blur checked: Config.blur
@@ -249,7 +249,7 @@ FormCard.FormCardPage {
FormCard.AbstractFormDelegate { FormCard.AbstractFormDelegate {
id: transparencyDelegate id: transparencyDelegate
visible: Controller.hasWindowSystem && Config.blur visible: WindowController.hasWindowSystem && Config.blur
enabled: !Config.isTransparancyImmutable enabled: !Config.isTransparancyImmutable
background: Item {} background: Item {}
contentItem: ColumnLayout { contentItem: ColumnLayout {

View File

@@ -6,6 +6,7 @@ import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
import QtMultimedia import QtMultimedia
import org.kde.coreaddons
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.neochat import org.kde.neochat
@@ -127,7 +128,7 @@ MessageDelegate {
QQC2.Label { QQC2.Label {
visible: root.contentMaxWidth > Kirigami.Units.gridUnit * 12 visible: root.contentMaxWidth > Kirigami.Units.gridUnit * 12
text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration) text: Format.formatDuration(audio.position) + "/" + Format.formatDuration(audio.duration)
} }
} }
QQC2.Label { QQC2.Label {
@@ -135,7 +136,7 @@ MessageDelegate {
Layout.rightMargin: Kirigami.Units.smallSpacing Layout.rightMargin: Kirigami.Units.smallSpacing
visible: audio.hasAudio && root.contentMaxWidth < Kirigami.Units.gridUnit * 12 visible: audio.hasAudio && root.contentMaxWidth < Kirigami.Units.gridUnit * 12
text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration) text: Format.formatDuration(audio.position) + "/" + Format.formatDuration(audio.duration)
} }
} }
} }

View File

@@ -32,7 +32,7 @@ KirigamiSettings.CategorizedSettings {
actionName: "security" actionName: "security"
text: i18n("Security") text: i18n("Security")
icon.name: "security-low" icon.name: "security-low"
page: Qt.resolvedUrl("Security.qml") page: Qt.resolvedUrl("RoomSecurity.qml")
initialProperties: { initialProperties: {
return { return {
room: root.room room: root.room

View File

@@ -31,7 +31,7 @@ Kirigami.ScrollablePage {
delegate: RoomDelegate { delegate: RoomDelegate {
id: roomDelegate id: roomDelegate
filterText: "" filterText: ""
onClicked: { onSelected: {
root.chosen(roomDelegate.currentRoom.id) root.chosen(roomDelegate.currentRoom.id)
} }
connection: root.connection connection: root.connection

View File

@@ -16,6 +16,11 @@ FormCard.FormCardPage {
title: i18n("Devices") title: i18n("Devices")
background: Kirigami.PlaceholderMessage {
text: i18n("Loading…")
visible: !thisDeviceCard.visible
}
required property NeoChatConnection connection required property NeoChatConnection connection
property DevicesModel devicesModel: DevicesModel { property DevicesModel devicesModel: DevicesModel {
@@ -24,6 +29,7 @@ FormCard.FormCardPage {
} }
DevicesCard { DevicesCard {
id: thisDeviceCard
title: i18n("This Device") title: i18n("This Device")
type: DevicesModel.This type: DevicesModel.This
showVerifyButton: false showVerifyButton: false
@@ -59,26 +65,33 @@ FormCard.FormCardPage {
visible: !root.connection visible: !root.connection
} }
property Kirigami.OverlaySheet passwordSheet: Kirigami.OverlaySheet { property Kirigami.Dialog passwordSheet: Kirigami.Dialog {
id: passwordSheet id: passwordSheet
property string deviceId property string deviceId
preferredWidth: Kirigami.Units.gridUnit * 24
title: i18n("Remove device") title: i18n("Remove device")
Kirigami.FormLayout {
QQC2.TextField { standardButtons: QQC2.Dialog.Cancel
FormCard.FormCard {
FormCard.FormTextFieldDelegate {
id: passwordField id: passwordField
Kirigami.FormData.label: i18n("Password:") label: i18n("Password:")
echoMode: TextInput.Password echoMode: TextInput.Password
} }
QQC2.Button { }
text: i18n("Confirm") customFooterActions: [
onClicked: { Kirigami.Action {
text: i18nc("As in 'Remove this device'", "Remove")
icon.name: "delete"
onTriggered: {
devicesModel.logout(passwordSheet.deviceId, passwordField.text) devicesModel.logout(passwordSheet.deviceId, passwordField.text)
passwordField.text = "" passwordField.text = ""
passwordSheet.close() passwordSheet.close()
} }
} }
} ]
} }
} }

View File

@@ -11,6 +11,13 @@ import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat import org.kde.neochat
ColumnLayout { ColumnLayout {
id: root
/**
* @brief The current room that user is viewing.
*/
required property NeoChatRoom room
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
spacing: 0 spacing: 0
@@ -26,15 +33,15 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
onClicked: { onClicked: {
RoomManager.visitUser(room.getUser(room.directChatRemoteUser.id).object, "mention") RoomManager.visitUser(root.room.getUser(root.room.directChatRemoteUser.id).object, "mention")
} }
contentItem: KirigamiComponents.Avatar { contentItem: KirigamiComponents.Avatar {
name: room ? room.displayName : "" name: root.room ? root.room.displayName : ""
source: room ? ("image://mxc/" + room.avatarMediaId) : "" source: root.room ? ("image://mxc/" + root.room.avatarMediaId) : ""
Rectangle { Rectangle {
visible: room.usesEncryption visible: root.room.usesEncryption
color: Kirigami.Theme.backgroundColor color: Kirigami.Theme.backgroundColor
width: Kirigami.Units.gridUnit width: Kirigami.Units.gridUnit
@@ -56,7 +63,7 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
type: Kirigami.Heading.Type.Primary type: Kirigami.Heading.Type.Primary
wrapMode: QQC2.Label.Wrap wrapMode: QQC2.Label.Wrap
text: room.displayName text: root.room.displayName
textFormat: Text.PlainText textFormat: Text.PlainText
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }

View File

@@ -0,0 +1,159 @@
// 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
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat
ColumnLayout {
id: root
spacing: 0
/**
* @brief The connection for the current user.
*/
required property NeoChatConnection connection
/**
* @brief Emitted when the text is changed in the search field.
*/
signal textChanged(string newText)
Kirigami.Separator {
Layout.fillWidth: true
}
Kirigami.NavigationTabBar {
id: exploreTabBar
Layout.fillWidth: true
actions: [
Kirigami.Action {
id: infoAction
text: i18n("Search")
icon.name: "search"
onTriggered: {
if (explorePopup.visible && explorePopupLoader.sourceComponent == search) {
explorePopup.close();
exploreTabBar.currentIndex = -1;
} else if (explorePopup.visible && explorePopupLoader.sourceComponent != search) {
explorePopup.close();
explorePopup.open();
} else {
explorePopup.open();
}
explorePopupLoader.sourceComponent = search;
}
},
Kirigami.Action {
text: i18n("Explore rooms")
icon.name: "compass"
onTriggered: {
let dialog = pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/JoinRoomPage.qml", {connection: root.connection}, {title: i18nc("@title", "Explore Rooms")})
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
if (isJoined) {
RoomManager.enterRoom(root.connection.room(roomId));
} else {
Controller.joinRoom(roomId.length > 0 ? roomId : alias);
}
})
exploreTabBar.currentIndex = -1;
}
},
Kirigami.Action {
text: i18n("Start a Chat")
icon.name: "list-add-user"
onTriggered: {
pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/StartChatPage.qml", {connection: root.connection}, {title: i18nc("@title", "Start a Chat")})
exploreTabBar.currentIndex = -1;
}
},
Kirigami.Action {
text: i18n("Create New")
icon.name: "list-add"
onTriggered: {
if (explorePopup.visible && explorePopupLoader.sourceComponent == create) {
explorePopup.close();
exploreTabBar.currentIndex = -1;
} else if (explorePopup.visible && explorePopupLoader.sourceComponent != create) {
explorePopup.close();
explorePopup.open();
} else {
explorePopup.open();
}
explorePopupLoader.sourceComponent = create;
}
}
]
}
QQC2.Popup {
id: explorePopup
parent: root
y: -height + 1
width: root.width
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
topPadding: Kirigami.Units.largeSpacing
closePolicy: QQC2.Popup.CloseOnEscape
contentItem: Loader {
id: explorePopupLoader
sourceComponent: search
}
background: ColumnLayout {
spacing: 0
Kirigami.Separator {
Layout.fillWidth: true
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Kirigami.Theme.backgroundColor
}
}
Component {
id: search
Kirigami.SearchField {
onTextChanged: root.textChanged(text)
}
}
Component {
id: create
ColumnLayout {
spacing: 0
Delegates.RoundedItemDelegate {
Layout.fillWidth: true
action: Kirigami.Action {
text: i18n("Create a Room")
icon.name: "system-users-symbolic"
onTriggered: {
pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/CreateRoomDialog.qml", {connection: root.connection}, {title: i18nc("@title", "Create a Room")})
explorePopup.close()
}
shortcut: StandardKey.New
}
}
Delegates.RoundedItemDelegate {
Layout.fillWidth: true
action: Kirigami.Action {
text: i18n("Create a Space")
icon.name: "list-add"
onTriggered: {
pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/CreateRoomDialog.qml", {connection: root.connection, isSpace: true, title: i18nc("@title", "Create a Space")}, {title: i18nc("@title", "Create a Space")})
explorePopup.close()
}
}
}
}
}
}
}

View File

@@ -6,6 +6,7 @@ import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
import Qt.labs.platform import Qt.labs.platform
import org.kde.coreaddons
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.neochat import org.kde.neochat
@@ -104,7 +105,7 @@ MessageDelegate {
PropertyChanges { PropertyChanges {
target: sizeLabel target: sizeLabel
text: i18nc("file download progress", "%1 / %2", Controller.formatByteSize(root.progressInfo.progress), Controller.formatByteSize(root.progressInfo.total)) text: i18nc("file download progress", "%1 / %2", Format.formatByteSize(root.progressInfo.progress), Format.formatByteSize(root.progressInfo.total))
} }
PropertyChanges { PropertyChanges {
target: downloadButton target: downloadButton
@@ -140,7 +141,7 @@ MessageDelegate {
QQC2.Label { QQC2.Label {
id: sizeLabel id: sizeLabel
Layout.fillWidth: true Layout.fillWidth: true
text: Controller.formatByteSize(root.mediaInfo.size) text: Format.formatByteSize(root.mediaInfo.size)
opacity: 0.7 opacity: 0.7
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1 maximumLineCount: 1

View File

@@ -14,17 +14,23 @@ import org.kde.neochat
FormCard.FormCardPage { FormCard.FormCardPage {
id: root id: root
required property NeoChatConnection connection
title: i18nc("@title:window", "Notifications") title: i18nc("@title:window", "Notifications")
property PushRuleModel pushRuleModel: PushRuleModel {
connection: root.connection
}
FormCard.FormCard { FormCard.FormCard {
Layout.topMargin: Kirigami.Units.largeSpacing Layout.topMargin: Kirigami.Units.largeSpacing
FormCard.FormCheckDelegate { FormCard.FormCheckDelegate {
text: i18n("Enable notifications for this account") text: i18n("Enable notifications for this account")
description: i18n("Whether push notifications are generated by your Matrix server") description: i18n("Whether push notifications are generated by your Matrix server")
checked: Controller.pushRuleModel.globalNotificationsEnabled checked: root.pushRuleModel.globalNotificationsEnabled
enabled: Controller.pushRuleModel.globalNotificationsSet enabled: root.pushRuleModel.globalNotificationsSet
onToggled: { onToggled: {
Controller.pushRuleModel.globalNotificationsEnabled = checked root.pushRuleModel.globalNotificationsEnabled = checked
} }
} }
} }
@@ -35,7 +41,7 @@ FormCard.FormCardPage {
FormCard.FormCard { FormCard.FormCard {
Repeater { Repeater {
model: KSortFilterProxyModel { model: KSortFilterProxyModel {
sourceModel: Controller.pushRuleModel sourceModel: root.pushRuleModel
filterRowCallback: function(source_row, source_parent) { filterRowCallback: function(source_row, source_parent) {
let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole) let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole)
return sectionRole == PushNotificationSection.Room; return sectionRole == PushNotificationSection.Room;
@@ -52,7 +58,7 @@ FormCard.FormCardPage {
FormCard.FormCard { FormCard.FormCard {
Repeater { Repeater {
model: KSortFilterProxyModel { model: KSortFilterProxyModel {
sourceModel: Controller.pushRuleModel sourceModel: root.pushRuleModel
filterRowCallback: function(source_row, source_parent) { filterRowCallback: function(source_row, source_parent) {
let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole) let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole)
return sectionRole == PushNotificationSection.Mentions; return sectionRole == PushNotificationSection.Mentions;
@@ -69,7 +75,7 @@ FormCard.FormCardPage {
FormCard.FormCard { FormCard.FormCard {
Repeater { Repeater {
model: KSortFilterProxyModel { model: KSortFilterProxyModel {
sourceModel: Controller.pushRuleModel sourceModel: root.pushRuleModel
filterRowCallback: function(source_row, source_parent) { filterRowCallback: function(source_row, source_parent) {
let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole) let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole)
@@ -100,7 +106,7 @@ FormCard.FormCardPage {
} }
onAccepted: { onAccepted: {
Controller.pushRuleModel.addKeyword(keywordAddField.text) root.pushRuleModel.addKeyword(keywordAddField.text)
keywordAddField.text = "" keywordAddField.text = ""
} }
} }
@@ -114,7 +120,7 @@ FormCard.FormCardPage {
enabled: NotificationsManager.keywordNotificationAction !== PushNotificationAction.Unknown enabled: NotificationsManager.keywordNotificationAction !== PushNotificationAction.Unknown
onClicked: { onClicked: {
Controller.pushRuleModel.addKeyword(keywordAddField.text) root.pushRuleModel.addKeyword(keywordAddField.text)
keywordAddField.text = "" keywordAddField.text = ""
} }
@@ -133,7 +139,7 @@ FormCard.FormCardPage {
FormCard.FormCard { FormCard.FormCard {
Repeater { Repeater {
model: KSortFilterProxyModel { model: KSortFilterProxyModel {
sourceModel: Controller.pushRuleModel sourceModel: root.pushRuleModel
filterRowCallback: function(source_row, source_parent) { filterRowCallback: function(source_row, source_parent) {
let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole) let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole)
return sectionRole == PushNotificationSection.Invites; return sectionRole == PushNotificationSection.Invites;
@@ -154,7 +160,7 @@ FormCard.FormCardPage {
Repeater { Repeater {
model: KSortFilterProxyModel { model: KSortFilterProxyModel {
id: unknownModel id: unknownModel
sourceModel: Controller.pushRuleModel sourceModel: root.pushRuleModel
filterRowCallback: function(source_row, source_parent) { filterRowCallback: function(source_row, source_parent) {
let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole) let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole)
return sectionRole == PushNotificationSection.Unknown; return sectionRole == PushNotificationSection.Unknown;
@@ -169,9 +175,9 @@ FormCard.FormCardPage {
id: ruleDelegate id: ruleDelegate
NotificationRuleItem { NotificationRuleItem {
onDeleteRule: { onDeleteRule: {
Controller.pushRuleModel.removeKeyword(id) root.pushRuleModel.removeKeyword(id)
} }
onActionChanged: (action) => Controller.pushRuleModel.setPushRuleAction(id, action) onActionChanged: (action) => root.pushRuleModel.setPushRuleAction(id, action)
} }
} }
} }

View File

@@ -18,13 +18,13 @@ Kirigami.ScrollablePage {
title: i18n("Invite a User") title: i18n("Invite a User")
actions { actions: [
main: Kirigami.Action { Kirigami.Action {
icon.name: "dialog-close" icon.name: "dialog-close"
text: i18nc("@action", "Cancel") text: i18nc("@action", "Cancel")
onTriggered: applicationWindow().pageStack.layers.pop() onTriggered: root.closeDialog()
} }
} ]
header: RowLayout { header: RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing Layout.margins: Kirigami.Units.largeSpacing

View File

@@ -69,6 +69,7 @@ Kirigami.ScrollablePage {
valueRole: "url" valueRole: "url"
model: ServerListModel { model: ServerListModel {
id: serverListModel id: serverListModel
connection: root.connection
} }
delegate: Delegates.RoundedItemDelegate { delegate: Delegates.RoundedItemDelegate {

View File

@@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
Kirigami.Page {
title: i18n("Loading…")
Kirigami.LoadingPlaceholder {
id: loadingIndicator
anchors.centerIn: parent
}
}

View File

@@ -34,7 +34,7 @@ Components.AbstractMaximizeComponent {
] ]
content: MapView { content: MapView {
id: map id: mapView
map.plugin: Plugin { map.plugin: Plugin {
name: "osm" name: "osm"
PluginParameter { PluginParameter {
@@ -50,7 +50,7 @@ Components.AbstractMaximizeComponent {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: { onClicked: {
root.location = map.toCoordinate(Qt.point(mouseX, mouseY), false) root.location = mapView.map.toCoordinate(Qt.point(mouseX, mouseY), false)
} }
} }

View File

@@ -129,7 +129,6 @@ QQC2.TextArea {
ChatDocumentHandler { ChatDocumentHandler {
id: documentHandler id: documentHandler
isEdit: true
document: root.textDocument document: root.textDocument
cursorPosition: root.cursorPosition cursorPosition: root.cursorPosition
selectionStart: root.selectionStart selectionStart: root.selectionStart

View File

@@ -16,6 +16,10 @@ FormCard.FormCardPage {
property NeoChatRoom room property NeoChatRoom room
property PushRuleModel pushRuleModel: PushRuleModel {
connection: root.room.connection
}
title: i18nc('@title:window', 'Notifications') title: i18nc('@title:window', 'Notifications')
FormCard.FormHeader { FormCard.FormHeader {
@@ -63,7 +67,7 @@ FormCard.FormCardPage {
FormCard.FormCard { FormCard.FormCard {
Repeater { Repeater {
model: KSortFilterProxyModel { model: KSortFilterProxyModel {
sourceModel: Controller.pushRuleModel sourceModel: root.pushRuleModel
filterRowCallback: function(source_row, source_parent) { filterRowCallback: function(source_row, source_parent) {
let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole) let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole)
@@ -78,9 +82,9 @@ FormCard.FormCardPage {
id: ruleDelegate id: ruleDelegate
NotificationRuleItem { NotificationRuleItem {
onDeleteRule: { onDeleteRule: {
Controller.pushRuleModel.removeKeyword(id) root.pushRuleModel.removeKeyword(id)
} }
onActionChanged: (action) => Controller.pushRuleModel.setPushRuleAction(id, action) onActionChanged: (action) => root.pushRuleModel.setPushRuleAction(id, action)
} }
} }
} }
@@ -105,7 +109,7 @@ FormCard.FormCardPage {
} }
onAccepted: { onAccepted: {
Controller.pushRuleModel.addKeyword(keywordAddField.text, root.room.id) root.pushRuleModel.addKeyword(keywordAddField.text, root.room.id)
keywordAddField.text = "" keywordAddField.text = ""
} }
} }
@@ -119,7 +123,7 @@ FormCard.FormCardPage {
enabled: NotificationsManager.keywordNotificationAction !== PushNotificationAction.Unknown && keywordAddField.text.length > 0 enabled: NotificationsManager.keywordNotificationAction !== PushNotificationAction.Unknown && keywordAddField.text.length > 0
onClicked: { onClicked: {
Controller.pushRuleModel.addKeyword(keywordAddField.text, root.room.id) root.pushRuleModel.addKeyword(keywordAddField.text, root.room.id)
keywordAddField.text = "" keywordAddField.text = ""
} }

View File

@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigamiaddons.labs.components as Components
import org.kde.kirigami as Kirigami
import org.kde.prison
Components.AbstractMaximizeComponent {
id: root
required property string text
required property color avatarColor
required property string avatarSource
Shortcut {
sequences: [StandardKey.Cancel]
onActivated: root.close()
}
leading: Components.Avatar {
id: userAvatar
implicitWidth: Kirigami.Units.iconSizes.medium
implicitHeight: Kirigami.Units.iconSizes.medium
name: root.title
source: root.avatarSource
color: root.avatarColor
}
content: Item {
Keys.onEscapePressed: root.close()
Barcode {
barcodeType: Barcode.QRCode
content: root.text
height: Math.min(parent.height, Kirigami.Units.gridUnit * 20)
width: height
anchors.centerIn: parent
}
MouseArea {
id: closeArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: root.close()
}
}
}

View File

@@ -94,7 +94,7 @@ QQC2.Dialog {
connection: root.connection connection: root.connection
onClicked: { onSelected: {
RoomManager.enterRoom(currentRoom); RoomManager.enterRoom(currentRoom);
root.close() root.close()
} }

View File

@@ -6,6 +6,7 @@ import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
import org.kde.coreaddons
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.labs.components as KirigamiComponents import org.kde.kirigamiaddons.labs.components as KirigamiComponents
@@ -77,15 +78,10 @@ Item {
implicitWidth: mainLayout.implicitWidth implicitWidth: mainLayout.implicitWidth
implicitHeight: mainLayout.implicitHeight implicitHeight: mainLayout.implicitHeight
GridLayout { RowLayout {
id: mainLayout id: mainLayout
anchors.fill: parent anchors.fill: parent
implicitHeight: Math.max(replyAvatar.implicitHeight, replyName.implicitHeight) + loader.height spacing: Kirigami.Units.largeSpacing
rows: 2
columns: 3
rowSpacing: Kirigami.Units.smallSpacing
columnSpacing: Kirigami.Units.largeSpacing
Rectangle { Rectangle {
id: verticalBorder id: verticalBorder
@@ -96,47 +92,55 @@ Item {
implicitWidth: Kirigami.Units.smallSpacing implicitWidth: Kirigami.Units.smallSpacing
color: root.author.color color: root.author.color
} }
KirigamiComponents.Avatar { ColumnLayout {
id: replyAvatar spacing: Kirigami.Units.smallSpacing
implicitWidth: Kirigami.Units.iconSizes.small RowLayout {
implicitHeight: Kirigami.Units.iconSizes.small spacing: Kirigami.Units.largeSpacing
source: root.author.avatarSource KirigamiComponents.Avatar {
name: root.author.displayName id: replyAvatar
color: root.author.color
}
QQC2.Label {
id: replyName
Layout.fillWidth: true
color: root.author.color implicitWidth: Kirigami.Units.iconSizes.small
text: root.author.displayName implicitHeight: Kirigami.Units.iconSizes.small
elide: Text.ElideRight
}
Loader {
id: loader
Layout.fillWidth: true source: root.author.avatarSource
Layout.maximumHeight: loader.item && (root.type == DelegateType.Image || root.type == DelegateType.Sticker) ? loader.item.height : loader.item.implicitHeight name: root.author.displayName
Layout.columnSpan: 2 color: root.author.color
}
QQC2.Label {
id: replyName
Layout.fillWidth: true
sourceComponent: { color: root.author.color
switch (root.type) { text: root.author.displayName
case DelegateType.Image: elide: Text.ElideRight
case DelegateType.Sticker: }
return imageComponent; }
case DelegateType.Message: Loader {
case DelegateType.Notice: id: loader
return textComponent;
case DelegateType.File: Layout.fillWidth: true
case DelegateType.Video: Layout.maximumHeight: loader.item && (root.type == DelegateType.Image || root.type == DelegateType.Sticker) ? loader.item.height : loader.item.implicitHeight
case DelegateType.Audio: Layout.columnSpan: 2
return mimeComponent;
case DelegateType.Encrypted: sourceComponent: {
return encryptedComponent; switch (root.type) {
default: case DelegateType.Image:
return textComponent; case DelegateType.Sticker:
return imageComponent;
case DelegateType.Message:
case DelegateType.Notice:
return textComponent;
case DelegateType.File:
case DelegateType.Video:
case DelegateType.Audio:
return mimeComponent;
case DelegateType.Encrypted:
return encryptedComponent;
default:
return textComponent;
}
} }
} }
} }
@@ -187,7 +191,7 @@ Item {
MimeComponent { MimeComponent {
mimeIconSource: root.mediaInfo.mimeIcon mimeIconSource: root.mediaInfo.mimeIcon
label: root.display label: root.display
subLabel: root.type === DelegateType.File ? Controller.formatByteSize(root.mediaInfo.size) : Controller.formatDuration(root.mediaInfo.duration) subLabel: root.type === DelegateType.File ? Format.formatByteSize(root.mediaInfo.size) : Format.formatDuration(root.mediaInfo.duration)
} }
} }
Component { Component {

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