Compare commits

...

95 Commits

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

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

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

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

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

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

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

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

This is with my own colorscheme:

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

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

m.pin \
![image](/uploads/baee5d19c098eaab1abd91533fe431e5/image.png)
2023-03-27 16:38:34 +00:00
l10n daemon script
d0b1610a9f GIT_SILENT Sync po/docbooks with svn 2023-03-27 02:44:58 +00:00
Tobias Fella
dcf520a7a9 Fix locations for geouris with uncertainty 2023-03-26 22:55:05 +02:00
James Graham
a0ae8b28b2 Handle stripnewlines for plain text list
Handle stripping new lines when the plain text input is a markdown list.
2023-03-26 20:27:23 +00:00
l10n daemon script
78a6179a11 GIT_SILENT Sync po/docbooks with svn 2023-03-26 01:58:24 +00:00
l10n daemon script
9c91557d8f GIT_SILENT Sync po/docbooks with svn 2023-03-25 01:55:18 +00:00
Tobias Fella
fb24ffd20d Show a map for location events 2023-03-24 13:52:17 +00:00
l10n daemon script
0c985a0af1 GIT_SILENT Sync po/docbooks with svn 2023-03-24 01:52:23 +00:00
Tobias Fella
787dc5ab66 Ensure that the room list subtitle is a single line 2023-03-23 20:10:35 +00:00
l10n daemon script
f11abdeebd GIT_SILENT Sync po/docbooks with svn 2023-03-22 02:16:45 +00:00
l10n daemon script
89141dddf2 GIT_SILENT Sync po/docbooks with svn 2023-03-21 02:03:28 +00:00
l10n daemon script
69f08de0ff GIT_SILENT Sync po/docbooks with svn 2023-03-20 01:59:17 +00:00
James Graham
925e20ebb8 Handle quotes in plain strings
Unescape quotes in plain strings
2023-03-19 19:58:56 +00:00
James Graham
ee254a286d Handle single quotes for links
Make sure that the text handler can handle links which use single or double quotes for the attributes.
2023-03-19 19:06:08 +00:00
James Graham
3cc8d32dd3 Add kquickcharts to the flatpak manifest 2023-03-19 18:34:45 +00:00
l10n daemon script
50cf6d9750 GIT_SILENT Sync po/docbooks with svn 2023-03-19 01:51:40 +00:00
Tobias Fella
cedbb64932 Remove unused include 2023-03-19 02:00:45 +01:00
Tobias Fella
b45898a5b6 Fix typo in actionName 2023-03-19 01:59:55 +01:00
Tobias Fella
ccb1748ab3 Only try to find sqlite on android
Cleans up the cmake output a bit
2023-03-19 01:54:16 +01:00
James Graham
11e9af15f7 Add Quick Formatting Menu in the Chatbar
Add a menu to quickly apply text effects when selecting text in the chatbar.

![image](/uploads/aa4d3b6f7e89695e1168aeee536d2df4/image.png)
2023-03-18 10:44:28 +00:00
l10n daemon script
8ad41fec92 GIT_SILENT Sync po/docbooks with svn 2023-03-18 02:12:49 +00:00
James Graham
ac5212ebb2 Update space heirarchy cache
Add functions to update the space heirarchy cache when a new space is added or removed.
2023-03-17 14:23:39 +00:00
James Graham
4e16b91f54 Hide local edit message
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
2023-03-17 10:07:31 +00:00
l10n daemon script
343f4965ed GIT_SILENT Sync po/docbooks with svn 2023-03-17 02:11:51 +00:00
James Graham
ac775c5aaf Add pie shaped progress bar for chatbar instead of busy indicator
![2023-03-15_15-06-51](/uploads/7a56d2a1696e26b87451b0951995a730/2023-03-15_15-06-51.mp4)

Closes network/neochat#151
2023-03-16 19:22:37 +00:00
James Graham
db17888d42 Add action name required propereties for the room settings. 2023-03-16 09:16:21 +00:00
l10n daemon script
f12d10baa6 GIT_SILENT Sync po/docbooks with svn 2023-03-16 02:04:44 +00:00
James Graham
4289c1345f Update about neochat actionName to "aboutNeochat" and add one for about KDE 2023-03-15 14:56:37 +00:00
James Graham
0d6a83b063 Add end state to text handler and use to ensure that in nextTokenType to stop an out of bounds access to m_databuffer 2023-03-15 11:55:50 +00:00
l10n daemon script
76b04dcba9 GIT_SILENT Sync po/docbooks with svn 2023-03-15 01:58:28 +00:00
James Graham
64dee7eb12 Add right click menu to accountlist avatar
Adds a right click menu with the following options

![image](/uploads/09a266fb609bb8f94b206eb66f09e06d/image.png)

depends upon frameworks/kirigami!933
2023-03-14 23:00:41 +00:00
Carl Schwan
bc84ad8d56 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>
2023-03-14 22:27:44 +00:00
James Graham
afe8a2a5e4 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
2023-03-14 22:26:18 +00:00
Carl Schwan
87213dc9dd 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>
2023-03-14 22:24:22 +00:00
James Graham
300d2428eb Remove rogue console.log statement 2023-03-14 19:38:18 +00:00
James Graham
abe7d70822 Add home and coloured notifications icons 2023-03-14 15:55:31 +00:00
l10n daemon script
d8bf26158a GIT_SILENT Sync po/docbooks with svn 2023-03-14 01:57:26 +00:00
James Graham
741cb57105 Fix state event visiblity in timeline
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
2023-03-13 19:00:22 +00:00
James Graham
81c73037ca Room URL preview settings
- 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
2023-03-13 18:23:18 +00:00
James Graham
f6ba4f2ecd Improve Text Handling
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
2023-03-13 17:28:56 +00:00
James Graham
23303c0483 Other user read markers
Add the ability to see other user's read markers. 

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

Implements network/neochat#133
2023-03-13 15:12:46 +00:00
l10n daemon script
5a02448326 GIT_SILENT Sync po/docbooks with svn 2023-03-13 02:08:04 +00:00
l10n daemon script
411ae25e80 GIT_SILENT Sync po/docbooks with svn 2023-03-12 03:18:42 +00:00
l10n daemon script
6e1b3fe860 GIT_SILENT Sync po/docbooks with svn 2023-03-11 02:14:41 +00:00
Albert Astals Cid
4a38d83a68 GIT_SILENT Upgrade release service version to 23.07.70. 2023-03-10 22:00:27 +01:00
128 changed files with 30162 additions and 25249 deletions

1
.gitignore vendored
View File

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

View File

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

View File

@@ -11,6 +11,7 @@ Dependencies:
'frameworks/kconfig': '@stable'
'frameworks/syntax-highlighting': '@stable'
'frameworks/kitemmodels': '@stable'
'frameworks/kquickcharts': '@stable'
'frameworks/knotifications': '@stable'
'libraries/kquickimageeditor': '@stable'
'frameworks/sonnet': '@stable'
@@ -38,6 +39,7 @@ 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,7 +8,7 @@ cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "23")
set(RELEASE_SERVICE_VERSION_MINOR "03")
set(RELEASE_SERVICE_VERSION_MINOR "07")
set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
@@ -68,12 +68,6 @@ set_package_properties(KF${QT_MAJOR_VERSION}Kirigami2 PROPERTIES
)
find_package(KF${QT_MAJOR_VERSION}KirigamiAddons 0.7.2 REQUIRED)
find_package(Qt${QT_MAJOR_VERSION}Keychain)
set_package_properties(Qt${QT_MAJOR_VERSION}Keychain PROPERTIES
TYPE REQUIRED
PURPOSE "Secure storage of account secrets"
)
if(ANDROID)
find_package(OpenSSL)
set_package_properties(OpenSSL PROPERTIES
@@ -93,7 +87,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
find_package(KF${QT_MAJOR_VERSION}DBusAddons ${KF_MIN_VERSION} REQUIRED)
endif()
find_package(Quotient 0.6)
find_package(Quotient 0.7)
set_package_properties(Quotient PROPERTIES
TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API"
@@ -111,6 +105,7 @@ 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
@@ -130,13 +125,9 @@ 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()
@@ -150,7 +141,7 @@ install(FILES org.kde.neochat.tray.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/h
add_definitions(-DQT_NO_FOREACH)
add_subdirectory(src)
if (BUILD_TESTING AND Quotient_VERSION_MINOR GREATER 6)
if (BUILD_TESTING)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
add_subdirectory(autotests)
endif()

View File

@@ -8,3 +8,9 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test Quotient
TEST_NAME neochatroomtest
)
ecm_add_test(
texthandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME texthandlertest
)

View File

@@ -136,7 +136,7 @@ void NeoChatRoomTest::initTestCase()
void NeoChatRoomTest::subtitleTextTest()
{
QCOMPARE(room->timelineSize(), 1);
QCOMPARE(room->subtitleText(), QStringLiteral("@example:example.org: This is an example text message"));
QCOMPARE(room->lastEventToString(), QStringLiteral("@example:example.org: This is an example text message"));
}
void NeoChatRoomTest::eventTest()

View File

@@ -0,0 +1,587 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QTest>
#include "texthandler.h"
#include <qnamespace.h>
#include <quotient_common.h>
#include <syncdata.h>
using namespace Quotient;
class TestRoom : public NeoChatRoom
{
public:
using NeoChatRoom::NeoChatRoom;
void update(SyncRoomData &&data, bool fromCache = false)
{
Room::updateData(std::move(data), fromCache);
}
};
class TextHandlerTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void allowedAttributes();
void stripDisallowedTags();
void stripDisallowedAttributes();
void emptyCodeTags();
void sendSimpleStringCase();
void sendSingleParaMarkup();
void sendMultipleSectionMarkup();
void sendBadLinks();
void sendEscapeCode();
void sendCodeClass();
void receiveStripReply();
void receivePlainTextIn();
void receiveRichInPlainOut_data();
void receiveRichInPlainOut();
void receivePlainStripHtml();
void receivePlainStripMarkup();
void receiveStripNewlines();
void receiveRichUserPill();
void receiveRichStrikethrough();
void receiveRichtextIn();
void receiveRichMxcUrl();
void receiveRichPlainUrl();
void receiveRichEmote();
void receiveRichEdited_data();
void receiveRichEdited();
};
void TextHandlerTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
const auto json = QJsonDocument::fromJson(R"EVENT({
"account_data": {
"events": [
{
"content": {
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
},
{
"content": {
"custom_config_key": "custom_config_value"
},
"type": "org.example.custom.room.config"
}
]
},
"ephemeral": {
"events": [
{
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.typing"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an **example** text message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>",
"msgtype": "m.text"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "/me This is an emote.",
"format": "org.matrix.custom.html",
"formatted_body": "This is an emote.",
"msgtype": "m.emote"
},
"event_id": "$153273582443PhrSn:example.org",
"origin_server_ts": 1532735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1231
}
},
{
"content": {
"body": "tested",
"msgtype": "m.text"
},
"event_id": "$zrCiBxBnqqTn0Z5FY78qSZAszno_w8nJJXzfBULG-3E",
"origin_server_ts": 1680948575928,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1747776,
"m.relations": {
"m.replace": {
"event_id": "$UX0PlpyI7vYO32iHMuuYEP7ECMh4sX3XLGiB2SwM4mQ",
"origin_server_ts": 1680948580992,
"sender": "@example:example.org"
}
}
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
})EVENT");
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
room->update(std::move(roomData));
}
void TextHandlerTest::allowedAttributes()
{
const QString testInputString1 = QStringLiteral("<p><span data-mx-spoiler><font color=#FFFFFF>Test</font><span></p>");
const QString testOutputString1 = QStringLiteral("<p><span data-mx-spoiler><font color=#FFFFFF>Test</font><span></p>");
// Handle urls where the href has either single (') or double (") quotes.
const QString testInputString2 = QStringLiteral("<p><a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a></p>");
const QString testOutputString2 = QStringLiteral("<p><a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a></p>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString1);
QCOMPARE(testTextHandler.handleSendText(), testOutputString1);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString1);
testTextHandler.setData(testInputString2);
QCOMPARE(testTextHandler.handleSendText(), testOutputString2);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString2);
}
void TextHandlerTest::stripDisallowedTags()
{
const QString testInputString = QStringLiteral("<p>Allowed</p> <span>Allowed</span> <body>Disallowed</body>");
const QString testOutputString = QStringLiteral("<p>Allowed</p> <span>Allowed</span> Disallowed");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::stripDisallowedAttributes()
{
const QString testInputString = QStringLiteral("<p style=\"font-size:50px;\" color=#FFFFFF>Test</p>");
const QString testOutputString = QStringLiteral("<p>Test</p>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
/**
* Make sure that empty code tags are handled.
* (this was a bug during development hence the test)
*/
void TextHandlerTest::emptyCodeTags()
{
const QString testInputString = QStringLiteral("<pre><code></code></pre>");
const QString testOutputString = QStringLiteral("<pre><code></code></pre>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::sendSimpleStringCase()
{
const QString testInputString = QStringLiteral("This data should just be put in a paragraph.");
const QString testOutputString = QStringLiteral("<p>This data should just be put in a paragraph.</p>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendSingleParaMarkup()
{
const QString testInputString = QStringLiteral(
"Text para with **bold**, *italic*, [link](https://kde.org), ![image](mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e), `inline code`.");
const QString testOutputString = QStringLiteral(
"<p>Text para with <strong>bold</strong>, <em>italic</em>, <a href=\"https://kde.org\">link</a>, <img "
"src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e\" alt=\"image\">, <code>inline code</code>.</p>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendMultipleSectionMarkup()
{
const QString testInputString =
QStringLiteral("Text para\n> blockquote\n* List 1\n* List 2\n1. one\n2. two\n# Heading 1\n## Heading 2\nhorizontal rule\n\n---\n```\ncodeblock\n```");
const QString testOutputString = QStringLiteral(
"<p>Text para</p>\n<blockquote>\n<p>blockquote</p>\n</blockquote>\n<ul>\n<li>List 1</li>\n<li>List "
"2</li>\n</ul>\n<ol>\n<li>one</li>\n<li>two</li>\n</ol>\n<h1>Heading 1</h1>\n<h2>Heading 2</h2>\n<p>horizontal "
"rule</p>\n<hr>\n<pre><code>codeblock\n</code></pre>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendBadLinks()
{
const QString testInputString = QStringLiteral("[link](kde.org), ![image](https://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e)");
const QString testOutputString = QStringLiteral("<p><a>link</a>, <img alt=\"image\"></p>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
/**
* All text between code tags is treated as plain so it should get escaped.
*/
void TextHandlerTest::sendEscapeCode()
{
const QString testInputString = QStringLiteral("```\n<p>Test <span style=\"font-size:50px;\">some</span> code</p>\n```");
const QString testOutputString =
QStringLiteral("<pre><code>&lt;p&gt;Test &lt;span style=&quot;font-size:50px;&quot;&gt;some&lt;/span&gt; code&lt;/p&gt;\n</code></pre>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendCodeClass()
{
const QString testInputString = QStringLiteral("```html\nsome code\n```\n<pre><code class=\"code-underline\">some more code</code></pre>");
const QString testOutputString = QStringLiteral("<pre><code class=\"language-html\">some code\n</code></pre>\n<pre><code>some more code</code></pre>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::receiveStripReply()
{
const QString testInputString = QStringLiteral(
"<mx-reply><blockquote><a href=\"https://matrix.to/#/!somewhere:example.org/$event:example.org\">In reply to</a><a "
"href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a><br />Message replied to.</blockquote></mx-reply>Reply message.");
const QString testOutputString = QStringLiteral("Reply message.");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString);
}
void TextHandlerTest::receiveRichInPlainOut_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::newRow("ampersand") << QStringLiteral("a &amp; b") << QStringLiteral("a & b");
QTest::newRow("quote") << QStringLiteral("&quot;a and b&quot;") << QStringLiteral("\"a and b\"");
}
void TextHandlerTest::receiveRichInPlainOut()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testOutputString);
}
void TextHandlerTest::receivePlainTextIn()
{
const QString testInputString = QStringLiteral("<plain text in tag bracket>\nTest link https://kde.org.");
const QString testOutputStringRich = QStringLiteral("&lt;plain text in tag bracket&gt;<br>Test link <a href=\"https://kde.org\">https://kde.org</a>.");
QString testOutputStringPlain = QStringLiteral("<plain text in tag bracket>\nTest link https://kde.org.");
// Make sure quotes are maintained in a plain string.
const QString testInputString2 = QStringLiteral("last line is \"Time to switch to a new topic.\"");
const QString testOutputString2 = QStringLiteral("last line is \"Time to switch to a new topic.\"");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText), testOutputStringRich);
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputStringPlain);
testTextHandler.setData(testInputString2);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText), testOutputString2);
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString2);
}
void TextHandlerTest::receiveStripNewlines()
{
const QString testInputStringPlain = QStringLiteral("Test\nmany\nnew\nlines.");
const QString testInputStringRich = QStringLiteral("Test<br>many<br />new<br>lines.");
const QString testOutputString = QStringLiteral("Test many new lines.");
const QString testInputStringPlain2 = QStringLiteral("* List\n* Items");
const QString testOutputString2 = QStringLiteral("List Items");
TextHandler testTextHandler;
testTextHandler.setData(testInputStringPlain);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::PlainText, true), testOutputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText, nullptr, nullptr, true), testOutputString);
testTextHandler.setData(testInputStringRich);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText, true), testOutputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, nullptr, nullptr, true), testOutputString);
testTextHandler.setData(testInputStringPlain2);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText, true), testOutputString2);
}
/**
* For a plain text output of a received string all html is stripped except for
* code which is unescaped if it's html.
*/
void TextHandlerTest::receivePlainStripHtml()
{
const QString testInputString = QStringLiteral("<p>Test</p> <pre><code>Some code <strong>with tags</strong></code></pre>");
const QString testOutputString = QStringLiteral("Test Some code <strong>with tags</strong>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testOutputString);
}
void TextHandlerTest::receivePlainStripMarkup()
{
const QString testInputString = QStringLiteral("**bold** `<p>inline code</p>` *italic*");
const QString testOutputString = QStringLiteral("bold <p>inline code</p> italic");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString);
}
void TextHandlerTest::receiveRichUserPill()
{
const QString testInputString = QStringLiteral("<p><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></p>");
const QString testOutputString = QStringLiteral("<p><b><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></b></p>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::receiveRichStrikethrough()
{
const QString testInputString = QStringLiteral("<p><del>Test</del></p>");
const QString testOutputString = QStringLiteral("<p><s>Test</s></p>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::receiveRichtextIn()
{
const QString testInputString = QStringLiteral("<p>Test</p> <pre><code>Some code <strong>with tags</strong></code></pre>");
const QString testOutputString = QStringLiteral("<p>Test</p> <pre><code>Some code &lt;strong&gt;with tags&lt;/strong&gt;</code></pre>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::receiveRichMxcUrl()
{
const QString testInputString = QStringLiteral(
"<img src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e\" alt=\"image\"><img src=\"mxc://kde.org/34c3464b3a1bd7f55af2d559e07d2c773c430e73\" "
"alt=\"image\">");
const QString testOutputString = QStringLiteral(
"<img "
"src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e?user_id=@bob:kde.org&room_id=%23myroom:kde.org&event_id=$143273582443PhrSn:example.org\" "
"alt=\"image\"><img "
"src=\"mxc://kde.org/34c3464b3a1bd7f55af2d559e07d2c773c430e73?user_id=@bob:kde.org&room_id=%23myroom:kde.org&event_id=$143273582443PhrSn:example.org\" "
"alt=\"image\">");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(0).get()), testOutputString);
}
/**
* For when your rich input string has a plain text url left in.
*
* This test is to show that a url that is already rich will be left alone but a
* plain one will be linkified.
*/
void TextHandlerTest::receiveRichPlainUrl()
{
// This is an actual link that caused trouble which is why it's so long. Keeping
// so we can confirm consistent behaviour for complex urls.
const QString testInputStringLink1 = QStringLiteral(
"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im "
"<a "
"href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/"
"$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">Link already rich</a>");
const QString testOutputStringLink1 = QStringLiteral(
"<a "
"href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/"
"$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">https://matrix.to/#/"
"!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im</a> <a "
"href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/"
"$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">Link already rich</a>");
// Another real case. The linkification wasn't handling it when a single link
// contains what looks like and email. It was been broken into 3 but needs to
// be just single link.
const QString testInputStringLink2 = QStringLiteral("https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/");
const QString testOutputStringLink2 = QStringLiteral(
"<a "
"href=\"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/\">https://lore.kernel.org/lkml/"
"CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/</a>");
QString testInputStringEmail = QStringLiteral(R"(email@example.com <a href="mailto:email@example.com">Link already rich</a>)");
QString testOutputStringEmail =
QStringLiteral(R"(<a href="mailto:email@example.com">email@example.com</a> <a href="mailto:email@example.com">Link already rich</a>)");
QString testInputStringMxId = QStringLiteral("@user:kde.org <a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a>");
QString testOutputStringMxId = QStringLiteral(
"<b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a></b>");
TextHandler testTextHandler;
testTextHandler.setData(testInputStringLink1);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringLink1);
testTextHandler.setData(testInputStringLink2);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringLink2);
testTextHandler.setData(testInputStringEmail);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringEmail);
testTextHandler.setData(testInputStringMxId);
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

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

@@ -24,14 +24,12 @@ add_library(neochat STATIC
models/publicroomlistmodel.cpp
models/userdirectorylistmodel.cpp
models/keywordnotificationrulemodel.cpp
utils.cpp
notificationsmanager.cpp
models/sortfilterroomlistmodel.cpp
chatdocumenthandler.cpp
models/devicesmodel.cpp
filetypesingleton.cpp
login.cpp
stickerevent.cpp
models/webshortcutmodel.cpp
blurhash.cpp
blurhashimageprovider.cpp
@@ -47,8 +45,13 @@ add_library(neochat STATIC
models/statemodel.cpp
filetransferpseudojob.cpp
models/searchmodel.cpp
texthandler.cpp
pollevent.cpp
pollhandler.cpp
)
target_compile_definitions(neochat PUBLIC "-DQT_NO_KEYWORDS")
add_executable(neochat-app
main.cpp
res.qrc
@@ -60,14 +63,6 @@ target_link_libraries(neochat-app PRIVATE
neochat
)
if(Quotient_VERSION_MINOR GREATER 6)
target_compile_definitions(neochat PUBLIC QUOTIENT_07)
target_sources(neochat PRIVATE pollevent.cpp pollhandler.cpp)
else()
target_compile_definitions(neochat PUBLIC QUOTIENT_VERSION=\"${Quotient_VERSION}\")
target_sources(neochat PRIVATE neochataccountregistry.cpp)
endif()
ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
target_sources(neochat-app PRIVATE ${NEOCHAT_ICON})
@@ -95,6 +90,7 @@ endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient cmark::cmark ${QTKEYCHAIN_LIBRARIES} QCoro::Core)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
if(NEOCHAT_FLATPAK)
@@ -175,6 +171,10 @@ 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

@@ -20,25 +20,10 @@
#include "neochatroom.h"
#include "neochatuser.h"
#include "roommanager.h"
#include "texthandler.h"
using namespace Quotient;
QString markdownToHTML(const QString &markdown)
{
const auto str = markdown.toUtf8();
char *tmp_buf = cmark_markdown_to_html(str.constData(), str.size(), CMARK_OPT_HARDBREAKS | CMARK_OPT_UNSAFE);
const std::string html(tmp_buf);
free(tmp_buf);
auto result = QString::fromStdString(html).trimmed();
result.replace("<!-- raw HTML omitted -->", "");
return result;
}
ActionsHandler::ActionsHandler(QObject *parent)
: QObject(parent)
{
@@ -169,7 +154,10 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
}
handledText = CustomEmojiModel::instance().preprocessText(handledText);
handledText = markdownToHTML(handledText);
TextHandler textHandler;
textHandler.setData(handledText);
handledText = textHandler.handleSendText();
if (handledText.count("<p>") == 1 && handledText.count("</p>") == 1) {
handledText.remove("<p>");
handledText.remove("</p>");

View File

@@ -50,5 +50,3 @@ private:
QString handleMentions(QString handledText, const bool &isEdit = false);
void handleMessage(const QString &text, QString handledText, const bool &isEdit = false);
};
QString markdownToHTML(const QString &markdown);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,23 +11,20 @@ using namespace Quotient;
MessageFilterModel::MessageFilterModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
connect(NeoChatConfig::self(), &NeoChatConfig::ShowStateEventChanged, this, [this] {
invalidateFilter();
});
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLeaveJoinEventChanged, this, [this] {
beginResetModel();
endResetModel();
invalidateFilter();
});
connect(NeoChatConfig::self(), &NeoChatConfig::ShowRenameChanged, this, [this] {
beginResetModel();
endResetModel();
invalidateFilter();
});
connect(NeoChatConfig::self(), &NeoChatConfig::ShowAvatarUpdateChanged, this, [this] {
beginResetModel();
endResetModel();
invalidateFilter();
});
connect(NeoChatConfig::self(), &NeoChatConfig::ShowDeletedMessagesChanged, this, [this] {
beginResetModel();
endResetModel();
invalidateFilter();
});
}
@@ -35,31 +32,20 @@ bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour
{
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
const int specialMarks = index.data(MessageEventModel::SpecialMarksRole).toInt();
if (index.data(MessageEventModel::IsNameChangeRole).toBool() && !NeoChatConfig::self()->showRename()) {
return false;
}
if (index.data(MessageEventModel::IsAvatarChangeRole).toBool() && !NeoChatConfig::self()->showAvatarUpdate()) {
return false;
}
if (index.data(MessageEventModel::IsRedactedRole).toBool() && !NeoChatConfig::self()->showDeletedMessages()) {
return false;
}
const int specialMarks = index.data(MessageEventModel::SpecialMarksRole).toInt();
if (specialMarks == EventStatus::Hidden || specialMarks == EventStatus::Replaced) {
return false;
}
const auto eventType = index.data(MessageEventModel::EventTypeRole).toInt();
const auto eventType = index.data(MessageEventModel::DelegateTypeRole).toInt();
if (eventType == MessageEventModel::Other) {
return false;
}
if (!NeoChatConfig::self()->showLeaveJoinEvent() && eventType == MessageEventModel::State) {
return false;
}
return true;
}

View File

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

View File

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

View File

@@ -107,14 +107,10 @@ private:
QString m_activeSpaceId = "";
void connectRoomSignals(NeoChatRoom *room);
#ifndef QUOTIENT_07
void handleNotifications();
#endif
Q_SIGNALS:
void connectionChanged();
void notificationCountChanged();
void roomAdded(NeoChatRoom *_t1);
void newHighlight(const QString &_t1, const QString &_t2, const QString &_t3, const QString &_t4, const QString &_t5, const QImage &_t6);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,10 +29,6 @@
<label>Merge Room Lists</label>
<default>false</default>
</entry>
<entry name="ShowLeaveJoinEvent" type="bool">
<label>Show leave and join events in the timeline</label>
<default>true</default>
</entry>
<entry name="AllowQuickEdit" type="bool">
<label>Use s/text/replacement syntax to edit your last message.</label>
<default>false</default>
@@ -72,6 +68,14 @@
<label>Use a compact room list layout</label>
<default>false</default>
</entry>
<entry name="ShowStateEvent" type="bool">
<label>Show state events in the timeline</label>
<default>true</default>
</entry>
<entry name="ShowLeaveJoinEvent" type="bool">
<label>Show leave and join events in the timeline</label>
<default>true</default>
</entry>
<entry name="ShowRename" type="bool">
<label>Show rename events in the timeline</label>
<default>true</default>

View File

@@ -8,14 +8,15 @@
#include <QMimeDatabase>
#include <QTemporaryFile>
#include <QTextDocument>
#include <functional>
#include <QMediaMetaData>
#include <QMediaPlayer>
#include <jobs/basejob.h>
#include <qcoro/qcorosignal.h>
#include <connection.h>
#include <csapi/account-data.h>
#include <csapi/directory.h>
#include <csapi/pushrules.h>
#include <csapi/redaction.h>
@@ -23,6 +24,7 @@
#include <csapi/room_state.h>
#include <csapi/typing.h>
#include <events/encryptionevent.h>
#include <events/eventrelation.h>
#include <events/reactionevent.h>
#include <events/redactionevent.h>
#include <events/roomavatarevent.h>
@@ -30,24 +32,20 @@
#include <events/roommemberevent.h>
#include <events/roompowerlevelsevent.h>
#include <events/simplestateevents.h>
#include <events/stickerevent.h>
#include <eventstats.h>
#include <jobs/downloadfilejob.h>
#ifndef QUOTIENT_07
#include <joinstate.h>
#endif
#include <qt_connection_util.h>
#include "controller.h"
#include "filetransferpseudojob.h"
#include "joinrulesevent.h"
#include "neochatconfig.h"
#include "neochatuser.h"
#include "notificationsmanager.h"
#ifdef QUOTIENT_07
#include "pollevent.h"
#include "pollhandler.h"
#endif
#include "filetransferpseudojob.h"
#include "stickerevent.h"
#include "utils.h"
#include "texthandler.h"
#ifndef Q_OS_ANDROID
#include <KIO/Job>
@@ -84,7 +82,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
if (this->joinState() != JoinState::Invite) {
return;
}
const QString senderId = getCurrentState<RoomMemberEvent>(localUser()->id())->senderId();
const QString senderId = currentState().get<RoomMemberEvent>(localUser()->id())->senderId();
QImage avatar_image;
if (!user(senderId)->avatarUrl(this).isEmpty()) {
avatar_image = user(senderId)->avatar(128, this);
@@ -98,6 +96,42 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
Q_EMIT canEncryptRoomChanged();
});
connect(connection, &Connection::capabilitiesLoaded, this, &NeoChatRoom::maxRoomVersionChanged);
connect(this, &Room::changed, this, [this]() {
Q_EMIT defaultUrlPreviewStateChanged();
});
connect(this, &Room::accountDataChanged, this, [this](QString type) {
if (type == "org.matrix.room.preview_urls") {
Q_EMIT urlPreviewEnabledChanged();
}
});
}
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)
@@ -137,17 +171,9 @@ QCoro::Task<void> NeoChatRoom::doUploadFile(QUrl url, QString body)
} else {
content = new EventContent::FileContent(url, fileInfo.size(), mime, fileInfo.fileName());
}
#ifdef QUOTIENT_07
QString txnId = postFile(body.isEmpty() ? url.fileName() : body, content);
#else
QString txnId = postFile(body.isEmpty() ? url.fileName() : body, url, false);
#endif
setHasFileUploading(true);
#ifdef QUOTIENT_07
connect(this, &Room::fileTransferCompleted, [this, txnId](const QString &id, FileSourceInfo) {
#else
connect(this, &Room::fileTransferCompleted, [this, txnId](const QString &id, const QUrl & /*localFile*/, const QUrl & /*mxcUrl*/) {
#endif
if (id == txnId) {
setFileUploadingProgress(0);
setHasFileUploading(false);
@@ -205,7 +231,7 @@ void NeoChatRoom::sendTypingNotification(bool isTyping)
connection()->callApi<SetTypingJob>(BackgroundRequest, localUser()->id(), id(), isTyping, 10000);
}
const RoomEvent *NeoChatRoom::lastEvent(bool ignoreStateEvent) const
const RoomEvent *NeoChatRoom::lastEvent() const
{
for (auto timelineItem = messageEvents().rbegin(); timelineItem < messageEvents().rend(); timelineItem++) {
const RoomEvent *event = timelineItem->get();
@@ -217,8 +243,20 @@ const RoomEvent *NeoChatRoom::lastEvent(bool ignoreStateEvent) const
continue;
}
if (event->isStateEvent()
&& (ignoreStateEvent || !NeoChatConfig::self()->showLeaveJoinEvent() || static_cast<const StateEventBase &>(*event).repeatsState())) {
if (event->isStateEvent() && !NeoChatConfig::showStateEvent()) {
continue;
}
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(event)) {
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::showLeaveJoinEvent()) {
continue;
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::showRename()) {
continue;
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::showAvatarUpdate()) {
continue;
}
}
if (event->isStateEvent() && static_cast<const StateEvent &>(*event).repeatsState()) {
continue;
}
@@ -232,14 +270,20 @@ const RoomEvent *NeoChatRoom::lastEvent(bool ignoreStateEvent) const
continue;
}
#ifdef QUOTIENT_07
if (auto lastEvent = eventCast<const StateEvent>(event)) {
#else
if (auto lastEvent = eventCast<const StateEventBase>(event)) {
#endif
return lastEvent;
}
if (auto lastEvent = eventCast<const RoomMessageEvent>(event)) {
return lastEvent;
}
#ifdef QUOTIENT_07
if (auto lastEvent = eventCast<const PollStartEvent>(event)) {
return lastEvent;
}
#endif
}
return nullptr;
}
@@ -257,12 +301,12 @@ bool NeoChatRoom::lastEventIsSpoiler() const
return false;
}
QString NeoChatRoom::lastEventToString() const
QString NeoChatRoom::lastEventToString(Qt::TextFormat format, bool stripNewlines) const
{
if (auto event = lastEvent()) {
return roomMembername(event->senderId()) + (event->isStateEvent() ? " " : ": ") + eventToString(*event);
return safeMemberName(event->senderId()) + (event->isStateEvent() ? " " : ": ") + eventToString(*event, format, stripNewlines);
}
return QLatin1String("");
return {};
}
bool NeoChatRoom::isEventHighlighted(const RoomEvent *e) const
@@ -278,7 +322,7 @@ void NeoChatRoom::checkForHighlights(const Quotient::TimelineItem &ti)
}
if (auto *e = ti.viewAs<RoomMessageEvent>()) {
const auto &text = e->plainBody();
if (text.contains(localUserId) || text.contains(roomMembername(localUserId))) {
if (text.contains(localUserId) || text.contains(safeMemberName(localUserId))) {
highlights.insert(e);
}
}
@@ -301,7 +345,7 @@ void NeoChatRoom::onAddHistoricalTimelineEvents(rev_iter_t from)
void NeoChatRoom::onRedaction(const RoomEvent &prevEvent, const RoomEvent & /*after*/)
{
if (const auto &e = eventCast<const ReactionEvent>(&prevEvent)) {
if (auto relatedEventId = e->relation().eventId; !relatedEventId.isEmpty()) {
if (auto relatedEventId = e->eventId(); !relatedEventId.isEmpty()) {
Q_EMIT updatedEvent(relatedEventId);
}
}
@@ -309,9 +353,8 @@ void NeoChatRoom::onRedaction(const RoomEvent &prevEvent, const RoomEvent & /*af
void NeoChatRoom::countChanged()
{
if (displayed() && !hasUnreadMessages()) {
resetNotificationCount();
resetHighlightCount();
if (displayed() && unreadStats().empty()) {
setReadReceipt(lastEvent()->id());
}
}
@@ -321,7 +364,7 @@ QDateTime NeoChatRoom::lastActiveTime()
return QDateTime();
}
if (auto event = lastEvent(true)) {
if (auto event = lastEvent()) {
return event->originTimestamp();
}
@@ -329,69 +372,6 @@ QDateTime NeoChatRoom::lastActiveTime()
return messageEvents().rbegin()->get()->originTimestamp();
}
QString NeoChatRoom::subtitleText()
{
static const QRegularExpression blockquote("(\r\n\t|\n|\r\t|)> ");
static const QRegularExpression heading("(\r\n\t|\n|\r\t|)\\#{1,6} ");
static const QRegularExpression newlines("(\r\n\t|\n|\r\t|\r\n)");
static const QRegularExpression bold1("(\\*\\*|__)(?=\\S)([^\\r]*\\S)\\1");
static const QRegularExpression bold2("(\\*|_)(?=\\S)([^\\r]*\\S)\\1");
static const QRegularExpression strike1("~~(.*)~~");
static const QRegularExpression strike2("~(.*)~");
static const QRegularExpression del("<del>(.*)</del>");
static const QRegularExpression multileLineCode("```([^```]+)```");
static const QRegularExpression singleLinecode("`([^`]+)`");
QString subtitle = lastEventToString().size() == 0 ? topic() : lastEventToString();
subtitle
// replace blockquote, i.e. '> text'
.replace(blockquote, " ")
// replace headings, i.e. "# text"
.replace(heading, " ")
// replace newlines
.replace(newlines, " ")
// replace '**text**' and '__text__'
.replace(bold1, "\\2")
// replace '*text*' and '_text_'
.replace(bold2, "\\2")
// replace '~~text~~'
.replace(strike1, "\\1")
// replace '~text~'
.replace(strike2, "\\1")
// replace '<del>text</del>'
.replace(del, "\\1")
// replace '```code```'
.replace(multileLineCode, "\\1")
// replace '`code`'
.replace(singleLinecode, "\\1");
return subtitle.size() > 0 ? subtitle : QStringLiteral(" ");
}
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();
@@ -425,15 +405,6 @@ QVariantMap NeoChatRoom::getUser(const QString &userID) const
{QStringLiteral("color"), user.color()}};
}
QUrl NeoChatRoom::urlToMxcUrl(const QUrl &mxcUrl)
{
#ifdef QUOTIENT_07
return connection()->makeMediaUrl(mxcUrl);
#else
return DownloadFileJob::makeRequestUrl(connection()->homeserver(), mxcUrl);
#endif
}
QString NeoChatRoom::avatarMediaId() const
{
if (const auto avatar = Room::avatarMediaId(); !avatar.isEmpty()) {
@@ -451,87 +422,71 @@ QString NeoChatRoom::avatarMediaId() const
return {};
}
QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format, bool removeReply) const
QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format, bool stripNewlines) const
{
const bool prettyPrint = (format == Qt::RichText);
using namespace Quotient;
#ifdef QUOTIENT_07
return switchOnType(
#else
return visit(
#endif
evt,
[this, prettyPrint, removeReply](const RoomMessageEvent &e) {
[this, format, stripNewlines](const RoomMessageEvent &e) {
using namespace MessageEventContent;
// 1. prettyPrint/HTML
if (prettyPrint && e.hasTextContent() && e.mimeType().name() != "text/plain") {
auto htmlBody = static_cast<const TextContent *>(e.content())->body;
if (removeReply) {
htmlBody.remove(utils::removeRichReplyRegex);
}
htmlBody.replace(utils::userPillRegExp, R"(<b class="user-pill">\1</b>)");
htmlBody.replace(utils::strikethroughRegExp, "<s>\\1</s>");
auto url = connection()->homeserver();
auto base = url.scheme() + QStringLiteral("://") + url.host() + (url.port() != -1 ? ':' + QString::number(url.port()) : QString());
htmlBody.replace(utils::mxcImageRegExp, QStringLiteral(R"(<img \1 src="%1/_matrix/media/r0/download/\2/\3" \4 > )").arg(base));
return htmlBody;
}
TextHandler textHandler;
if (e.hasFileContent()) {
auto fileCaption = e.content()->fileInfo()->originalName.toHtmlEscaped();
auto fileCaption = e.content()->fileInfo()->originalName;
if (fileCaption.isEmpty()) {
fileCaption = prettyPrint ? Quotient::prettyPrint(e.plainBody()) : e.plainBody();
fileCaption = e.plainBody();
} else if (e.content()->fileInfo()->originalName != e.plainBody()) {
fileCaption = e.plainBody() + " | " + fileCaption;
}
return !fileCaption.isEmpty() ? fileCaption : i18n("a file");
textHandler.setData(fileCaption);
return !fileCaption.isEmpty() ? textHandler.handleRecievePlainText(Qt::PlainText, stripNewlines) : i18n("a file");
}
// 2. prettyPrint/text 3. plainText/HTML 4. plainText/text
QString plainBody;
if (e.hasTextContent() && e.content() && e.mimeType().name() == "text/plain") { // 2/4
plainBody = static_cast<const TextContent *>(e.content())->body;
} else { // 3
plainBody = e.plainBody();
QString body;
if (e.hasTextContent() && e.content()) {
body = static_cast<const TextContent *>(e.content())->body;
} else {
body = e.plainBody();
}
if (prettyPrint) {
if (removeReply) {
plainBody.remove(utils::removeReplyRegex);
}
return Quotient::prettyPrint(plainBody);
textHandler.setData(body);
Qt::TextFormat inputFormat;
if (e.mimeType().name() == "text/plain") {
inputFormat = Qt::PlainText;
} else {
inputFormat = Qt::RichText;
}
if (removeReply) {
return plainBody.remove(utils::removeReplyRegex);
if (format == Qt::RichText) {
return textHandler.handleRecieveRichText(inputFormat, this, &e, stripNewlines);
} else {
return textHandler.handleRecievePlainText(inputFormat, stripNewlines);
}
return plainBody;
},
[](const StickerEvent &e) {
return e.body();
},
[this](const RoomMemberEvent &e) {
[this, prettyPrint](const RoomMemberEvent &e) {
// FIXME: Rewind to the name that was at the time of this event
auto subjectName = this->htmlSafeMemberName(e.userId());
if (e.membership() == MembershipType::Leave) {
#ifdef QUOTIENT_07
if (e.membership() == Membership::Leave) {
if (e.prevContent() && e.prevContent()->displayName) {
subjectName = sanitized(*e.prevContent()->displayName).toHtmlEscaped();
#else
if (e.prevContent() && e.prevContent()->displayName.isEmpty()) {
subjectName = sanitized(e.prevContent()->displayName).toHtmlEscaped();
#endif
}
}
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
.arg(e.userId(), static_cast<NeoChatUser *>(user(e.userId()))->color().name(), subjectName);
if (prettyPrint) {
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
.arg(e.userId(), static_cast<NeoChatUser *>(user(e.userId()))->color().name(), subjectName);
}
// The below code assumes senderName output in AuthorRole
switch (e.membership()) {
case MembershipType::Invite:
case Membership::Invite:
if (e.repeatsState()) {
auto text = i18n("reinvited %1 to the room", subjectName);
if (!e.reason().isEmpty()) {
@@ -540,13 +495,13 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
return text;
}
Q_FALLTHROUGH();
case MembershipType::Join: {
case Membership::Join: {
QString text{};
// Part 1: invites and joins
if (e.repeatsState()) {
text = i18n("joined the room (repeated)");
} else if (e.changesMembership()) {
text = e.membership() == MembershipType::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
text = e.membership() == Membership::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
}
if (!text.isEmpty()) {
if (!e.reason().isEmpty()) {
@@ -556,23 +511,19 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
}
// Part 2: profile changes of joined members
if (e.isRename()) {
if (e.displayName().isEmpty()) {
if (e.newDisplayName()) {
text = i18nc("their refers to a singular user", "cleared their display name");
} else {
text = i18nc("their refers to a singular user", "changed their display name to %1", e.displayName().toHtmlEscaped());
text = i18nc("their refers to a singular user", "changed their display name to %1", e.newDisplayName()->toHtmlEscaped());
}
}
if (e.isAvatarUpdate()) {
if (!text.isEmpty()) {
text += i18n(" and ");
}
if (e.avatarUrl().isEmpty()) {
if (e.newAvatarUrl()) {
text += i18nc("their refers to a singular user", "cleared their avatar");
#ifdef QUOTIENT_07
} else if (!e.prevContent()->avatarUrl) {
#else
} else if (e.prevContent()->avatarUrl.isEmpty()) {
#endif
text += i18n("set an avatar");
} else {
text += i18nc("their refers to a singular user", "updated their avatar");
@@ -583,18 +534,18 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
}
return text;
}
case MembershipType::Leave:
if (e.prevContent() && e.prevContent()->membership == MembershipType::Invite) {
case Membership::Leave:
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
return (e.senderId() != e.userId()) ? i18n("withdrew %1's invitation", subjectName) : i18n("rejected the invitation");
}
if (e.prevContent() && e.prevContent()->membership == MembershipType::Ban) {
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
return (e.senderId() != e.userId()) ? i18n("unbanned %1", subjectName) : i18n("self-unbanned");
}
return (e.senderId() != e.userId())
? i18n("has put %1 out of the room: %2", subjectName, e.contentJson()["reason"_ls].toString().toHtmlEscaped())
: i18n("left the room");
case MembershipType::Ban:
case Membership::Ban:
if (e.senderId() != e.userId()) {
if (e.reason().isEmpty()) {
return i18n("banned %1 from the room", subjectName);
@@ -604,7 +555,7 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
} else {
return i18n("self-banned from the room");
}
case MembershipType::Knock: {
case Membership::Knock: {
QString reason(e.contentJson()["reason"_ls].toString().toHtmlEscaped());
return reason.isEmpty() ? i18n("requested an invite") : i18n("requested an invite with reason: %1", reason);
}
@@ -618,8 +569,12 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
[](const RoomNameEvent &e) {
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name to: %1", e.name().toHtmlEscaped());
},
[prettyPrint](const RoomTopicEvent &e) {
return (e.topic().isEmpty()) ? i18n("cleared the topic") : i18n("set the topic to: %1", prettyPrint ? Quotient::prettyPrint(e.topic()) : e.topic());
[prettyPrint, stripNewlines](const RoomTopicEvent &e) {
return (e.topic().isEmpty()) ? i18n("cleared the topic")
: i18n("set the topic to: %1",
prettyPrint ? Quotient::prettyPrint(e.topic())
: stripNewlines ? e.topic().replace(u'\n', u' ')
: e.topic());
},
[](const RoomAvatarEvent &) {
return i18n("changed the room avatar");
@@ -634,7 +589,7 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
[](const RoomPowerLevelsEvent &) {
return i18nc("'power level' means permission level", "changed the power levels for this room");
},
[](const StateEventBase &e) {
[](const StateEvent &e) {
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
return i18n("changed the server access control lists for this room");
}
@@ -650,21 +605,15 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
: i18n("updated %1 state for %2", e.matrixType(), e.stateKey().toHtmlEscaped());
},
#ifdef QUOTIENT_07
[](const PollStartEvent &e) {
return e.question();
},
#endif
i18n("Unknown event"));
}
QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
{
#ifdef QUOTIENT_07
return switchOnType(
#else
return visit(
#endif
evt,
[](const RoomMessageEvent &e) {
Q_UNUSED(e)
@@ -676,25 +625,25 @@ QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
},
[](const RoomMemberEvent &e) {
switch (e.membership()) {
case MembershipType::Invite:
case Membership::Invite:
if (e.repeatsState()) {
return i18n("reinvited someone to the room");
}
Q_FALLTHROUGH();
case MembershipType::Join: {
case Membership::Join: {
QString text{};
// Part 1: invites and joins
if (e.repeatsState()) {
text = i18n("joined the room (repeated)");
} else if (e.changesMembership()) {
text = e.membership() == MembershipType::Invite ? i18n("invited someone to the room") : i18n("joined the room");
text = e.membership() == Membership::Invite ? i18n("invited someone to the room") : i18n("joined the room");
}
if (!text.isEmpty()) {
return text;
}
// Part 2: profile changes of joined members
if (e.isRename()) {
if (e.displayName().isEmpty()) {
if (e.newDisplayName()) {
text = i18nc("their refers to a singular user", "cleared their display name");
} else {
text = i18nc("their refers to a singular user", "changed their display name");
@@ -704,13 +653,9 @@ QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
if (!text.isEmpty()) {
text += i18n(" and ");
}
if (e.avatarUrl().isEmpty()) {
if (e.newAvatarUrl()) {
text += i18nc("their refers to a singular user", "cleared their avatar");
#ifdef QUOTIENT_07
} else if (!e.prevContent()->avatarUrl) {
#else
} else if (e.prevContent()->avatarUrl.isEmpty()) {
#endif
text += i18n("set an avatar");
} else {
text += i18nc("their refers to a singular user", "updated their avatar");
@@ -721,22 +666,22 @@ QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
}
return text;
}
case MembershipType::Leave:
if (e.prevContent() && e.prevContent()->membership == MembershipType::Invite) {
case Membership::Leave:
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
return (e.senderId() != e.userId()) ? i18n("withdrew a user's invitation") : i18n("rejected the invitation");
}
if (e.prevContent() && e.prevContent()->membership == MembershipType::Ban) {
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
return (e.senderId() != e.userId()) ? i18n("unbanned a user") : i18n("self-unbanned");
}
return (e.senderId() != e.userId()) ? i18n("put a user out of the room") : i18n("left the room");
case MembershipType::Ban:
case Membership::Ban:
if (e.senderId() != e.userId()) {
return i18n("banned a user from the room");
} else {
return i18n("self-banned from the room");
}
case MembershipType::Knock: {
case Membership::Knock: {
return i18n("requested an invite");
}
default:;
@@ -764,7 +709,7 @@ QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
[](const RoomPowerLevelsEvent &) {
return i18nc("'power level' means permission level", "changed the power levels for this room");
},
[](const StateEventBase &e) {
[](const StateEvent &e) {
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
return i18n("changed the server access control lists for this room");
}
@@ -779,56 +724,23 @@ QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
}
return i18n("updated the state");
},
#ifdef QUOTIENT_07
[](const PollStartEvent &e) {
Q_UNUSED(e);
return i18n("started a poll");
},
#endif
i18n("Unknown event"));
}
void NeoChatRoom::changeAvatar(const QUrl &localFile)
{
const auto job = connection()->uploadFile(localFile.toLocalFile());
#ifdef QUOTIENT_07
if (isJobPending(job)) {
#else
if (isJobRunning(job)) {
#endif
connect(job, &BaseJob::success, this, [this, job] {
#ifdef QUOTIENT_07
connection()->callApi<SetRoomStateWithKeyJob>(id(), "m.room.avatar", QString(), QJsonObject{{"url", job->contentUri().toString()}});
#else
connection()->callApi<SetRoomStateWithKeyJob>(id(), "m.room.avatar", QString(), QJsonObject{{"url", job->contentUri()}});
#endif
});
}
}
void NeoChatRoom::addLocalAlias(const QString &alias)
{
auto a = aliases();
if (a.contains(alias)) {
return;
}
a += alias;
setLocalAliases(a);
}
void NeoChatRoom::removeLocalAlias(const QString &alias)
{
auto a = aliases();
if (!a.contains(alias)) {
return;
}
a.removeAll(alias);
setLocalAliases(a);
}
QString msgTypeToString(MessageEventType msgType)
{
switch (msgType) {
@@ -933,11 +845,11 @@ void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction
QStringList redactEventIds; // What if there are multiple reaction events?
const auto &annotations = relatedEvents(evt, EventRelation::Annotation());
const auto &annotations = relatedEvents(evt, EventRelation::AnnotationType);
if (!annotations.isEmpty()) {
for (const auto &a : annotations) {
if (auto e = eventCast<const ReactionEvent>(a)) {
if (e->relation().key != reaction) {
if (e->key() != reaction) {
continue;
}
@@ -960,18 +872,15 @@ void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction
bool NeoChatRoom::containsUser(const QString &userID) const
{
auto u = Room::user(userID);
if (!u) {
return false;
}
return Room::memberJoinState(u) != JoinState::Leave;
return !isMember(userID);
}
bool NeoChatRoom::canSendEvent(const QString &eventType) const
{
auto plEvent = getCurrentState<RoomPowerLevelsEvent>();
auto plEvent = currentState().get<RoomPowerLevelsEvent>();
if (!plEvent) {
return true;
}
auto pl = plEvent->powerLevelForEvent(eventType);
auto currentPl = plEvent->powerLevelForUser(localUser()->id());
@@ -980,28 +889,19 @@ bool NeoChatRoom::canSendEvent(const QString &eventType) const
bool NeoChatRoom::canSendState(const QString &eventType) const
{
auto plEvent = getCurrentState<RoomPowerLevelsEvent>();
auto plEvent = currentState().get<RoomPowerLevelsEvent>();
if (!plEvent) {
return false;
}
auto pl = plEvent->powerLevelForState(eventType);
auto currentPl = plEvent->powerLevelForUser(localUser()->id());
#ifndef QUOTIENT_07
if (eventType == "m.room.history_visibility") {
return false;
} else {
return currentPl >= pl;
}
#else
return currentPl >= pl;
#endif
}
bool NeoChatRoom::readMarkerLoaded() const
{
#ifdef QUOTIENT_07
const auto it = findInTimeline(lastFullyReadEventId());
#else
const auto it = findInTimeline(readMarkerEventId());
#endif
return it != historyEdge();
}
@@ -1012,12 +912,7 @@ bool NeoChatRoom::isInvite() const
bool NeoChatRoom::isUserBanned(const QString &user) const
{
return getCurrentState<RoomMemberEvent>(user)->membership() == MembershipType::Ban;
}
QString NeoChatRoom::htmlSafeName() const
{
return name().toHtmlEscaped();
return currentState().get<RoomMemberEvent>(user)->membership() == Membership::Ban;
}
QString NeoChatRoom::htmlSafeDisplayName() const
@@ -1032,7 +927,7 @@ void NeoChatRoom::deleteMessagesByUser(const QString &user, const QString &reaso
QString NeoChatRoom::joinRule() const
{
return getCurrentState<JoinRulesEvent>()->joinRule();
return currentState().get<JoinRulesEvent>()->joinRule();
}
void NeoChatRoom::setJoinRule(const QString &joinRule)
@@ -1041,21 +936,13 @@ void NeoChatRoom::setJoinRule(const QString &joinRule)
qWarning() << "Power level too low to set join rules";
return;
}
#ifdef QUOTIENT_07
setState("m.room.join_rules", "", QJsonObject{{"join_rule", joinRule}});
#else
setState<JoinRulesEvent>(QJsonObject{{"type", "m.room.join_rules"}, {"state_key", ""}, {"content", QJsonObject{{"join_rule", joinRule}}}});
#endif
// Not emitting joinRuleChanged() here, since that would override the change in the UI with the *current* value, which is not the *new* value.
}
QString NeoChatRoom::historyVisibility() const
{
#ifdef QUOTIENT_07
return currentState().get("m.room.history_visibility")->contentJson()["history_visibility"_ls].toString();
#else
return getCurrentState("m.room.history_visibility")->contentJson()["history_visibility"_ls].toString();
#endif
}
void NeoChatRoom::setHistoryVisibility(const QString &historyVisibilityRule)
@@ -1065,20 +952,85 @@ void NeoChatRoom::setHistoryVisibility(const QString &historyVisibilityRule)
return;
}
#ifdef QUOTIENT_07
setState("m.room.history_visibility", "", QJsonObject{{"history_visibility", historyVisibilityRule}});
#else
qWarning() << "Quotient 0.7 required to set history visibility";
return;
#endif
// Not emitting historyVisibilityChanged() here, since that would override the change in the UI with the *current* value, which is not the *new* value.
}
int NeoChatRoom::getUserPowerLevel(const QString &userId) const
bool NeoChatRoom::defaultUrlPreviewState() const
{
auto powerLevelEvent = getCurrentState<RoomPowerLevelsEvent>();
return powerLevelEvent->powerLevelForUser(userId);
auto urlPreviewsDisabled = currentState().get("org.matrix.room.preview_urls");
// Some rooms will not have this state event set so check for a nullptr return.
if (urlPreviewsDisabled != nullptr) {
return !urlPreviewsDisabled->contentJson()["disable"].toBool();
} else {
return false;
}
}
void NeoChatRoom::setDefaultUrlPreviewState(const bool &defaultUrlPreviewState)
{
if (!canSendState("org.matrix.room.preview_urls")) {
qWarning() << "Power level too low to set the default URL preview state for the room";
return;
}
/**
* Note the org.matrix.room.preview_urls room state event is completely undocumented
* so here it is because I'm nice.
*
* Also note this is a different event to org.matrix.room.preview_urls for room
* account data, because even though it has the same name and content it's totally different.
*
* {
* "content": {
* "disable": false
* },
* "origin_server_ts": 1673115224071,
* "sender": "@bob:kde.org",
* "state_key": "",
* "type": "org.matrix.room.preview_urls",
* "unsigned": {
* "replaces_state": "replaced_event_id",
* "prev_content": {
* "disable": true
* },
* "prev_sender": "@jeff:kde.org",
* "age": 99
* },
* "event_id": "$event_id",
* "room_id": "!room_id:kde.org"
* }
*
* You just have to set disable to true to disable URL previews by default.
*/
setState("org.matrix.room.preview_urls", "", QJsonObject{{"disable", !defaultUrlPreviewState}});
}
bool NeoChatRoom::urlPreviewEnabled() const
{
if (hasAccountData("org.matrix.room.preview_urls")) {
return !accountData("org.matrix.room.preview_urls")->contentJson()["disable"].toBool();
} else {
return defaultUrlPreviewState();
}
}
void NeoChatRoom::setUrlPreviewEnabled(const bool &urlPreviewEnabled)
{
/**
* Once again this is undocumented and even though the name and content are the
* same this is a different event to the org.matrix.room.preview_urls room state event.
*
* {
* "content": {
* "disable": true
* }
* "type": "org.matrix.room.preview_urls",
* }
*/
connection()->callApi<SetAccountDataPerRoomJob>(localUser()->id(), id(), "org.matrix.room.preview_urls", QJsonObject{{"disable", !urlPreviewEnabled}});
}
void NeoChatRoom::setUserPowerLevel(const QString &userID, const int &powerLevel)
@@ -1091,42 +1043,32 @@ void NeoChatRoom::setUserPowerLevel(const QString &userID, const int &powerLevel
qWarning() << "Power level too low to set user power levels";
return;
}
#ifdef QUOTIENT_07
if (!isMember(userID)) {
#else
if (memberJoinState(user(userID)) == JoinState::Join) {
#endif
qWarning() << "User is not a member of this room so power level cannot be set";
return;
}
int clampPowerLevel = std::clamp(powerLevel, 0, 100);
#ifdef QUOTIENT_07
auto powerLevelContent = currentState().get("m.room.power_levels")->contentJson();
#else
auto powerLevelContent = getCurrentState<RoomPowerLevelsEvent>()->contentJson();
#endif
auto powerLevelUserOverrides = powerLevelContent["users"].toObject();
if (powerLevelUserOverrides[userID] != clampPowerLevel) {
powerLevelUserOverrides[userID] = clampPowerLevel;
powerLevelContent["users"] = powerLevelUserOverrides;
#ifdef QUOTIENT_07
setState("m.room.power_levels", "", powerLevelContent);
#else
setState<RoomPowerLevelsEvent>(QJsonObject{{"type", "m.room.power_levels"}, {"state_key", ""}, {"content", powerLevelContent}});
#endif
}
}
int NeoChatRoom::getUserPowerLevel(const QString &userId) const
{
auto powerLevelEvent = getCurrentState<RoomPowerLevelsEvent>();
return powerLevelEvent->powerLevelForUser(userId);
}
int NeoChatRoom::powerLevel(const QString &eventName, const bool &isStateEvent) const
{
#ifdef QUOTIENT_07
const auto powerLevelEvent = currentState().get<RoomPowerLevelsEvent>();
#else
const auto powerLevelEvent = getCurrentState<RoomPowerLevelsEvent>();
#endif
if (eventName == "ban") {
return powerLevelEvent->ban();
} else if (eventName == "kick") {
@@ -1150,11 +1092,7 @@ int NeoChatRoom::powerLevel(const QString &eventName, const bool &isStateEvent)
void NeoChatRoom::setPowerLevel(const QString &eventName, const int &newPowerLevel, const bool &isStateEvent)
{
#ifdef QUOTIENT_07
auto powerLevelContent = currentState().get("m.room.power_levels")->contentJson();
#else
auto powerLevelContent = getCurrentState<RoomPowerLevelsEvent>()->contentJson();
#endif
int clampPowerLevel = std::clamp(newPowerLevel, 0, 100);
int powerLevel = 0;
@@ -1183,11 +1121,7 @@ void NeoChatRoom::setPowerLevel(const QString &eventName, const int &newPowerLev
}
}
#ifdef QUOTIENT_07
setState("m.room.power_levels", "", powerLevelContent);
#else
setState<RoomPowerLevelsEvent>(QJsonObject{{"type", "m.room.power_levels"}, {"state_key", ""}, {"content", powerLevelContent}});
#endif
}
int NeoChatRoom::defaultUserPowerLevel() const
@@ -1410,11 +1344,12 @@ bool NeoChatRoom::isSpace()
return false;
}
#ifdef QUOTIENT_07
return creationEvent->roomType() == RoomType::Space;
#else
return false;
#endif
}
PushNotificationState::State NeoChatRoom::pushNotificationState() const
{
return m_currentPushNotificationState;
}
void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
@@ -1736,15 +1671,9 @@ void NeoChatRoom::setSavedText(const QString &savedText)
bool NeoChatRoom::canEncryptRoom() const
{
#ifdef QUOTIENT_07
#ifdef Quotient_E2EE_ENABLED
return !usesEncryption() && canSendState("m.room.encryption");
#endif
#endif
return false;
}
#ifdef QUOTIENT_07
PollHandler *NeoChatRoom::poll(const QString &eventId)
{
if (!m_polls.contains(eventId)) {
@@ -1755,7 +1684,6 @@ PollHandler *NeoChatRoom::poll(const QString &eventId)
}
return m_polls[eventId];
}
#endif
bool NeoChatRoom::downloadTempFile(const QString &eventId)
{

View File

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

View File

@@ -11,22 +11,18 @@
#include <KNotification>
#include <KNotificationReplyAction>
#ifdef QUOTIENT_07
#include <accountregistry.h>
#else
#include "neochataccountregistry.h"
#endif
#include <connection.h>
#include <csapi/pushrules.h>
#include <jobs/basejob.h>
#include <user.h>
#include "actionshandler.h"
#include "controller.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "roommanager.h"
#include "texthandler.h"
#include "windowcontroller.h"
using namespace Quotient;
@@ -72,11 +68,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
connect(notification, &KNotification::defaultActivated, this, [=]() {
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
if (room->localUser()->id() != Controller::instance().activeConnection()->userId()) {
#ifdef QUOTIENT_07
Controller::instance().setActiveConnection(Accounts.get(room->localUser()->id()));
#else
Controller::instance().setActiveConnection(AccountRegistry::instance().get(room->localUser()->id()));
#endif
}
RoomManager::instance().enterRoom(room);
});
@@ -85,7 +77,9 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
std::unique_ptr<KNotificationReplyAction> replyAction(new KNotificationReplyAction(i18n("Reply")));
replyAction->setPlaceholderText(i18n("Reply..."));
connect(replyAction.get(), &KNotificationReplyAction::replied, this, [room, replyEventId](const QString &text) {
room->postMessage(text, markdownToHTML(text), RoomMessageEvent::MsgType::Text, replyEventId, QString());
TextHandler textHandler;
textHandler.setData(text);
room->postMessage(text, textHandler.handleSendText(), RoomMessageEvent::MsgType::Text, replyEventId, QString());
});
notification->setReplyAction(std::move(replyAction));
}

View File

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

View File

@@ -139,11 +139,41 @@ 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()
} else if (event.modifiers & Qt.ShiftModifier) {
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
textField.insert(cursorPosition, "\n")
} else {
chatBar.postMessage();
@@ -152,7 +182,7 @@ QQC2.Control {
Keys.onReturnPressed: {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier) {
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
textField.insert(cursorPosition, "\n")
} else {
chatBar.postMessage();
@@ -167,7 +197,7 @@ QQC2.Control {
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
chatBar.pasteImage();
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
let replyEvent = messageEventModel.getLatestMessageFromIndex(0)
let replyEvent = messageEventModel.getLatestMessageFromRow(0)
if (replyEvent && replyEvent["event_id"]) {
currentRoom.chatBoxReplyId = replyEvent["event_id"]
}
@@ -180,9 +210,14 @@ QQC2.Control {
completionMenu.decrementIndex()
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
completionMenu.incrementIndex()
} else if (event.key === Qt.Key_Backspace && textField.text.length <= 1) {
currentRoom.sendTypingNotification(false)
repeatTimer.stop()
} 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()
}
}
}
Keys.onShortcutOverride: {
@@ -313,14 +348,9 @@ QQC2.Control {
QQC2.ToolTip.text: modelData.tooltip
HoverHandler { id: hoverHandler }
QQC2.BusyIndicator {
anchors.fill: parent
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
visible: running
running: modelData.isBusy
PieProgressBar {
visible: modelData.isBusy
progress: currentRoom.fileUploadingProgress
}
}
}
@@ -364,6 +394,8 @@ QQC2.Control {
cursorPosition: textField.cursorPosition
selectionStart: textField.selectionStart
selectionEnd: textField.selectionEnd
mentionColor: Kirigami.Theme.linkColor
errorColor: Kirigami.Theme.negativeTextColor
Component.onCompleted: {
RoomManager.chatDocumentHandler = documentHandler;
}
@@ -398,4 +430,47 @@ 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

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

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

View File

@@ -0,0 +1,37 @@
// 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 org.kde.kirigami 2.15 as Kirigami
Flow {
id: root
property var avatarSize: Kirigami.Units.iconSizes.small
property alias model: avatarFlowRepeater.model
property string toolTipText
spacing: -avatarSize / 2
Repeater {
id: avatarFlowRepeater
delegate: Kirigami.Avatar {
implicitWidth: avatarSize
implicitHeight: avatarSize
name: modelData.displayName
source: modelData.avatarMediaId ? ("image://mxc/" + modelData.avatarMediaId) : ""
color: modelData.color
}
}
QQC2.ToolTip.text: toolTipText
QQC2.ToolTip.visible: hoverHandler.hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
HoverHandler {
id: hoverHandler
margin: Kirigami.Units.smallSpacing
}
}

View File

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

View File

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

View File

@@ -49,7 +49,7 @@ Loader {
*/
property bool indicatorEnabled: false
active: !currentRoom.usesEncryption && model.display && links && links.length > 0
active: !currentRoom.usesEncryption && model.display && links && links.length > 0 && currentRoom.urlPreviewEnabled
visible: Config.showLinkPreview && active
sourceComponent: linkPreviewer.loaded ? linkPreviewComponent : loadingComponent

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,27 +14,7 @@ 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
readonly property var linkRegex: /(href=["'])?(\b(https?):\/\/[^\s\<\>\"\'\\\?\:\)\(]+(\(.*?\))*(\?(?=[a-z])[^\s\\\)]+|$)?)/g
property string textMessage: model.display.includes("http")
? model.display.replace(linkRegex, function() {
if (arguments[0].includes("/_matrix/media/r0/download/")) {
return arguments[0];
}
if (arguments[1]) {
return arguments[0];
}
const l = arguments[2];
if ([".", ","].includes(l[l.length-1])) {
const link = l.substring(0, l.length-1);
const leftover = l[l.length-1];
return `<a href="${link}">${link}</a>${leftover}`;
}
return `<a href="${l}">${l}</a>`;
})
: model.display
property string textMessage: model.display
property bool spoilerRevealed: !hasSpoiler.test(textMessage)
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage))
@@ -53,6 +33,9 @@ table {
border-collapse: collapse;
border-style: solid;
}
code {
background-color:" + Kirigami.Theme.alternateBackgroundColor + ";
}
table th,
table td {
border: 1px solid black;
@@ -75,7 +58,7 @@ a{
background: " + Kirigami.Theme.textColor + ";
}
" : "") + "
</style>" + (isEmote ? "* <a href='https://matrix.to/#/" + author.id + "' style='color: " + author.color + "'>" + author.displayName + "</a> " : "") + textMessage + (isEdited && !contentLabel.isReplyLabel ? (" <span style=\"color: " + Kirigami.Theme.disabledTextColor + "\">" + "<span style='font-size: " + Kirigami.Theme.defaultFont.pixelSize +"px'>" + i18n(" (edited)") + "</span>") : "")
</style>" + textMessage
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor

View File

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

View File

@@ -134,5 +134,12 @@ QQC2.Control {
folded = !folded
foldedChanged()
}
AvatarFlow {
Layout.alignment: Qt.AlignRight
Layout.rightMargin: Kirigami.Units.largeSpacing
visible: showReadMarkers
model: readMarkers
toolTipText: readMarkersString
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

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