Compare commits

..

73 Commits

Author SHA1 Message Date
Slawek Kaplonski
4732d41242 Mark all messages as read on current room change
When user enters to the room, all messages from that room should
be already marked as ready without need to e.g. scroll up and then down
to the bottom again.
2023-04-15 14:29:46 +02:00
James Graham
516b1cff88 Document publicroomlistmodel 2023-04-15 09:00:49 +00:00
James Graham
6438977964 Document messagefiltermodel 2023-04-15 08:46:45 +00:00
James Graham
d750263d39 Document keywordnotificationrulemodel 2023-04-15 08:32:35 +00:00
James Graham
3ed952db9e Document emojimodel 2023-04-15 08:15:23 +00:00
James Graham
f8040a1bf6 Guard getTagType and isCloseTag
Add guard clauses for getTagType and isCloseTag to avoid crashing if the string is empty.

CCBUG: 468448
2023-04-15 07:59:05 +00:00
l10n daemon script
d83b31fd86 GIT_SILENT Sync po/docbooks with svn 2023-04-15 01:59:35 +00:00
James Graham
e0dbb657f6 Document devicesmodel 2023-04-14 13:14:39 +00:00
James Graham
a807cc6143 Document customemojimodel 2023-04-14 12:57:05 +00:00
l10n daemon script
fe064c0ef8 GIT_SILENT Sync po/docbooks with svn 2023-04-14 01:50:25 +00:00
James Graham
6cc773426f Document completionmodel
Document completionmodel and remove unnecessary includes
2023-04-13 17:35:44 +00:00
James Graham
db94408ba6 Document completionproxymodel 2023-04-13 17:15:38 +00:00
James Graham
333bd3cdb9 Document collapsestateproxymodel
Document collapsestateproxymodel and make som public functions private.
2023-04-13 16:41:41 +00:00
l10n daemon script
9a0d82eb31 GIT_SILENT Sync po/docbooks with svn 2023-04-13 01:58:59 +00:00
James Graham
0990c0507c Document actionsmodel and cleanup includes 2023-04-12 21:12:14 +00:00
Heiko Becker
9f76ce22c1 GIT_SILENT Update Appstream for new release
(cherry picked from commit 2c3ce1e88b)
2023-04-12 19:09:48 +02:00
l10n daemon script
6acd6075ff GIT_SILENT Sync po/docbooks with svn 2023-04-12 01:55:28 +00: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
105 changed files with 22877 additions and 22202 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

@@ -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,6 +60,9 @@ private Q_SLOTS:
void receiveRichtextIn();
void receiveRichMxcUrl();
void receiveRichPlainUrl();
void receiveRichEmote();
void receiveRichEdited_data();
void receiveRichEdited();
};
#ifdef QUOTIENT_07
@@ -146,7 +150,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"
}
}
}
}
],
@@ -309,15 +350,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 +397,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 +407,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);
}
/**
@@ -434,7 +489,7 @@ 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
@@ -493,5 +548,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"

View File

@@ -231,6 +231,7 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="23.04.0" date="2023-04-20"/>
<release version="23.01" date="2023-01-30">
<url>https://plasma-mobile.org/2023/01/30/january-blog-post/</url>
<description>

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

@@ -47,7 +47,6 @@ add_library(neochat STATIC
filetransferpseudojob.cpp
models/searchmodel.cpp
texthandler.cpp
models/locationsmodel.cpp
)
add_executable(neochat-app
@@ -179,6 +178,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

@@ -52,7 +52,6 @@
#include "models/devicesmodel.h"
#include "models/emojimodel.h"
#include "models/keywordnotificationrulemodel.h"
#include "models/locationsmodel.h"
#include "models/messageeventmodel.h"
#include "models/messagefiltermodel.h"
#include "models/publicroomlistmodel.h"
@@ -236,7 +235,6 @@ 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");
qmlRegisterType<LocationsModel>("org.kde.neochat", 1, 0, "LocationsModel");
#ifdef QUOTIENT_07
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");
#endif

View File

@@ -5,7 +5,6 @@
#include "controller.h"
#include "neochatroom.h"
#include "neochatuser.h"
#include "roommanager.h"
#include <events/roommemberevent.h>
#include <events/roompowerlevelsevent.h>
@@ -19,6 +18,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"),
@@ -268,31 +302,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 +310,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 +333,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>"),

View File

@@ -9,20 +9,42 @@
class NeoChatRoom;
/**
* @class ActionsModel
*
* This class defines a model for chat actions.
*
* @note A chat action is a message starting with /, resulting in something other
* than a normal message being sent (e.g. /me, /join).
*/
class ActionsModel : public QAbstractListModel
{
public:
/**
* @brief Definition of an action.
*/
struct Action {
// The prefix, without '/' and space after the word
QString prefix;
QString prefix; /**< The prefix, without '/' and space after the word. */
/**
* @brief The function to execute when the action is triggered.
*/
std::function<QString(const QString &, NeoChatRoom *)> handle;
// If this is true, this action transforms a message to a different message and it will be sent.
// If this is false, this message does some action on the client and should not be sent as a message.
/**
* @brief Whether the action is a message type action.
*
* If this is true, a message action will be sent. If this is false, this
* message does some action on the client and should not be sent as a message.
*/
bool messageAction;
// If this action changes the message type, this is the new message type. Otherwise it's nullopt
/**
* @brief The new message type of a message being sent.
*
* For a non-message action or a message action that outputs the same type
* as its input, it's nullopt.
*/
std::optional<Quotient::RoomMessageEvent::MsgType> messageType = std::nullopt;
KLazyLocalizedString parameters;
KLazyLocalizedString description;
KLazyLocalizedString parameters; /**< The input parameters expected by the action. */
KLazyLocalizedString description; /**< The description of the action. */
};
static ActionsModel &instance()
{
@@ -30,18 +52,41 @@ public:
return _instance;
}
/**
* @brief Defines the model roles.
*/
enum Roles {
Prefix = Qt::DisplayRole,
Description,
CompletionType,
Parameters,
Prefix = Qt::DisplayRole, /**< The prefix, without '/' and space after the word. */
Description, /**< The description of the action. */
CompletionType, /**< The completion type (always "action" for this model). */
Parameters, /**< The input parameters expected by the action. */
};
Q_ENUM(Roles);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
QHash<int, QByteArray> roleNames() const override;
/**
* @brief Return a vector with all supported actions.
*/
QVector<Action> &allActions() const;
private:

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

@@ -6,26 +6,58 @@
#include "messageeventmodel.h"
#include <QSortFilterProxyModel>
/**
* @class CollapseStateProxyModel
*
* This model aggregates multiple sequential state events into a single entry.
*
* Events are only aggregated if they happened on the same day.
*
* @sa MessageEventModel
*/
class CollapseStateProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
AggregateDisplayRole = MessageEventModel::LastRole + 1,
StateEventsRole,
AuthorListRole,
AggregateDisplayRole = MessageEventModel::LastRole + 1, /**< Single line aggregation of all the state events. */
StateEventsRole, /**< List of state events in the aggregated state. */
AuthorListRole, /**< List of unique authors of the aggregated state event. */
};
/**
* @brief Whether a row should be shown out or not.
*
* @sa QSortFilterProxyModel::filterAcceptsRow
*/
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Get the given role value at the given index.
*
* @sa QSortFilterProxyModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief QString aggregating the text of consecutive state events starting at row.
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractProxyModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
/**
* @brief Aggregation of the text of consecutive state events starting at row.
*
* If state events happen on different days they will be split into two aggregate
* events.
*/
[[nodiscard]] QString aggregateEventToString(int row) const;
/**
* @brief Return a list of consecutive state events starting at row.
*
@@ -33,6 +65,7 @@ public:
* events.
*/
[[nodiscard]] QVariantList stateEventsList(int row) const;
/**
* @brief List of unique authors for the aggregate state events starting at row.
*/

View File

@@ -5,7 +5,6 @@
#include <QDebug>
#include "actionsmodel.h"
#include "chatdocumenthandler.h"
#include "completionproxymodel.h"
#include "customemojimodel.h"
#include "emojimodel.h"

View File

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

View File

@@ -2,9 +2,8 @@
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "completionproxymodel.h"
#include <QDebug>
#include "neochatroom.h"
#include <QDebug>
bool CompletionProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
@@ -44,7 +43,6 @@ int CompletionProxyModel::secondaryFilterRole() const
void CompletionProxyModel::setSecondaryFilterRole(int role)
{
m_secondaryFilterRole = role;
Q_EMIT secondaryFilterRoleChanged();
}
QString CompletionProxyModel::filterText() const
@@ -55,7 +53,6 @@ QString CompletionProxyModel::filterText() const
void CompletionProxyModel::setFilterText(const QString &filterText)
{
m_filterText = filterText;
Q_EMIT filterTextChanged();
}
void CompletionProxyModel::setFullText(const QString &fullText)

View File

@@ -5,28 +5,71 @@
#include <QSortFilterProxyModel>
/**
* @class CompletionProxyModel
*
* A filter model to sort and filter completion results.
*
* This model is designed to work with multiple source models depending upon the
* completion type.
*
* A model value will be shown if its primary or secondary role values start with
* the filter text. The exception is if the full text perfectly matches
* the primary filter role value in which case the completion ends (i.e. the filter
* will return no results).
*
* @note The filter is primarily design to work with strings, therefore make sure
* that the source model roles that are to be filtered are strings.
*/
class CompletionProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
Q_PROPERTY(int secondaryFilterRole READ secondaryFilterRole WRITE setSecondaryFilterRole NOTIFY secondaryFilterRoleChanged)
Q_PROPERTY(QString filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
public:
/**
* @brief Wether a row should be shown or not.
*
* @sa QSortFilterProxyModel::filterAcceptsRow
*/
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
/**
* @brief Returns true if the value of source_left is less than source_right.
*
* @sa QSortFilterProxyModel::lessThan
*/
bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
/**
* @brief Get the current secondary filter role.
*/
int secondaryFilterRole() const;
/**
* @brief Set the secondary filter role.
*
* Refer to the source model for what value corresponds to what role.
*/
void setSecondaryFilterRole(int role);
/**
* @brief Get the current text being used to filter the source model.
*/
QString filterText() const;
/**
* @brief Set the text to be used to filter the source model.
*/
void setFilterText(const QString &filterText);
/**
* @brief Set the full text in the chatbar after the completion start.
*
* This is used to automatically end the completion if the user replicated the
* primary filter role value perfectly.
*/
void setFullText(const QString &fullText);
Q_SIGNALS:
void secondaryFilterRoleChanged();
void filterTextChanged();
private:
int m_secondaryFilterRole = -1;
QString m_filterText;

View File

@@ -17,19 +17,29 @@ struct CustomEmoji {
Q_PROPERTY(QString name MEMBER name)
};
/**
* @class CustomEmojiModel
*
* This class defines the model for custom user emojis.
*
* This is based upon the im.ponies.user_emotes spec (MSC2545).
*/
class CustomEmojiModel : public QAbstractListModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
Name = Qt::DisplayRole,
ImageURL,
ModelData, // for emulating the regular emoji model's usage, otherwise the UI code would get too complicated
MxcUrl = 50,
DisplayRole = 51,
ReplacedTextRole = 52,
DescriptionRole = 53, // also invalid, reserved
Name = Qt::DisplayRole, /**< The name of the emoji. */
ImageURL, /**< The URL for the custom emoji. */
ModelData, /**< for emulating the regular emoji model's usage, otherwise the UI code would get too complicated. */
MxcUrl = 50, /**< The mxc source URL for the custom emoji. */
DisplayRole = 51, /**< The name of the emoji. For compatibility with EmojiModel. */
ReplacedTextRole = 52, /**< The name of the emoji. For compatibility with EmojiModel. */
DescriptionRole = 53, /**< Invalid, reserved. For compatibility with EmojiModel. */
};
Q_ENUM(Roles);
@@ -39,14 +49,45 @@ public:
return _instance;
}
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
QHash<int, QByteArray> roleNames() const override;
/**
* @brief Substitute any custom emojis for an image in the input text.
*/
Q_INVOKABLE QString preprocessText(const QString &it);
/**
* @brief Return a list of custom emojis where the name contains the filter text.
*/
Q_INVOKABLE QVariantList filterModel(const QString &filter);
/**
* @brief Add a new emoji to the model.
*/
Q_INVOKABLE void addEmoji(const QString &name, const QUrl &location);
/**
* @brief Remove an emoji from the model.
*/
Q_INVOKABLE void removeEmoji(const QString &name);
private:

View File

@@ -12,28 +12,67 @@ namespace Quotient
class Connection;
}
/**
* @class DevicesModel
*
* This class defines the model for managing the devices of the local user.
*
* A device is any session where the local user is logged into a client. This means
* the same physical device can have multiple sessions for example if the user uses
* multiple clients on the same machine.
*/
class DevicesModel : public QAbstractListModel
{
Q_OBJECT
/**
* @brief The current connection that the model is getting its devices from.
*/
Q_PROPERTY(Quotient::Connection *connection READ connection NOTIFY connectionChanged)
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
Id,
DisplayName,
LastIp,
LastTimestamp,
Id, /**< The device ID. */
DisplayName, /**< Display name set by the user for this device. */
LastIp, /**< The IP address where this device was last seen. */
LastTimestamp, /**< The timestamp when this devices was last seen. */
};
Q_ENUM(Roles);
DevicesModel(QObject *parent = nullptr);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
QVariant data(const QModelIndex &index, int role) const override;
QHash<int, QByteArray> roleNames() const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
QHash<int, QByteArray> roleNames() const override;
/**
* @brief Logout the device at the given index.
*/
Q_INVOKABLE void logout(int index, const QString &password);
/**
* @brief Set the display name of the device at the given index.
*/
Q_INVOKABLE void setName(int index, const QString &name);
Quotient::Connection *connection() const;

View File

@@ -51,12 +51,30 @@ struct Emoji {
Q_DECLARE_METATYPE(Emoji)
/**
* @class EmojiModel
*
* This class defines the model for visualising a list of emojis.
*/
class EmojiModel : public QAbstractListModel
{
Q_OBJECT
/**
* @brief Return a list of recently used emojis.
*/
Q_PROPERTY(QVariantList history READ history NOTIFY historyChanged)
/**
* @brief Return a list of emoji categories.
*
* @note No custom emoji categories will be included.
*/
Q_PROPERTY(QVariantList categories READ categories CONSTANT)
/**
* @brief Return a list of emoji categories with custom emojis.
*/
Q_PROPERTY(QVariantList categoriesWithCustom READ categoriesWithCustom CONSTANT)
public:
@@ -66,47 +84,92 @@ public:
return _instance;
}
/**
* @brief Defines the model roles.
*/
enum RoleNames {
ShortNameRole = Qt::DisplayRole,
UnicodeRole,
InvalidRole = 50,
DisplayRole = 51,
ReplacedTextRole = 52,
DescriptionRole = 53,
ShortNameRole = Qt::DisplayRole, /**< The name of the emoji. */
UnicodeRole, /**< The unicode character of the emoji. */
InvalidRole = 50, /**< Invalid, reserved. */
DisplayRole = 51, /**< The display text for an emoji. */
ReplacedTextRole = 52, /**< The text to replace the short name with (i.e. the unicode character). */
DescriptionRole = 53, /**< The long description of an emoji. */
};
Q_ENUM(RoleNames);
/**
* @brief Defines the potential categories an emoji can be placed in.
*/
enum Category {
Custom,
Search,
SearchNoCustom,
History,
HistoryNoCustom,
Smileys,
People,
Nature,
Food,
Activities,
Travel,
Objects,
Symbols,
Flags,
Component,
Custom, /**< A custom user emoji. */
Search, /**< The results of a filter. */
SearchNoCustom, /**< The results of a filter with no custom emojis. */
History, /**< Recently used emojis. */
HistoryNoCustom, /**< Recently used emojis with no custom emojis. */
Smileys, /**< Smileys & emotion emojis. */
People, /**< People & Body emojis. */
Nature, /**< Animals & Nature emojis. */
Food, /**< Food & Drink emojis. */
Activities, /**< Activities emojis. */
Travel, /**< Travel & Places emojis. */
Objects, /**< Objects emojis. */
Symbols, /**< Symbols emojis. */
Flags, /**< Flags emojis. */
Component, /**< ??? */
};
Q_ENUM(Category)
[[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 RoleNames, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE QVariantList history() const;
/**
* @brief Return a filtered list of emojis.
*
* @note This includes custom emojis, use filterModelNoCustom to return a result
* without custom emojis.
*
* @sa filterModelNoCustom
*/
Q_INVOKABLE static QVariantList filterModel(const QString &filter, bool limit = true);
/**
* @brief Return a filtered list of emojis without custom emojis.
*
* @note Use filterModel to return a result with custom emojis.
*
* @sa filterModel
*/
Q_INVOKABLE static QVariantList filterModelNoCustom(const QString &filter, bool limit = true);
/**
* @brief Return a list of emojis for the given category.
*/
Q_INVOKABLE QVariantList emojis(Category category) const;
/**
* @brief Return a list of emoji tones for the given base emoji.
*/
Q_INVOKABLE QVariantList tones(const QString &baseEmoji) const;
Q_INVOKABLE QVariantList history() const;
QVariantList categories() const;
QVariantList categoriesWithCustom() const;

View File

@@ -7,23 +7,54 @@
#include <QAbstractListModel>
/**
* @class KeywordNotificationRuleModel
*
* This class defines the model for managing notification push rule keywords.
*/
class KeywordNotificationRuleModel : public QAbstractListModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum EventRoles {
NameRole = Qt::DisplayRole,
NameRole = Qt::DisplayRole, /**< The push rule keyword. */
};
KeywordNotificationRuleModel(QObject *parent = nullptr);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Add a new keyword to the model.
*/
Q_INVOKABLE void addKeyword(const QString &keyword);
/**
* @brief Remove a keyword from the model.
*/
Q_INVOKABLE void removeKeywordAtIndex(int index);
private Q_SLOTS:

View File

@@ -1,102 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "locationsmodel.h"
using namespace Quotient;
LocationsModel::LocationsModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(this, &LocationsModel::roomChanged, this, [=]() {
for (const auto &event : m_room->messageEvents()) {
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"] == "m.location") {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
connect(m_room, &NeoChatRoom::aboutToAddHistoricalMessages, this, [=](const auto &events) {
for (const auto &event : events) {
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"] == "m.location") {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
});
connect(m_room, &NeoChatRoom::aboutToAddNewMessages, this, [=](const auto &events) {
for (const auto &event : events) {
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"] == "m.location") {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
});
});
}
void LocationsModel::addLocation(const RoomMessageEvent *event)
{
const auto uri = event->contentJson()["org.matrix.msc3488.location"]["uri"].toString();
const auto parts = uri.mid(4).split(QLatin1Char(','));
const auto latitude = parts[0].toFloat();
const auto longitude = parts[1].toFloat();
beginInsertRows(QModelIndex(), m_locations.size(), m_locations.size() + 1);
m_locations += LocationData{
.eventId = event->id(),
.latitude = latitude,
.longitude = longitude,
.text = event->contentJson()["body"].toString(),
.author = dynamic_cast<NeoChatUser *>(m_room->user(event->senderId())),
};
endInsertRows();
}
NeoChatRoom *LocationsModel::room() const
{
return m_room;
}
void LocationsModel::setRoom(NeoChatRoom *room)
{
if (m_room) {
disconnect(this, nullptr, m_room, nullptr);
}
m_room = room;
Q_EMIT roomChanged();
}
QHash<int, QByteArray> LocationsModel::roleNames() const
{
return {
{LongitudeRole, "longitude"},
{LatitudeRole, "latitude"},
{TextRole, "text"},
};
}
QVariant LocationsModel::data(const QModelIndex &index, int roleName) const
{
auto row = index.row();
if (roleName == LongitudeRole) {
return m_locations[row].longitude;
} else if (roleName == LatitudeRole) {
return m_locations[row].latitude;
} else if (roleName == TextRole) {
return m_locations[row].text;
}
return {};
}
int LocationsModel::rowCount(const QModelIndex &parent) const
{
return m_locations.size();
}

View File

@@ -1,49 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include "neochatroom.h"
#include <events/roommessageevent.h>
class LocationsModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
TextRole = Qt::DisplayRole,
LongitudeRole,
LatitudeRole,
};
Q_ENUM(Roles)
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
explicit LocationsModel(QObject *parent = nullptr);
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] QVariant data(const QModelIndex &index, int roleName) const override;
[[nodiscard]] int rowCount(const QModelIndex &parent) const override;
Q_SIGNALS:
void roomChanged();
private:
QPointer<NeoChatRoom> m_room;
struct LocationData {
QString eventId;
float latitude;
float longitude;
QString text;
NeoChatUser *author;
};
QList<LocationData> m_locations;
void addLocation(const Quotient::RoomMessageEvent *event);
};

View File

@@ -33,7 +33,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 +45,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 +66,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 +86,11 @@ MessageEventModel::MessageEventModel(QObject *parent)
MessageEventModel::~MessageEventModel() = default;
NeoChatRoom *MessageEventModel::room() const
{
return m_currentRoom;
}
void MessageEventModel::setRoom(NeoChatRoom *room)
{
if (room == m_currentRoom) {
@@ -314,7 +318,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 +449,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();
@@ -497,7 +501,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return evt.originalJson();
}
if (role == EventTypeRole) {
if (role == DelegateTypeRole) {
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
switch (e->msgtype()) {
case MessageEventType::Emote:
@@ -566,7 +570,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if(e->msgtype() == Quotient::MessageEventType::Location) {
if (e->msgtype() == Quotient::MessageEventType::Location) {
return e->contentJson();
}
// Cannot use e.contentJson() here because some
@@ -659,14 +663,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();
}
@@ -682,29 +678,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();
}
@@ -790,7 +768,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();
}
@@ -815,6 +793,32 @@ 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());
@@ -985,7 +989,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
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()) {
@@ -1027,7 +1031,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;
@@ -1037,14 +1041,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();
@@ -1066,7 +1070,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,75 +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,
Location,
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)
@@ -83,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

@@ -5,10 +5,21 @@
#include <QSortFilterProxyModel>
/**
* @class MessageFilterModel
*
* This model filters out any messages that should be hidden.
*
* Deleted messages are only hidden if the user hasn't set them to be shown.
*/
class MessageFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
MessageFilterModel(QObject *parent = nullptr);
/**
* @brief Custom filter function to remove hidden messages.
*/
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
};

View File

@@ -12,6 +12,11 @@ PublicRoomListModel::PublicRoomListModel(QObject *parent)
{
}
Quotient::Connection *PublicRoomListModel::connection() const
{
return m_connection;
}
void PublicRoomListModel::setConnection(Connection *conn)
{
if (m_connection == conn) {
@@ -47,6 +52,11 @@ void PublicRoomListModel::setConnection(Connection *conn)
Q_EMIT hasMoreChanged();
}
QString PublicRoomListModel::server() const
{
return m_server;
}
void PublicRoomListModel::setServer(const QString &value)
{
if (m_server == value) {
@@ -76,6 +86,11 @@ void PublicRoomListModel::setServer(const QString &value)
Q_EMIT hasMoreChanged();
}
QString PublicRoomListModel::keyword() const
{
return m_keyword;
}
void PublicRoomListModel::setKeyword(const QString &value)
{
if (m_keyword == value) {

View File

@@ -13,54 +13,96 @@ namespace Quotient
class Connection;
}
/**
* @class PublicRoomListModel
*
* This class defines the model for visualising a list of public rooms.
*
* The model finds the public rooms visible to the given server (which doesn't have
* to be the user's home server) and can also apply a filter if desired.
*
* Due to the fact that the public room list could be huge the model is lazily loaded
* and requires that the next batch of rooms be manually called.
*/
class PublicRoomListModel : public QAbstractListModel
{
Q_OBJECT
/**
* @brief The current connection that the model is getting its rooms from.
*/
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
/**
* @brief The server to get the public room list from.
*/
Q_PROPERTY(QString server READ server WRITE setServer NOTIFY serverChanged)
/**
* @brief The filter keyword for the list of public rooms.
*/
Q_PROPERTY(QString keyword READ keyword WRITE setKeyword NOTIFY keywordChanged)
/**
* @brief Whether the model has more items to load.
*/
Q_PROPERTY(bool hasMore READ hasMore NOTIFY hasMoreChanged)
public:
/**
* @brief Defines the model roles.
*/
enum EventRoles {
NameRole = Qt::DisplayRole + 1,
AvatarRole,
TopicRole,
RoomIDRole,
AliasRole,
MemberCountRole,
AllowGuestsRole,
WorldReadableRole,
IsJoinedRole,
NameRole = Qt::DisplayRole + 1, /**< The name of the room. */
AvatarRole, /**< The source URL for the room's avatar. */
TopicRole, /**< The room topic. */
RoomIDRole, /**< The room matrix ID. */
AliasRole, /**< The room canonical alias. */
MemberCountRole, /**< The number of members in the room. */
AllowGuestsRole, /**< Whether the room allows guest users. */
WorldReadableRole, /**< Whether the room events can be seen by non-members. */
IsJoinedRole, /**< Whether the local user has joined the room. */
};
PublicRoomListModel(QObject *parent = nullptr);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role = NameRole) 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;
[[nodiscard]] Quotient::Connection *connection() const
{
return m_connection;
}
[[nodiscard]] Quotient::Connection *connection() const;
void setConnection(Quotient::Connection *conn);
[[nodiscard]] QString server() const
{
return m_server;
}
[[nodiscard]] QString server() const;
void setServer(const QString &value);
[[nodiscard]] QString keyword() const
{
return m_keyword;
}
[[nodiscard]] QString keyword() const;
void setKeyword(const QString &value);
[[nodiscard]] bool hasMore() const;
/**
* @brief Load the next set of rooms.
*
* @param count the maximum number of rooms to load.
*/
Q_INVOKABLE void next(int count = 50);
private:

View File

@@ -10,11 +10,13 @@
#include "user.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>
@@ -29,15 +31,6 @@ 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 +39,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 +149,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);
refresh(room, {UnreadCountRole, NotificationCountRole, HighlightCountRole});
});
connect(room, &Room::notificationCountChanged, this, [this, room] {
refresh(room);
@@ -172,7 +167,7 @@ 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});

View File

@@ -217,6 +217,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

@@ -8,7 +8,6 @@
#include <QMimeDatabase>
#include <QTemporaryFile>
#include <QTextDocument>
#include <functional>
#include <QMediaMetaData>
#include <QMediaPlayer>
@@ -110,6 +109,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);
@@ -361,30 +388,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 +421,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()) {
@@ -468,7 +462,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;
@@ -604,8 +598,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");
@@ -791,30 +789,6 @@ void NeoChatRoom::changeAvatar(const QUrl &localFile)
}
}
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) {
@@ -1001,11 +975,6 @@ bool NeoChatRoom::isUserBanned(const QString &user) const
return getCurrentState<RoomMemberEvent>(user)->membership() == MembershipType::Ban;
}
QString NeoChatRoom::htmlSafeName() const
{
return name().toHtmlEscaped();
}
QString NeoChatRoom::htmlSafeDisplayName() const
{
return displayName().toHtmlEscaped();
@@ -1488,6 +1457,11 @@ bool NeoChatRoom::isSpace()
#endif
}
PushNotificationState::State NeoChatRoom::pushNotificationState() const
{
return m_currentPushNotificationState;
}
void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
{
// The caller should never try to set the state to unknown.

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,44 +715,40 @@ 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
*
* Note: this is different to setLocalAliases as that can only
* get the room to publish and alias that is already mapped.
*/
Q_INVOKABLE void mapAlias(const QString &alias);
Q_INVOKABLE void unmapAlias(const QString &alias);
Q_INVOKABLE void setCanonicalAlias(const QString &newAlias);
#ifdef QUOTIENT_07
/**
* @brief Get a PollHandler object for the given event Id.
*
* 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 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;
@@ -432,26 +827,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

@@ -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,78 +0,0 @@
// 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 QtLocation 5.15
import QtPositioning 5.15
import org.kde.kirigami 2.15 as Kirigami
ApplicationWindow {
id: root
required property var content
flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground
visibility: Qt.WindowFullScreen
title: i18n("View Location")
Shortcut {
sequence: "Escape"
onActivated: root.destroy()
}
color: Kirigami.Theme.backgroundColor
background: AbstractButton {
onClicked: root.destroy()
}
Map {
id: map
anchors.fill: parent
property string latlong: root.content.geo_uri.split(':')[1]
property string latitude: latlong.split(',')[0]
property string longitude: latlong.split(',')[1]
center: QtPositioning.coordinate(latitude, 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/"
}
}
MapCircle {
radius: 1500 / map.zoomLevel
color: Kirigami.Theme.highlightColor
border.color: Kirigami.Theme.linkColor
border.width: Kirigami.Units.devicePixelRatio * 2
smooth: true
opacity: 0.25
center: QtPositioning.coordinate(map.latitude, map.longitude)
}
onCopyrightLinkActivated: {
Qt.openUrlExternally(link)
}
}
Button {
anchors.top: parent.top
anchors.right: parent.right
text: i18n("Close")
icon.name: "dialog-close"
display: AbstractButton.IconOnly
width: Kirigami.Units.gridUnit * 2
height: Kirigami.Units.gridUnit * 2
onClicked: root.destroy()
}
}

View File

@@ -1,59 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtLocation 5.15
import QtPositioning 5.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.neochat 1.0
Kirigami.Page {
id: locationsPage
required property var room
title: i18nc("Locations on a map", "Locations")
padding: 0
Map {
id: map
anchors.fill: parent
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/"
}
}
MapItemView {
model: LocationsModel {
room: locationsPage.room
}
delegate: MapQuickItem {
id: point
required property var longitude
required property var latitude
required property string text
anchorPoint.x: icon.width / 2
anchorPoint.y: icon.height / 2
coordinate: QtPositioning.coordinate(point.latitude, point.longitude)
autoFadeIn: false
sourceItem: Kirigami.Icon {
id: icon
width: height
height: Kirigami.Units.iconSizes.medium
source: "flag-blue"
}
}
}
}
}

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 {

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

@@ -14,12 +14,6 @@ import org.kde.neochat 1.0
TimelineContainer {
id: locationDelegate
property string latlong: model.content.geo_uri.split(':')[1]
property string latitude: latlong.split(',')[0]
property string longitude: latlong.split(',')[1]
property string formattedBody: model.content.formatted_body
ColumnLayout {
Layout.maximumWidth: locationDelegate.contentMaxWidth
Layout.preferredWidth: locationDelegate.contentMaxWidth
@@ -28,7 +22,7 @@ TimelineContainer {
Layout.fillWidth: true
Layout.preferredHeight: locationDelegate.contentMaxWidth / 16 * 9
center: QtPositioning.coordinate(locationDelegate.latitude, locationDelegate.longitude)
center: QtPositioning.coordinate(model.latitude, model.longitude)
zoomLevel: 15
plugin: Plugin {
name: "osm"
@@ -41,34 +35,56 @@ TimelineContainer {
value: "https://autoconfig.kde.org/qtlocation/"
}
}
MapCircle {
radius: 1500 / map.zoomLevel
color: Kirigami.Theme.highlightColor
border.color: Kirigami.Theme.linkColor
border.width: Kirigami.Units.devicePixelRatio * 2
smooth: true
opacity: 0.25
center: QtPositioning.coordinate(latitude, longitude)
}
onCopyrightLinkActivated: {
Qt.openUrlExternally(link)
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
onTapped: {
let map = fullScreenMap.createObject(parent, {content: model.content});
map.open()
}
onLongPressed: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody ?? model.body, parent.selectedText)
onLongPressed: openMessageContext(model, "", model.message)
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody ?? model.body, parent.selectedText)
onTapped: openMessageContext(model, "", model.message)
}
}
Component {
id: fullScreenMap
FullScreenMap { }
}
}
}

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 {

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

@@ -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

@@ -1,505 +0,0 @@
// 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
Kirigami.ScrollablePage {
id: page
/**
* @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 bool collapsed: Config.collapsed
onCollapsedChanged: if (collapsed) {
sortFilterRoomListModel.filterText = "";
}
header: ColumnLayout {
visible: !page.collapsed
spacing: 0
ListView {
id: spaceList
property string activeSpaceId: ""
orientation: Qt.Horizontal
spacing: Kirigami.Units.smallSpacing
clip: true
visible: spaceList.count > 0
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
Layout.fillWidth: true
model: SortFilterSpaceListModel {
id: sortFilterSpaceListModel
sourceModel: RoomListModel {
id: spaceListModel
connection: Controller.activeConnection
}
}
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 = "";
spaceList.activeSpaceId = '';
listView.positionViewAtIndex(0, ListView.Beginning);
}
QQC2.ToolTip.text: homeButton.text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
delegate: QQC2.ItemDelegate {
required property string avatar
required property var currentRoom
required property int index
required property string id
height: parent.height
width: height
leftPadding: topPadding
rightPadding: topPadding
contentItem: Kirigami.Avatar {
name: currentRoom.displayName
source: avatar !== "" ? "image://mxc/" + avatar : ""
}
onClicked: {
spaceList.activeSpaceId = id;
sortFilterRoomListModel.activeSpaceId = id;
}
Accessible.name: currentRoom.displayName
QQC2.ToolTip.text: currentRoom.displayName
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onPressAndHold: {
spaceList.createContextMenu(currentRoom)
}
TapHandler {
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse
onTapped: spaceList.createContextMenu(currentRoom)
}
}
function createContextMenu(room) {
const menu = spaceListContextMenu.createObject(page, {room: room})
menu.open()
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
Component {
id: spaceListContextMenu
SpaceListContextMenu {}
}
}
property var enteredRoom
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: page.width - Kirigami.Units.largeSpacing
collapsed: page.collapsed
}
ListView {
id: listView
activeFocusOnTab: true
clip: AccountRegistry.count > 1
header: QQC2.ItemDelegate {
visible: page.collapsed
action: Kirigami.Action {
id: enterRoomAction
onTriggered: quickView.item.open();
}
topPadding: Kirigami.Units.largeSpacing
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
width: visible ? ListView.view.width : 0
height: visible ? Kirigami.Units.gridUnit * 2 : 0
Kirigami.Icon {
anchors.centerIn: parent
width: 22
height: 22
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: roomListModel
onCurrentChanged: {
listView.currentIndex = sortFilterRoomListModel.mapFromSource(current).row
}
}
model: SortFilterRoomListModel {
id: sortFilterRoomListModel
sourceModel: RoomListModel {
id: roomListModel
connection: Controller.activeConnection
}
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: page.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")
icon.width: Kirigami.Units.iconSizes.small
icon.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)
icon.width: Kirigami.Units.iconSizes.smallMedium
icon.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: page.collapsed ? collapsedModeListComponent : normalModeListComponent
Component {
id: collapsedModeListComponent
QQC2.ItemDelegate {
action: Kirigami.Action {
id: enterRoomAction
onTriggered: {
RoomManager.enterRoom(currentRoom);
}
}
Keys.onEnterPressed: enterRoomAction.trigger()
Keys.onReturnPressed: enterRoomAction.trigger()
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: model.categoryVisible || sortFilterRoomListModel.filterText.length > 0 || Config.mergeRoomList
contentItem: Kirigami.Avatar {
source: avatar ? "image://mxc/" + avatar : ""
name: model.name || i18n("No Name")
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
sourceSize.height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
}
QQC2.ToolTip {
enabled: text.length !== 0
text: name ?? ""
}
}
}
Component {
id: roomListContextMenu
RoomListContextMenu {}
}
Component {
id: normalModeListComponent
Kirigami.BasicListItem {
id: roomListItem
visible: model.categoryVisible || sortFilterRoomListModel.filterText.length > 0 || Config.mergeRoomList
topPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
highlighted: listView.currentIndex === index
focus: true
icon: undefined
action: Kirigami.Action {
id: enterRoomAction
onTriggered: {
RoomManager.enterRoom(currentRoom);
}
}
Keys.onEnterPressed: enterRoomAction.trigger()
Keys.onReturnPressed: enterRoomAction.trigger()
bold: unreadCount > 0
label: name ?? ""
labelItem.textFormat: Text.PlainText
subtitle: subtitleText
subtitleItem.textFormat: Text.PlainText
subtitleItem.visible: !Config.compactRoomList
onPressAndHold: {
createRoomListContextMenu()
}
TapHandler {
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse
onTapped: createRoomListContextMenu()
}
leading: Kirigami.Avatar {
source: avatar ? "image://mxc/" + avatar : ""
name: model.name || i18n("No Name")
implicitWidth: visible ? height : 0
visible: Config.showAvatarInRoomDrawer
sourceSize.width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
sourceSize.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
Layout.rightMargin: Kirigami.Units.smallSpacing
visible: currentRoom.pushNotificationState === PushNotificationState.Mute && !configButton.visible && unreadCount <= 0
Accessible.name: i18n("Muted room")
}
QQC2.Label {
id: notificationCountLabel
text: notificationCount > 0 ? notificationCount : "●"
visible: unreadCount > 0
color: Kirigami.Theme.textColor
Layout.rightMargin: Kirigami.Units.smallSpacing
Layout.minimumHeight: Kirigami.Units.iconSizes.smallMedium
Layout.minimumWidth: Math.max(notificationCountTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 2, height)
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
}
TextMetrics {
id: notificationCountTextMetrics
text: notificationCountLabel.text
}
}
QQC2.Button {
id: configButton
visible: roomListItem.hovered && !Kirigami.Settings.isMobile && !Config.compactRoomList
Accessible.name: i18n("Configure room")
action: Kirigami.Action {
id: optionAction
icon.name: "configure"
onTriggered: {
createRoomListContextMenu()
}
}
}
}
function createRoomListContextMenu() {
const menu = roomListContextMenu.createObject(page, {room: currentRoom})
if (!Kirigami.Settings.isMobile && !Config.compactRoomList) {
configButton.visible = true
configButton.down = true
}
menu.closed.connect(function() {
configButton.down = undefined
configButton.visible = Qt.binding(function() { return roomListItem.hovered && !Kirigami.Settings.isMobile && !Config.compactRoomList })
})
menu.open()
}
readonly property bool hasUnread: unreadCount > 0
}
}
}
footer: UserInfo {
width: parent.width
visible: !page.collapsed
}
MouseArea {
anchors.top: parent.top
anchors.bottom: parent.bottom
parent: applicationWindow().overlay.parent
x: page.currentWidth - width / 2
width: Kirigami.Units.smallSpacing * 2
z: page.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 + (page.contentItem.QQC2.ScrollBar.vertical.visible ? page.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

@@ -41,6 +41,7 @@ Kirigami.ScrollablePage {
} else if (page.currentRoom.isInvite) {
page.currentRoom.clearInvitationNotification();
}
currentRoom.markAllMessagesAsRead();
}
}
@@ -51,6 +52,7 @@ Kirigami.ScrollablePage {
if (!Kirigami.Settings.isMobile) {
chatBox.chatBar.forceActiveFocus();
}
currentRoom.markAllMessagesAsRead();
}
Connections {
@@ -263,7 +265,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 +478,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 +583,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 +644,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 +704,7 @@ Kirigami.ScrollablePage {
eventId: event.eventId,
formattedBody: event.formattedBody,
source: event.source,
eventType: event.eventType,
eventType: event.delegateType,
plainMessage: plainMessage,
});
contextMenu.open();

View File

@@ -157,24 +157,35 @@ Kirigami.OverlayDrawer {
}
}
TextEdit {
QQC2.ScrollView {
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.fillWidth: true
text: room && room.topic ? room.topic.replace(replaceLinks, "<a href=\"$1\">$1</a>") : i18n("No Topic")
readonly property var replaceLinks: /(http[s]?:\/\/[^ \r\n]*)/g
textFormat: TextEdit.MarkdownText
wrapMode: Text.WordWrap
selectByMouse: true
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
onLinkActivated: UrlHelper.openUrl(link)
readOnly: true
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
Layout.fillHeight: true
Layout.minimumHeight: Math.min(topicText.contentHeight, Kirigami.Units.gridUnit * 15)
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
QQC2.TextArea {
id: topicText
padding: 0
text: room && room.topic ? room.topic.replace(replaceLinks, "<a href=\"$1\">$1</a>") : i18n("No Topic")
readonly property var replaceLinks: /(http[s]?:\/\/[^ \r\n]*)/g
textFormat: TextEdit.MarkdownText
wrapMode: Text.WordWrap
selectByMouse: true
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
onLinkActivated: UrlHelper.openUrl(link)
readOnly: true
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
background: Item {}
}
}
@@ -209,18 +220,6 @@ Kirigami.OverlayDrawer {
})
}
}
Kirigami.BasicListItem {
id: locationsButton
icon: "map-flat"
text: i18n("Show locations for this room")
onClicked: pageStack.pushDialogLayer("qrc:/LocationsPage.qml", {
room: room
}, {
title: i18nc("Locations on a map", "Locations")
})
}
Kirigami.BasicListItem {
id: favouriteButton
@@ -242,6 +241,9 @@ Kirigami.OverlayDrawer {
QQC2.ToolTip.text: i18n("Search user in room")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onToggled: {
userListSearchField.text = "";
}
}
QQC2.ToolButton {
@@ -260,7 +262,7 @@ Kirigami.OverlayDrawer {
QQC2.Label {
Layout.alignment: Qt.AlignRight
text: room ? i18np("%1 Member", "%1 Members", room.joinedCount) : i18n("No Member Count")
text: room ? i18np("%1 member", "%1 members", room.joinedCount) : i18n("No member count")
}
}
@@ -282,6 +284,7 @@ Kirigami.OverlayDrawer {
QQC2.ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: Math.min(topicText.contentHeight, Kirigami.Units.gridUnit * 15)
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff

View File

@@ -9,6 +9,8 @@ import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import './RoomList' as RoomList
import './Dialog' as Dialog
Kirigami.ApplicationWindow {
id: root
@@ -24,7 +26,7 @@ Kirigami.ApplicationWindow {
pageStack.initialPage: LoadingPage {}
pageStack.globalToolBar.canContainHandles: true
property RoomListPage roomListPage
property RoomList.Page roomListPage
property bool roomListLoaded: false
property RoomPage roomPage
@@ -97,10 +99,6 @@ Kirigami.ApplicationWindow {
roomItem.forceActiveFocus();
}
function onPushWelcomePage() {
// TODO
}
function onOpenRoomInNewWindow(room) {
const secondaryWindow = roomWindow.createObject(undefined, {currentRoom: room});
secondaryWindow.width = root.width - pageStack.get(0).width;
@@ -167,7 +165,7 @@ Kirigami.ApplicationWindow {
pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar
pageStack.globalToolBar.showNavigationButtons: pageStack.currentIndex > 0 || pageStack.layers.depth > 1 ? Kirigami.ApplicationHeaderStyle.ShowBackButton : 0
ConfirmLogoutDialog {
Dialog.ConfirmLogout {
id: confirmLogoutDialog
}
@@ -200,7 +198,7 @@ Kirigami.ApplicationWindow {
Component {
id: roomListComponent
RoomListPage {
RoomList.Page {
id: roomList
Shortcut {

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