Compare commits

..

40 Commits

Author SHA1 Message Date
Heiko Becker
2c3ce1e88b GIT_SILENT Update Appstream for new release 2023-04-12 19:09:36 +02:00
Heiko Becker
33533a75bd GIT_SILENT Upgrade release service version to 23.04.0. 2023-04-12 17:54:51 +02:00
l10n daemon script
8c6671265c GIT_SILENT Sync po/docbooks with svn 2023-04-11 04:32:05 +00:00
l10n daemon script
bfa643557d GIT_SILENT Sync po/docbooks with svn 2023-04-10 03:18:44 +00:00
l10n daemon script
2b0f6a1d4d GIT_SILENT Sync po/docbooks with svn 2023-04-07 03:22:23 +00:00
l10n daemon script
0e8deee005 GIT_SILENT Sync po/docbooks with svn 2023-04-04 03:39:56 +00:00
Albert Astals Cid
fe60d11ba3 GIT_SILENT Upgrade release service version to 23.03.90. 2023-03-30 22:18:46 +02:00
l10n daemon script
e9d3862537 GIT_SILENT Sync po/docbooks with svn 2023-03-29 03:34:52 +00:00
l10n daemon script
e03b2e9adf GIT_SILENT Sync po/docbooks with svn 2023-03-28 04:36:27 +00:00
James Graham
566743bc19 Cherrypick Handle stripnewlines for plain text list to 23.04
Handle stripping new lines when the plain text input is a markdown list.


(cherry picked from commit a0ae8b28b2)
2023-03-27 13:30:27 +00:00
l10n daemon script
a3b4f05e2c GIT_SILENT Sync po/docbooks with svn 2023-03-27 05:40:33 +00:00
l10n daemon script
977e9bbf99 GIT_SILENT Sync po/docbooks with svn 2023-03-26 03:30:14 +00:00
l10n daemon script
ad11a509aa GIT_SILENT Sync po/docbooks with svn 2023-03-24 03:24:50 +00:00
Tobias Fella
abbe68f9ab Ensure that the room list subtitle is a single line
(cherry picked from commit 787dc5ab66)
2023-03-23 20:24:47 +00:00
James Graham
b7b0749cd8 Cherrypick Handle quotes in plain strings to 23.04
Unescape quotes in plain strings


(cherry picked from commit 925e20ebb8)
2023-03-22 18:12:50 +00:00
l10n daemon script
f8ae06f8a7 GIT_SILENT Sync po/docbooks with svn 2023-03-22 04:14:06 +00:00
l10n daemon script
9946ba008b GIT_SILENT Sync po/docbooks with svn 2023-03-21 03:51:57 +00:00
l10n daemon script
376916212d GIT_SILENT Sync po/docbooks with svn 2023-03-20 03:42:39 +00:00
James Graham
1ea6ffe416 Handle single quotes for links
Make sure that the text handler can handle links which use single or double quotes for the attributes.


(cherry picked from commit ee254a286d)
2023-03-19 19:11:52 +00:00
l10n daemon script
2d99ae0404 GIT_SILENT Sync po/docbooks with svn 2023-03-19 03:27:02 +00:00
l10n daemon script
88eeed74da GIT_SILENT Sync po/docbooks with svn 2023-03-18 03:57:25 +00:00
James Graham
0032d417ac Cherrypick Hide local edit message to 23.04
When a local message is edited make sure that the initial message that will be merged remains hidden so it doesn't flash up then disappear.

closes network/neochat#314


(cherry picked from commit 4e16b91f54)
2023-03-17 10:40:23 +00:00
l10n daemon script
bd9fb097e3 GIT_SILENT Sync po/docbooks with svn 2023-03-17 04:03:03 +00:00
James Graham
ecd7a5edff Cherrypick Text Handler Avoid accessing QString out of bounds to 23.04
Add end state to text handler and use to ensure that in nextTokenType to stop an out of bounds access to m_databuffer


(cherry picked from commit 0d6a83b063)
2023-03-16 17:32:58 +00:00
Carl Schwan
d3f0902835 Remove rogue console statement 2023-03-16 16:48:45 +00:00
l10n daemon script
6eb258f8a3 GIT_SILENT Sync po/docbooks with svn 2023-03-16 03:33:29 +00:00
l10n daemon script
dff908edd2 GIT_SILENT Sync po/docbooks with svn 2023-03-15 03:36:56 +00:00
Carl Schwan
7e06606cbd Don't focus chatbar on mobile
This popup the virtual keyboard and is quite distrubing on mobile.

Signed-off-by: Carl Schwan <carl@carlschwan.eu>


(cherry picked from commit bc84ad8d56)
2023-03-14 22:28:32 +00:00
James Graham
f78f92cd0f Newline Mobile
Make return/enter create a newline on mobile. This is the traditional behaviour in other messengers as the send icon and return have are the same time to access and solves the linked issue.

Closes network/neochat#466


(cherry picked from commit afe8a2a5e4)
2023-03-14 22:27:08 +00:00
Carl Schwan
c4fdfa22d9 Show back button when pushing the settings on mobile
To use with https://invent.kde.org/frameworks/kirigami/-/merge_requests/992

Signed-off-by: Carl Schwan <carl@carlschwan.eu>


(cherry picked from commit 87213dc9dd)
2023-03-14 22:24:55 +00:00
l10n daemon script
3e5d1429f7 GIT_SILENT Sync po/docbooks with svn 2023-03-14 03:39:03 +00:00
James Graham
76b5463dac Cherrypick Fix state event visiblity in timeline 23.04
Rework the filtering of state events in the timeline and for lastevent. This is now consistent everywhere and includes the following:
- The timeline settings are obeyed everywhere
- A new setting is added to filter all state events
- Last event obeys the timeline setting in all cases
- The roomlist will show a state event as the latest event if it's visible in the timeline
- Names are no longer hyperlinked in eventToString if plaintext is selected.

BUG: 455048\
Closes network/neochat#148


(cherry picked from commit 741cb57105)
2023-03-13 20:03:32 +00:00
James Graham
8b26a9f45f Cherrypick Room URL preview settings to 23.04
- Add the ability to set the deafult url preview setting for the room if you have sufficient power level.
- Add the ability for the user to set the desired url preview setting.

network/neochat#560


(cherry picked from commit 81c73037ca)
2023-03-13 18:45:18 +00:00
James Graham
da1c664f94 Cherrypick Improve Text Handling to 23.04
Improve the handling of text both when sending and receiving.

The main feature is to fix the linked bug (and a host of others that are unreported but similar) which is caused by the fact that we don't properly clean html. This mr does that as per the matrix spec https://spec.matrix.org/v1.5/client-server-api/#mroommessage-msgtypes. So any disallowed tags or attributes are removed and it does the special handling for certain attributes.

Additionally the functions are also designed to cover any other text formatting required, particularly fro received strings.

The receive side is covered by 2 functions `handleRecieveRichText` and `handleRecievePlainText`. The rich/plain in the function name refers to the output type not the input type (both can take plain and rich input), so `handleRecieveRichText` is called to get a string suitable to go in a rich text control and `handleRecievePlainText` for a plain control.

The functions also handle the following some of which was previously handled by `eventToString` in `NeoChatRoom`:
- Strip and reply from the string
- Format any user mentions
- Linkify links in plain strings
- Handle mxc urls in rich text (uses the new `room->makeMediaUrl` functionality from libQuotient)
- `handleRecievePlainText` also deals with markup making `NeoChatRoom->subtitle` redundant

There is also an extensive test suite which defines the behaviour and the best way to review this is probably to look at the tests and decide whether you agree with the expected output given the inputs and/or if there is any missing behaviour.

The final aim especially with the test suite is to give us a framework to make further updates in the future easier and hopefully prevent a new feature breaking old behaviour with the tests.

BUG: 463932 \
BUG: 466330 \
BUG: 466930


(cherry picked from commit f6ba4f2ecd)
2023-03-13 18:18:17 +00:00
James Graham
498cfedfea Cherrypick Other user read markers 23.04
Add the ability to see other user's read markers. 

![image](/uploads/a4bdc35a60ab10685c7d3ea4d9b13c87/image.png)

Implements network/neochat#133


(cherry picked from commit 23303c0483)
2023-03-13 16:01:20 +00:00
l10n daemon script
8983129520 GIT_SILENT Sync po/docbooks with svn 2023-03-13 03:53:42 +00:00
l10n daemon script
1c8acb2acd GIT_SILENT Sync po/docbooks with svn 2023-03-12 15:28:42 +00:00
l10n daemon script
53fa4be4d8 GIT_SILENT Sync po/docbooks with svn 2023-03-12 06:18:02 +00:00
l10n daemon script
bf30152e33 GIT_SILENT Sync po/docbooks with svn 2023-03-11 05:16:10 +00:00
Albert Astals Cid
d86c61ac8a GIT_SILENT Upgrade release service version to 23.03.80. 2023-03-10 21:01:32 +01:00
107 changed files with 19483 additions and 22537 deletions

1
.gitignore vendored
View File

@@ -10,4 +10,3 @@ 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

@@ -11,7 +11,6 @@ Dependencies:
'frameworks/kconfig': '@stable'
'frameworks/syntax-highlighting': '@stable'
'frameworks/kitemmodels': '@stable'
'frameworks/kquickcharts': '@stable'
'frameworks/knotifications': '@stable'
'libraries/kquickimageeditor': '@stable'
'frameworks/sonnet': '@stable'
@@ -39,7 +38,6 @@ Dependencies:
'frameworks/kconfig': '@latest-kf6'
'frameworks/syntax-highlighting': '@latest-kf6'
'frameworks/kitemmodels': '@latest-kf6'
'frameworks/kquickcharts': '@latest-kf6'
'frameworks/knotifications': '@latest-kf6'
'libraries/kquickimageeditor': '@latest-kf6'
'frameworks/sonnet': '@latest-kf6'

View File

@@ -8,8 +8,8 @@ cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "23")
set(RELEASE_SERVICE_VERSION_MINOR "07")
set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION_MINOR "04")
set(RELEASE_SERVICE_VERSION_MICRO "0")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
@@ -111,7 +111,6 @@ set_package_properties(cmark PROPERTIES
ecm_find_qmlmodule(org.kde.kquickimageeditor 1.0)
ecm_find_qmlmodule(org.kde.kitemmodels 1.0)
ecm_find_qmlmodule(org.kde.quickcharts 1.0)
find_package(KQuickImageEditor COMPONENTS)
set_package_properties(KQuickImageEditor PROPERTIES
@@ -131,12 +130,13 @@ set_package_properties(KF${QT_MAJOR_VERSION}DocTools PROPERTIES DESCRIPTION
TYPE OPTIONAL
)
find_package(Sqlite3)
if(NOT Quotient_VERSION_MINOR GREATER 6)
cmake_policy(SET CMP0063 OLD)
endif()
if(ANDROID)
find_package(Sqlite3)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
endif()

View File

@@ -6,7 +6,7 @@
#include "texthandler.h"
#include <qnamespace.h>
#include <connection.h>
#include <quotient_common.h>
#include <syncdata.h>
@@ -49,8 +49,7 @@ private Q_SLOTS:
void receiveStripReply();
void receivePlainTextIn();
void receiveRichInPlainOut_data();
void receiveRichInPlainOut();
void recieveRichInPlainOut();
void receivePlainStripHtml();
void receivePlainStripMarkup();
void receiveStripNewlines();
@@ -60,9 +59,6 @@ private Q_SLOTS:
void receiveRichtextIn();
void receiveRichMxcUrl();
void receiveRichPlainUrl();
void receiveRichEmote();
void receiveRichEdited_data();
void receiveRichEdited();
};
#ifdef QUOTIENT_07
@@ -150,44 +146,7 @@ void TextHandlerTest::initTestCase()
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "/me This is an emote.",
"format": "org.matrix.custom.html",
"formatted_body": "This is an emote.",
"msgtype": "m.emote"
},
"event_id": "$153273582443PhrSn:example.org",
"origin_server_ts": 1532735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1231
}
},
{
"content": {
"body": "tested",
"msgtype": "m.text"
},
"event_id": "$zrCiBxBnqqTn0Z5FY78qSZAszno_w8nJJXzfBULG-3E",
"origin_server_ts": 1680948575928,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1747776,
"m.relations": {
"m.replace": {
"event_id": "$UX0PlpyI7vYO32iHMuuYEP7ECMh4sX3XLGiB2SwM4mQ",
"origin_server_ts": 1680948580992,
"sender": "@example:example.org"
}
}
"age": 1235
}
}
],
@@ -350,24 +309,15 @@ void TextHandlerTest::receiveStripReply()
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString);
}
void TextHandlerTest::receiveRichInPlainOut_data()
void TextHandlerTest::recieveRichInPlainOut()
{
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);
const QString testInputString = QStringLiteral("a &amp; b");
const QString testOutputString = QStringLiteral("a & b");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testOutputString);
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString);
}
void TextHandlerTest::receivePlainTextIn()
@@ -489,7 +439,7 @@ void TextHandlerTest::receiveRichMxcUrl()
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(0).get()), testOutputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().back().get()), testOutputString);
}
#endif
@@ -548,44 +498,5 @@ void TextHandlerTest::receiveRichPlainUrl()
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxId);
}
// Test that user pill is add to an emote message.
// N.B. The second message in the test timeline is marked as an emote.
void TextHandlerTest::receiveRichEmote()
{
auto event = room->messageEvents().at(1).get();
auto author = static_cast<NeoChatUser *>(room->user(event->senderId()));
const QString testInputString = QStringLiteral("This is an emote.");
const QString testOutputString = QStringLiteral("* <a href=\"https://matrix.to/#/@example:example.org\" style=\"color:") + author->color().name()
+ QStringLiteral("\">@example:example.org</a> This is an emote.");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, event), testOutputString);
}
void TextHandlerTest::receiveRichEdited_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::newRow("basic") << QStringLiteral("Edited") << QStringLiteral("Edited <span style=\"color:#000000\">(edited)</span>");
QTest::newRow("multiple paragraphs") << QStringLiteral("<p>Edited</p>\n<p>Edited</p>")
<< QStringLiteral("<p>Edited</p>\n<p>Edited <span style=\"color:#000000\">(edited)</span></p>");
QTest::newRow("blockquote") << QStringLiteral("<blockquote>Edited</blockquote>")
<< QStringLiteral("<blockquote>Edited</blockquote><p> <span style=\"color:#000000\">(edited)</span></p>");
}
void TextHandlerTest::receiveRichEdited()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(2).get()), testOutputString);
}
QTEST_MAIN(TextHandlerTest)
#include "texthandlertest.moc"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: neochat\n"
"Report-Msgid-Bugs-To: https://bugs.kde.org\n"
"POT-Creation-Date: 2023-03-31 00:47+0000\n"
"POT-Creation-Date: 2023-03-24 02:39+0000\n"
"PO-Revision-Date: 2021-12-15 11:17+0100\n"
"Last-Translator: Øystein Steffensen-Alværvik <oysteins.omsetting@protonmail."
"com>\n"
@@ -125,90 +125,90 @@ msgstr ""
msgid "Network Error"
msgstr "Nettverksfeil"
#: src/main.cpp:153
#: src/main.cpp:154
#, kde-format
msgid "NeoChat"
msgstr "NeoChat"
#: src/main.cpp:155
#: src/main.cpp:156
#, kde-format
msgid "Matrix client"
msgstr "Matrix-klient"
#: src/main.cpp:157
#: src/main.cpp:158
#, kde-format
msgid "© 2018-2020 Black Hat, 2020-2022 KDE Community"
msgstr ""
#: src/main.cpp:158
#: src/main.cpp:159
#, kde-format
msgid "Carl Schwan"
msgstr "Carl Schwan"
#: src/main.cpp:158 src/main.cpp:159 src/main.cpp:160
#: src/main.cpp:159 src/main.cpp:160 src/main.cpp:161
#, kde-format
msgid "Maintainer"
msgstr ""
#: src/main.cpp:159
#: src/main.cpp:160
#, kde-format
msgid "Tobias Fella"
msgstr "Tobias Fella"
#: src/main.cpp:160
#: src/main.cpp:161
#, kde-format
msgid "James Graham"
msgstr ""
#: src/main.cpp:161
#: src/main.cpp:162
#, kde-format
msgid "Black Hat"
msgstr "Black Hat"
#: src/main.cpp:161
#: src/main.cpp:162
#, kde-format
msgid "Original author of Spectral"
msgstr ""
#: src/main.cpp:162
#: src/main.cpp:163
#, kde-format
msgid "Alexey Rusakov"
msgstr ""
#: src/main.cpp:162
#: src/main.cpp:163
#, kde-format
msgid "Maintainer of Quotient"
msgstr ""
#: src/main.cpp:163
#: src/main.cpp:164
#, kde-format
msgctxt "NAME OF TRANSLATORS"
msgid "Your names"
msgstr "Karl Ove Hufthammer"
#: src/main.cpp:163
#: src/main.cpp:164
#, kde-format
msgctxt "EMAIL OF TRANSLATORS"
msgid "Your emails"
msgstr "karl@huftis.org"
#: src/main.cpp:167
#: src/main.cpp:168
#, kde-format
msgid "A Qt5 library to write cross-platform clients for Matrix"
msgstr ""
#: src/main.cpp:170
#: src/main.cpp:171
#, kde-format
msgctxt "<version number> (built against <possibly different version number>)"
msgid "%1 (built against %2)"
msgstr ""
#: src/main.cpp:306
#: src/main.cpp:307
#, kde-format
msgid "Client for the matrix communication protocol"
msgstr "Lynmeldings­klient for Matrix-protokollen"
#: src/main.cpp:307
#: src/main.cpp:308
#, kde-format
msgid "Supports matrix: url scheme"
msgstr ""
@@ -631,50 +631,50 @@ msgctxt "'Custom' is a category of emoji"
msgid "Custom"
msgstr ""
#: src/models/messageeventmodel.cpp:362 src/models/searchmodel.cpp:156
#: src/models/messageeventmodel.cpp:359 src/models/searchmodel.cpp:156
#, kde-format
msgid "Today"
msgstr "I dag"
#: src/models/messageeventmodel.cpp:365 src/models/searchmodel.cpp:159
#: src/models/messageeventmodel.cpp:362 src/models/searchmodel.cpp:159
#, kde-format
msgid "Yesterday"
msgstr "I går"
#: src/models/messageeventmodel.cpp:368 src/models/searchmodel.cpp:162
#: src/models/messageeventmodel.cpp:365 src/models/searchmodel.cpp:162
#, kde-format
msgid "The day before yesterday"
msgstr "I forgårs"
#: src/models/messageeventmodel.cpp:470 src/models/messageeventmodel.cpp:479
#: src/models/messageeventmodel.cpp:467 src/models/messageeventmodel.cpp:476
#, kde-format
msgid "<i>[This message was deleted]</i>"
msgstr "<i>[Denne meldinga er sletta]</i>"
#: src/models/messageeventmodel.cpp:471
#: src/models/messageeventmodel.cpp:468
#, kde-format
msgid "<i>[This message was deleted: %1]</i>"
msgstr "<i>[Denne meldinga er sletta: %1]</i>"
# Eller «SENSURERT»?
#: src/models/messageeventmodel.cpp:568
#: src/models/messageeventmodel.cpp:563
#, kde-format
msgid "[REDACTED]"
msgstr "[TREKT TILBAKE]"
#: src/models/messageeventmodel.cpp:568
#: src/models/messageeventmodel.cpp:563
#, kde-format
msgid "[REDACTED: %1]"
msgstr "[TREKT TILBAKE: %1]"
#: src/models/messageeventmodel.cpp:882
#: src/models/messageeventmodel.cpp:848
#, kde-format
msgid "1 user: "
msgid_plural "%1 users: "
msgstr[0] ""
msgstr[1] ""
#: src/models/messageeventmodel.cpp:889
#: src/models/messageeventmodel.cpp:855
#, kde-format
msgctxt "list separator"
msgid ", "
@@ -1138,7 +1138,7 @@ msgstr ""
msgid "Send a message…"
msgstr ""
#: src/qml/Component/ChatBox/ChatBar.qml:308
#: src/qml/Component/ChatBox/ChatBar.qml:273
#, kde-format
msgctxt "@action:button"
msgid "Cancel reply"
@@ -1149,36 +1149,6 @@ msgstr ""
msgid "NeoChat is offline. Please check your network connection."
msgstr ""
#: src/qml/Component/ChatBox/QuickFormatBar.qml:23
#, kde-format
msgid "Bold"
msgstr ""
#: src/qml/Component/ChatBox/QuickFormatBar.qml:42
#, kde-format
msgid "Italic"
msgstr ""
#: src/qml/Component/ChatBox/QuickFormatBar.qml:61
#, kde-format
msgid "Strikethrough"
msgstr ""
#: src/qml/Component/ChatBox/QuickFormatBar.qml:80
#, kde-format
msgid "Code block"
msgstr ""
#: src/qml/Component/ChatBox/QuickFormatBar.qml:99
#, kde-format
msgid "Quote"
msgstr ""
#: src/qml/Component/ChatBox/QuickFormatBar.qml:118
#, kde-format
msgid "Insert link"
msgstr ""
#: src/qml/Component/ChatBox/ReplyPane.qml:32
#, kde-format
msgid "Replying to:"
@@ -1189,7 +1159,7 @@ msgstr ""
msgid "No emojis"
msgstr ""
#: src/qml/Component/ExploreComponent.qml:19 src/qml/Page/RoomListPage.qml:221
#: src/qml/Component/ExploreComponent.qml:19 src/qml/Page/RoomListPage.qml:224
#, kde-format
msgid "Explore rooms"
msgstr "Utforsk rom"
@@ -1500,34 +1470,32 @@ msgctxt "@title:window"
msgid "Login"
msgstr ""
#: src/qml/Component/UserInfo.qml:145 src/qml/Menu/AccountMenu.qml:17
#: src/qml/Component/UserInfo.qml:145
#, kde-format
msgid "Edit this account"
msgstr "Rediger kontoen"
#: src/qml/Component/UserInfo.qml:150 src/qml/Menu/AccountMenu.qml:22
#: src/qml/Settings/AccountsPage.qml:36
#: src/qml/Component/UserInfo.qml:150 src/qml/Settings/AccountsPage.qml:36
#, kde-format
msgid "Account editor"
msgstr ""
#: src/qml/Component/UserInfo.qml:183
#: src/qml/Component/UserInfo.qml:178
#, kde-format
msgid "Switch User"
msgstr ""
#: src/qml/Component/UserInfo.qml:199 src/qml/Page/InviteUserPage.qml:42
#: src/qml/Component/UserInfo.qml:194 src/qml/Page/InviteUserPage.qml:42
#, kde-format
msgid "Add"
msgstr "Legg til"
#: src/qml/Component/UserInfo.qml:210 src/qml/Menu/AccountMenu.qml:28
#: src/qml/Menu/AccountMenu.qml:33 src/qml/Menu/GlobalMenu.qml:28
#: src/qml/Component/UserInfo.qml:205 src/qml/Menu/GlobalMenu.qml:28
#, kde-format
msgid "Configure"
msgstr ""
#: src/qml/Component/UserInfo.qml:211
#: src/qml/Component/UserInfo.qml:206
#, kde-format
msgid "Open Settings"
msgstr ""
@@ -1925,22 +1893,6 @@ msgstr ""
msgid "Do you want to start a chat with %1?"
msgstr ""
#: src/qml/Menu/AccountMenu.qml:26
#, kde-format
msgid "Notification settings"
msgstr ""
#: src/qml/Menu/AccountMenu.qml:31 src/qml/Settings/DevicesPage.qml:14
#: src/qml/Settings/DevicesPage.qml:26 src/qml/Settings/SettingsPage.qml:56
#, kde-format
msgid "Devices"
msgstr "Einingar"
#: src/qml/Menu/AccountMenu.qml:36
#, kde-format
msgid "Logout"
msgstr "Logg ut"
#: src/qml/Menu/EditMenu.qml:17
#, kde-format
msgctxt "text editing menu action"
@@ -2504,38 +2456,38 @@ msgstr ""
msgid "Joined"
msgstr "Vart med"
#: src/qml/Page/RoomListPage.qml:55
#: src/qml/Page/RoomListPage.qml:58
#, kde-format
msgctxt "@action:button"
msgid "Show All Rooms"
msgstr ""
#: src/qml/Page/RoomListPage.qml:218
#: src/qml/Page/RoomListPage.qml:221
#, kde-format
msgid "No rooms found"
msgstr "Fann ingen rom"
#: src/qml/Page/RoomListPage.qml:218
#: src/qml/Page/RoomListPage.qml:221
#, kde-format
msgid "Join some rooms to get started"
msgstr "Start ved å verta med i nokre rom"
#: src/qml/Page/RoomListPage.qml:221
#: src/qml/Page/RoomListPage.qml:224
#, kde-format
msgid "Search in room directory"
msgstr "Søk i romkatalogen"
#: src/qml/Page/RoomListPage.qml:319 src/qml/Page/RoomListPage.qml:371
#: src/qml/Page/RoomListPage.qml:322 src/qml/Page/RoomListPage.qml:374
#, kde-format
msgid "No Name"
msgstr "Namnlaus"
#: src/qml/Page/RoomListPage.qml:386
#: src/qml/Page/RoomListPage.qml:389
#, kde-format
msgid "Muted room"
msgstr ""
#: src/qml/Page/RoomListPage.qml:413
#: src/qml/Page/RoomListPage.qml:416
#, kde-format
msgid "Configure room"
msgstr "Set opp rommet"
@@ -2707,23 +2659,23 @@ msgstr[1] "%1 medlemmar"
msgid "No Member Count"
msgstr "Manglar medlemstal"
#: src/qml/RoomSettings/Categories.qml:16 src/qml/RoomSettings/General.qml:20
#: src/qml/Settings/SettingsPage.qml:13
#: src/qml/RoomSettings/Categories.qml:15 src/qml/RoomSettings/General.qml:20
#: src/qml/Settings/SettingsPage.qml:12
#, kde-format
msgid "General"
msgstr ""
#: src/qml/RoomSettings/Categories.qml:27 src/qml/RoomSettings/Security.qml:19
#: src/qml/RoomSettings/Categories.qml:25 src/qml/RoomSettings/Security.qml:19
#, kde-format
msgid "Security"
msgstr ""
#: src/qml/RoomSettings/Categories.qml:38
#: src/qml/RoomSettings/Categories.qml:35
#, kde-format
msgid "Permissions"
msgstr ""
#: src/qml/RoomSettings/Categories.qml:49 src/qml/Settings/SettingsPage.qml:25
#: src/qml/RoomSettings/Categories.qml:45 src/qml/Settings/SettingsPage.qml:22
#, kde-format
msgid "Notifications"
msgstr ""
@@ -3224,7 +3176,7 @@ msgid "Homeserver url"
msgstr ""
#: src/qml/Settings/AccountsPage.qml:15 src/qml/Settings/AccountsPage.qml:26
#: src/qml/Settings/SettingsPage.qml:31
#: src/qml/Settings/SettingsPage.qml:27
#, kde-format
msgid "Accounts"
msgstr "Kontoar"
@@ -3320,6 +3272,12 @@ msgstr ""
msgid "Color theme"
msgstr ""
#: src/qml/Settings/DevicesPage.qml:14 src/qml/Settings/DevicesPage.qml:26
#: src/qml/Settings/SettingsPage.qml:48
#, kde-format
msgid "Devices"
msgstr "Einingar"
#: src/qml/Settings/DevicesPage.qml:86
#, kde-format
msgid "New device name"
@@ -3586,7 +3544,7 @@ msgid "Call invitation"
msgstr ""
#: src/qml/Settings/NetworkProxyPage.qml:27
#: src/qml/Settings/SettingsPage.qml:50
#: src/qml/Settings/SettingsPage.qml:43
#, kde-format
msgid "Network Proxy"
msgstr ""
@@ -3667,27 +3625,27 @@ msgstr ""
msgid "Delete keyword"
msgstr ""
#: src/qml/Settings/SettingsPage.qml:19
#: src/qml/Settings/SettingsPage.qml:17
#, kde-format
msgid "Appearance"
msgstr ""
#: src/qml/Settings/SettingsPage.qml:37
#: src/qml/Settings/SettingsPage.qml:32
#, kde-format
msgid "Custom Emojis"
msgstr ""
#: src/qml/Settings/SettingsPage.qml:43
#: src/qml/Settings/SettingsPage.qml:37
#, kde-format
msgid "Spell Checking"
msgstr ""
#: src/qml/Settings/SettingsPage.qml:62
#: src/qml/Settings/SettingsPage.qml:53
#, kde-format
msgid "About NeoChat"
msgstr "Om Neochat"
#: src/qml/Settings/SettingsPage.qml:68
#: src/qml/Settings/SettingsPage.qml:58
#, kde-format
msgid "About KDE"
msgstr ""

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

@@ -175,10 +175,6 @@ if(ANDROID)
"window-new"
"globe"
"visibility"
"home"
"preferences-desktop-notification"
"computer-symbolic"
"gps"
)
else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF${QT_MAJOR_VERSION}::KIOWidgets)

View File

@@ -138,13 +138,12 @@ int ChatDocumentHandler::completionStartIndex() const
return 0;
}
#if !defined(Q_OS_ANDROID) && QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
#if 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(' ')) {
@@ -325,35 +324,3 @@ 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,9 +35,6 @@ 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);
@@ -63,13 +60,6 @@ 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();
@@ -78,8 +68,6 @@ Q_SIGNALS:
void completionModelChanged();
void selectionStartChanged();
void selectionEndChanged();
void errorColorChanged();
void mentionColorChanged();
private:
int completionStartIndex() const;
@@ -91,9 +79,6 @@ 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

@@ -44,6 +44,7 @@
#include "clipboard.h"
#include "controller.h"
#include "filetypesingleton.h"
#include "joinrulesevent.h"
#include "linkpreviewer.h"
#include "login.h"
#include "matriximageprovider.h"

View File

@@ -5,6 +5,7 @@
#include "controller.h"
#include "neochatroom.h"
#include "neochatuser.h"
#include "roommanager.h"
#include <events/roommemberevent.h>
#include <events/roompowerlevelsevent.h>
@@ -18,41 +19,6 @@ 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"),
@@ -302,7 +268,31 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("part"),
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();
},
false,
std::nullopt,
kli18n("[<room alias or id>]"),
@@ -310,7 +300,31 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("leave"),
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();
},
false,
std::nullopt,
kli18n("[<room alias or id>]"),
@@ -333,15 +347,14 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("roomnick"),
roomNickLambda,
false,
std::nullopt,
kli18n("<display name>"),
kli18n("Changes your display name in this room"),
},
Action{
QStringLiteral("myroomnick"),
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();
},
false,
std::nullopt,
kli18n("<display name>"),

View File

@@ -9,42 +9,20 @@
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 {
QString prefix; /**< The prefix, without '/' and space after the word. */
/**
* @brief The function to execute when the action is triggered.
*/
// The prefix, without '/' and space after the word
QString prefix;
std::function<QString(const QString &, NeoChatRoom *)> handle;
/**
* @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.
*/
// 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.
bool messageAction;
/**
* @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.
*/
// If this action changes the message type, this is the new message type. Otherwise it's nullopt
std::optional<Quotient::RoomMessageEvent::MsgType> messageType = std::nullopt;
KLazyLocalizedString parameters; /**< The input parameters expected by the action. */
KLazyLocalizedString description; /**< The description of the action. */
KLazyLocalizedString parameters;
KLazyLocalizedString description;
};
static ActionsModel &instance()
{
@@ -52,41 +30,18 @@ public:
return _instance;
}
/**
* @brief Defines the model roles.
*/
enum Roles {
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. */
Prefix = Qt::DisplayRole,
Description,
CompletionType,
Parameters,
};
Q_ENUM(Roles);
/**
* @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()
*/
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
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::DelegateTypeRole)
return sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::EventTypeRole)
!= MessageEventModel::DelegateType::State // If this is not a state, show it
|| sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::DelegateTypeRole)
|| sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::EventTypeRole)
!= 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::DelegateTypeRole)
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole)
!= 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::DelegateTypeRole)
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole)
!= 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::DelegateTypeRole)
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole)
!= 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,58 +6,26 @@
#include "messageeventmodel.h"
#include <QSortFilterProxyModel>
/**
* @class CollapseStateProxyModel
*
* This model aggregates multiple sequential state events into a single entry.
*
* Events are only aggregated if they happened on the same day.
*
* @sa MessageEventModel
*/
class CollapseStateProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
AggregateDisplayRole = MessageEventModel::LastRole + 1, /**< Single line aggregation of all the state events. */
StateEventsRole, /**< List of state events in the aggregated state. */
AuthorListRole, /**< List of unique authors of the aggregated state event. */
AggregateDisplayRole = MessageEventModel::LastRole + 1,
StateEventsRole,
AuthorListRole,
};
/**
* @brief Whether a row should be shown out or not.
*
* @sa QSortFilterProxyModel::filterAcceptsRow
*/
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
/**
* @brief Get the given role value at the given index.
*
* @sa QSortFilterProxyModel::data
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractProxyModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
/**
* @brief Aggregation of the text of consecutive state events starting at row.
* @brief QString aggregating 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.
*
@@ -65,7 +33,6 @@ private:
* events.
*/
[[nodiscard]] QVariantList stateEventsList(int row) const;
/**
* @brief List of unique authors for the aggregate state events starting at row.
*/

View File

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

View File

@@ -13,89 +13,41 @@ 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, /**< A user in the current room. */
Room, /**< A matrix room. */
Emoji, /**< An emoji. */
Command, /**< A / command. */
None, /**< No available completion for the current text. */
User,
Room,
Emoji,
Command,
None,
};
Q_ENUM(AutoCompletionType)
/**
* @brief Defines the model roles.
*/
enum Roles {
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. */
Text = Qt::DisplayRole,
Subtitle,
Icon,
ReplacedText,
};
Q_ENUM(Roles);
CompletionModel(QObject *parent = nullptr);
/**
* @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()
*/
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
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);
@@ -104,7 +56,6 @@ public:
void setRoomListModel(RoomListModel *roomListModel);
AutoCompletionType autoCompletionType() const;
void setAutoCompletionType(AutoCompletionType autoCompletionType);
Q_SIGNALS:
void textChanged();
@@ -119,7 +70,7 @@ private:
NeoChatRoom *m_room = nullptr;
AutoCompletionType m_autoCompletionType = None;
void updateCompletion();
void setAutoCompletionType(AutoCompletionType autoCompletionType);
UserListModel *m_userListModel;
RoomListModel *m_roomListModel;

View File

@@ -2,9 +2,10 @@
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "completionproxymodel.h"
#include <QDebug>
#include "neochatroom.h"
bool CompletionProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
Q_UNUSED(sourceParent);
@@ -43,6 +44,7 @@ int CompletionProxyModel::secondaryFilterRole() const
void CompletionProxyModel::setSecondaryFilterRole(int role)
{
m_secondaryFilterRole = role;
Q_EMIT secondaryFilterRoleChanged();
}
QString CompletionProxyModel::filterText() const
@@ -53,6 +55,7 @@ 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,71 +5,28 @@
#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,29 +17,19 @@ 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, /**< 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. */
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
};
Q_ENUM(Roles);
@@ -49,45 +39,14 @@ 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,67 +12,28 @@ 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, /**< 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. */
Id,
DisplayName,
LastIp,
LastTimestamp,
};
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;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
QHash<int, QByteArray> roleNames() const override;
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,30 +51,12 @@ 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:
@@ -84,92 +66,47 @@ public:
return _instance;
}
/**
* @brief Defines the model roles.
*/
enum RoleNames {
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. */
ShortNameRole = Qt::DisplayRole,
UnicodeRole,
InvalidRole = 50,
DisplayRole = 51,
ReplacedTextRole = 52,
DescriptionRole = 53,
};
Q_ENUM(RoleNames);
/**
* @brief Defines the potential categories an emoji can be placed in.
*/
enum Category {
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, /**< ??? */
Custom,
Search,
SearchNoCustom,
History,
HistoryNoCustom,
Smileys,
People,
Nature,
Food,
Activities,
Travel,
Objects,
Symbols,
Flags,
Component,
};
Q_ENUM(Category)
/**
* @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]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @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 QVariantList history() const;
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,54 +7,23 @@
#include <QAbstractListModel>
/**
* @class KeywordNotificationRuleModel
*
* This class defines the model for managing notification push rule keywords.
*/
class KeywordNotificationRuleModel : public QAbstractListModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum EventRoles {
NameRole = Qt::DisplayRole, /**< The push rule keyword. */
NameRole = Qt::DisplayRole,
};
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

@@ -33,7 +33,7 @@ using namespace Quotient;
QHash<int, QByteArray> MessageEventModel::roleNames() const
{
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
roles[DelegateTypeRole] = "delegateType";
roles[EventTypeRole] = "eventType";
roles[MessageRole] = "message";
roles[EventIdRole] = "eventId";
roles[TimeRole] = "time";
@@ -45,16 +45,19 @@ 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";
@@ -66,14 +69,12 @@ 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);
@@ -86,11 +87,6 @@ MessageEventModel::MessageEventModel(QObject *parent)
MessageEventModel::~MessageEventModel() = default;
NeoChatRoom *MessageEventModel::room() const
{
return m_currentRoom;
}
void MessageEventModel::setRoom(NeoChatRoom *room)
{
if (room == m_currentRoom) {
@@ -318,7 +314,7 @@ int MessageEventModel::refreshEventRoles(const QString &id, const QVector<int> &
return -1;
}
row = int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex();
if (data(index(row, 0), DelegateTypeRole).toInt() == ReadMarker || data(index(row, 0), DelegateTypeRole).toInt() == Other) {
if (data(index(row, 0), EventTypeRole).toInt() == ReadMarker || data(index(row, 0), EventTypeRole).toInt() == Other) {
row++;
}
}
@@ -449,7 +445,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
if (m_lastReadEventIndex.row() == row) {
switch (role) {
case DelegateTypeRole:
case EventTypeRole:
return DelegateType::ReadMarker;
case TimeRole: {
const QDateTime eventDate = data(index(m_lastReadEventIndex.row() + 1, 0), TimeRole).toDateTime().toLocalTime();
@@ -501,7 +497,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return evt.originalJson();
}
if (role == DelegateTypeRole) {
if (role == EventTypeRole) {
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
switch (e->msgtype()) {
case MessageEventType::Emote:
@@ -514,8 +510,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return DelegateType::Audio;
case MessageEventType::Video:
return DelegateType::Video;
case MessageEventType::Location:
return DelegateType::Location;
default:
break;
}
@@ -570,9 +564,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if (e->msgtype() == Quotient::MessageEventType::Location) {
return e->contentJson();
}
// Cannot use e.contentJson() here because some
// EventContent classes inject values into the copy of the
// content JSON stored in EventContent::Base
@@ -663,6 +654,14 @@ 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();
}
@@ -678,11 +677,29 @@ 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();
}
@@ -768,7 +785,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
// While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index.
// See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows
if (data(i, SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) {
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, DelegateTypeRole) == MessageEventModel::State
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, EventTypeRole) == 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();
}
@@ -793,32 +810,6 @@ 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());
@@ -989,7 +980,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return {};
}
int MessageEventModel::eventIdToRow(const QString &eventID) const
int MessageEventModel::eventIDToIndex(const QString &eventID) const
{
const auto it = m_currentRoom->findInTimeline(eventID);
if (it == m_currentRoom->historyEdge()) {
@@ -1031,7 +1022,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(eventIdToRow(eventId), 0);
QModelIndex idx = index(eventIDToIndex(eventId), 0);
targetMessage.insert("message", idx.data(Qt::UserRole + 2));
return targetMessage;
@@ -1041,14 +1032,14 @@ QVariant MessageEventModel::getLastLocalUserMessageEventId()
return targetMessage;
}
QVariant MessageEventModel::getLatestMessageFromRow(const int startRow)
QVariant MessageEventModel::getLatestMessageFromIndex(const int baseline)
{
QVariantMap replyResponse;
const auto &timelineBottom = m_currentRoom->messageEvents().rbegin() + startRow;
const auto &timelineBottom = m_currentRoom->messageEvents().rbegin() + baseline;
// set a cap limit of startRow + 35 messages, to prevent loading a lot of messages
// set a cap limit of baseline + 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(startRow + 35, m_currentRoom->timelineSize());
const auto limit = timelineBottom + std::min(baseline + 35, m_currentRoom->timelineSize());
for (auto it = timelineBottom; it != limit; ++it) {
auto evt = it->event();
@@ -1070,7 +1061,7 @@ QVariant MessageEventModel::getLatestMessageFromRow(const int startRow)
}
replyResponse.insert("event_id", eventId);
// Need to get the message from the original eventId or body will have * on the front
QModelIndex idx = index(eventIdToRow(eventId), 0);
QModelIndex idx = index(eventIDToIndex(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,99 +7,74 @@
#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, /**< 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. */
Emote,
Notice,
Image,
Audio,
Video,
File,
Message,
Sticker,
State,
Encrypted,
ReadMarker,
Poll,
Other,
};
Q_ENUM(DelegateType);
/**
* @brief Defines the model roles.
*/
enum EventRoles {
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. */
EventTypeRole = Qt::UserRole + 1,
MessageRole,
EventIdRole,
TimeRole,
SectionRole,
AuthorRole,
ContentRole,
ContentTypeRole,
HighlightRole,
SpecialMarksRole,
LongOperationRole,
AnnotationRole,
UserMarkerRole,
FormattedBodyRole,
GenericDisplayRole,
MimeTypeRole, /**< The mime type of the message's file or media. */
FileMimetypeIcon, /**< The icon name for the mime type of a file. */
MimeTypeRole,
FileMimetypeIcon,
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. */
IsReplyRole,
ReplyRole,
ReplyIdRole,
ShowAuthorRole, /**< Whether the author's name should be shown. */
ShowSectionRole, /**< Whether the section header should be shown. */
ShowAuthorRole,
ShowSectionRole,
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. */
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,
IsEditedRole,
SourceRole,
MediaUrlRole,
// For debugging
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. */
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,
LastRole, // Keep this last
};
Q_ENUM(EventRoles)
@@ -107,67 +82,20 @@ public:
explicit MessageEventModel(QObject *parent = nullptr);
~MessageEventModel() override;
[[nodiscard]] NeoChatRoom *room() const;
[[nodiscard]] NeoChatRoom *room() const
{
return m_currentRoom;
}
void setRoom(NeoChatRoom *room);
/**
* @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]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @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]] int eventIDToIndex(const QString &eventID) const;
Q_INVOKABLE [[nodiscard]] QVariant getLastLocalUserMessageEventId();
/**
* @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);
Q_INVOKABLE [[nodiscard]] QVariant getLatestMessageFromIndex(const int baseline);
Q_INVOKABLE void loadReply(const QModelIndex &row);
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::DelegateTypeRole).toInt();
const auto eventType = index.data(MessageEventModel::EventTypeRole).toInt();
if (eventType == MessageEventModel::Other) {
return false;

View File

@@ -5,21 +5,10 @@
#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,11 +12,6 @@ PublicRoomListModel::PublicRoomListModel(QObject *parent)
{
}
Quotient::Connection *PublicRoomListModel::connection() const
{
return m_connection;
}
void PublicRoomListModel::setConnection(Connection *conn)
{
if (m_connection == conn) {
@@ -52,11 +47,6 @@ 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) {
@@ -86,11 +76,6 @@ 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,96 +13,54 @@ 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, /**< 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. */
NameRole = Qt::DisplayRole + 1,
AvatarRole,
TopicRole,
RoomIDRole,
AliasRole,
MemberCountRole,
AllowGuestsRole,
WorldReadableRole,
IsJoinedRole,
};
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;
[[nodiscard]] Quotient::Connection *connection() const
{
return m_connection;
}
void setConnection(Quotient::Connection *conn);
[[nodiscard]] QString server() const;
[[nodiscard]] QString server() const
{
return m_server;
}
void setServer(const QString &value);
[[nodiscard]] QString keyword() const;
[[nodiscard]] QString keyword() const
{
return m_keyword;
}
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,13 +10,11 @@
#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>
@@ -31,6 +29,15 @@ 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)
{
@@ -39,32 +46,30 @@ RoomListModel::RoomListModel(QObject *parent)
m_categoryVisibility[collapsedSection] = false;
}
connect(this, &RoomListModel::notificationCountChanged, this, [this]() {
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
#ifndef Q_OS_ANDROID
// copied from Telegram desktop
const auto launcherUrl = "application://org.kde.neochat.desktop";
// Gnome requires that count is a 64bit integer
const qint64 counterSlice = std::min(m_notificationCount, 9999);
QVariantMap dbusUnityProperties;
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 (counterSlice > 0) {
dbusUnityProperties["count"] = counterSlice;
dbusUnityProperties["count-visible"] = true;
} else {
dbusUnityProperties["count-visible"] = false;
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);
}
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;
@@ -149,10 +154,10 @@ void RoomListModel::doAddRoom(Room *r)
void RoomListModel::connectRoomSignals(NeoChatRoom *room)
{
connect(room, &Room::displaynameChanged, this, [this, room] {
refresh(room, {DisplayNameRole, NameRole});
refresh(room);
});
connect(room, &Room::unreadMessagesChanged, this, [this, room] {
refresh(room, {UnreadCountRole, NotificationCountRole, HighlightCountRole});
refresh(room);
});
connect(room, &Room::notificationCountChanged, this, [this, room] {
refresh(room);
@@ -167,7 +172,7 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
refresh(room);
});
connect(room, &Room::addedMessages, this, [this, room] {
refresh(room, {LastEventRole, SubtitleTextRole, LastActiveTimeRole});
refresh(room, {LastEventRole, SubtitleTextRole});
});
connect(room, &Room::pendingEventMerged, this, [this, room] {
refresh(room, {LastEventRole, SubtitleTextRole});

View File

@@ -217,9 +217,6 @@ 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,6 +8,7 @@
#include <QMimeDatabase>
#include <QTemporaryFile>
#include <QTextDocument>
#include <functional>
#include <QMediaMetaData>
#include <QMediaPlayer>
@@ -109,34 +110,6 @@ 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);
@@ -388,6 +361,30 @@ 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();
@@ -421,6 +418,15 @@ 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()) {
@@ -789,6 +795,30 @@ 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) {
@@ -975,6 +1005,11 @@ 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();
@@ -1457,11 +1492,6 @@ 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,13 +3,14 @@
#pragma once
#include <qobjectdefs.h>
#include <room.h>
#include <QCache>
#include <QObject>
#include <QTextCursor>
#include <QCoroTask>
#include <qcoro/task.h>
#include "neochatuser.h"
#include "pollhandler.h"
@@ -19,150 +20,36 @@ class PushNotificationState : public QObject
Q_OBJECT
public:
/**
* @brief Describes the push notification state for the room.
*/
enum State {
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. */
Unknown,
Default,
Mute,
MentionKeyword,
All,
};
Q_ENUM(State);
};
/**
* @brief Defines a user mention in the current chat or edit text.
*/
struct Mention {
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). */
QTextCursor cursor;
QString text;
int start = 0;
int position = 0;
QString id;
};
/**
* @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)
/**
* @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 readMarkerLoaded READ readMarkerLoaded NOTIFY readMarkerLoadedChanged)
Q_PROPERTY(QDateTime lastActiveTime READ lastActiveTime NOTIFY lastActiveTimeChanged)
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)
/**
@@ -178,441 +65,98 @@ class NeoChatRoom : public Quotient::Room
*/
Q_PROPERTY(bool urlPreviewEnabled READ urlPreviewEnabled WRITE setUrlPreviewEnabled NOTIFY urlPreviewEnabledChanged)
/**
* @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.
*/
// Properties for the various permission levels for the room
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)
/**
* @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 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
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)
/**
* @brief The event id of a message being replied to.
*
* Will be QString() if not replying to a message.
*/
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)
Q_PROPERTY(QString chatBoxAttachmentPath READ chatBoxAttachmentPath WRITE setChatBoxAttachmentPath NOTIFY chatBoxAttachmentPathChanged)
Q_PROPERTY(bool canEncryptRoom READ canEncryptRoom NOTIFY canEncryptRoomChanged)
/**
* @brief The file path of the attachment to be sent.
* @brief Get the maximum room version that the server supports.
*
* Only returns main integer room versions (i.e. no msc room versions).
*/
Q_PROPERTY(QString chatBoxAttachmentPath READ chatBoxAttachmentPath WRITE setChatBoxAttachmentPath NOTIFY chatBoxAttachmentPathChanged)
Q_PROPERTY(int maxRoomVersion READ maxRoomVersion NOTIFY maxRoomVersionChanged)
Q_PROPERTY(NeoChatUser *directChatRemoteUser READ directChatRemoteUser CONSTANT)
public:
/**
* @brief Define the types on inline messages that can be shown.
*/
enum MessageType {
Positive, /**< Positive message, typically green. */
Info, /**< Info message, typically highlight color. */
Error, /**< Error message, typically red. */
Positive,
Info,
Error,
};
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;
[[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.
*/
/// 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]] const Quotient::RoomEvent *lastEvent() const;
/**
* @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()
*/
/// Convenient way to get the last event but in a string format.
///
/// \see lastEvent
/// \see lastEventIsSpoiler
[[nodiscard]] QString lastEventToString(Qt::TextFormat format = Qt::PlainText, bool stripNewlines = false) const;
/**
* @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()
*/
/// Convenient way to check if the last event looks like it has spoilers.
///
/// \see lastEvent
/// \see lastEventToString
[[nodiscard]] bool lastEventIsSpoiler() const;
[[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;
/// Convenient way to get the QDateTime of the last event.
///
/// \see lastEvent
[[nodiscard]] QDateTime lastActiveTime();
[[nodiscard]] bool isSpace();
bool isInvite() const;
Q_INVOKABLE void clearInvitationNotification();
bool isEventHighlighted(const Quotient::RoomEvent *e) const;
[[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);
@@ -622,8 +166,6 @@ 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.
*
@@ -694,6 +236,65 @@ 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);
@@ -715,40 +316,44 @@ public:
QString chatBoxAttachmentPath() const;
void setChatBoxAttachmentPath(const QString &attachmentPath);
/**
* @brief Retrieve the mentions for the current chatbox text.
*/
QVector<Mention> *mentions();
/**
* @brief Retrieve the mentions for the current edit text.
* @brief Vector of mentions in 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);
#ifdef QUOTIENT_07
/**
* @brief Get a PollHandler object for the given event Id.
bool canEncryptRoom() const;
Q_INVOKABLE bool downloadTempFile(const QString &eventId);
/*
* Map an alias to the room
*
* 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
* 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
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;
@@ -827,78 +432,26 @@ 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);
/**
* @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.
*/
/// @param rawText The text as it was typed.
/// @param cleanedText The text with link to the users.
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);
/**
* @brief Toggle the reaction state of the given reaction for the local user.
*/
void addLocalAlias(const QString &alias);
void removeLocalAlias(const QString &alias);
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

@@ -139,37 +139,7 @@ QQC2.Control {
currentRoom.chatBoxText = text
}
onCursorRectangleChanged: chatBarScrollView.ensureVisible(cursorRectangle)
onSelectedTextChanged: {
if (selectedText.length > 0) {
quickFormatBar.selectionStart = selectionStart
quickFormatBar.selectionEnd = selectionEnd
quickFormatBar.open()
}
}
QuickFormatBar {
id: quickFormatBar
x: textField.cursorRectangle.x
y: textField.cursorRectangle.y - height
onFormattingSelected: chatBar.formatText(format, selectionStart, selectionEnd)
}
Keys.onDeletePressed: {
if (selectedText.length > 0) {
remove(selectionStart, selectionEnd)
} else {
remove(cursorPosition, cursorPosition + 1)
}
if (textField.text == selectedText || textField.text.length <= 1) {
currentRoom.sendTypingNotification(false)
repeatTimer.stop()
}
if (quickFormatBar.visible) {
quickFormatBar.close()
}
}
Keys.onEnterPressed: {
if (completionMenu.visible) {
completionMenu.complete()
@@ -197,7 +167,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.getLatestMessageFromRow(0)
let replyEvent = messageEventModel.getLatestMessageFromIndex(0)
if (replyEvent && replyEvent["event_id"]) {
currentRoom.chatBoxReplyId = replyEvent["event_id"]
}
@@ -210,14 +180,9 @@ QQC2.Control {
completionMenu.decrementIndex()
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
completionMenu.incrementIndex()
} else if (event.key === Qt.Key_Backspace) {
if (textField.text == selectedText || textField.text.length <= 1) {
currentRoom.sendTypingNotification(false)
repeatTimer.stop()
}
if (quickFormatBar.visible && selectedText.length > 0) {
quickFormatBar.close()
}
} else if (event.key === Qt.Key_Backspace && textField.text.length <= 1) {
currentRoom.sendTypingNotification(false)
repeatTimer.stop()
}
}
Keys.onShortcutOverride: {
@@ -348,9 +313,14 @@ QQC2.Control {
QQC2.ToolTip.text: modelData.tooltip
HoverHandler { id: hoverHandler }
PieProgressBar {
visible: modelData.isBusy
progress: currentRoom.fileUploadingProgress
QQC2.BusyIndicator {
anchors.fill: parent
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
visible: running
running: modelData.isBusy
}
}
}
@@ -394,8 +364,6 @@ QQC2.Control {
cursorPosition: textField.cursorPosition
selectionStart: textField.selectionStart
selectionEnd: textField.selectionEnd
mentionColor: Kirigami.Theme.linkColor
errorColor: Kirigami.Theme.negativeTextColor
Component.onCompleted: {
RoomManager.chatDocumentHandler = documentHandler;
}
@@ -430,47 +398,4 @@ QQC2.Control {
currentRoom.chatBoxReplyId = "";
messageSent()
}
function formatText(format, selectionStart, selectionEnd) {
let index = textField.cursorPosition;
/*
* There cannot be white space at the beginning or end of the string for the
* formatting to work so move the sectionStart and sectionEnd markers past any whitespace.
*/
let innerText = textField.text.substr(selectionStart, selectionEnd - selectionStart);
if (innerText.charAt(innerText.length - 1) === " ") {
let trimmedRightString = innerText.replace(/\s*$/,"");
let trimDifference = innerText.length - trimmedRightString.length;
selectionEnd -= trimDifference;
}
if (innerText.charAt(0) === " ") {
let trimmedLeftString = innerText.replace(/^\s*/,"");
let trimDifference = innerText.length - trimmedLeftString.length;
selectionStart = selectionStart + trimDifference;
}
let startText = textField.text.substr(0, selectionStart);
// Needs updating with the new selectionStart and selectionEnd with white space trimmed.
innerText = textField.text.substr(selectionStart, selectionEnd - selectionStart);
let endText = textField.text.substr(selectionEnd);
textField.text = "";
textField.text = startText + format.start + innerText + format.end + format.extra + endText;
/*
* Put the cursor where it was when the popup was opened accounting for the
* new markup.
*
* The exception is for a hyperlink where it is placed ready to start typing
* the url.
*/
if (format.extra !== "") {
textField.cursorPosition = selectionEnd + format.start.length + format.end.length;
} else if (index == selectionStart) {
textField.cursorPosition = index;
} else {
textField.cursorPosition = index + format.start.length + format.end.length;
}
}
}

View File

@@ -1,65 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick 2.15
import org.kde.kirigami 2.18 as Kirigami
import org.kde.quickcharts 1.0 as Charts
/**
* @brief A circular progress bar that fills an arc as progress goes up.
*/
Rectangle {
id: root
/**
* @brief Progress of the circle as a percentage.
*
* Range - 0% to 100%.
*/
property int progress: 0
/**
* @brief Offset angle for the start of the pie fill arc.
*
* This defaults to 0, i.e. an upward vertical line from the center. This rotates
* that start point by the desired number of degrees.
*
* Range - 0 degrees to 360 degrees
*/
property int startOffset: 0
/**
* @brief Fill color of the pie.
*/
property color pieColor: Kirigami.Theme.highlightColor
width: Kirigami.Units.iconSizes.smallMedium
height: Kirigami.Units.iconSizes.smallMedium
radius: width / 2
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.15)
Charts.PieChart {
id: chart
anchors.fill: parent
anchors.margins: 1
filled: true
// Set chart background color so the parent filled rectangle looks like
// an outline.
backgroundColor: Kirigami.Theme.backgroundColor
fromAngle: root.startOffset
toAngle: 360 + root.startOffset
range {
from: 0
to: 100
automatic: false
}
valueSources: Charts.SingleValueSource {
value: root.progress
}
colorSource: Charts.SingleValueSource {
value: Kirigami.Theme.highlightColor
}
}
}

View File

@@ -1,136 +0,0 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
QQC2.Popup {
id: root
property var selectionStart
property var selectionEnd
signal formattingSelected(var format, int selectionStart, int selectionEnd)
padding: 1
contentItem: Flow {
QQC2.ToolButton {
icon.name: "format-text-bold"
text: i18n("Bold")
display: QQC2.AbstractButton.IconOnly
onClicked: {
const format = {
start: "**",
end: "**",
extra: "",
}
formattingSelected(format, selectionStart, selectionEnd)
root.close()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
icon.name: "format-text-italic"
text: i18n("Italic")
display: QQC2.AbstractButton.IconOnly
onClicked: {
const format = {
start: "*",
end: "*",
extra: "",
}
formattingSelected(format, selectionStart, selectionEnd)
root.close()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
icon.name: "format-text-strikethrough"
text: i18n("Strikethrough")
display: QQC2.AbstractButton.IconOnly
onClicked: {
const format = {
start: "<del>",
end: "</del>",
extra: "",
}
formattingSelected(format, selectionStart, selectionEnd)
root.close()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
icon.name: "format-text-code"
text: i18n("Code block")
display: QQC2.AbstractButton.IconOnly
onClicked: {
const format = {
start: "`",
end: "`",
extra: "",
}
formattingSelected(format, selectionStart, selectionEnd)
root.close()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
icon.name: "format-text-blockquote"
text: i18n("Quote")
display: QQC2.AbstractButton.IconOnly
onClicked: {
const format = {
start: selectionStart == 0 ? ">" : "\n>",
end: "\n\n",
extra: "",
}
formattingSelected(format, selectionStart, selectionEnd)
root.close()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
icon.name: "link"
text: i18n("Insert link")
display: QQC2.AbstractButton.IconOnly
onClicked: {
const format = {
start: "[",
end: "](",
extra: ")",
}
formattingSelected(format, selectionStart, selectionEnd)
root.close()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
}

View File

@@ -1,132 +1,115 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: GPL-3.0-only
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 QtQuick.Layouts 1.10
import QtQuick.Controls 2.12 as QQC2
import org.kde.kirigami 2.14 as Kirigami
import org.kde.kitemmodels 1.0
import org.kde.neochat 1.0
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
QQC2.Popup {
id: _popup
Shortcut {
sequence: "Ctrl+K"
onActivated: root.open()
enabled: !Kirigami.Settings.hasPlatformMenuBar
onActivated: _popup.open()
}
onVisibleChanged: {
if (!visible) {
return
}
searchField.forceActiveFocus()
searchField.text = ""
roomList.currentIndex = 0
quickSearch.forceActiveFocus()
quickSearch.text = ""
}
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();
}
}
anchors.centerIn: QQC2.Overlay.overlay
background: Kirigami.Card {}
height: 2 * Math.round(implicitHeight / 2)
padding: Kirigami.Units.largeSpacing * 2
QQC2.ScrollView {
anchors.fill: parent
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()
}
}
ListView {
id: roomList
id: cView
orientation: Qt.Horizontal
spacing: Kirigami.Units.largeSpacing
currentIndex: 0
highlightMoveDuration: 200
Keys.forwardTo: searchField
keyNavigationEnabled: true
model: SortFilterRoomListModel {
filterText: searchField.text
id: sortFilterRoomListModel
sourceModel: RoomListModel {
id: roomListModel
connection: Controller.activeConnection
}
filterText: quickSearch.text
roomSortOrder: SortFilterRoomListModel.LastActivity
}
delegate: Kirigami.BasicListItem {
id: roomListItem
required property var currentRoom
required property string name
required property int index
required property int unreadCount
required property string subtitleText
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 int 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()
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
}
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
actions.main: Kirigami.Action {
id: enterRoomAction
onTriggered: {
RoomManager.enterRoom(currentRoom);
_popup.close()
}
}
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: "delegateType"
role: "eventType"
DelegateChoice {
roleValue: MessageEventModel.State
@@ -20,7 +20,9 @@ DelegateChooser {
DelegateChoice {
roleValue: MessageEventModel.Emote
delegate: MessageDelegate {}
delegate: MessageDelegate {
isEmote: true
}
}
DelegateChoice {
@@ -73,11 +75,6 @@ DelegateChooser {
delegate: PollDelegate {}
}
DelegateChoice {
roleValue: MessageEventModel.Location
delegate: LocationDelegate {}
}
DelegateChoice {
roleValue: MessageEventModel.Other
delegate: Item {}

View File

@@ -110,18 +110,14 @@ 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

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

View File

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

View File

@@ -119,8 +119,6 @@ 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,6 +96,7 @@ Item {
RichLabel {
textMessage: reply.display
textFormat: Text.RichText
isReplyLabel: true
HoverHandler {
enabled: !hoveredLink

View File

@@ -14,6 +14,8 @@ 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)
@@ -33,9 +35,6 @@ table {
border-collapse: collapse;
border-style: solid;
}
code {
background-color:" + Kirigami.Theme.alternateBackgroundColor + ";
}
table th,
table td {
border: 1px solid black;
@@ -58,7 +57,7 @@ a{
background: " + Kirigami.Theme.textColor + ";
}
" : "") + "
</style>" + textMessage
</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>") : "")
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: Config.compactLayout ? Kirigami.Theme.View : Kirigami.Theme.Window
Kirigami.Theme.colorSet: Kirigami.Theme.Window
}
}

View File

@@ -21,6 +21,7 @@ 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
@@ -211,7 +212,7 @@ ColumnLayout {
id: rowLayout
spacing: Kirigami.Units.smallSpacing
visible: model.showAuthor
visible: model.showAuthor && !isEmote
QQC2.Label {
id: nameLabel
@@ -305,7 +306,6 @@ 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: delegateType !== MessageEventModel.State && delegateType !== MessageEventModel.Notice && reaction != undefined && reaction.length > 0
visible: eventType !== MessageEventModel.State && eventType !== MessageEventModel.Notice && reaction != undefined && reaction.length > 0
}
AvatarFlow {
Layout.alignment: Qt.AlignRight

View File

@@ -144,17 +144,12 @@ QQC2.ToolBar {
actions.main: Kirigami.Action {
text: i18n("Edit this account")
icon.name: "document-edit"
onTriggered: pageStack.pushDialogLayer(Qt.resolvedUrl('qrc:/AccountEditorPage.qml'), {
onTriggered: pageStack.pushDialogLayer(Qt.resolvedUrl('./AccountEditorPage.qml'), {
connection: Controller.activeConnection
}, {
title: i18n("Account editor")
});
}
TapHandler {
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse
onTapped: accountMenu.open()
}
}
}
ColumnLayout {
@@ -219,11 +214,6 @@ QQC2.ToolBar {
Item {
width: 1
}
AccountMenu {
id: accountMenu
y: -height
}
}
}
}

View File

@@ -20,8 +20,8 @@ QQC2.Dialog {
}
}
x: parent ? Math.round((parent.width - width) / 2) : 0
y: parent ? Math.round((parent.height - height) / 2) : 0
x: Math.round((parent.width - width) / 2)
y: Math.round((parent.height - height) / 2)
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

@@ -1,45 +0,0 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
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
margins: Kirigami.Units.smallSpacing
QQC2.MenuItem {
text: i18n("Edit this account")
icon.name: "document-edit"
onTriggered: pageStack.pushDialogLayer("qrc:/AccountEditorPage.qml", {
connection: Controller.activeConnection
}, {
title: i18n("Account editor")
});
}
QQC2.MenuItem {
text: i18n("Notification settings")
icon.name: "notifications"
onTriggered: pageStack.pushDialogLayer("qrc:/SettingsPage.qml", {defaultPage: "notifications"}, { title: i18n("Configure")})
}
QQC2.MenuItem {
text: i18n("Devices")
icon.name: "computer-symbolic"
onTriggered: pageStack.pushDialogLayer("qrc:/SettingsPage.qml", {defaultPage: "devices"}, { title: i18n("Configure")})
}
QQC2.MenuItem {
text: i18n("Logout")
icon.name: "list-remove-user"
onTriggered: confirmLogoutDialog.open()
}
Dialog.ConfirmLogout {
id: confirmLogoutDialog
}
}

View File

@@ -1,53 +0,0 @@
// 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

@@ -1,301 +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
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

@@ -1,136 +0,0 @@
// 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

@@ -1,47 +0,0 @@
// 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

@@ -1,64 +0,0 @@
// 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

@@ -0,0 +1,505 @@
// 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,7 +41,6 @@ Kirigami.ScrollablePage {
} else if (page.currentRoom.isInvite) {
page.currentRoom.clearInvitationNotification();
}
currentRoom.markAllMessagesAsRead();
}
}
@@ -52,7 +51,6 @@ Kirigami.ScrollablePage {
if (!Kirigami.Settings.isMobile) {
chatBox.chatBar.forceActiveFocus();
}
currentRoom.markAllMessagesAsRead();
}
Connections {
@@ -265,7 +263,7 @@ Kirigami.ScrollablePage {
anchors.leftMargin: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.x : 0
anchors.right: parent.right
maxWidth: Config.compactLayout ? messageListView.width : (messageListView.sectionBannerItem ? messageListView.sectionBannerItem.width - Kirigami.Units.largeSpacing * 2 : 0)
maxWidth: 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 : ""
@@ -478,7 +476,7 @@ Kirigami.ScrollablePage {
id: hoverActions
property var event: null
property bool userMsg: event && event.author.id === Controller.activeConnection.localUserId
property bool showEdit: event && (userMsg && (event.delegateType === MessageEventModel.Emote || event.delegateType === MessageEventModel.Message))
property bool showEdit: event && (userMsg && (event.eventType === MessageEventModel.Emote || event.eventType === MessageEventModel.Message))
property var delegate: null
property var bubble: null
property var hovered: bubble && bubble.hovered
@@ -583,42 +581,37 @@ Kirigami.ScrollablePage {
}
}
background: Rectangle {
color: Config.compactLayout ? Kirigami.Theme.backgroundColor : "transparent"
background: FancyEffectsContainer {
id: fancyEffectsContainer
z: 100
FancyEffectsContainer {
id: fancyEffectsContainer
anchors.fill: parent
z: 100
enabled: Config.showFancyEffects
function processFancyEffectsReason(fancyEffect) {
if (fancyEffect === "snowflake") {
fancyEffectsContainer.showSnowEffect()
}
if (fancyEffect === "fireworks") {
fancyEffectsContainer.showFireworksEffect()
}
if (fancyEffect === "confetti") {
fancyEffectsContainer.showConfettiEffect()
}
}
Connections {
enabled: Config.showFancyEffects
function processFancyEffectsReason(fancyEffect) {
if (fancyEffect === "snowflake") {
fancyEffectsContainer.showSnowEffect()
}
if (fancyEffect === "fireworks") {
fancyEffectsContainer.showFireworksEffect()
}
if (fancyEffect === "confetti") {
fancyEffectsContainer.showConfettiEffect()
}
target: messageEventModel
function onFancyEffectsReasonFound(fancyEffect) {
fancyEffectsContainer.processFancyEffectsReason(fancyEffect)
}
}
Connections {
enabled: Config.showFancyEffects
target: messageEventModel
function onFancyEffectsReasonFound(fancyEffect) {
fancyEffectsContainer.processFancyEffectsReason(fancyEffect)
}
}
Connections {
enabled: Config.showFancyEffects
target: actionsHandler
function onShowEffect(fancyEffect) {
fancyEffectsContainer.processFancyEffectsReason(fancyEffect)
}
Connections {
enabled: Config.showFancyEffects
target: actionsHandler
function onShowEffect(fancyEffect) {
fancyEffectsContainer.processFancyEffectsReason(fancyEffect)
}
}
}
@@ -644,7 +637,7 @@ Kirigami.ScrollablePage {
}
function eventToIndex(eventID) {
const index = messageEventModel.eventIdToRow(eventID)
const index = messageEventModel.eventIDToIndex(eventID)
if (index === -1)
return -1
return sortedMessageEventModel.mapFromSource(messageEventModel.index(index, 0)).row
@@ -704,7 +697,7 @@ Kirigami.ScrollablePage {
eventId: event.eventId,
formattedBody: event.formattedBody,
source: event.source,
eventType: event.delegateType,
eventType: event.eventType,
plainMessage: plainMessage,
});
contextMenu.open();

View File

@@ -157,35 +157,24 @@ Kirigami.OverlayDrawer {
}
}
QQC2.ScrollView {
TextEdit {
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
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
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 {}
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
}
}
@@ -241,9 +230,6 @@ 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 {
@@ -262,7 +248,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")
}
}
@@ -284,7 +270,6 @@ 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

@@ -12,7 +12,6 @@ Kirigami.CategorizedSettings {
objectName: "settingsPage"
actions: [
Kirigami.SettingAction {
actionName: "general"
text: i18n("General")
icon.name: "settings-configure"
page: Qt.resolvedUrl("General.qml")
@@ -23,7 +22,6 @@ Kirigami.CategorizedSettings {
}
},
Kirigami.SettingAction {
actionName: "security"
text: i18n("Security")
icon.name: "security-low"
page: Qt.resolvedUrl("Security.qml")
@@ -34,7 +32,6 @@ Kirigami.CategorizedSettings {
}
},
Kirigami.SettingAction {
actionName: "permissions"
text: i18n("Permissions")
icon.name: "visibility"
page: Qt.resolvedUrl("Permissions.qml")
@@ -45,7 +42,6 @@ Kirigami.CategorizedSettings {
}
},
Kirigami.SettingAction {
actionName: "notifications"
text: i18n("Notifications")
icon.name: "notifications"
page: Qt.resolvedUrl("PushNotification.qml")

View File

@@ -9,62 +9,52 @@ Kirigami.CategorizedSettings {
objectName: "settingsPage"
actions: [
Kirigami.SettingAction {
actionName: "general"
text: i18n("General")
icon.name: "org.kde.neochat"
page: Qt.resolvedUrl("GeneralSettingsPage.qml")
},
Kirigami.SettingAction {
actionName: "appearance"
text: i18n("Appearance")
icon.name: "preferences-desktop-theme-global"
page: Qt.resolvedUrl("AppearanceSettingsPage.qml")
},
Kirigami.SettingAction {
actionName: "notifications"
text: i18n("Notifications")
icon.name: "preferences-desktop-notification"
page: Qt.resolvedUrl("GlobalNotificationsPage.qml")
},
Kirigami.SettingAction {
actionName: "accounts"
text: i18n("Accounts")
icon.name: "preferences-system-users"
page: Qt.resolvedUrl("AccountsPage.qml")
},
Kirigami.SettingAction {
actionName: "customEmojis"
text: i18n("Custom Emojis")
icon.name: "preferences-desktop-emoticons"
page: Qt.resolvedUrl("Emoticons.qml")
},
Kirigami.SettingAction {
actionName: "spellChecking"
text: i18n("Spell Checking")
icon.name: "tools-check-spelling"
page: Qt.resolvedUrl("SonnetConfigPage.qml")
visible: Qt.platform.os !== "android"
},
Kirigami.SettingAction {
actionName: "networkProxy"
text: i18n("Network Proxy")
icon.name: "network-connect"
page: Qt.resolvedUrl("NetworkProxyPage.qml")
},
Kirigami.SettingAction {
actionName: "devices"
text: i18n("Devices")
icon.name: "computer"
page: Qt.resolvedUrl("DevicesPage.qml")
},
Kirigami.SettingAction {
actionName: "aboutNeochat"
text: i18n("About NeoChat")
icon.name: "help-about"
page: Qt.resolvedUrl("About.qml")
},
Kirigami.SettingAction {
actionName: "aboutKDE"
text: i18n("About KDE")
icon.name: "kde"
page: Qt.resolvedUrl("AboutKDE.qml")

View File

@@ -9,8 +9,6 @@ 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
@@ -26,7 +24,7 @@ Kirigami.ApplicationWindow {
pageStack.initialPage: LoadingPage {}
pageStack.globalToolBar.canContainHandles: true
property RoomList.Page roomListPage
property RoomListPage roomListPage
property bool roomListLoaded: false
property RoomPage roomPage
@@ -99,6 +97,10 @@ 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;
@@ -165,7 +167,7 @@ Kirigami.ApplicationWindow {
pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar
pageStack.globalToolBar.showNavigationButtons: pageStack.currentIndex > 0 || pageStack.layers.depth > 1 ? Kirigami.ApplicationHeaderStyle.ShowBackButton : 0
Dialog.ConfirmLogout {
ConfirmLogoutDialog {
id: confirmLogoutDialog
}
@@ -198,7 +200,7 @@ Kirigami.ApplicationWindow {
Component {
id: roomListComponent
RoomList.Page {
RoomListPage {
id: roomList
Shortcut {

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