Compare commits

..

73 Commits

Author SHA1 Message Date
l10n daemon script
add48f88ce GIT_SILENT Sync po/docbooks with svn 2024-03-19 03:01:18 +00:00
Joshua Goins
09d0dd2b7a Fix the quick format bar not actually doing anything
(cherry picked from commit 5109b4fcd1)
2024-03-18 16:17:52 -04:00
Joshua Goins
498f6d9e64 Exclude lonely question marks from the linkify regex
Many URLs we see in the KDE rooms end with a question mark, without a
space. The linkify regex for plain URLs incorrectly considered them as
part of the link, which usually breaks them when opened in a web
browser. Now the regex excludes these, unless they are accompanied by
another character (so links like kde.org/realurl?is=true will still
work.)

(cherry picked from commit 1b7f482d0b)
2024-03-18 16:17:52 -04:00
l10n daemon script
f33780e996 GIT_SILENT Sync po/docbooks with svn 2024-03-16 03:42:32 +00:00
Heiko Becker
f731877519 GIT_SILENT Update Appstream for new release 2024-03-15 22:12:49 +01:00
Heiko Becker
5e48d5cb25 GIT_SILENT Upgrade release service version to 24.02.1. 2024-03-15 21:09:19 +01:00
Joshua Goins
afba8430f7 Don't destroy formatting when editing previous messages
Adds a few new methods to grab the markdown/slightly rich text from the
message, and will intelligently re-insert user mentions as needed.

(cherry picked from commit e2eb6ab33c)
2024-03-15 15:10:29 -04:00
Joshua Goins
18d14446bf Prevent collision between KUnifiedPush DBus and KRunner DBus
These share the same D-Bus service name (org.kde.neochat) which comes
with a fun little addition: KRunner activation! While this is not a
problem while NeoChat is running - since it's already registered - this
becomes an issue while searching for NeoChat in something like the
Kickoff. The Kickoff (and consequently, KRunner) tries to activate the
NeoChat D-Bus service which runs our unified push parts.

This introduces a "FakeRunner" which watches closely for calls to the
KRunner interface while we're in unified push mode (or directly called
from D-Bus but not running) so it quits immediately.

(cherry picked from commit 35b08d085c)
2024-03-15 14:48:00 -04:00
l10n daemon script
84c2173a04 GIT_SILENT Sync po/docbooks with svn 2024-03-14 03:36:36 +00:00
l10n daemon script
7f3112f53d GIT_SILENT Sync po/docbooks with svn 2024-03-13 02:54:10 +00:00
l10n daemon script
371016f977 GIT_SILENT made messages (after extraction) 2024-03-13 02:23:20 +00:00
Ingo Klöcker
35efe9693d This is no longer needed now that ECM 6.0.0 is available 2024-03-12 16:38:17 +01:00
Ingo Klöcker
586cf3fc6c Create an APPX package for NeoChat 24.02 2024-03-12 16:38:17 +01:00
l10n daemon script
a88b5d6af9 GIT_SILENT Sync po/docbooks with svn 2024-03-12 02:51:19 +00:00
Nicolas Fella
35aa08b279 Remove manual window toggling for system tray icon
KStatusNotifierItem automatically does this for us
since we associate our window with it

Doing it again causes the window to be toggled again, which means
it won't be shown

BUG: 479721

BUG: 482779
(cherry picked from commit 550dc43dc0)
2024-03-11 11:43:57 +01:00
l10n daemon script
a75072d069 GIT_SILENT Sync po/docbooks with svn 2024-03-11 02:51:12 +00:00
Tobias Fella
4177ade7a0 Don't link KDBusAddons on windows
(cherry picked from commit ad6c7dbd1f)
2024-03-10 14:31:39 +01:00
l10n daemon script
8c1eab76cf GIT_SILENT Sync po/docbooks with svn 2024-03-09 03:00:03 +00:00
l10n daemon script
4d72ace337 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2024-03-09 02:56:34 +00:00
l10n daemon script
d575323651 GIT_SILENT Sync po/docbooks with svn 2024-03-07 02:50:29 +00:00
Ingo Klöcker
28fdf28a23 Don't require kirigami-addons master
The default version of kirigami-addons (0.11.90) should be sufficient
for 24.02. And the Qt version is already set by the CraftConfig used for
Qt 6 builds.
2024-03-06 14:52:45 +00:00
Albert Astals Cid
1032ae7ca7 flatpak: Switch to non-preview runtime
(cherry picked from commit 5b007129e3)
2024-03-06 00:33:51 +01:00
l10n daemon script
84960385f8 GIT_SILENT Sync po/docbooks with svn 2024-03-04 02:51:12 +00:00
l10n daemon script
baa1486657 GIT_SILENT Sync po/docbooks with svn 2024-03-03 03:04:25 +00:00
l10n daemon script
1bfbfa51b4 GIT_SILENT made messages (after extraction) 2024-03-03 02:32:39 +00:00
l10n daemon script
e575a86252 GIT_SILENT Sync po/docbooks with svn 2024-03-02 02:56:51 +00:00
Tobias Fella
6f6aebcada Fix (un)ignoring unknown users
(cherry picked from commit 943f6c762c)
2024-03-01 09:33:06 +00:00
l10n daemon script
7a9695e3ee GIT_SILENT Sync po/docbooks with svn 2024-02-29 02:59:21 +00:00
l10n daemon script
9e589d71ff GIT_SILENT Sync po/docbooks with svn 2024-02-28 02:54:05 +00:00
l10n daemon script
5de5fa3053 GIT_SILENT Sync po/docbooks with svn 2024-02-25 03:07:03 +00:00
Tobias Fella
abcb5a0334 Fix saving images from maximize component
Fixes #643
2024-02-21 16:53:12 +01:00
l10n daemon script
b901ea6e2a GIT_SILENT Sync po/docbooks with svn 2024-02-21 02:56:16 +00:00
James Graham
8cfd515db2 Add feature flag for reply in thread
Currently the ability to reply in threads was added but not the ability to actually view threads so this doesn't currently make much sense to just have enabled int he main build.

Note: I want to cherrypick this so it's just the flag. I'll add a feature flag page to dev tools for master soon.


(cherry picked from commit 864f9b8f74)
2024-02-20 20:11:01 +00:00
l10n daemon script
3e5181d64e GIT_SILENT Sync po/docbooks with svn 2024-02-20 02:59:05 +00:00
Carl Schwan
ae53bf5df2 Add neochat 24.02 release note
(cherry picked from commit dc5366e924)
2024-02-19 12:33:21 +00:00
l10n daemon script
d2ed304672 GIT_SILENT Sync po/docbooks with svn 2024-02-19 02:58:48 +00:00
l10n daemon script
4924bd05a8 GIT_SILENT Sync po/docbooks with svn 2024-02-18 02:57:59 +00:00
l10n daemon script
9aa7553a1f GIT_SILENT Sync po/docbooks with svn 2024-02-16 03:02:48 +00:00
Heiko Becker
1c910165c1 GIT_SILENT Update Appstream for new release 2024-02-16 00:48:30 +01:00
Heiko Becker
05a84da722 GIT_SILENT Upgrade release service version to 24.02.0. 2024-02-16 00:01:13 +01:00
Carl Schwan
1ab8b85f06 Fix reaction update event when the event is not there anymore
Happens when interacting witht Mjonir quite often


(cherry picked from commit 6d3839dd42)
2024-02-15 20:13:28 +00:00
Carl Schwan
05883bcb71 Fix reaction delegate sizing for text reaction
(cherry picked from commit 755a060e12)
2024-02-15 20:13:01 +00:00
l10n daemon script
20cb6dc864 GIT_SILENT Sync po/docbooks with svn 2024-02-15 02:52:41 +00:00
l10n daemon script
b6cf60acdb GIT_SILENT made messages (after extraction) 2024-02-15 02:21:10 +00:00
Tobias Fella
d4a6a41981 Skip Welcome screen when there's only one connection and it's loaded
If the connection is stuck, we can still log in to a different one that way.

(cherry picked from commit 7150445f8e)
2024-02-14 18:16:07 +01:00
Tobias Fella
8c2682c943 Allow dropping connections from the welcome page
This is the last piece required to make sure that we can recover from broken connections, e.g., when the access token is invalid.

(cherry picked from commit b02bdd22dd)
2024-02-14 18:14:20 +01:00
Heiko Becker
9c56561853 GIT_SILENT Update Appstream for new release
(cherry picked from commit 0cd0a6a672)
2024-02-14 14:39:59 +01:00
l10n daemon script
ab9410cc03 GIT_SILENT Sync po/docbooks with svn 2024-02-14 03:00:27 +00:00
Tobias Fella
43fae7af04 Show custom emoji reactions as per MSC4027
(cherry picked from commit ca57732871)
2024-02-12 16:02:48 +01:00
Tobias Fella
0cf19d21f2 Fix AudioDelegate playback
(cherry picked from commit b909cb2db8)
2024-02-10 23:06:44 +01:00
l10n daemon script
ce448bd027 GIT_SILENT Sync po/docbooks with svn 2024-02-09 03:07:07 +00:00
l10n daemon script
056e91df9f GIT_SILENT Sync po/docbooks with svn 2024-02-05 03:32:35 +00:00
l10n daemon script
258815ca10 GIT_SILENT Sync po/docbooks with svn 2024-02-02 02:54:20 +00:00
l10n daemon script
174373fb15 GIT_SILENT Sync po/docbooks with svn 2024-01-31 03:10:56 +00:00
l10n daemon script
aa0790d7fd GIT_SILENT Sync po/docbooks with svn 2024-01-30 02:58:53 +00:00
l10n daemon script
e6c589c6ac GIT_SILENT Sync po/docbooks with svn 2024-01-29 02:59:49 +00:00
James Graham
4b1805bdaa Fix copying selected text from a message
(cherry picked from commit 48502480df)
2024-01-28 10:06:27 +00:00
l10n daemon script
6055460bff GIT_SILENT Sync po/docbooks with svn 2024-01-28 02:58:51 +00:00
l10n daemon script
0642685874 GIT_SILENT Sync po/docbooks with svn 2024-01-27 02:57:15 +00:00
l10n daemon script
e2b7e6778e GIT_SILENT Sync po/docbooks with svn 2024-01-26 02:59:18 +00:00
l10n daemon script
22bf9b8a59 GIT_SILENT Sync po/docbooks with svn 2024-01-24 02:56:01 +00:00
l10n daemon script
85fc1a1f46 GIT_SILENT Sync po/docbooks with svn 2024-01-23 03:02:41 +00:00
l10n daemon script
624123407c GIT_SILENT Sync po/docbooks with svn 2024-01-22 03:38:56 +00:00
James Graham
7bad41739f Cherrypick 24.02 Clip QuickSwitcher
Clip QuickSwitcher to stop the delegates overlapping the dialog


(cherry picked from commit 8e8105d04d)
2024-01-17 17:19:40 +00:00
l10n daemon script
ee16504aa0 GIT_SILENT Sync po/docbooks with svn 2024-01-17 02:57:22 +00:00
Ingo Klöcker
cf308bcdce Require master of ECM
We need the fix for APK packaging with Android NDK r25


(cherry picked from commit 21d9e69712)
2024-01-16 14:21:07 +00:00
l10n daemon script
6fbfa48c77 GIT_SILENT Sync po/docbooks with svn 2024-01-16 02:57:45 +00:00
l10n daemon script
67d71cb590 GIT_SILENT Sync po/docbooks with svn 2024-01-15 02:56:34 +00:00
l10n daemon script
c5817df2c9 GIT_SILENT Sync po/docbooks with svn 2024-01-14 03:43:35 +00:00
Joshua Goins
b94fcd6858 Make the search message dialog header way prettier, like it is in KCMs
I think I've heard of this before...

(cherry picked from commit 2247a2a7af)
2024-01-13 20:37:48 -05:00
Joshua Goins
8a8874fcb6 Add missing thread roles in SearchModel
This fixes the message search so it works again!

(cherry picked from commit 08a0fbfd6b)
2024-01-13 20:35:25 -05:00
James Graham
b593f7321b Cherrypick 24.02 Readonly Room
Add readonly property to a room and use it to decide whether to show chatbar, replies and edits

BUG: 479590


(cherry picked from commit ec4aa73e37)
2024-01-13 12:06:00 +00:00
Albert Astals Cid
5002258e34 GIT_SILENT Upgrade release service version to 24.01.95. 2024-01-11 20:53:22 +01:00
265 changed files with 32549 additions and 40916 deletions

View File

@@ -2,6 +2,4 @@
; SPDX-License-Identifier: CC0-1.0
[BlueprintSettings]
kde/frameworks/extra-cmake-modules.version=master
kde/unreleased/kirigami-addons.version=master
libs/qt.qtMajorVersion=6
kde/applications/neochat.packageAppx = True

View File

@@ -2,7 +2,7 @@
"id": "org.kde.neochat",
"branch": "master",
"runtime": "org.kde.Platform",
"runtime-version": "6.6-kf6preview",
"runtime-version": "6.6",
"sdk": "org.kde.Sdk",
"command": "neochat",
"tags": [

View File

@@ -8,8 +8,8 @@ cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "24")
set(RELEASE_SERVICE_VERSION_MINOR "04")
set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION_MINOR "02")
set(RELEASE_SERVICE_VERSION_MICRO "1")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})

View File

@@ -12,6 +12,7 @@
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include "enums/delegatetype.h"
#include "linkpreviewer.h"
#include "models/reactionmodel.h"
#include "neochatroom.h"
@@ -28,14 +29,19 @@ class EventHandlerTest : public QObject
private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
EventHandler emptyHandler = EventHandler(nullptr, nullptr);
EventHandler eventHandler;
EventHandler emptyHandler;
EventHandler noEventHandler;
private Q_SLOTS:
void initTestCase();
void nullSetEvent();
void eventId();
void nullEventId();
void delegateType_data();
void delegateType();
void nullDelegateType();
void author();
void nullAuthor();
void authorDisplayName();
@@ -55,6 +61,7 @@ private Q_SLOTS:
void genericBody_data();
void genericBody();
void nullGenericBody();
void markdownBody();
void subtitle();
void nullSubtitle();
void mediaInfo();
@@ -63,6 +70,8 @@ private Q_SLOTS:
void nullHasReply();
void replyId();
void nullReplyId();
void replyDelegateType();
void nullReplyDelegateType();
void replyAuthor();
void nullReplyAuthor();
void replyBody();
@@ -75,32 +84,72 @@ private Q_SLOTS:
void nullLocation();
void readMarkers();
void nullReadMarkers();
void cleanup();
};
void EventHandlerTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QLatin1String("test-eventhandler-sync.json"));
eventHandler.setRoom(room);
noEventHandler.setRoom(room);
}
void EventHandlerTest::nullSetEvent()
{
QTest::ignoreMessage(QtWarningMsg, "cannot setEvent when m_room is set to nullptr.");
emptyHandler.setEvent(room->messageEvents().at(0).get());
}
void EventHandlerTest::eventId()
{
EventHandler eventHandler(room, room->messageEvents().at(0).get());
eventHandler.setEvent(room->messageEvents().at(0).get());
QCOMPARE(eventHandler.getId(), QStringLiteral("$153456789:example.org"));
}
void EventHandlerTest::nullEventId()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getId called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getId(), QString());
}
void EventHandlerTest::delegateType_data()
{
QTest::addColumn<int>("eventNum");
QTest::addColumn<DelegateType::Type>("delegateType");
QTest::newRow("message") << 0 << DelegateType::Message;
QTest::newRow("state") << 1 << DelegateType::State;
QTest::newRow("message 2") << 2 << DelegateType::Message;
QTest::newRow("reaction") << 3 << DelegateType::Other;
QTest::newRow("video") << 4 << DelegateType::Video;
QTest::newRow("location") << 7 << DelegateType::Location;
}
void EventHandlerTest::delegateType()
{
QFETCH(int, eventNum);
QFETCH(DelegateType::Type, delegateType);
eventHandler.setEvent(room->messageEvents().at(eventNum).get());
QCOMPARE(eventHandler.getDelegateType(), delegateType);
}
void EventHandlerTest::nullDelegateType()
{
QTest::ignoreMessage(QtWarningMsg, "getDelegateType called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getDelegateType(), DelegateType::Other);
}
void EventHandlerTest::author()
{
auto event = room->messageEvents().at(0).get();
auto author = room->user(event->senderId());
EventHandler eventHandler(room, event);
eventHandler.setEvent(event);
auto eventHandlerAuthor = eventHandler.getAuthor();
@@ -118,14 +167,15 @@ void EventHandlerTest::nullAuthor()
QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getAuthor(), QVariantMap());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_event set to nullptr. Returning empty user.");
QCOMPARE(noEventHandler.getAuthor(), room->getUser(nullptr));
}
void EventHandlerTest::authorDisplayName()
{
EventHandler eventHandler(room, room->messageEvents().at(1).get());
auto event = room->messageEvents().at(1).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getAuthorDisplayName(), QStringLiteral("before"));
}
@@ -134,14 +184,15 @@ void EventHandlerTest::nullAuthorDisplayName()
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getAuthorDisplayName(), QString());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getAuthorDisplayName(), QString());
}
void EventHandlerTest::singleLineSidplayName()
{
EventHandler eventHandler(room, room->messageEvents().at(11).get());
auto event = room->messageEvents().at(11).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.singleLineAuthorDisplayname(), QStringLiteral("Look at me I put newlines in my display name"));
}
@@ -150,14 +201,14 @@ void EventHandlerTest::nullSingleLineDisplayName()
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_room set to nullptr.");
QCOMPARE(emptyHandler.singleLineAuthorDisplayname(), QString());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_event set to nullptr.");
QCOMPARE(noEventHandler.singleLineAuthorDisplayname(), QString());
}
void EventHandlerTest::time()
{
EventHandler eventHandler(room, room->messageEvents().at(0).get());
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getTime(), QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC));
QCOMPARE(eventHandler.getTime(true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC));
@@ -165,18 +216,18 @@ void EventHandlerTest::time()
void EventHandlerTest::nullTime()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getTime called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getTime(), QDateTime());
EventHandler eventHandler(room, room->messageEvents().at(0).get());
eventHandler.setEvent(room->messageEvents().at(0).get());
QTest::ignoreMessage(QtWarningMsg, "a value must be provided for lastUpdated for a pending event.");
QCOMPARE(eventHandler.getTime(true), QDateTime());
}
void EventHandlerTest::timeString()
{
EventHandler eventHandler(room, room->messageEvents().at(0).get());
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
KFormat format;
@@ -196,22 +247,25 @@ void EventHandlerTest::timeString()
void EventHandlerTest::nullTimeString()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getTimeString called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getTimeString(false), QString());
EventHandler eventHandler(room, room->messageEvents().at(0).get());
eventHandler.setEvent(room->messageEvents().at(0).get());
QTest::ignoreMessage(QtWarningMsg, "a value must be provided for lastUpdated for a pending event.");
QCOMPARE(eventHandler.getTimeString(false, QLocale::ShortFormat, true), QString());
}
void EventHandlerTest::highlighted()
{
EventHandler eventHandlerHighlight(room, room->messageEvents().at(2).get());
QCOMPARE(eventHandlerHighlight.isHighlighted(), true);
auto event = room->messageEvents().at(2).get();
eventHandler.setEvent(event);
EventHandler eventHandlerNoHighlight(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoHighlight.isHighlighted(), false);
QCOMPARE(eventHandler.isHighlighted(), true);
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isHighlighted(), false);
}
void EventHandlerTest::nullHighlighted()
@@ -219,18 +273,21 @@ void EventHandlerTest::nullHighlighted()
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with m_room set to nullptr.");
QCOMPARE(emptyHandler.isHighlighted(), false);
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with m_event set to nullptr.");
QCOMPARE(noEventHandler.isHighlighted(), false);
}
void EventHandlerTest::hidden()
{
EventHandler eventHandlerHidden(room, room->messageEvents().at(3).get());
QCOMPARE(eventHandlerHidden.isHidden(), true);
auto event = room->messageEvents().at(3).get();
eventHandler.setEvent(event);
EventHandler eventHandlerNoHidden(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoHidden.isHidden(), false);
QCOMPARE(eventHandler.isHidden(), true);
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isHidden(), false);
}
void EventHandlerTest::nullHidden()
@@ -238,14 +295,14 @@ void EventHandlerTest::nullHidden()
QTest::ignoreMessage(QtWarningMsg, "isHidden called with m_room set to nullptr.");
QCOMPARE(emptyHandler.isHidden(), false);
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "isHidden called with m_event set to nullptr.");
QCOMPARE(noEventHandler.isHidden(), false);
}
void EventHandlerTest::body()
{
EventHandler eventHandler(room, room->messageEvents().at(0).get());
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
QCOMPARE(eventHandler.getRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
@@ -255,8 +312,6 @@ void EventHandlerTest::body()
void EventHandlerTest::nullBody()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getRichBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getRichBody(), QString());
@@ -281,30 +336,39 @@ void EventHandlerTest::genericBody()
QFETCH(int, eventNum);
QFETCH(QString, output);
EventHandler eventHandler(room, room->messageEvents().at(eventNum).get());
eventHandler.setEvent(room->messageEvents().at(eventNum).get());
QCOMPARE(eventHandler.getGenericBody(), output);
}
void EventHandlerTest::nullGenericBody()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getGenericBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getGenericBody(), QString());
}
void EventHandlerTest::markdownBody()
{
eventHandler.setEvent(room->messageEvents().at(0).get());
QCOMPARE(eventHandler.getMarkdownBody(), QStringLiteral("This is an example\ntext message"));
}
void EventHandlerTest::subtitle()
{
EventHandler eventHandler(room, room->messageEvents().at(0).get());
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.subtitleText(), QStringLiteral("after: This is an example text message"));
EventHandler eventHandler2(room, room->messageEvents().at(2).get());
QCOMPARE(eventHandler2.subtitleText(), QStringLiteral("after: This is a highlight @bob:kde.org and this is a link https://kde.org"));
event = room->messageEvents().at(2).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.subtitleText(), QStringLiteral("after: This is a highlight @bob:kde.org and this is a link https://kde.org"));
}
void EventHandlerTest::nullSubtitle()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "subtitleText called with m_event set to nullptr.");
QCOMPARE(noEventHandler.subtitleText(), QString());
}
@@ -312,7 +376,7 @@ void EventHandlerTest::nullSubtitle()
void EventHandlerTest::mediaInfo()
{
auto event = room->messageEvents().at(4).get();
EventHandler eventHandler(room, event);
eventHandler.setEvent(event);
auto mediaInfo = eventHandler.getMediaInfo();
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
@@ -337,48 +401,76 @@ void EventHandlerTest::nullMediaInfo()
QTest::ignoreMessage(QtWarningMsg, "getMediaInfo called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getMediaInfo(), QVariantMap());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getMediaInfo called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getMediaInfo(), QVariantMap());
}
void EventHandlerTest::hasReply()
{
EventHandler eventHandlerReply(room, room->messageEvents().at(5).get());
QCOMPARE(eventHandlerReply.hasReply(), true);
auto event = room->messageEvents().at(5).get();
eventHandler.setEvent(event);
EventHandler eventHandlerNoReply(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoReply.hasReply(), false);
QCOMPARE(eventHandler.hasReply(), true);
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.hasReply(), false);
}
void EventHandlerTest::nullHasReply()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "hasReply called with m_event set to nullptr.");
QCOMPARE(noEventHandler.hasReply(), false);
}
void EventHandlerTest::replyId()
{
EventHandler eventHandlerReply(room, room->messageEvents().at(5).get());
QCOMPARE(eventHandlerReply.getReplyId(), QStringLiteral("$153456789:example.org"));
auto event = room->messageEvents().at(5).get();
eventHandler.setEvent(event);
EventHandler eventHandlerNoReply(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoReply.getReplyId(), QStringLiteral(""));
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$153456789:example.org"));
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyId(), QStringLiteral(""));
}
void EventHandlerTest::nullReplyId()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getReplyId called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyId(), QString());
}
void EventHandlerTest::replyDelegateType()
{
auto event = room->messageEvents().at(5).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Message);
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Other);
}
void EventHandlerTest::nullReplyDelegateType()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyDelegateType called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyDelegateType(), DelegateType::Other);
QTest::ignoreMessage(QtWarningMsg, "getReplyDelegateType called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyDelegateType(), DelegateType::Other);
}
void EventHandlerTest::replyAuthor()
{
auto event = room->messageEvents().at(5).get();
auto replyEvent = room->messageEvents().at(0).get();
auto replyAuthor = room->user(replyEvent->senderId());
EventHandler eventHandler(room, room->messageEvents().at(5).get());
eventHandler.setEvent(event);
auto eventHandlerReplyAuthor = eventHandler.getReplyAuthor();
@@ -390,8 +482,10 @@ void EventHandlerTest::replyAuthor()
QCOMPARE(eventHandlerReplyAuthor["color"_ls], Utils::getUserColor(replyAuthor->hueF()));
QCOMPARE(eventHandlerReplyAuthor["object"_ls], QVariant::fromValue(replyAuthor));
EventHandler eventHandlerNoAuthor(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoAuthor.getReplyAuthor(), room->getUser(nullptr));
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyAuthor(), room->getUser(nullptr));
}
void EventHandlerTest::nullReplyAuthor()
@@ -399,14 +493,14 @@ void EventHandlerTest::nullReplyAuthor()
QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyAuthor(), QVariantMap());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_event set to nullptr. Returning empty user.");
QCOMPARE(noEventHandler.getReplyAuthor(), room->getUser(nullptr));
}
void EventHandlerTest::replyBody()
{
EventHandler eventHandler(room, room->messageEvents().at(5).get());
auto event = room->messageEvents().at(5).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
QCOMPARE(eventHandler.getReplyRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
@@ -416,8 +510,6 @@ void EventHandlerTest::replyBody()
void EventHandlerTest::nullReplyBody()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getReplyRichBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyRichBody(), QString());
@@ -429,7 +521,7 @@ void EventHandlerTest::replyMediaInfo()
{
auto event = room->messageEvents().at(6).get();
auto replyEvent = room->messageEvents().at(4).get();
EventHandler eventHandler(room, event);
eventHandler.setEvent(event);
auto mediaInfo = eventHandler.getReplyMediaInfo();
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
@@ -454,26 +546,31 @@ void EventHandlerTest::nullReplyMediaInfo()
QTest::ignoreMessage(QtWarningMsg, "getReplyMediaInfo called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyMediaInfo(), QVariantMap());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getReplyMediaInfo called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyMediaInfo(), QVariantMap());
}
void EventHandlerTest::thread()
{
EventHandler eventHandlerNoThread(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoThread.isThreaded(), false);
QCOMPARE(eventHandlerNoThread.threadRoot(), QString());
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
EventHandler eventHandlerThreadRoot(room, room->messageEvents().at(9).get());
QCOMPARE(eventHandlerThreadRoot.isThreaded(), true);
QCOMPARE(eventHandlerThreadRoot.threadRoot(), QStringLiteral("$threadroot:example.org"));
QCOMPARE(eventHandlerThreadRoot.getReplyId(), QStringLiteral("$threadroot:example.org"));
QCOMPARE(eventHandler.isThreaded(), false);
QCOMPARE(eventHandler.threadRoot(), QString());
EventHandler eventHandlerThreadReply(room, room->messageEvents().at(10).get());
QCOMPARE(eventHandlerThreadReply.isThreaded(), true);
QCOMPARE(eventHandlerThreadReply.threadRoot(), QStringLiteral("$threadroot:example.org"));
QCOMPARE(eventHandlerThreadReply.getReplyId(), QStringLiteral("$threadmessage1:example.org"));
event = room->messageEvents().at(9).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isThreaded(), true);
QCOMPARE(eventHandler.threadRoot(), QStringLiteral("$threadroot:example.org"));
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$threadroot:example.org"));
event = room->messageEvents().at(10).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isThreaded(), true);
QCOMPARE(eventHandler.threadRoot(), QStringLiteral("$threadroot:example.org"));
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$threadmessage1:example.org"));
}
void EventHandlerTest::nullThread()
@@ -481,14 +578,14 @@ void EventHandlerTest::nullThread()
QTest::ignoreMessage(QtWarningMsg, "isThreaded called with m_event set to nullptr.");
QCOMPARE(emptyHandler.isThreaded(), false);
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "threadRoot called with m_event set to nullptr.");
QCOMPARE(noEventHandler.threadRoot(), QString());
}
void EventHandlerTest::location()
{
EventHandler eventHandler(room, room->messageEvents().at(7).get());
auto event = room->messageEvents().at(7).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getLatitude(), QStringLiteral("51.7035").toFloat());
QCOMPARE(eventHandler.getLongitude(), QStringLiteral("-1.14394").toFloat());
@@ -509,7 +606,9 @@ void EventHandlerTest::nullLocation()
void EventHandlerTest::readMarkers()
{
EventHandler eventHandler(room, room->messageEvents().at(0).get());
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.hasReadMarkers(), true);
auto readMarkers = eventHandler.getReadMarkers();
@@ -520,16 +619,18 @@ void EventHandlerTest::readMarkers()
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QString());
QCOMPARE(eventHandler.getReadMarkersString(), QStringLiteral("1 user: @alice:matrix.org"));
EventHandler eventHandler2(room, room->messageEvents().at(2).get());
QCOMPARE(eventHandler2.hasReadMarkers(), true);
event = room->messageEvents().at(2).get();
eventHandler.setEvent(event);
readMarkers = eventHandler2.getReadMarkers();
QCOMPARE(eventHandler.hasReadMarkers(), true);
readMarkers = eventHandler.getReadMarkers();
QCOMPARE(readMarkers.size(), 5);
QCOMPARE(eventHandler2.getNumberExcessReadMarkers(), QStringLiteral("+ 1"));
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QStringLiteral("+ 1"));
// There are no guarantees on the order of the users it will be different every time so don't match the whole string.
QCOMPARE(eventHandler2.getReadMarkersString().startsWith(QStringLiteral("6 users:")), true);
QCOMPARE(eventHandler.getReadMarkersString().startsWith(QStringLiteral("6 users:")), true);
}
void EventHandlerTest::nullReadMarkers()
@@ -546,8 +647,6 @@ void EventHandlerTest::nullReadMarkers()
QTest::ignoreMessage(QtWarningMsg, "getReadMarkersString called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReadMarkersString(), QString());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "hasReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.hasReadMarkers(), false);
@@ -561,5 +660,10 @@ void EventHandlerTest::nullReadMarkers()
QCOMPARE(noEventHandler.getReadMarkersString(), QString());
}
void EventHandlerTest::cleanup()
{
eventHandler.setEvent(nullptr);
}
QTEST_MAIN(EventHandlerTest)
#include "eventhandlertest.moc"

View File

@@ -103,6 +103,7 @@ void MessageEventModelTest::simpleTimeline()
QCOMPARE(model->data(model->index(1)), QStringLiteral("<b>This is an example<br>text message</b>"));
QCOMPARE(model->data(model->index(1), MessageEventModel::DelegateTypeRole), DelegateType::Message);
QCOMPARE(model->data(model->index(1), MessageEventModel::PlainText), QStringLiteral("This is an example\ntext message"));
QCOMPARE(model->data(model->index(1), MessageEventModel::EventIdRole), QStringLiteral("$153456789:example.org"));
QTest::ignoreMessage(QtWarningMsg, "Index QModelIndex(-1,-1,0x0,QObject(0x0)) is not valid (expected valid)");

View File

@@ -59,7 +59,8 @@
<summary xml:lang="fi">Keskustelu ystäviesi kanssa Matrixissa</summary>
<summary xml:lang="fr">Discuter avec vos ami(e)s sur le réseau Matrix</summary>
<summary xml:lang="gl">Charle coas súas amizades en Matrix.</summary>
<summary xml:lang="ia">Starta Conversation conntu amicos sur matrix</summary>
<summary xml:lang="hu">Csevegjen barátaival a matrixon</summary>
<summary xml:lang="ia">Starta Conversation con tu amicos sur matrix</summary>
<summary xml:lang="it">Conversa con i tuoi contatti su matrix</summary>
<summary xml:lang="ka">ესაუბრეთ მეგობრებს Matrix-ზე</summary>
<summary xml:lang="ko">Matrix를 사용하여 친구들과 대화하기</summary>
@@ -69,7 +70,7 @@
<summary xml:lang="sl">Klepet z vašimi prijatelji na matrixu</summary>
<summary xml:lang="sv">Chatta med dina vänner på Matrix</summary>
<summary xml:lang="ta">மேட்ரிக்ஸு மூலம் உங்கள் நண்பர்களிடம் பேசலாம்</summary>
<summary xml:lang="tr">Matrix'te arkadaşlarınızla sohbet edin</summary>
<summary xml:lang="tr">Matrixte arkadaşlarınızla sohbet edin</summary>
<summary xml:lang="uk">Спілкуйтеся з вашими друзями у matrix</summary>
<summary xml:lang="x-test">xxChat with your friends on matrixxx</summary>
<summary xml:lang="zh-CN">在 Matrix 上与朋友聊天</summary>
@@ -87,7 +88,7 @@ to provide a convergent experience across multiple platforms.</p>
<p xml:lang="fi">NeoChat on asiakassovellus Matrixille, hajautetulle pikaviestinyhteyskäytännölle. Sillä voi lähettää teksti-, video- ja ääniviestejä perheelle, tutuille ja ystäville. Se käyttää KDE-kehystä ja erityisesti Kirigamia tuottaakseen mukautuvan monialustaisen käyttökokemuksen.</p>
<p xml:lang="fr">NeoChat est un client pour le protocole Matrix, un protocole décentralisé de communications pour messagerie instantané. Il vous permet d'envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos amis. Il utilise les environnements de développement et plus précisément Kirigami pour fournir une expérience convergente sur plusieurs plate-formes. </p>
<p xml:lang="gl">NeoChat é un cliente para Matrix, o protocolo de comunicación descentralizada para mensaxaría instantánea. Podes enviar mensaxes de texto, vídeos e ficheiros de son á túa familia, colegas e amizades. Usas infraestruturas de KDE e principalmente Kirigami para proporcionar unha experiencia de uso converxente para varias plataformas.</p>
<p xml:lang="hu">A NeoChat egy kliens a Matrixhoz, az azonnali üzenetküldés decentralizált komunikációs protokolljához.. Szöveges üzeneteket, videókat és hangfájlokat küldhet családjának, kollégáinak és barátainak. A KDE keretrendszert használja, a Kirigaminak köszönhetően konvergens élményt nyújt több platformon is.</p>
<p xml:lang="hu">A NeoChat egy kliens a Matrixhoz, az azonnali üzenetküldés decentralizált komunikációs protokolljához. Szöveges üzeneteket, videókat és hangfájlokat küldhet családjának, kollégáinak és barátainak. A KDE keretrendszert használja, a Kirigaminak köszönhetően konvergens élményt nyújt több platformon is.</p>
<p xml:lang="ia">NeoChat es un cliente per Matrix, le protocollo de communication decentralisate per messager instantanee. Illo te permitte inviar messager de texto, files de video e audio a tu familia, collegas e amicos usante. Illo usa KDE frameworks e super toto Kirigamii forni un experientia convergente trans platteforme multiple.</p>
<p xml:lang="it">NeoChat è un client per Matrix, il protocollo di comunicazione decentralizzato per la messaggistica istantanea. Ti consente di inviare messaggi di testo, video e file audio a familiari, colleghi e amici. Utilizza i framework KDE e in particolare Kirigami per fornire un'esperienza convergente su più piattaforme.</p>
<p xml:lang="ka">NeoChat არის Matrix კლიენტი. ის საშუალებას გაძლევთ გაგზავნოთ ტექსტური შეტყობინებები, ვიდეოები და აუდიო ფაილები თქვენს ოჯახს, კოლეგებსა და მეგობრებს მატრიქსის პროტოკოლის გამოყენებით.</p>
@@ -113,6 +114,7 @@ to provide a convergent experience across multiple platforms.</p>
<p xml:lang="fi">NeoChat pyrkii olemaan Matrix-määritelmän täysominaisuuksinen sovellus, joten se tukee kaikkea nykyisessä vakaassa määritelmässä muutamaa huomattavaa poikkeusta lukuun ottamatta (VoIP, säikeet ja jotkin piirteet päästä päähän -salauksessa). Joitakin pienempiäkin puutteita on Matrix-määritelmän jatkuvan kehityksen vuoksi, mutta lopputavoitteena on tarjota määritelmän täysi tuki.</p>
<p xml:lang="fr">L'objectif de NeoChat est d'être une application complète pour le protocole Matrix. En tant que tel, tout dans la spécification stable actuelle avec les exceptions notables de VoIP, les processus et certains aspects du chiffrement de bout en bout sont pris en charge. Il y a quelques autres petites omissions en raison du fait que la spécification du protocole Matrix est en constante évolution. Cependant, l'objectif reste de fournir un soutien éventuel pour l'ensemble de la spécification.</p>
<p xml:lang="gl">NeoChat pretende ser unha aplicación completa para a especificación de Matrix. Coas excepcións de VoIP, conversas fiadas e algúns aspectos da cifraxe de extremo a extremo, a versión estábel segue as especificacións. Existen algunhas outras pequenas omisións debido ao feito de que Matrix está en continua evolución pero a intención é implementar a especificación completa.</p>
<p xml:lang="hu">A NeoChat célja, hogy a Matrix specifikációnak megfelelő teljes funkcionalitású alkalmazás legyen. Mint ilyen, a jelenlegi stabil specifikáció támogatott a VoIP, a szálak és a végpontok közötti titkosítás egyes elemeinek kivételével. Van még néhány kisebb hiányosság annak köszönhetően, hogy a Matrix specifikáció folyamatosan fejlődik, de végső cél a teljes specifikáció megvalósítása.</p>
<p xml:lang="ia">NeoChat aspira a esser un application plenemente eminente per le specification de Matrix. Tal como omne cosas in le specification currentemente stabile con le exceptiones notabile de VOIP, threads e alcun aspectos del cryptation End-to-End es supportate. Il ha ltere pauc omissiones, debite al facto que le specification de Matrix es in evolution constante ma le aspiration remane a fornir supporto eventual per le integre specification.</p>
<p xml:lang="it">NeoChat mira ad essere un'applicazione completa per le specifiche Matrix. Pertanto, sono supportati tutti gli elementi dell'attuale specifica stabile con le notevoli eccezioni di VoIP, conversazioni e alcuni aspetti della cifratura end-to-end. Ci sono alcune altre piccole omissioni dovute al fatto che le specifiche Matrix sono in continua evoluzione, ma l'obiettivo rimane quello di fornire un eventuale supporto per l'intera specifica.</p>
<p xml:lang="ka">NeoChat-ი მიზნად ისახავს Matrix სპეციფიკაციის სრული განხორციელება ჰქონდეს. როგორც ასეთი, ყველაფერი მიმდინარე სპეციფიკაციიდან, VoIP-ის, ძაფებისა და გამჭოლი დაშიფვრის ზოგიერთი ასპექტის გარდა, მხარდაჭერილია. შეძლება ასევე იყოს მცირე ლაფსუსებიც იმის გამო, რომ Matrix-ის სპეციფიკაცია მუდმივად ვითარგდება, მაგრამ ჩვენი მიზანი მისი სრული მხარდაჭერაა.</p>
@@ -123,7 +125,7 @@ to provide a convergent experience across multiple platforms.</p>
<p xml:lang="pt">O NeoChat pretende ser uma aplicação completa para a especificação do Matrix. Como tal, tudo o que existe na especificação estável actual, com as notáveis excepções do VoIP, tópicos e alguns aspectos da Encriptação Ponto-a-Ponto, são suportados. Existem mais algumas omissões, devido ao facto que a norma do Matrix está em constante evolução, mas o objectivo continua a ser oferecer o suporte eventual para a norma por inteiro.</p>
<p xml:lang="sl">Neochat cilja, da bi bila popolna aplikacija po specifikaciji Matrixa. Kot takšna vsebuje vse v trenutni stabilni specifikaciji z pomembnimi izjemami pri VoIP, nitih in nekaterih vidikov šifriranja od konca do konca. Obstaja nekaj drugih manjših opustitev zaradi dejstva, da se specifikacija Matrix nenehno razvija, vendar cilj ostaja zagotoviti morebitno podporo celotni specifikaciji.</p>
<p xml:lang="sv">NeoChat har som mål att vara ett fullständigt program enligt Matrix-specifikationen. Som sådant stöds allt i den nuvarande stabila specifikationen, med de nämnvärda undantagen VoIP, trådar och några aspekter av kryptering hela vägen. Det finns några ytterligare utelämnanden på grund av att Matrix-specifikationen hela tiden utvecklas, men målet förblir att till slut erbjuda stöd för hela specifikationen.</p>
<p xml:lang="tr">NeoChat, Matrix belirtimi için tam özellikli bir uygulama olmayı hedefler. Bu nedenle; VoIP, ileti zincirleri ve Uçtan Uca Şifreleme'nin bazı yönleri gibi dikkate değer istisnalar dışında var olan kararlı belirtimdeki her şey desteklenir. Matrix belirtiminin sürekli gelişmesi nedeniyle birkaç küçük eksiklik daha var; ancak amaç tüm belirtim için nihai destek sağlamak olmayı sürdürüyor.</p>
<p xml:lang="tr">NeoChat, Matrix belirtimi için tam özellikli bir uygulama olmayı hedefler. Bu nedenle; VoIP, ileti zincirleri ve Uçtan Uca Şifrelemenin bazı yönleri gibi dikkate değer istisnalar dışında var olan kararlı belirtimdeki her şey desteklenir. Matrix belirtiminin sürekli gelişmesi nedeniyle birkaç küçük eksiklik daha var; ancak amaç tüm belirtim için nihai destek sağlamak olmayı sürdürüyor.</p>
<p xml:lang="uk">Метою створення NeoChat є повноцінна реалізація програми для специфікації Matrix. Як наслідок, реалізовано усе у поточній стабільній специфікації, окрім голосового інтернет-зв'язку, потоків та деяких аспектів міжвузлового шифрування. Є також декілька інших незначних прогалин через те, що специфікація Matrix постійно змінюється, але метою лишається повна підтримка специфікації.</p>
<p xml:lang="x-test">xxNeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.xx</p>
<p xml:lang="zh-TW">NeoChat 以完整支援 Matrix 標準為目標,因此目前穩定版標準除了 VoIP、對話串與端對端加密的某些部分以外的所有部分都有支援。其他部分還有一些較小的不支援的部分這是因為 Matrix 標準隨時都在改進,但目標仍然時最終提供整個標準的完整支援。</p>
@@ -138,6 +140,7 @@ to provide a convergent experience across multiple platforms.</p>
<p xml:lang="fi">Matrix-määritelmän kehittyessä NeoChat tukee myös monia epävakaita ominaisuuksia. Tällä hetkellä näitä ovat:</p>
<p xml:lang="fr">En raison de la nature du développement des spécifications du protocole Matrix, NeoChat prend également en charge de nombreuses fonctionnalités instables. Actuellement, ce sont :</p>
<p xml:lang="gl">Debido á natureza do desenvolvemento da especificación de Matrix, NeoChat tamén inclúe varias funcionalidades non estábeis:</p>
<p xml:lang="hu">A Matrix specifikáció fejlesztésének jellegéből adódóan a NeoChat számos instabil funkciót is támogat. Jelenleg a következőket:</p>
<p xml:lang="ia">Debite al natura del disveloppamento de specification de Matrix NeoChat tamben supporta numerose characteristicas instabile. Currentemente istes es:</p>
<p xml:lang="it">A causa della natura dello sviluppo delle specifiche Matrix, NeoChat supporta anche numerose funzionalità instabili. Attualmente queste sono:</p>
<p xml:lang="ka">Matrix-ის სპეციფიკაციის განვითარების ბუნების გამო NeoChat-ს ასევე აქვს უამრავი არასტაბილური ფუნქციაც. ახლა ისინია:</p>
@@ -165,6 +168,7 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="fi">Kyselyt MSC3381</li>
<li xml:lang="fr">Sondages - MSC3381</li>
<li xml:lang="gl">Enquisas — MSC3381</li>
<li xml:lang="hu">Szavazások - MSC3381</li>
<li xml:lang="ia">Inquestas - MSC3381</li>
<li xml:lang="it">Sondaggi - MSC3381</li>
<li xml:lang="ka">Polls - MSC3381</li>
@@ -176,7 +180,7 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="sl">Polls - MSC3381</li>
<li xml:lang="sv">Polls - MSC3381</li>
<li xml:lang="ta">வாக்கெடுப்புகள் - MSC3381</li>
<li xml:lang="tr">Anketler - MSC3381</li>
<li xml:lang="tr">Anketler MSC3381</li>
<li xml:lang="uk">Опитування - MSC3381</li>
<li xml:lang="x-test">xxPolls - MSC3381xx</li>
<li xml:lang="zh-TW">投票 - MSC3381</li>
@@ -191,6 +195,7 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="fi">Tarrapakkaukset MSC2545</li>
<li xml:lang="fr">Paquets d'auto-collants - MSC2545</li>
<li xml:lang="gl">Paquetes de adhesivos — MSC2545</li>
<li xml:lang="hu">Matricacsomagok - MSC2545</li>
<li xml:lang="ia">Etiquetta gummate (sticker) -MSC2545</li>
<li xml:lang="it">Pacchetti di adesivi - MSC2545</li>
<li xml:lang="ka">სტიკერების პაკეტები - MSC2545</li>
@@ -202,7 +207,7 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="sl">Sticker Packs - MSC2545</li>
<li xml:lang="sv">Sticker Packs - MSC2545</li>
<li xml:lang="ta">ஒட்டி தொகுப்புகள் - MSC2545</li>
<li xml:lang="tr">Yapışkan Paketleri - MSC2545</li>
<li xml:lang="tr">Yapışkan Paketleri MSC2545</li>
<li xml:lang="uk">Пакунки наліпок - MSC2545</li>
<li xml:lang="x-test">xxSticker Packs - MSC2545xx</li>
<li xml:lang="zh-TW">貼圖包 - MSC2545</li>
@@ -217,6 +222,7 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="fi">Sijaintitapahtumat MSC3488</li>
<li xml:lang="fr">Événements de lieu - MSC3488</li>
<li xml:lang="gl">Localización de eventos — MSC3488</li>
<li xml:lang="hu">Események helyadatai - MSC3488</li>
<li xml:lang="ia">Eventos de Location - MSC3488</li>
<li xml:lang="it">Località eventi - MSC3488</li>
<li xml:lang="ka">მდებარეობის მოვლენები - MSC3488</li>
@@ -228,7 +234,7 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="sl">Location Events - MSC3488</li>
<li xml:lang="sv">Location Events - MSC3488</li>
<li xml:lang="ta">இட நிகழ்வுகள் - MSC3488</li>
<li xml:lang="tr">Konum Etkinlikleri - MSC3488</li>
<li xml:lang="tr">Konum Etkinlikleri MSC3488</li>
<li xml:lang="uk">Місцеві зустрічі - MSC3488</li>
<li xml:lang="x-test">xxLocation Events - MSC3488xx</li>
<li xml:lang="zh-TW">位置事件 - MSC3488</li>
@@ -239,11 +245,44 @@ to provide a convergent experience across multiple platforms.</p>
<categories>
<category>Network</category>
</categories>
<developer>
<id>kde.org</id>
<name>The KDE Community</name>
<url>https://kde.org</url>
</developer>
<developer_name>The KDE Community</developer_name>
<developer_name xml:lang="ar">مجتمع كِيدِي</developer_name>
<developer_name xml:lang="az">KDE Cəmiyyəti</developer_name>
<developer_name xml:lang="ca">La comunitat KDE</developer_name>
<developer_name xml:lang="ca-valencia">La comunitat KDE</developer_name>
<developer_name xml:lang="cs">Komunita KDE</developer_name>
<developer_name xml:lang="de">Die KDE-Gemeinschaft</developer_name>
<developer_name xml:lang="el">Η Κοινότητα του KDE</developer_name>
<developer_name xml:lang="en-GB">The KDE Community</developer_name>
<developer_name xml:lang="eo">La KDE-Komunumo</developer_name>
<developer_name xml:lang="es">La comunidad KDE</developer_name>
<developer_name xml:lang="eu">KDE komunitatea</developer_name>
<developer_name xml:lang="fi">KDE-yhteisö</developer_name>
<developer_name xml:lang="fr">La communauté de KDE</developer_name>
<developer_name xml:lang="gl">A comunidade KDE</developer_name>
<developer_name xml:lang="hu">A KDE Közösség</developer_name>
<developer_name xml:lang="ia">Le communitate de KDE</developer_name>
<developer_name xml:lang="id">Komunitas KDE</developer_name>
<developer_name xml:lang="ie">Li comunité de KDE</developer_name>
<developer_name xml:lang="it">La comunità KDE</developer_name>
<developer_name xml:lang="ka">KDE-ის საზოგადოება</developer_name>
<developer_name xml:lang="ko">KDE 커뮤니티</developer_name>
<developer_name xml:lang="nl">De KDE gemeenschap</developer_name>
<developer_name xml:lang="nn">KDE-fellesskapet</developer_name>
<developer_name xml:lang="pa">ਕੇਡੀਈ ਕਮਿਊਨਟੀ</developer_name>
<developer_name xml:lang="pl">Społeczność KDE</developer_name>
<developer_name xml:lang="pt">A Comunidade do KDE</developer_name>
<developer_name xml:lang="pt-BR">A comunidade KDE</developer_name>
<developer_name xml:lang="ru">Сообщество KDE</developer_name>
<developer_name xml:lang="sk">KDE Komunita</developer_name>
<developer_name xml:lang="sl">Skupnost KDE</developer_name>
<developer_name xml:lang="sv">KDE-gemenskapen</developer_name>
<developer_name xml:lang="ta">கே.டீ.யீ. சமூகம்</developer_name>
<developer_name xml:lang="tr">KDE Topluluğu</developer_name>
<developer_name xml:lang="uk">Спільнота KDE</developer_name>
<developer_name xml:lang="x-test">xxThe KDE Communityxx</developer_name>
<developer_name xml:lang="zh-CN">KDE 社区</developer_name>
<developer_name xml:lang="zh-TW">KDE 社群</developer_name>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0</project_license>
<custom>
@@ -275,6 +314,7 @@ to provide a convergent experience across multiple platforms.</p>
<caption xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</caption>
<caption xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</caption>
<caption xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</caption>
<caption xml:lang="hu">A fő nézet a szobalistával, csevegéssel és szobainformációkkal</caption>
<caption xml:lang="ia">Vista principal con lista de sala, chat e information de sala</caption>
<caption xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption>
<caption xml:lang="ka">მთავარი ხედი სურათების სიით, ჩატით და ოთახის ინფორმაციით</caption>
@@ -304,6 +344,7 @@ to provide a convergent experience across multiple platforms.</p>
<caption xml:lang="fi">Kirjautumisnäkymä</caption>
<caption xml:lang="fr">Écran de connexion</caption>
<caption xml:lang="gl">Pantalla de identificación.</caption>
<caption xml:lang="hu">Bejelentkező képernyő</caption>
<caption xml:lang="ia">Schermo de accesso</caption>
<caption xml:lang="it">Schermata di accesso</caption>
<caption xml:lang="ka">შესვლის ეკრანი</caption>
@@ -325,6 +366,7 @@ to provide a convergent experience across multiple platforms.</p>
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="24.02.1" date="2024-03-21"/>
<release version="24.02.0" date="2024-02-28">
<url>https://kde.org/announcements/megarelease/6/#neochat</url>
<description>

View File

@@ -65,7 +65,7 @@ GenericName[ie]=Cliente de Matrix
GenericName[it]=Client Matrix
GenericName[ka]=Matrix -ის კლიენტი
GenericName[ko]=Matrix 클라이언트
GenericName[lt]=Matrix kliento programa
GenericName[lt]=Matrix kliento programą
GenericName[nl]=Matrix-client
GenericName[nn]=Matrix-klient
GenericName[pa]=ਮੈਟਰਿਕਸ ਕਲਾਈਂਟ

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

@@ -77,7 +77,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
></term>
<listitem>
<para
>Bir kullanıcı veya oda için matrix URIsi; örneğin, matrix:u/kullanıcı:örnek.org ve matrix:r/kök:örnek.org. Bu, NeoChatin verilen odayı veya konuşmayı açmayı denemesini sağlar. </para>
>Bir kullanıcı veya oda için matrix URI'si; örneğin, matrix:u/kullanıcı:örnek.org ve matrix:r/kök:örnek.org. Bu, NeoChat'in verilen odayı veya konuşmayı açmayı denemesini sağlar. </para>
</listitem>
</varlistentry>
</variablelist>

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

@@ -56,8 +56,6 @@ add_library(neochat STATIC
notificationsmanager.h
models/sortfilterroomlistmodel.cpp
models/sortfilterroomlistmodel.h
models/roomtreemodel.cpp
models/roomtreemodel.h
chatdocumenthandler.cpp
chatdocumenthandler.h
models/devicesmodel.cpp
@@ -146,21 +144,6 @@ add_library(neochat STATIC
models/timelinemodel.cpp
models/timelinemodel.h
enums/pushrule.h
models/itinerarymodel.cpp
models/itinerarymodel.h
proxycontroller.cpp
proxycontroller.h
models/linemodel.cpp
models/linemodel.h
events/locationbeaconevent.h
events/serveraclevent.h
events/widgetevent.h
enums/messagecomponenttype.h
models/messagecontentmodel.cpp
models/messagecontentmodel.h
enums/neochatroomtype.h
models/sortfilterroomtreemodel.cpp
models/sortfilterroomtreemodel.h
)
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
@@ -178,10 +161,11 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/UserInfoDesktop.qml
qml/RoomPage.qml
qml/RoomWindow.qml
qml/ExploreRoomsPage.qml
qml/JoinRoomPage.qml
qml/ManualRoomDialog.qml
qml/ExplorerDelegate.qml
qml/InviteUserPage.qml
qml/StartChatPage.qml
qml/ImageEditorPage.qml
qml/WelcomePage.qml
qml/General.qml
@@ -206,12 +190,21 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/TimelineDelegate.qml
qml/ReplyComponent.qml
qml/StateDelegate.qml
qml/RichLabel.qml
qml/MessageDelegate.qml
qml/Bubble.qml
qml/SectionDelegate.qml
qml/VideoDelegate.qml
qml/ReactionDelegate.qml
qml/LinkPreviewDelegate.qml
qml/AudioDelegate.qml
qml/FileDelegate.qml
qml/ImageDelegate.qml
qml/EncryptedDelegate.qml
qml/EventDelegate.qml
qml/TextDelegate.qml
qml/ReadMarkerDelegate.qml
qml/PollDelegate.qml
qml/MimeComponent.qml
qml/StateComponent.qml
qml/MessageEditComponent.qml
@@ -274,13 +267,15 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/EmojiTonesPicker.qml
qml/EmojiDelegate.qml
qml/EmojiGrid.qml
qml/RoomSearchPage.qml
qml/SearchPage.qml
qml/LocationDelegate.qml
qml/LocationChooser.qml
qml/TimelineView.qml
qml/InvitationView.qml
qml/AvatarTabButton.qml
qml/SpaceDrawer.qml
qml/OsmLocationPlugin.qml
qml/LiveLocationDelegate.qml
qml/FullScreenMap.qml
qml/LocationsPage.qml
qml/LocationMapItem.qml
@@ -303,31 +298,11 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/NotificationsView.qml
qml/LoadingDelegate.qml
qml/TimelineEndDelegate.qml
qml/SearchPage.qml
qml/ServerComboBox.qml
qml/UserSearchPage.qml
qml/ManualUserDialog.qml
qml/MessageComponentChooser.qml
qml/TextComponent.qml
qml/ImageComponent.qml
qml/VideoComponent.qml
qml/AudioComponent.qml
qml/EncryptedComponent.qml
qml/FileComponent.qml
qml/LocationComponent.qml
qml/LiveLocationComponent.qml
qml/PollComponent.qml
qml/LinkPreviewComponent.qml
qml/LoadComponent.qml
qml/RecommendedSpaceDialog.qml
qml/RoomTreeSection.qml
RESOURCES
qml/confetti.png
qml/glowdot.png
)
configure_file(config-neochat.h.in ${CMAKE_CURRENT_BINARY_DIR}/config-neochat.h)
if(WIN32)
set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib")
endif()
@@ -341,15 +316,6 @@ ecm_qt_declare_logging_category(neochat
EXPORT NEOCHAT
)
ecm_qt_declare_logging_category(neochat
HEADER "publicroomlist_logging.h"
IDENTIFIER "PublicRoomList"
CATEGORY_NAME "org.kde.neochat.publicroomlistmodel"
DESCRIPTION "Neochat: publicroomlistmodel"
DEFAULT_SEVERITY Info
EXPORT NEOCHAT
)
ecm_qt_declare_logging_category(neochat
HEADER "eventhandler_logging.h"
IDENTIFIER "EventHandling"
@@ -399,6 +365,10 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
target_compile_definitions(neochat PUBLIC -DHAVE_RUNNER)
target_compile_definitions(neochat PUBLIC -DHAVE_X11)
target_sources(neochat PRIVATE runner.cpp)
if (TARGET KUnifiedPush)
target_sources(neochat PRIVATE fakerunner.cpp)
endif()
endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models ${CMAKE_CURRENT_SOURCE_DIR}/enums)
@@ -535,7 +505,7 @@ if(NOT ANDROID)
set_target_properties(neochat-app PROPERTIES OUTPUT_NAME "neochat")
endif()
if(TARGET KF6::DBusAddons)
if(TARGET KF6::DBusAddons AND NOT WIN32)
target_link_libraries(neochat PUBLIC KF6::DBusAddons)
target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS)
endif()

View File

@@ -3,6 +3,7 @@
#include "chatbarcache.h"
#include "chatdocumenthandler.h"
#include "eventhandler.h"
#include "neochatroom.h"
@@ -43,14 +44,14 @@ void ChatBarCache::setReplyId(const QString &replyId)
if (m_relationType == Reply && m_relationId == replyId) {
return;
}
const auto oldEventId = std::exchange(m_relationId, replyId);
m_relationId = replyId;
if (m_relationId.isEmpty()) {
m_relationType = None;
} else {
m_relationType = Reply;
}
m_attachmentPath = QString();
Q_EMIT relationIdChanged(oldEventId, m_relationId);
Q_EMIT relationIdChanged();
Q_EMIT attachmentPathChanged();
}
@@ -72,14 +73,14 @@ void ChatBarCache::setEditId(const QString &editId)
if (m_relationType == Edit && m_relationId == editId) {
return;
}
const auto oldEventId = std::exchange(m_relationId, editId);
m_relationId = editId;
if (m_relationId.isEmpty()) {
m_relationType = None;
} else {
m_relationType = Edit;
}
m_attachmentPath = QString();
Q_EMIT relationIdChanged(oldEventId, m_relationId);
Q_EMIT relationIdChanged();
Q_EMIT attachmentPathChanged();
}
@@ -114,10 +115,11 @@ QString ChatBarCache::relationMessage() const
qWarning() << "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.";
return {};
}
EventHandler eventhandler;
eventhandler.setRoom(room);
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
EventHandler eventhandler(room, &**event);
return eventhandler.getPlainBody();
eventhandler.setEvent(&**event);
return eventhandler.getMarkdownBody();
}
return {};
}
@@ -153,9 +155,9 @@ void ChatBarCache::setAttachmentPath(const QString &attachmentPath)
}
m_attachmentPath = attachmentPath;
m_relationType = None;
const auto oldEventId = std::exchange(m_relationId, QString());
m_relationId = QString();
Q_EMIT attachmentPathChanged();
Q_EMIT relationIdChanged(oldEventId, m_relationId);
Q_EMIT relationIdChanged();
}
QList<Mention> *ChatBarCache::mentions()
@@ -163,6 +165,54 @@ QList<Mention> *ChatBarCache::mentions()
return &m_mentions;
}
void ChatBarCache::updateMentions(QQuickTextDocument *document, ChatDocumentHandler *documentHandler)
{
documentHandler->setDocument(document);
if (parent() == nullptr) {
qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.";
return;
}
if (m_relationId.isEmpty()) {
return;
}
auto room = dynamic_cast<NeoChatRoom *>(parent());
if (room == nullptr) {
qWarning() << "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.";
return;
}
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
if (const auto &roomMessageEvent = &*event->viewAs<Quotient::RoomMessageEvent>()) {
// Replaces the mentions that are baked into the HTML but plaintext in the original markdown
const QRegularExpression re(QStringLiteral(R"lit(<a\shref="https:\/\/matrix.to\/#\/([\S]*)"\s?>([\S]*)<\/a>)lit"));
m_mentions.clear();
int linkSize = 0;
auto matches = re.globalMatch(EventHandler::rawMessageBody(*roomMessageEvent));
while (matches.hasNext()) {
const QRegularExpressionMatch match = matches.next();
if (match.hasMatch()) {
const QString id = match.captured(1);
const QString name = match.captured(2);
const int position = match.capturedStart(0) - linkSize;
const int end = position + name.length();
linkSize += match.capturedLength(0) - name.length();
QTextCursor cursor(documentHandler->document()->textDocument());
cursor.setPosition(position);
cursor.setPosition(end, QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true);
m_mentions.push_back(Mention{.cursor = cursor, .text = name, .start = position, .position = end, .id = id});
}
}
}
}
}
QString ChatBarCache::savedText() const
{
return m_savedText;

View File

@@ -5,8 +5,11 @@
#include <QObject>
#include <QQmlEngine>
#include <QQuickTextDocument>
#include <QTextCursor>
class ChatDocumentHandler;
/**
* @brief Defines a user mention in the current chat or edit text.
*/
@@ -174,6 +177,11 @@ public:
*/
QList<Mention> *mentions();
/**
* @brief Update the mentions in @p document when editing a message.
*/
Q_INVOKABLE void updateMentions(QQuickTextDocument *document, ChatDocumentHandler *documentHandler);
/**
* @brief Get the saved chat bar text.
*/
@@ -186,7 +194,7 @@ public:
Q_SIGNALS:
void textChanged();
void relationIdChanged(const QString &oldEventId, const QString &newEventId);
void relationIdChanged();
void threadIdChanged();
void attachmentPathChanged();

View File

@@ -1,8 +0,0 @@
/*
SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#pragma once
#define CMAKE_INSTALL_FULL_LIBEXECDIR_KF6 "${KDE_INSTALL_FULL_LIBEXECDIR_KF}"

View File

@@ -23,12 +23,12 @@
#include <Quotient/csapi/logout.h>
#include <Quotient/csapi/notifications.h>
#include <Quotient/eventstats.h>
#include <Quotient/jobs/downloadfilejob.h>
#include <Quotient/qt_connection_util.h>
#include "neochatconfig.h"
#include "neochatroom.h"
#include "notificationsmanager.h"
#include "proxycontroller.h"
#include "roommanager.h"
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
@@ -46,7 +46,7 @@ Controller::Controller(QObject *parent)
{
Connection::setRoomType<NeoChatRoom>();
ProxyController::instance().setApplicationProxy();
setApplicationProxy();
#ifndef Q_OS_ANDROID
setQuitOnLastWindowClosed();
@@ -287,10 +287,28 @@ void Controller::setActiveConnection(NeoChatConnection *connection)
if (connection == m_connection) {
return;
}
if (m_connection != nullptr) {
disconnect(m_connection, &NeoChatConnection::syncError, this, nullptr);
disconnect(m_connection, &NeoChatConnection::accountDataChanged, this, nullptr);
}
m_connection = connection;
if (connection != nullptr) {
connect(connection, &NeoChatConnection::requestFailed, this, [](BaseJob *job) {
if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"_ls].toString() == "M_TOO_LARGE"_ls) {
RoomManager::instance().warning(i18n("File too large to download."), i18n("Contact your matrix server administrator for support."));
}
});
}
NeoChatConfig::self()->save();
Q_EMIT activeConnectionChanged();
}
void Controller::forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item)
{
// HACK: Workaround bug QTBUG 93281
connect(textDocument->textDocument(), SIGNAL(imagesLoaded()), item, SLOT(updateWholeDocument()));
}
void Controller::listenForNotifications()
{
#ifdef HAVE_KUNIFIEDPUSH
@@ -312,6 +330,36 @@ void Controller::listenForNotifications()
#endif
}
void Controller::setApplicationProxy()
{
NeoChatConfig *cfg = NeoChatConfig::self();
QNetworkProxy proxy;
// type match to ProxyType from neochatconfig.kcfg
switch (cfg->proxyType()) {
case 1: // HTTP
proxy.setType(QNetworkProxy::HttpProxy);
proxy.setHostName(cfg->proxyHost());
proxy.setPort(cfg->proxyPort());
proxy.setUser(cfg->proxyUser());
proxy.setPassword(cfg->proxyPassword());
break;
case 2: // SOCKS 5
proxy.setType(QNetworkProxy::Socks5Proxy);
proxy.setHostName(cfg->proxyHost());
proxy.setPort(cfg->proxyPort());
proxy.setUser(cfg->proxyUser());
proxy.setPassword(cfg->proxyPassword());
break;
case 0: // System Default
default:
// do nothing
break;
}
QNetworkProxy::setApplicationProxy(proxy);
}
bool Controller::isFlatpak() const
{
#ifdef NEOCHAT_FLATPAK

View File

@@ -84,8 +84,22 @@ public:
[[nodiscard]] bool supportSystemTray() const;
/**
* @brief Sets the QNetworkProxy for the application.
*
* @sa QNetworkProxy::setApplicationProxy
*/
Q_INVOKABLE void setApplicationProxy();
bool isFlatpak() const;
/**
* @brief Force a QQuickTextDocument to refresh when images are loaded.
*
* HACK: This is a workaround for QTBUG 93281.
*/
Q_INVOKABLE void forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item);
/**
* @brief Start listening for notifications in dbus-activated mode.
* These notifications will quit the application when closed.

View File

@@ -6,13 +6,6 @@
#include <QObject>
#include <QQmlEngine>
#include <Quotient/events/encryptedevent.h>
#include <Quotient/events/roomevent.h>
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/stickerevent.h>
#include "events/pollevent.h"
/**
* @class DelegateType
*
@@ -33,34 +26,23 @@ public:
* similar to the spec it is not the same.
*/
enum Type {
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. */
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
Loading, /**< A delegate to tell the user more messages are being loaded. */
TimelineEnd, /**< A delegate to inform that all messages are loaded. */
Other, /**< Anything that cannot be classified as another type. */
};
Q_ENUM(Type);
/**
* @brief Return the delegate type for the given event.
*
* @param event the event to return a type for.
*
* @sa Type
*/
static Type typeForEvent(const Quotient::RoomEvent &event)
{
if (event.is<Quotient::RoomMessageEvent>() || event.is<Quotient::StickerEvent>() || event.is<Quotient::EncryptedEvent>()
|| event.is<Quotient::PollStartEvent>()) {
return Message;
}
if (event.isStateEvent()) {
if (event.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
return Message;
}
return State;
}
return Other;
}
};

View File

@@ -1,107 +0,0 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <Quotient/events/encryptedevent.h>
#include <Quotient/events/roomevent.h>
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/stickerevent.h>
#include "events/pollevent.h"
/**
* @class MessageComponentType
*
* This class is designed to define the MessageComponentType enumeration.
*/
class MessageComponentType : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief The type of component that is needed for an event.
*
* @note While similar this is not the matrix event or message type. This is
* to tell a QML Bubble what component to use to visualise all or part of
* a room message.
*/
enum Type {
Text, /**< A text message. */
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. */
Poll, /**< The initial event for a poll. */
Location, /**< A location event. */
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
Encrypted, /**< An encrypted message that cannot be decrypted. */
Reply, /**< A component to show a replied-to message. */
ReplyLoad, /**< A loading dialog for a reply. */
LinkPreview, /**< A preview of a URL in the message. */
LinkPreviewLoad, /**< A loading dialog for a link preview. */
Edit, /**< A text edit for editing a message. */
Other, /**< Anything that cannot be classified as another type. */
};
Q_ENUM(Type);
/**
* @brief Return the delegate type for the given event.
*
* @param event the event to return a type for.
*
* @sa Type
*/
static Type typeForEvent(const Quotient::RoomEvent &event)
{
using namespace Quotient;
if (const auto e = eventCast<const RoomMessageEvent>(&event)) {
switch (e->msgtype()) {
case MessageEventType::Emote:
return MessageComponentType::Text;
case MessageEventType::Notice:
return MessageComponentType::Text;
case MessageEventType::Image:
return MessageComponentType::Image;
case MessageEventType::Audio:
return MessageComponentType::Audio;
case MessageEventType::Video:
return MessageComponentType::Video;
case MessageEventType::Location:
return MessageComponentType::Location;
case MessageEventType::File:
return MessageComponentType::File;
default:
return MessageComponentType::Text;
}
}
if (is<const StickerEvent>(event)) {
return MessageComponentType::Image;
}
if (event.isStateEvent()) {
if (event.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
return MessageComponentType::LiveLocation;
}
return MessageComponentType::Other;
}
if (is<const EncryptedEvent>(event)) {
return MessageComponentType::Encrypted;
}
if (is<PollStartEvent>(event)) {
const auto pollEvent = eventCast<const PollStartEvent>(&event);
if (pollEvent->isRedacted()) {
return MessageComponentType::Text;
}
return MessageComponentType::Poll;
}
return MessageComponentType::Other;
}
};

View File

@@ -1,97 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QObject>
#include "neochatroom.h"
#include <Quotient/quotient_common.h>
#include <KLocalizedString>
class NeoChatRoomType : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief Defines the room list categories a room can be assigned.
*/
enum Types {
Search = 0, /**< So we can show a search delegate if needed, e.g. collapsed mode. */
Invited, /**< The user has been invited to the room. */
Favorite, /**< The room is set as a favourite. */
Direct, /**< The room is a direct chat. */
Normal, /**< The default category for a joined room. */
Deprioritized, /**< The room is set as low priority. */
Space, /**< The room is a space. */
AddDirect, /**< So we can show the add friend delegate. */
};
Q_ENUM(Types);
static NeoChatRoomType::Types typeForRoom(const NeoChatRoom *room)
{
if (room->isSpace()) {
return NeoChatRoomType::Space;
}
if (room->joinState() == Quotient::JoinState::Invite) {
return NeoChatRoomType::Invited;
}
if (room->isFavourite()) {
return NeoChatRoomType::Favorite;
}
if (room->isLowPriority()) {
return NeoChatRoomType::Deprioritized;
}
if (room->isDirectChat()) {
return NeoChatRoomType::Direct;
}
return NeoChatRoomType::Normal;
}
static QString typeName(int category)
{
switch (category) {
case NeoChatRoomType::Invited:
return i18n("Invited");
case NeoChatRoomType::Favorite:
return i18n("Favorite");
case NeoChatRoomType::Direct:
return i18n("Friends");
case NeoChatRoomType::Normal:
return i18n("Normal");
case NeoChatRoomType::Deprioritized:
return i18n("Low priority");
case NeoChatRoomType::Space:
return i18n("Spaces");
case NeoChatRoomType::Search:
return i18n("Search");
default:
return {};
}
}
static QString typeIconName(int category)
{
switch (category) {
case NeoChatRoomType::Invited:
return QStringLiteral("user-invisible");
case NeoChatRoomType::Favorite:
return QStringLiteral("favorite");
case NeoChatRoomType::Direct:
return QStringLiteral("dialog-messages");
case NeoChatRoomType::Normal:
return QStringLiteral("group");
case NeoChatRoomType::Deprioritized:
return QStringLiteral("object-order-lower");
case NeoChatRoomType::Space:
return QStringLiteral("group");
case NeoChatRoomType::Search:
return QStringLiteral("search");
default:
return QStringLiteral("tools-report-bug");
}
}
};

View File

@@ -14,19 +14,15 @@
#include <Quotient/events/roomavatarevent.h>
#include <Quotient/events/roomcanonicalaliasevent.h>
#include <Quotient/events/roommemberevent.h>
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/roompowerlevelsevent.h>
#include <Quotient/events/simplestateevents.h>
#include <Quotient/events/stickerevent.h>
#include <Quotient/quotient_common.h>
#include "delegatetype.h"
#include "eventhandler_logging.h"
#include "events/locationbeaconevent.h"
#include "events/pollevent.h"
#include "events/serveraclevent.h"
#include "events/widgetevent.h"
#include "linkpreviewer.h"
#include "messagecomponenttype.h"
#include "models/reactionmodel.h"
#include "neochatconfig.h"
#include "neochatroom.h"
@@ -35,10 +31,34 @@
using namespace Quotient;
EventHandler::EventHandler(const NeoChatRoom *room, const RoomEvent *event)
: m_room(room)
, m_event(event)
const NeoChatRoom *EventHandler::getRoom() const
{
return m_room;
}
void EventHandler::setRoom(const NeoChatRoom *room)
{
if (room == m_room) {
return;
}
m_room = room;
}
const Quotient::Event *EventHandler::getEvent() const
{
return m_event;
}
void EventHandler::setEvent(const Quotient::RoomEvent *event)
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "cannot setEvent when m_room is set to nullptr.";
return;
}
if (event == m_event) {
return;
}
m_event = event;
}
QString EventHandler::getId() const
@@ -51,14 +71,62 @@ QString EventHandler::getId() const
return !m_event->id().isEmpty() ? m_event->id() : m_event->transactionId();
}
MessageComponentType::Type EventHandler::messageComponentType() const
DelegateType::Type EventHandler::getDelegateTypeForEvent(const Quotient::RoomEvent *event) const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "messageComponentType called with m_event set to nullptr.";
return MessageComponentType::Other;
if (auto e = eventCast<const RoomMessageEvent>(event)) {
switch (e->msgtype()) {
case MessageEventType::Emote:
return DelegateType::Emote;
case MessageEventType::Notice:
return DelegateType::Notice;
case MessageEventType::Image:
return DelegateType::Image;
case MessageEventType::Audio:
return DelegateType::Audio;
case MessageEventType::Video:
return DelegateType::Video;
case MessageEventType::Location:
return DelegateType::Location;
default:
break;
}
if (e->hasFileContent()) {
return DelegateType::File;
}
return DelegateType::Message;
}
if (is<const StickerEvent>(*event)) {
return DelegateType::Sticker;
}
if (event->isStateEvent()) {
if (event->matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
return DelegateType::LiveLocation;
}
return DelegateType::State;
}
if (is<const EncryptedEvent>(*event)) {
return DelegateType::Encrypted;
}
if (is<PollStartEvent>(*event)) {
const auto pollEvent = eventCast<const PollStartEvent>(event);
if (pollEvent->isRedacted()) {
return DelegateType::Message;
}
return DelegateType::Poll;
}
return MessageComponentType::typeForEvent(*m_event);
return DelegateType::Other;
}
DelegateType::Type EventHandler::getDelegateType() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getDelegateType called with m_event set to nullptr.";
return DelegateType::Other;
}
return getDelegateTypeForEvent(m_event);
}
QVariantMap EventHandler::getAuthor(bool isPending) const
@@ -232,6 +300,27 @@ bool EventHandler::isHidden()
return false;
}
QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event)
{
if (event.hasFileContent()) {
auto fileCaption = event.content()->fileInfo()->originalName;
if (fileCaption.isEmpty()) {
fileCaption = event.plainBody();
} else if (event.content()->fileInfo()->originalName != event.plainBody()) {
fileCaption = event.plainBody() + " | "_ls + fileCaption;
}
return fileCaption;
}
QString body;
if (event.hasTextContent() && event.content()) {
body = static_cast<const MessageEventContent::TextContent *>(event.content())->body;
} else {
body = event.plainBody();
}
return body;
}
QString EventHandler::getRichBody(bool stripNewlines) const
{
if (m_event == nullptr) {
@@ -250,6 +339,22 @@ QString EventHandler::getPlainBody(bool stripNewlines) const
return getBody(m_event, Qt::PlainText, stripNewlines);
}
QString EventHandler::getMarkdownBody() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getMarkdownBody called with m_event set to nullptr.";
return {};
}
if (!m_event->is<RoomMessageEvent>()) {
qCWarning(EventHandling) << "getMarkdownBody called when m_event isn't a RoomMessageEvent.";
return {};
}
const auto roomMessageEvent = eventCast<const RoomMessageEvent>(m_event);
return roomMessageEvent->plainBody();
}
QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const
{
if (event->isRedacted()) {
@@ -277,7 +382,8 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
}
if (prettyPrint) {
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\">%2</a>").arg(e.userId(), subjectName);
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
.arg(e.userId(), Utils::getUserColor(m_room->user(e.userId())->hueF()).name(), subjectName);
}
// The below code assumes senderName output in AuthorRole
@@ -388,22 +494,22 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
[](const RoomPowerLevelsEvent &) {
return i18nc("'power level' means permission level", "changed the power levels for this room");
},
[](const LocationBeaconEvent &e) {
return e.contentJson()["description"_ls].toString();
},
[](const ServerAclEvent &) {
return i18n("changed the server access control lists for this room");
},
[](const WidgetEvent &e) {
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
return i18nc("[User] added <name> widget", "added %1 widget", e.contentJson()["name"_ls].toString());
}
if (e.contentJson().isEmpty()) {
return i18nc("[User] removed <name> widget", "removed %1 widget", e.fullJson()["unsigned"_ls]["prev_content"_ls]["name"_ls].toString());
}
return i18nc("[User] configured <name> widget", "configured %1 widget", e.contentJson()["name"_ls].toString());
},
[prettyPrint](const StateEvent &e) {
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
return i18n("changed the server access control lists for this room");
}
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
return i18nc("[User] added <name> widget", "added %1 widget", e.contentJson()["name"_ls].toString());
}
if (e.contentJson().isEmpty()) {
return i18nc("[User] removed <name> widget", "removed %1 widget", e.fullJson()["unsigned"_ls]["prev_content"_ls]["name"_ls].toString());
}
return i18nc("[User] configured <name> widget", "configured %1 widget", e.contentJson()["name"_ls].toString());
}
if (e.matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
return e.contentJson()["description"_ls].toString();
}
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
: i18n("updated %1 state for %2", e.matrixType(), prettyPrint ? e.stateKey().toHtmlEscaped() : e.stateKey());
},
@@ -557,22 +663,19 @@ QString EventHandler::getGenericBody() const
[](const RoomPowerLevelsEvent &) {
return i18nc("'power level' means permission level", "changed the power levels for this room");
},
[](const LocationBeaconEvent &) {
return i18n("sent a live location beacon");
},
[](const ServerAclEvent &) {
return i18n("changed the server access control lists for this room");
},
[](const WidgetEvent &e) {
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
return i18n("added a widget");
[](const StateEvent &e) {
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
return i18n("changed the server access control lists for this room");
}
if (e.contentJson().isEmpty()) {
return i18n("removed a widget");
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
return i18n("added a widget");
}
if (e.contentJson().isEmpty()) {
return i18n("removed a widget");
}
return i18n("configured a widget");
}
return i18n("configured a widget");
},
[](const StateEvent &) {
return i18n("updated the state");
},
[](const PollStartEvent &e) {
@@ -729,22 +832,22 @@ QString EventHandler::getReplyId() const
return m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
}
MessageComponentType::Type EventHandler::replyMessageComponentType() const
DelegateType::Type EventHandler::getReplyDelegateType() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "replyMessageComponentType called with m_room set to nullptr.";
return MessageComponentType::Other;
qCWarning(EventHandling) << "getReplyDelegateType called with m_room set to nullptr.";
return DelegateType::Other;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "replyMessageComponentType called with m_event set to nullptr.";
return MessageComponentType::Other;
qCWarning(EventHandling) << "getReplyDelegateType called with m_event set to nullptr.";
return DelegateType::Other;
}
auto replyEvent = m_room->getReplyForEvent(*m_event);
if (replyEvent == nullptr) {
return MessageComponentType::Other;
return DelegateType::Other;
}
return MessageComponentType::typeForEvent(*replyEvent);
return getDelegateTypeForEvent(replyEvent);
}
QVariantMap EventHandler::getReplyAuthor() const

View File

@@ -11,7 +11,7 @@
#include <Quotient/events/roomevent.h>
#include <Quotient/events/roommessageevent.h>
#include "enums/messagecomponenttype.h"
#include "enums/delegatetype.h"
class LinkPreviewer;
class NeoChatRoom;
@@ -31,12 +31,30 @@ class ReactionModel;
* information. This is to minimize warnings from QML especially during startup
* and room changes.
*/
class EventHandler
class EventHandler : public QObject
{
Q_GADGET
Q_OBJECT
public:
EventHandler(const NeoChatRoom *room, const Quotient::RoomEvent *event);
/**
* @brief Return the current room the EventHandler is using.
*/
const NeoChatRoom *getRoom() const;
/**
* @brief Set the current room the EventHandler to using.
*/
void setRoom(const NeoChatRoom *room);
/**
* @brief Return the current event the EventHandler is using.
*/
const Quotient::Event *getEvent() const;
/**
* @brief Set the current event the EventHandler to using.
*/
void setEvent(const Quotient::RoomEvent *event);
/**
* @brief Return the Matrix ID of the event.
@@ -44,9 +62,13 @@ public:
QString getId() const;
/**
* @brief The MessageComponentType to use to visualise the main event content.
* @brief Return the DelegateType of 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.
*/
MessageComponentType::Type messageComponentType() const;
DelegateType::Type getDelegateType() const;
/**
* @brief Get the author of the event in context of the room.
@@ -137,6 +159,14 @@ public:
*/
bool isHidden();
/**
* @brief Output a string for the room message content without any formatting.
*
* This is the content of the formatted_body key if present or the body key if
* not.
*/
static QString rawMessageBody(const Quotient::RoomMessageEvent &event);
/**
* @brief Output a string for the message content ready for display in a rich text field.
*
@@ -169,6 +199,13 @@ public:
*/
QString getPlainBody(bool stripNewlines = false) const;
/**
* @brief Output the original body for the message content, useful for editing the original message.
*
* The event type must be a room message event.
*/
QString getMarkdownBody() const;
/**
* @brief Output a generic string for the message content ready for display.
*
@@ -220,9 +257,13 @@ public:
QString getReplyId() const;
/**
* @brief The MessageComponentType to use to visualise the reply content.
* @brief Return the DelegateType of the event replied to.
*
* @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.
*/
MessageComponentType::Type replyMessageComponentType() const;
DelegateType::Type getReplyDelegateType() const;
/**
* @brief Get the author of the event replied to in context of the room.
@@ -378,6 +419,8 @@ private:
KFormat m_format;
DelegateType::Type getDelegateTypeForEvent(const Quotient::RoomEvent *event) const;
QString getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const;
QString getMessageBody(const Quotient::RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const;

View File

@@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <Quotient/events/simplestateevents.h>
namespace Quotient
{
// Defined so we can directly switch on type.
DEFINE_SIMPLE_STATE_EVENT(LocationBeaconEvent, "org.matrix.msc3672.beacon_info", QString, body, "body")
} // namespace Quotient

View File

@@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <Quotient/events/simplestateevents.h>
namespace Quotient
{
// Defined so we can directly switch on type.
DEFINE_SIMPLE_STATE_EVENT(ServerAclEvent, "m.room.server_acl", bool, allow_ip_literals, "allow_ip_literals")
} // namespace Quotient

View File

@@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <Quotient/events/simplestateevents.h>
namespace Quotient
{
// Defined so we can directly switch on type.
DEFINE_SIMPLE_STATE_EVENT(WidgetEvent, "im.vector.modular.widgets", QString, name, "name")
} // namespace Quotient

36
src/fakerunner.cpp Normal file
View File

@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "fakerunner.h"
#include <QCoreApplication>
#include <QDBusMetaType>
Q_SCRIPTABLE RemoteActions FakeRunner::Actions()
{
QCoreApplication::quit();
return {};
}
Q_SCRIPTABLE RemoteMatches FakeRunner::Match(const QString &searchTerm)
{
QCoreApplication::quit();
return {};
}
Q_SCRIPTABLE void FakeRunner::Run(const QString &id, const QString &actionId)
{
QCoreApplication::quit();
}
FakeRunner::FakeRunner()
: QObject()
{
qDBusRegisterMetaType<RemoteMatch>();
qDBusRegisterMetaType<RemoteMatches>();
qDBusRegisterMetaType<RemoteAction>();
qDBusRegisterMetaType<RemoteActions>();
qDBusRegisterMetaType<RemoteImage>();
}
#include "moc_fakerunner.cpp"

31
src/fakerunner.h Normal file
View File

@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QDBusContext>
#include "runner.h"
/**
* This is a close-to-identical copy of the regular Runner interface,
* only used when activated for push notifications. This stubs it out so
* Plasma Search and Kickoff doesn't accidentally activate the push notification
* service.
*
* @sa Runner
*/
class FakeRunner : public QObject, protected QDBusContext
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.kde.krunner1")
public:
Q_SCRIPTABLE RemoteActions Actions();
Q_SCRIPTABLE RemoteMatches Match(const QString &searchTerm);
Q_SCRIPTABLE void Run(const QString &id, const QString &actionId);
FakeRunner();
};

View File

@@ -13,8 +13,8 @@
using namespace Quotient;
LinkPreviewer::LinkPreviewer(const NeoChatRoom *room, const Quotient::RoomMessageEvent *event, QObject *parent)
: QObject(parent)
LinkPreviewer::LinkPreviewer(const NeoChatRoom *room, const Quotient::RoomMessageEvent *event)
: QObject(nullptr)
, m_currentRoom(room)
, m_event(event)
, m_loaded(false)

View File

@@ -60,7 +60,7 @@ class LinkPreviewer : public QObject
Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged)
public:
explicit LinkPreviewer(const NeoChatRoom *room = nullptr, const Quotient::RoomMessageEvent *event = nullptr, QObject *parent = nullptr);
explicit LinkPreviewer(const NeoChatRoom *room = nullptr, const Quotient::RoomMessageEvent *event = nullptr);
[[nodiscard]] QUrl url() const;
[[nodiscard]] bool loaded() const;

View File

@@ -48,6 +48,11 @@
#ifdef HAVE_RUNNER
#include "runner.h"
#include <QDBusConnection>
#include <QDBusMetaType>
#endif
#if defined(HAVE_RUNNER) && defined(HAVE_KUNIFIEDPUSH)
#include "fakerunner.h"
#endif
#ifdef Q_OS_WINDOWS
@@ -196,6 +201,14 @@ int main(int argc, char *argv[])
// We want to be replaceable by the main client
KDBusService service(KDBusService::Replace);
#ifdef HAVE_RUNNER
// If we are built with KRunner and KUnifiedPush support, we need to do something special.
// Because KRunner may call us on the D-Bus (under the same service name org.kde.neochat) then it may
// accidentally activate us for push notifications instead. If this happens, then immediately quit if the fake
// runner is called.
QDBusConnection::sessionBus().registerObject("/RoomRunner"_ls, new FakeRunner(), QDBusConnection::ExportScriptableContents);
#endif
Controller::listenForNotifications();
return QCoreApplication::exec();
}

View File

@@ -230,7 +230,7 @@ QList<ActionsModel::Action> actions{
}
auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text);
if (targetRoom) {
RoomManager::instance().resolveResource(targetRoom->id());
RoomManager::instance().enterRoom(dynamic_cast<NeoChatRoom *>(targetRoom));
return QString();
}
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
@@ -256,7 +256,7 @@ QList<ActionsModel::Action> actions{
}
auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text);
if (targetRoom) {
RoomManager::instance().resolveResource(targetRoom->id());
RoomManager::instance().enterRoom(dynamic_cast<NeoChatRoom *>(targetRoom));
return QString();
}
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Knocking room <roomname>.", "Knocking room %1.", text));
@@ -354,17 +354,12 @@ QList<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 user = room->connection()->users()[text];
if (room->connection()->ignoredUsers().contains(user->id())) {
if (room->connection()->ignoredUsers().contains(text)) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<username> is already ignored.", "%1 is already ignored.", text));
return QString();
}
if (user) {
room->connection()->addToIgnoredUsers(user);
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is now ignored", "%1 is now ignored.", text));
} else {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("<username> is not a known user", "%1 is not a known user.", text));
}
room->connection()->addToIgnoredUsers(room->connection()->user(text));
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is now ignored", "%1 is now ignored.", text));
return QString();
},
false,
@@ -382,17 +377,12 @@ QList<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 user = room->connection()->users()[text];
if (user) {
if (!room->connection()->ignoredUsers().contains(user->id())) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<username> is not ignored.", "%1 is not ignored.", text));
return QString();
}
room->connection()->removeFromIgnoredUsers(user);
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is no longer ignored.", "%1 is no longer ignored.", text));
} else {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("<username> is not a known user", "%1 is not a known user.", text));
if (!room->connection()->ignoredUsers().contains(text)) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<username> is not ignored.", "%1 is not ignored.", text));
return QString();
}
room->connection()->removeFromIgnoredUsers(room->connection()->user(text));
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is no longer ignored.", "%1 is no longer ignored.", text));
return QString();
},
false,

View File

@@ -1,163 +0,0 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "itinerarymodel.h"
#include <QProcess>
#include "config-neochat.h"
#ifndef Q_OS_ANDROID
#include <KIO/ApplicationLauncherJob>
#endif
ItineraryModel::ItineraryModel(QObject *parent)
: QAbstractListModel(parent)
{
}
void ItineraryModel::setConnection(NeoChatConnection *connection)
{
if (m_connection == connection) {
return;
}
m_connection = connection;
Q_EMIT connectionChanged();
}
NeoChatConnection *ItineraryModel::connection() const
{
return m_connection;
}
QVariant ItineraryModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
auto row = index.row();
auto data = m_data[row];
if (role == NameRole) {
if (data[QStringLiteral("@type")] == QStringLiteral("TrainReservation")) {
return data[QStringLiteral("reservationFor")][QStringLiteral("trainNumber")];
}
if (data[QStringLiteral("@type")] == QStringLiteral("LodgingReservation")) {
return data[QStringLiteral("reservationFor")][QStringLiteral("name")];
}
}
if (role == TypeRole) {
return data[QStringLiteral("@type")];
}
if (role == DepartureStationRole) {
return data[QStringLiteral("reservationFor")][QStringLiteral("departureStation")][QStringLiteral("name")];
}
if (role == ArrivalStationRole) {
return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalStation")][QStringLiteral("name")];
}
if (role == DepartureTimeRole) {
const auto &time = data[QStringLiteral("reservationFor")][QStringLiteral("departureTime")];
auto dateTime = (time.isString() ? time : time[QStringLiteral("@value")]).toVariant().toDateTime();
if (const auto &timeZone = time[QStringLiteral("timezone")].toString(); timeZone.length() > 0) {
dateTime.setTimeZone(QTimeZone(timeZone.toLatin1().data()));
}
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
if (role == ArrivalTimeRole) {
const auto &time = data[QStringLiteral("reservationFor")][QStringLiteral("arrivalTime")];
auto dateTime = (time.isString() ? time : time[QStringLiteral("@value")]).toVariant().toDateTime();
if (const auto &timeZone = time[QStringLiteral("timezone")].toString(); timeZone.length() > 0) {
dateTime.setTimeZone(QTimeZone(timeZone.toLatin1().data()));
}
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
if (role == AddressRole) {
const auto &addressData = data[QStringLiteral("reservationFor")][QStringLiteral("address")];
return QStringLiteral("%1 - %2 %3 %4")
.arg(addressData[QStringLiteral("streetAddress")].toString(),
addressData[QStringLiteral("postalCode")].toString(),
addressData[QStringLiteral("addressLocality")].toString(),
addressData[QStringLiteral("addressCountry")].toString());
}
if (role == StartTimeRole) {
auto dateTime = data[QStringLiteral("checkinTime")][QStringLiteral("@value")].toVariant().toDateTime();
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
if (role == EndTimeRole) {
auto dateTime = data[QStringLiteral("checkoutTime")][QStringLiteral("@value")].toVariant().toDateTime();
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
if (role == DeparturePlatformRole) {
return data[QStringLiteral("reservationFor")][QStringLiteral("departurePlatform")];
}
if (role == ArrivalPlatformRole) {
return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalPlatform")];
}
if (role == CoachRole) {
return data[QStringLiteral("reservedTicket")][QStringLiteral("ticketedSeat")][QStringLiteral("seatSection")];
}
if (role == SeatRole) {
return data[QStringLiteral("reservedTicket")][QStringLiteral("ticketedSeat")][QStringLiteral("seatNumber")];
}
return {};
}
int ItineraryModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_data.size();
}
QHash<int, QByteArray> ItineraryModel::roleNames() const
{
return {
{NameRole, "name"},
{TypeRole, "type"},
{DepartureStationRole, "departureStation"},
{ArrivalStationRole, "arrivalStation"},
{DepartureTimeRole, "departureTime"},
{ArrivalTimeRole, "arrivalTime"},
{AddressRole, "address"},
{StartTimeRole, "startTime"},
{EndTimeRole, "endTime"},
{DeparturePlatformRole, "departurePlatform"},
{ArrivalPlatformRole, "arrivalPlatform"},
{CoachRole, "coach"},
{SeatRole, "seat"},
};
}
QString ItineraryModel::path() const
{
return m_path;
}
void ItineraryModel::setPath(const QString &path)
{
if (path == m_path) {
return;
}
m_path = path;
Q_EMIT pathChanged();
loadData();
}
void ItineraryModel::loadData()
{
auto process = new QProcess(this);
process->start(QLatin1String(CMAKE_INSTALL_FULL_LIBEXECDIR_KF6) + QLatin1String("/kitinerary-extractor"), {m_path.mid(7)});
connect(process, &QProcess::finished, this, [this, process]() {
auto data = process->readAllStandardOutput();
beginResetModel();
m_data = QJsonDocument::fromJson(data).array();
endResetModel();
});
}
void ItineraryModel::sendToItinerary()
{
#ifndef Q_OS_ANDROID
auto job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(QStringLiteral("org.kde.itinerary")));
job->setUrls({QUrl::fromLocalFile(m_path.mid(7))});
job->start();
#endif
}

View File

@@ -1,62 +0,0 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QPointer>
#include <QQmlEngine>
#include <QString>
#include "neochatconnection.h"
class ItineraryModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
public:
enum Roles {
NameRole = Qt::DisplayRole,
TypeRole,
DepartureStationRole,
ArrivalStationRole,
DepartureTimeRole,
ArrivalTimeRole,
AddressRole,
StartTimeRole,
EndTimeRole,
DeparturePlatformRole,
ArrivalPlatformRole,
CoachRole,
SeatRole,
};
Q_ENUM(Roles)
explicit ItineraryModel(QObject *parent = nullptr);
void setConnection(NeoChatConnection *connection);
NeoChatConnection *connection() const;
QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent = {}) const override;
QHash<int, QByteArray> roleNames() const override;
QString path() const;
void setPath(const QString &path);
Q_INVOKABLE void sendToItinerary();
Q_SIGNALS:
void connectionChanged();
void pathChanged();
private:
QPointer<NeoChatConnection> m_connection;
QJsonArray m_data;
QString m_path;
void loadData();
};

View File

@@ -1,64 +0,0 @@
// SPDX-FileCopyrightText: 2024 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 "linemodel.h"
LineModel::LineModel(QObject *parent)
: QAbstractListModel(parent)
{
}
QQuickTextDocument *LineModel::document() const
{
return m_document;
}
void LineModel::setDocument(QQuickTextDocument *document)
{
if (document == m_document) {
return;
}
m_document = document;
Q_EMIT documentChanged();
resetModel();
}
QVariant LineModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
const auto &row = index.row();
if (row < 0 || row > rowCount()) {
return {};
}
if (role == LineHeightRole) {
auto textDoc = m_document->textDocument();
return int(textDoc->documentLayout()->blockBoundingRect(textDoc->findBlockByNumber(row)).height());
}
return {};
}
int LineModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
if (m_document == nullptr) {
return 0;
}
return m_document->textDocument()->blockCount();
}
QHash<int, QByteArray> LineModel::roleNames() const
{
return {{LineHeightRole, "docLineHeight"}};
}
void LineModel::resetModel()
{
beginResetModel();
endResetModel();
}

View File

@@ -1,80 +0,0 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QAbstractListModel>
#include <QAbstractTextDocumentLayout>
#include <QQmlEngine>
#include <QQuickTextDocument>
#include <QTextBlock>
#include <qtmetamacros.h>
/**
* @class LineModel
*
* A model to provide line info for a QQuickTextDocument.
*
* @sa QQuickTextDocument
*/
class LineModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The QQuickTextDocument that is being handled.
*/
Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
LineHeightRole = Qt::UserRole + 1, /**< The delegate type of the message. */
};
Q_ENUM(Roles)
explicit LineModel(QObject *parent = nullptr);
[[nodiscard]] QQuickTextDocument *document() const;
void setDocument(QQuickTextDocument *document);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role) 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 Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Reset the model.
*
* This needs to be called when the QQuickTextDocument container changes width
* or height as this may change line heights due to wrapping.
*
* @sa QQuickTextDocument
*/
Q_INVOKABLE void resetModel();
Q_SIGNALS:
void documentChanged();
private:
QPointer<QQuickTextDocument> m_document = nullptr;
};

View File

@@ -3,9 +3,9 @@
#include "mediamessagefiltermodel.h"
#include <Quotient/events/roommessageevent.h>
#include <Quotient/room.h>
#include "enums/delegatetype.h"
#include "messageeventmodel.h"
#include "messagefiltermodel.h"
@@ -20,8 +20,8 @@ bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex
{
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
if (index.data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("image"))
|| index.data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("video"))) {
if (index.data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image
|| index.data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Video) {
return true;
}
return false;
@@ -30,9 +30,9 @@ bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex
QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
{
if (role == SourceRole) {
if (mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("image"))) {
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("source")].toUrl();
} else if (mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("video"))) {
} else if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Video) {
auto progressInfo = mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>();
if (progressInfo.completed()) {
@@ -48,7 +48,7 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("tempInfo")].toMap()[QStringLiteral("source")].toUrl();
}
if (role == TypeRole) {
if (mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("image"))) {
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image) {
return MediaType::Image;
} else {
return MediaType::Video;

View File

@@ -1,245 +0,0 @@
// SPDX-FileCopyrightText: 2024 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 "messagecontentmodel.h"
#include <Quotient/events/redactionevent.h>
#include <Quotient/events/stickerevent.h>
#include <KLocalizedString>
#include "chatbarcache.h"
#include "enums/messagecomponenttype.h"
#include "eventhandler.h"
#include "linkpreviewer.h"
#include "neochatroom.h"
MessageContentModel::MessageContentModel(const Quotient::RoomEvent *event, NeoChatRoom *room)
: QAbstractListModel(nullptr)
, m_room(room)
, m_event(event)
{
if (m_room != nullptr) {
connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) {
if (m_room != nullptr && m_event != nullptr) {
if (m_event->id() == serverEvent->id()) {
beginResetModel();
m_event = serverEvent;
endResetModel();
}
}
});
connect(m_room, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) {
if (m_room != nullptr && m_event != nullptr) {
if (m_event->id() == newEvent->id()) {
beginResetModel();
m_event = newEvent;
endResetModel();
}
}
});
connect(m_room, &NeoChatRoom::replyLoaded, this, [this](const QString &eventId, const QString &replyId) {
Q_UNUSED(eventId)
if (m_event != nullptr && m_room != nullptr) {
const auto eventHandler = EventHandler(m_room, m_event);
if (replyId == eventHandler.getReplyId()) {
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
beginResetModel();
m_components[0] = MessageComponentType::Reply;
endResetModel();
}
}
});
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_event->id()) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_event->id()) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_event->id()) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_event->id()) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room->editCache(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
if (m_event != nullptr && (oldEventId == m_event->id() || newEventId == m_event->id())) {
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
beginResetModel();
endResetModel();
}
});
}
if (const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (LinkPreviewer::hasPreviewableLinks(event)) {
m_linkPreviewer = new LinkPreviewer(m_room, event, this);
connect(m_linkPreviewer, &LinkPreviewer::loadedChanged, [this]() {
if (m_linkPreviewer->loaded()) {
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
beginResetModel();
m_components[m_components.size() - 1] = MessageComponentType::LinkPreview;
endResetModel();
}
});
}
}
updateComponents();
}
static LinkPreviewer *emptyLinkPreview = new LinkPreviewer;
QVariant MessageContentModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
if (index.row() >= rowCount()) {
qDebug() << "MessageContentModel, something's wrong: index.row() >= rowCount()";
return {};
}
EventHandler eventHandler(m_room, m_event);
if (role == DisplayRole) {
if (m_event->isRedacted()) {
auto reason = m_event->redactedBecause()->reason();
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>")
: i18n("<i>[This message was deleted: %1]</i>", m_event->redactedBecause()->reason());
}
return eventHandler.getRichBody();
}
if (role == ComponentTypeRole) {
const auto component = m_components[index.row()];
if (component == MessageComponentType::Text && !m_event->id().isEmpty() && m_room->editCache()->editId() == m_event->id()) {
return MessageComponentType::Edit;
}
return component;
}
if (role == EventIdRole) {
return eventHandler.getId();
}
if (role == AuthorRole) {
return eventHandler.getAuthor(false);
}
if (role == MediaInfoRole) {
return eventHandler.getMediaInfo();
}
if (role == FileTransferInfoRole) {
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (event->hasFileContent()) {
return QVariant::fromValue(m_room->fileTransferInfo(event->id()));
}
}
if (auto event = eventCast<const Quotient::StickerEvent>(m_event)) {
return QVariant::fromValue(m_room->fileTransferInfo(event->id()));
}
}
if (role == LatitudeRole) {
return eventHandler.getLatitude();
}
if (role == LongitudeRole) {
return eventHandler.getLongitude();
}
if (role == AssetRole) {
return eventHandler.getLocationAssetType();
}
if (role == PollHandlerRole) {
return QVariant::fromValue<PollHandler *>(m_room->poll(m_event->id()));
}
if (role == IsReplyRole) {
return eventHandler.hasReply();
}
if (role == ReplyComponentType) {
return eventHandler.replyMessageComponentType();
}
if (role == ReplyEventIdRole) {
return eventHandler.getReplyId();
}
if (role == ReplyAuthorRole) {
return eventHandler.getReplyAuthor();
}
if (role == ReplyDisplayRole) {
return eventHandler.getReplyRichBody();
}
if (role == ReplyMediaInfoRole) {
return eventHandler.getReplyMediaInfo();
}
if (role == LinkPreviewerRole) {
if (m_linkPreviewer != nullptr) {
return QVariant::fromValue<LinkPreviewer *>(m_linkPreviewer);
} else {
return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview);
}
}
return {};
}
int MessageContentModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_components.size();
}
QHash<int, QByteArray> MessageContentModel::roleNames() const
{
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
roles[DisplayRole] = "display";
roles[ComponentTypeRole] = "componentType";
roles[EventIdRole] = "eventId";
roles[AuthorRole] = "author";
roles[MediaInfoRole] = "mediaInfo";
roles[FileTransferInfoRole] = "fileTransferInfo";
roles[LatitudeRole] = "latitude";
roles[LongitudeRole] = "longitude";
roles[AssetRole] = "asset";
roles[PollHandlerRole] = "pollHandler";
roles[IsReplyRole] = "isReply";
roles[ReplyComponentType] = "replyComponentType";
roles[ReplyEventIdRole] = "replyEventId";
roles[ReplyAuthorRole] = "replyAuthor";
roles[ReplyDisplayRole] = "replyDisplay";
roles[ReplyMediaInfoRole] = "replyMediaInfo";
roles[LinkPreviewerRole] = "linkPreviewer";
return roles;
}
void MessageContentModel::updateComponents()
{
beginResetModel();
m_components.clear();
EventHandler eventHandler(m_room, m_event);
if (eventHandler.hasReply()) {
if (m_room->findInTimeline(eventHandler.getReplyId()) == m_room->historyEdge()) {
m_components += MessageComponentType::ReplyLoad;
m_room->loadReply(m_event->id(), eventHandler.getReplyId());
} else {
m_components += MessageComponentType::Reply;
}
}
m_components += eventHandler.messageComponentType();
if (m_linkPreviewer != nullptr) {
if (m_linkPreviewer->loaded()) {
m_components += MessageComponentType::LinkPreview;
} else {
m_components += MessageComponentType::LinkPreviewLoad;
}
}
endResetModel();
}

View File

@@ -1,83 +0,0 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QAbstractListModel>
#include <QQmlEngine>
#include "eventhandler.h"
#include "linkpreviewer.h"
#include "messagecomponenttype.h"
#include "neochatroom.h"
/**
* @class MessageContentModel
*
* A model to visualise the components of a single RoomMessageEvent.
*/
class MessageContentModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
DisplayRole = Qt::DisplayRole, /**< The display text for the message. */
ComponentTypeRole, /**< The type of component to visualise the message. */
EventIdRole, /**< The matrix event ID of the event. */
AuthorRole, /**< The author of the event. */
MediaInfoRole, /**< The media info for the event. */
FileTransferInfoRole, /**< FileTransferInfo for any downloading files. */
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. */
PollHandlerRole, /**< The PollHandler for the event, if any. */
IsReplyRole, /**< Is the message a reply to another event. */
ReplyComponentType, /**< The type of component to visualise the reply message. */
ReplyEventIdRole, /**< The matrix ID of the message that was replied to. */
ReplyAuthorRole, /**< The author of the event that was replied to. */
ReplyDisplayRole, /**< The body of the message that was replied to. */
ReplyMediaInfoRole, /**< The media info of the message that was replied to. */
LinkPreviewerRole, /**< The link preview details. */
};
Q_ENUM(Roles)
explicit MessageContentModel(const Quotient::RoomEvent *event, NeoChatRoom *room);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
NeoChatRoom *m_room = nullptr;
const Quotient::RoomEvent *m_event = nullptr;
QVector<MessageComponentType::Type> m_components;
void updateComponents();
LinkPreviewer *m_linkPreviewer = nullptr;
};

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
#include "messageeventmodel.h"
#include "linkpreviewer.h"
#include "messageeventmodel_logging.h"
#include "neochatconfig.h"
@@ -9,7 +10,6 @@
#include <Quotient/connection.h>
#include <Quotient/csapi/rooms.h>
#include <Quotient/events/redactionevent.h>
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/stickerevent.h>
#include <Quotient/user.h>
@@ -22,8 +22,6 @@
#include "enums/delegatetype.h"
#include "eventhandler.h"
#include "events/pollevent.h"
#include "linkpreviewer.h"
#include "messagecontentmodel.h"
#include "models/reactionmodel.h"
#include "texthandler.h"
@@ -33,6 +31,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
{
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
roles[DelegateTypeRole] = "delegateType";
roles[PlainText] = "plainText";
roles[EventIdRole] = "eventId";
roles[TimeRole] = "time";
roles[TimeStringRole] = "timeString";
@@ -41,6 +40,15 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[HighlightRole] = "isHighlighted";
roles[SpecialMarksRole] = "marks";
roles[ProgressInfoRole] = "progressInfo";
roles[ShowLinkPreviewRole] = "showLinkPreview";
roles[LinkPreviewRole] = "linkPreview";
roles[MediaInfoRole] = "mediaInfo";
roles[IsReplyRole] = "isReply";
roles[ReplyAuthor] = "replyAuthor";
roles[ReplyIdRole] = "replyId";
roles[ReplyDelegateTypeRole] = "replyDelegateType";
roles[ReplyDisplayRole] = "replyDisplay";
roles[ReplyMediaInfoRole] = "replyMediaInfo";
roles[IsThreadedRole] = "isThreaded";
roles[ThreadRootRole] = "threadRoot";
roles[ShowAuthorRole] = "showAuthor";
@@ -56,8 +64,10 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[IsRedactedRole] = "isRedacted";
roles[GenericDisplayRole] = "genericDisplay";
roles[IsPendingRole] = "isPending";
roles[ContentModelRole] = "contentModel";
roles[MediaInfoRole] = "mediaInfo";
roles[LatitudeRole] = "latitude";
roles[LongitudeRole] = "longitude";
roles[AssetRole] = "asset";
roles[PollHandlerRole] = "pollHandler";
return roles;
}
@@ -86,6 +96,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
beginResetModel();
if (m_currentRoom) {
m_currentRoom->disconnect(this);
m_linkPreviewers.clear();
m_reactionModels.clear();
}
@@ -108,6 +119,14 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
room->getPreviousContent(50);
}
lastReadEventId = room->lastFullyReadEventId();
connect(m_currentRoom, &NeoChatRoom::replyLoaded, this, [this](const auto &eventId, const auto &replyId) {
Q_UNUSED(replyId);
auto row = eventIdToRow(eventId);
if (row == -1) {
return;
}
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyDelegateTypeRole, ReplyDisplayRole, ReplyMediaInfoRole, ReplyAuthor});
});
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
for (auto &&event : events) {
@@ -219,6 +238,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
moveReadMarker(toEventId);
});
connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) {
refreshLastUserEvents(refreshEvent(newEvent->id()) - timelineBaseIndex());
const RoomMessageEvent *message = eventCast<const RoomMessageEvent>(newEvent);
if (message != nullptr) {
createEventObjects(message);
@@ -245,6 +265,10 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
refreshEventRoles(event->id(), {ReadMarkersRole, ReadMarkersStringRole, ExcessReadMarkersRole});
}
});
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);
connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [this] {
beginResetModel();
endResetModel();
@@ -415,6 +439,8 @@ void MessageEventModel::fetchMore(const QModelIndex &parent)
}
}
static LinkPreviewer *emptyLinkPreview = new LinkPreviewer;
QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
{
if (!checkIndex(idx, QAbstractItemModel::CheckIndexOption::IndexIsValid)) {
@@ -455,7 +481,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
const auto pendingIt = m_currentRoom->pendingEvents().crbegin() + std::min(row, timelineBaseIndex());
const auto &evt = isPending ? **pendingIt : **timelineIt;
EventHandler eventHandler(m_currentRoom, &evt);
EventHandler eventHandler;
eventHandler.setRoom(m_currentRoom);
eventHandler.setEvent(&evt);
if (role == Qt::DisplayRole) {
if (evt.isRedacted()) {
@@ -466,24 +494,16 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return eventHandler.getRichBody();
}
if (role == ContentModelRole) {
if (!evt.isStateEvent()) {
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(&evt, m_currentRoom));
}
if (evt.isStateEvent()) {
if (evt.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(&evt, m_currentRoom));
}
}
return {};
}
if (role == GenericDisplayRole) {
return eventHandler.getGenericBody();
}
if (role == PlainText) {
return eventHandler.getPlainBody();
}
if (role == DelegateTypeRole) {
return DelegateType::typeForEvent(evt);
return eventHandler.getDelegateType();
}
if (role == AuthorRole) {
@@ -541,6 +561,46 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return eventHandler.getTimeString(true, QLocale::ShortFormat, isPending, lastUpdated);
}
if (role == ShowLinkPreviewRole) {
return m_linkPreviewers.contains(evt.id());
}
if (role == LinkPreviewRole) {
if (m_linkPreviewers.contains(evt.id())) {
return QVariant::fromValue<LinkPreviewer *>(m_linkPreviewers[evt.id()].data());
} else {
return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview);
}
}
if (role == MediaInfoRole) {
return eventHandler.getMediaInfo();
}
if (role == IsReplyRole) {
return eventHandler.hasReply();
}
if (role == ReplyIdRole) {
return eventHandler.getReplyId();
}
if (role == ReplyDelegateTypeRole) {
return eventHandler.getReplyDelegateType();
}
if (role == ReplyAuthor) {
return eventHandler.getReplyAuthor();
}
if (role == ReplyDisplayRole) {
return eventHandler.getReplyRichBody();
}
if (role == ReplyMediaInfoRole) {
return eventHandler.getReplyMediaInfo();
}
if (role == IsThreadedRole) {
return eventHandler.isThreaded();
}
@@ -584,6 +644,18 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return false;
}
if (role == LatitudeRole) {
return eventHandler.getLatitude();
}
if (role == LongitudeRole) {
return eventHandler.getLongitude();
}
if (role == AssetRole) {
return eventHandler.getLocationAssetType();
}
if (role == ReadMarkersRole) {
return eventHandler.getReadMarkers();
}
@@ -633,8 +705,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return row < static_cast<int>(m_currentRoom->pendingEvents().size());
}
if (role == MediaInfoRole) {
return eventHandler.getMediaInfo();
if (role == PollHandlerRole) {
return QVariant::fromValue<PollHandler *>(m_currentRoom->poll(evt.id()));
}
return {};
@@ -654,6 +726,16 @@ void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *eve
{
auto eventId = event->id();
if (m_linkPreviewers.contains(eventId)) {
if (!LinkPreviewer::hasPreviewableLinks(event)) {
m_linkPreviewers.remove(eventId);
}
} else {
if (LinkPreviewer::hasPreviewableLinks(event)) {
m_linkPreviewers[eventId] = QSharedPointer<LinkPreviewer>(new LinkPreviewer(m_currentRoom, event));
}
}
// ReactionModel handles updates to add and remove reactions, we only need to
// handle adding and removing whole models here.
if (m_reactionModels.contains(eventId)) {
@@ -681,7 +763,7 @@ void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *eve
bool MessageEventModel::event(QEvent *event)
{
if (event->type() == QEvent::ApplicationPaletteChange) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReadMarkersRole});
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReplyAuthor, ReadMarkersRole});
}
return QObject::event(event);
}

View File

@@ -40,6 +40,7 @@ public:
*/
enum EventRoles {
DelegateTypeRole = Qt::UserRole + 1, /**< The delegate type of the message. */
PlainText, /**< Plain text representation of the message. */
EventIdRole, /**< The matrix event ID of the event. */
TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */
TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */
@@ -49,9 +50,18 @@ public:
SpecialMarksRole, /**< Whether the event is hidden or not. */
ProgressInfoRole, /**< Progress info when downloading files. */
GenericDisplayRole, /**< A generic string based upon the message type. */
ShowLinkPreviewRole, /**< Whether a link preview should be shown. */
LinkPreviewRole, /**< The link preview details. */
MediaInfoRole, /**< The media info for the event. */
ContentModelRole, /**< The MessageContentModel for the event. */
IsReplyRole, /**< Is the message a reply to another event. */
ReplyAuthor, /**< The author of the event that was replied to. */
ReplyIdRole, /**< The matrix ID of the message that was replied to. */
ReplyDelegateTypeRole, /**< The delegate type of the message that was replied to. */
ReplyDisplayRole, /**< The body of the message that was replied to. */
ReplyMediaInfoRole, /**< The media info of the message that was replied to. */
IsThreadedRole,
ThreadRootRole,
@@ -70,6 +80,10 @@ public:
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. */
PollHandlerRole, /**< The PollHandler for the event, if any. */
LastRole, // Keep this last
};
Q_ENUM(EventRoles)
@@ -121,6 +135,7 @@ private:
bool movingEvent = false;
KFormat m_format;
QMap<QString, QSharedPointer<LinkPreviewer>> m_linkPreviewers;
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
[[nodiscard]] int timelineBaseIndex() const;

View File

@@ -127,8 +127,9 @@ QString MessageFilterModel::aggregateEventToString(int sourceRow) const
chunks.removeDuplicates();
// The author text is either "n users" if > 1 user or the matrix.to link to a single user.
QString userText = uniqueAuthors.length() > 1 ? i18ncp("n users", " %1 user ", " %1 users ", uniqueAuthors.length())
: QStringLiteral("<a href=\"https://matrix.to/#/%1\">%3</a> ")
: QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a> ")
.arg(uniqueAuthors[0].toMap()[QStringLiteral("id")].toString(),
uniqueAuthors[0].toMap()[QStringLiteral("color")].toString(),
uniqueAuthors[0].toMap()[QStringLiteral("displayName")].toString().toHtmlEscaped());
QString chunksText;

View File

@@ -122,7 +122,9 @@ void NotificationsModel::loadData()
const auto &authorAvatar = avatar.isValid() && avatar.scheme() == QStringLiteral("mxc") ? avatar : QUrl();
const auto &roomEvent = eventCast<const RoomEvent>(notification.event.get());
EventHandler eventHandler(dynamic_cast<NeoChatRoom *>(room), roomEvent);
EventHandler eventHandler;
eventHandler.setRoom(dynamic_cast<NeoChatRoom *>(room));
eventHandler.setEvent(roomEvent);
beginInsertRows({}, m_notifications.length(), m_notifications.length());
m_notifications += Notification{
.roomId = notification.roomId,

View File

@@ -5,8 +5,6 @@
#include <Quotient/connection.h>
#include "publicroomlist_logging.h"
using namespace Quotient;
PublicRoomListModel::PublicRoomListModel(QObject *parent)
@@ -43,7 +41,7 @@ void PublicRoomListModel::setConnection(Connection *conn)
if (job) {
job->abandon();
job = nullptr;
Q_EMIT searchingChanged();
Q_EMIT loadingChanged();
}
if (m_connection) {
@@ -52,6 +50,7 @@ void PublicRoomListModel::setConnection(Connection *conn)
Q_EMIT connectionChanged();
Q_EMIT serverChanged();
Q_EMIT hasMoreChanged();
}
QString PublicRoomListModel::server() const
@@ -72,13 +71,14 @@ void PublicRoomListModel::setServer(const QString &value)
nextBatch = QString();
attempted = false;
rooms.clear();
Q_EMIT loadingChanged();
endResetModel();
if (job) {
job->abandon();
job = nullptr;
Q_EMIT searchingChanged();
Q_EMIT loadingChanged();
}
if (m_connection) {
@@ -86,30 +86,42 @@ void PublicRoomListModel::setServer(const QString &value)
}
Q_EMIT serverChanged();
Q_EMIT hasMoreChanged();
}
QString PublicRoomListModel::searchText() const
QString PublicRoomListModel::keyword() const
{
return m_searchText;
return m_keyword;
}
void PublicRoomListModel::setSearchText(const QString &value)
void PublicRoomListModel::setKeyword(const QString &value)
{
if (m_searchText == value) {
if (m_keyword == value) {
return;
}
m_searchText = value;
Q_EMIT searchTextChanged();
m_keyword = value;
beginResetModel();
nextBatch = QString();
attempted = false;
rooms.clear();
endResetModel();
if (job) {
job->abandon();
job = nullptr;
Q_EMIT searchingChanged();
Q_EMIT loadingChanged();
}
if (m_connection) {
next();
}
Q_EMIT keywordChanged();
Q_EMIT hasMoreChanged();
}
bool PublicRoomListModel::showOnlySpaces() const
@@ -126,28 +138,15 @@ void PublicRoomListModel::setShowOnlySpaces(bool showOnlySpaces)
Q_EMIT showOnlySpacesChanged();
}
void PublicRoomListModel::search(int limit)
void PublicRoomListModel::next(int count)
{
if (limit < 1 || attempted) {
if (count < 1) {
return;
}
if (job) {
qCDebug(PublicRoomList) << "Other job running, ignore";
return;
}
qDebug() << "PublicRoomListModel: Other jobs running, ignore";
next(limit);
}
void PublicRoomListModel::next(int limit)
{
if (m_connection == nullptr || limit < 1) {
return;
}
if (job) {
qCDebug(PublicRoomList) << "Other job running, ignore";
return;
}
@@ -155,17 +154,11 @@ void PublicRoomListModel::next(int limit)
if (m_showOnlySpaces) {
roomTypes += QLatin1String("m.space");
}
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, limit, nextBatch, QueryPublicRoomsJob::Filter{m_searchText, roomTypes});
Q_EMIT searchingChanged();
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_keyword, roomTypes});
Q_EMIT loadingChanged();
connect(job, &BaseJob::finished, this, [this] {
if (!attempted) {
beginResetModel();
rooms.clear();
endResetModel();
attempted = true;
}
attempted = true;
if (job->status() == BaseJob::Success) {
nextBatch = job->nextBatch();
@@ -173,10 +166,14 @@ void PublicRoomListModel::next(int limit)
this->beginInsertRows({}, rooms.count(), rooms.count() + job->chunk().count() - 1);
rooms.append(job->chunk());
this->endInsertRows();
if (job->nextBatch().isEmpty()) {
Q_EMIT hasMoreChanged();
}
}
this->job = nullptr;
Q_EMIT searchingChanged();
Q_EMIT loadingChanged();
});
}
@@ -187,7 +184,8 @@ QVariant PublicRoomListModel::data(const QModelIndex &index, int role) const
}
if (index.row() >= rooms.count()) {
qCDebug(PublicRoomList) << "something's wrong: index.row() >= rooms.count()";
qDebug() << "PublicRoomListModel, something's wrong: index.row() >= "
"rooms.count()";
return {};
}
auto room = rooms.at(index.row());
@@ -273,19 +271,12 @@ int PublicRoomListModel::rowCount(const QModelIndex &parent) const
return rooms.count();
}
bool PublicRoomListModel::canFetchMore(const QModelIndex &parent) const
bool PublicRoomListModel::hasMore() const
{
Q_UNUSED(parent)
return !nextBatch.isEmpty();
return !(attempted && nextBatch.isEmpty());
}
void PublicRoomListModel::fetchMore(const QModelIndex &parent)
{
Q_UNUSED(parent)
next();
}
bool PublicRoomListModel::searching() const
bool PublicRoomListModel::loading() const
{
return job != nullptr;
}

View File

@@ -41,9 +41,9 @@ class PublicRoomListModel : public QAbstractListModel
Q_PROPERTY(QString server READ server WRITE setServer NOTIFY serverChanged)
/**
* @brief The text to search the public room list for.
* @brief The filter keyword for the list of public rooms.
*/
Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
Q_PROPERTY(QString keyword READ keyword WRITE setKeyword NOTIFY keywordChanged)
/**
* @brief Whether only space rooms should be shown.
@@ -51,9 +51,14 @@ class PublicRoomListModel : public QAbstractListModel
Q_PROPERTY(bool showOnlySpaces READ showOnlySpaces WRITE setShowOnlySpaces NOTIFY showOnlySpacesChanged)
/**
* @brief Whether the model is searching.
* @brief Whether the model has more items to load.
*/
Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged)
Q_PROPERTY(bool hasMore READ hasMore NOTIFY hasMoreChanged)
/**
* @biref Whether the model is still loading.
*/
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
public:
/**
@@ -100,38 +105,31 @@ public:
[[nodiscard]] QString server() const;
void setServer(const QString &value);
[[nodiscard]] QString searchText() const;
void setSearchText(const QString &searchText);
[[nodiscard]] QString keyword() const;
void setKeyword(const QString &value);
[[nodiscard]] bool showOnlySpaces() const;
void setShowOnlySpaces(bool showOnlySpaces);
[[nodiscard]] bool searching() const;
[[nodiscard]] bool hasMore() const;
/**
* @brief Search the room directory.
*
* @param limit the maximum number of rooms to load.
*/
Q_INVOKABLE void search(int limit = 50);
private:
QPointer<Quotient::Connection> m_connection = nullptr;
QString m_server;
QString m_searchText;
bool m_showOnlySpaces = false;
[[nodiscard]] bool loading() const;
/**
* @brief Load the next set of rooms.
*
* @param limit the maximum number of rooms to load.
* @param count the maximum number of rooms to load.
*/
void next(int limit = 50);
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
Q_INVOKABLE void next(int count = 50);
private:
Quotient::Connection *m_connection = nullptr;
QString m_server;
QString m_keyword;
bool m_showOnlySpaces = false;
bool attempted = false;
bool m_searching = false;
bool m_loading = false;
QString nextBatch;
QList<Quotient::PublicRoomsChunk> rooms;
@@ -141,7 +139,8 @@ private:
Q_SIGNALS:
void connectionChanged();
void serverChanged();
void searchTextChanged();
void keywordChanged();
void showOnlySpacesChanged();
void searchingChanged();
void hasMoreChanged();
void loadingChanged();
};

View File

@@ -28,6 +28,11 @@ Q_DECLARE_METATYPE(Quotient::JoinState)
RoomListModel::RoomListModel(QObject *parent)
: QAbstractListModel(parent)
{
const auto collapsedSections = NeoChatConfig::collapsedSections();
for (auto collapsedSection : collapsedSections) {
m_categoryVisibility[collapsedSection] = false;
}
connect(this, &RoomListModel::highlightCountChanged, this, [this]() {
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
#ifndef Q_OS_ANDROID
@@ -293,7 +298,30 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
return room->topic();
}
if (role == CategoryRole) {
return NeoChatRoomType::typeForRoom(room);
if (room->joinState() == JoinState::Invite) {
return NeoChatRoomType::Invited;
}
if (room->isFavourite()) {
return NeoChatRoomType::Favorite;
}
if (room->isLowPriority()) {
return NeoChatRoomType::Deprioritized;
}
if (room->isDirectChat()) {
return NeoChatRoomType::Direct;
}
const RoomCreateEvent *creationEvent = room->creation();
if (!creationEvent) {
return NeoChatRoomType::Normal;
}
QJsonObject contentJson = creationEvent->contentJson();
QJsonObject::const_iterator typeIter = contentJson.find("type"_ls);
if (typeIter != contentJson.end()) {
if (typeIter.value().toString() == "m.space"_ls) {
return NeoChatRoomType::Space;
}
}
return NeoChatRoomType::Normal;
}
if (role == NotificationCountRole) {
return room->notificationCount();
@@ -313,11 +341,16 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
if (role == CurrentRoomRole) {
return QVariant::fromValue(room);
}
if (role == CategoryVisibleRole) {
return m_categoryVisibility.value(data(index, CategoryRole).toInt(), true);
}
if (role == SubtitleTextRole) {
if (room->lastEvent() == nullptr || room->lastEventIsSpoiler()) {
return QString();
}
EventHandler eventHandler(room, room->lastEvent());
EventHandler eventHandler;
eventHandler.setRoom(room);
eventHandler.setEvent(room->lastEvent());
return eventHandler.subtitleText();
}
if (role == AvatarImageRole) {
@@ -330,14 +363,11 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
return room->isSpace();
}
if (role == IsChildSpaceRole) {
return SpaceHierarchyCache::instance().isChild(room->id());
return SpaceHierarchyCache::instance().isChildSpace(room->id());
}
if (role == ReplacementIdRole) {
return room->successorId();
}
if (role == IsDirectChat) {
return room->isDirectChat();
}
return QVariant();
}
@@ -366,14 +396,75 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
roles[LastActiveTimeRole] = "lastActiveTime";
roles[JoinStateRole] = "joinState";
roles[CurrentRoomRole] = "currentRoom";
roles[CategoryVisibleRole] = "categoryVisible";
roles[SubtitleTextRole] = "subtitleText";
roles[IsSpaceRole] = "isSpace";
roles[RoomIdRole] = "roomId";
roles[IsChildSpaceRole] = "isChildSpace";
roles[IsDirectChat] = "isDirectChat";
return roles;
}
QString RoomListModel::categoryName(int category)
{
switch (category) {
case NeoChatRoomType::Invited:
return i18n("Invited");
case NeoChatRoomType::Favorite:
return i18n("Favorite");
case NeoChatRoomType::Direct:
return i18n("Direct Messages");
case NeoChatRoomType::Normal:
return i18n("Normal");
case NeoChatRoomType::Deprioritized:
return i18n("Low priority");
case NeoChatRoomType::Space:
return i18n("Spaces");
default:
return {};
}
}
QString RoomListModel::categoryIconName(int category)
{
switch (category) {
case NeoChatRoomType::Invited:
return QStringLiteral("user-invisible");
case NeoChatRoomType::Favorite:
return QStringLiteral("favorite");
case NeoChatRoomType::Direct:
return QStringLiteral("dialog-messages");
case NeoChatRoomType::Normal:
return QStringLiteral("group");
case NeoChatRoomType::Deprioritized:
return QStringLiteral("object-order-lower");
case NeoChatRoomType::Space:
return QStringLiteral("group");
default:
return QStringLiteral("tools-report-bug");
}
}
void RoomListModel::setCategoryVisible(int category, bool visible)
{
beginResetModel();
auto collapsedSections = NeoChatConfig::collapsedSections();
if (visible) {
collapsedSections.removeAll(category);
} else {
collapsedSections.push_back(category);
}
NeoChatConfig::setCollapsedSections(collapsedSections);
NeoChatConfig::self()->save();
m_categoryVisibility[category] = visible;
endResetModel();
}
bool RoomListModel::categoryVisible(int category) const
{
return m_categoryVisibility.value(category, true);
}
NeoChatRoom *RoomListModel::roomByAliasOrId(const QString &aliasOrId)
{
for (const auto &room : std::as_const(m_rooms)) {

View File

@@ -6,8 +6,6 @@
#include <QAbstractListModel>
#include <QQmlEngine>
#include "enums/neochatroomtype.h"
class NeoChatRoom;
namespace Quotient
@@ -16,6 +14,27 @@ class Connection;
class Room;
}
class NeoChatRoomType : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief Defines the room list categories a room can be assigned.
*/
enum Types {
Invited = 1, /**< The user has been invited to the room. */
Favorite, /**< The room is set as a favourite. */
Direct, /**< The room is a direct chat. */
Normal, /**< The default category for a joined room. */
Deprioritized, /**< The room is set as low priority. */
Space, /**< The room is a space. */
};
Q_ENUM(Types)
};
/**
* @class RoomListModel
*
@@ -51,13 +70,13 @@ public:
LastActiveTimeRole, /**< The timestamp of the last event sent in the room. */
JoinStateRole, /**< The local user's join state in the room. */
CurrentRoomRole, /**< The room object for the room. */
CategoryVisibleRole, /**< If the room's category is visible. */
SubtitleTextRole, /**< The text to show as the room subtitle. */
AvatarImageRole, /**< The room avatar as an image. */
RoomIdRole, /**< The room matrix ID. */
IsSpaceRole, /**< Whether the room is a space. */
IsChildSpaceRole, /**< Whether this space is a child of a different space. */
ReplacementIdRole, /**< The room id of the room replacing this one, if any. */
IsDirectChat, /**< Whether this room is a direct chat. */
};
Q_ENUM(EventRoles)
@@ -96,6 +115,30 @@ public:
*/
Q_INVOKABLE [[nodiscard]] NeoChatRoom *roomAt(int row) const;
/**
* @brief Return a string to represent the given room category.
*/
Q_INVOKABLE [[nodiscard]] static QString categoryName(int category);
/**
* @brief Return a string with the name of the given room category icon.
*/
Q_INVOKABLE [[nodiscard]] static QString categoryIconName(int category);
/**
* @brief Set whether a given category should be visible or not.
*
* @param category the NeoChatRoomType::Types value for the category (it's an
* int due to the pain of Q_INVOKABLES and cpp enums).
* @param visible true if the category should be visible, false if not.
*/
Q_INVOKABLE void setCategoryVisible(int category, bool visible);
/**
* @brief Return whether a room category is set to be visible.
*/
Q_INVOKABLE [[nodiscard]] bool categoryVisible(int category) const;
/**
* @brief Return the model row for the given room.
*/
@@ -121,6 +164,8 @@ private:
Quotient::Connection *m_connection = nullptr;
QList<NeoChatRoom *> m_rooms;
QMap<int, bool> m_categoryVisibility;
int m_notificationCount = 0;
int m_highlightCount = 0;
QString m_activeSpaceId;

View File

@@ -1,323 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "roomtreemodel.h"
#include <Quotient/connection.h>
#include <Quotient/room.h>
#include "eventhandler.h"
#include "neochatconnection.h"
#include "neochatroomtype.h"
#include "spacehierarchycache.h"
using namespace Quotient;
RoomTreeModel::RoomTreeModel(QObject *parent)
: QAbstractItemModel(parent)
{
initializeCategories();
}
void RoomTreeModel::initializeCategories()
{
for (const auto &key : m_rooms.keys()) {
for (const auto &room : m_rooms[key]) {
room->disconnect(this);
}
}
m_rooms.clear();
for (int i = 0; i < 8; i++) {
m_rooms[NeoChatRoomType::Types(i)] = {};
}
}
void RoomTreeModel::setConnection(NeoChatConnection *connection)
{
if (m_connection == connection) {
return;
}
disconnect(m_connection.get(), nullptr, this, nullptr);
m_connection = connection;
beginResetModel();
initializeCategories();
endResetModel();
connect(connection, &Connection::newRoom, this, &RoomTreeModel::newRoom);
connect(connection, &Connection::leftRoom, this, &RoomTreeModel::leftRoom);
for (const auto &room : m_connection->allRooms()) {
newRoom(dynamic_cast<NeoChatRoom *>(room));
}
Q_EMIT connectionChanged();
}
void RoomTreeModel::newRoom(Room *r)
{
const auto room = dynamic_cast<NeoChatRoom *>(r);
const auto type = NeoChatRoomType::typeForRoom(room);
beginInsertRows(index(type, 0), m_rooms[type].size(), m_rooms[type].size());
m_rooms[type].append(room);
connectRoomSignals(room);
endInsertRows();
}
void RoomTreeModel::leftRoom(Room *r)
{
const auto room = dynamic_cast<NeoChatRoom *>(r);
const auto type = NeoChatRoomType::typeForRoom(room);
auto row = m_rooms[type].indexOf(room);
if (row == -1) {
return;
}
beginRemoveRows(index(type, 0), row, row);
m_rooms[type][row]->disconnect(this);
m_rooms[type].removeAt(row);
endRemoveRows();
}
void RoomTreeModel::moveRoom(Quotient::Room *room)
{
// We can't assume the type as it has changed so currently the return of
// NeoChatRoomType::typeForRoom doesn't match it's current location. So find the room.
NeoChatRoomType::Types oldType;
int oldRow = -1;
for (const auto &key : m_rooms.keys()) {
if (m_rooms[key].contains(room)) {
oldType = key;
oldRow = m_rooms[key].indexOf(room);
}
}
if (oldRow == -1) {
return;
}
const auto newType = NeoChatRoomType::typeForRoom(dynamic_cast<NeoChatRoom *>(room));
if (newType == oldType) {
return;
}
const auto oldParent = index(oldType, 0, {});
const auto newParent = index(newType, 0, {});
// HACK: We're doing this as a remove then insert because moving doesn't work
// properly with DelegateChooser for whatever reason.
beginRemoveRows(oldParent, oldRow, oldRow);
m_rooms[oldType].removeAt(oldRow);
endRemoveRows();
beginInsertRows(newParent, m_rooms[newType].size(), m_rooms[newType].size());
m_rooms[newType].append(dynamic_cast<NeoChatRoom *>(room));
endInsertRows();
}
void RoomTreeModel::connectRoomSignals(NeoChatRoom *room)
{
connect(room, &Room::displaynameChanged, this, [this, room] {
refreshRoomRoles(room, {DisplayNameRole});
});
connect(room, &Room::unreadStatsChanged, this, [this, room] {
refreshRoomRoles(room, {NotificationCountRole, HighlightCountRole});
});
connect(room, &Room::avatarChanged, this, [this, room] {
refreshRoomRoles(room, {AvatarRole});
});
connect(room, &Room::tagsChanged, this, [this, room] {
moveRoom(room);
});
connect(room, &Room::joinStateChanged, this, [this, room] {
refreshRoomRoles(room);
});
connect(room, &Room::addedMessages, this, [this, room] {
refreshRoomRoles(room, {SubtitleTextRole, LastActiveTimeRole});
});
connect(room, &Room::pendingEventMerged, this, [this, room] {
refreshRoomRoles(room, {SubtitleTextRole});
});
}
void RoomTreeModel::refreshRoomRoles(NeoChatRoom *room, const QList<int> &roles)
{
const auto roomType = NeoChatRoomType::typeForRoom(room);
const auto it = std::find(m_rooms[roomType].begin(), m_rooms[roomType].end(), room);
if (it == m_rooms[roomType].end()) {
qCritical() << "Room" << room->id() << "not found in the room list";
return;
}
const auto parentIndex = index(roomType, 0, {});
const auto idx = index(it - m_rooms[roomType].begin(), 0, parentIndex);
Q_EMIT dataChanged(idx, idx, roles);
}
NeoChatConnection *RoomTreeModel::connection() const
{
return m_connection;
}
int RoomTreeModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return 1;
}
int RoomTreeModel::rowCount(const QModelIndex &parent) const
{
if (!parent.isValid()) {
return m_rooms.keys().size();
}
if (!parent.parent().isValid()) {
return m_rooms.values()[parent.row()].size();
}
return 0;
}
QModelIndex RoomTreeModel::parent(const QModelIndex &index) const
{
if (!index.internalPointer()) {
return {};
}
return this->index(NeoChatRoomType::typeForRoom(static_cast<NeoChatRoom *>(index.internalPointer())), 0, QModelIndex());
}
QModelIndex RoomTreeModel::index(int row, int column, const QModelIndex &parent) const
{
if (!parent.isValid()) {
return createIndex(row, column, nullptr);
}
if (row >= rowCount(parent)) {
return {};
}
return createIndex(row, column, m_rooms[NeoChatRoomType::Types(parent.row())][row]);
}
QHash<int, QByteArray> RoomTreeModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[DisplayNameRole] = "displayName";
roles[AvatarRole] = "avatar";
roles[CanonicalAliasRole] = "canonicalAlias";
roles[TopicRole] = "topic";
roles[CategoryRole] = "category";
roles[NotificationCountRole] = "notificationCount";
roles[HighlightCountRole] = "highlightCount";
roles[LastActiveTimeRole] = "lastActiveTime";
roles[JoinStateRole] = "joinState";
roles[CurrentRoomRole] = "currentRoom";
roles[SubtitleTextRole] = "subtitleText";
roles[IsSpaceRole] = "isSpace";
roles[RoomIdRole] = "roomId";
roles[IsChildSpaceRole] = "isChildSpace";
roles[IsDirectChat] = "isDirectChat";
roles[DelegateTypeRole] = "delegateType";
roles[IconRole] = "icon";
return roles;
}
// TODO room type changes
QVariant RoomTreeModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return QVariant();
}
if (!index.parent().isValid()) {
if (role == DisplayNameRole) {
return NeoChatRoomType::typeName(index.row());
}
if (role == DelegateTypeRole) {
if (index.row() == NeoChatRoomType::Search) {
return QStringLiteral("search");
}
if (index.row() == NeoChatRoomType::AddDirect) {
return QStringLiteral("addDirect");
}
return QStringLiteral("section");
}
if (role == IconRole) {
return NeoChatRoomType::typeIconName(index.row());
}
if (role == CategoryRole) {
return index.row();
}
return {};
}
const auto room = m_rooms.values()[index.parent().row()][index.row()].get();
Q_ASSERT(room);
if (role == DisplayNameRole) {
return room->displayName();
}
if (role == AvatarRole) {
return room->avatarMediaId();
}
if (role == CanonicalAliasRole) {
return room->canonicalAlias();
}
if (role == TopicRole) {
return room->topic();
}
if (role == CategoryRole) {
return NeoChatRoomType::typeForRoom(room);
}
if (role == NotificationCountRole) {
return room->notificationCount();
}
if (role == HighlightCountRole) {
return room->highlightCount();
}
if (role == LastActiveTimeRole) {
return room->lastActiveTime();
}
if (role == JoinStateRole) {
if (!room->successorId().isEmpty()) {
return QStringLiteral("upgraded");
}
return QVariant::fromValue(room->joinState());
}
if (role == CurrentRoomRole) {
return QVariant::fromValue(room);
}
if (role == SubtitleTextRole) {
if (room->lastEvent() == nullptr || room->lastEventIsSpoiler()) {
return QString();
}
EventHandler eventHandler(room, room->lastEvent());
return eventHandler.subtitleText();
}
if (role == AvatarImageRole) {
return room->avatar(128);
}
if (role == RoomIdRole) {
return room->id();
}
if (role == IsSpaceRole) {
return room->isSpace();
}
if (role == IsChildSpaceRole) {
return SpaceHierarchyCache::instance().isChild(room->id());
}
if (role == ReplacementIdRole) {
return room->successorId();
}
if (role == IsDirectChat) {
return room->isDirectChat();
}
if (role == DelegateTypeRole) {
return QStringLiteral("normal");
}
return {};
}
QModelIndex RoomTreeModel::indexForRoom(NeoChatRoom *room) const
{
if (room == nullptr) {
return {};
}
const auto type = NeoChatRoomType::typeForRoom(room);
auto row = m_rooms[type].indexOf(room);
if (row >= 0) {
return index(row, 0, index(type, 0));
}
return {};
}
#include "moc_roomtreemodel.cpp"

View File

@@ -1,94 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QAbstractItemModel>
#include <QPointer>
#include "enums/neochatroomtype.h"
namespace Quotient
{
class Room;
}
class NeoChatConnection;
class NeoChatRoom;
class RoomTreeModel : public QAbstractItemModel
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
public:
/**
* @brief Defines the model roles.
*/
enum EventRoles {
DisplayNameRole = Qt::DisplayRole, /**< The display name of the room. */
AvatarRole, /**< The source URL for the room's avatar. */
CanonicalAliasRole, /**< The room canonical alias. */
TopicRole, /**< The room topic. */
CategoryRole, /**< The room category, e.g favourite. */
NotificationCountRole, /**< The number of notifications in the room. */
HighlightCountRole, /**< The number of highlighted messages in the room. */
LastActiveTimeRole, /**< The timestamp of the last event sent in the room. */
JoinStateRole, /**< The local user's join state in the room. */
CurrentRoomRole, /**< The room object for the room. */
SubtitleTextRole, /**< The text to show as the room subtitle. */
AvatarImageRole, /**< The room avatar as an image. */
RoomIdRole, /**< The room matrix ID. */
IsSpaceRole, /**< Whether the room is a space. */
IsChildSpaceRole, /**< Whether this space is a child of a different space. */
ReplacementIdRole, /**< The room id of the room replacing this one, if any. */
IsDirectChat, /**< Whether this room is a direct chat. */
DelegateTypeRole,
IconRole,
};
Q_ENUM(EventRoles)
explicit RoomTreeModel(QObject *parent = nullptr);
void setConnection(NeoChatConnection *connection);
NeoChatConnection *connection() const;
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
QModelIndex parent(const QModelIndex &index) const override;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
Q_INVOKABLE QModelIndex indexForRoom(NeoChatRoom *room) const;
Q_SIGNALS:
void connectionChanged();
private:
QPointer<NeoChatConnection> m_connection = nullptr;
QMap<NeoChatRoomType::Types, QList<QPointer<NeoChatRoom>>> m_rooms;
void initializeCategories();
void connectRoomSignals(NeoChatRoom *room);
void newRoom(Quotient::Room *room);
void leftRoom(Quotient::Room *room);
void moveRoom(Quotient::Room *room);
void refreshRoomRoles(NeoChatRoom *room, const QList<int> &roles = {});
};

View File

@@ -3,9 +3,8 @@
#include "searchmodel.h"
#include "enums/delegatetype.h"
#include "eventhandler.h"
#include "models/messagecontentmodel.h"
#include "messageeventmodel.h"
#include "neochatroom.h"
#include <QGuiApplication>
@@ -37,7 +36,7 @@ void SearchModel::setSearchText(const QString &searchText)
void SearchModel::search()
{
Q_ASSERT(m_room);
Q_ASSERT(m_connection);
setSearching(true);
if (m_job) {
m_job->abandon();
@@ -63,7 +62,7 @@ void SearchModel::search()
};
auto job = m_room->connection()->callApi<SearchJob>(SearchJob::Categories{criteria});
auto job = m_connection->callApi<SearchJob>(SearchJob::Categories{criteria});
m_job = job;
connect(job, &BaseJob::finished, this, [this, job] {
beginResetModel();
@@ -75,14 +74,29 @@ void SearchModel::search()
});
}
Connection *SearchModel::connection() const
{
return m_connection;
}
void SearchModel::setConnection(Connection *connection)
{
m_connection = connection;
Q_EMIT connectionChanged();
}
QVariant SearchModel::data(const QModelIndex &index, int role) const
{
auto row = index.row();
const auto &event = *m_result->results[row].result;
EventHandler eventHandler(m_room, &event);
EventHandler eventHandler;
eventHandler.setRoom(m_room);
eventHandler.setEvent(&event);
switch (role) {
case DisplayRole:
return eventHandler.getRichBody();
case ShowAuthorRole:
return true;
case AuthorRole:
@@ -102,8 +116,22 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
return false;
case ShowReadMarkersRole:
return false;
case IsReplyRole:
return eventHandler.hasReply();
case ReplyIdRole:
return eventHandler.hasReply();
case ReplyAuthorRole:
return eventHandler.getReplyAuthor();
case ReplyDelegateTypeRole:
return eventHandler.getReplyDelegateType();
case ReplyDisplayRole:
return eventHandler.getReplyRichBody();
case ReplyMediaInfoRole:
return eventHandler.getReplyMediaInfo();
case IsPendingRole:
return false;
case ShowLinkPreviewRole:
return false;
case HighlightRole:
return eventHandler.isHighlighted();
case EventIdRole:
@@ -112,17 +140,6 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
return eventHandler.isThreaded();
case ThreadRootRole:
return eventHandler.threadRoot();
case ContentModelRole: {
if (!event.isStateEvent()) {
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(&event, m_room));
}
if (event.isStateEvent()) {
if (event.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(&event, m_room));
}
}
return {};
}
}
return DelegateType::Message;
}
@@ -140,6 +157,7 @@ QHash<int, QByteArray> SearchModel::roleNames() const
{
return {
{DelegateTypeRole, "delegateType"},
{DisplayRole, "display"},
{AuthorRole, "author"},
{ShowSectionRole, "showSection"},
{SectionRole, "section"},
@@ -150,15 +168,25 @@ QHash<int, QByteArray> SearchModel::roleNames() const
{ExcessReadMarkersRole, "excessReadMarkers"},
{HighlightRole, "isHighlighted"},
{ReadMarkersString, "readMarkersString"},
{PlainTextRole, "plainText"},
{VerifiedRole, "verified"},
{ProgressInfoRole, "progressInfo"},
{ShowReactionsRole, "showReactions"},
{IsReplyRole, "isReply"},
{ReplyAuthorRole, "replyAuthor"},
{ReplyIdRole, "replyId"},
{ReplyDelegateTypeRole, "replyDelegateType"},
{ReplyDisplayRole, "replyDisplay"},
{ReplyMediaInfoRole, "replyMediaInfo"},
{ReactionRole, "reaction"},
{ReadMarkersRole, "readMarkers"},
{IsPendingRole, "isPending"},
{ShowReadMarkersRole, "showReadMarkers"},
{MimeTypeRole, "mimeType"},
{ShowLinkPreviewRole, "showLinkPreview"},
{LinkPreviewRole, "linkPreview"},
{IsThreadedRole, "isThreaded"},
{ThreadRootRole, "threadRoot"},
{ContentModelRole, "contentModel"},
};
}
@@ -174,6 +202,19 @@ void SearchModel::setRoom(NeoChatRoom *room)
}
m_room = room;
Q_EMIT roomChanged();
connect(m_room, &NeoChatRoom::replyLoaded, this, [this](const auto &eventId, const auto &replyId) {
Q_UNUSED(replyId);
const auto &results = m_result->results;
auto it = std::find_if(results.begin(), results.end(), [eventId](const auto &event) {
return event.result->id() == eventId;
});
if (it == results.end()) {
return;
}
auto row = it - results.begin();
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyDelegateTypeRole, ReplyDisplayRole, ReplyMediaInfoRole, ReplyAuthorRole});
});
}
bool SearchModel::searching() const

View File

@@ -31,6 +31,11 @@ class SearchModel : public QAbstractListModel
*/
Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
/**
* @brief The current connection that the model is using to search for messages.
*/
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
/**
* @brief The current room that the search is being done from.
*/
@@ -51,7 +56,8 @@ public:
* since the same delegates are used.
*/
enum Roles {
DelegateTypeRole = Qt::DisplayRole + 1,
DisplayRole = Qt::DisplayRole,
DelegateTypeRole,
ShowAuthorRole,
AuthorRole,
ShowSectionRole,
@@ -62,15 +68,25 @@ public:
ExcessReadMarkersRole,
HighlightRole,
ReadMarkersString,
PlainTextRole,
VerifiedRole,
ProgressInfoRole,
ShowReactionsRole,
IsReplyRole,
ReplyAuthorRole,
ReplyIdRole,
ReplyDelegateTypeRole,
ReplyDisplayRole,
ReplyMediaInfoRole,
ReactionRole,
ReadMarkersRole,
IsPendingRole,
ShowReadMarkersRole,
MimeTypeRole,
ShowLinkPreviewRole,
LinkPreviewRole,
IsThreadedRole,
ThreadRootRole,
ContentModelRole,
};
Q_ENUM(Roles)
explicit SearchModel(QObject *parent = nullptr);
@@ -78,6 +94,9 @@ public:
QString searchText() const;
void setSearchText(const QString &searchText);
Quotient::Connection *connection() const;
void setConnection(Quotient::Connection *connection);
NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
@@ -111,6 +130,7 @@ public:
Q_SIGNALS:
void searchTextChanged();
void connectionChanged();
void roomChanged();
void searchingChanged();
@@ -121,6 +141,7 @@ private:
void setSearching(bool searching);
QString m_searchText;
Quotient::Connection *m_connection = nullptr;
NeoChatRoom *m_room = nullptr;
Quotient::Omittable<Quotient::SearchJob::ResultRoomEvents> m_result = Quotient::none;
Quotient::SearchJob *m_job = nullptr;

View File

@@ -3,7 +3,9 @@
#include "sortfilterroomlistmodel.h"
#include "neochatconnection.h"
#include "roomlistmodel.h"
#include "spacehierarchycache.h"
SortFilterRoomListModel::SortFilterRoomListModel(QObject *parent)
: QSortFilterProxyModel(parent)
@@ -19,6 +21,53 @@ SortFilterRoomListModel::SortFilterRoomListModel(QObject *parent)
});
}
void SortFilterRoomListModel::setRoomSortOrder(SortFilterRoomListModel::RoomSortOrder sortOrder)
{
m_sortOrder = sortOrder;
Q_EMIT roomSortOrderChanged();
if (sortOrder == SortFilterRoomListModel::Alphabetical) {
setSortRole(RoomListModel::DisplayNameRole);
} else if (sortOrder == SortFilterRoomListModel::LastActivity) {
setSortRole(RoomListModel::LastActiveTimeRole);
}
invalidate();
}
SortFilterRoomListModel::RoomSortOrder SortFilterRoomListModel::roomSortOrder() const
{
return m_sortOrder;
}
bool SortFilterRoomListModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
{
if (m_sortOrder == SortFilterRoomListModel::LastActivity) {
// display favorite rooms always on top
const auto categoryLeft = static_cast<NeoChatRoomType::Types>(sourceModel()->data(source_left, RoomListModel::CategoryRole).toInt());
const auto categoryRight = static_cast<NeoChatRoomType::Types>(sourceModel()->data(source_right, RoomListModel::CategoryRole).toInt());
if (categoryLeft == NeoChatRoomType::Types::Favorite && categoryRight == NeoChatRoomType::Types::Favorite) {
return sourceModel()->data(source_left, RoomListModel::LastActiveTimeRole).toDateTime()
> sourceModel()->data(source_right, RoomListModel::LastActiveTimeRole).toDateTime();
}
if (categoryLeft == NeoChatRoomType::Types::Favorite) {
return true;
} else if (categoryRight == NeoChatRoomType::Types::Favorite) {
return false;
}
return sourceModel()->data(source_left, RoomListModel::LastActiveTimeRole).toDateTime()
> sourceModel()->data(source_right, RoomListModel::LastActiveTimeRole).toDateTime();
}
if (m_sortOrder != SortFilterRoomListModel::Categories) {
return QSortFilterProxyModel::lessThan(source_left, source_right);
}
if (sourceModel()->data(source_left, RoomListModel::CategoryRole) != sourceModel()->data(source_right, RoomListModel::CategoryRole)) {
return sourceModel()->data(source_left, RoomListModel::CategoryRole).toInt() < sourceModel()->data(source_right, RoomListModel::CategoryRole).toInt();
}
return sourceModel()->data(source_left, RoomListModel::LastActiveTimeRole).toDateTime()
> sourceModel()->data(source_right, RoomListModel::LastActiveTimeRole).toDateTime();
}
void SortFilterRoomListModel::setFilterText(const QString &text)
{
m_filterText = text;
@@ -32,15 +81,39 @@ QString SortFilterRoomListModel::filterText() const
bool SortFilterRoomListModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
Q_UNUSED(source_parent);
if (sourceModel()->data(index, RoomListModel::JoinStateRole).toString() == QStringLiteral("upgraded")
&& dynamic_cast<RoomListModel *>(sourceModel())->connection()->room(sourceModel()->data(index, RoomListModel::ReplacementIdRole).toString())) {
if (sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() == QStringLiteral("upgraded")
&& dynamic_cast<RoomListModel *>(sourceModel())
->connection()
->room(sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::ReplacementIdRole).toString())) {
return false;
}
return sourceModel()->data(index, RoomListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
&& sourceModel()->data(index, RoomListModel::IsSpaceRole).toBool() == false;
bool acceptRoom =
sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsSpaceRole).toBool() == false;
if (m_activeSpaceId.isEmpty()) {
return acceptRoom;
} else {
const auto &rooms = SpaceHierarchyCache::instance().getRoomListForSpace(m_activeSpaceId, false);
return std::find(rooms.begin(), rooms.end(), sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::RoomIdRole).toString())
!= rooms.end()
&& acceptRoom;
}
}
QString SortFilterRoomListModel::activeSpaceId() const
{
return m_activeSpaceId;
}
void SortFilterRoomListModel::setActiveSpaceId(const QString &spaceId)
{
m_activeSpaceId = spaceId;
Q_EMIT activeSpaceIdChanged();
invalidate();
}
#include "moc_sortfilterroomlistmodel.cpp"

View File

@@ -30,18 +30,50 @@ class SortFilterRoomListModel : public QSortFilterProxyModel
Q_OBJECT
QML_ELEMENT
/**
* @brief The order by which the rooms will be sorted.
*
* @sa RoomSortOrder
*/
Q_PROPERTY(RoomSortOrder roomSortOrder READ roomSortOrder WRITE setRoomSortOrder NOTIFY roomSortOrderChanged)
/**
* @brief The text to use to filter room names.
*/
Q_PROPERTY(QString filterText READ filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
/**
* @brief Set the ID of the space to show rooms for.
*/
Q_PROPERTY(QString activeSpaceId READ activeSpaceId WRITE setActiveSpaceId NOTIFY activeSpaceIdChanged)
public:
enum RoomSortOrder {
Alphabetical,
LastActivity,
Categories,
};
Q_ENUM(RoomSortOrder)
explicit SortFilterRoomListModel(QObject *parent = nullptr);
void setRoomSortOrder(RoomSortOrder sortOrder);
[[nodiscard]] RoomSortOrder roomSortOrder() const;
void setFilterText(const QString &text);
[[nodiscard]] QString filterText() const;
QString activeSpaceId() const;
void setActiveSpaceId(const QString &spaceId);
protected:
/**
* @brief Returns true if the value of source_left is less than source_right.
*
* @sa QSortFilterProxyModel::lessThan
*/
[[nodiscard]] bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
/**
* @brief Whether a row should be shown out or not.
*
@@ -50,8 +82,12 @@ protected:
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
Q_SIGNALS:
void roomSortOrderChanged();
void filterTextChanged();
void activeSpaceIdChanged();
private:
RoomSortOrder m_sortOrder = Categories;
QString m_filterText;
QString m_activeSpaceId;
};

View File

@@ -1,161 +0,0 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "sortfilterroomtreemodel.h"
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroomtype.h"
#include "roomtreemodel.h"
#include "spacehierarchycache.h"
SortFilterRoomTreeModel::SortFilterRoomTreeModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
setRecursiveFilteringEnabled(true);
sort(0);
invalidateFilter();
connect(this, &SortFilterRoomTreeModel::filterTextChanged, this, &SortFilterRoomTreeModel::invalidateFilter);
connect(this, &SortFilterRoomTreeModel::sourceModelChanged, this, [this]() {
sourceModel()->disconnect(this);
connect(sourceModel(), &QAbstractItemModel::rowsInserted, this, &SortFilterRoomTreeModel::invalidateFilter);
connect(sourceModel(), &QAbstractItemModel::rowsRemoved, this, &SortFilterRoomTreeModel::invalidateFilter);
});
connect(NeoChatConfig::self(), &NeoChatConfig::CollapsedChanged, this, &SortFilterRoomTreeModel::invalidateFilter);
}
void SortFilterRoomTreeModel::setRoomSortOrder(SortFilterRoomTreeModel::RoomSortOrder sortOrder)
{
m_sortOrder = sortOrder;
Q_EMIT roomSortOrderChanged();
if (sortOrder == SortFilterRoomTreeModel::Alphabetical) {
setSortRole(RoomTreeModel::DisplayNameRole);
} else if (sortOrder == SortFilterRoomTreeModel::LastActivity) {
setSortRole(RoomTreeModel::LastActiveTimeRole);
}
invalidate();
}
SortFilterRoomTreeModel::RoomSortOrder SortFilterRoomTreeModel::roomSortOrder() const
{
return m_sortOrder;
}
bool SortFilterRoomTreeModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
{
if (m_sortOrder == SortFilterRoomTreeModel::LastActivity) {
// display favorite rooms always on top
const auto categoryLeft = static_cast<NeoChatRoomType::Types>(sourceModel()->data(source_left, RoomTreeModel::CategoryRole).toInt());
const auto categoryRight = static_cast<NeoChatRoomType::Types>(sourceModel()->data(source_right, RoomTreeModel::CategoryRole).toInt());
if (categoryLeft == NeoChatRoomType::Types::Favorite && categoryRight == NeoChatRoomType::Types::Favorite) {
return sourceModel()->data(source_left, RoomTreeModel::LastActiveTimeRole).toDateTime()
> sourceModel()->data(source_right, RoomTreeModel::LastActiveTimeRole).toDateTime();
}
if (categoryLeft == NeoChatRoomType::Types::Favorite) {
return true;
} else if (categoryRight == NeoChatRoomType::Types::Favorite) {
return false;
}
return sourceModel()->data(source_left, RoomTreeModel::LastActiveTimeRole).toDateTime()
> sourceModel()->data(source_right, RoomTreeModel::LastActiveTimeRole).toDateTime();
}
if (m_sortOrder != SortFilterRoomTreeModel::Categories) {
return QSortFilterProxyModel::lessThan(source_left, source_right);
}
if (sourceModel()->data(source_left, RoomTreeModel::CategoryRole) != sourceModel()->data(source_right, RoomTreeModel::CategoryRole)) {
return sourceModel()->data(source_left, RoomTreeModel::CategoryRole).toInt() < sourceModel()->data(source_right, RoomTreeModel::CategoryRole).toInt();
}
return sourceModel()->data(source_left, RoomTreeModel::LastActiveTimeRole).toDateTime()
> sourceModel()->data(source_right, RoomTreeModel::LastActiveTimeRole).toDateTime();
}
void SortFilterRoomTreeModel::setFilterText(const QString &text)
{
m_filterText = text;
Q_EMIT filterTextChanged();
}
QString SortFilterRoomTreeModel::filterText() const
{
return m_filterText;
}
bool SortFilterRoomTreeModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
if (!source_parent.isValid()) {
if (sourceModel()->data(sourceModel()->index(source_row, 0), RoomTreeModel::CategoryRole).toInt() == NeoChatRoomType::Search
&& NeoChatConfig::collapsed()) {
return true;
}
if (sourceModel()->data(sourceModel()->index(source_row, 0), RoomTreeModel::CategoryRole).toInt() == NeoChatRoomType::AddDirect
&& m_mode == DirectChats) {
return true;
}
return false;
}
QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
bool acceptRoom = sourceModel()->data(index, RoomTreeModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
&& sourceModel()->data(index, RoomTreeModel::IsSpaceRole).toBool() == false;
bool isDirectChat = sourceModel()->data(index, RoomTreeModel::IsDirectChat).toBool();
// In `show direct chats` mode we only care about whether or not it's a direct chat or if the filter string matches.'
if (m_mode == DirectChats) {
return isDirectChat && acceptRoom;
}
// When not in `show direct chats` mode, filter them out.
if (isDirectChat && m_mode == Rooms) {
return false;
}
if (sourceModel()->data(index, RoomTreeModel::JoinStateRole).toString() == QStringLiteral("upgraded")
&& dynamic_cast<RoomTreeModel *>(sourceModel())->connection()->room(sourceModel()->data(index, RoomTreeModel::ReplacementIdRole).toString())) {
return false;
}
if (m_activeSpaceId.isEmpty()) {
if (!SpaceHierarchyCache::instance().isChild(sourceModel()->data(index, RoomTreeModel::RoomIdRole).toString())) {
return acceptRoom;
}
return false;
} else {
const auto &rooms = SpaceHierarchyCache::instance().getRoomListForSpace(m_activeSpaceId, false);
return std::find(rooms.begin(), rooms.end(), sourceModel()->data(index, RoomTreeModel::RoomIdRole).toString()) != rooms.end() && acceptRoom;
}
}
QString SortFilterRoomTreeModel::activeSpaceId() const
{
return m_activeSpaceId;
}
void SortFilterRoomTreeModel::setActiveSpaceId(const QString &spaceId)
{
m_activeSpaceId = spaceId;
Q_EMIT activeSpaceIdChanged();
invalidate();
}
SortFilterRoomTreeModel::Mode SortFilterRoomTreeModel::mode() const
{
return m_mode;
}
void SortFilterRoomTreeModel::setMode(SortFilterRoomTreeModel::Mode mode)
{
if (m_mode == mode) {
return;
}
m_mode = mode;
Q_EMIT modeChanged();
invalidate();
}
#include "moc_sortfilterroomtreemodel.cpp"

View File

@@ -1,111 +0,0 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QQmlEngine>
#include <QSortFilterProxyModel>
/**
* @class SortFilterRoomTreeModel
*
* This model sorts and filters the room list.
*
* There are numerous room sort orders available:
* - Categories - sort rooms by their NeoChatRoomType and then by last activty within
* each category.
* - LastActivity - sort rooms by the last active time in the room.
* - Alphabetical - sort the rooms alphabetically by room name.
*
* The model can be given a filter string that will only show rooms who's name includes
* the text.
*
* The model can also be given an active space ID and will only show rooms within
* that space.
*
* All space rooms and upgraded rooms will also be filtered out.
*/
class SortFilterRoomTreeModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The order by which the rooms will be sorted.
*
* @sa RoomSortOrder
*/
Q_PROPERTY(RoomSortOrder roomSortOrder READ roomSortOrder WRITE setRoomSortOrder NOTIFY roomSortOrderChanged)
/**
* @brief The text to use to filter room names.
*/
Q_PROPERTY(QString filterText READ filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
/**
* @brief Set the ID of the space to show rooms for.
*/
Q_PROPERTY(QString activeSpaceId READ activeSpaceId WRITE setActiveSpaceId NOTIFY activeSpaceIdChanged)
/**
* @brief Whether only direct chats should be shown.
*/
Q_PROPERTY(Mode mode READ mode WRITE setMode NOTIFY modeChanged)
public:
enum RoomSortOrder {
Alphabetical,
LastActivity,
Categories,
};
Q_ENUM(RoomSortOrder)
enum Mode {
Rooms,
DirectChats,
All,
};
Q_ENUM(Mode)
explicit SortFilterRoomTreeModel(QObject *parent = nullptr);
void setRoomSortOrder(RoomSortOrder sortOrder);
[[nodiscard]] RoomSortOrder roomSortOrder() const;
void setFilterText(const QString &text);
[[nodiscard]] QString filterText() const;
QString activeSpaceId() const;
void setActiveSpaceId(const QString &spaceId);
Mode mode() const;
void setMode(Mode mode);
protected:
/**
* @brief Returns true if the value of source_left is less than source_right.
*
* @sa QSortFilterProxyModel::lessThan
*/
[[nodiscard]] bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
/**
* @brief Whether a row should be shown out or not.
*
* @sa QSortFilterProxyModel::filterAcceptsRow
*/
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
Q_SIGNALS:
void roomSortOrderChanged();
void filterTextChanged();
void activeSpaceIdChanged();
void modeChanged();
private:
RoomSortOrder m_sortOrder = Categories;
Mode m_mode = All;
QString m_filterText;
QString m_activeSpaceId;
};

View File

@@ -244,20 +244,6 @@ QVariant SpaceChildrenModel::data(const QModelIndex &index, int role) const
}
return QVariant::fromValue(nullptr);
}
if (role == OrderRole) {
if (child->parentItem() == nullptr) {
return QString();
}
const auto childState = child->parentItem()->childStateContent(child);
return childState[QLatin1String("order")].toString();
}
if (role == ChildTimestampRole) {
if (child->parentItem() == nullptr) {
return QString();
}
const auto childState = child->parentItem()->childState(child);
return childState[QLatin1String("origin_server_ts")].toString();
}
return {};
}
@@ -339,8 +325,6 @@ QHash<int, QByteArray> SpaceChildrenModel::roleNames() const
roles[IsDeclaredParentRole] = "isDeclaredParent";
roles[CanRemove] = "canRemove";
roles[ParentRoomRole] = "parentRoom";
roles[OrderRole] = "order";
roles[ChildTimestampRole] = "childTimestamp";
return roles;
}

View File

@@ -51,8 +51,6 @@ public:
IsDeclaredParentRole,
CanRemove,
ParentRoomRole,
OrderRole,
ChildTimestampRole,
};
explicit SpaceChildrenModel(QObject *parent = nullptr);

View File

@@ -26,27 +26,10 @@ QString SpaceChildSortFilterModel::filterText() const
bool SpaceChildSortFilterModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
{
if (source_left.data(SpaceChildrenModel::IsSpaceRole).toBool() && source_right.data(SpaceChildrenModel::IsSpaceRole).toBool()) {
if (!source_left.data(SpaceChildrenModel::OrderRole).toString().isEmpty() && !source_right.data(SpaceChildrenModel::OrderRole).toString().isEmpty()) {
return QString::compare(source_left.data(SpaceChildrenModel::OrderRole).toString(), source_right.data(SpaceChildrenModel::OrderRole).toString())
< 0;
}
return source_left.data(SpaceChildrenModel::ChildTimestampRole).toDateTime() > source_right.data(SpaceChildrenModel::ChildTimestampRole).toDateTime();
}
if (source_left.data(SpaceChildrenModel::IsSpaceRole).toBool()) {
return true;
} else if (source_right.data(SpaceChildrenModel::IsSpaceRole).toBool()) {
if (!source_left.data(SpaceChildrenModel::IsSpaceRole).toBool() && source_right.data(SpaceChildrenModel::IsSpaceRole).toBool()) {
return false;
}
if (!source_left.data(SpaceChildrenModel::OrderRole).toString().isEmpty() && !source_right.data(SpaceChildrenModel::OrderRole).toString().isEmpty()) {
return QString::compare(source_left.data(SpaceChildrenModel::OrderRole).toString(), source_right.data(SpaceChildrenModel::OrderRole).toString()) < 0;
}
if (!source_left.data(SpaceChildrenModel::OrderRole).toString().isEmpty()) {
return true;
} else if (!source_right.data(SpaceChildrenModel::OrderRole).toString().isEmpty()) {
return false;
}
return source_left.data(SpaceChildrenModel::ChildTimestampRole).toDateTime() > source_right.data(SpaceChildrenModel::ChildTimestampRole).toDateTime();
return true;
}
bool SpaceChildSortFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const

View File

@@ -141,22 +141,6 @@ bool SpaceTreeItem::isSpace() const
return m_isSpace;
}
QJsonObject SpaceTreeItem::childState(const SpaceTreeItem *child) const
{
if (child == nullptr) {
return {};
}
if (child->parentItem() != this) {
return {};
}
for (const auto &childState : m_childStates) {
if (childState->stateKey() == child->id()) {
return childState->fullJson();
}
}
return {};
}
QJsonObject SpaceTreeItem::childStateContent(const SpaceTreeItem *child) const
{
if (child == nullptr) {

View File

@@ -125,11 +125,6 @@ public:
*/
bool isSpace() const;
/**
* @brief Return the m.space.child stripped state Json for the given child.
*/
QJsonObject childState(const SpaceTreeItem *child) const;
/**
* @brief Return the m.space.child state event content for the given child.
*/

View File

@@ -26,6 +26,7 @@ void UserDirectoryListModel::setConnection(Connection *conn)
beginResetModel();
m_limited = false;
attempted = false;
users.clear();
@@ -36,50 +37,53 @@ void UserDirectoryListModel::setConnection(Connection *conn)
endResetModel();
m_connection = conn;
if (job) {
job->abandon();
job = nullptr;
}
Q_EMIT connectionChanged();
if (m_job) {
m_job->abandon();
m_job = nullptr;
Q_EMIT searchingChanged();
}
Q_EMIT limitedChanged();
}
QString UserDirectoryListModel::searchText() const
QString UserDirectoryListModel::keyword() const
{
return m_searchText;
return m_keyword;
}
void UserDirectoryListModel::setSearchText(const QString &value)
void UserDirectoryListModel::setKeyword(const QString &value)
{
if (m_searchText == value) {
if (m_keyword == value) {
return;
}
m_searchText = value;
Q_EMIT searchTextChanged();
if (m_job) {
m_job->abandon();
m_job = nullptr;
Q_EMIT searchingChanged();
}
m_keyword = value;
m_limited = false;
attempted = false;
if (job) {
job->abandon();
job = nullptr;
}
Q_EMIT keywordChanged();
Q_EMIT limitedChanged();
}
bool UserDirectoryListModel::searching() const
bool UserDirectoryListModel::limited() const
{
return m_job != nullptr;
return m_limited;
}
void UserDirectoryListModel::search(int limit)
void UserDirectoryListModel::search(int count)
{
if (limit < 1) {
if (count < 1) {
return;
}
if (m_job) {
if (job) {
qDebug() << "UserDirectoryListModel: Other jobs running, ignore";
return;
@@ -89,22 +93,25 @@ void UserDirectoryListModel::search(int limit)
return;
}
m_job = m_connection->callApi<SearchUserDirectoryJob>(m_searchText, limit);
Q_EMIT searchingChanged();
job = m_connection->callApi<SearchUserDirectoryJob>(m_keyword, count);
connect(m_job, &BaseJob::finished, this, [this] {
connect(job, &BaseJob::finished, this, [this] {
attempted = true;
if (m_job->status() == BaseJob::Success) {
auto users = m_job->results();
if (job->status() == BaseJob::Success) {
auto users = job->results();
this->beginResetModel();
this->users = users;
this->m_limited = job->limited();
this->endResetModel();
}
this->m_job = nullptr;
Q_EMIT searchingChanged();
this->job = nullptr;
Q_EMIT limitedChanged();
});
}
@@ -120,7 +127,7 @@ QVariant UserDirectoryListModel::data(const QModelIndex &index, int role) const
return {};
}
auto user = users.at(index.row());
if (role == DisplayNameRole) {
if (role == NameRole) {
auto displayName = user.displayName;
if (!displayName.isEmpty()) {
return displayName;
@@ -135,17 +142,18 @@ QVariant UserDirectoryListModel::data(const QModelIndex &index, int role) const
}
if (role == AvatarRole) {
auto avatarUrl = user.avatarUrl;
if (avatarUrl.isEmpty() || !m_connection) {
return QUrl();
if (avatarUrl.isEmpty()) {
return QString();
}
return m_connection->makeMediaUrl(avatarUrl);
return avatarUrl.url().remove(0, 6);
}
if (role == UserIDRole) {
return user.userId;
}
if (role == DirectChatExistsRole) {
if (role == DirectChatsRole) {
if (!m_connection) {
return false;
return QStringList();
};
auto userObj = m_connection->user(user.userId);
@@ -154,11 +162,11 @@ QVariant UserDirectoryListModel::data(const QModelIndex &index, int role) const
if (userObj && directChats.contains(userObj)) {
auto directChatsForUser = directChats.values(userObj);
if (!directChatsForUser.isEmpty()) {
return true;
return QVariant::fromValue(directChatsForUser);
}
}
return false;
return QStringList();
}
return {};
@@ -168,10 +176,10 @@ QHash<int, QByteArray> UserDirectoryListModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[DisplayNameRole] = "displayName";
roles[AvatarRole] = "avatarUrl";
roles[UserIDRole] = "userId";
roles[DirectChatExistsRole] = "directChatExists";
roles[NameRole] = "name";
roles[AvatarRole] = "avatar";
roles[UserIDRole] = "userID";
roles[DirectChatsRole] = "directChats";
return roles;
}

View File

@@ -35,24 +35,24 @@ class UserDirectoryListModel : public QAbstractListModel
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
/**
* @brief The text to search the public room list for.
* @brief The keyword to use in the search.
*/
Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
Q_PROPERTY(QString keyword READ keyword WRITE setKeyword NOTIFY keywordChanged)
/**
* @brief Whether the model is searching.
* @brief Whether the current results have been truncated.
*/
Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged)
Q_PROPERTY(bool limited READ limited NOTIFY limitedChanged)
public:
/**
* @brief Defines the model roles.
*/
enum EventRoles {
DisplayNameRole = Qt::DisplayRole, /**< The user's display name. */
NameRole = Qt::DisplayRole + 1, /**< The user's display name. */
AvatarRole, /**< The source URL for the user's avatar. */
UserIDRole, /**< Matrix ID of the user. */
DirectChatExistsRole, /**< Whether there is already a direct chat with the user. */
DirectChatsRole, /**< A list of direct chat matrix IDs with the user. */
};
explicit UserDirectoryListModel(QObject *parent = nullptr);
@@ -60,17 +60,17 @@ public:
[[nodiscard]] Quotient::Connection *connection() const;
void setConnection(Quotient::Connection *conn);
[[nodiscard]] QString searchText() const;
void setSearchText(const QString &searchText);
[[nodiscard]] QString keyword() const;
void setKeyword(const QString &value);
[[nodiscard]] bool searching() const;
[[nodiscard]] bool limited() const;
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
[[nodiscard]] QVariant data(const QModelIndex &index, int role = NameRole) const override;
/**
* @brief Number of rows in the model.
@@ -87,23 +87,23 @@ public:
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Search the user directory.
*
* @param limit the maximum number of rooms to load.
* @brief Start the user search.
*/
Q_INVOKABLE void search(int limit = 50);
Q_INVOKABLE void search(int count = 50);
Q_SIGNALS:
void connectionChanged();
void searchTextChanged();
void searchingChanged();
void keywordChanged();
void limitedChanged();
private:
Quotient::Connection *m_connection = nullptr;
QString m_searchText;
QString m_keyword;
bool m_limited = false;
bool attempted = false;
QList<Quotient::SearchUserDirectoryJob::User> users;
Quotient::SearchUserDirectoryJob *m_job = nullptr;
Quotient::SearchUserDirectoryJob *job = nullptr;
};

View File

@@ -76,6 +76,7 @@ Comment[ru]=Клиент для Matrix — децентрализованног
Comment[sk]=Klient pre matrix, decentralizovaný komunikačný protokol
Comment[sl]=Odjemalec za decentralizirani komunikacijski protokol matrix
Comment[sv]=En klient för matrix, det decentraliserade kommunikationsprotokollet
Comment[ta]=மையமில்லா தகவல் பரிமாற்ற நெறிமுறையான மேட்ரிக்ஸுக்கான செயலி
Comment[tr]=Merkezi olmayan iletişim protokolü Matrix için bir istemci
Comment[uk]=Клієнт matrix, децентралізованого протоколу обміну даними
Comment[x-test]=xxA client for matrix, the decentralized communication protocolxx
@@ -188,7 +189,6 @@ Name[ie]=Nov invitation
Name[it]=Nuovo invito
Name[ka]=ახალი მოსაწვევი
Name[ko]=새 초대장
Name[lt]=Naujas pakvietimas
Name[nl]=Nieuwe uitnodiging
Name[nn]=Ny invitasjon
Name[pa]=ਨਵਾਂ ਸੱਦਾ
@@ -226,7 +226,6 @@ Comment[ie]=Vu have un nov invitation a un chambre
Comment[it]=È presente un nuovo invito a una stanza
Comment[ka]=გაქვთ ახალი ოთახის მოსაწვევი
Comment[ko]=새로운 대화방 초대장을 받음
Comment[lt]=Yra naujas pakvietimas į kambarį
Comment[nl]=Er is een nieuwe uitnodiging naar een room
Comment[nn]=Du har ein ny invitasjon til eit rom
Comment[pa]=ਰੂਮ ਲਈ ਨਵਾਂ ਸੱਦਾ ਹੈ

View File

@@ -22,6 +22,10 @@
<label>Background transparency value</label>
<default>0.3</default>
</entry>
<entry name="MergeRoomList" type="bool">
<label>Merge Room Lists</label>
<default>false</default>
</entry>
<entry name="AllowQuickEdit" type="bool">
<label>Use s/text/replacement syntax to edit your last message.</label>
<default>false</default>
@@ -152,5 +156,11 @@
<default></default>
</entry>
</group>
<group name="FeatureFlags">
<entry name="Threads" type="bool">
<label>Enable threads</label>
<default>false</default>
</entry>
</group>
</kcfg>

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