Compare commits

...

59 Commits

Author SHA1 Message Date
Tobias Fella
ce3915e22a Remove unneeded change 2023-04-11 16:02:47 +02:00
Tobias Fella
741f6ea659 Apply 1 suggestion(s) to 1 file(s) 2023-04-11 16:02:47 +02:00
Tobias Fella
310e9b7ba3 Remove compatibility with libQuotient 0.6 2023-04-11 16:02:45 +02:00
Tobias Fella
57b6f00d8e Use better include for QCoroTask 2023-04-11 15:34:12 +02:00
Tobias Fella
280c9327cb Remove unused signal 2023-04-11 15:31:40 +02:00
l10n daemon script
05bcbb695f GIT_SILENT Sync po/docbooks with svn 2023-04-11 02:21:00 +00:00
James Graham
552f4e8f13 Fix Rich Emote Test Windows Qt5
Make sure that the receiveRichEmote test grabs an actual colour for the user as it can vary by platform.
2023-04-10 12:55:31 +00:00
Carl Schwan
1223c5348d Rename ConfirmLogoutDialog to Dialog.ConfirmLogout
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2023-04-10 13:54:57 +02:00
Carl Schwan
3ccff4f337 Refactor room list in multiple sub components
Also modernize the codebase where possible (e.g use required properties,
reorder properties, fix warnings, don't use Action when not needed)

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2023-04-10 13:53:42 +02:00
l10n daemon script
a67f3334ea GIT_SILENT Sync po/docbooks with svn 2023-04-10 01:50:51 +00:00
Tobias Fella
52dafbb6c8 Revert "Implement sending location messages"
This reverts commit f3a04635cf
2023-04-09 20:22:06 +00:00
James Graham
4341cc437d Handle escaped html for plain text output
Always unescape html for plain text output no matter what the input is.
2023-04-09 14:20:22 +00:00
James Graham
7bb7dd7bbb Document messageeventmodel
Document the API and cleanup some unused roles.
2023-04-09 14:02:30 +00:00
James Graham
b4090d9671 Fix Chat bubble background changing color when created
Only enable the animating of the bubble background when the temporary highlight is applied to avoid it flashing white on entry.

BUG: 468124
2023-04-09 12:13:46 +00:00
l10n daemon script
3d2bcce99a GIT_SILENT Sync po/docbooks with svn 2023-04-09 01:54:23 +00:00
Tobias Fella
f3a04635cf Implement sending location messages 2023-04-09 00:44:02 +00:00
Tobias Fella
419cb07557 Revert "Temporarily disable android Qt6 CI"
This reverts commit d6ebb1308c
2023-04-09 02:22:53 +02:00
Tobias Fella
57fccaa076 Rework QuickSwitcher
Now looks similar to the implementations in Kalendar and KXMLGui

![image](/uploads/56497b81348d553ff9b8ea30ab41fba7/image.png)

Replaces !505
2023-04-09 00:15:00 +00:00
James Graham
4bf65339f8 Texthandler edited
Move the handling of adding whether the message has been edited to texthandler.
2023-04-08 20:58:44 +00:00
l10n daemon script
e5f2e209a2 GIT_SILENT Sync po/docbooks with svn 2023-04-08 01:58:38 +00:00
James Graham
be3b5cdb8a Handle emotes in texthander
This moves the code to add the emote user pill into texthandler removing the now redundant checks from the message delegates.
2023-04-07 19:03:04 +00:00
l10n daemon script
abd56baa51 GIT_SILENT Sync po/docbooks with svn 2023-04-07 01:49:52 +00:00
l10n daemon script
ff27a1d0bb GIT_SILENT Sync po/docbooks with svn 2023-04-06 01:51:39 +00:00
Wang Zichong
8af2d4d273 Support both roomnick and myroomnick slash command
myroomnick is a more common-used command, so user can keep using
/myroomnick if they are from other matrix client.
2023-04-05 23:19:07 +08:00
Carl Schwan
f64c8e28da Use newly introduced setBadgeNumber api on Qt >= 6.6 2023-04-05 10:15:04 +02:00
l10n daemon script
4570d6350b GIT_SILENT Sync po/docbooks with svn 2023-04-05 01:59:34 +00:00
Alessio Mattiazzi
6dd51a35c5 Clear room member filter when hiding search bar
closes #575
2023-04-04 21:04:05 +00:00
l10n daemon script
2470990d75 GIT_SILENT Sync po/docbooks with svn 2023-04-04 02:01:31 +00:00
Tobias Fella
1a87e605d6 Fix formatting 2023-04-03 23:32:31 +02:00
Jan Bidler
e995740790 Text cleanup
This MR goes over text and cleans it up to be more unifying. Mostly just lowercasing words in non-titles, but it also changes the "show fancy effects" text to be clearer with its meaning.  
It also fixes the room access being printed in the room settings:  ![image](/uploads/864606de81f0f603ebb85dbe7c745491/image.png)  
I'd like to do a second pass later with another MR to unify triple dots (...) and ellipsis (…)
2023-04-03 20:47:14 +00:00
l10n daemon script
0fb8b740a4 GIT_SILENT Sync po/docbooks with svn 2023-04-03 01:53:49 +00:00
James Graham
5087161e4b Document NeoChatRoom
Add documentation to NeochatRoom and reorganise the functions in the header file in a more logical order.

For Q_PROPERTIES the documentation is on the property not the functions it calls as seems to be the convention elsewhere.
2023-04-02 17:56:03 +00:00
James Graham
918e9e492a Make sure the file delegate label fill the width and elide if needed. 2023-04-02 15:41:55 +00:00
Marc Deop i Argemí
f10805dddb feat: add background-color to <code> blocks 2023-04-02 10:54:41 +02:00
l10n daemon script
55e4d03dfe GIT_SILENT Sync po/docbooks with svn 2023-04-02 01:54:00 +00:00
James Graham
9f9086b4b0 Make the room page header span the width in compact mode with the sam emargin on both sides. 2023-04-01 17:03:53 +00:00
James Graham
e00ce79d26 Change the background colour in compact mode to achieve the same level of contrast as with bubbles. 2023-04-01 15:52:48 +00:00
James Graham
a13b2e6bd2 Cleanup Neochatroom
Remove unneeded functions, remove Q_INVOKABLE from setters that are part of a Q_PROPERTY, move inlined getters and setters to the .cpp
2023-04-01 12:49:27 +00:00
James Graham
cefe5acdaa Fix Long Topics in the RoomDrawer Overflowing
Use a scrollView for the topic so that a scrollBar is shown when it really long

BUG: 467512
2023-04-01 10:24:26 +00:00
James Graham
1438aea965 Revert "Add kquickcharts to the flatpak manifest"
This reverts commit 3cc8d32dd3
2023-04-01 08:23:48 +00:00
l10n daemon script
1b60e24c64 GIT_SILENT Sync po/docbooks with svn 2023-04-01 01:55:31 +00:00
l10n daemon script
4f1c8f6f35 GIT_SILENT Sync po/docbooks with svn 2023-03-31 01:57:59 +00:00
Carl Schwan
882945260a Fix dataChanged being sent with all roles
This is expensive so we should try to reduce it to the maximum

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2023-03-30 18:37:50 +00:00
Carl Schwan
3677088104 Remove one RoomListModel instance
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2023-03-30 18:21:03 +00:00
Akseli Lahtinen
8f141cd88d Set the mention and error color according to colorscheme
Use kirigami to set the proper link and error color for mentions and error underlining in chatbox

This is with my own colorscheme:

![image](/uploads/6269b85f936a98b02f42a9ce01da76b1/image.png)
2023-03-30 16:25:01 +00:00
l10n daemon script
0d1b35b610 GIT_SILENT Sync po/docbooks with svn 2023-03-30 01:53:26 +00:00
Tobias Fella
87c20bf03c Update .gitignore 2023-03-29 22:18:20 +02:00
l10n daemon script
77479ca22d GIT_SILENT Sync po/docbooks with svn 2023-03-29 01:56:21 +00:00
Tobias Fella
34ad743e98 Don't crash in UserListModel when there is no room
BUG: 466045
2023-03-28 11:47:14 +00:00
l10n daemon script
493e27622f GIT_SILENT Sync po/docbooks with svn 2023-03-28 02:21:27 +00:00
James Graham
b1b6c7ceed Improve location marker
Change location marker to GPS icon, make sure the point is the anchor and colour it with highlight colour

m.self \
![image](/uploads/d7136e7f70a106665fcaea0588a334f7/image.png)

m.pin \
![image](/uploads/baee5d19c098eaab1abd91533fe431e5/image.png)
2023-03-27 16:38:34 +00:00
l10n daemon script
d0b1610a9f GIT_SILENT Sync po/docbooks with svn 2023-03-27 02:44:58 +00:00
Tobias Fella
dcf520a7a9 Fix locations for geouris with uncertainty 2023-03-26 22:55:05 +02:00
James Graham
a0ae8b28b2 Handle stripnewlines for plain text list
Handle stripping new lines when the plain text input is a markdown list.
2023-03-26 20:27:23 +00:00
l10n daemon script
78a6179a11 GIT_SILENT Sync po/docbooks with svn 2023-03-26 01:58:24 +00:00
l10n daemon script
9c91557d8f GIT_SILENT Sync po/docbooks with svn 2023-03-25 01:55:18 +00:00
Tobias Fella
fb24ffd20d Show a map for location events 2023-03-24 13:52:17 +00:00
l10n daemon script
0c985a0af1 GIT_SILENT Sync po/docbooks with svn 2023-03-24 01:52:23 +00:00
Tobias Fella
787dc5ab66 Ensure that the room list subtitle is a single line 2023-03-23 20:10:35 +00:00
111 changed files with 22272 additions and 21948 deletions

View File

@@ -28,17 +28,6 @@
"buildsystem": "cmake-ninja",
"sources": [ { "type": "git", "url": "https://invent.kde.org/libraries/kirigami-addons.git" } ]
},
{
"name": "kquickcharts",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://invent.kde.org/frameworks/kquickcharts.git",
"branch": "kf5"
}
]
},
{
"name": "kquickimageeditor",
"buildsystem": "cmake-ninja",

1
.gitignore vendored
View File

@@ -10,3 +10,4 @@ kate.project.ctags.*
*.user
.flatpak-builder/
.idea/
cmake-build-*

View File

@@ -4,7 +4,7 @@
include:
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml

View File

@@ -68,12 +68,6 @@ set_package_properties(KF${QT_MAJOR_VERSION}Kirigami2 PROPERTIES
)
find_package(KF${QT_MAJOR_VERSION}KirigamiAddons 0.7.2 REQUIRED)
find_package(Qt${QT_MAJOR_VERSION}Keychain)
set_package_properties(Qt${QT_MAJOR_VERSION}Keychain PROPERTIES
TYPE REQUIRED
PURPOSE "Secure storage of account secrets"
)
if(ANDROID)
find_package(OpenSSL)
set_package_properties(OpenSSL PROPERTIES
@@ -93,7 +87,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
find_package(KF${QT_MAJOR_VERSION}DBusAddons ${KF_MIN_VERSION} REQUIRED)
endif()
find_package(Quotient 0.6)
find_package(Quotient 0.7)
set_package_properties(Quotient PROPERTIES
TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API"
@@ -131,9 +125,6 @@ set_package_properties(KF${QT_MAJOR_VERSION}DocTools PROPERTIES DESCRIPTION
TYPE OPTIONAL
)
if(NOT Quotient_VERSION_MINOR GREATER 6)
cmake_policy(SET CMP0063 OLD)
endif()
if(ANDROID)
find_package(Sqlite3)
@@ -150,7 +141,7 @@ install(FILES org.kde.neochat.tray.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/h
add_definitions(-DQT_NO_FOREACH)
add_subdirectory(src)
if (BUILD_TESTING AND Quotient_VERSION_MINOR GREATER 6)
if (BUILD_TESTING)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
add_subdirectory(autotests)
endif()

View File

@@ -6,7 +6,7 @@
#include "texthandler.h"
#include <connection.h>
#include <qnamespace.h>
#include <quotient_common.h>
#include <syncdata.h>
@@ -49,7 +49,8 @@ private Q_SLOTS:
void receiveStripReply();
void receivePlainTextIn();
void recieveRichInPlainOut();
void receiveRichInPlainOut_data();
void receiveRichInPlainOut();
void receivePlainStripHtml();
void receivePlainStripMarkup();
void receiveStripNewlines();
@@ -59,9 +60,11 @@ private Q_SLOTS:
void receiveRichtextIn();
void receiveRichMxcUrl();
void receiveRichPlainUrl();
void receiveRichEmote();
void receiveRichEdited_data();
void receiveRichEdited();
};
#ifdef QUOTIENT_07
void TextHandlerTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
@@ -146,7 +149,44 @@ void TextHandlerTest::initTestCase()
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1235
"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"
}
}
}
}
],
@@ -157,7 +197,6 @@ void TextHandlerTest::initTestCase()
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
room->update(std::move(roomData));
}
#endif
void TextHandlerTest::allowedAttributes()
{
@@ -309,15 +348,24 @@ void TextHandlerTest::receiveStripReply()
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString);
}
void TextHandlerTest::recieveRichInPlainOut()
void TextHandlerTest::receiveRichInPlainOut_data()
{
const QString testInputString = QStringLiteral("a &amp; b");
const QString testOutputString = QStringLiteral("a & b");
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::newRow("ampersand") << QStringLiteral("a &amp; b") << QStringLiteral("a & b");
QTest::newRow("quote") << QStringLiteral("&quot;a and b&quot;") << QStringLiteral("\"a and b\"");
}
void TextHandlerTest::receiveRichInPlainOut()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testOutputString);
}
void TextHandlerTest::receivePlainTextIn()
@@ -347,6 +395,9 @@ void TextHandlerTest::receiveStripNewlines()
const QString testInputStringRich = QStringLiteral("Test<br>many<br />new<br>lines.");
const QString testOutputString = QStringLiteral("Test many new lines.");
const QString testInputStringPlain2 = QStringLiteral("* List\n* Items");
const QString testOutputString2 = QStringLiteral("List Items");
TextHandler testTextHandler;
testTextHandler.setData(testInputStringPlain);
@@ -354,9 +405,11 @@ void TextHandlerTest::receiveStripNewlines()
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText, nullptr, nullptr, true), testOutputString);
testTextHandler.setData(testInputStringRich);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText, true), testOutputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, nullptr, nullptr, true), testOutputString);
testTextHandler.setData(testInputStringPlain2);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText, true), testOutputString2);
}
/**
@@ -418,7 +471,6 @@ void TextHandlerTest::receiveRichtextIn()
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
#ifdef QUOTIENT_07
void TextHandlerTest::receiveRichMxcUrl()
{
const QString testInputString = QStringLiteral(
@@ -434,9 +486,8 @@ void TextHandlerTest::receiveRichMxcUrl()
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().back().get()), testOutputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(0).get()), testOutputString);
}
#endif
/**
* For when your rich input string has a plain text url left in.
@@ -493,5 +544,44 @@ void TextHandlerTest::receiveRichPlainUrl()
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxId);
}
// Test that user pill is add to an emote message.
// N.B. The second message in the test timeline is marked as an emote.
void TextHandlerTest::receiveRichEmote()
{
auto event = room->messageEvents().at(1).get();
auto author = static_cast<NeoChatUser *>(room->user(event->senderId()));
const QString testInputString = QStringLiteral("This is an emote.");
const QString testOutputString = QStringLiteral("* <a href=\"https://matrix.to/#/@example:example.org\" style=\"color:") + author->color().name()
+ QStringLiteral("\">@example:example.org</a> This is an emote.");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, event), testOutputString);
}
void TextHandlerTest::receiveRichEdited_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::newRow("basic") << QStringLiteral("Edited") << QStringLiteral("Edited <span style=\"color:#000000\">(edited)</span>");
QTest::newRow("multiple paragraphs") << QStringLiteral("<p>Edited</p>\n<p>Edited</p>")
<< QStringLiteral("<p>Edited</p>\n<p>Edited <span style=\"color:#000000\">(edited)</span></p>");
QTest::newRow("blockquote") << QStringLiteral("<blockquote>Edited</blockquote>")
<< QStringLiteral("<blockquote>Edited</blockquote><p> <span style=\"color:#000000\">(edited)</span></p>");
}
void TextHandlerTest::receiveRichEdited()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(2).get()), testOutputString);
}
QTEST_MAIN(TextHandlerTest)
#include "texthandlertest.moc"

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

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: neochat\n"
"Report-Msgid-Bugs-To: https://bugs.kde.org\n"
"POT-Creation-Date: 2023-03-20 00:50+0000\n"
"POT-Creation-Date: 2023-03-31 00:47+0000\n"
"PO-Revision-Date: 2021-12-15 11:17+0100\n"
"Last-Translator: Øystein Steffensen-Alværvik <oysteins.omsetting@protonmail."
"com>\n"
@@ -631,50 +631,50 @@ msgctxt "'Custom' is a category of emoji"
msgid "Custom"
msgstr ""
#: src/models/messageeventmodel.cpp:359 src/models/searchmodel.cpp:156
#: src/models/messageeventmodel.cpp:362 src/models/searchmodel.cpp:156
#, kde-format
msgid "Today"
msgstr "I dag"
#: src/models/messageeventmodel.cpp:362 src/models/searchmodel.cpp:159
#: src/models/messageeventmodel.cpp:365 src/models/searchmodel.cpp:159
#, kde-format
msgid "Yesterday"
msgstr "I går"
#: src/models/messageeventmodel.cpp:365 src/models/searchmodel.cpp:162
#: src/models/messageeventmodel.cpp:368 src/models/searchmodel.cpp:162
#, kde-format
msgid "The day before yesterday"
msgstr "I forgårs"
#: src/models/messageeventmodel.cpp:467 src/models/messageeventmodel.cpp:476
#: src/models/messageeventmodel.cpp:470 src/models/messageeventmodel.cpp:479
#, kde-format
msgid "<i>[This message was deleted]</i>"
msgstr "<i>[Denne meldinga er sletta]</i>"
#: src/models/messageeventmodel.cpp:468
#: src/models/messageeventmodel.cpp:471
#, kde-format
msgid "<i>[This message was deleted: %1]</i>"
msgstr "<i>[Denne meldinga er sletta: %1]</i>"
# Eller «SENSURERT»?
#: src/models/messageeventmodel.cpp:563
#: src/models/messageeventmodel.cpp:568
#, kde-format
msgid "[REDACTED]"
msgstr "[TREKT TILBAKE]"
#: src/models/messageeventmodel.cpp:563
#: src/models/messageeventmodel.cpp:568
#, kde-format
msgid "[REDACTED: %1]"
msgstr "[TREKT TILBAKE: %1]"
#: src/models/messageeventmodel.cpp:848
#: src/models/messageeventmodel.cpp:882
#, kde-format
msgid "1 user: "
msgid_plural "%1 users: "
msgstr[0] ""
msgstr[1] ""
#: src/models/messageeventmodel.cpp:855
#: src/models/messageeventmodel.cpp:889
#, kde-format
msgctxt "list separator"
msgid ", "
@@ -726,7 +726,7 @@ msgctxt "Optional reason for an invitation"
msgid ": %1"
msgstr ": %1"
#: src/neochatroom.cpp:533 src/neochatroom.cpp:674
#: src/neochatroom.cpp:533 src/neochatroom.cpp:678
#, kde-format
msgid "joined the room (repeated)"
msgstr "kom inn i rommet (på nytt)"
@@ -736,7 +736,7 @@ msgstr "kom inn i rommet (på nytt)"
msgid "invited %1 to the room"
msgstr "inviterte %1 til rommet"
#: src/neochatroom.cpp:535 src/neochatroom.cpp:676
#: src/neochatroom.cpp:535 src/neochatroom.cpp:680
#, kde-format
msgid "joined the room"
msgstr "kom inn i rommet"
@@ -746,7 +746,7 @@ msgstr "kom inn i rommet"
msgid ": %1"
msgstr ": %1"
#: src/neochatroom.cpp:546 src/neochatroom.cpp:684
#: src/neochatroom.cpp:546 src/neochatroom.cpp:688
#, kde-format
msgctxt "their refers to a singular user"
msgid "cleared their display name"
@@ -758,29 +758,29 @@ msgctxt "their refers to a singular user"
msgid "changed their display name to %1"
msgstr "endra visingsnamn til %1"
#: src/neochatroom.cpp:553 src/neochatroom.cpp:691
#: src/neochatroom.cpp:553 src/neochatroom.cpp:695
#, kde-format
msgid " and "
msgstr " og "
#: src/neochatroom.cpp:556 src/neochatroom.cpp:694
#: src/neochatroom.cpp:556 src/neochatroom.cpp:698
#, kde-format
msgctxt "their refers to a singular user"
msgid "cleared their avatar"
msgstr "fjerna avataren"
#: src/neochatroom.cpp:562 src/neochatroom.cpp:700
#: src/neochatroom.cpp:562 src/neochatroom.cpp:704
#, kde-format
msgid "set an avatar"
msgstr "valde ein avatar"
#: src/neochatroom.cpp:564 src/neochatroom.cpp:702
#: src/neochatroom.cpp:564 src/neochatroom.cpp:706
#, kde-format
msgctxt "their refers to a singular user"
msgid "updated their avatar"
msgstr "bytte avataren sin"
#: src/neochatroom.cpp:568 src/neochatroom.cpp:706
#: src/neochatroom.cpp:568 src/neochatroom.cpp:710
#, kde-format
msgctxt "<user> changed nothing"
msgid "changed nothing"
@@ -791,7 +791,7 @@ msgstr ""
msgid "withdrew %1's invitation"
msgstr "trekte tilbake invitasjonen til %1"
#: src/neochatroom.cpp:574 src/neochatroom.cpp:712
#: src/neochatroom.cpp:574 src/neochatroom.cpp:716
#, kde-format
msgid "rejected the invitation"
msgstr "avviste invitasjonen"
@@ -801,7 +801,7 @@ msgstr "avviste invitasjonen"
msgid "unbanned %1"
msgstr "oppheva utestenging av %1"
#: src/neochatroom.cpp:578 src/neochatroom.cpp:716
#: src/neochatroom.cpp:578 src/neochatroom.cpp:720
#, kde-format
msgid "self-unbanned"
msgstr "utestengde seg sjølv"
@@ -811,7 +811,7 @@ msgstr "utestengde seg sjølv"
msgid "has put %1 out of the room: %2"
msgstr "fjerna %1 frå rommet: %2"
#: src/neochatroom.cpp:582 src/neochatroom.cpp:718
#: src/neochatroom.cpp:582 src/neochatroom.cpp:722
#, kde-format
msgid "left the room"
msgstr "forlét rommet"
@@ -826,12 +826,12 @@ msgstr ""
msgid "banned %1 from the room: %2"
msgstr "utestengde %1 frå rommet: %2"
#: src/neochatroom.cpp:591 src/neochatroom.cpp:723
#: src/neochatroom.cpp:591 src/neochatroom.cpp:727
#, kde-format
msgid "self-banned from the room"
msgstr "utestengde seg sjølv frå rommet"
#: src/neochatroom.cpp:595 src/neochatroom.cpp:726
#: src/neochatroom.cpp:595 src/neochatroom.cpp:730
#, kde-format
msgid "requested an invite"
msgstr ""
@@ -841,12 +841,12 @@ msgstr ""
msgid "requested an invite with reason: %1"
msgstr ""
#: src/neochatroom.cpp:599 src/neochatroom.cpp:730
#: src/neochatroom.cpp:599 src/neochatroom.cpp:734
#, kde-format
msgid "made something unknown"
msgstr "gjorde noko ukjend"
#: src/neochatroom.cpp:602 src/neochatroom.cpp:733
#: src/neochatroom.cpp:602 src/neochatroom.cpp:737
#, kde-format
msgid "cleared the room main alias"
msgstr "fjerna hovudaliaset til rommet"
@@ -856,7 +856,7 @@ msgstr "fjerna hovudaliaset til rommet"
msgid "set the room main alias to: %1"
msgstr "bytte hovudalias for rommet til: %1"
#: src/neochatroom.cpp:605 src/neochatroom.cpp:736
#: src/neochatroom.cpp:605 src/neochatroom.cpp:740
#, kde-format
msgid "cleared the room name"
msgstr "fjerna romnamnet"
@@ -866,177 +866,177 @@ msgstr "fjerna romnamnet"
msgid "set the room name to: %1"
msgstr "bytte romnamnet til: %1"
#: src/neochatroom.cpp:608 src/neochatroom.cpp:739
#: src/neochatroom.cpp:608 src/neochatroom.cpp:743
#, kde-format
msgid "cleared the topic"
msgstr "tømte emnefeltet"
#: src/neochatroom.cpp:608
#: src/neochatroom.cpp:609
#, kde-format
msgid "set the topic to: %1"
msgstr "bytte emnet til: %1"
#: src/neochatroom.cpp:611 src/neochatroom.cpp:742
#: src/neochatroom.cpp:615 src/neochatroom.cpp:746
#, kde-format
msgid "changed the room avatar"
msgstr "bytte ut romavataren"
#: src/neochatroom.cpp:614 src/neochatroom.cpp:745
#: src/neochatroom.cpp:618 src/neochatroom.cpp:749
#, kde-format
msgid "activated End-to-End Encryption"
msgstr "slå på ende-til-ende-kryptering"
#: src/neochatroom.cpp:617
#: src/neochatroom.cpp:621
#, kde-format
msgid "upgraded the room to version %1"
msgstr "oppgraderte rommet til versjon %1"
#: src/neochatroom.cpp:618
#: src/neochatroom.cpp:622
#, kde-format
msgid "created the room, version %1"
msgstr "oppretta rommet, versjon %1"
#: src/neochatroom.cpp:621 src/neochatroom.cpp:751
#: src/neochatroom.cpp:625 src/neochatroom.cpp:755
#, kde-format
msgctxt "'power level' means permission level"
msgid "changed the power levels for this room"
msgstr ""
#: src/neochatroom.cpp:625 src/neochatroom.cpp:755
#: src/neochatroom.cpp:629 src/neochatroom.cpp:759
#, kde-format
msgid "changed the server access control lists for this room"
msgstr ""
#: src/neochatroom.cpp:629
#: src/neochatroom.cpp:633
#, kde-format
msgctxt "[User] added <name> widget"
msgid "added %1 widget"
msgstr ""
#: src/neochatroom.cpp:632
#: src/neochatroom.cpp:636
#, kde-format
msgctxt "[User] removed <name> widget"
msgid "removed %1 widget"
msgstr ""
#: src/neochatroom.cpp:634
#: src/neochatroom.cpp:638
#, kde-format
msgctxt "[User] configured <name> widget"
msgid "configured %1 widget"
msgstr ""
#: src/neochatroom.cpp:636
#: src/neochatroom.cpp:640
#, kde-format
msgid "updated %1 state"
msgstr "oppdaterte %1-tilstand"
#: src/neochatroom.cpp:637
#: src/neochatroom.cpp:641
#, kde-format
msgid "updated %1 state for %2"
msgstr "oppdaterte %1-tilstand for %2"
#: src/neochatroom.cpp:644 src/neochatroom.cpp:773
#: src/neochatroom.cpp:648 src/neochatroom.cpp:777
#, kde-format
msgid "Unknown event"
msgstr "Ukjend hending"
#: src/neochatroom.cpp:657
#: src/neochatroom.cpp:661
#, kde-format
msgid "sent a message"
msgstr ""
#: src/neochatroom.cpp:661
#: src/neochatroom.cpp:665
#, kde-format
msgid "sent a sticker"
msgstr ""
#: src/neochatroom.cpp:667
#: src/neochatroom.cpp:671
#, kde-format
msgid "reinvited someone to the room"
msgstr ""
#: src/neochatroom.cpp:676
#: src/neochatroom.cpp:680
#, kde-format
msgid "invited someone to the room"
msgstr ""
#: src/neochatroom.cpp:686
#: src/neochatroom.cpp:690
#, kde-format
msgctxt "their refers to a singular user"
msgid "changed their display name"
msgstr ""
#: src/neochatroom.cpp:712
#: src/neochatroom.cpp:716
#, kde-format
msgid "withdrew a user's invitation"
msgstr ""
#: src/neochatroom.cpp:716
#: src/neochatroom.cpp:720
#, kde-format
msgid "unbanned a user"
msgstr ""
#: src/neochatroom.cpp:718
#: src/neochatroom.cpp:722
#, kde-format
msgid "put a user out of the room"
msgstr ""
#: src/neochatroom.cpp:721
#: src/neochatroom.cpp:725
#, kde-format
msgid "banned a user from the room"
msgstr ""
#: src/neochatroom.cpp:733
#: src/neochatroom.cpp:737
#, kde-format
msgid "set the room main alias"
msgstr ""
#: src/neochatroom.cpp:736
#: src/neochatroom.cpp:740
#, kde-format
msgid "set the room name"
msgstr ""
#: src/neochatroom.cpp:739
#: src/neochatroom.cpp:743
#, kde-format
msgid "set the topic"
msgstr ""
#: src/neochatroom.cpp:748
#: src/neochatroom.cpp:752
#, kde-format
msgid "upgraded the room version"
msgstr ""
#: src/neochatroom.cpp:748
#: src/neochatroom.cpp:752
#, kde-format
msgid "created the room"
msgstr ""
#: src/neochatroom.cpp:759
#: src/neochatroom.cpp:763
#, kde-format
msgid "added a widget"
msgstr ""
#: src/neochatroom.cpp:762
#: src/neochatroom.cpp:766
#, kde-format
msgid "removed a widget"
msgstr ""
#: src/neochatroom.cpp:764
#: src/neochatroom.cpp:768
#, kde-format
msgid "configured a widget"
msgstr ""
#: src/neochatroom.cpp:766
#: src/neochatroom.cpp:770
#, kde-format
msgid "updated the state"
msgstr ""
#: src/neochatroom.cpp:770
#: src/neochatroom.cpp:774
#, kde-format
msgid "started a poll"
msgstr ""
#: src/neochatroom.cpp:1689 src/neochatroom.cpp:1690
#: src/neochatroom.cpp:1693 src/neochatroom.cpp:1694
#, kde-format
msgid "Report sent successfully."
msgstr ""
@@ -1189,7 +1189,7 @@ msgstr ""
msgid "No emojis"
msgstr ""
#: src/qml/Component/ExploreComponent.qml:19 src/qml/Page/RoomListPage.qml:224
#: src/qml/Component/ExploreComponent.qml:19 src/qml/Page/RoomListPage.qml:221
#, kde-format
msgid "Explore rooms"
msgstr "Utforsk rom"
@@ -2504,38 +2504,38 @@ msgstr ""
msgid "Joined"
msgstr "Vart med"
#: src/qml/Page/RoomListPage.qml:58
#: src/qml/Page/RoomListPage.qml:55
#, kde-format
msgctxt "@action:button"
msgid "Show All Rooms"
msgstr ""
#: src/qml/Page/RoomListPage.qml:221
#: src/qml/Page/RoomListPage.qml:218
#, kde-format
msgid "No rooms found"
msgstr "Fann ingen rom"
#: src/qml/Page/RoomListPage.qml:221
#: src/qml/Page/RoomListPage.qml:218
#, kde-format
msgid "Join some rooms to get started"
msgstr "Start ved å verta med i nokre rom"
#: src/qml/Page/RoomListPage.qml:224
#: src/qml/Page/RoomListPage.qml:221
#, kde-format
msgid "Search in room directory"
msgstr "Søk i romkatalogen"
#: src/qml/Page/RoomListPage.qml:322 src/qml/Page/RoomListPage.qml:374
#: src/qml/Page/RoomListPage.qml:319 src/qml/Page/RoomListPage.qml:371
#, kde-format
msgid "No Name"
msgstr "Namnlaus"
#: src/qml/Page/RoomListPage.qml:389
#: src/qml/Page/RoomListPage.qml:386
#, kde-format
msgid "Muted room"
msgstr ""
#: src/qml/Page/RoomListPage.qml:416
#: src/qml/Page/RoomListPage.qml:413
#, kde-format
msgid "Configure room"
msgstr "Set opp rommet"

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

@@ -30,7 +30,6 @@ add_library(neochat STATIC
models/devicesmodel.cpp
filetypesingleton.cpp
login.cpp
stickerevent.cpp
models/webshortcutmodel.cpp
blurhash.cpp
blurhashimageprovider.cpp
@@ -47,8 +46,12 @@ add_library(neochat STATIC
filetransferpseudojob.cpp
models/searchmodel.cpp
texthandler.cpp
pollevent.cpp
pollhandler.cpp
)
target_compile_definitions(neochat PUBLIC "-DQT_NO_KEYWORDS")
add_executable(neochat-app
main.cpp
res.qrc
@@ -60,14 +63,6 @@ target_link_libraries(neochat-app PRIVATE
neochat
)
if(Quotient_VERSION_MINOR GREATER 6)
target_compile_definitions(neochat PUBLIC QUOTIENT_07)
target_sources(neochat PRIVATE pollevent.cpp pollhandler.cpp)
else()
target_compile_definitions(neochat PUBLIC QUOTIENT_VERSION=\"${Quotient_VERSION}\")
target_sources(neochat PRIVATE neochataccountregistry.cpp)
endif()
ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
target_sources(neochat-app PRIVATE ${NEOCHAT_ICON})
@@ -95,6 +90,7 @@ endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient cmark::cmark ${QTKEYCHAIN_LIBRARIES} QCoro::Core)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
if(NEOCHAT_FLATPAK)
@@ -178,6 +174,7 @@ if(ANDROID)
"home"
"preferences-desktop-notification"
"computer-symbolic"
"gps"
)
else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF${QT_MAJOR_VERSION}::KIOWidgets)

View File

@@ -138,12 +138,13 @@ int ChatDocumentHandler::completionStartIndex() const
return 0;
}
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
#if !defined(Q_OS_ANDROID) && QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
const long long cursor = cursorPosition();
#else
const auto cursor = cursorPosition();
#endif
const auto &text = getText();
auto start = std::min(cursor, text.size()) - 1;
while (start > -1) {
if (text.at(start) == QLatin1Char(' ')) {
@@ -324,3 +325,35 @@ void ChatDocumentHandler::pushMention(const Mention mention) const
m_room->mentions()->push_back(mention);
}
}
QColor ChatDocumentHandler::mentionColor() const
{
return m_mentionColor;
}
void ChatDocumentHandler::setMentionColor(const QColor &color)
{
if (m_mentionColor == color) {
return;
}
m_mentionColor = color;
m_highlighter->mentionFormat.setForeground(m_mentionColor);
m_highlighter->rehighlight();
Q_EMIT mentionColorChanged();
}
QColor ChatDocumentHandler::errorColor() const
{
return m_errorColor;
}
void ChatDocumentHandler::setErrorColor(const QColor &color)
{
if (m_errorColor == color) {
return;
}
m_errorColor = color;
m_highlighter->errorFormat.setForeground(m_errorColor);
m_highlighter->rehighlight();
Q_EMIT errorColorChanged();
}

View File

@@ -35,6 +35,9 @@ class ChatDocumentHandler : public QObject
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
Q_PROPERTY(QColor mentionColor READ mentionColor WRITE setMentionColor NOTIFY mentionColorChanged);
Q_PROPERTY(QColor errorColor READ errorColor WRITE setErrorColor NOTIFY errorColorChanged);
public:
explicit ChatDocumentHandler(QObject *parent = nullptr);
@@ -60,6 +63,13 @@ public:
void updateCompletions();
CompletionModel *completionModel() const;
[[nodiscard]] QColor mentionColor() const;
void setMentionColor(const QColor &color);
[[nodiscard]] QColor errorColor() const;
void setErrorColor(const QColor &color);
Q_SIGNALS:
void isEditChanged();
void documentChanged();
@@ -68,6 +78,8 @@ Q_SIGNALS:
void completionModelChanged();
void selectionStartChanged();
void selectionEndChanged();
void errorColorChanged();
void mentionColorChanged();
private:
int completionStartIndex() const;
@@ -79,6 +91,9 @@ private:
NeoChatRoom *m_room = nullptr;
bool completionVisible = false;
QColor m_mentionColor;
QColor m_errorColor;
int m_cursorPosition;
int m_selectionStart;
int m_selectionEnd;

View File

@@ -31,11 +31,7 @@
#include <signal.h>
#ifdef QUOTIENT_07
#include "accountregistry.h"
#else
#include "neochataccountregistry.h"
#endif
#include <accountregistry.h>
#include <connection.h>
#include <csapi/content-repo.h>
@@ -43,11 +39,8 @@
#include <csapi/profile.h>
#include <jobs/downloadfilejob.h>
#include <qt_connection_util.h>
#ifdef QUOTIENT_07
#include <csapi/notifications.h>
#include <eventstats.h>
#endif
#include "neochatconfig.h"
#include "neochatroom.h"
@@ -56,6 +49,8 @@
#include "roommanager.h"
#include "windowcontroller.h"
#include <accountregistry.h>
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
#include "trayicon.h"
#elif !defined(Q_OS_ANDROID)
@@ -93,10 +88,28 @@ Controller::Controller(QObject *parent)
});
#endif
QTimer::singleShot(0, this, [this] {
invokeLogin();
connectUntil(&Accounts, &AccountRegistry::rowsInserted, this, [this]() {
if (auto *connection = Accounts.get(NeoChatConfig::self()->activeConnection())) {
connectSingleShot(connection, &Connection::loadedRoomState, this, [this, connection]() {
setActiveConnection(connection);
});
return true;
}
return false;
});
connect(&Accounts, &AccountRegistry::rowsRemoved, this, [this]() {
if (!Accounts.isLoggedIn(NeoChatConfig::self()->activeConnection())) {
if (Accounts.size() > 0) {
setActiveConnection(Accounts.accounts().at(0));
} else {
setActiveConnection(nullptr);
}
}
});
QMetaObject::invokeMethod(&Accounts, &AccountRegistry::invokeLogin);
QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [] {
NeoChatConfig::self()->save();
});
@@ -125,23 +138,29 @@ Controller::Controller(QObject *parent)
}
#endif
connect(&AccountRegistry::instance(), &AccountRegistry::accountCountChanged, this, &Controller::activeConnectionIndexChanged);
connect(&Accounts, &AccountRegistry::accountCountChanged, this, &Controller::activeConnectionIndexChanged);
#ifdef QUOTIENT_07
static int oldAccountCount = 0;
connect(&AccountRegistry::instance(), &AccountRegistry::accountCountChanged, this, [=]() {
if (AccountRegistry::instance().size() > oldAccountCount) {
auto connection = AccountRegistry::instance().accounts()[AccountRegistry::instance().size() - 1];
connect(connection, &Connection::syncDone, this, [=]() {
handleNotifications(connection);
connect(&Accounts, &AccountRegistry::accountCountChanged, this, [this]() {
if (Accounts.size() > oldAccountCount) {
auto connection = Accounts.accounts()[Accounts.size() - 1];
connect(connection, &Connection::syncDone, this, [this, connection]() {
bool changes = false;
for (const auto &room : connection->allRooms()) {
if (m_notificationCounts[room] != room->unreadStats().notableCount) {
m_notificationCounts[room] = room->unreadStats().notableCount;
changes = true;
}
}
if (changes) {
handleNotifications(connection);
}
});
}
oldAccountCount = AccountRegistry::instance().size();
oldAccountCount = Accounts.size();
});
#endif
}
#ifdef QUOTIENT_07
void Controller::handleNotifications(QPointer<Quotient::Connection> connection)
{
static QStringList initial;
@@ -215,7 +234,6 @@ void Controller::handleNotifications(QPointer<Quotient::Connection> connection)
}
});
}
#endif
Controller &Controller::instance()
{
@@ -228,239 +246,10 @@ void Controller::showWindow()
WindowController::instance().showAndRaiseWindow(QString());
}
void Controller::loginWithAccessToken(const QString &serverAddr, const QString &user, const QString &token, const QString &deviceName)
{
if (user.isEmpty() || token.isEmpty()) {
return;
}
QUrl serverUrl(serverAddr);
auto conn = new Connection();
if (serverUrl.isValid()) {
conn->setHomeserver(serverUrl);
}
connect(conn, &Connection::connected, this, [this, conn, deviceName] {
AccountSettings account(conn->userId());
account.setKeepLoggedIn(true);
account.setHomeserver(conn->homeserver());
account.setDeviceId(conn->deviceId());
account.setDeviceName(deviceName);
if (!saveAccessTokenToKeyChain(account, conn->accessToken())) {
qWarning() << "Couldn't save access token";
}
account.sync();
addConnection(conn);
setActiveConnection(conn);
});
connect(conn, &Connection::networkError, this, [this](QString error, const QString &, int, int) {
Q_EMIT errorOccured(i18n("Network Error: %1", error));
});
conn->assumeIdentity(user, token, deviceName);
}
void Controller::logout(Connection *conn, bool serverSideLogout)
{
if (!conn) {
qCritical() << "Attempt to logout null connection";
return;
}
SettingsGroup("Accounts").remove(conn->userId());
QKeychain::DeletePasswordJob job(qAppName());
job.setAutoDelete(true);
job.setKey(conn->userId());
QEventLoop loop;
QKeychain::DeletePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
if (AccountRegistry::instance().count() > 1) {
// Only set the connection if the the account being logged out is currently active
if (conn == activeConnection()) {
setActiveConnection(AccountRegistry::instance().accounts()[0]);
}
} else {
setActiveConnection(nullptr);
}
if (!serverSideLogout) {
return;
}
conn->logout();
}
void Controller::addConnection(Connection *c)
{
Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection");
#ifndef QUOTIENT_07
AccountRegistry::instance().add(c);
#endif
c->setLazyLoading(true);
connect(c, &Connection::syncDone, this, [this, c] {
setBusy(false);
Q_EMIT syncDone();
c->sync(30000);
c->saveState();
});
connect(c, &Connection::loggedOut, this, [this, c] {
dropConnection(c);
});
connect(c, &Connection::requestFailed, this, [this](BaseJob *job) {
if (job->error() == BaseJob::UserConsentRequiredError) {
Q_EMIT userConsentRequired(job->errorUrl());
}
});
setBusy(true);
c->sync();
Q_EMIT connectionAdded(c);
Q_EMIT accountCountChanged();
}
void Controller::dropConnection(Connection *c)
{
Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection");
#ifndef QUOTIENT_07
AccountRegistry::instance().drop(c);
#endif
Q_EMIT connectionDropped(c);
Q_EMIT accountCountChanged();
#ifndef QUOTIENT_07
c->deleteLater();
#endif
}
void Controller::invokeLogin()
{
const auto accounts = SettingsGroup("Accounts").childGroups();
QString id = NeoChatConfig::self()->activeConnection();
for (const auto &accountId : accounts) {
AccountSettings account{accountId};
if (id.isEmpty()) {
// handle case where the account config is empty
id = accountId;
}
if (!account.homeserver().isEmpty()) {
auto accessTokenLoadingJob = loadAccessTokenFromKeyChain(account);
connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, [accountId, id, this, accessTokenLoadingJob](QKeychain::Job *) {
AccountSettings account{accountId};
QString accessToken;
if (accessTokenLoadingJob->error() == QKeychain::Error::NoError) {
accessToken = accessTokenLoadingJob->binaryData();
} else {
return;
}
auto connection = new Connection(account.homeserver());
connect(connection, &Connection::connected, this, [this, connection, id] {
connection->loadState();
addConnection(connection);
if (connection->userId() == id) {
setActiveConnection(connection);
connectSingleShot(connection, &Connection::syncDone, this, &Controller::initiated);
}
});
connect(connection, &Connection::loginError, this, [this, connection](const QString &error, const QString &) {
if (error == "Unrecognised access token") {
Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked"));
logout(connection, false);
} else if (error == "Connection closed") {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
// Failed due to network connection issue. This might happen when the homeserver is
// temporary down, or the user trying to re-launch NeoChat in a network that cannot
// connect to the homeserver. In this case, we don't want to do logout().
} else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
logout(connection, true);
}
Q_EMIT initiated();
});
connect(connection, &Connection::networkError, this, [this](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured(i18n("Network Error: %1", error));
});
connection->assumeIdentity(account.userId(), accessToken, account.deviceId());
});
}
}
if (accounts.isEmpty()) {
Q_EMIT initiated();
}
}
QKeychain::ReadPasswordJob *Controller::loadAccessTokenFromKeyChain(const AccountSettings &account)
{
qDebug() << "Reading access token from the keychain for" << account.userId();
auto job = new QKeychain::ReadPasswordJob(qAppName(), this);
job->setKey(account.userId());
// Handling of errors
connect(job, &QKeychain::Job::finished, this, [this, &account, job]() {
if (job->error() == QKeychain::Error::NoError) {
return;
}
switch (job->error()) {
case QKeychain::EntryNotFound:
Q_EMIT globalErrorOccured(i18n("Access token wasn't found"), i18n("Maybe it was deleted?"));
break;
case QKeychain::AccessDeniedByUser:
case QKeychain::AccessDenied:
Q_EMIT globalErrorOccured(i18n("Access to keychain was denied."), i18n("Please allow NeoChat to read the access token"));
break;
case QKeychain::NoBackendAvailable:
Q_EMIT globalErrorOccured(i18n("No keychain available."), i18n("Please install a keychain, e.g. KWallet or GNOME keyring on Linux"));
break;
case QKeychain::OtherError:
Q_EMIT globalErrorOccured(i18n("Unable to read access token"), job->errorString());
break;
default:
break;
}
});
job->start();
return job;
}
bool Controller::saveAccessTokenToKeyChain(const AccountSettings &account, const QByteArray &accessToken)
{
qDebug() << "Save the access token to the keychain for " << account.userId();
QKeychain::WritePasswordJob job(qAppName());
job.setAutoDelete(false);
job.setKey(account.userId());
job.setBinaryData(accessToken);
QEventLoop loop;
QKeychain::WritePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
if (job.error()) {
qWarning() << "Could not save access token to the keychain: " << qPrintable(job.errorString());
return false;
}
return true;
}
void Controller::changeAvatar(Connection *conn, const QUrl &localFile)
{
auto job = conn->uploadFile(localFile.toLocalFile());
#ifdef QUOTIENT_07
if (isJobPending(job)) {
#else
if (isJobRunning(job)) {
#endif
connect(job, &BaseJob::success, this, [conn, job] {
conn->callApi<SetAvatarUrlJob>(conn->userId(), job->contentUri());
});
@@ -487,7 +276,7 @@ bool Controller::supportSystemTray() const
void Controller::changePassword(Connection *connection, const QString &currentPassword, const QString &newPassword)
{
NeochatChangePasswordJob *job = connection->callApi<NeochatChangePasswordJob>(newPassword, false);
auto *job = connection->callApi<NeochatChangePasswordJob>(newPassword, false);
connect(job, &BaseJob::result, this, [this, job, currentPassword, newPassword, connection] {
if (job->error() == 103) {
QJsonObject replyData = job->jsonData();
@@ -498,7 +287,7 @@ void Controller::changePassword(Connection *connection, const QString &currentPa
authData["user"] = connection->user()->id();
QJsonObject identifier = {{"type", "m.id.user"}, {"user", connection->user()->id()}};
authData["identifier"] = identifier;
NeochatChangePasswordJob *innerJob = connection->callApi<NeochatChangePasswordJob>(newPassword, false, authData);
auto *innerJob = connection->callApi<NeochatChangePasswordJob>(newPassword, false, authData);
connect(innerJob, &BaseJob::success, this, [this]() {
Q_EMIT passwordStatus(PasswordStatus::Success);
});
@@ -518,11 +307,7 @@ bool Controller::setAvatar(Connection *connection, const QUrl &avatarSource)
User *localUser = connection->user();
QString decoded = avatarSource.path();
if (decoded.isEmpty()) {
#ifdef QUOTIENT_07
connection->callApi<SetAvatarUrlJob>(localUser->id(), avatarSource);
#else
connection->callApi<SetAvatarUrlJob>(localUser->id(), QString());
#endif
return true;
}
if (QImageReader(decoded).read().isNull()) {
@@ -533,11 +318,7 @@ bool Controller::setAvatar(Connection *connection, const QUrl &avatarSource)
}
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
#ifdef QUOTIENT_07
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
#else
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), QStringLiteral("/_matrix/client/r0/account/password"))
#endif
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("new_password"), newPassword);
@@ -546,9 +327,12 @@ NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, b
setRequestData(_data);
}
int Controller::accountCount() const
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
: Quotient::BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
{
return AccountRegistry::instance().count();
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(std::move(_data));
}
bool Controller::quitOnLastWindowClosed()
@@ -629,18 +413,6 @@ void Controller::saveWindowGeometry()
WindowController::instance().saveGeometry();
}
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
#ifdef QUOTIENT_07
: Quotient::BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
#else
: Quotient::BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId))
#endif
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(std::move(_data));
}
void Controller::createRoom(const QString &name, const QString &topic)
{
auto createRoomJob = m_connection->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
@@ -651,7 +423,7 @@ void Controller::createRoom(const QString &name, const QString &topic)
this,
&Controller::roomAdded,
this,
[this](NeoChatRoom *room) {
[](NeoChatRoom *room) {
RoomManager::instance().enterRoom(room);
},
Qt::QueuedConnection);
@@ -731,11 +503,7 @@ QString Controller::plainText(QQuickTextDocument *document) const
bool Controller::encryptionSupported() const
{
#ifdef QUOTIENT_07
return Quotient::encryptionSupported();
#else
return false;
#endif
}
void Controller::forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item)
@@ -776,29 +544,10 @@ void Controller::setApplicationProxy()
int Controller::activeConnectionIndex() const
{
#ifdef QUOTIENT_07
auto result = std::find_if(Accounts.accounts().begin(), Accounts.accounts().end(), [this](const auto &it) {
return it == m_connection;
});
return result - Accounts.accounts().begin();
#else
for (int i = 0; i < AccountRegistry::instance().rowCount(); i++) {
if (AccountRegistry::instance().data(AccountRegistry::instance().index(i, 0), AccountRegistry::UserIdRole).toString() == m_connection->userId()) {
return i;
}
}
return 0;
#endif
}
int Controller::quotientMinorVersion() const
{
// TODO libQuotient 0.7: Replace with version function from libQuotient
#ifdef QUOTIENT_07
return 7;
#else
return 6;
#endif
}
bool Controller::isFlatpak() const

View File

@@ -23,15 +23,9 @@ class Connection;
class Room;
}
namespace QKeychain
{
class ReadPasswordJob;
}
class Controller : public QObject
{
Q_OBJECT
Q_PROPERTY(int accountCount READ accountCount NOTIFY accountCountChanged)
Q_PROPERTY(bool quitOnLastWindowClosed READ quitOnLastWindowClosed WRITE setQuitOnLastWindowClosed NOTIFY quitOnLastWindowClosedChanged)
Q_PROPERTY(Quotient::Connection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
Q_PROPERTY(bool busy READ busy WRITE setBusy NOTIFY busyChanged)
@@ -40,7 +34,6 @@ class Controller : public QObject
Q_PROPERTY(bool isOnline READ isOnline NOTIFY isOnlineChanged)
Q_PROPERTY(bool encryptionSupported READ encryptionSupported CONSTANT)
Q_PROPERTY(int activeConnectionIndex READ activeConnectionIndex NOTIFY activeConnectionIndexChanged)
Q_PROPERTY(int quotientMinorVersion READ quotientMinorVersion CONSTANT)
Q_PROPERTY(bool isFlatpak READ isFlatpak CONSTANT)
public:
@@ -49,17 +42,10 @@ public:
void setActiveConnection(Quotient::Connection *connection);
[[nodiscard]] Quotient::Connection *activeConnection() const;
void addConnection(Quotient::Connection *c);
void dropConnection(Quotient::Connection *c);
Q_INVOKABLE void loginWithAccessToken(const QString &, const QString &, const QString &, const QString &);
Q_INVOKABLE void changePassword(Quotient::Connection *connection, const QString &currentPassword, const QString &newPassword);
Q_INVOKABLE bool setAvatar(Quotient::Connection *connection, const QUrl &avatarSource);
[[nodiscard]] int accountCount() const;
[[nodiscard]] static bool quitOnLastWindowClosed();
void setQuitOnLastWindowClosed(bool value);
@@ -68,8 +54,6 @@ public:
[[nodiscard]] bool supportSystemTray() const;
bool saveAccessTokenToKeyChain(const Quotient::AccountSettings &account, const QByteArray &accessToken);
int activeConnectionIndex() const;
enum PasswordStatus {
@@ -100,7 +84,6 @@ public:
Q_INVOKABLE void setApplicationProxy();
int quotientMinorVersion() const;
bool isFlatpak() const;
private:
@@ -110,20 +93,15 @@ private:
bool m_busy = false;
TrayIcon *m_trayIcon = nullptr;
QKeychain::ReadPasswordJob *loadAccessTokenFromKeyChain(const Quotient::AccountSettings &account);
void loadSettings();
void saveSettings() const;
bool m_isOnline = true;
QMap<Quotient::Room *, int> m_notificationCounts;
bool hasWindowSystem() const;
#ifdef QUOTIENT_07
void handleNotifications(QPointer<Quotient::Connection> connection);
#endif
private Q_SLOTS:
void invokeLogin();
void showWindow();
Q_SIGNALS:
@@ -136,7 +114,6 @@ Q_SIGNALS:
void syncDone();
void connectionAdded(Quotient::Connection *_t1);
void connectionDropped(Quotient::Connection *_t1);
void accountCountChanged();
void initiated();
void notificationClicked(const QString &_t1, const QString &_t2);
void quitOnLastWindowClosedChanged();
@@ -154,13 +131,12 @@ Q_SIGNALS:
void roomAdded(NeoChatRoom *room);
public Q_SLOTS:
void logout(Quotient::Connection *conn, bool serverSideLogout);
void changeAvatar(Quotient::Connection *conn, const QUrl &localFile);
static void markAllMessagesAsRead(Quotient::Connection *conn);
void saveWindowGeometry();
};
// TODO libQuotient 0.7: Drop
// TODO libQuotient 0.?: Drop
class NeochatChangePasswordJob : public Quotient::BaseJob
{
public:

View File

@@ -25,6 +25,7 @@ void FileTransferPseudoJob::fileTransferProgress(QString id, qint64 progress, qi
void FileTransferPseudoJob::fileTransferCompleted(QString id, QUrl localFile)
{
Q_UNUSED(localFile);
if (id != m_eventId) {
return;
}

View File

@@ -7,30 +7,17 @@
namespace Quotient
{
#ifdef QUOTIENT_07
class JoinRulesEvent : public StateEvent
#else
class JoinRulesEvent : public StateEventBase
#endif
{
public:
#ifdef QUOTIENT_07
QUO_EVENT(JoinRulesEvent, "m.room.join_rules")
#else
DEFINE_EVENT_TYPEID("m.room.join_rules", JoinRulesEvent)
#endif
explicit JoinRulesEvent(const QJsonObject &obj)
#ifdef QUOTIENT_07
: StateEvent(obj)
#else
: StateEventBase(typeId(), obj)
#endif
{
}
QString joinRule() const;
QJsonArray allow() const;
};
REGISTER_EVENT_TYPE(JoinRulesEvent)
}

View File

@@ -3,16 +3,13 @@
#include "login.h"
#ifdef QUOTIENT_07
#include <accountregistry.h>
#else
#include "neochataccountregistry.h"
#endif
#include <connection.h>
#include <qt_connection_util.h>
#include "controller.h"
#include "neochatroom.h"
#include <KLocalizedString>
@@ -43,7 +40,7 @@ void Login::init()
return;
}
m_isLoggedIn = AccountRegistry::instance().isLoggedIn(m_matrixId);
m_isLoggedIn = Accounts.isLoggedIn(m_matrixId);
Q_EMIT isLoggedInChanged();
if (m_isLoggedIn) {
return;
@@ -74,11 +71,7 @@ void Login::init()
account.setHomeserver(m_connection->homeserver());
account.setDeviceId(m_connection->deviceId());
account.setDeviceName(m_deviceName);
if (!Controller::instance().saveAccessTokenToKeyChain(account, m_connection->accessToken())) {
qWarning() << "Couldn't save access token";
}
account.sync();
Controller::instance().addConnection(m_connection);
Controller::instance().setActiveConnection(m_connection);
m_connection = nullptr;
});
@@ -97,8 +90,9 @@ void Login::init()
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
});
connectSingleShot(m_connection, &Connection::syncDone, this, [this]() {
Q_EMIT Controller::instance().initiated();
connectSingleShot(m_connection, &Connection::loadedRoomState, this, [this]() {
Controller::instance().setActiveConnection(m_connection);
// TODO close settings window
});
}

View File

@@ -28,15 +28,14 @@
#include "neochat-version.h"
#ifdef QUOTIENT_07
#include <accountregistry.h>
#else
#include "neochataccountregistry.h"
#endif
#include <networkaccessmanager.h>
#include <room.h>
#include <util.h>
#include <keyverificationsession.h>
#include <accountregistry.h>
#include <room.h>
#include <networkaccessmanager.h>
#include "actionshandler.h"
#include "blurhashimageprovider.h"
@@ -68,16 +67,11 @@
#include "neochatroom.h"
#include "neochatuser.h"
#include "notificationsmanager.h"
#ifdef QUOTIENT_07
#include "pollhandler.h"
#endif
#include "roommanager.h"
#include "spacehierarchycache.h"
#include "urlhelper.h"
#include "windowcontroller.h"
#ifdef QUOTIENT_07
#include <keyverificationsession.h>
#endif
#ifdef HAVE_COLORSCHEME
#include "colorschemer.h"
#endif
@@ -85,6 +79,7 @@
#include "models/statemodel.h"
#include "neochatuser.h"
#ifdef HAVE_RUNNER
#include "runner.h"
#include <QDBusConnection>
@@ -165,14 +160,10 @@ int main(int argc, char *argv[])
about.addComponent(QStringLiteral("libQuotient"),
i18n("A Qt5 library to write cross-platform clients for Matrix"),
#ifdef QUOTIENT_07
i18nc("<version number> (built against <possibly different version number>)",
"%1 (built against %2)",
Quotient::versionString(),
QStringLiteral(Quotient_VERSION_STRING)),
#else
QStringLiteral(QUOTIENT_VERSION),
#endif
QStringLiteral("https://github.com/quotient-im/libquotient"),
KAboutLicense::LGPL_V2_1);
@@ -209,11 +200,8 @@ int main(int argc, char *argv[])
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "LoginHelper", login);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "UrlHelper", &urlHelper);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "EmojiModel", &EmojiModel::instance());
#ifdef QUOTIENT_07
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Quotient::Accounts);
#else
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Quotient::AccountRegistry::instance());
#endif
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Accounts", &Quotient::Accounts);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Accounts", &Quotient::Accounts);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "SpaceHierarchyCache", &SpaceHierarchyCache::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CustomEmojiModel", &CustomEmojiModel::instance());
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
@@ -235,9 +223,7 @@ int main(int argc, char *argv[])
qmlRegisterType<CompletionModel>("org.kde.neochat", 1, 0, "CompletionModel");
qmlRegisterType<StateModel>("org.kde.neochat", 1, 0, "StateModel");
qmlRegisterType<SearchModel>("org.kde.neochat", 1, 0, "SearchModel");
#ifdef QUOTIENT_07
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");
#endif
qmlRegisterType<KeywordNotificationRuleModel>("org.kde.neochat", 1, 0, "KeywordNotificationRuleModel");
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM");
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM");
@@ -256,12 +242,10 @@ int main(int argc, char *argv[])
qRegisterMetaType<NeoChatUser *>("NeoChatUser*");
qRegisterMetaType<GetRoomEventsJob *>("GetRoomEventsJob*");
qRegisterMetaType<QMimeType>("QMimeType");
#ifdef QUOTIENT_07
#ifdef Quotient_E2EE_ENABLED
qRegisterMetaType<KeyVerificationSession *>("KeyVerificationSession*");
qmlRegisterUncreatableType<KeyVerificationSession>("org.kde.neochat", 1, 0, "KeyVerificationSession", {});
qRegisterMetaType<QVector<EmojiEntry>>("QVector<EmojiEntry>");
#endif
#endif
qmlRegisterSingletonType("org.kde.neochat", 1, 0, "About", [](QQmlEngine *engine, QJSEngine *) -> QJSValue {
return engine->toScriptValue(KAboutData::applicationData());

View File

@@ -19,6 +19,41 @@ QStringList rainbowColors{"#ff2b00", "#ff5500", "#ff8000", "#ffaa00", "#ffd500",
"#00ff2b", "#00ff55", "#00ff80", "#00ffaa", "#00ffd5", "#00ffff", "#00d4ff", "#00aaff", "#007fff", "#0055ff", "#002bff", "#0000ff",
"#2a00ff", "#5500ff", "#7f00ff", "#aa00ff", "#d400ff", "#ff00ff", "#ff00d4", "#ff00aa", "#ff0080", "#ff0055", "#ff002b", "#ff0000"};
auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18n("Leaving this room."));
room->connection()->leaveRoom(room);
} else {
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(NeoChatRoom::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
auto leaving = room->connection()->room(text);
if (!leaving) {
leaving = room->connection()->roomByAlias(text);
}
if (leaving) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
room->connection()->leaveRoom(leaving);
} else {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Room <roomname> not found", "Room %1 not found.", text));
}
}
return QString();
};
auto roomNickLambda = [](const QString &text, NeoChatRoom *room) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen."));
} else {
room->connection()->user()->rename(text, room);
}
return QString();
};
QVector<ActionsModel::Action> actions{
Action{
QStringLiteral("shrug"),
@@ -157,7 +192,6 @@ QVector<ActionsModel::Action> actions{
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString();
}
#ifdef QUOTIENT_07
const RoomMemberEvent *roomMemberEvent = room->currentState().get<RoomMemberEvent>(text);
if (roomMemberEvent && roomMemberEvent->membership() == Membership::Invite) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already invited to this room.", "%1 is already invited to this room.", text));
@@ -167,7 +201,6 @@ QVector<ActionsModel::Action> actions{
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is banned from this room.", "%1 is banned from this room.", text));
return QString();
}
#endif
if (room->localUser()->id() == text) {
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18n("You are already in this room."));
return QString();
@@ -209,7 +242,6 @@ QVector<ActionsModel::Action> actions{
kli18n("<room alias or id>"),
kli18n("Joins the given room"),
},
#ifdef QUOTIENT_07
Action{
QStringLiteral("knock"),
[](const QString &text, NeoChatRoom *room) {
@@ -242,7 +274,6 @@ QVector<ActionsModel::Action> actions{
kli18n("<room alias or id> [<reason>]"),
kli18n("Requests to join the given room"),
},
#endif
Action{
QStringLiteral("j"),
[](const QString &text, NeoChatRoom *room) {
@@ -268,31 +299,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("part"),
[](const QString &text, NeoChatRoom *room) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18n("Leaving this room."));
room->connection()->leaveRoom(room);
} else {
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(NeoChatRoom::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
auto leaving = room->connection()->room(text);
if (!leaving) {
leaving = room->connection()->roomByAlias(text);
}
if (leaving) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
room->connection()->leaveRoom(leaving);
} else {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Room <roomname> not found", "Room %1 not found.", text));
}
}
return QString();
},
leaveRoomLambda,
false,
std::nullopt,
kli18n("[<room alias or id>]"),
@@ -300,31 +307,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("leave"),
[](const QString &text, NeoChatRoom *room) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18n("Leaving this room."));
room->connection()->leaveRoom(room);
} else {
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT room->showMessage(NeoChatRoom::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
auto leaving = room->connection()->room(text);
if (!leaving) {
leaving = room->connection()->roomByAlias(text);
}
if (leaving) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
room->connection()->leaveRoom(leaving);
} else {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Room <roomname> not found", "Room %1 not found.", text));
}
}
return QString();
},
leaveRoomLambda,
false,
std::nullopt,
kli18n("[<room alias or id>]"),
@@ -347,14 +330,15 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("roomnick"),
[](const QString &text, NeoChatRoom *room) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen."));
} else {
room->connection()->user()->rename(text, room);
}
return QString();
},
roomNickLambda,
false,
std::nullopt,
kli18n("<display name>"),
kli18n("Changes your display name in this room"),
},
Action{
QStringLiteral("myroomnick"),
roomNickLambda,
false,
std::nullopt,
kli18n("<display name>"),
@@ -448,14 +432,12 @@ QVector<ActionsModel::Action> actions{
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString();
}
#ifdef QUOTIENT_07
auto state = room->currentState().get<RoomMemberEvent>(parts[0]);
if (state && state->membership() == Membership::Ban) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already banned from this room.", "%1 is already banned from this room.", text));
return QString();
}
#endif
auto plEvent = room->getCurrentState<RoomPowerLevelsEvent>();
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
if (plEvent->ban() > plEvent->powerLevelForUser(room->localUser()->id())) {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("You are not allowed to ban users from this room."));
return QString();
@@ -485,18 +467,16 @@ QVector<ActionsModel::Action> actions{
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString();
}
auto plEvent = room->getCurrentState<RoomPowerLevelsEvent>();
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
if (plEvent->ban() > plEvent->powerLevelForUser(room->localUser()->id())) {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("You are not allowed to unban users from this room."));
return QString();
}
#ifdef QUOTIENT_07
auto state = room->currentState().get<RoomMemberEvent>(text);
if (state && state->membership() != Membership::Ban) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is not banned from this room.", "%1 is not banned from this room.", text));
return QString();
}
#endif
room->unban(text);
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> was unbanned from this room.", "%1 was unbanned from this room.", text));
@@ -523,13 +503,11 @@ QVector<ActionsModel::Action> actions{
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("You cannot kick yourself from the room."));
return QString();
}
#ifdef QUOTIENT_07
if (!room->isMember(parts[0])) {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("<username> is not in this room", "%1 is not in this room.", parts[0]));
return QString();
}
#endif
auto plEvent = room->getCurrentState<RoomPowerLevelsEvent>();
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
auto kick = plEvent->kick();
if (plEvent->powerLevelForUser(room->localUser()->id()) < kick) {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("You are not allowed to kick users from this room."));

View File

@@ -9,9 +9,9 @@
bool CollapseStateProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
Q_UNUSED(source_parent);
return sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::EventTypeRole)
return sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If this is not a state, show it
|| sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::EventTypeRole)
|| sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If this is the first state in a block, show it. TODO hidden events?
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::ShowSectionRole).toBool(); // If it's a new day, show it
}
@@ -47,7 +47,7 @@ QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
if (!uniqueAuthors.contains(nextAuthor)) {
uniqueAuthors.append(nextAuthor);
}
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole)
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
) {
@@ -105,7 +105,7 @@ QVariantList CollapseStateProxyModel::stateEventsList(int sourceRow) const
{"text", sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()},
};
stateEvents.append(nextState);
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole)
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
) {
@@ -123,7 +123,7 @@ QVariantList CollapseStateProxyModel::authorList(int sourceRow) const
if (!uniqueAuthors.contains(nextAvatar)) {
uniqueAuthors.append(nextAvatar);
}
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole)
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
) {

View File

@@ -11,12 +11,6 @@
using namespace Quotient;
#ifdef QUOTIENT_07
#define running isJobPending
#else
#define running isJobRunning
#endif
void CustomEmojiModel::fetchEmojis()
{
if (!Controller::instance().activeConnection()) {
@@ -57,18 +51,12 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
auto job = Controller::instance().activeConnection()->uploadFile(location.toLocalFile());
if (running(job)) {
connect(job, &BaseJob::success, this, [this, name, job] {
if (isJobPending(job)) {
connect(job, &BaseJob::success, this, [name, job] {
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes");
auto json = data != nullptr ? data->contentJson() : QJsonObject();
auto emojiData = json["images"].toObject();
emojiData[QStringLiteral("%1").arg(name)] = QJsonObject({
#ifdef QUOTIENT_07
{QStringLiteral("url"), job->contentUri().toString()}
#else
{QStringLiteral("url"), job->contentUri()}
#endif
});
emojiData[QStringLiteral("%1").arg(name)] = QJsonObject({{QStringLiteral("url"), job->contentUri().toString()}});
json["images"] = emojiData;
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes", json);
});
@@ -141,8 +129,9 @@ QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
return QUrl(QStringLiteral("image://mxc/") + data.url.mid(6));
case Roles::MxcUrl:
return data.url.mid(6);
default:
return {};
}
return QVariant();
}

View File

@@ -67,7 +67,7 @@ QHash<int, QByteArray> DevicesModel::roleNames() const
void DevicesModel::logout(int index, const QString &password)
{
auto job = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId);
auto job = Controller::instance().activeConnection()->callApi<DeleteDeviceJob>(m_devices[index].deviceId);
connect(job, &BaseJob::result, this, [this, job, password, index] {
auto onSuccess = [this, index]() {

View File

@@ -11,12 +11,10 @@
#include <events/roomavatarevent.h>
#include <events/roommemberevent.h>
#include <events/simplestateevents.h>
#include <events/stickerevent.h>
#include <user.h>
#ifdef QUOTIENT_07
#include "pollevent.h"
#endif
#include "stickerevent.h"
#include <QDebug>
#include <QGuiApplication>
@@ -33,7 +31,7 @@ using namespace Quotient;
QHash<int, QByteArray> MessageEventModel::roleNames() const
{
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
roles[EventTypeRole] = "eventType";
roles[DelegateTypeRole] = "delegateType";
roles[MessageRole] = "message";
roles[EventIdRole] = "eventId";
roles[TimeRole] = "time";
@@ -45,19 +43,16 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[SpecialMarksRole] = "marks";
roles[LongOperationRole] = "progressInfo";
roles[FileMimetypeIcon] = "fileMimetypeIcon";
roles[AnnotationRole] = "annotation";
roles[EventResolvedTypeRole] = "eventResolvedType";
roles[IsReplyRole] = "isReply";
roles[ReplyRole] = "reply";
roles[ReplyIdRole] = "replyId";
roles[UserMarkerRole] = "userMarker";
roles[ShowAuthorRole] = "showAuthor";
roles[ShowSectionRole] = "showSection";
roles[ReadMarkersRole] = "readMarkers";
roles[ReadMarkersStringRole] = "readMarkersString";
roles[ShowReadMarkersRole] = "showReadMarkers";
roles[ReactionRole] = "reaction";
roles[IsEditedRole] = "isEdited";
roles[SourceRole] = "source";
roles[MimeTypeRole] = "mimeType";
roles[FormattedBodyRole] = "formattedBody";
@@ -69,12 +64,14 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[IsRedactedRole] = "isRedacted";
roles[GenericDisplayRole] = "genericDisplay";
roles[IsPendingRole] = "isPending";
roles[LatitudeRole] = "latitude";
roles[LongitudeRole] = "longitude";
roles[AssetRole] = "asset";
return roles;
}
MessageEventModel::MessageEventModel(QObject *parent)
: QAbstractListModel(parent)
, m_currentRoom(nullptr)
{
using namespace Quotient;
qmlRegisterAnonymousType<FileTransferInfo>("org.kde.neochat", 1);
@@ -87,6 +84,11 @@ MessageEventModel::MessageEventModel(QObject *parent)
MessageEventModel::~MessageEventModel() = default;
NeoChatRoom *MessageEventModel::room() const
{
return m_currentRoom;
}
void MessageEventModel::setRoom(NeoChatRoom *room)
{
if (room == m_currentRoom) {
@@ -105,11 +107,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
if (m_currentRoom->timelineSize() < 10 && !room->allHistoryLoaded()) {
room->getPreviousContent(50);
}
#ifdef QUOTIENT_07
lastReadEventId = room->lastFullyReadEventId();
#else
lastReadEventId = room->readMarkerEventId();
#endif
using namespace Quotient;
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
@@ -158,11 +156,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
endInsertRows();
if (!m_lastReadEventIndex.isValid()) {
// no read marker, so see if we need to create one.
#ifdef QUOTIENT_07
moveReadMarker(m_currentRoom->lastFullyReadEventId());
#else
moveReadMarker(m_currentRoom->readMarkerEventId());
#endif
}
if (biggest < m_currentRoom->maxTimelineIndex()) {
auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - biggest + timelineBaseIndex() - 1;
@@ -203,7 +197,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
beginRemoveRows({}, i, i);
});
connect(m_currentRoom, &Room::pendingEventDiscarded, this, &MessageEventModel::endRemoveRows);
connect(m_currentRoom, &Room::readMarkerMoved, this, [this](const QString &fromEventId, const QString &toEventId) {
connect(m_currentRoom, &Room::fullyReadMarkerMoved, this, [this](const QString &fromEventId, const QString &toEventId) {
Q_UNUSED(fromEventId);
moveReadMarker(toEventId);
});
@@ -226,9 +220,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
connect(m_currentRoom, &Room::fileTransferProgress, this, &MessageEventModel::refreshEvent);
connect(m_currentRoom, &Room::fileTransferCompleted, this, &MessageEventModel::refreshEvent);
connect(m_currentRoom, &Room::fileTransferFailed, this, &MessageEventModel::refreshEvent);
#ifndef QUOTIENT_07
connect(m_currentRoom, &Room::fileTransferCancelled, this, &MessageEventModel::refreshEvent);
#endif
connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [this] {
beginResetModel();
endResetModel();
@@ -314,7 +305,7 @@ int MessageEventModel::refreshEventRoles(const QString &id, const QVector<int> &
return -1;
}
row = int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex();
if (data(index(row, 0), EventTypeRole).toInt() == ReadMarker || data(index(row, 0), EventTypeRole).toInt() == Other) {
if (data(index(row, 0), DelegateTypeRole).toInt() == ReadMarker || data(index(row, 0), DelegateTypeRole).toInt() == Other) {
row++;
}
}
@@ -445,7 +436,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
if (m_lastReadEventIndex.row() == row) {
switch (role) {
case EventTypeRole:
case DelegateTypeRole:
return DelegateType::ReadMarker;
case TimeRole: {
const QDateTime eventDate = data(index(m_lastReadEventIndex.row() + 1, 0), TimeRole).toDateTime().toLocalTime();
@@ -494,10 +485,10 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == SourceRole) {
return evt.originalJson();
return QJsonDocument(evt.fullJson()).toJson();
}
if (role == EventTypeRole) {
if (role == DelegateTypeRole) {
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
switch (e->msgtype()) {
case MessageEventType::Emote:
@@ -510,6 +501,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return DelegateType::Audio;
case MessageEventType::Video:
return DelegateType::Video;
case MessageEventType::Location:
return DelegateType::Location;
default:
break;
}
@@ -528,20 +521,18 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
if (is<const EncryptedEvent>(evt)) {
return DelegateType::Encrypted;
}
#ifdef QUOTIENT_07
if (is<PollStartEvent>(evt)) {
if (evt.isRedacted()) {
return DelegateType::Message;
}
return DelegateType::Poll;
}
#endif
return DelegateType::Other;
}
if (role == EventResolvedTypeRole) {
return EventTypeRegistry::getMatrixType(evt.type());
return evt.type();
}
if (role == AuthorRole) {
@@ -564,6 +555,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if (e->msgtype() == Quotient::MessageEventType::Location) {
return e->contentJson();
}
// Cannot use e.contentJson() here because some
// EventContent classes inject values into the copy of the
// content JSON stored in EventContent::Base
@@ -637,7 +631,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return EventStatus::Hidden;
}
if (evt.isStateEvent() && static_cast<const StateEventBase &>(evt).repeatsState()) {
if (evt.isStateEvent() && static_cast<const StateEvent &>(evt).repeatsState()) {
return EventStatus::Hidden;
}
@@ -654,14 +648,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return EventStatus::Normal;
}
if (role == IsEditedRole) {
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
return !e->unsignedJson().isEmpty() && e->unsignedJson().contains("m.relations")
&& e->unsignedJson()["m.relations"].toObject().contains("m.replace");
}
return false;
}
if (role == EventIdRole) {
return !evt.id().isEmpty() ? evt.id() : evt.transactionId();
}
@@ -677,29 +663,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
}
if (role == AnnotationRole) {
if (isPending) {
return pendingIt->annotation();
}
}
if (role == TimeRole || role == SectionRole) {
auto ts = isPending ? pendingIt->lastUpdated() : makeMessageTimestamp(timelineIt);
return role == TimeRole ? QVariant(ts) : renderDate(ts);
}
if (role == UserMarkerRole) {
QVariantList variantList;
const auto users = m_currentRoom->usersAtEventId(evt.id());
for (User *user : users) {
if (user == m_currentRoom->localUser()) {
continue;
}
variantList.append(QVariant::fromValue(user));
}
return variantList;
}
if (role == IsReplyRole) {
return !evt.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString().isEmpty();
}
@@ -785,7 +753,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
// While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index.
// See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows
if (data(i, SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) {
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, EventTypeRole) == MessageEventModel::State
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, DelegateTypeRole) == MessageEventModel::State
|| data(i, TimeRole).toDateTime().msecsTo(data(idx, TimeRole).toDateTime()) > 600000
|| data(i, TimeRole).toDateTime().toLocalTime().date().day() != data(idx, TimeRole).toDateTime().toLocalTime().date().day();
}
@@ -810,23 +778,40 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return false;
}
if (role == LatitudeRole) {
const auto geoUri = evt.contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) {
return {};
}
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[0];
return latitude.toFloat();
}
if (role == LongitudeRole) {
const auto geoUri = evt.contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) {
return {};
}
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[1];
return latitude.toFloat();
}
if (role == AssetRole) {
const auto assetType = evt.contentJson()["org.matrix.msc3488.asset"].toObject()["type"].toString();
if (assetType.isEmpty()) {
return {};
}
return assetType;
}
if (role == ReadMarkersRole) {
#ifdef QUOTIENT_07
auto userIds = room()->userIdsAtEvent(evt.id());
userIds.remove(m_currentRoom->localUser()->id());
#else
auto userIds = room()->usersAtEventId(evt.id());
userIds.removeAll(m_currentRoom->localUser());
#endif
QVariantList users;
users.reserve(userIds.size());
for (const auto &userId : userIds) {
#ifdef QUOTIENT_07
auto user = static_cast<NeoChatUser *>(m_currentRoom->user(userId));
#else
auto user = static_cast<NeoChatUser *>(userId);
#endif
users += userAtEvent(user, m_currentRoom, evt);
}
@@ -834,24 +819,15 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == ReadMarkersStringRole) {
#ifdef QUOTIENT_07
auto userIds = room()->userIdsAtEvent(evt.id());
userIds.remove(m_currentRoom->localUser()->id());
#else
auto userIds = room()->usersAtEventId(evt.id());
userIds.removeAll(m_currentRoom->localUser());
#endif
/**
* The string ends up in the form
* "x users: user1DisplayName, user2DisplayName, etc."
*/
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
for (const auto &userId : userIds) {
#ifdef QUOTIENT_07
auto user = static_cast<NeoChatUser *>(m_currentRoom->user(userId));
#else
auto user = static_cast<NeoChatUser *>(userId);
#endif
readMarkersString += user->displayname(m_currentRoom) + i18nc("list separator", ", ");
}
readMarkersString.chop(2);
@@ -859,18 +835,13 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == ShowReadMarkersRole) {
#ifdef QUOTIENT_07
auto userIds = room()->userIdsAtEvent(evt.id());
userIds.remove(m_currentRoom->localUser()->id());
#else
auto userIds = room()->usersAtEventId(evt.id());
userIds.removeAll(m_currentRoom->localUser());
#endif
return userIds.size() > 0;
}
if (role == ReactionRole) {
const auto &annotations = m_currentRoom->relatedEvents(evt, EventRelation::Annotation());
const auto &annotations = m_currentRoom->relatedEvents(evt, EventRelation::AnnotationType);
if (annotations.isEmpty()) {
return {};
};
@@ -880,7 +851,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
continue;
}
if (auto e = eventCast<const ReactionEvent>(a)) {
reactions[e->relation().key].append(static_cast<NeoChatUser *>(m_currentRoom->user(e->senderId())));
reactions[e->eventId()].append(static_cast<NeoChatUser *>(m_currentRoom->user(e->senderId())));
}
}
@@ -907,7 +878,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == MediaUrlRole) {
#ifdef QUOTIENT_07
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if (!e->hasFileContent()) {
return QVariant();
@@ -924,7 +894,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
// if (auto e = eventCast<const StickerEvent>(&evt)) {
// return m_currentRoom->makeMediaUrl(e->id(), e->url());
// }
#endif
// Construct link in the same form as urlToDownload as that function doesn't work for stickers
if (auto e = eventCast<const StickerEvent>(&evt)) {
@@ -939,14 +908,12 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == VerifiedRole) {
#ifdef QUOTIENT_07
#ifdef Quotient_E2EE_ENABLED
if (evt.originalEvent()) {
auto encrypted = dynamic_cast<const EncryptedEvent *>(evt.originalEvent());
Q_ASSERT(encrypted);
return m_currentRoom->connection()->isVerifiedSession(encrypted->sessionId().toLatin1());
}
#endif
#endif
return false;
}
@@ -974,13 +941,13 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == IsPendingRole) {
return row < m_currentRoom->pendingEvents().size();
return row < static_cast<int>(m_currentRoom->pendingEvents().size());
}
return {};
}
int MessageEventModel::eventIDToIndex(const QString &eventID) const
int MessageEventModel::eventIdToRow(const QString &eventID) const
{
const auto it = m_currentRoom->findInTimeline(eventID);
if (it == m_currentRoom->historyEdge()) {
@@ -1022,7 +989,7 @@ QVariant MessageEventModel::getLastLocalUserMessageEventId()
targetMessage.insert("event_id", eventId);
targetMessage.insert("formattedBody", content["formatted_body"].toString());
// Need to get the message from the original eventId or body will have * on the front
QModelIndex idx = index(eventIDToIndex(eventId), 0);
QModelIndex idx = index(eventIdToRow(eventId), 0);
targetMessage.insert("message", idx.data(Qt::UserRole + 2));
return targetMessage;
@@ -1032,14 +999,14 @@ QVariant MessageEventModel::getLastLocalUserMessageEventId()
return targetMessage;
}
QVariant MessageEventModel::getLatestMessageFromIndex(const int baseline)
QVariant MessageEventModel::getLatestMessageFromRow(const int startRow)
{
QVariantMap replyResponse;
const auto &timelineBottom = m_currentRoom->messageEvents().rbegin() + baseline;
const auto &timelineBottom = m_currentRoom->messageEvents().rbegin() + startRow;
// set a cap limit of baseline + 35 messages, to prevent loading a lot of messages
// set a cap limit of startRow + 35 messages, to prevent loading a lot of messages
// in rooms where the user has not sent many messages
const auto limit = timelineBottom + std::min(baseline + 35, m_currentRoom->timelineSize());
const auto limit = timelineBottom + std::min(startRow + 35, m_currentRoom->timelineSize());
for (auto it = timelineBottom; it != limit; ++it) {
auto evt = it->event();
@@ -1061,7 +1028,7 @@ QVariant MessageEventModel::getLatestMessageFromIndex(const int baseline)
}
replyResponse.insert("event_id", eventId);
// Need to get the message from the original eventId or body will have * on the front
QModelIndex idx = index(eventIDToIndex(eventId), 0);
QModelIndex idx = index(eventIdToRow(eventId), 0);
replyResponse.insert("message", idx.data(Qt::UserRole + 2));
replyResponse.insert("sender_id", QVariant::fromValue(m_currentRoom->getUser((*it)->senderId())));
replyResponse.insert("at", -it->index());

View File

@@ -7,74 +7,99 @@
#include "neochatroom.h"
/**
* @class MessageEventModel
*
* This class defines the model for visualising the room timeline.
*
* This model covers all event types in the timeline with many of the roles being
* specific to a subset of events. This means the user needs to understand which
* roles will return useful information for a given event type.
*
* @sa NeoChatRoom
*/
class MessageEventModel : public QAbstractListModel
{
Q_OBJECT
/**
* @brief The current room that the model is getting its messages from.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
public:
/**
* @brief The type of delegate that is needed for the event.
*
* @note While similar this is not the matrix event or message type. This is
* to tell a QML ListView what delegate to show for each event. So while
* similar to the spec it is not the same.
*/
enum DelegateType {
Emote,
Notice,
Image,
Audio,
Video,
File,
Message,
Sticker,
State,
Encrypted,
ReadMarker,
Poll,
Other,
Emote, /**< A message that begins with /me. */
Notice, /**< A notice event. */
Image, /**< A message that is an image. */
Audio, /**< A message that is an audio recording. */
Video, /**< A message that is a video. */
File, /**< A message that is a file. */
Message, /**< A text message. */
Sticker, /**< A message that is a sticker. */
State, /**< A state event in the room. */
Encrypted, /**< An encrypted message that cannot be decrypted. */
ReadMarker, /**< The local user read marker. */
Poll, /**< The initial event for a poll. */
Location, /**< A location event. */
Other, /**< Anything that cannot be classified as another type. */
};
Q_ENUM(DelegateType);
/**
* @brief Defines the model roles.
*/
enum EventRoles {
EventTypeRole = Qt::UserRole + 1,
MessageRole,
EventIdRole,
TimeRole,
SectionRole,
AuthorRole,
ContentRole,
ContentTypeRole,
HighlightRole,
SpecialMarksRole,
LongOperationRole,
AnnotationRole,
UserMarkerRole,
FormattedBodyRole,
GenericDisplayRole,
DelegateTypeRole = Qt::UserRole + 1, /**< The delegate type of the message. */
MessageRole, /**< Plain text representation of the message. */
EventIdRole, /**< The matrix event ID of the event. */
TimeRole, /**< The timestamp for when the event was sent. */
SectionRole, /**< The date of the event as a string. */
AuthorRole, /**< The author of the event. */
ContentRole, /**< The full message content. */
ContentTypeRole, /**< The content mime type. */
HighlightRole, /**< Whether the event should be highlighted. */
SpecialMarksRole, /**< Whether the event is hidden or not. */
LongOperationRole, /**< Progress info when downloading files. */
FormattedBodyRole, /**< The formatted body of a rich message. */
GenericDisplayRole, /**< A generic string based upon the message type. */
MimeTypeRole,
FileMimetypeIcon,
MimeTypeRole, /**< The mime type of the message's file or media. */
FileMimetypeIcon, /**< The icon name for the mime type of a file. */
IsReplyRole,
ReplyRole,
ReplyIdRole,
IsReplyRole, /**< Is the message a reply to another event. */
ReplyRole, /**< The content data of the message that was replied to. */
ReplyIdRole, /**< The matrix ID of the message that was replied to. */
ShowAuthorRole,
ShowSectionRole,
ShowAuthorRole, /**< Whether the author's name should be shown. */
ShowSectionRole, /**< Whether the section header should be shown. */
ReadMarkersRole, /**< QVariantList of users at the event for read marker tracking. */
ReadMarkersStringRole, /**< QString with the display name and mxID of the users at the event. */
ShowReadMarkersRole, /**< bool with whether there are any other user read markers to be shown. */
ReactionRole,
ReadMarkersRole, /**< Other users at the event for read marker tracking. */
ReadMarkersStringRole, /**< String with the display name and mxID of the users at the event. */
ShowReadMarkersRole, /**< Whether there are any other user read markers to be shown. */
ReactionRole, /**< List of reactions to this event. */
SourceRole, /**< The full message source JSON. */
MediaUrlRole, /**< The source URL for any media in the message. */
IsEditedRole,
SourceRole,
MediaUrlRole,
// For debugging
EventResolvedTypeRole,
AuthorIdRole,
VerifiedRole,
// Sender's displayname, always without the matrix id
DisplayNameForInitialsRole,
// The displayname for the event's sender; for name change events, the old displayname
AuthorDisplayNameRole,
IsRedactedRole,
IsPendingRole,
EventResolvedTypeRole, /**< The event type the message. */
AuthorIdRole, /**< Matrix ID of the message author. */
VerifiedRole, /**< Whether an encrypted message is sent in a verified session. */
DisplayNameForInitialsRole, /**< Sender's displayname, always without the matrix id. */
AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */
IsRedactedRole, /**< Whether an event has been deleted. */
IsPendingRole, /**< Whether an event is waiting to be accepted by the server. */
LatitudeRole, /**< Latitude for a location event. */
LongitudeRole, /**< Longitude for a location event. */
AssetRole, /**< Type of location event, e.g. self pin of the user location. */
LastRole, // Keep this last
};
Q_ENUM(EventRoles)
@@ -82,20 +107,67 @@ public:
explicit MessageEventModel(QObject *parent = nullptr);
~MessageEventModel() override;
[[nodiscard]] NeoChatRoom *room() const
{
return m_currentRoom;
}
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE [[nodiscard]] int eventIDToIndex(const QString &eventID) const;
/**
* @brief Get the row number of the given event ID in the model.
*/
Q_INVOKABLE [[nodiscard]] int eventIdToRow(const QString &eventID) const;
/**
* @brief Get the last message sent by the local user.
*
* @note This checks a maximum of the previous 35 message for performance reasons.
*
* @return a QVariantMap for the event with the following parameters:
* - eventId - The event ID.
* - formattedBody - The message text formatted as Qt::RichText.
* - message - The message text formatted as Qt::PlainText.
*/
Q_INVOKABLE [[nodiscard]] QVariant getLastLocalUserMessageEventId();
Q_INVOKABLE [[nodiscard]] QVariant getLatestMessageFromIndex(const int baseline);
Q_INVOKABLE void loadReply(const QModelIndex &row);
/**
* @brief Get the last message sent earlier than the given row.
*
* @note This checks a maximum of the previous 35 message for performance reasons.
*
* @return a QVariantMap for the event with the following parameters:
* - eventId - The event ID.
* - message - The message text formatted as Qt::PlainText.
* - sender_id - The matrix ID of the sender.
* - at - The QModelIndex of the message.
*/
Q_INVOKABLE [[nodiscard]] QVariant getLatestMessageFromRow(const int startRow);
/**
* @brief Load the event that the item at the given index replied to.
*
* This is used to ensure that the reply data is available when the message that
* was replied to is outside the currently loaded timeline.
*/
Q_INVOKABLE void loadReply(const QModelIndex &index);
private Q_SLOTS:
int refreshEvent(const QString &eventId);

View File

@@ -41,7 +41,7 @@ bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour
return false;
}
const auto eventType = index.data(MessageEventModel::EventTypeRole).toInt();
const auto eventType = index.data(MessageEventModel::DelegateTypeRole).toInt();
if (eventType == MessageEventModel::Other) {
return false;

View File

@@ -117,11 +117,7 @@ void PublicRoomListModel::next(int count)
return;
}
#ifdef QUOTIENT_07
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_keyword, {}});
#else
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_keyword});
#endif
connect(job, &BaseJob::finished, this, [this] {
attempted = true;
@@ -177,11 +173,7 @@ QVariant PublicRoomListModel::data(const QModelIndex &index, int role) const
if (avatarUrl.isEmpty()) {
return "";
}
#ifdef QUOTIENT_07
return avatarUrl.url().remove(0, 6);
#else
return avatarUrl.remove(0, 6);
#endif
}
if (role == TopicRole) {
return room.topic;

View File

@@ -8,36 +8,25 @@
#include "neochatroom.h"
#include "roommanager.h"
#include "user.h"
#include <eventstats.h>
#include <QDebug>
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
#ifndef Q_OS_ANDROID
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusMessage>
#endif
#endif
#include <KLocalizedString>
#include <QGuiApplication>
#include <utility>
#ifndef QUOTIENT_07
#include "notificationsmanager.h"
#include <csapi/notifications.h>
#endif
using namespace Quotient;
Q_DECLARE_METATYPE(Quotient::JoinState)
#ifndef Q_OS_ANDROID
bool useUnityCounter()
{
static const auto Result = QDBusInterface("com.canonical.Unity", "/").isValid();
return Result;
}
#endif
RoomListModel::RoomListModel(QObject *parent)
: QAbstractListModel(parent)
{
@@ -46,30 +35,32 @@ RoomListModel::RoomListModel(QObject *parent)
m_categoryVisibility[collapsedSection] = false;
}
#ifndef Q_OS_ANDROID
connect(this, &RoomListModel::notificationCountChanged, this, [this]() {
if (useUnityCounter()) {
// copied from Telegram desktop
const auto launcherUrl = "application://org.kde.neochat.desktop";
// Gnome requires that count is a 64bit integer
const qint64 counterSlice = std::min(m_notificationCount, 9999);
QVariantMap dbusUnityProperties;
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
#ifndef Q_OS_ANDROID
// copied from Telegram desktop
const auto launcherUrl = "application://org.kde.neochat.desktop";
// Gnome requires that count is a 64bit integer
const qint64 counterSlice = std::min(m_notificationCount, 9999);
QVariantMap dbusUnityProperties;
if (counterSlice > 0) {
dbusUnityProperties["count"] = counterSlice;
dbusUnityProperties["count-visible"] = true;
} else {
dbusUnityProperties["count-visible"] = false;
}
auto signal = QDBusMessage::createSignal("/com/canonical/unity/launcherentry/neochat", "com.canonical.Unity.LauncherEntry", "Update");
signal.setArguments({launcherUrl, dbusUnityProperties});
QDBusConnection::sessionBus().send(signal);
if (counterSlice > 0) {
dbusUnityProperties["count"] = counterSlice;
dbusUnityProperties["count-visible"] = true;
} else {
dbusUnityProperties["count-visible"] = false;
}
auto signal = QDBusMessage::createSignal("/com/canonical/unity/launcherentry/neochat", "com.canonical.Unity.LauncherEntry", "Update");
signal.setArguments({launcherUrl, dbusUnityProperties});
QDBusConnection::sessionBus().send(signal);
#endif // Q_OS_ANDROID
#else
qGuiApp->setBadgeNumber(m_notificationCount);
#endif // QT_VERSION_CHECK(6, 6, 0)
});
#endif
}
RoomListModel::~RoomListModel() = default;
@@ -154,10 +145,10 @@ void RoomListModel::doAddRoom(Room *r)
void RoomListModel::connectRoomSignals(NeoChatRoom *room)
{
connect(room, &Room::displaynameChanged, this, [this, room] {
refresh(room);
refresh(room, {DisplayNameRole, NameRole});
});
connect(room, &Room::unreadMessagesChanged, this, [this, room] {
refresh(room);
connect(room, &Room::unreadStatsChanged, this, [this, room] {
refresh(room, {UnreadCountRole, NotificationCountRole, HighlightCountRole});
});
connect(room, &Room::notificationCountChanged, this, [this, room] {
refresh(room);
@@ -172,76 +163,14 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
refresh(room);
});
connect(room, &Room::addedMessages, this, [this, room] {
refresh(room, {LastEventRole, SubtitleTextRole});
refresh(room, {LastEventRole, SubtitleTextRole, LastActiveTimeRole});
});
connect(room, &Room::pendingEventMerged, this, [this, room] {
refresh(room, {LastEventRole, SubtitleTextRole});
});
#ifndef QUOTIENT_07
connect(room, &Room::notificationCountChanged, this, &RoomListModel::handleNotifications);
#endif
#ifndef QUOTIENT_07
connect(room, &Room::notificationCountChanged, this, &RoomListModel::refreshNotificationCount);
#else
connect(room, &Room::unreadStatsChanged, this, &RoomListModel::refreshNotificationCount);
#endif
}
#ifndef QUOTIENT_07
void RoomListModel::handleNotifications()
{
static bool initial = true;
static QStringList oldNotifications;
auto job = m_connection->callApi<GetNotificationsJob>();
connect(job, &BaseJob::success, this, [this, job]() {
const auto notifications = job->jsonData()["notifications"].toArray();
if (initial) {
initial = false;
for (const auto &n : notifications) {
oldNotifications += n.toObject()["event"].toObject()["event_id"].toString();
}
return;
}
for (const auto &n : notifications) {
const auto notification = n.toObject();
if (notification["read"].toBool()) {
oldNotifications.removeOne(notification["event"].toObject()["event_id"].toString());
continue;
}
if (oldNotifications.contains(notification["event"].toObject()["event_id"].toString())) {
continue;
}
oldNotifications += notification["event"].toObject()["event_id"].toString();
auto room = m_connection->room(notification["room_id"].toString());
auto currentRoom = RoomManager::instance().currentRoom();
bool roomIsActive = currentRoom && room->id() == currentRoom->id();
// If room exists, room is NOT active OR the application is NOT active, show notification
if (room && !(roomIsActive && QGuiApplication::applicationState() == Qt::ApplicationActive)) {
// The room might have been deleted (for example rejected invitation).
auto sender = room->user(notification["event"].toObject()["sender"].toString());
QImage avatar_image;
if (!sender->avatarUrl(room).isEmpty()) {
avatar_image = sender->avatar(128, room);
} else {
avatar_image = room->avatar(128);
}
NotificationsManager::instance().postNotification(dynamic_cast<NeoChatRoom *>(room),
sender->displayname(room),
notification["event"].toObject()["content"].toObject()["body"].toString(),
avatar_image,
notification["event"].toObject()["event_id"].toString(),
true);
}
}
});
}
#endif
void RoomListModel::refreshNotificationCount()
{
int count = 0;
@@ -366,7 +295,7 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
return NeoChatRoomType::Normal;
}
if (role == UnreadCountRole) {
return room->unreadCount();
return room->unreadStats().notableCount;
}
if (role == NotificationCountRole) {
return room->notificationCount();

View File

@@ -107,9 +107,6 @@ private:
QString m_activeSpaceId = "";
void connectRoomSignals(NeoChatRoom *room);
#ifndef QUOTIENT_07
void handleNotifications();
#endif
Q_SIGNALS:
void connectionChanged();

View File

@@ -8,9 +8,7 @@
#include <KLocalizedString>
#include <connection.h>
#ifdef QUOTIENT_07
#include <csapi/search.h>
#endif
using namespace Quotient;
@@ -34,7 +32,6 @@ void SearchModel::setSearchText(const QString &searchText)
void SearchModel::search()
{
#ifdef QUOTIENT_07
Q_ASSERT(m_connection);
setSearching(true);
if (m_job) {
@@ -43,20 +40,26 @@ void SearchModel::search()
}
SearchJob::RoomEventsCriteria criteria{
m_searchText,
{},
RoomEventFilter{
.rooms = {m_room->id()},
},
"recent",
SearchJob::IncludeEventContext{3, 3, true},
false,
none,
.searchTerm = m_searchText,
.keys = {},
.filter =
RoomEventFilter{
.unreadThreadNotifications = none,
.lazyLoadMembers = true,
.includeRedundantMembers = false,
.notRooms = {},
.rooms = {m_room->id()},
.containsUrl = false,
},
.orderBy = "recent",
.eventContext = SearchJob::IncludeEventContext{3, 3, true},
.includeState = false,
.groupings = none,
};
auto job = m_connection->callApi<SearchJob>(SearchJob::Categories{criteria});
m_job = job;
connect(job, &BaseJob::finished, this, [=] {
connect(job, &BaseJob::finished, this, [this, job] {
beginResetModel();
m_result = job->searchCategories().roomEvents;
endResetModel();
@@ -64,7 +67,6 @@ void SearchModel::search()
m_job = nullptr;
// TODO error handling
});
#endif
}
Connection *SearchModel::connection() const
@@ -80,7 +82,6 @@ void SearchModel::setConnection(Connection *connection)
QVariant SearchModel::data(const QModelIndex &index, int role) const
{
#ifdef QUOTIENT_07
auto row = index.row();
const auto &event = *m_result->results[row].result;
switch (role) {
@@ -110,17 +111,14 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
return event.originTimestamp();
}
return MessageEventModel::DelegateType::Message;
#endif
return {};
}
int SearchModel::rowCount(const QModelIndex &parent) const
{
#ifdef QUOTIENT_07
Q_UNUSED(parent);
if (m_result.has_value()) {
return m_result->results.size();
}
#endif
return 0;
}

View File

@@ -6,9 +6,7 @@
#include <QAbstractListModel>
#include <QString>
#ifdef QUOTIENT_07
#include <csapi/search.h>
#endif
namespace Quotient
{
@@ -68,10 +66,8 @@ private:
QString m_searchText;
Quotient::Connection *m_connection = nullptr;
NeoChatRoom *m_room = nullptr;
#ifdef QUOTIENT_07
Quotient::Omittable<Quotient::SearchJob::ResultRoomEvents> m_result = Quotient::none;
Quotient::SearchJob *m_job = nullptr;
#endif
bool m_searching = false;
};

View File

@@ -95,11 +95,7 @@ void ServerListModel::checkServer(const QString &url)
KConfigGroup serverGroup(&dataResource, "Servers");
if (!serverGroup.hasKey(url)) {
#ifdef QUOTIENT_07
if (Quotient::isJobPending(m_checkServerJob)) {
#else
if (Quotient::isJobRunning(m_checkServerJob)) {
#endif
m_checkServerJob->abandon();
}

View File

@@ -14,7 +14,6 @@ QHash<int, QByteArray> StateModel::roleNames() const
}
QVariant StateModel::data(const QModelIndex &index, int role) const
{
#ifdef QUOTIENT_07
auto row = index.row();
switch (role) {
case TypeRole:
@@ -24,18 +23,13 @@ QVariant StateModel::data(const QModelIndex &index, int role) const
case SourceRole:
return QJsonDocument(m_room->currentState().events()[m_room->currentState().events().keys()[row]]->fullJson()).toJson();
}
#endif
return {};
}
int StateModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
#ifdef QUOTIENT_07
return m_room->currentState().events().size();
#else
return 0;
#endif
}
NeoChatRoom *StateModel::room() const
@@ -49,7 +43,7 @@ void StateModel::setRoom(NeoChatRoom *room)
Q_EMIT roomChanged();
beginResetModel();
endResetModel();
connect(room, &NeoChatRoom::changed, this, [=] {
connect(room, &NeoChatRoom::changed, this, [this] {
beginResetModel();
endResetModel();
});

View File

@@ -131,11 +131,7 @@ QVariant UserDirectoryListModel::data(const QModelIndex &index, int role) const
if (avatarUrl.isEmpty()) {
return "";
}
#ifdef QUOTIENT_07
return avatarUrl.url().remove(0, 6);
#else
return avatarUrl.remove(0, 6);
#endif
}
if (role == UserIDRole) {
return user.userId;

View File

@@ -44,13 +44,9 @@ void UserListModel::setRoom(NeoChatRoom *room)
std::sort(m_users.begin(), m_users.end(), room->memberSorter());
}
for (User *user : std::as_const(m_users)) {
#ifdef QUOTIENT_07
connect(user, &User::defaultAvatarChanged, this, [this, user]() {
avatarChanged(user, m_currentRoom);
});
#else
connect(user, &User::avatarChanged, this, &UserListModel::avatarChanged);
#endif
}
connect(m_currentRoom->connection(), &Connection::loggedOut, this, [this]() {
setRoom(nullptr);
@@ -96,15 +92,14 @@ QVariant UserListModel::data(const QModelIndex &index, int role) const
return QVariant::fromValue(user);
}
if (role == PowerLevelRole) {
auto pl = m_currentRoom->getCurrentState<RoomPowerLevelsEvent>();
auto pl = m_currentRoom->currentState().get<RoomPowerLevelsEvent>();
if (!pl) {
return 0;
}
return pl->powerLevelForUser(user->id());
}
if (role == PowerLevelStringRole) {
#ifdef QUOTIENT_07
auto pl = m_currentRoom->currentState().get<RoomPowerLevelsEvent>();
#else
auto pl = m_currentRoom->getCurrentState<RoomPowerLevelsEvent>();
#endif
// User might not in the room yet, in this case pl can be nullptr.
// e.g. When invited but user not accepted or denied the invitation.
if (!pl) {
@@ -143,13 +138,9 @@ void UserListModel::userAdded(Quotient::User *user)
beginInsertRows(QModelIndex(), pos, pos);
m_users.insert(pos, user);
endInsertRows();
#ifdef QUOTIENT_07
connect(user, &User::defaultAvatarChanged, this, [this, user]() {
avatarChanged(user, m_currentRoom);
});
#else
connect(user, &Quotient::User::avatarChanged, this, &UserListModel::avatarChanged);
#endif
}
void UserListModel::userRemoved(Quotient::User *user)
@@ -188,13 +179,9 @@ void UserListModel::refreshAll()
std::sort(m_users.begin(), m_users.end(), m_currentRoom->memberSorter());
}
for (User *user : std::as_const(m_users)) {
#ifdef QUOTIENT_07
connect(user, &User::defaultAvatarChanged, this, [this, user]() {
avatarChanged(user, m_currentRoom);
});
#else
connect(user, &User::avatarChanged, this, &UserListModel::avatarChanged);
#endif
}
connect(m_currentRoom->connection(), &Connection::loggedOut, this, [this]() {
setRoom(nullptr);
@@ -217,6 +204,9 @@ int UserListModel::findUserPos(Quotient::User *user) const
int UserListModel::findUserPos(const QString &username) const
{
if (!m_currentRoom) {
return 0;
}
return m_currentRoom->memberSorter().lowerBoundIndex(m_users, username);
}

View File

@@ -1,105 +0,0 @@
// SPDX-FileCopyrightText: Kitsune Ral <Kitsune-Ral@users.sf.net>
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "neochataccountregistry.h"
#include <connection.h>
using namespace Quotient;
void AccountRegistry::add(Connection *c)
{
if (m_accounts.contains(c))
return;
beginInsertRows(QModelIndex(), m_accounts.size(), m_accounts.size());
m_accounts += c;
endInsertRows();
emit accountCountChanged();
}
void AccountRegistry::drop(Connection *c)
{
beginRemoveRows(QModelIndex(), m_accounts.indexOf(c), m_accounts.indexOf(c));
m_accounts.removeOne(c);
endRemoveRows();
Q_ASSERT(!m_accounts.contains(c));
emit accountCountChanged();
}
bool AccountRegistry::isLoggedIn(const QString &userId) const
{
return std::any_of(m_accounts.cbegin(), m_accounts.cend(), [&userId](Connection *a) {
return a->userId() == userId;
});
}
bool AccountRegistry::contains(Connection *c) const
{
return m_accounts.contains(c);
}
AccountRegistry::AccountRegistry() = default;
QVariant AccountRegistry::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
if (index.row() >= m_accounts.count()) {
return {};
}
const auto account = m_accounts[index.row()];
switch (role) {
case ConnectionRole:
return QVariant::fromValue(account);
case UserIdRole:
return QVariant::fromValue(account->userId());
default:
return {};
}
return {};
}
int AccountRegistry::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_accounts.count();
}
QHash<int, QByteArray> AccountRegistry::roleNames() const
{
return {{ConnectionRole, "connection"}, {UserIdRole, "userId"}};
}
bool AccountRegistry::isEmpty() const
{
return m_accounts.isEmpty();
}
int AccountRegistry::count() const
{
return m_accounts.count();
}
const QVector<Connection *> AccountRegistry::accounts() const
{
return m_accounts;
}
Connection *AccountRegistry::get(const QString &userId)
{
for (const auto &connection : m_accounts) {
if (connection->userId() == userId) {
return connection;
}
}
return nullptr;
}

View File

@@ -1,54 +0,0 @@
// SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net>
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <QAbstractListModel>
#include <QList>
#include <QObject>
namespace Quotient
{
class Connection;
class AccountRegistry : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int accountCount READ count NOTIFY accountCountChanged);
public:
enum EventRoles {
ConnectionRole = Qt::UserRole + 1,
UserIdRole = Qt::DisplayRole,
};
static AccountRegistry &instance()
{
static AccountRegistry _instance;
return _instance;
}
const QVector<Connection *> accounts() const;
void add(Connection *a);
void drop(Connection *a);
bool isLoggedIn(const QString &userId) const;
bool isEmpty() const;
int count() const;
bool contains(Connection *) const;
Connection *get(const QString &userId);
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
Q_SIGNALS:
void accountCountChanged();
private:
AccountRegistry();
QVector<Connection *> m_accounts;
};
}

View File

@@ -8,7 +8,6 @@
#include <QMimeDatabase>
#include <QTemporaryFile>
#include <QTextDocument>
#include <functional>
#include <QMediaMetaData>
#include <QMediaPlayer>
@@ -25,6 +24,7 @@
#include <csapi/room_state.h>
#include <csapi/typing.h>
#include <events/encryptionevent.h>
#include <events/eventrelation.h>
#include <events/reactionevent.h>
#include <events/redactionevent.h>
#include <events/roomavatarevent.h>
@@ -32,23 +32,19 @@
#include <events/roommemberevent.h>
#include <events/roompowerlevelsevent.h>
#include <events/simplestateevents.h>
#include <events/stickerevent.h>
#include <eventstats.h>
#include <jobs/downloadfilejob.h>
#ifndef QUOTIENT_07
#include <joinstate.h>
#endif
#include <qt_connection_util.h>
#include "controller.h"
#include "filetransferpseudojob.h"
#include "joinrulesevent.h"
#include "neochatconfig.h"
#include "neochatuser.h"
#include "notificationsmanager.h"
#ifdef QUOTIENT_07
#include "pollevent.h"
#include "pollhandler.h"
#endif
#include "filetransferpseudojob.h"
#include "stickerevent.h"
#include "texthandler.h"
#ifndef Q_OS_ANDROID
@@ -86,7 +82,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
if (this->joinState() != JoinState::Invite) {
return;
}
const QString senderId = getCurrentState<RoomMemberEvent>(localUser()->id())->senderId();
const QString senderId = currentState().get<RoomMemberEvent>(localUser()->id())->senderId();
QImage avatar_image;
if (!user(senderId)->avatarUrl(this).isEmpty()) {
avatar_image = user(senderId)->avatar(128, this);
@@ -110,6 +106,34 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
});
}
bool NeoChatRoom::hasFileUploading() const
{
return m_hasFileUploading;
}
void NeoChatRoom::setHasFileUploading(bool value)
{
if (value == m_hasFileUploading) {
return;
}
m_hasFileUploading = value;
Q_EMIT hasFileUploadingChanged();
}
int NeoChatRoom::fileUploadingProgress() const
{
return m_fileUploadingProgress;
}
void NeoChatRoom::setFileUploadingProgress(int value)
{
if (m_fileUploadingProgress == value) {
return;
}
m_fileUploadingProgress = value;
Q_EMIT fileUploadingProgressChanged();
}
void NeoChatRoom::uploadFile(const QUrl &url, const QString &body)
{
doUploadFile(url, body);
@@ -147,17 +171,9 @@ QCoro::Task<void> NeoChatRoom::doUploadFile(QUrl url, QString body)
} else {
content = new EventContent::FileContent(url, fileInfo.size(), mime, fileInfo.fileName());
}
#ifdef QUOTIENT_07
QString txnId = postFile(body.isEmpty() ? url.fileName() : body, content);
#else
QString txnId = postFile(body.isEmpty() ? url.fileName() : body, url, false);
#endif
setHasFileUploading(true);
#ifdef QUOTIENT_07
connect(this, &Room::fileTransferCompleted, [this, txnId](const QString &id, FileSourceInfo) {
#else
connect(this, &Room::fileTransferCompleted, [this, txnId](const QString &id, const QUrl & /*localFile*/, const QUrl & /*mxcUrl*/) {
#endif
if (id == txnId) {
setFileUploadingProgress(0);
setHasFileUploading(false);
@@ -227,21 +243,20 @@ const RoomEvent *NeoChatRoom::lastEvent() const
continue;
}
if (event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
if (event->isStateEvent() && !NeoChatConfig::showStateEvent()) {
continue;
}
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(event)) {
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::showLeaveJoinEvent()) {
continue;
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::showRename()) {
continue;
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
&& !NeoChatConfig::self()->showAvatarUpdate()) {
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::showAvatarUpdate()) {
continue;
}
}
if (event->isStateEvent() && static_cast<const StateEventBase &>(*event).repeatsState()) {
if (event->isStateEvent() && static_cast<const StateEvent &>(*event).repeatsState()) {
continue;
}
@@ -266,11 +281,9 @@ const RoomEvent *NeoChatRoom::lastEvent() const
if (auto lastEvent = eventCast<const RoomMessageEvent>(event)) {
return lastEvent;
}
#ifdef QUOTIENT_07
if (auto lastEvent = eventCast<const PollStartEvent>(event)) {
return lastEvent;
}
#endif
}
return nullptr;
}
@@ -291,10 +304,9 @@ bool NeoChatRoom::lastEventIsSpoiler() const
QString NeoChatRoom::lastEventToString(Qt::TextFormat format, bool stripNewlines) const
{
if (auto event = lastEvent()) {
return roomMembername(event->senderId()) + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": "))
+ eventToString(*event, format, stripNewlines);
return safeMemberName(event->senderId()) + (event->isStateEvent() ? " " : ": ") + eventToString(*event, format, stripNewlines);
}
return QLatin1String("");
return {};
}
bool NeoChatRoom::isEventHighlighted(const RoomEvent *e) const
@@ -310,7 +322,7 @@ void NeoChatRoom::checkForHighlights(const Quotient::TimelineItem &ti)
}
if (auto *e = ti.viewAs<RoomMessageEvent>()) {
const auto &text = e->plainBody();
if (text.contains(localUserId) || text.contains(roomMembername(localUserId))) {
if (text.contains(localUserId) || text.contains(safeMemberName(localUserId))) {
highlights.insert(e);
}
}
@@ -333,7 +345,7 @@ void NeoChatRoom::onAddHistoricalTimelineEvents(rev_iter_t from)
void NeoChatRoom::onRedaction(const RoomEvent &prevEvent, const RoomEvent & /*after*/)
{
if (const auto &e = eventCast<const ReactionEvent>(&prevEvent)) {
if (auto relatedEventId = e->relation().eventId; !relatedEventId.isEmpty()) {
if (auto relatedEventId = e->eventId(); !relatedEventId.isEmpty()) {
Q_EMIT updatedEvent(relatedEventId);
}
}
@@ -341,9 +353,8 @@ void NeoChatRoom::onRedaction(const RoomEvent &prevEvent, const RoomEvent & /*af
void NeoChatRoom::countChanged()
{
if (displayed() && !hasUnreadMessages()) {
resetNotificationCount();
resetHighlightCount();
if (displayed() && unreadStats().empty()) {
setReadReceipt(lastEvent()->id());
}
}
@@ -361,30 +372,6 @@ QDateTime NeoChatRoom::lastActiveTime()
return messageEvents().rbegin()->get()->originTimestamp();
}
int NeoChatRoom::savedTopVisibleIndex() const
{
return firstDisplayedMarker() == historyEdge() ? 0 : int(firstDisplayedMarker() - messageEvents().rbegin());
}
int NeoChatRoom::savedBottomVisibleIndex() const
{
return lastDisplayedMarker() == historyEdge() ? 0 : int(lastDisplayedMarker() - messageEvents().rbegin());
}
void NeoChatRoom::saveViewport(int topIndex, int bottomIndex)
{
if (topIndex == -1 || bottomIndex == -1 || (bottomIndex == savedBottomVisibleIndex() && (bottomIndex == 0 || topIndex == savedTopVisibleIndex()))) {
return;
}
if (bottomIndex == 0) {
setFirstDisplayedEventId({});
setLastDisplayedEventId({});
return;
}
setFirstDisplayedEvent(maxTimelineIndex() - topIndex);
setLastDisplayedEvent(maxTimelineIndex() - bottomIndex);
}
QVariantList NeoChatRoom::getUsers(const QString &keyword, int limit) const
{
const auto userList = users();
@@ -418,15 +405,6 @@ QVariantMap NeoChatRoom::getUser(const QString &userID) const
{QStringLiteral("color"), user.color()}};
}
QUrl NeoChatRoom::urlToMxcUrl(const QUrl &mxcUrl)
{
#ifdef QUOTIENT_07
return connection()->makeMediaUrl(mxcUrl);
#else
return DownloadFileJob::makeRequestUrl(connection()->homeserver(), mxcUrl);
#endif
}
QString NeoChatRoom::avatarMediaId() const
{
if (const auto avatar = Room::avatarMediaId(); !avatar.isEmpty()) {
@@ -449,11 +427,7 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
const bool prettyPrint = (format == Qt::RichText);
using namespace Quotient;
#ifdef QUOTIENT_07
return switchOnType(
#else
return visit(
#endif
evt,
[this, format, stripNewlines](const RoomMessageEvent &e) {
using namespace MessageEventContent;
@@ -468,7 +442,7 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
fileCaption = e.plainBody() + " | " + fileCaption;
}
textHandler.setData(fileCaption);
return !fileCaption.isEmpty() ? textHandler.handleRecievePlainText() : i18n("a file");
return !fileCaption.isEmpty() ? textHandler.handleRecievePlainText(Qt::PlainText, stripNewlines) : i18n("a file");
}
QString body;
@@ -499,14 +473,9 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
[this, prettyPrint](const RoomMemberEvent &e) {
// FIXME: Rewind to the name that was at the time of this event
auto subjectName = this->htmlSafeMemberName(e.userId());
if (e.membership() == MembershipType::Leave) {
#ifdef QUOTIENT_07
if (e.membership() == Membership::Leave) {
if (e.prevContent() && e.prevContent()->displayName) {
subjectName = sanitized(*e.prevContent()->displayName).toHtmlEscaped();
#else
if (e.prevContent() && e.prevContent()->displayName.isEmpty()) {
subjectName = sanitized(e.prevContent()->displayName).toHtmlEscaped();
#endif
}
}
@@ -517,7 +486,7 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
// The below code assumes senderName output in AuthorRole
switch (e.membership()) {
case MembershipType::Invite:
case Membership::Invite:
if (e.repeatsState()) {
auto text = i18n("reinvited %1 to the room", subjectName);
if (!e.reason().isEmpty()) {
@@ -526,13 +495,13 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
return text;
}
Q_FALLTHROUGH();
case MembershipType::Join: {
case Membership::Join: {
QString text{};
// Part 1: invites and joins
if (e.repeatsState()) {
text = i18n("joined the room (repeated)");
} else if (e.changesMembership()) {
text = e.membership() == MembershipType::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
text = e.membership() == Membership::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
}
if (!text.isEmpty()) {
if (!e.reason().isEmpty()) {
@@ -542,23 +511,19 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
}
// Part 2: profile changes of joined members
if (e.isRename()) {
if (e.displayName().isEmpty()) {
if (e.newDisplayName()) {
text = i18nc("their refers to a singular user", "cleared their display name");
} else {
text = i18nc("their refers to a singular user", "changed their display name to %1", e.displayName().toHtmlEscaped());
text = i18nc("their refers to a singular user", "changed their display name to %1", e.newDisplayName()->toHtmlEscaped());
}
}
if (e.isAvatarUpdate()) {
if (!text.isEmpty()) {
text += i18n(" and ");
}
if (e.avatarUrl().isEmpty()) {
if (e.newAvatarUrl()) {
text += i18nc("their refers to a singular user", "cleared their avatar");
#ifdef QUOTIENT_07
} else if (!e.prevContent()->avatarUrl) {
#else
} else if (e.prevContent()->avatarUrl.isEmpty()) {
#endif
text += i18n("set an avatar");
} else {
text += i18nc("their refers to a singular user", "updated their avatar");
@@ -569,18 +534,18 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
}
return text;
}
case MembershipType::Leave:
if (e.prevContent() && e.prevContent()->membership == MembershipType::Invite) {
case Membership::Leave:
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
return (e.senderId() != e.userId()) ? i18n("withdrew %1's invitation", subjectName) : i18n("rejected the invitation");
}
if (e.prevContent() && e.prevContent()->membership == MembershipType::Ban) {
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
return (e.senderId() != e.userId()) ? i18n("unbanned %1", subjectName) : i18n("self-unbanned");
}
return (e.senderId() != e.userId())
? i18n("has put %1 out of the room: %2", subjectName, e.contentJson()["reason"_ls].toString().toHtmlEscaped())
: i18n("left the room");
case MembershipType::Ban:
case Membership::Ban:
if (e.senderId() != e.userId()) {
if (e.reason().isEmpty()) {
return i18n("banned %1 from the room", subjectName);
@@ -590,7 +555,7 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
} else {
return i18n("self-banned from the room");
}
case MembershipType::Knock: {
case Membership::Knock: {
QString reason(e.contentJson()["reason"_ls].toString().toHtmlEscaped());
return reason.isEmpty() ? i18n("requested an invite") : i18n("requested an invite with reason: %1", reason);
}
@@ -604,8 +569,12 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
[](const RoomNameEvent &e) {
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name to: %1", e.name().toHtmlEscaped());
},
[prettyPrint](const RoomTopicEvent &e) {
return (e.topic().isEmpty()) ? i18n("cleared the topic") : i18n("set the topic to: %1", prettyPrint ? Quotient::prettyPrint(e.topic()) : e.topic());
[prettyPrint, stripNewlines](const RoomTopicEvent &e) {
return (e.topic().isEmpty()) ? i18n("cleared the topic")
: i18n("set the topic to: %1",
prettyPrint ? Quotient::prettyPrint(e.topic())
: stripNewlines ? e.topic().replace(u'\n', u' ')
: e.topic());
},
[](const RoomAvatarEvent &) {
return i18n("changed the room avatar");
@@ -620,7 +589,7 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
[](const RoomPowerLevelsEvent &) {
return i18nc("'power level' means permission level", "changed the power levels for this room");
},
[](const StateEventBase &e) {
[](const StateEvent &e) {
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
return i18n("changed the server access control lists for this room");
}
@@ -636,21 +605,15 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
: i18n("updated %1 state for %2", e.matrixType(), e.stateKey().toHtmlEscaped());
},
#ifdef QUOTIENT_07
[](const PollStartEvent &e) {
return e.question();
},
#endif
i18n("Unknown event"));
}
QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
{
#ifdef QUOTIENT_07
return switchOnType(
#else
return visit(
#endif
evt,
[](const RoomMessageEvent &e) {
Q_UNUSED(e)
@@ -662,25 +625,25 @@ QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
},
[](const RoomMemberEvent &e) {
switch (e.membership()) {
case MembershipType::Invite:
case Membership::Invite:
if (e.repeatsState()) {
return i18n("reinvited someone to the room");
}
Q_FALLTHROUGH();
case MembershipType::Join: {
case Membership::Join: {
QString text{};
// Part 1: invites and joins
if (e.repeatsState()) {
text = i18n("joined the room (repeated)");
} else if (e.changesMembership()) {
text = e.membership() == MembershipType::Invite ? i18n("invited someone to the room") : i18n("joined the room");
text = e.membership() == Membership::Invite ? i18n("invited someone to the room") : i18n("joined the room");
}
if (!text.isEmpty()) {
return text;
}
// Part 2: profile changes of joined members
if (e.isRename()) {
if (e.displayName().isEmpty()) {
if (e.newDisplayName()) {
text = i18nc("their refers to a singular user", "cleared their display name");
} else {
text = i18nc("their refers to a singular user", "changed their display name");
@@ -690,13 +653,9 @@ QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
if (!text.isEmpty()) {
text += i18n(" and ");
}
if (e.avatarUrl().isEmpty()) {
if (e.newAvatarUrl()) {
text += i18nc("their refers to a singular user", "cleared their avatar");
#ifdef QUOTIENT_07
} else if (!e.prevContent()->avatarUrl) {
#else
} else if (e.prevContent()->avatarUrl.isEmpty()) {
#endif
text += i18n("set an avatar");
} else {
text += i18nc("their refers to a singular user", "updated their avatar");
@@ -707,22 +666,22 @@ QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
}
return text;
}
case MembershipType::Leave:
if (e.prevContent() && e.prevContent()->membership == MembershipType::Invite) {
case Membership::Leave:
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
return (e.senderId() != e.userId()) ? i18n("withdrew a user's invitation") : i18n("rejected the invitation");
}
if (e.prevContent() && e.prevContent()->membership == MembershipType::Ban) {
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
return (e.senderId() != e.userId()) ? i18n("unbanned a user") : i18n("self-unbanned");
}
return (e.senderId() != e.userId()) ? i18n("put a user out of the room") : i18n("left the room");
case MembershipType::Ban:
case Membership::Ban:
if (e.senderId() != e.userId()) {
return i18n("banned a user from the room");
} else {
return i18n("self-banned from the room");
}
case MembershipType::Knock: {
case Membership::Knock: {
return i18n("requested an invite");
}
default:;
@@ -750,7 +709,7 @@ QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
[](const RoomPowerLevelsEvent &) {
return i18nc("'power level' means permission level", "changed the power levels for this room");
},
[](const StateEventBase &e) {
[](const StateEvent &e) {
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
return i18n("changed the server access control lists for this room");
}
@@ -765,56 +724,23 @@ QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
}
return i18n("updated the state");
},
#ifdef QUOTIENT_07
[](const PollStartEvent &e) {
Q_UNUSED(e);
return i18n("started a poll");
},
#endif
i18n("Unknown event"));
}
void NeoChatRoom::changeAvatar(const QUrl &localFile)
{
const auto job = connection()->uploadFile(localFile.toLocalFile());
#ifdef QUOTIENT_07
if (isJobPending(job)) {
#else
if (isJobRunning(job)) {
#endif
connect(job, &BaseJob::success, this, [this, job] {
#ifdef QUOTIENT_07
connection()->callApi<SetRoomStateWithKeyJob>(id(), "m.room.avatar", QString(), QJsonObject{{"url", job->contentUri().toString()}});
#else
connection()->callApi<SetRoomStateWithKeyJob>(id(), "m.room.avatar", QString(), QJsonObject{{"url", job->contentUri()}});
#endif
});
}
}
void NeoChatRoom::addLocalAlias(const QString &alias)
{
auto a = aliases();
if (a.contains(alias)) {
return;
}
a += alias;
setLocalAliases(a);
}
void NeoChatRoom::removeLocalAlias(const QString &alias)
{
auto a = aliases();
if (!a.contains(alias)) {
return;
}
a.removeAll(alias);
setLocalAliases(a);
}
QString msgTypeToString(MessageEventType msgType)
{
switch (msgType) {
@@ -919,11 +845,11 @@ void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction
QStringList redactEventIds; // What if there are multiple reaction events?
const auto &annotations = relatedEvents(evt, EventRelation::Annotation());
const auto &annotations = relatedEvents(evt, EventRelation::AnnotationType);
if (!annotations.isEmpty()) {
for (const auto &a : annotations) {
if (auto e = eventCast<const ReactionEvent>(a)) {
if (e->relation().key != reaction) {
if (e->key() != reaction) {
continue;
}
@@ -946,18 +872,15 @@ void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction
bool NeoChatRoom::containsUser(const QString &userID) const
{
auto u = Room::user(userID);
if (!u) {
return false;
}
return Room::memberJoinState(u) != JoinState::Leave;
return !isMember(userID);
}
bool NeoChatRoom::canSendEvent(const QString &eventType) const
{
auto plEvent = getCurrentState<RoomPowerLevelsEvent>();
auto plEvent = currentState().get<RoomPowerLevelsEvent>();
if (!plEvent) {
return true;
}
auto pl = plEvent->powerLevelForEvent(eventType);
auto currentPl = plEvent->powerLevelForUser(localUser()->id());
@@ -966,28 +889,19 @@ bool NeoChatRoom::canSendEvent(const QString &eventType) const
bool NeoChatRoom::canSendState(const QString &eventType) const
{
auto plEvent = getCurrentState<RoomPowerLevelsEvent>();
auto plEvent = currentState().get<RoomPowerLevelsEvent>();
if (!plEvent) {
return false;
}
auto pl = plEvent->powerLevelForState(eventType);
auto currentPl = plEvent->powerLevelForUser(localUser()->id());
#ifndef QUOTIENT_07
if (eventType == "m.room.history_visibility" || eventType == "org.matrix.room.preview_urls") {
return false;
} else {
return currentPl >= pl;
}
#else
return currentPl >= pl;
#endif
}
bool NeoChatRoom::readMarkerLoaded() const
{
#ifdef QUOTIENT_07
const auto it = findInTimeline(lastFullyReadEventId());
#else
const auto it = findInTimeline(readMarkerEventId());
#endif
return it != historyEdge();
}
@@ -998,12 +912,7 @@ bool NeoChatRoom::isInvite() const
bool NeoChatRoom::isUserBanned(const QString &user) const
{
return getCurrentState<RoomMemberEvent>(user)->membership() == MembershipType::Ban;
}
QString NeoChatRoom::htmlSafeName() const
{
return name().toHtmlEscaped();
return currentState().get<RoomMemberEvent>(user)->membership() == Membership::Ban;
}
QString NeoChatRoom::htmlSafeDisplayName() const
@@ -1018,7 +927,7 @@ void NeoChatRoom::deleteMessagesByUser(const QString &user, const QString &reaso
QString NeoChatRoom::joinRule() const
{
return getCurrentState<JoinRulesEvent>()->joinRule();
return currentState().get<JoinRulesEvent>()->joinRule();
}
void NeoChatRoom::setJoinRule(const QString &joinRule)
@@ -1027,21 +936,13 @@ void NeoChatRoom::setJoinRule(const QString &joinRule)
qWarning() << "Power level too low to set join rules";
return;
}
#ifdef QUOTIENT_07
setState("m.room.join_rules", "", QJsonObject{{"join_rule", joinRule}});
#else
setState<JoinRulesEvent>(QJsonObject{{"type", "m.room.join_rules"}, {"state_key", ""}, {"content", QJsonObject{{"join_rule", joinRule}}}});
#endif
// Not emitting joinRuleChanged() here, since that would override the change in the UI with the *current* value, which is not the *new* value.
}
QString NeoChatRoom::historyVisibility() const
{
#ifdef QUOTIENT_07
return currentState().get("m.room.history_visibility")->contentJson()["history_visibility"_ls].toString();
#else
return getCurrentState("m.room.history_visibility")->contentJson()["history_visibility"_ls].toString();
#endif
}
void NeoChatRoom::setHistoryVisibility(const QString &historyVisibilityRule)
@@ -1051,23 +952,14 @@ void NeoChatRoom::setHistoryVisibility(const QString &historyVisibilityRule)
return;
}
#ifdef QUOTIENT_07
setState("m.room.history_visibility", "", QJsonObject{{"history_visibility", historyVisibilityRule}});
#else
qWarning() << "Quotient 0.7 required to set history visibility";
return;
#endif
// Not emitting historyVisibilityChanged() here, since that would override the change in the UI with the *current* value, which is not the *new* value.
}
bool NeoChatRoom::defaultUrlPreviewState() const
{
#ifdef QUOTIENT_07
auto urlPreviewsDisabled = currentState().get("org.matrix.room.preview_urls");
#else
auto urlPreviewsDisabled = getCurrentState("org.matrix.room.preview_urls");
#endif
// Some rooms will not have this state event set so check for a nullptr return.
if (urlPreviewsDisabled != nullptr) {
@@ -1113,12 +1005,7 @@ void NeoChatRoom::setDefaultUrlPreviewState(const bool &defaultUrlPreviewState)
*
* You just have to set disable to true to disable URL previews by default.
*/
#ifdef QUOTIENT_07
setState("org.matrix.room.preview_urls", "", QJsonObject{{"disable", !defaultUrlPreviewState}});
#else
qWarning() << "Quotient 0.7 required to set room default url preview setting";
return;
#endif
}
bool NeoChatRoom::urlPreviewEnabled() const
@@ -1156,32 +1043,20 @@ void NeoChatRoom::setUserPowerLevel(const QString &userID, const int &powerLevel
qWarning() << "Power level too low to set user power levels";
return;
}
#ifdef QUOTIENT_07
if (!isMember(userID)) {
#else
if (memberJoinState(user(userID)) == JoinState::Join) {
#endif
qWarning() << "User is not a member of this room so power level cannot be set";
return;
}
int clampPowerLevel = std::clamp(powerLevel, 0, 100);
#ifdef QUOTIENT_07
auto powerLevelContent = currentState().get("m.room.power_levels")->contentJson();
#else
auto powerLevelContent = getCurrentState<RoomPowerLevelsEvent>()->contentJson();
#endif
auto powerLevelUserOverrides = powerLevelContent["users"].toObject();
if (powerLevelUserOverrides[userID] != clampPowerLevel) {
powerLevelUserOverrides[userID] = clampPowerLevel;
powerLevelContent["users"] = powerLevelUserOverrides;
#ifdef QUOTIENT_07
setState("m.room.power_levels", "", powerLevelContent);
#else
setState<RoomPowerLevelsEvent>(QJsonObject{{"type", "m.room.power_levels"}, {"state_key", ""}, {"content", powerLevelContent}});
#endif
}
}
@@ -1193,11 +1068,7 @@ int NeoChatRoom::getUserPowerLevel(const QString &userId) const
int NeoChatRoom::powerLevel(const QString &eventName, const bool &isStateEvent) const
{
#ifdef QUOTIENT_07
const auto powerLevelEvent = currentState().get<RoomPowerLevelsEvent>();
#else
const auto powerLevelEvent = getCurrentState<RoomPowerLevelsEvent>();
#endif
if (eventName == "ban") {
return powerLevelEvent->ban();
} else if (eventName == "kick") {
@@ -1221,11 +1092,7 @@ int NeoChatRoom::powerLevel(const QString &eventName, const bool &isStateEvent)
void NeoChatRoom::setPowerLevel(const QString &eventName, const int &newPowerLevel, const bool &isStateEvent)
{
#ifdef QUOTIENT_07
auto powerLevelContent = currentState().get("m.room.power_levels")->contentJson();
#else
auto powerLevelContent = getCurrentState<RoomPowerLevelsEvent>()->contentJson();
#endif
int clampPowerLevel = std::clamp(newPowerLevel, 0, 100);
int powerLevel = 0;
@@ -1254,11 +1121,7 @@ void NeoChatRoom::setPowerLevel(const QString &eventName, const int &newPowerLev
}
}
#ifdef QUOTIENT_07
setState("m.room.power_levels", "", powerLevelContent);
#else
setState<RoomPowerLevelsEvent>(QJsonObject{{"type", "m.room.power_levels"}, {"state_key", ""}, {"content", powerLevelContent}});
#endif
}
int NeoChatRoom::defaultUserPowerLevel() const
@@ -1481,11 +1344,12 @@ bool NeoChatRoom::isSpace()
return false;
}
#ifdef QUOTIENT_07
return creationEvent->roomType() == RoomType::Space;
#else
return false;
#endif
}
PushNotificationState::State NeoChatRoom::pushNotificationState() const
{
return m_currentPushNotificationState;
}
void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
@@ -1807,15 +1671,9 @@ void NeoChatRoom::setSavedText(const QString &savedText)
bool NeoChatRoom::canEncryptRoom() const
{
#ifdef QUOTIENT_07
#ifdef Quotient_E2EE_ENABLED
return !usesEncryption() && canSendState("m.room.encryption");
#endif
#endif
return false;
}
#ifdef QUOTIENT_07
PollHandler *NeoChatRoom::poll(const QString &eventId)
{
if (!m_polls.contains(eventId)) {
@@ -1826,7 +1684,6 @@ PollHandler *NeoChatRoom::poll(const QString &eventId)
}
return m_polls[eventId];
}
#endif
bool NeoChatRoom::downloadTempFile(const QString &eventId)
{

View File

@@ -3,14 +3,13 @@
#pragma once
#include <qobjectdefs.h>
#include <room.h>
#include <QCache>
#include <QObject>
#include <QTextCursor>
#include <qcoro/task.h>
#include <QCoroTask>
#include "neochatuser.h"
#include "pollhandler.h"
@@ -20,36 +19,150 @@ class PushNotificationState : public QObject
Q_OBJECT
public:
/**
* @brief Describes the push notification state for the room.
*/
enum State {
Unknown,
Default,
Mute,
MentionKeyword,
All,
Unknown, /**< The state has not yet been obtained from the server. */
Default, /**< The room follows the globally configured rules for the local user. */
Mute, /**< No notifications for messages in the room. */
MentionKeyword, /**< Notifications only for local user mentions and keywords. */
All, /**< Notifications for all messages. */
};
Q_ENUM(State);
};
/**
* @brief Defines a user mention in the current chat or edit text.
*/
struct Mention {
QTextCursor cursor;
QString text;
int start = 0;
int position = 0;
QString id;
QTextCursor cursor; /**< Contains the mention's text and position in the text. */
QString text; /**< The inserted text of the mention. */
int start = 0; /**< Start position of the mention. */
int position = 0; /**< End position of the mention. */
QString id; /**< The id the mention (used to create link when sending the message). */
};
/**
* @class NeoChatRoom
*
* This class is designed to act as a wrapper over Quotient::Room to provide API and
* functionality not available in Quotient::Room.
*
* The functions fall into two main categories:
* - Helper functions to make functionality easily accessible in QML.
* - Implement functions not yet available in Quotient::Room.
*
* @sa Quotient::Room
*/
class NeoChatRoom : public Quotient::Room
{
Q_OBJECT
/**
* @brief A list of users currently typing in the room.
*
* The list does not include the local user.
*
* This is different to getting a list of NeoChatUser objects or 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 puts the result as a list of QVariantMap objects.
*
* @return a QVariantMap for the user with the following
* parameters:
* - id - User ID.
* - avatarMediaId - Avatar id in the context of this room.
* - displayName - Display name in the context of this room.
* - display - Name in the context of this room.
*
* @sa Quotient::User, NeoChatUser
*/
Q_PROPERTY(QVariantList usersTyping READ getUsersTyping NOTIFY typingChanged)
Q_PROPERTY(bool hasFileUploading READ hasFileUploading WRITE setHasFileUploading NOTIFY hasFileUploadingChanged)
Q_PROPERTY(int fileUploadingProgress READ fileUploadingProgress NOTIFY fileUploadingProgressChanged)
Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
Q_PROPERTY(bool readMarkerLoaded READ readMarkerLoaded NOTIFY readMarkerLoadedChanged)
/**
* @brief Convenience function to get the QDateTime of the last event.
*
* @sa lastEvent()
*/
Q_PROPERTY(QDateTime lastActiveTime READ lastActiveTime NOTIFY lastActiveTimeChanged)
/**
* @brief Whether a file is being uploaded to the server.
*/
Q_PROPERTY(bool hasFileUploading READ hasFileUploading WRITE setHasFileUploading NOTIFY hasFileUploadingChanged)
/**
* @brief Progress of a file upload as a percentage 0 - 100%.
*
* The value will be 0 if no file is uploading.
*
* @sa hasFileUploading
*/
Q_PROPERTY(int fileUploadingProgress READ fileUploadingProgress NOTIFY fileUploadingProgressChanged)
/**
* @brief Whether the read marker should be shown.
*/
Q_PROPERTY(bool readMarkerLoaded READ readMarkerLoaded NOTIFY readMarkerLoadedChanged)
/**
* @brief Display name with any html special characters escaped.
*/
Q_PROPERTY(QString htmlSafeDisplayName READ htmlSafeDisplayName NOTIFY displayNameChanged)
/**
* @brief The avatar image to be used for the room.
*/
Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
/**
* @brief Get a user object for the other person in a direct chat.
*/
Q_PROPERTY(NeoChatUser *directChatRemoteUser READ directChatRemoteUser CONSTANT)
/**
* @brief If the room is a space.
*/
Q_PROPERTY(bool isSpace READ isSpace CONSTANT)
/**
* @brief Whether the local user has an invite to the room.
*
* False for any other state including if the local user is a member.
*/
Q_PROPERTY(bool isInvite READ isInvite NOTIFY isInviteChanged)
/**
* @brief The current join rule for the room as a QString.
*
* Possible values are [public, knock, invite, private, restricted].
*
* @sa https://spec.matrix.org/v1.5/client-server-api/#mroomjoin_rules
*/
Q_PROPERTY(QString joinRule READ joinRule WRITE setJoinRule NOTIFY joinRuleChanged)
/**
* @brief Get the maximum room version that the server supports.
*
* Only returns main integer room versions (i.e. no msc room versions).
*/
Q_PROPERTY(int maxRoomVersion READ maxRoomVersion NOTIFY maxRoomVersionChanged)
/**
* @brief The rule for which messages should generate notifications for the room.
*
* @sa PushNotificationState::State
*/
Q_PROPERTY(PushNotificationState::State pushNotificationState READ pushNotificationState WRITE setPushNotificationState NOTIFY pushNotificationStateChanged)
/**
* @brief The current history visibilty setting for the room.
*
* Possible values are [invited, joined, shared, world_readable].
*
* @sa https://spec.matrix.org/v1.5/client-server-api/#room-history-visibility
*/
Q_PROPERTY(QString historyVisibility READ historyVisibility WRITE setHistoryVisibility NOTIFY historyVisibilityChanged)
/**
@@ -65,98 +178,441 @@ class NeoChatRoom : public Quotient::Room
*/
Q_PROPERTY(bool urlPreviewEnabled READ urlPreviewEnabled WRITE setUrlPreviewEnabled NOTIFY urlPreviewEnabledChanged)
// Properties for the various permission levels for the room
/**
* @brief Whether the local user can encrypt the room.
*
* Requires libQuotient 0.7 compiled with the Quotient_E2EE_ENABLED parameter to
* be able to return true.
*
* A local user can encrypt a room if they have permission to send the m.room.encryption
* state event.
*
* @sa https://spec.matrix.org/v1.5/client-server-api/#mroomencryption
*/
Q_PROPERTY(bool canEncryptRoom READ canEncryptRoom NOTIFY canEncryptRoomChanged)
/**
* @brief The default power level in the room for new users.
*/
Q_PROPERTY(int defaultUserPowerLevel READ defaultUserPowerLevel WRITE setDefaultUserPowerLevel NOTIFY defaultUserPowerLevelChanged)
/**
* @brief The power level required to invite users to the room.
*/
Q_PROPERTY(int invitePowerLevel READ invitePowerLevel WRITE setInvitePowerLevel NOTIFY invitePowerLevelChanged)
/**
* @brief The power level required to kick users from the room.
*/
Q_PROPERTY(int kickPowerLevel READ kickPowerLevel WRITE setKickPowerLevel NOTIFY kickPowerLevelChanged)
/**
* @brief The power level required to ban users from the room.
*/
Q_PROPERTY(int banPowerLevel READ banPowerLevel WRITE setBanPowerLevel NOTIFY banPowerLevelChanged)
/**
* @brief The power level required to delete other user messages.
*/
Q_PROPERTY(int redactPowerLevel READ redactPowerLevel WRITE setRedactPowerLevel NOTIFY redactPowerLevelChanged)
/**
* @brief The default power level for state events that are not explicitly specified.
*/
Q_PROPERTY(int statePowerLevel READ statePowerLevel WRITE setStatePowerLevel NOTIFY statePowerLevelChanged)
/**
* @brief The default power level for event that are not explicitly specified.
*/
Q_PROPERTY(int defaultEventPowerLevel READ defaultEventPowerLevel WRITE setDefaultEventPowerLevel NOTIFY defaultEventPowerLevelChanged)
/**
* @brief The power level required to change power levels for the room.
*/
Q_PROPERTY(int powerLevelPowerLevel READ powerLevelPowerLevel WRITE setPowerLevelPowerLevel NOTIFY powerLevelPowerLevelChanged)
/**
* @brief The power level required to change the room name.
*/
Q_PROPERTY(int namePowerLevel READ namePowerLevel WRITE setNamePowerLevel NOTIFY namePowerLevelChanged)
/**
* @brief The power level required to change the room avatar.
*/
Q_PROPERTY(int avatarPowerLevel READ avatarPowerLevel WRITE setAvatarPowerLevel NOTIFY avatarPowerLevelChanged)
/**
* @brief The power level required to change the room aliases.
*/
Q_PROPERTY(int canonicalAliasPowerLevel READ canonicalAliasPowerLevel WRITE setCanonicalAliasPowerLevel NOTIFY canonicalAliasPowerLevelChanged)
/**
* @brief The power level required to change the room topic.
*/
Q_PROPERTY(int topicPowerLevel READ topicPowerLevel WRITE setTopicPowerLevel NOTIFY topicPowerLevelChanged)
/**
* @brief The power level required to encrypt the room.
*/
Q_PROPERTY(int encryptionPowerLevel READ encryptionPowerLevel WRITE setEncryptionPowerLevel NOTIFY encryptionPowerLevelChanged)
/**
* @brief The power level required to change the room history visibility.
*/
Q_PROPERTY(int historyVisibilityPowerLevel READ historyVisibilityPowerLevel WRITE setHistoryVisibilityPowerLevel NOTIFY historyVisibilityPowerLevelChanged)
/**
* @brief The power level required to pin events in the room.
*/
Q_PROPERTY(int pinnedEventsPowerLevel READ pinnedEventsPowerLevel WRITE setPinnedEventsPowerLevel NOTIFY pinnedEventsPowerLevelChanged)
/**
* @brief The power level required to upgrade the room.
*/
Q_PROPERTY(int tombstonePowerLevel READ tombstonePowerLevel WRITE setTombstonePowerLevel NOTIFY tombstonePowerLevelChanged)
/**
* @brief The power level required to set the room server access control list (ACL).
*/
Q_PROPERTY(int serverAclPowerLevel READ serverAclPowerLevel WRITE setServerAclPowerLevel NOTIFY serverAclPowerLevelChanged)
/**
* @brief The power level required to add children to a space.
*/
Q_PROPERTY(int spaceChildPowerLevel READ spaceChildPowerLevel WRITE setSpaceChildPowerLevel NOTIFY spaceChildPowerLevelChanged)
/**
* @brief The power level required to set the room parent space.
*/
Q_PROPERTY(int spaceParentPowerLevel READ spaceParentPowerLevel WRITE setSpaceParentPowerLevel NOTIFY spaceParentPowerLevelChanged)
Q_PROPERTY(QString htmlSafeDisplayName READ htmlSafeDisplayName NOTIFY displayNameChanged)
Q_PROPERTY(PushNotificationState::State pushNotificationState MEMBER m_currentPushNotificationState WRITE setPushNotificationState NOTIFY
pushNotificationStateChanged)
// Due to problems with QTextDocument, unlike the other properties here, chatBoxText is *not* used to store the text when switching rooms
/**
* @brief The current text in the chatbox for the room.
*
* Due to problems with QTextDocument, unlike the other properties here,
* chatBoxText is *not* used to store the text when switching rooms.
*/
Q_PROPERTY(QString chatBoxText READ chatBoxText WRITE setChatBoxText NOTIFY chatBoxTextChanged)
/**
* @brief The text for any message currently being edited in the room.
*/
Q_PROPERTY(QString editText READ editText WRITE setEditText NOTIFY editTextChanged)
Q_PROPERTY(QString chatBoxReplyId READ chatBoxReplyId WRITE setChatBoxReplyId NOTIFY chatBoxReplyIdChanged)
Q_PROPERTY(QString chatBoxEditId READ chatBoxEditId WRITE setChatBoxEditId NOTIFY chatBoxEditIdChanged)
Q_PROPERTY(NeoChatUser *chatBoxReplyUser READ chatBoxReplyUser NOTIFY chatBoxReplyIdChanged)
Q_PROPERTY(QString chatBoxReplyMessage READ chatBoxReplyMessage NOTIFY chatBoxReplyIdChanged)
Q_PROPERTY(NeoChatUser *chatBoxEditUser READ chatBoxEditUser NOTIFY chatBoxEditIdChanged)
Q_PROPERTY(QString chatBoxEditMessage READ chatBoxEditMessage NOTIFY chatBoxEditIdChanged)
Q_PROPERTY(QString chatBoxAttachmentPath READ chatBoxAttachmentPath WRITE setChatBoxAttachmentPath NOTIFY chatBoxAttachmentPathChanged)
Q_PROPERTY(bool canEncryptRoom READ canEncryptRoom NOTIFY canEncryptRoomChanged)
/**
* @brief Get the maximum room version that the server supports.
* @brief The event id of a message being replied to.
*
* Only returns main integer room versions (i.e. no msc room versions).
* Will be QString() if not replying to a message.
*/
Q_PROPERTY(int maxRoomVersion READ maxRoomVersion NOTIFY maxRoomVersionChanged)
Q_PROPERTY(NeoChatUser *directChatRemoteUser READ directChatRemoteUser CONSTANT)
Q_PROPERTY(QString chatBoxReplyId READ chatBoxReplyId WRITE setChatBoxReplyId NOTIFY chatBoxReplyIdChanged)
/**
* @brief The event id of a message being edited.
*
* Will be QString() if not editing to a message.
*/
Q_PROPERTY(QString chatBoxEditId READ chatBoxEditId WRITE setChatBoxEditId NOTIFY chatBoxEditIdChanged)
/**
* @brief Get the user object for the message being replied to.
*
* Returns a nullptr if not replying to a message.
*/
Q_PROPERTY(NeoChatUser *chatBoxReplyUser READ chatBoxReplyUser NOTIFY chatBoxReplyIdChanged)
/**
* @brief The content of the message being replied to.
*
* Will be QString() if not replying to a message.
*/
Q_PROPERTY(QString chatBoxReplyMessage READ chatBoxReplyMessage NOTIFY chatBoxReplyIdChanged)
/**
* @brief Get the user object for the message being edited.
*
* Returns a nullptr if not editing a message.
*/
Q_PROPERTY(NeoChatUser *chatBoxEditUser READ chatBoxEditUser NOTIFY chatBoxEditIdChanged)
/**
* @brief The content of the message being edited.
*
* Will be QString() if not editing a message.
*/
Q_PROPERTY(QString chatBoxEditMessage READ chatBoxEditMessage NOTIFY chatBoxEditIdChanged)
/**
* @brief The file path of the attachment to be sent.
*/
Q_PROPERTY(QString chatBoxAttachmentPath READ chatBoxAttachmentPath WRITE setChatBoxAttachmentPath NOTIFY chatBoxAttachmentPathChanged)
public:
/**
* @brief Define the types on inline messages that can be shown.
*/
enum MessageType {
Positive,
Info,
Error,
Positive, /**< Positive message, typically green. */
Info, /**< Info message, typically highlight color. */
Error, /**< Error message, typically red. */
};
Q_ENUM(MessageType);
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 NeoChatUser objects or 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, NeoChatUser
*/
Q_INVOKABLE [[nodiscard]] QVariantList getUsers(const QString &keyword, int limit = -1) const;
/**
* @brief Get a user in the context of this room.
*
* This is different to getting a NeoChatUser object or Quotient::User object
* as neither of those can provide details like the displayName or avatarMediaId
* without the room context as these can vary from room to room. This function
* provides the room context and outputs the result as QVariantMap.
*
* @param userID the ID of the user to output.
*
* @return a QVariantMap for the 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, NeoChatUser
*/
Q_INVOKABLE [[nodiscard]] QVariantMap getUser(const QString &userID) const;
[[nodiscard]] QVariantList getUsersTyping() const;
/// Get the interesting last event.
///
/// This function respect the showLeaveJoinEvent setting and discard
/// other not interesting events. This function can return an empty pointer
/// when the room is empty of RoomMessageEvent.
[[nodiscard]] QDateTime lastActiveTime();
/**
* @brief Get the last interesting event.
*
* This function respects the user's state event setting and discards
* other not interesting events.
*
* @warning This function can return an empty pointer if the room does not have
* any RoomMessageEvents loaded.
*/
[[nodiscard]] const Quotient::RoomEvent *lastEvent() const;
/// Convenient way to get the last event but in a string format.
///
/// \see lastEvent
/// \see lastEventIsSpoiler
/**
* @brief Output a string for the message content ready for display.
*
* The output string is dependant upon the event type and the desired output format.
*
* For most messages this is the body content of the message. For media messages
* This will be the caption and for state events it will be a string specific
* to that event with some dynamic details about the event added.
*
* E.g. For a room topic state event the text will be:
* "set the topic to: <new topic text>"
*
* @param evt the event for which a string is desired.
* @param format the output format, usually Qt::PlainText or Qt::RichText.
* @param stripNewlines whether the output should have new lines in it.
*/
[[nodiscard]] QString eventToString(const Quotient::RoomEvent &evt, Qt::TextFormat format = Qt::PlainText, bool stripNewlines = false) const;
/**
* @brief Output a generic string for the message content ready for display.
*
* The output string is dependant upon the event type.
*
* Unlike NeoChatRoom::eventToString the string is the same for all events of
* the same type
*
* E.g. For a message the text will be:
* "sent a message"
*
* @sa eventToString()
*/
[[nodiscard]] QString eventToGenericString(const Quotient::RoomEvent &evt) const;
/**
* @brief Convenient way to call eventToString on the last event.
*
* @sa lastEvent()
* @sa eventToString()
*/
[[nodiscard]] QString lastEventToString(Qt::TextFormat format = Qt::PlainText, bool stripNewlines = false) const;
/// Convenient way to check if the last event looks like it has spoilers.
///
/// \see lastEvent
/// \see lastEventToString
/**
* @brief Convenient way to check if the last event looks like it has spoilers.
*
* This does a basic check to see if the message contains a data-mx-spoiler
* attribute within the text which makes it likely that the message has a spoiler
* section. However this is not 100% reliable as during parsing it may be
* removed if used within an illegal tag or on a tag for which data-mx-spoiler
* is not a valid attribute.
*
* @sa lastEvent()
*/
[[nodiscard]] bool lastEventIsSpoiler() const;
/// Convenient way to get the QDateTime of the last event.
///
/// \see lastEvent
[[nodiscard]] QDateTime lastActiveTime();
[[nodiscard]] bool hasFileUploading() const;
void setHasFileUploading(bool value);
[[nodiscard]] int fileUploadingProgress() const;
void setFileUploadingProgress(int value);
/**
* @brief Download a file for the given event to a local file location.
*/
Q_INVOKABLE void download(const QString &eventId, const QUrl &localFilename = {});
/**
* @brief Download a file for the given event as a temporary file.
*/
Q_INVOKABLE bool downloadTempFile(const QString &eventId);
/**
* @brief Check if the given event is highlighted.
*
* An event is highlighted if it contains the local user's id but was not sent by the
* local user.
*/
bool isEventHighlighted(const Quotient::RoomEvent *e) const;
/**
* @brief Convenience function to find out if the room contains the given user.
*
* A room contains the user if the user can be found and their JoinState is
* not JoinState::Leave.
*/
Q_INVOKABLE [[nodiscard]] bool containsUser(const QString &userID) const;
/**
* @brief True if the given user ID is banned from the room.
*/
Q_INVOKABLE [[nodiscard]] bool isUserBanned(const QString &user) const;
/**
* @brief True if the local user can send the given event type.
*/
Q_INVOKABLE [[nodiscard]] bool canSendEvent(const QString &eventType) const;
/**
* @brief True if the local user can send the given state event type.
*/
Q_INVOKABLE [[nodiscard]] bool canSendState(const QString &eventType) const;
/**
* @brief Send a report to the server for an event.
*
* @param eventId the ID of the event being reported.
* @param reason the reason given for reporting the event.
*/
Q_INVOKABLE void reportEvent(const QString &eventId, const QString &reason);
[[nodiscard]] bool readMarkerLoaded() const;
QString htmlSafeDisplayName() const;
#ifndef QUOTIENT_07
/**
* @brief Get a display name for the user with html escaped.
*/
Q_INVOKABLE QString htmlSafeMemberName(const QString &userId) const
{
return safeMemberName(userId).toHtmlEscaped();
}
#endif
/**
* @brief Get subtitle text for room
*
* Fetches last event and removes markdown formatting
*
* @see lastEventToString()
*/
[[nodiscard]] QString subtitleText();
[[nodiscard]] QString avatarMediaId() const;
NeoChatUser *directChatRemoteUser() const;
[[nodiscard]] bool isSpace();
bool isEventHighlighted(const Quotient::RoomEvent *e) const;
bool isInvite() const;
Q_INVOKABLE void clearInvitationNotification();
[[nodiscard]] QString joinRule() const;
void setJoinRule(const QString &joinRule);
int maxRoomVersion() const;
/**
* @brief Map an alias to the room and publish.
*
* The alias is first mapped to the room and then published as an
* alternate alias. Publishing the alias will fail if the user does not have
* permission to send m.room.canonical_alias event messages.
*
* @note This is different to Quotient::Room::setLocalAliases() as that can only
* get the room to publish an alias that is already mapped.
*
* @property alias QString in the form #new_alias:server.org
*
* @sa Quotient::Room::setLocalAliases()
*/
Q_INVOKABLE void mapAlias(const QString &alias);
/**
* @brief Unmap an alias from the room.
*
* An unmapped alias is also removed as either the canonical alias or an alternate
* alias.
*
* @note This is different to Quotient::Room::setLocalAliases() as that can only
* get the room to un-publish an alias, while the mapping still exists.
*
* @property alias QString in the form #mapped_alias:server.org
*
* @sa Quotient::Room::setLocalAliases()
*/
Q_INVOKABLE void unmapAlias(const QString &alias);
/**
* @brief Set the canonical alias of the room to an available mapped alias.
*
* If the new alias was already published as an alternate alias it will be removed
* from that list.
*
* @note This is an overload of the function Quotient::Room::setCanonicalAlias().
* This is to provide the functionality to remove the new canonical alias as a
* published alt alias when set.
*
* @property newAlias QString in the form #new_alias:server.org
*
* @sa Quotient::Room::setCanonicalAlias()
* */
Q_INVOKABLE void setCanonicalAlias(const QString &newAlias);
PushNotificationState::State pushNotificationState() const;
void setPushNotificationState(PushNotificationState::State state);
[[nodiscard]] QString historyVisibility() const;
void setHistoryVisibility(const QString &historyVisibilityRule);
@@ -166,6 +622,8 @@ public:
[[nodiscard]] bool urlPreviewEnabled() const;
void setUrlPreviewEnabled(const bool &urlPreviewEnabled);
bool canEncryptRoom() const;
/**
* @brief Get the power level for the given user ID in the room.
*
@@ -236,65 +694,6 @@ public:
[[nodiscard]] int spaceParentPowerLevel() const;
void setSpaceParentPowerLevel(const int &newPowerLevel);
[[nodiscard]] bool hasFileUploading() const
{
return m_hasFileUploading;
}
void setHasFileUploading(bool value)
{
if (value == m_hasFileUploading) {
return;
}
m_hasFileUploading = value;
Q_EMIT hasFileUploadingChanged();
}
[[nodiscard]] int fileUploadingProgress() const
{
return m_fileUploadingProgress;
}
void setFileUploadingProgress(int value)
{
if (m_fileUploadingProgress == value) {
return;
}
m_fileUploadingProgress = value;
Q_EMIT fileUploadingProgressChanged();
}
[[nodiscard]] bool readMarkerLoaded() const;
Q_INVOKABLE [[nodiscard]] int savedTopVisibleIndex() const;
Q_INVOKABLE [[nodiscard]] int savedBottomVisibleIndex() const;
Q_INVOKABLE void saveViewport(int topIndex, int bottomIndex);
Q_INVOKABLE [[nodiscard]] QVariantList getUsers(const QString &keyword, int limit = -1) const;
Q_INVOKABLE [[nodiscard]] QVariantMap getUser(const QString &userID) const;
Q_INVOKABLE QUrl urlToMxcUrl(const QUrl &mxcUrl);
[[nodiscard]] QString avatarMediaId() const;
[[nodiscard]] QString eventToString(const Quotient::RoomEvent &evt, Qt::TextFormat format = Qt::PlainText, bool stripNewlines = false) const;
[[nodiscard]] QString eventToGenericString(const Quotient::RoomEvent &evt) const;
Q_INVOKABLE [[nodiscard]] bool containsUser(const QString &userID) const;
Q_INVOKABLE [[nodiscard]] bool isUserBanned(const QString &user) const;
Q_INVOKABLE [[nodiscard]] bool canSendEvent(const QString &eventType) const;
Q_INVOKABLE [[nodiscard]] bool canSendState(const QString &eventType) const;
bool isInvite() const;
Q_INVOKABLE QString htmlSafeName() const;
Q_INVOKABLE QString htmlSafeDisplayName() const;
Q_INVOKABLE void clearInvitationNotification();
Q_INVOKABLE void reportEvent(const QString &eventId, const QString &reason);
Q_INVOKABLE void setPushNotificationState(PushNotificationState::State state);
Q_INVOKABLE void download(const QString &eventId, const QUrl &localFilename = {});
QString chatBoxText() const;
void setChatBoxText(const QString &text);
@@ -316,43 +715,37 @@ public:
QString chatBoxAttachmentPath() const;
void setChatBoxAttachmentPath(const QString &attachmentPath);
/**
* @brief Retrieve the mentions for the current chatbox text.
*/
QVector<Mention> *mentions();
/**
* @brief Vector of mentions in the current edit text.
* @brief Retrieve the mentions for the current edit text.
*/
QVector<Mention> *editMentions();
/**
* @brief Get the saved chatbox text for the room.
*/
QString savedText() const;
/**
* @brief Save the chatbox text for the room.
*/
void setSavedText(const QString &savedText);
bool canEncryptRoom() const;
Q_INVOKABLE bool downloadTempFile(const QString &eventId);
/*
* Map an alias to the room
/**
* @brief Get a PollHandler object for the given event Id.
*
* Note: this is different to setLocalAliases as that can only
* get the room to publish and alias that is already mapped.
* Will return an existing PollHandler if one already exists for the event ID.
* A new PollHandler will be created if one doesn't exist.
*
* @note Requires libQuotient 0.7.
*
* @sa PollHandler
*/
Q_INVOKABLE void mapAlias(const QString &alias);
Q_INVOKABLE void unmapAlias(const QString &alias);
Q_INVOKABLE void setCanonicalAlias(const QString &newAlias);
#ifdef QUOTIENT_07
Q_INVOKABLE PollHandler *poll(const QString &eventId);
#endif
#ifndef QUOTIENT_07
Q_INVOKABLE QString htmlSafeMemberName(const QString &userId) const
{
return safeMemberName(userId).toHtmlEscaped();
}
#endif
int maxRoomVersion() const;
NeoChatUser *directChatRemoteUser() const;
private:
QSet<const Quotient::RoomEvent *> highlights;
@@ -380,9 +773,7 @@ private:
QVector<Mention> m_mentions;
QVector<Mention> m_editMentions;
QString m_savedText;
#ifdef QUOTIENT_07
QCache<QString, PollHandler> m_polls;
#endif
private Q_SLOTS:
void countChanged();
@@ -432,26 +823,78 @@ Q_SIGNALS:
void spaceParentPowerLevelChanged();
public Q_SLOTS:
/**
* @brief Upload a file to the matrix server and post the file to the room.
*
* @param url the location of the file to be uploaded.
* @param body the caption that is to be given to the file.
*/
void uploadFile(const QUrl &url, const QString &body = QString());
/**
* @brief Accept an invitation for the local user to join the room.
*/
void acceptInvitation();
/**
* @brief Leave and forget the room for the local user.
*
* @note This means that not only will the user no longer receive events in
* the room but the will forget any history up to this point.
*
* @sa https://spec.matrix.org/latest/client-server-api/#leaving-rooms
*/
void forget();
/**
* @brief Set the typing notification state on the room for the local user.
*/
void sendTypingNotification(bool isTyping);
/// @param rawText The text as it was typed.
/// @param cleanedText The text with link to the users.
/**
* @brief Send a message to the room.
*
* @param rawText the text as it was typed.
* @param cleanedText the text marked up as html.
* @param type the type of message being sent.
* @param replyEventId the id of the message being replied to if a reply.
* @param relateToEventId the id of the message being edited if an edit.
*/
void postMessage(const QString &rawText,
const QString &cleanedText,
Quotient::MessageEventType type = Quotient::MessageEventType::Text,
const QString &replyEventId = QString(),
const QString &relateToEventId = QString());
/**
* @brief Send an html message to the room.
*
* @param text the text as it was typed.
* @param html the text marked up as html.
* @param type the type of message being sent.
* @param replyEventId the id of the message being replied to if a reply.
* @param relateToEventId the id of the message being edited if an edit.
*/
void postHtmlMessage(const QString &text,
const QString &html,
Quotient::MessageEventType type = Quotient::MessageEventType::Text,
const QString &replyEventId = QString(),
const QString &relateToEventId = QString());
/**
* @brief Set the room avatar.
*/
void changeAvatar(const QUrl &localFile);
void addLocalAlias(const QString &alias);
void removeLocalAlias(const QString &alias);
/**
* @brief Toggle the reaction state of the given reaction for the local user.
*/
void toggleReaction(const QString &eventId, const QString &reaction);
/**
* @brief Delete recent messages by the given user.
*
* This will delete all messages by that user in this room that are currently loaded.
*/
void deleteMessagesByUser(const QString &user, const QString &reason);
};

View File

@@ -11,11 +11,7 @@
#include <KNotification>
#include <KNotificationReplyAction>
#ifdef QUOTIENT_07
#include <accountregistry.h>
#else
#include "neochataccountregistry.h"
#endif
#include <connection.h>
#include <csapi/pushrules.h>
@@ -72,11 +68,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
connect(notification, &KNotification::defaultActivated, this, [=]() {
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
if (room->localUser()->id() != Controller::instance().activeConnection()->userId()) {
#ifdef QUOTIENT_07
Controller::instance().setActiveConnection(Accounts.get(room->localUser()->id()));
#else
Controller::instance().setActiveConnection(AccountRegistry::instance().get(room->localUser()->id()));
#endif
}
RoomManager::instance().enterRoom(room);
});

View File

@@ -34,7 +34,7 @@ void PollHandler::setRoom(NeoChatRoom *room)
connect(room, &NeoChatRoom::aboutToAddNewMessages, this, [this](Quotient::RoomEventsRange events) {
for (const auto &event : events) {
if (event->is<PollEndEvent>()) {
auto pl = m_room->getCurrentState<RoomPowerLevelsEvent>();
auto pl = m_room->currentState().get<RoomPowerLevelsEvent>();
auto userPl = pl->powerLevelForUser(event->senderId());
if (event->senderId() == (*m_room->findInTimeline(m_pollStartEventId))->senderId() || userPl >= pl->redact()) {
m_hasEnded = true;
@@ -75,7 +75,7 @@ void PollHandler::checkLoadRelations()
connect(job, &BaseJob::success, this, [this, job]() {
for (const auto &event : job->chunk()) {
if (event->is<PollEndEvent>()) {
auto pl = m_room->getCurrentState<RoomPowerLevelsEvent>();
auto pl = m_room->currentState().get<RoomPowerLevelsEvent>();
auto userPl = pl->powerLevelForUser(event->senderId());
if (event->senderId() == (*m_room->findInTimeline(m_pollStartEventId))->senderId() || userPl >= pl->redact()) {
m_hasEnded = true;

View File

@@ -197,7 +197,7 @@ QQC2.Control {
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
chatBar.pasteImage();
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
let replyEvent = messageEventModel.getLatestMessageFromIndex(0)
let replyEvent = messageEventModel.getLatestMessageFromRow(0)
if (replyEvent && replyEvent["event_id"]) {
currentRoom.chatBoxReplyId = replyEvent["event_id"]
}
@@ -394,6 +394,8 @@ QQC2.Control {
cursorPosition: textField.cursorPosition
selectionStart: textField.selectionStart
selectionEnd: textField.selectionEnd
mentionColor: Kirigami.Theme.linkColor
errorColor: Kirigami.Theme.negativeTextColor
Component.onCompleted: {
RoomManager.chatDocumentHandler = documentHandler;
}

View File

@@ -1,115 +1,132 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Layouts 1.10
import QtQuick.Controls 2.12 as QQC2
import org.kde.kirigami 2.14 as Kirigami
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0
QQC2.Popup {
id: _popup
QQC2.Dialog {
id: root
parent: applicationWindow().overlay
width: Math.min(700, parent.width)
height: 400
leftPadding: 0
rightPadding: 0
bottomPadding: 0
topPadding: 0
anchors.centerIn: applicationWindow().overlay
Keys.forwardTo: searchField
Shortcut {
sequence: "Ctrl+K"
enabled: !Kirigami.Settings.hasPlatformMenuBar
onActivated: _popup.open()
onActivated: root.open()
}
onVisibleChanged: {
if (!visible) {
return
}
quickSearch.forceActiveFocus()
quickSearch.text = ""
searchField.forceActiveFocus()
searchField.text = ""
roomList.currentIndex = 0
}
anchors.centerIn: QQC2.Overlay.overlay
background: Kirigami.Card {}
height: 2 * Math.round(implicitHeight / 2)
padding: Kirigami.Units.largeSpacing * 2
contentItem: ColumnLayout {
spacing: Kirigami.Units.largeSpacing * 2
Kirigami.SearchField {
id: quickSearch
// TODO: get this broken property removed/disabled by default in Kirigami,
// we used to be able to expect that the text field wouldn't attempt to
// perform a mini-DDOS attack using signals.
autoAccept: false
/**
* The focus is manged by the popup and we don't want to use the standard
* shortcut as it could block other SearchFields from using it.
*/
focusSequence: ""
Layout.preferredWidth: Kirigami.Units.gridUnit * 21 // 3 * 7 = 21, roughly 7 avatars on screen
Keys.onLeftPressed: cView.decrementCurrentIndex()
Keys.onRightPressed: cView.incrementCurrentIndex()
onAccepted: {
const item = cView.itemAtIndex(cView.currentIndex)
RoomManager.enterRoom(item.currentRoom)
_popup.close()
header: Kirigami.SearchField {
id: searchField
Keys.onDownPressed: {
roomList.forceActiveFocus()
if (roomList.currentIndex < roomList.count - 1) {
roomList.currentIndex++
} else {
roomList.currentIndex = 0
}
}
Keys.onUpPressed: {
if (roomList.currentIndex === 0) {
roomList.currentIndex = roomList.count - 1
} else {
roomList.currentIndex--
}
}
Keys.onEnterPressed: {
RoomManager.enterRoom(roomList.currentItem.currentRoom);
root.close();
}
Keys.onReturnPressed: {
RoomManager.enterRoom(roomList.currentItem.currentRoom);
root.close();
}
}
QQC2.ScrollView {
anchors.fill: parent
ListView {
id: cView
orientation: Qt.Horizontal
spacing: Kirigami.Units.largeSpacing
id: roomList
currentIndex: 0
highlightMoveDuration: 200
Keys.forwardTo: searchField
keyNavigationEnabled: true
model: SortFilterRoomListModel {
id: sortFilterRoomListModel
filterText: searchField.text
sourceModel: RoomListModel {
id: roomListModel
connection: Controller.activeConnection
}
filterText: quickSearch.text
roomSortOrder: SortFilterRoomListModel.LastActivity
}
delegate: Kirigami.BasicListItem {
id: roomListItem
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.fillWidth: true
delegate: Kirigami.Avatar {
id: del
implicitHeight: Kirigami.Units.gridUnit * 3
implicitWidth: Kirigami.Units.gridUnit * 3
required property string avatar
required property var currentRoom
required property string name
required property int index
required property int unreadCount
required property string subtitleText
required property string avatar
name: currentRoom.displayName
// When an item is hovered set the currentIndex of listview to it so that it is highlighted
onHoveredChanged: {
if (!hovered) {
return
}
cView.currentIndex = index
topPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
highlighted: roomList.currentIndex === roomListItem.index
focus: true
icon: undefined
onClicked: {
RoomManager.enterRoom(roomListItem.currentRoom);
root.close()
}
Keys.onEnterPressed: {
RoomManager.enterRoom(roomListItem.currentRoom);
root.close();
}
Keys.onReturnPressed: {
RoomManager.enterRoom(roomListItem.currentRoom);
root.close();
}
bold: roomListItem.unreadCount > 0
label: roomListItem.name ?? ""
labelItem.textFormat: Text.PlainText
subtitle: roomListItem.subtitleText
subtitleItem.textFormat: Text.PlainText
onPressAndHold: {
createRoomListContextMenu()
}
actions.main: Kirigami.Action {
id: enterRoomAction
onTriggered: {
RoomManager.enterRoom(currentRoom);
_popup.close()
}
leading: Kirigami.Avatar {
source: roomListItem.avatar ? "image://mxc/" + roomListItem.avatar : ""
name: roomListItem.name || i18n("No Name")
implicitWidth: height
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
}
source: avatar != "" ? "image://mxc/" + avatar : ""
}
}
}
modal: true
focus: true
}

View File

@@ -11,7 +11,7 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
DelegateChooser {
role: "eventType"
role: "delegateType"
DelegateChoice {
roleValue: MessageEventModel.State
@@ -20,9 +20,7 @@ DelegateChooser {
DelegateChoice {
roleValue: MessageEventModel.Emote
delegate: MessageDelegate {
isEmote: true
}
delegate: MessageDelegate {}
}
DelegateChoice {
@@ -75,6 +73,11 @@ DelegateChooser {
delegate: PollDelegate {}
}
DelegateChoice {
roleValue: MessageEventModel.Location
delegate: LocationDelegate {}
}
DelegateChoice {
roleValue: MessageEventModel.Other
delegate: Item {}

View File

@@ -110,14 +110,18 @@ TimelineContainer {
ColumnLayout {
spacing: 0
QQC2.Label {
Layout.fillWidth: true
text: model.display
wrapMode: Text.Wrap
elide: Text.ElideRight
}
QQC2.Label {
id: sizeLabel
Layout.fillWidth: true
text: Controller.formatByteSize(content.info ? content.info.size : 0)
opacity: 0.7
elide: Text.ElideRight
maximumLineCount: 1
}
}

View File

@@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtLocation 5.15
import QtPositioning 5.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
TimelineContainer {
id: locationDelegate
ColumnLayout {
Layout.maximumWidth: locationDelegate.contentMaxWidth
Layout.preferredWidth: locationDelegate.contentMaxWidth
Map {
id: map
Layout.fillWidth: true
Layout.preferredHeight: locationDelegate.contentMaxWidth / 16 * 9
center: QtPositioning.coordinate(model.latitude, model.longitude)
zoomLevel: 15
plugin: Plugin {
name: "osm"
PluginParameter {
name: "osm.useragent"
value: Application.name + "/" + Application.version + " (kde-devel@kde.org)"
}
PluginParameter {
name: "osm.mapping.providersrepository.address"
value: "https://autoconfig.kde.org/qtlocation/"
}
}
onCopyrightLinkActivated: Qt.openUrlExternally(link)
MapQuickItem {
id: point
anchorPoint.x: sourceItem.width / 2
anchorPoint.y: sourceItem.height
coordinate: QtPositioning.coordinate(model.latitude, model.longitude)
autoFadeIn: false
sourceItem: Kirigami.Icon {
width: height
height: Kirigami.Units.iconSizes.huge
source: "gps"
isMask: true
color: Kirigami.Theme.highlightColor
Kirigami.Icon {
anchors.centerIn: parent
anchors.verticalCenterOffset: -parent.height / 8
visible: model.asset === "m.pin"
width: height
height: parent.height / 3 + 1
source: "pin"
isMask: true
color: Kirigami.Theme.highlightColor
}
Kirigami.Avatar {
anchors.centerIn: parent
anchors.verticalCenterOffset: -parent.height / 8
visible: model.asset === "m.self"
width: height
height: parent.height / 3 + 1
name: model.author.name ?? model.author.displayName
source: model.author.avatarMediaId ? ("image://mxc/" + model.author.avatarMediaId) : ""
color: model.author.color
}
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openMessageContext(model, "", model.message)
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openMessageContext(model, "", model.message)
}
}
}
}

View File

@@ -13,7 +13,6 @@ import org.kde.neochat 1.0
TimelineContainer {
id: messageDelegate
property bool isEmote: false
onOpenContextMenu: openMessageContext(model, label.selectedText, Controller.plainText(label.textDocument))
innerObject: ColumnLayout {
@@ -22,7 +21,6 @@ TimelineContainer {
id: label
Layout.fillWidth: true
visible: currentRoom.chatBoxEditId !== model.eventId
isEmote: messageDelegate.isEmote
}
Loader {
Layout.fillWidth: true

View File

@@ -119,6 +119,8 @@ QQC2.TextArea {
selectionStart: root.selectionStart
selectionEnd: root.selectionEnd
room: root.room // We don't care about saving for edits so this is OK.
mentionColor: Kirigami.Theme.linkColor
errorColor: Kirigami.Theme.negativeTextColor
}
TextMetrics {

View File

@@ -96,7 +96,6 @@ Item {
RichLabel {
textMessage: reply.display
textFormat: Text.RichText
isReplyLabel: true
HoverHandler {
enabled: !hoveredLink

View File

@@ -14,8 +14,6 @@ TextEdit {
readonly property var isEmoji: /^(<span style='.*'>)?(\u00a9|\u00ae|[\u20D0-\u2fff]|[\u3190-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+(<\/span>)?$/
readonly property var hasSpoiler: /data-mx-spoiler/g
property bool isEmote: false
property bool isReplyLabel: false
property string textMessage: model.display
property bool spoilerRevealed: !hasSpoiler.test(textMessage)
@@ -35,6 +33,9 @@ table {
border-collapse: collapse;
border-style: solid;
}
code {
background-color:" + Kirigami.Theme.alternateBackgroundColor + ";
}
table th,
table td {
border: 1px solid black;
@@ -57,7 +58,7 @@ a{
background: " + Kirigami.Theme.textColor + ";
}
" : "") + "
</style>" + (isEmote ? "* <a href='https://matrix.to/#/" + author.id + "' style='color: " + author.color + "'>" + author.displayName + "</a> " : "") + textMessage + (isEdited && !contentLabel.isReplyLabel ? (" <span style=\"color: " + Kirigami.Theme.disabledTextColor + "\">" + "<span style='font-size: " + Kirigami.Theme.defaultFont.pixelSize +"px'>" + i18n(" (edited)") + "</span>") : "")
</style>" + textMessage
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor

View File

@@ -42,6 +42,6 @@ QQC2.ItemDelegate {
background: Rectangle {
color: Config.blur ? "transparent" : Kirigami.Theme.backgroundColor
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
Kirigami.Theme.colorSet: Config.compactLayout ? Kirigami.Theme.View : Kirigami.Theme.Window
}
}

View File

@@ -21,7 +21,6 @@ ColumnLayout {
default property alias innerObject : column.children
property Item hoverComponent: hoverActions ?? null
property bool isEmote: false
property bool cardBackground: true
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && model.author.isLocalUser && !Config.compactLayout
property bool isHighlighted: model.isHighlighted || isTemporaryHighlighted
@@ -212,7 +211,7 @@ ColumnLayout {
id: rowLayout
spacing: Kirigami.Units.smallSpacing
visible: model.showAuthor && !isEmote
visible: model.showAuthor
QQC2.Label {
id: nameLabel
@@ -306,6 +305,7 @@ ColumnLayout {
border.width: 1
Behavior on color {
enabled: isTemporaryHighlighted
ColorAnimation {target: bubbleBackground; duration: Kirigami.Units.veryLongDuration; easing.type: Easing.InOutCubic}
}
}
@@ -336,7 +336,7 @@ ColumnLayout {
Layout.leftMargin: showUserMessageOnRight ? 0 : bubble.x + bubble.anchors.leftMargin
Layout.rightMargin: showUserMessageOnRight ? Kirigami.Units.largeSpacing : 0
visible: eventType !== MessageEventModel.State && eventType !== MessageEventModel.Notice && reaction != undefined && reaction.length > 0
visible: delegateType !== MessageEventModel.State && delegateType !== MessageEventModel.Notice && reaction != undefined && reaction.length > 0
}
AvatarFlow {
Layout.alignment: Qt.AlignRight

View File

@@ -20,8 +20,8 @@ QQC2.Dialog {
}
}
x: Math.round((parent.width - width) / 2)
y: Math.round((parent.height - height) / 2)
x: parent ? Math.round((parent.width - width) / 2) : 0
y: parent ? Math.round((parent.height - height) / 2) : 0
modal: true
footer: QQC2.DialogButtonBox {
@@ -35,7 +35,7 @@ QQC2.Dialog {
text: i18n("Sign out")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
onClicked: {
Controller.logout(Controller.activeConnection, true);
Controller.activeConnection.logout();
root.close();
}
}

View File

@@ -18,13 +18,13 @@ Kirigami.OverlaySheet {
contentItem: Kirigami.FormLayout {
QQC2.TextField {
id: roomNameField
Kirigami.FormData.label: i18n("Room Name")
Kirigami.FormData.label: i18n("Room name:")
onAccepted: roomTopicField.forceActiveFocus();
}
QQC2.TextField {
id: roomTopicField
Kirigami.FormData.label: i18n("Room Topic")
Kirigami.FormData.label: i18n("Room topic:")
onAccepted: okButton.forceActiveFocus();
}

View File

@@ -40,12 +40,12 @@ Labs.MenuBar {
Labs.MenuItem {
text: i18nc("menu", "New Private Chat…")
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.activeConnection
onTriggered: pushReplaceLayer("qrc:/StartChatPage.qml", {connection: Controller.activeConnection})
}
Labs.MenuItem {
text: i18nc("menu", "New Group…")
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.activeConnection
shortcut: StandardKey.New
onTriggered: {
const dialog = createRoomDialog.createObject(root.overlay)

View File

@@ -8,6 +8,7 @@ import QtQuick.Layouts 1.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
import '../Dialog' as Dialog
QQC2.Menu {
id: root
@@ -38,7 +39,7 @@ QQC2.Menu {
onTriggered: confirmLogoutDialog.open()
}
ConfirmLogoutDialog {
Dialog.ConfirmLogout {
id: confirmLogoutDialog
}
}

View File

@@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2023 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import QtQml.Models 2.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0
import './' as RoomList
QQC2.ItemDelegate {
id: root
required property var currentRoom
required property bool categoryVisible
required property string filterText
required property string avatar
required property string name
topPadding: Kirigami.Units.largeSpacing
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
width: ListView.view.width
height: visible ? ListView.view.width : 0
visible: root.categoryVisible || filterText.length > 0 || Config.mergeRoomList
contentItem: Kirigami.Avatar {
source: root.avatar ? `image://mxc/${root.avatar}` : ""
name: root.name || i18n("No Name")
sourceSize {
width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
}
}
onClicked: RoomManager.enterRoom(root.currentRoom)
Keys.onEnterPressed: RoomManager.enterRoom(root.currentRoom)
Keys.onReturnPressed: RoomManager.enterRoom(root.currentRoom)
QQC2.ToolTip.visible: text.length > 0 && hovered
QQC2.ToolTip.text: root.name ?? ""
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}

View File

@@ -0,0 +1,301 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import QtQml.Models 2.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0
import './' as RoomList
Kirigami.ScrollablePage {
id: root
/**
* @brief The current width of the room list.
*
* @note Other objects can access the value but the private function makes sure
* that only the internal members can modify it.
*/
readonly property int currentWidth: _private.currentWidth
readonly property RoomListModel roomListModel: RoomListModel {
connection: Controller.activeConnection
}
readonly property bool collapsed: Config.collapsed
property var enteredRoom: null
onCollapsedChanged: if (collapsed) {
sortFilterRoomListModel.filterText = "";
}
header: ColumnLayout {
visible: !root.collapsed
spacing: 0
RoomList.SpaceListView {
roomListModel: root.roomListModel
}
Kirigami.Separator {
Layout.fillWidth: true
}
Component {
id: spaceListContextMenu
SpaceListContextMenu {}
}
}
Connections {
target: RoomManager
function onCurrentRoomChanged() {
itemSelection.setCurrentIndex(roomListModel.index(roomListModel.indexForRoom(RoomManager.currentRoom), 0), ItemSelectionModel.SelectCurrent)
}
}
function goToNextRoomFiltered(condition) {
let index = listView.currentIndex;
while (index++ !== listView.count - 1) {
if (condition(listView.itemAtIndex(index))) {
listView.currentIndex = index;
listView.currentItem.action.trigger();
return;
}
}
}
function goToPreviousRoomFiltered(condition) {
let index = listView.currentIndex;
while (index-- !== 0) {
if (condition(listView.itemAtIndex(index))) {
listView.currentIndex = index;
listView.currentItem.action.trigger();
return;
}
}
}
function goToNextRoom() {
goToNextRoomFiltered((item) => item.visible);
}
function goToPreviousRoom() {
goToPreviousRoomFiltered((item) => item.visible);
}
function goToNextUnreadRoom() {
goToNextRoomFiltered((item) => (item.visible && item.hasUnread));
}
function goToPreviousUnreadRoom() {
goToPreviousRoomFiltered((item) => (item.visible && item.hasUnread));
}
titleDelegate: ExploreComponent {
Layout.fillWidth: true
desiredWidth: root.width - Kirigami.Units.largeSpacing
collapsed: root.collapsed
}
ListView {
id: listView
activeFocusOnTab: true
clip: AccountRegistry.count > 1
header: QQC2.ItemDelegate {
width: visible ? ListView.view.width : 0
height: visible ? Kirigami.Units.gridUnit * 2 : 0
visible: root.collapsed
topPadding: Kirigami.Units.largeSpacing
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
onClicked: quickView.item.open();
Kirigami.Icon {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.smallMedium
height: Kirigami.Units.iconSizes.smallMedium
source: "search"
}
Kirigami.Separator {
width: parent.width
anchors.bottom: parent.bottom
}
}
Layout.fillWidth: true
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
width: parent.width - (Kirigami.Units.largeSpacing * 4)
visible: listView.count == 0
text: sortFilterRoomListModel.filterText.length > 0 ? i18n("No rooms found") : i18n("Join some rooms to get started")
helpfulAction: Kirigami.Action {
icon.name: sortFilterRoomListModel.filterText.length > 0 ? "search" : "list-add"
text: sortFilterRoomListModel.filterText.length > 0 ? i18n("Search in room directory") : i18n("Explore rooms")
onTriggered: pageStack.layers.push("qrc:/JoinRoomPage.qml", {
connection: Controller.activeConnection,
keyword: sortFilterRoomListModel.filterText
})
}
}
ItemSelectionModel {
id: itemSelection
model: root.roomListModel
onCurrentChanged: listView.currentIndex = sortFilterRoomListModel.mapFromSource(current).row
}
model: SortFilterRoomListModel {
id: sortFilterRoomListModel
sourceModel: root.roomListModel
roomSortOrder: Config.mergeRoomList ? SortFilterRoomListModel.LastActivity : SortFilterRoomListModel.Categories
onLayoutChanged: {
listView.currentIndex = sortFilterRoomListModel.mapFromSource(itemSelection.currentIndex).row
}
}
section.property: sortFilterRoomListModel.filterText.length === 0 && !Config.mergeRoomList ? "category" : null
section.delegate: root.collapsed ? foldButton : sectionHeader
Component {
id: sectionHeader
Kirigami.ListSectionHeader {
height: implicitHeight
label: roomListModel.categoryName(section)
action: Kirigami.Action {
onTriggered: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
}
contentItem.children: QQC2.ToolButton {
icon {
name: roomListModel.categoryVisible(section) ? "go-up" : "go-down"
width: Kirigami.Units.iconSizes.small
height: Kirigami.Units.iconSizes.small
}
onClicked: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
}
}
}
Component {
id: foldButton
Item {
width: ListView.view.width
height: visible ? width : 0
QQC2.ToolButton {
id: button
anchors.centerIn: parent
icon {
name: hovered ? (roomListModel.categoryVisible(section) ? "go-up" : "go-down") : roomListModel.categoryIconName(section)
width: Kirigami.Units.iconSizes.smallMedium
height: Kirigami.Units.iconSizes.smallMedium
}
onClicked: roomListModel.setCategoryVisible(section, !roomListModel.categoryVisible(section))
QQC2.ToolTip.text: roomListModel.categoryName(section)
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
}
reuseItems: true
currentIndex: -1 // we don't want any room highlighted by default
delegate: root.collapsed ? collapsedModeListComponent : normalModeListComponent
Component {
id: collapsedModeListComponent
RoomList.CollapsedRoomDelegate {
filterText: sortFilterRoomListModel.filterText
}
}
Component {
id: normalModeListComponent
RoomList.RoomDelegate {
filterText: sortFilterRoomListModel.filterText
}
}
}
footer: UserInfo {
width: parent.width
visible: !root.collapsed
}
MouseArea {
anchors.top: parent.top
anchors.bottom: parent.bottom
parent: applicationWindow().overlay.parent
x: root.currentWidth - width / 2
width: Kirigami.Units.smallSpacing * 2
z: root.z + 1
enabled: RoomManager.hasOpenRoom && applicationWindow().width >= Kirigami.Units.gridUnit * 35
visible: enabled
cursorShape: Qt.SplitHCursor
property int _lastX
onPressed: mouse => {
_lastX = mouse.x;
}
onPositionChanged: mouse => {
if (_lastX == -1) {
return;
}
if (mouse.x > _lastX) {
// we moved to the right
if (_private.currentWidth < _private.collapseWidth && _private.currentWidth + (mouse.x - _lastX) >= _private.collapseWidth) {
// Here we get back directly to a more wide mode.
_private.currentWidth = _private.collapseWidth;
Config.collapsed = false;
} else if (_private.currentWidth >= _private.collapseWidth) {
// Increase page width
_private.currentWidth = Math.min(_private.defaultWidth, _private.currentWidth + (mouse.x - _lastX));
}
} else if (mouse.x < _lastX) {
const tmpWidth = _private.currentWidth - (_lastX - mouse.x);
if (tmpWidth < _private.collapseWidth) {
_private.currentWidth = Qt.binding(() => _private.collapsedSize + (root.contentItem.QQC2.ScrollBar.vertical.visible ? root.contentItem.QQC2.ScrollBar.vertical.width : 0));
Config.collapsed = true;
} else {
_private.currentWidth = tmpWidth;
}
}
}
}
/*
* Hold the modifiable currentWidth in a private object so that only internal
* members can modify it.
*/
QtObject {
id: _private
property int currentWidth: Config.collapsed ? collapsedSize : defaultWidth
readonly property int defaultWidth: Kirigami.Units.gridUnit * 17
readonly property int collapseWidth: Kirigami.Units.gridUnit * 10
readonly property int collapsedSize: Kirigami.Units.gridUnit * 3 - Kirigami.Units.smallSpacing * 3
}
}

View File

@@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: 2023 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import QtQml.Models 2.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0
import './' as RoomList
Kirigami.BasicListItem {
id: root
required property int index
required property int unreadCount
required property int notificationCount
required property int highlightCount
required property var currentRoom
required property bool categoryVisible
required property string filterText
required property string avatar
required property string subtitleText
required property string name
readonly property bool hasUnread: unreadCount > 0
topPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
visible: root.categoryVisible || root.filterText.length > 0 || Config.mergeRoomList
highlighted: ListView.view.currentIndex === index
focus: true
icon: undefined
bold: root.unreadCount > 0
label: root.name ?? ""
labelItem.textFormat: Text.PlainText
subtitle: root.subtitleText
subtitleItem {
textFormat: Text.PlainText
visible: !Config.compactRoomList
}
onClicked: RoomManager.enterRoom(root.currentRoom)
onPressAndHold: createRoomListContextMenu()
Keys.onEnterPressed: RoomManager.enterRoom(root.currentRoom)
Keys.onReturnPressed: RoomManager.enterRoom(root.currentRoom)
TapHandler {
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse
onTapped: createRoomListContextMenu()
}
leading: Kirigami.Avatar {
source: root.avatar ? `image://mxc/${root.avatar}` : ""
name: root.name || i18n("No Name")
implicitWidth: visible ? height : 0
visible: Config.showAvatarInRoomDrawer
sourceSize {
width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
}
}
trailing: RowLayout {
Kirigami.Icon {
source: "notifications-disabled"
enabled: false
implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: Kirigami.Units.iconSizes.smallMedium
visible: currentRoom.pushNotificationState === PushNotificationState.Mute && !configButton.visible && unreadCount <= 0
Accessible.name: i18n("Muted room")
Layout.rightMargin: Kirigami.Units.smallSpacing
}
QQC2.Label {
id: notificationCountLabel
text: notificationCount > 0 ? notificationCount : "●"
visible: unreadCount > 0
color: Kirigami.Theme.textColor
horizontalAlignment: Text.AlignHCenter
background: Rectangle {
visible: notificationCount > 0
Kirigami.Theme.colorSet: Kirigami.Theme.Button
color: highlightCount > 0 ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.disabledTextColor
opacity: highlightCount > 0 ? 1 : 0.3
radius: height / 2
}
Layout.rightMargin: Kirigami.Units.smallSpacing
Layout.minimumHeight: Kirigami.Units.iconSizes.smallMedium
Layout.minimumWidth: Math.max(notificationCountTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 2, height)
TextMetrics {
id: notificationCountTextMetrics
text: notificationCountLabel.text
}
}
QQC2.Button {
id: configButton
visible: root.hovered && !Kirigami.Settings.isMobile && !Config.compactRoomList
text: i18n("Configure room")
display: QQC2.Button.IconOnly
icon.name: "configure"
onClicked: createRoomListContextMenu()
}
}
function createRoomListContextMenu() {
const component = Qt.createComponent(Qt.resolvedUrl("./ContextMenu.qml"))
const menu = component.createObject(root, {
room: root.currentRoom,
});
if (!Kirigami.Settings.isMobile && !Config.compactRoomList) {
configButton.visible = true;
configButton.down = true;
}
menu.closed.connect(function() {
configButton.down = undefined;
configButton.visible = Qt.binding(() => {
return root.hovered && !Kirigami.Settings.isMobile
&& !Config.compactRoomList;
});
})
menu.open()
}
}

View File

@@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: 2022 Snehit Sah <snehitsah@protonmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.neochat 1.0
QQC2.ItemDelegate {
id: root
required property string avatar
required property var currentRoom
required property int index
required property string id
signal createContextMenu(currentRoom: var)
signal spaceSelected(spaceId: string)
height: ListView.view.height
width: height
leftPadding: topPadding
rightPadding: topPadding
contentItem: Kirigami.Avatar {
name: currentRoom.displayName
source: avatar !== "" ? "image://mxc/" + avatar : ""
}
onClicked: root.spaceSelected(id)
onPressAndHold: root.createContextMenu(root.currentRoom)
Accessible.name: currentRoom.displayName
QQC2.ToolTip.text: currentRoom.displayName
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
TapHandler {
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse
onTapped: root.createContextMenu(root.currentRoom)
}
}

View File

@@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: 2022 Snehit Sah <snehitsah@protonmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.neochat 1.0
ListView {
id: root
required property RoomListModel roomListModel
orientation: Qt.Horizontal
spacing: Kirigami.Units.smallSpacing
clip: true
visible: root.count > 0
model: SortFilterSpaceListModel {
id: sortFilterSpaceListModel
sourceModel: root.roomListModel
}
header: QQC2.ItemDelegate {
id: homeButton
icon.name: "home"
text: i18nc("@action:button", "Show All Rooms")
height: parent.height
width: height
leftPadding: topPadding
rightPadding: topPadding
contentItem: Kirigami.Icon {
source: "home"
}
onClicked: {
sortFilterRoomListModel.activeSpaceId = "";
listView.positionViewAtIndex(0, ListView.Beginning);
}
QQC2.ToolTip.text: homeButton.text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
delegate: SpaceDelegate {
onSpaceSelected: (spaceId) => {
sortFilterRoomListModel.activeSpaceId = spaceId;
}
onCreateContextMenu: () => {
const menu = spaceListContextMenu.createObject(page, {
room: currentRoom,
});
menu.open();
}
}
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
Layout.fillWidth: true
}

View File

@@ -76,7 +76,7 @@ QQC2.ToolBar {
visible: switchUserButton.checked
onVisibleChanged: if (visible) accounts.forceActiveFocus()
clip: true
model: AccountRegistry
model: Accounts
keyNavigationEnabled: false
Keys.onDownPressed: {

View File

@@ -46,10 +46,7 @@ Kirigami.ScrollablePage {
model: SortFilterSpaceListModel {
id: sortFilterSpaceListModel
sourceModel: RoomListModel {
id: spaceListModel
connection: Controller.activeConnection
}
sourceModel: roomListModel
}
header: QQC2.ItemDelegate {
@@ -185,7 +182,7 @@ Kirigami.ScrollablePage {
id: listView
activeFocusOnTab: true
clip: AccountRegistry.count > 1
clip: Accounts.count > 1
header: QQC2.ItemDelegate {
visible: page.collapsed

View File

@@ -263,7 +263,7 @@ Kirigami.ScrollablePage {
anchors.leftMargin: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.x : 0
anchors.right: parent.right
maxWidth: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.width - Kirigami.Units.largeSpacing * 2 : 0
maxWidth: Config.compactLayout ? messageListView.width : (messageListView.sectionBannerItem ? messageListView.sectionBannerItem.width - Kirigami.Units.largeSpacing * 2 : 0)
z: 3
visible: messageListView.sectionBannerItem != undefined && messageListView.sectionBannerItem.ListView.section != "" && !Config.blur
labelText: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.ListView.section : ""
@@ -476,7 +476,7 @@ Kirigami.ScrollablePage {
id: hoverActions
property var event: null
property bool userMsg: event && event.author.id === Controller.activeConnection.localUserId
property bool showEdit: event && (userMsg && (event.eventType === MessageEventModel.Emote || event.eventType === MessageEventModel.Message))
property bool showEdit: event && (userMsg && (event.delegateType === MessageEventModel.Emote || event.delegateType === MessageEventModel.Message))
property var delegate: null
property var bubble: null
property var hovered: bubble && bubble.hovered
@@ -581,37 +581,42 @@ Kirigami.ScrollablePage {
}
}
background: FancyEffectsContainer {
id: fancyEffectsContainer
z: 100
background: Rectangle {
color: Config.compactLayout ? Kirigami.Theme.backgroundColor : "transparent"
enabled: Config.showFancyEffects
FancyEffectsContainer {
id: fancyEffectsContainer
anchors.fill: parent
z: 100
function processFancyEffectsReason(fancyEffect) {
if (fancyEffect === "snowflake") {
fancyEffectsContainer.showSnowEffect()
}
if (fancyEffect === "fireworks") {
fancyEffectsContainer.showFireworksEffect()
}
if (fancyEffect === "confetti") {
fancyEffectsContainer.showConfettiEffect()
}
}
Connections {
enabled: Config.showFancyEffects
target: messageEventModel
function onFancyEffectsReasonFound(fancyEffect) {
fancyEffectsContainer.processFancyEffectsReason(fancyEffect)
}
}
Connections {
enabled: Config.showFancyEffects
target: actionsHandler
function onShowEffect(fancyEffect) {
fancyEffectsContainer.processFancyEffectsReason(fancyEffect)
function processFancyEffectsReason(fancyEffect) {
if (fancyEffect === "snowflake") {
fancyEffectsContainer.showSnowEffect()
}
if (fancyEffect === "fireworks") {
fancyEffectsContainer.showFireworksEffect()
}
if (fancyEffect === "confetti") {
fancyEffectsContainer.showConfettiEffect()
}
}
Connections {
enabled: Config.showFancyEffects
target: messageEventModel
function onFancyEffectsReasonFound(fancyEffect) {
fancyEffectsContainer.processFancyEffectsReason(fancyEffect)
}
}
Connections {
enabled: Config.showFancyEffects
target: actionsHandler
function onShowEffect(fancyEffect) {
fancyEffectsContainer.processFancyEffectsReason(fancyEffect)
}
}
}
}
@@ -637,7 +642,7 @@ Kirigami.ScrollablePage {
}
function eventToIndex(eventID) {
const index = messageEventModel.eventIDToIndex(eventID)
const index = messageEventModel.eventIdToRow(eventID)
if (index === -1)
return -1
return sortedMessageEventModel.mapFromSource(messageEventModel.index(index, 0)).row
@@ -697,7 +702,7 @@ Kirigami.ScrollablePage {
eventId: event.eventId,
formattedBody: event.formattedBody,
source: event.source,
eventType: event.eventType,
eventType: event.delegateType,
plainMessage: plainMessage,
});
contextMenu.open();

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