Compare commits

...

61 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
126 changed files with 20175 additions and 16475 deletions

View File

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

View File

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

View File

@@ -9,9 +9,9 @@
android:versionName="${versionName}"
android:versionCode="${versionCode}"
android:installLocation="auto">
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat" android:usesCleartextTraffic="true">
<application android:name="org.qtproject.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"
android:name="org.qtproject.qt5.android.bindings.QtActivity"
android:name="org.qtproject.qt.android.bindings.QtActivity"
android:label="NeoChat"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTop"

View File

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

View File

@@ -40,3 +40,9 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test
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;
TestRoom *room = nullptr;
EventHandler eventHandler;
EventHandler emptyHandler;
EventHandler noEventHandler;
private Q_SLOTS:
void initTestCase();
void nullSetEvent();
void eventId();
void nullEventId();
void delegateType_data();
void delegateType();
void nullDelegateType();
void author();
void nullAuthor();
void authorDisplayName();
void nullAuthorDisplayName();
void time();
void nullTime();
void timeString();
void nullTimeString();
void highlighted();
void nullHighlighted();
void hidden();
void nullHidden();
void body();
void nullBody();
void genericBody_data();
void genericBody();
void nullGenericBody();
void mediaInfo();
void nullMediaInfo();
void linkPreviewer();
void nullLinkPreviewer();
void reactions();
void nullReactions();
void hasReply();
void nullHasReply();
void replyId();
void nullReplyId();
void replyDelegateType();
void nullReplyDelegateType();
void replyAuthor();
void nullReplyAuthor();
void replyBody();
void nullReplyBody();
void replyMediaInfo();
void nullReplyMediaInfo();
void thread();
void nullThread();
void location();
void nullLocation();
void readMarkers();
void nullReadMarkers();
void cleanup();
};
void EventHandlerTest::initTestCase()
@@ -82,6 +109,13 @@ void EventHandlerTest::initTestCase()
room->update(std::move(roomData));
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()
@@ -91,6 +125,12 @@ void EventHandlerTest::eventId()
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()
{
QTest::addColumn<int>("eventNum");
@@ -114,6 +154,12 @@ void EventHandlerTest::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()
{
auto event = room->messageEvents().at(0).get();
@@ -131,6 +177,15 @@ void EventHandlerTest::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()
{
auto event = room->messageEvents().at(1).get();
@@ -139,6 +194,15 @@ void EventHandlerTest::authorDisplayName()
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()
{
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));
}
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()
{
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));
}
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()
{
auto event = room->messageEvents().at(2).get();
@@ -182,6 +266,15 @@ void EventHandlerTest::highlighted()
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()
{
auto event = room->messageEvents().at(3).get();
@@ -195,6 +288,15 @@ void EventHandlerTest::hidden()
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()
{
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"));
}
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()
{
QTest::addColumn<int>("eventNum");
@@ -228,6 +339,12 @@ void EventHandlerTest::genericBody()
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()
{
auto event = room->messageEvents().at(4).get();
@@ -251,6 +368,15 @@ void EventHandlerTest::mediaInfo()
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()
{
auto event = room->messageEvents().at(2).get();
@@ -264,6 +390,15 @@ void EventHandlerTest::linkPreviewer()
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()
{
auto event = room->messageEvents().at(0).get();
@@ -272,6 +407,15 @@ void EventHandlerTest::reactions()
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()
{
auto event = room->messageEvents().at(5).get();
@@ -285,6 +429,12 @@ void EventHandlerTest::hasReply()
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()
{
auto event = room->messageEvents().at(5).get();
@@ -298,6 +448,12 @@ void EventHandlerTest::replyId()
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()
{
auto event = room->messageEvents().at(5).get();
@@ -311,6 +467,15 @@ void EventHandlerTest::replyDelegateType()
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()
{
auto event = room->messageEvents().at(5).get();
@@ -334,6 +499,15 @@ void EventHandlerTest::replyAuthor()
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()
{
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"));
}
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()
{
auto event = room->messageEvents().at(6).get();
@@ -369,6 +552,15 @@ void EventHandlerTest::replyMediaInfo()
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()
{
auto event = room->messageEvents().at(0).get();
@@ -392,6 +584,15 @@ void EventHandlerTest::thread()
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()
{
auto event = room->messageEvents().at(7).get();
@@ -402,6 +603,18 @@ void EventHandlerTest::location()
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()
{
auto event = room->messageEvents().at(0).get();
@@ -431,5 +644,37 @@ void EventHandlerTest::readMarkers()
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)
#include "eventhandlertest.moc"

View File

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

View File

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

View File

@@ -343,6 +343,7 @@ to provide a convergent experience across multiple platforms.</p>
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="23.08.3" date="2023-11-09"/>
<release version="23.08.2" date="2023-10-12"/>
<release version="23.08.0" date="2023-08-24">
<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/AccountMenu.qml
qml/ExploreComponent.qml
qml/ExploreComponentMobile.qml
qml/ContextMenu.qml
qml/CollapsedRoomDelegate.qml
qml/RoomDelegate.qml
qml/RoomListPage.qml
qml/SpaceListContextMenu.qml
qml/UserInfo.qml
qml/LoadingPage.qml
qml/UserInfoDesktop.qml
qml/RoomPage.qml
qml/RoomWindow.qml
qml/JoinRoomPage.qml
@@ -163,7 +164,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/ImageEditorPage.qml
qml/WelcomePage.qml
qml/General.qml
qml/Security.qml
qml/RoomSecurity.qml
qml/PushNotification.qml
qml/Categories.qml
qml/Permissions.qml
@@ -286,6 +287,9 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/SpaceHierarchyDelegate.qml
qml/RemoveChildDialog.qml
qml/SelectParentDialog.qml
qml/Security.qml
qml/QrCodeMaximizeComponent.qml
qml/SelectSpacesDialog.qml
RESOURCES
qml/confetti.png
qml/glowdot.png
@@ -307,6 +311,13 @@ ecm_qt_declare_logging_category(neochat
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
main.cpp
)
@@ -453,6 +464,7 @@ if(ANDROID)
"gps"
"system-users-symbolic"
)
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR/android})
else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets)
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(),
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();

View File

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

View File

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

View File

@@ -62,14 +62,6 @@ class ChatDocumentHandler : public QObject
Q_OBJECT
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.
*/
@@ -96,7 +88,7 @@ class ChatDocumentHandler : public QObject
* This is typically provided to a qml component to visualise the current
* 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.
@@ -121,9 +113,6 @@ class ChatDocumentHandler : public QObject
public:
explicit ChatDocumentHandler(QObject *parent = nullptr);
[[nodiscard]] bool isEdit() const;
void setIsEdit(bool edit);
[[nodiscard]] QQuickTextDocument *document() const;
void setDocument(QQuickTextDocument *document);
@@ -144,7 +133,6 @@ public:
Q_INVOKABLE void complete(int index);
void updateCompletions();
CompletionModel *completionModel() const;
[[nodiscard]] QColor mentionColor() const;
@@ -154,12 +142,10 @@ public:
void setErrorColor(const QColor &color);
Q_SIGNALS:
void isEditChanged();
void documentChanged();
void cursorPositionChanged();
void roomChanged();
void chatBarCacheChanged();
void completionModelChanged();
void selectionStartChanged();
void selectionEndChanged();
void errorColorChanged();
@@ -168,8 +154,6 @@ Q_SIGNALS:
private:
int completionStartIndex() const;
bool m_isEdit = false;
QPointer<QQuickTextDocument> m_document;
QPointer<NeoChatRoom> m_room;

View File

@@ -6,13 +6,8 @@
#include <qt6keychain/keychain.h>
#include <KConfig>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KWindowConfig>
#ifdef HAVE_WINDOWSYSTEM
#include <KWindowEffects>
#endif
#include <QFile>
#include <QFileInfo>
@@ -65,7 +60,8 @@ Controller::Controller(QObject *parent)
invokeLogin();
});
QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [] {
QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] {
delete m_trayIcon;
NeoChatConfig::self()->save();
});
@@ -103,10 +99,6 @@ Controller::Controller(QObject *parent)
}
oldAccountCount = m_accountRegistry.size();
});
QTimer::singleShot(0, this, [this] {
m_pushRuleModel = new PushRuleModel;
});
}
Controller &Controller::instance()
@@ -175,6 +167,8 @@ void Controller::invokeLogin()
QString id = NeoChatConfig::self()->activeConnection();
for (const auto &accountId : accounts) {
AccountSettings account{accountId};
m_accountsLoading += accountId;
Q_EMIT accountsLoadingChanged();
if (id.isEmpty()) {
// handle case where the account config is empty
id = accountId;
@@ -194,6 +188,8 @@ void Controller::invokeLogin()
connect(connection, &NeoChatConnection::connected, this, [this, connection, id] {
connection->loadState();
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
Q_EMIT accountsLoadingChanged();
if (connection->userId() == id) {
setActiveConnection(connection);
connectSingleShot(connection, &NeoChatConnection::syncDone, this, &Controller::initiated);
@@ -354,12 +350,6 @@ void Controller::setActiveConnection(NeoChatConnection *connection)
}
NeoChatConfig::self()->save();
Q_EMIT activeConnectionChanged();
Q_EMIT activeConnectionIndexChanged();
}
PushRuleModel *Controller::pushRuleModel() const
{
return m_pushRuleModel;
}
void Controller::saveWindowGeometry()
@@ -384,56 +374,6 @@ void Controller::joinRoom(const QString &alias)
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)
{
// HACK: Workaround bug QTBUG 93281
@@ -470,14 +410,6 @@ void Controller::setApplicationProxy()
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
{
#ifdef NEOCHAT_FLATPAK

View File

@@ -3,13 +3,10 @@
#pragma once
#include "models/pushrulemodel.h"
#include <QObject>
#include <QQmlEngine>
#include <QQuickItem>
#include <KFormat>
#include "neochatconnection.h"
#include <Quotient/accountregistry.h>
#include <Quotient/jobs/basejob.h>
@@ -49,26 +46,11 @@ class Controller : public QObject
*/
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.
*/
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.
*/
@@ -81,6 +63,8 @@ class Controller : public QObject
*/
Q_PROPERTY(bool isFlatpak READ isFlatpak CONSTANT)
Q_PROPERTY(QStringList accountsLoading MEMBER m_accountsLoading NOTIFY accountsLoadingChanged)
public:
/**
* @brief Defines the status after an attempt to change the password on an account.
@@ -102,8 +86,6 @@ public:
void setActiveConnection(NeoChatConnection *connection);
[[nodiscard]] NeoChatConnection *activeConnection() const;
[[nodiscard]] PushRuleModel *pushRuleModel() const;
/**
* @brief Add a new connection to the account registry.
*/
@@ -114,8 +96,6 @@ public:
*/
void dropConnection(NeoChatConnection *c);
int activeConnectionIndex() const;
/**
* @brief Save an access token to the keychain for the given account.
*/
@@ -126,20 +106,8 @@ public:
*/
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;
/**
* @brief Set the background blur status of the given item.
*/
Q_INVOKABLE void setBlur(QQuickItem *item, bool blur);
bool isOnline() const;
/**
@@ -151,20 +119,6 @@ public:
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.
*
@@ -187,10 +141,8 @@ private:
bool m_isOnline = true;
QMap<Quotient::Room *, int> m_notificationCounts;
bool hasWindowSystem() const;
QPointer<PushRuleModel> m_pushRuleModel;
Quotient::AccountRegistry m_accountRegistry;
QStringList m_accountsLoading;
private Q_SLOTS:
void invokeLogin();
@@ -213,7 +165,7 @@ Q_SIGNALS:
void passwordStatus(Controller::PasswordStatus status);
void userConsentRequired(QUrl url);
void isOnlineChanged(bool isOnline);
void activeConnectionIndexChanged();
void accountsLoadingChanged();
public Q_SLOTS:
void saveWindowGeometry();

View File

@@ -19,6 +19,7 @@
#include <Quotient/events/stickerevent.h>
#include <Quotient/quotient_common.h>
#include "delegatetype.h"
#include "eventhandler_logging.h"
#include "events/pollevent.h"
#include "linkpreviewer.h"
@@ -50,6 +51,10 @@ const Quotient::Event *EventHandler::getEvent() const
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) {
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
{
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 {};
}
if (isPending && lastUpdated == QDateTime()) {
@@ -216,6 +221,15 @@ bool EventHandler::isHighlighted()
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()) {
return true;
}
@@ -611,6 +625,14 @@ QString EventHandler::getGenericBody() 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);
}
@@ -723,6 +745,14 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
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>()) {
return nullptr;
}
@@ -754,7 +784,7 @@ QSharedPointer<ReactionModel> EventHandler::getReactions() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReactions called with m_room set to nullptr.";
return {};
return nullptr;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReactions called with m_event set to nullptr.";
@@ -806,16 +836,33 @@ QSharedPointer<ReactionModel> EventHandler::getReactions() 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();
}
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();
}
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);
if (replyEvent == nullptr) {
return DelegateType::Other;
@@ -903,6 +950,11 @@ QVariantMap EventHandler::getReplyMediaInfo() 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)
&& 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));
@@ -910,6 +962,11 @@ bool EventHandler::isThreaded() 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.
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) {
@@ -925,6 +982,11 @@ QString EventHandler::threadRoot() 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();
if (geoUri.isEmpty()) {
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
{
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();
if (geoUri.isEmpty()) {
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
{
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();
if (assetType.isEmpty()) {
return {};
@@ -954,6 +1026,15 @@ QString EventHandler::getLocationAssetType() 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());
userIds.remove(m_room->localUser()->id());
return userIds.size() > 0;
@@ -961,6 +1042,15 @@ bool EventHandler::hasReadMarkers() 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());
userIds_temp.remove(m_room->localUser()->id());
@@ -981,6 +1071,15 @@ QVariantList EventHandler::getReadMarkers(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());
userIds.remove(m_room->localUser()->id());
@@ -993,6 +1092,15 @@ QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) 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());
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));
});
connectSingleShot(m_connection, &Connection::syncDone, this, []() {
connectSingleShot(m_connection, &Connection::syncDone, this, [this]() {
Q_EMIT loaded();
Q_EMIT Controller::instance().initiated();
});
}

View File

@@ -79,7 +79,17 @@ class LoginHelper : public QObject
Q_PROPERTY(bool isInvalidPassword READ isInvalidPassword NOTIFY isInvalidPasswordChanged)
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();
@@ -125,6 +135,7 @@ Q_SIGNALS:
void isLoggingInChanged();
void isLoggedInChanged();
void isInvalidPasswordChanged();
void loaded();
private:
void setHomeserverReachable(bool reachable);
@@ -141,4 +152,5 @@ private:
bool m_isLoggingIn = false;
bool m_isLoggedIn = false;
bool m_invalidPassword = false;
explicit LoginHelper(QObject *parent = nullptr);
};

View File

@@ -35,7 +35,6 @@
#include "neochat-version.h"
#include <Quotient/networkaccessmanager.h>
#include <Quotient/util.h>
#include "blurhashimageprovider.h"
#include "colorschemer.h"
@@ -132,7 +131,11 @@ int main(int argc, char *argv[])
i18n("Matrix client"),
KAboutLicense::GPL_V3,
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("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"));
@@ -141,7 +144,7 @@ int main(int argc, char *argv[])
about.setOrganizationDomain("kde.org");
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>)",
"%1 (built against %2)",
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.load(QUrl(QStringLiteral("qrc:/org/kde/neochat/qml/main.qml")));
@@ -233,8 +236,8 @@ int main(int argc, char *argv[])
}
#ifdef HAVE_RUNNER
Runner runner;
QDBusConnection::sessionBus().registerObject("/RoomRunner"_ls, &runner, QDBusConnection::ExportScriptableContents);
auto runner = Runner::create(&engine, &engine);
QDBusConnection::sessionBus().registerObject("/RoomRunner"_ls, runner, QDBusConnection::ExportScriptableContents);
#endif
QWindow *window = windowFromEngine(&engine);

View File

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

View File

@@ -10,6 +10,8 @@
#include <QReadWriteLock>
class NeoChatConnection;
/**
* @class ThumbnailResponse
*
@@ -21,7 +23,7 @@ class ThumbnailResponse : public QQuickImageResponse
{
Q_OBJECT
public:
explicit ThumbnailResponse(QString mediaId, QSize requestedSize);
explicit ThumbnailResponse(QString mediaId, QSize requestedSize, NeoChatConnection *m_connection);
~ThumbnailResponse() override = default;
private Q_SLOTS:
@@ -33,6 +35,7 @@ private:
QSize requestedSize;
const QString localFile;
Quotient::MediaThumbnailJob *job = nullptr;
NeoChatConnection *m_connection;
QImage image;
QString errorStr;
@@ -51,11 +54,27 @@ private:
*/
class MatrixImageProvider : public QQuickAsyncImageProvider
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(NeoChatConnection *connection MEMBER m_connection)
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.
*
* @sa QQuickAsyncImageProvider::requestImageResponse
*/
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();
}
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);
if (parts.length() >= 2) {
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));
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));
return QString();
}

View File

@@ -15,13 +15,28 @@
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()
{
if (!Controller::instance().activeConnection()) {
if (!m_connection) {
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) {
return;
}
@@ -53,10 +68,10 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
{
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] {
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes"_ls);
connect(job, &BaseJob::success, this, [name, location, job, this] {
const auto &data = m_connection->accountData("im.ponies.user_emotes"_ls);
auto json = data != nullptr ? data->contentJson() : QJsonObject();
auto emojiData = json["images"_ls].toObject();
@@ -78,7 +93,7 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
});
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;
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);
auto json = data->contentJson();
const QString _name = name.mid(1).chopped(1);
@@ -109,18 +124,18 @@ void CustomEmojiModel::removeEmoji(const QString &name)
emojiData.remove(_name);
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)
: QAbstractListModel(parent)
{
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this]() {
if (!Controller::instance().activeConnection()) {
connect(this, &CustomEmojiModel::connectionChanged, this, [this]() {
if (!m_connection) {
return;
}
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")) {
return;
}

View File

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

View File

@@ -24,8 +24,8 @@ DevicesModel::DevicesModel(QObject *parent)
void DevicesModel::fetchDevices()
{
if (Controller::instance().activeConnection()) {
auto job = Controller::instance().activeConnection()->callApi<GetDevicesJob>();
if (m_connection) {
auto job = m_connection->callApi<GetDevicesJob>();
connect(job, &BaseJob::success, this, [this, job]() {
beginResetModel();
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++)
;
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] {
auto onSuccess = [this, index]() {
@@ -117,9 +117,9 @@ void DevicesModel::logout(const QString &deviceId, const QString &password)
authData["session"_ls] = replyData["session"_ls];
authData["password"_ls] = password;
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;
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);
} else {
onSuccess();
@@ -132,7 +132,7 @@ void DevicesModel::setName(const QString &deviceId, const QString &name)
int 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;
beginResetModel();
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) {
Q_UNUSED(deviceId);
if (userId == Controller::instance().activeConnection()->userId()) {
if (userId == m_connection->userId()) {
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)
{
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)
@@ -87,7 +74,7 @@ void PushRuleModel::updateNotificationRules(const QString &type)
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());
beginResetModel();
@@ -156,8 +143,7 @@ PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule ru
*
* Rooms that the user hasn't joined shouldn't have a rule.
*/
auto connection = Controller::instance().activeConnection();
if (connection->room(ruleId) != nullptr) {
if (m_connection->room(ruleId) != nullptr) {
return PushNotificationSection::Undefined;
}
/**
@@ -171,7 +157,7 @@ PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule ru
if (!testUserId.startsWith(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;
}
// 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);
}
auto job = Controller::instance().activeConnection()->callApi<Quotient::SetPushRuleJob>(QLatin1String("global"),
PushNotificationKind::kindString(kind),
keyword,
actions,
QString(),
QString(),
pushConditions,
roomId.isEmpty() ? keyword : QString());
auto job = m_connection->callApi<Quotient::SetPushRuleJob>(QLatin1String("global"),
PushNotificationKind::kindString(kind),
keyword,
actions,
QString(),
QString(),
pushConditions,
roomId.isEmpty() ? keyword : QString());
connect(job, &Quotient::BaseJob::failure, this, [job, keyword]() {
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 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]() {
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)
{
auto job = Controller::instance().activeConnection()->callApi<Quotient::IsPushRuleEnabledJob>(QStringLiteral("global"), kind, ruleId);
connect(job, &Quotient::BaseJob::success, this, [job, kind, ruleId, enabled]() {
auto job = m_connection->callApi<Quotient::IsPushRuleEnabledJob>(QStringLiteral("global"), kind, ruleId);
connect(job, &Quotient::BaseJob::success, this, [job, kind, ruleId, enabled, this]() {
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);
}
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)
@@ -453,4 +439,23 @@ QList<QVariant> PushRuleModel::actionToVariant(PushNotificationAction::Action ac
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"

View File

@@ -157,6 +157,8 @@ class PushRuleModel : public QAbstractListModel
*/
Q_PROPERTY(bool globalNotificationsSet READ globalNotificationsSet NOTIFY globalNotificationsSetChanged)
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
public:
struct Rule {
QString id;
@@ -225,18 +227,22 @@ public:
*/
Q_INVOKABLE void removeKeyword(const QString &keyword);
void setConnection(NeoChatConnection *connection);
NeoChatConnection *connection() const;
Q_SIGNALS:
void defaultStateChanged();
void globalNotificationsEnabledChanged();
void globalNotificationsSetChanged();
void connectionChanged();
private Q_SLOTS:
void controllerConnectionChanged();
void updateNotificationRules(const QString &type);
private:
PushNotificationAction::Action m_defaultKeywordAction;
QList<Rule> m_rules;
NeoChatConnection *m_connection;
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 KConfigGroup serverGroup = stateConfig->group(QStringLiteral("Servers"));
QString domain = Controller::instance().activeConnection()->domain();
QString domain = m_connection->domain();
// Add the user's homeserver
m_servers.append(Server{
@@ -100,7 +100,7 @@ void ServerListModel::checkServer(const QString &url)
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] {
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"

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,7 @@
#include <Quotient/csapi/content-repo.h>
#include <Quotient/csapi/profile.h>
#include <Quotient/database.h>
#include <Quotient/qt_connection_util.h>
#include <Quotient/settings.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"

View File

@@ -23,6 +23,8 @@ class NeoChatConnection : public Quotient::Connection
* Set to an empty string to remove the label.
*/
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:
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);
/**
* @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:
void labelChanged();
};

View File

@@ -123,6 +123,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
Q_EMIT canEncryptRoomChanged();
Q_EMIT parentIdsChanged();
Q_EMIT canonicalParentChanged();
Q_EMIT joinRuleChanged();
});
connect(connection, &Connection::capabilitiesLoaded, this, &NeoChatRoom::maxRoomVersionChanged);
connect(this, &Room::changed, this, [this]() {
@@ -418,30 +419,6 @@ QDateTime NeoChatRoom::lastActiveTime()
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.
static const QVariantMap emptyUser = {
{"isLocalUser"_ls, false},
@@ -736,16 +713,51 @@ QString NeoChatRoom::joinRule() const
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)) {
qWarning() << "Power level too low to set join rules";
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.
}
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
{
return currentState().get("m.room.history_visibility"_ls)->contentJson()["history_visibility"_ls].toString();
@@ -1165,6 +1177,21 @@ QList<QString> NeoChatRoom::parentIds() const
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
{
auto parentEvents = currentState().eventsOfType("m.space.parent"_ls);
@@ -1362,7 +1389,7 @@ void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
for (const auto &i : roomRuleArray) {
QJsonObject roomRule = i.toObject();
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) {
QJsonObject overrideRule = i.toObject();
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};
// Add new override rule and make sure it's enabled
auto job = Controller::instance()
.activeConnection()
->callApi<SetPushRuleJob>("global"_ls, "override"_ls, id(), actions, QString(), QString(), conditions, QString());
auto job = connection()->callApi<SetPushRuleJob>("global"_ls, "override"_ls, id(), actions, QString(), QString(), conditions, QString());
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]() {
m_pushNotificationStateUpdating = false;
});
@@ -1437,11 +1462,9 @@ void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
// No conditions for a room rule
const QList<PushCondition> conditions;
auto setJob = Controller::instance()
.activeConnection()
->callApi<SetPushRuleJob>("global"_ls, "room"_ls, id(), actions, QString(), QString(), conditions, QString());
auto setJob = connection()->callApi<SetPushRuleJob>("global"_ls, "room"_ls, id(), actions, QString(), QString(), conditions, QString());
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]() {
m_pushNotificationStateUpdating = false;
});
@@ -1470,11 +1493,9 @@ void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
const QList<PushCondition> conditions;
// Add new room rule and make sure enabled
auto setJob = Controller::instance()
.activeConnection()
->callApi<SetPushRuleJob>("global"_ls, "room"_ls, id(), actions, QString(), QString(), conditions, QString());
auto setJob = connection()->callApi<SetPushRuleJob>("global"_ls, "room"_ls, id(), actions, QString(), QString(), conditions, QString());
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]() {
m_pushNotificationStateUpdating = false;
});

View File

@@ -160,6 +160,13 @@ class NeoChatRoom : public Quotient::Room
*/
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.
*
@@ -324,28 +331,6 @@ public:
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.
*
@@ -527,6 +512,17 @@ public:
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;
void setCanonicalParent(const QString &parentId);
@@ -581,7 +577,23 @@ public:
Q_INVOKABLE void clearInvitationNotification();
[[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;

View File

@@ -204,15 +204,15 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
notification->setText(notification->text() + QLatin1Char('\n') + entry);
notification->setPixmap(createNotificationImage(icon, room));
notification->setDefaultAction(i18n("Open NeoChat in this room"));
connect(notification, &KNotification::defaultActivated, this, [notification, room]() {
auto defaultAction = notification->addDefaultAction(i18n("Open NeoChat in this room"));
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
if (!room) {
return;
}
if (room->localUser()->id() != Controller::instance().activeConnection()->userId()) {
Controller::instance().setActiveConnection(dynamic_cast<NeoChatConnection *>(Controller::instance().accounts().get(room->localUser()->id())));
}
auto connection = dynamic_cast<NeoChatConnection *>(Controller::instance().accounts().get(room->localUser()->id()));
Controller::instance().setActiveConnection(connection);
RoomManager::instance().setConnection(connection);
RoomManager::instance().enterRoom(room);
});
@@ -240,28 +240,31 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *room, const QStri
notification->setTitle(title);
notification->setPixmap(createNotificationImage(icon, nullptr));
notification->setFlags(KNotification::Persistent);
notification->setDefaultAction(i18n("Open this invitation in NeoChat"));
connect(notification, &KNotification::defaultActivated, this, [notification, room]() {
auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat"));
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
notification->close();
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) {
return;
}
room->acceptInvitation();
notification->close();
});
connect(notification, &KNotification::action2Activated, this, [room, notification]() {
connect(rejectAction, &KNotificationAction::activated, this, [room, notification]() {
if (!room) {
return;
}
RoomManager::instance().leaveRoom(room);
notification->close();
});
connect(notification, &KNotification::action3Activated, this, [room, notification]() {
connect(rejectAndIgnoreAction, &KNotificationAction::activated, this, [room, notification]() {
if (!room) {
return;
}

View File

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

View File

@@ -6,6 +6,7 @@ import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtMultimedia
import org.kde.coreaddons
import org.kde.kirigami as Kirigami
import org.kde.neochat
@@ -127,7 +128,7 @@ MessageDelegate {
QQC2.Label {
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 {
@@ -135,7 +136,7 @@ MessageDelegate {
Layout.rightMargin: Kirigami.Units.smallSpacing
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"
text: i18n("Security")
icon.name: "security-low"
page: Qt.resolvedUrl("Security.qml")
page: Qt.resolvedUrl("RoomSecurity.qml")
initialProperties: {
return {
room: root.room

View File

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

View File

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

View File

@@ -11,6 +11,13 @@ import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat
ColumnLayout {
id: root
/**
* @brief The current room that user is viewing.
*/
required property NeoChatRoom room
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 0
@@ -26,15 +33,15 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter
onClicked: {
RoomManager.visitUser(room.getUser(room.directChatRemoteUser.id).object, "mention")
RoomManager.visitUser(root.room.getUser(root.room.directChatRemoteUser.id).object, "mention")
}
contentItem: KirigamiComponents.Avatar {
name: room ? room.displayName : ""
source: room ? ("image://mxc/" + room.avatarMediaId) : ""
name: root.room ? root.room.displayName : ""
source: root.room ? ("image://mxc/" + root.room.avatarMediaId) : ""
Rectangle {
visible: room.usesEncryption
visible: root.room.usesEncryption
color: Kirigami.Theme.backgroundColor
width: Kirigami.Units.gridUnit
@@ -56,7 +63,7 @@ ColumnLayout {
Layout.fillWidth: true
type: Kirigami.Heading.Type.Primary
wrapMode: QQC2.Label.Wrap
text: room.displayName
text: root.room.displayName
textFormat: Text.PlainText
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 Qt.labs.platform
import org.kde.coreaddons
import org.kde.kirigami as Kirigami
import org.kde.neochat
@@ -104,7 +105,7 @@ MessageDelegate {
PropertyChanges {
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 {
target: downloadButton
@@ -140,7 +141,7 @@ MessageDelegate {
QQC2.Label {
id: sizeLabel
Layout.fillWidth: true
text: Controller.formatByteSize(root.mediaInfo.size)
text: Format.formatByteSize(root.mediaInfo.size)
opacity: 0.7
elide: Text.ElideRight
maximumLineCount: 1

View File

@@ -14,17 +14,23 @@ import org.kde.neochat
FormCard.FormCardPage {
id: root
required property NeoChatConnection connection
title: i18nc("@title:window", "Notifications")
property PushRuleModel pushRuleModel: PushRuleModel {
connection: root.connection
}
FormCard.FormCard {
Layout.topMargin: Kirigami.Units.largeSpacing
FormCard.FormCheckDelegate {
text: i18n("Enable notifications for this account")
description: i18n("Whether push notifications are generated by your Matrix server")
checked: Controller.pushRuleModel.globalNotificationsEnabled
enabled: Controller.pushRuleModel.globalNotificationsSet
checked: root.pushRuleModel.globalNotificationsEnabled
enabled: root.pushRuleModel.globalNotificationsSet
onToggled: {
Controller.pushRuleModel.globalNotificationsEnabled = checked
root.pushRuleModel.globalNotificationsEnabled = checked
}
}
}
@@ -35,7 +41,7 @@ FormCard.FormCardPage {
FormCard.FormCard {
Repeater {
model: KSortFilterProxyModel {
sourceModel: Controller.pushRuleModel
sourceModel: root.pushRuleModel
filterRowCallback: function(source_row, source_parent) {
let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole)
return sectionRole == PushNotificationSection.Room;
@@ -52,7 +58,7 @@ FormCard.FormCardPage {
FormCard.FormCard {
Repeater {
model: KSortFilterProxyModel {
sourceModel: Controller.pushRuleModel
sourceModel: root.pushRuleModel
filterRowCallback: function(source_row, source_parent) {
let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole)
return sectionRole == PushNotificationSection.Mentions;
@@ -69,7 +75,7 @@ FormCard.FormCardPage {
FormCard.FormCard {
Repeater {
model: KSortFilterProxyModel {
sourceModel: Controller.pushRuleModel
sourceModel: root.pushRuleModel
filterRowCallback: function(source_row, source_parent) {
let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole)
@@ -100,7 +106,7 @@ FormCard.FormCardPage {
}
onAccepted: {
Controller.pushRuleModel.addKeyword(keywordAddField.text)
root.pushRuleModel.addKeyword(keywordAddField.text)
keywordAddField.text = ""
}
}
@@ -114,7 +120,7 @@ FormCard.FormCardPage {
enabled: NotificationsManager.keywordNotificationAction !== PushNotificationAction.Unknown
onClicked: {
Controller.pushRuleModel.addKeyword(keywordAddField.text)
root.pushRuleModel.addKeyword(keywordAddField.text)
keywordAddField.text = ""
}
@@ -133,7 +139,7 @@ FormCard.FormCardPage {
FormCard.FormCard {
Repeater {
model: KSortFilterProxyModel {
sourceModel: Controller.pushRuleModel
sourceModel: root.pushRuleModel
filterRowCallback: function(source_row, source_parent) {
let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole)
return sectionRole == PushNotificationSection.Invites;
@@ -154,7 +160,7 @@ FormCard.FormCardPage {
Repeater {
model: KSortFilterProxyModel {
id: unknownModel
sourceModel: Controller.pushRuleModel
sourceModel: root.pushRuleModel
filterRowCallback: function(source_row, source_parent) {
let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole)
return sectionRole == PushNotificationSection.Unknown;
@@ -169,9 +175,9 @@ FormCard.FormCardPage {
id: ruleDelegate
NotificationRuleItem {
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")
actions {
main: Kirigami.Action {
actions: [
Kirigami.Action {
icon.name: "dialog-close"
text: i18nc("@action", "Cancel")
onTriggered: applicationWindow().pageStack.layers.pop()
onTriggered: root.closeDialog()
}
}
]
header: RowLayout {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing

View File

@@ -69,6 +69,7 @@ Kirigami.ScrollablePage {
valueRole: "url"
model: ServerListModel {
id: serverListModel
connection: root.connection
}
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 {
id: map
id: mapView
map.plugin: Plugin {
name: "osm"
PluginParameter {
@@ -50,7 +50,7 @@ Components.AbstractMaximizeComponent {
MouseArea {
anchors.fill: parent
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 {
id: documentHandler
isEdit: true
document: root.textDocument
cursorPosition: root.cursorPosition
selectionStart: root.selectionStart

View File

@@ -16,6 +16,10 @@ FormCard.FormCardPage {
property NeoChatRoom room
property PushRuleModel pushRuleModel: PushRuleModel {
connection: root.room.connection
}
title: i18nc('@title:window', 'Notifications')
FormCard.FormHeader {
@@ -63,7 +67,7 @@ FormCard.FormCardPage {
FormCard.FormCard {
Repeater {
model: KSortFilterProxyModel {
sourceModel: Controller.pushRuleModel
sourceModel: root.pushRuleModel
filterRowCallback: function(source_row, source_parent) {
let sectionRole = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PushRuleModel.SectionRole)
@@ -78,9 +82,9 @@ FormCard.FormCardPage {
id: ruleDelegate
NotificationRuleItem {
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: {
Controller.pushRuleModel.addKeyword(keywordAddField.text, root.room.id)
root.pushRuleModel.addKeyword(keywordAddField.text, root.room.id)
keywordAddField.text = ""
}
}
@@ -119,7 +123,7 @@ FormCard.FormCardPage {
enabled: NotificationsManager.keywordNotificationAction !== PushNotificationAction.Unknown && keywordAddField.text.length > 0
onClicked: {
Controller.pushRuleModel.addKeyword(keywordAddField.text, root.room.id)
root.pushRuleModel.addKeyword(keywordAddField.text, root.room.id)
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
onClicked: {
onSelected: {
RoomManager.enterRoom(currentRoom);
root.close()
}

View File

@@ -6,6 +6,7 @@ import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.coreaddons
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
@@ -77,15 +78,10 @@ Item {
implicitWidth: mainLayout.implicitWidth
implicitHeight: mainLayout.implicitHeight
GridLayout {
RowLayout {
id: mainLayout
anchors.fill: parent
implicitHeight: Math.max(replyAvatar.implicitHeight, replyName.implicitHeight) + loader.height
rows: 2
columns: 3
rowSpacing: Kirigami.Units.smallSpacing
columnSpacing: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
Rectangle {
id: verticalBorder
@@ -96,47 +92,55 @@ Item {
implicitWidth: Kirigami.Units.smallSpacing
color: root.author.color
}
KirigamiComponents.Avatar {
id: replyAvatar
ColumnLayout {
spacing: Kirigami.Units.smallSpacing
implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: Kirigami.Units.iconSizes.small
RowLayout {
spacing: Kirigami.Units.largeSpacing
source: root.author.avatarSource
name: root.author.displayName
color: root.author.color
}
QQC2.Label {
id: replyName
Layout.fillWidth: true
KirigamiComponents.Avatar {
id: replyAvatar
color: root.author.color
text: root.author.displayName
elide: Text.ElideRight
}
Loader {
id: loader
implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: Kirigami.Units.iconSizes.small
Layout.fillWidth: true
Layout.maximumHeight: loader.item && (root.type == DelegateType.Image || root.type == DelegateType.Sticker) ? loader.item.height : loader.item.implicitHeight
Layout.columnSpan: 2
source: root.author.avatarSource
name: root.author.displayName
color: root.author.color
}
QQC2.Label {
id: replyName
Layout.fillWidth: true
sourceComponent: {
switch (root.type) {
case DelegateType.Image:
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;
color: root.author.color
text: root.author.displayName
elide: Text.ElideRight
}
}
Loader {
id: loader
Layout.fillWidth: true
Layout.maximumHeight: loader.item && (root.type == DelegateType.Image || root.type == DelegateType.Sticker) ? loader.item.height : loader.item.implicitHeight
Layout.columnSpan: 2
sourceComponent: {
switch (root.type) {
case DelegateType.Image:
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 {
mimeIconSource: root.mediaInfo.mimeIcon
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 {

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