Compare commits

..

101 Commits

Author SHA1 Message Date
Tobias Fella
7c74a6cbe1 Improve space management strings 2024-03-29 11:56:06 +01:00
Tobias Fella
e6a11b2ad8 Make various models more robust against deleted rooms 2024-03-29 11:55:14 +01:00
James Graham
158942d1b5 UserInfo compact
Make UserInfo work in compact mode. This includes showing the account switch popup in a dialog

BUG: 482261
2024-03-29 09:09:13 +00:00
l10n daemon script
aaa97ec029 GIT_SILENT Sync po/docbooks with svn 2024-03-29 01:30:58 +00:00
Tobias Fella
882ead5715 Remove external room window feature
At its best, this worked ok-ish, though it was always missing basic features.

It's also a massive memory leak and significantly complicates the codebase.
(Which is not yet cleaned up by this commit)

Currently, it is entirely broken and noone noticed or cared enough to report or fix that.

BUG: 455984
2024-03-28 22:05:50 +01:00
Tobias Fella
ab4519dedd Show custom delegate for in-room user verification
This is independent of the in-room verification actually working, but prevents a fallback from appearing
2024-03-28 22:03:58 +01:00
Tobias Fella
c3fd2428a2 Add QR code scanner 2024-03-28 22:02:55 +01:00
James Graham
fbb4b962fa Support selected text for replies in the right click menu
Support selected text for replies in the right click menu

BUG: 463885
2024-03-28 21:01:21 +00:00
Tobias Fella
9bf65de649 Use custom room drawer icons 2024-03-28 21:34:47 +01:00
Tobias Fella
75f069cb7d SpaceChildrenModel: Handle space being deleted 2024-03-28 16:46:38 +01:00
l10n daemon script
87d50125ab GIT_SILENT Sync po/docbooks with svn 2024-03-28 01:38:17 +00:00
l10n daemon script
dc2cf21cb8 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-28 01:18:33 +00:00
Tobias Fella
bae14ecd35 Simplify spacedrawer width code 2024-03-27 21:09:27 +01:00
Gary Wang
b48c1c3b80 Allow show sender detail from message context menu
When curtain user is spamming a lot of messages in a short amount
of time, mod need to scroll all the way up to the first spam message to
know who send those spam message, thus start banning them.

This patch add a context menu to open the sender detail dialog, it could
make banning the spam user and batch-deleting spam messages easier.
2024-03-27 19:39:29 +00:00
James Graham
0f9eb4beeb Fix logout current connection crash
Make sure that the neochat can handle switching connection when the current one is logged out. This is mostly about using QPointer to handle use after free issues due to room objects being deleted.
2024-03-27 15:25:24 +00:00
l10n daemon script
0ab8624d79 GIT_SILENT Sync po/docbooks with svn 2024-03-27 09:54:21 +00:00
l10n daemon script
e872c934c3 GIT_SILENT Sync po/docbooks with svn 2024-03-27 01:31:49 +00:00
James Graham
c3d5d18aae Use unique pointers for space child items 2024-03-26 13:41:03 +00:00
James Graham
ff5853a850 Create a QML module for settings 2024-03-26 13:23:43 +00:00
l10n daemon script
f772906324 GIT_SILENT Sync po/docbooks with svn 2024-03-26 01:31:34 +00:00
l10n daemon script
07eabb2dc1 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-26 01:13:52 +00:00
l10n daemon script
b3c88763a4 GIT_SILENT Sync po/docbooks with svn 2024-03-25 01:31:49 +00:00
Tobias Fella
f7081f8829 Always encrypt DMs when creating them 2024-03-24 11:36:21 +01:00
James Graham
ceef2167fd Don't Maximize Stickers
Make sure that sticker don't open the maximize component as they aren't in the media model

BUG: 482701
2024-03-24 10:01:41 +00:00
James Graham
1dcfd94328 Fix Message Components for Tags with Attributes
Don't assume that the close tag is the length of the start tag +1

BUG: 482331
2024-03-24 10:01:00 +00:00
l10n daemon script
a1aa0804e2 GIT_SILENT Sync po/docbooks with svn 2024-03-24 01:20:31 +00:00
l10n daemon script
77176478eb 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-24 01:14:27 +00:00
l10n daemon script
bf4ebfa7a8 GIT_SILENT made messages (after extraction) 2024-03-24 00:38:05 +00:00
Tobias Fella
6e7d622b41 Fix crash when visiting user
We're adding the "join" action so that rooms are joined.
libQuotient doesn't like it when we the action is join and the uri is for a user.

BUG: 483744
2024-03-23 20:46:56 +01:00
Tobias Fella
8398b7d24d Fix manual user search dialog 2024-03-23 20:45:15 +01:00
James Graham
aef9b7375a Fix Opening Maximized Media
Make sure the Image and Video Components can correctly get the index for opening the Maximize Media component.
2024-03-23 17:52:29 +00:00
James Graham
ba45318b56 Improved itinerary delegates
Steal the look of itinerary items from itinerary but simplified. Also includes new support for flights and restaurants

![image](/uploads/a574d2362edad52ecf91ce89a1849f27/image.png)
2024-03-23 09:33:51 +00:00
l10n daemon script
7d4f8780ad GIT_SILENT Sync po/docbooks with svn 2024-03-23 01:19:12 +00:00
l10n daemon script
b504c990f8 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-23 01:13:26 +00:00
Tobias Fella
e07b876677 Add UI for entering key backup passphrase 2024-03-22 22:04:07 +01:00
Tobias Fella
a0bfd34951 Fix removing a parent space when we're not joined 2024-03-21 21:25:26 +01:00
l10n daemon script
b173714bbe GIT_SILENT Sync po/docbooks with svn 2024-03-21 01:20:56 +00:00
l10n daemon script
db4021b601 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-21 01:14:53 +00:00
l10n daemon script
2f46fd1d2c GIT_SILENT Sync po/docbooks with svn 2024-03-20 01:22:05 +00:00
Tobias Fella
1f85f848e2 Clean up button 2024-03-19 21:31:11 +01:00
Tobias Fella
d7e0954e86 Fix opening manual room dialog 2024-03-19 21:27:18 +01:00
James Graham
1671e05d12 More file previews
This adds previews for downloaded pdfs and code files.

![image](/uploads/9c199e91a1b4ea296c9b82a76e11038b/image.png)

![image](/uploads/17ea3869469417ee78e650ce750dbeb7/image.png)
2024-03-19 20:06:32 +00:00
Tobias Fella
33c55d1563 Add back errouneously removed import 2024-03-19 21:00:48 +01:00
Tobias Fella
4984181613 Refactor and improve emoji detection in reactions
UCHAR_EMOJI_PRESENTATION is replaced with UCHAR_EMOJI, which seems to be more useful for decting emojis.
The lambda is also turned into a proper function as there's no apparent reason for it to be a lambda.
2024-03-19 20:51:50 +01:00
Tobias Fella
af75136269 Bump compiler settings level to 6.0 2024-03-19 20:05:23 +01:00
l10n daemon script
c8eb75a148 GIT_SILENT Sync po/docbooks with svn 2024-03-19 01:22:10 +00:00
Joshua Goins
5109b4fcd1 Fix the quick format bar not actually doing anything 2024-03-18 20:17:06 +00:00
Joshua Goins
1b7f482d0b 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.)
2024-03-18 15:46:22 -04:00
James Graham
6f9a273d39 Timeline Module
Move all the timeline QML files into their own QML module. Having them all in the same location is annoying and hard to work with.
2024-03-18 18:39:59 +00:00
l10n daemon script
51d354a9c8 GIT_SILENT Sync po/docbooks with svn 2024-03-18 01:31:43 +00:00
l10n daemon script
40b2b9554b 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-18 01:13:51 +00:00
l10n daemon script
ba1aca84ff GIT_SILENT Sync po/docbooks with svn 2024-03-17 01:30:01 +00:00
James Graham
17688a49d5 Remove stray log 2024-03-16 13:33:37 +00:00
James Graham
5ff199cc3e Itinerary Component
Move the itinerary model representation to it's own component and instantiate from MessageComponentModel. This starts to lay some groundwork for previewing other files.
2024-03-16 09:28:30 +00:00
l10n daemon script
81a79105d7 GIT_SILENT Sync po/docbooks with svn 2024-03-16 01:34:25 +00:00
l10n daemon script
e39760ccfb 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-16 01:15:17 +00:00
Heiko Becker
1c43da2532 GIT_SILENT Update Appstream for new release
(cherry picked from commit f731877519)
2024-03-15 22:12:56 +01:00
Joshua Goins
2846def00f Fix typo in MessageEditComponent 2024-03-15 15:12:46 -04:00
Joshua Goins
e2eb6ab33c 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.
2024-03-15 14:54:06 -04:00
Joshua Goins
35b08d085c 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.
2024-03-15 18:41:04 +00:00
Joshua Goins
064b0581a7 Make the tabs in developer tools full-width
It looks slightly better, and since there's only four tabs it increases
the tappable area for them.
2024-03-15 18:39:21 +00:00
l10n daemon script
0cc38aa69a GIT_SILENT Sync po/docbooks with svn 2024-03-15 01:31:04 +00:00
l10n daemon script
8312483659 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-15 01:13:30 +00:00
l10n daemon script
0ceb0b4421 GIT_SILENT Sync po/docbooks with svn 2024-03-14 01:32:04 +00:00
l10n daemon script
cc373365fb 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-14 01:14:11 +00:00
l10n daemon script
75d9b6e2a1 GIT_SILENT Sync po/docbooks with svn 2024-03-13 01:20:21 +00:00
l10n daemon script
78fa38ba68 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-13 01:15:08 +00:00
l10n daemon script
0b28712a34 GIT_SILENT made messages (after extraction) 2024-03-13 00:38:07 +00:00
Tobias Fella
48937c8d9a Make sure that timeline is scrolled to end when switching room 2024-03-12 22:45:29 +01:00
l10n daemon script
8c966a5e1a GIT_SILENT Sync po/docbooks with svn 2024-03-12 01:18:54 +00:00
l10n daemon script
f49dd371b7 GIT_SILENT made messages (after extraction) 2024-03-12 00:37:52 +00:00
Tobias Fella
6947fbc12a Add purpose plugin
Implements #182
2024-03-11 20:07:00 +01:00
Nicolas Fella
550dc43dc0 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
2024-03-11 11:31:49 +01:00
l10n daemon script
23c9a4fea7 GIT_SILENT Sync po/docbooks with svn 2024-03-11 01:18:43 +00:00
Carl Schwan
7d26f3351f Fix crash in RoomTreeModel 2024-03-10 19:15:05 +01:00
Tobias Fella
b546554fef Require frameworks 6.0 2024-03-10 14:30:49 +01:00
James Graham
cc058a7cd3 Re-order spaces by dragging and dropping
Title
2024-03-10 11:18:28 +00:00
James Graham
7654b83339 Move the devtools button to UserInfo
Makes more sense now that room is selectable anyway and allows access from space home pages
2024-03-10 10:55:05 +00:00
James Graham
93426546ad Make sure that the MessageSourceSheet component is created properly in devtools 2024-03-10 10:25:53 +00:00
l10n daemon script
23bc38ca6c GIT_SILENT Sync po/docbooks with svn 2024-03-10 01:30:55 +00:00
Tobias Fella
5ccce364d3 Allow opening the settings from the welcome page
This is required to configure a proxy before logging in
2024-03-09 11:49:18 +01:00
l10n daemon script
b488b55a71 GIT_SILENT Sync po/docbooks with svn 2024-03-09 01:23:06 +00:00
l10n daemon script
0bace17074 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 01:14:29 +00:00
Tobias Fella
ad6c7dbd1f Don't link KDBusAddons on windows 2024-03-08 22:01:55 +01:00
James Graham
2a6e63595e Fix space tree refresh
Stop space hierarchy being duplicated. This is done by making sure old jobs are cleared at all resets and to make doubly sure when a child is inserted it overrides itself so there can never be duplicates
2024-03-08 18:36:43 +00:00
l10n daemon script
4d62ad1938 GIT_SILENT Sync po/docbooks with svn 2024-03-08 01:18:17 +00:00
l10n daemon script
e7c3a24011 GIT_SILENT Sync po/docbooks with svn 2024-03-07 01:17:40 +00:00
l10n daemon script
37468607fe GIT_SILENT Sync po/docbooks with svn 2024-03-06 01:18:26 +00:00
Albert Astals Cid
5b007129e3 flatpak: Switch to non-preview runtime 2024-03-06 00:33:31 +01:00
Tobias Fella
93ceb4d49c Improve hover link indicator accessibility
It's a bit pointless, since hovering it requires a mouse... but it's better than the previous behavior
2024-03-05 19:14:31 +01:00
Tobias Fella
85b806fcba Fix appstream 2024-03-05 18:27:39 +01:00
l10n daemon script
20596aabb8 GIT_SILENT Sync po/docbooks with svn 2024-03-05 01:18:31 +00:00
l10n daemon script
825108c59e GIT_SILENT made messages (after extraction) 2024-03-05 00:37:50 +00:00
James Graham
09c31b20e6 stripBlockTags Fixes
Make the code more robust by accounting for things like tag attributes
2024-03-04 21:05:11 +00:00
James Graham
78271a3738 Add highlight and copy button to code component 2024-03-04 20:09:22 +00:00
James Graham
e029aaadfc No Code String Convert
No need to try and convert code strings anymore this is now handled in KSyntaxHighlighter

see frameworks/syntax-highlighting!603 and frameworks/syntax-highlighting!604
2024-03-04 18:05:25 +00:00
l10n daemon script
f6efa35ed2 GIT_SILENT Sync po/docbooks with svn 2024-03-04 01:18:21 +00:00
l10n daemon script
728bad00b4 GIT_SILENT made messages (after extraction) 2024-03-04 00:37:35 +00:00
James Graham
97f3013f7a Visualise readacted messages
Make sure that a text delegate is added for redacted messages so that a message can be shown when show deletions is on
2024-03-03 16:33:21 +00:00
Tobias Fella
269a832ac9 Fix binding loop in NotificationsView 2024-03-03 16:56:18 +01:00
Tobias Fella
3b5b7af531 Fix QML warning 2024-03-03 16:43:52 +01:00
264 changed files with 85531 additions and 70557 deletions

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

@@ -49,3 +49,7 @@ License: CC0-1.0
Files: appiumtests/data/*
Copyright: 2023 Tobias Fella <tobias.fella@kde.org>
License: CC0-1.0
Files: src/purpose/purposeplugin.json
Copyright: 2023 Tobias Fella <tobias.fella@kde.org>
License: BSD-2-Clause

View File

@@ -14,7 +14,7 @@ set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
set(KF_MIN_VERSION "5.240.0")
set(KF_MIN_VERSION "6.0")
set(QT_MIN_VERSION "6.5")
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
@@ -24,7 +24,7 @@ set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(KDE_COMPILERSETTINGS_LEVEL 5.105)
set(KDE_COMPILERSETTINGS_LEVEL 6.0)
include(FeatureSummary)
include(ECMSetupVersion)
@@ -72,6 +72,10 @@ set_package_properties(KF6Kirigami PROPERTIES
)
find_package(KF6KirigamiAddons 0.7.2 REQUIRED)
if (UNIX AND NOT APPLE AND NOT ANDROID AND NOT NEOCHAT_FLATPAK AND NOT NEOCHAT_APPIMAGE)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Purpose)
endif ()
if(ANDROID)
find_package(OpenSSL)
set_package_properties(OpenSSL PROPERTIES
@@ -81,6 +85,7 @@ if(ANDROID)
else()
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem StatusNotifierItem)
find_package(KF6SyntaxHighlighting ${KF_MIN_VERSION} REQUIRED)
set_package_properties(KF6QQC2DesktopStyle PROPERTIES
TYPE RUNTIME
)

View File

@@ -23,11 +23,6 @@ ecm_add_test(
TEST_NAME delegatesizehelpertest
)
ecm_add_test(
roomtreemodeltest.cpp
LINK_LIBRARIES neochat Qt::Test
)
ecm_add_test(
mediasizehelpertest.cpp
LINK_LIBRARIES neochat Qt::Test

View File

@@ -55,6 +55,7 @@ private Q_SLOTS:
void genericBody_data();
void genericBody();
void nullGenericBody();
void markdownBody();
void subtitle();
void nullSubtitle();
void mediaInfo();
@@ -293,6 +294,13 @@ void EventHandlerTest::nullGenericBody()
QCOMPARE(noEventHandler.getGenericBody(), QString());
}
void EventHandlerTest::markdownBody()
{
EventHandler eventHandler(room, 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());

View File

@@ -5,7 +5,9 @@
#include <QSignalSpy>
#include <QTest>
#include "neochatconnection.h"
#include <Quotient/connection.h>
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include "testutils.h"
@@ -25,8 +27,7 @@ private Q_SLOTS:
void NeoChatRoomTest::initTestCase()
{
auto connection = new NeoChatConnection;
Connection::makeMockConnection(connection, QStringLiteral("@bob:kde.org"));
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), "test-min-sync.json"_ls);
}

View File

@@ -1,70 +0,0 @@
// SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include <QAbstractItemModelTester>
#include <QTest>
#include "enums/neochatroomtype.h"
#include "models/roomtreemodel.h"
#include "models/sortfilterroomtreemodel.h"
#include "neochatconnection.h"
#include "testutils.h"
using namespace Quotient;
class RoomTreeModelTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testTreeModel();
};
void RoomTreeModelTest::testTreeModel()
{
auto connection = new NeoChatConnection;
Connection::makeMockConnection(connection, QStringLiteral("@bob:kde.org"));
auto room = dynamic_cast<NeoChatRoom *>(new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QStringLiteral("test-min-sync.json")));
QVERIFY(room);
connection->addRoom(room);
RoomTreeModel model;
model.setConnection(connection);
SortFilterRoomTreeModel filterModel;
filterModel.setSourceModel(&model);
QAbstractItemModelTester tester(&model);
QAbstractItemModelTester testerFilter(&filterModel);
QCOMPARE(model.rowCount(), static_cast<int>(NeoChatRoomType::TypesCount));
// Check data category
auto category = static_cast<int>(NeoChatRoomType::typeForRoom(room));
QCOMPARE(category, NeoChatRoomType::Normal);
auto normalCategoryIdx = model.index(category, 0);
QCOMPARE(model.data(normalCategoryIdx, RoomTreeModel::DisplayNameRole).toString(), QStringLiteral("Normal"));
QCOMPARE(model.data(normalCategoryIdx, RoomTreeModel::DelegateTypeRole).toString(), QStringLiteral("section"));
QCOMPARE(model.data(normalCategoryIdx, RoomTreeModel::IconRole).toString(), QStringLiteral("group"));
QCOMPARE(model.data(normalCategoryIdx, RoomTreeModel::CategoryRole).toInt(), category);
QCOMPARE(model.rowCount(normalCategoryIdx), 1);
// Check data room
auto roomIdx = model.index(0, 0, normalCategoryIdx);
QCOMPARE(model.data(roomIdx, RoomTreeModel::CurrentRoomRole).value<NeoChatRoom *>(), room);
QCOMPARE(model.data(roomIdx, RoomTreeModel::CategoryRole).toInt(), category);
// Move room
room->setProperty("isFavorite", true);
model.moveRoom(room);
auto newCategory = static_cast<int>(NeoChatRoomType::typeForRoom(room));
QCOMPARE(newCategory, NeoChatRoomType::Favorite);
auto newCategoryIdx = model.index(newCategory, 0);
QVERIFY(newCategoryIdx != normalCategoryIdx);
}
QTEST_MAIN(RoomTreeModelTest)
#include "roomtreemodeltest.moc"

View File

@@ -513,7 +513,7 @@ void TextHandlerTest::componentOutput_data()
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
MessageComponent{MessageComponentType::Code,
QStringLiteral("Some code"),
QVariantMap{{QStringLiteral("class"), QStringLiteral("HTML")}}}};
QVariantMap{{QStringLiteral("class"), QStringLiteral("html")}}}};
QTest::newRow("quote") << QStringLiteral("<p>Text</p>\n<blockquote>\n<p>blockquote</p>\n</blockquote>")
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
MessageComponent{MessageComponentType::Quote, QStringLiteral("\"blockquote\""), {}}};
@@ -529,6 +529,19 @@ void TextHandlerTest::componentOutput_data()
QTest::newRow("inline code single block") << QStringLiteral("<code>https://kde.org</code>")
<< QList<MessageComponent>{
MessageComponent{MessageComponentType::Text, QStringLiteral("<code>https://kde.org</code>"), {}}};
QTest::newRow("long start tag")
<< QStringLiteral(
"Ah, you mean something like<br/><pre data-md=\"```\"><code class=\"language-qml\"># main.qml\nimport CustomQml\n...\nControls.TextField { id: "
"someField }\nCustomQml {\n someTextProperty: someField.text\n}\n</code></pre>Sure you can, it's still local to the same file where you "
"defined the id")
<< QList<MessageComponent>{
MessageComponent{MessageComponentType::Text, QStringLiteral("Ah, you mean something like"), {}},
MessageComponent{
MessageComponentType::Code,
QStringLiteral(
"# main.qml\nimport CustomQml\n...\nControls.TextField { id: someField }\nCustomQml {\n someTextProperty: someField.text\n}"),
QVariantMap{{QStringLiteral("class"), QStringLiteral("qml")}}},
MessageComponent{MessageComponentType::Text, QStringLiteral("Sure you can, it's still local to the same file where you defined the id"), {}}};
}
void TextHandlerTest::componentOutput()

View File

@@ -32,6 +32,7 @@
<name xml:lang="it">NeoChat</name>
<name xml:lang="ka">NeoChat</name>
<name xml:lang="ko">NeoChat</name>
<name xml:lang="lv">NeoChat</name>
<name xml:lang="nl">NeoChat</name>
<name xml:lang="nn">NeoChat</name>
<name xml:lang="pa">ਨਿਓ-ਚੈਟ</name>
@@ -59,10 +60,12 @@
<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="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>
<summary xml:lang="lv">Tērzējiet ar saviem draugiem „Matrix“ tīklā</summary>
<summary xml:lang="nl">Met uw vrienden chatten op matrix</summary>
<summary xml:lang="nn">Prat med vennar på Matrix</summary>
<summary xml:lang="pl">Rozmawiaj ze swoimi znajomymi w Matriksie</summary>
@@ -78,17 +81,22 @@
<p>NeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.</p>
<p xml:lang="ca">El NeoChat és una aplicació de xat que us permet aprofitar plenament la xarxa Matrix. Proporciona una manera segura d'enviar missatges de text, vídeos i arxius d'àudio a la vostra família, companys i amics.</p>
<p xml:lang="ca-valencia">NeoChat és una aplicació de xat que us permet aprofitar plenament la xarxa Matrix. Proporciona una manera segura d'enviar missatges de text, vídeos i arxius d'àudio a la vostra família, companys i amics.</p>
<p xml:lang="eo">NeoChat estas babilej-apo, kiu ebligas al vi plene profiti de la Matrix-reto. Ĝi provizas al vi sekuran manieron sendi tekstmesaĝojn, filmetojn kaj sondosierojn al via familio, kolegoj kaj amikoj.</p>
<p xml:lang="es">NeoChat es una aplicación de chat que le permite aprovechar al máximo la red Matrix. Le proporciona un modo seguro de enviar mensajes de texto, vídeos y archivos de sonido a su familia, colegas y amigos.</p>
<p xml:lang="eu">NeoChat, Matrix sarearen abantaila guztiei probetsua ateratzeko aukera ematen dizun berriketa aplikaizo bat da. Zure familiari, kideei eta lagunei testu mezuak, bideoak eta audio fitxategiak era seguruan bidaltzeko aukera ematen dizu.</p>
<p xml:lang="fr">NeoChat est une application de discussions vous permettant de profiter pleinement du réseau Matrix. Elle vous offre un moyen sécurisé denvoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos ami(e)s.</p>
<p xml:lang="hu">A NeoChat egy olyan csevegőalkalmazás, amellyel teljes mértékben kihasználhatja a Matrix hálózatot. Biztonságos módot biztosít szöveges üzenetek, videók és hangfájlok küldéséhez családtagjainak, kollégáinak és barátainak.</p>
<p xml:lang="ia">NeoChat es un app de conversation que te permitte prender avantage plen del rete Matrix. Il te forni un modo secur de inviar messages de texto, videos e files audio a tui familia, collegas e amicos.</p>
<p xml:lang="it">NeoChat è un'applicazione di chat che ti consente di sfruttare appieno la rete Matrix. Ti fornisce un modo sicuro per inviare messaggi di testo, video e file audio a familiari, colleghi e amici.</p>
<p xml:lang="ka">NeoChat ჩატის აპია, რომელიც საშუალება გაძლევთ, Matrix-ის ქსელის საშუალებები ბოლომდე გამოიყენოთ. ის გაძლევთ უსაფრთხო გზას, გააგზავნოთ ტექსტური შეტყობინებები, ვიდეოებ და აუდიოფაილები თქვენს ოჯახთან, კოლეგებთან და მეგობრებთან.</p>
<p xml:lang="lv">„NeoChat“ ir tērzēšanas programma, kas ļauj pilnvērtīgi izmantot „Matrix“ tīklu. Tā sniedz drošu veidu teksta ziņu, video un audio sūtīšanai ģimenes locekļiem, kolēģiem un draugiem.</p>
<p xml:lang="nl">NeoChat is een chat-toepassing die u het volledige voordeel van het Matrix-netwerk laat genieten. Het levert u op een veilige manier tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden te verzenden.</p>
<p xml:lang="pl">NoeChat to aplikacja do rozmów, która umożliwia wykorzystanie wszystkich możliwości Matriksa. Umożliwia wysyłanie wiadomości tekstowych, filmów i dźwięków w bezpieczny sposób do twojej rodziny, kolegów i przyjaciół.</p>
<p xml:lang="sl">NeoChat je aplikacija za klepet, ki vam omogoča, da v celoti izkoristite omrežje Matrix. Zagotavlja vam varen način za pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek vaši družini, sodelavcem in prijateljem.</p>
<p xml:lang="tr">NeoChat, Matrix ağının tüm özelliklerini kullanan bir sohbet uygulamasıdır. Ailenize, arkadaşlarınıza ve iş arkadaşlarınıza metin iletileri, ses ve video dosyaları göndermenin kolay bir yolunu sunar.</p>
<p xml:lang="uk">NeoChat є програмою для спілкування, за допомогою якої ви можете скористатися усіма перевагами мережі Matrix. За її допомогою ви можете безпечно надсилати текстові повідомлення, відео та звукові файли вашим родичам, колегам та друзям.</p>
<p xml:lang="x-test">xxNeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.xx</p>
<p xml:lang="zh-TW">NeoChat 是一個讓您能夠完全利用 Matrix 網路的聊天應用程式。它讓您安全地傳送文字訊息、影片或音訊檔給家人、同事或朋友等等。</p>
<p>NeoChat 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.</p>
<p xml:lang="ar">يهدف نيوتشات إلى أن يكون تطبيقًا كامل الميزات لمواصفات ماتركس. على هذا النحو يتم دعم كل شيء في المواصفات المستقرة الحالية مع الاستثناءات الملحوظة لـ VoIP والخيوط وبعض جوانب التشفير من طرف إلى طرف. هناك عدد قليل من الإغفالات الصغيرة الأخرى بسبب حقيقة أن مواصفات ماتركس تتطور باستمرار ، ولكن يبقى الهدف توفير الدعم النهائي للمواصفات بأكملها.</p>
<p xml:lang="ca">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de la VoIP, fils i alguns aspectes de l'encriptatge d'extrem a extrem. Hi ha algunes altres omissions més petites a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu segueix sent proporcionar suport eventual per a tota l'especificació.</p>
@@ -100,10 +108,12 @@
<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>
<p xml:lang="ko">NeoChat은 Matrix 표준을 따르는 프로그램을 목표로 합니다. 현재 안정 버전의 표준에서 제공하는 기능의 대부분을 지원하며, VoIP, 스레드, 일부 종단간 암호화와 같은 기능은 아직 지원하지 않습니다. Matrix 표준은 계속하여 진화 중이기 때문에 일부 기능이 빠져 있을 수도 있지만 장기적으로는 전체 표준을 지원하는 것이 목표입니다.</p>
<p xml:lang="lv">„NeoChat“ mērķis ir piedāvāt plašas iespējas atbilstoši „Matrix“ specifikācijai. Līdz ar to programma atbalsta visu pašreizējā stabilajā specifikācijā, izņemot VoIP, pavedienus un dažos aspektos galšifrēšanu. Pastāv citas atsevišķas sīkas neieviestas daļas, jo „Matrix“ specifikācija nepārtraukti attīstās, tomēr mērķis ir ar laiku nodrošināt atbalstu pilnai specifikācijai.</p>
<p xml:lang="nl">NeoChat richt zich op het volledig bieden van alle mogelijkheden van de Matrix-specificatie. Alles in de huidige stabiele specificatie met merkbare uitzondering van VoIP, gekoppelde discussies en sommige aspecten van eind-tot-eind versleuteling worden ondersteund. Er zijn een paar andere kleinere omissies vanwege het feit dat de Matrix specificatie constant evolueert maar het doel blijft het eventueel bieden van ondersteuning van de gehele specificatie.</p>
<p xml:lang="nn">NeoChat har som mål å støtta all funksjonalitet i Matrix-spesifikasjonen. Førebels er alt i den gjeldande stabile spesifikasjonen støtta, med unntak av VoIP, trådar og nokre delar av ende-til-kryptering. Det finst òg andre småting som ikkje er støtta, sidan Matrix-spesifikasjon er i stadig endring, men målet er altså støtte for alt.</p>
<p xml:lang="pl">NeoChat w zamyśle ma być pełnowartościową aplikacją wg wytycznych Matriksa. Z tego powodu, wszystko, co jest obecnie w stabilnych wytycznych z pominięciem VoIP, wątków i niektórych części szyfrowania Użytkownik-do-Użytkownika są obecnie obsługiwane. Pominięto też kilka mniejszych rzeczy ze względu na ciągły rozwój wytycznych Matriksa, lecz celem nadal jest zapewnienie obsługi wszystkich wytycznych.</p>
@@ -125,10 +135,12 @@
<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>
<p xml:lang="ko">Matrix 표준 개발의 특징으로 인하여 NeoChat은 일부 실험적인 기능을 지원합니다. 현재 지원하는 기능은 다음과 같습니다.</p>
<p xml:lang="lv">„Matrix“ specifikācijas veida dēļ „NeoChat“ attīstība atbalsta arī vairākas nestabilas iespējas, šobrīd šādas ir:</p>
<p xml:lang="nl">Vanwege de aard van de ontwikkeling van de Matrix specificatie ondersteunt NeoChat ook talloze onstabiele mogelijkheden. Dit zijn nu:</p>
<p xml:lang="nn">På grunn av måten Matrix-spesifikasjonen vert utvikla på, støttar NeoChat òg nokre uferdige funksjonar:</p>
<p xml:lang="pl">Ze względu na sposób rozwoju Matriksa, NeoChat obsługuje także kilka niestabilnych możliwości. Obecnie są to:</p>
@@ -152,10 +164,12 @@
<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>
<li xml:lang="ko">투표 - MSC3381</li>
<li xml:lang="lv">Aptaujas — MSC3381</li>
<li xml:lang="nl">Polls - MSC3381</li>
<li xml:lang="nn">Avstemmingar  MSC3381</li>
<li xml:lang="pl">Ankiety - MSC3381</li>
@@ -178,10 +192,12 @@
<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>
<li xml:lang="ko">스티커 팩 - MSC2545</li>
<li xml:lang="lv">Uzlīmju pakas — MSC2545</li>
<li xml:lang="nl">Sticker Packs - MSC2545</li>
<li xml:lang="nn">Klistremerke-pakkar  MSC2545</li>
<li xml:lang="pl">Paczki naklejek - MSC2545</li>
@@ -204,10 +220,12 @@
<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>
<li xml:lang="ko">위치 이벤트 - MSC3488</li>
<li xml:lang="lv">Atrašanās vietas notikumi — MSC3488</li>
<li xml:lang="nl">Locatie gebeurtenissen - MSC3488</li>
<li xml:lang="nn">Posisjonshendingar  MSC3488</li>
<li xml:lang="pl">Wydarzenia w miejscach - MSC3488</li>
@@ -234,8 +252,7 @@
<keyword>Matrix</keyword>
<keyword>Kirigami</keyword>
</keywords>
<developer>
<id>kde.org</id>
<developer id="kde.org">
<name>The KDE Community</name>
<url>https://kde.org</url>
</developer>
@@ -264,10 +281,12 @@
<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>
<caption xml:lang="ko">대화방 목록, 채팅, 대화방 정보가 표시된 주 보기</caption>
<caption xml:lang="lv">Pamata skats ar istabu sarakstu, tērzēšanu un istabas informāciju</caption>
<caption xml:lang="nl">Hoofdweergave met lijst met rooms, chat en roominformatie</caption>
<caption xml:lang="nn">Hovudvising med romliste, pratevindauge og rominformasjon</caption>
<caption xml:lang="pl">Główny widok z wykazem pokojów, rozmowami i szczegółami pokojów</caption>
@@ -285,17 +304,22 @@
<caption>Discover new communities with Matrix Spaces</caption>
<caption xml:lang="ca">Descobriu comunitats noves amb els espais de Matrix</caption>
<caption xml:lang="ca-valencia">Descobriu comunitats noves amb els espais de Matrix</caption>
<caption xml:lang="eo">Malkovru novajn komunumojn per Matrix Spaces</caption>
<caption xml:lang="es">Descubra nuevas comunidades con los espacios de Matrix</caption>
<caption xml:lang="eu">Ezagutu komunitate berriak Matrixeko Tokiak erabiliz</caption>
<caption xml:lang="fr">Découvrez de nouvelles communautés avec les espaces sous Matrix</caption>
<caption xml:lang="hu">Fedezzen fel új közösségeket a Matrix Terek segítségével</caption>
<caption xml:lang="ia">Discoperi nove communitate con Matrix Spaces (Spatios de Matrix)</caption>
<caption xml:lang="it">Scopri nuove comunità con Matrix Spaces</caption>
<caption xml:lang="ka">აღმოაჩინეთ ახალი საზოგადოებები Matrix Spaces-თან ერთად</caption>
<caption xml:lang="lv">Atklājiet jaunas kopienas ar „Matrix“ telpām</caption>
<caption xml:lang="nl">Ontdek nieuwe gemeenschappen met Matrix-ruimten</caption>
<caption xml:lang="pl">Odkrywaj nowe społeczności w Przestrzeniach Matriksa</caption>
<caption xml:lang="sl">Odkrijte nove skupnosti z Matrix Spaces</caption>
<caption xml:lang="tr">Matrix Alanlar ile yeni topluluklar keşfedin</caption>
<caption xml:lang="uk">Пошук нових спільнот за допомогою Matrix Spaces</caption>
<caption xml:lang="x-test">xxDiscover new communities with Matrix Spacesxx</caption>
<caption xml:lang="zh-TW">利用 Matrix 聊天空間發現新的社群</caption>
</screenshot>
<!--
Currently invalid. See https://github.com/ximion/appstream/issues/611
@@ -316,10 +340,12 @@
<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>
<caption xml:lang="ko">대화방 목록, 채팅, 대화방 정보가 표시된 주 보기</caption>
<caption xml:lang="lv">Pamata skats ar istabu sarakstu, tērzēšanu un istabas informāciju</caption>
<caption xml:lang="nl">Hoofdweergave met lijst met rooms, chat en roominformatie</caption>
<caption xml:lang="nn">Hovudvising med romliste, pratevindauge og rominformasjon</caption>
<caption xml:lang="pl">Główny widok z wykazem pokojów, rozmowami i szczegółami pokojów</caption>
@@ -345,10 +371,12 @@
<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>
<caption xml:lang="ko">로그인 화면</caption>
<caption xml:lang="lv">Ierakstīšanās logs</caption>
<caption xml:lang="nl">Aanmeldscherm</caption>
<caption xml:lang="nn">Innloggingsbilete</caption>
<caption xml:lang="pl">Ekran logowania</caption>
@@ -366,6 +394,7 @@
<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

@@ -26,6 +26,7 @@ Name[it]=NeoChat
Name[ka]=NeoChat
Name[ko]=NeoChat
Name[lt]=NeoChat
Name[lv]=NeoChat
Name[nl]=NeoChat
Name[nn]=NeoChat
Name[pa]=ਨਿਓ-ਚੈਟ
@@ -66,6 +67,7 @@ GenericName[it]=Client Matrix
GenericName[ka]=Matrix -ის კლიენტი
GenericName[ko]=Matrix 클라이언트
GenericName[lt]=Matrix kliento programa
GenericName[lv]=„Matrix“ klients
GenericName[nl]=Matrix-client
GenericName[nn]=Matrix-klient
GenericName[pa]=ਮੈਟਰਿਕਸ ਕਲਾਈਂਟ
@@ -105,6 +107,7 @@ Comment[it]=Client per il protocollo Matrix
Comment[ka]=კლიენტი Matrix-ის პროტოკოლისთვის
Comment[ko]=Matrix 프로토콜용 클라이언트
Comment[lt]=Matrix protokolo kliento programa
Comment[lv]=Klients „Matrix“ protokolam
Comment[nl]=Client voor het Matrix-protocol
Comment[nn]=Klient for Matrix-protokollen
Comment[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

5065
po/lv/neochat.po Normal file

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

@@ -3,6 +3,10 @@
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE AND NOT NEOCHAT_FLATPAK AND NOT NEOCHAT_APPIMAGE)
add_subdirectory(purpose)
endif()
add_library(neochat STATIC
controller.cpp
controller.h
@@ -122,6 +126,7 @@ add_library(neochat STATIC
events/pollevent.cpp
pollhandler.cpp
utils.h
utils.cpp
registration.cpp
neochatconnection.cpp
neochatconnection.h
@@ -165,6 +170,8 @@ add_library(neochat STATIC
mediamanager.h
models/statekeysmodel.cpp
models/statekeysmodel.h
sharehandler.cpp
sharehandler.h
)
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
@@ -181,18 +188,12 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/UserInfo.qml
qml/UserInfoDesktop.qml
qml/RoomPage.qml
qml/RoomWindow.qml
qml/ExploreRoomsPage.qml
qml/ManualRoomDialog.qml
qml/ExplorerDelegate.qml
qml/InviteUserPage.qml
qml/ImageEditorPage.qml
qml/WelcomePage.qml
qml/General.qml
qml/RoomSecurity.qml
qml/PushNotification.qml
qml/Categories.qml
qml/Permissions.qml
qml/NeochatMaximizeComponent.qml
qml/FancyEffectsContainer.qml
qml/TypingPane.qml
@@ -207,19 +208,6 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/RoomData.qml
qml/ServerData.qml
qml/EmojiPicker.qml
qml/TimelineDelegate.qml
qml/ReplyComponent.qml
qml/StateDelegate.qml
qml/MessageDelegate.qml
qml/Bubble.qml
qml/SectionDelegate.qml
qml/ReactionDelegate.qml
qml/EventDelegate.qml
qml/ReadMarkerDelegate.qml
qml/MimeComponent.qml
qml/StateComponent.qml
qml/MessageEditComponent.qml
qml/AvatarFlow.qml
qml/LoginStep.qml
qml/Login.qml
qml/Homeserver.qml
@@ -252,25 +240,6 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/FileDelegateContextMenu.qml
qml/MessageSourceSheet.qml
qml/ReportSheet.qml
qml/SettingsPage.qml
qml/ThemeRadioButton.qml
qml/ColorScheme.qml
qml/GeneralSettingsPage.qml
qml/EmoticonsPage.qml
qml/EmoticonEditorPage.qml
qml/EmoticonFormCard.qml
qml/GlobalNotificationsPage.qml
qml/NotificationRuleItem.qml
qml/AppearanceSettingsPage.qml
qml/AccountsPage.qml
qml/AccountEditorPage.qml
qml/DevicesPage.qml
qml/DeviceDelegate.qml
qml/DevicesCard.qml
qml/About.qml
qml/AboutKDE.qml
qml/SonnetConfigPage.qml
qml/NetworkProxyPage.qml
qml/DevtoolsPage.qml
qml/ConfirmEncryptionDialog.qml
qml/RemoveSheet.qml
@@ -299,44 +268,34 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/SpaceHierarchyDelegate.qml
qml/RemoveChildDialog.qml
qml/SelectParentDialog.qml
qml/Security.qml
qml/QrCodeMaximizeComponent.qml
qml/SelectSpacesDialog.qml
qml/AttachDialog.qml
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
qml/DelegateContextMenu.qml
qml/ShareDialog.qml
qml/FeatureFlagPage.qml
qml/IgnoredUsersDialog.qml
qml/AccountData.qml
qml/StateKeys.qml
qml/CodeComponent.qml
qml/QuoteComponent.qml
qml/UnlockSSSSDialog.qml
qml/QrScannerPage.qml
qml/JoinRoomDialog.qml
qml/ConfirmUrlDialog.qml
qml/AccountSwitchDialog.qml
RESOURCES
qml/confetti.png
qml/glowdot.png
)
add_subdirectory(settings)
add_subdirectory(timeline)
if(UNIX)
qt_target_qml_sources(neochat QML_FILES qml/ShareAction.qml)
else()
@@ -420,9 +379,14 @@ 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)
target_link_libraries(neochat PRIVATE settingsplugin timelineplugin)
target_link_libraries(neochat PUBLIC
Qt::Core
Qt::Quick
@@ -548,7 +512,7 @@ if(ANDROID)
)
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR}/android)
else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets)
target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets KF6::SyntaxHighlighting)
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
endif()
@@ -556,7 +520,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

@@ -80,6 +80,7 @@ QString ActionsHandler::handleMentions(QString handledText, QList<Mention> *ment
void ActionsHandler::handleMessage(const QString &text, QString handledText, ChatBarCache *chatBarCache)
{
Q_ASSERT(m_room);
if (NeoChatConfig::allowQuickEdit()) {
QRegularExpression sed(QStringLiteral("^s/([^/]*)/([^/]*)(/g)?$"));
auto match = sed.match(text);

View File

@@ -58,7 +58,7 @@ public Q_SLOTS:
void handleMessageEvent(ChatBarCache *chatBarCache);
private:
NeoChatRoom *m_room = nullptr;
QPointer<NeoChatRoom> m_room;
void checkEffects(const QString &text);
QString handleMentions(QString handledText, QList<Mention> *mentions);

View File

@@ -3,6 +3,7 @@
#include "chatbarcache.h"
#include "chatdocumenthandler.h"
#include "eventhandler.h"
#include "neochatroom.h"
@@ -117,7 +118,7 @@ QString ChatBarCache::relationMessage() const
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
EventHandler eventhandler(room, &**event);
return eventhandler.getPlainBody();
return eventhandler.getMarkdownBody();
}
return {};
}
@@ -163,6 +164,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.
*/

View File

@@ -156,6 +156,15 @@ void Controller::addConnection(NeoChatConnection *c)
c->saveState();
});
connect(c, &NeoChatConnection::loggedOut, this, [this, c] {
if (accounts().count() > 1) {
// Only set the connection if the the account being logged out is currently active
if (c == activeConnection()) {
setActiveConnection(dynamic_cast<NeoChatConnection *>(accounts().accounts()[0]));
}
} else {
setActiveConnection(nullptr);
}
dropConnection(c);
});
connect(c, &NeoChatConnection::badgeNotificationCountChanged, this, &Controller::updateBadgeNotificationCount);
@@ -377,6 +386,14 @@ AccountRegistry &Controller::accounts()
return m_accountRegistry;
}
QString Controller::loadFileContent(const QString &path) const
{
QUrl url(path);
QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString());
file.open(QFile::ReadOnly);
return QString::fromLatin1(file.readAll());
}
#include "moc_controller.cpp"
void Controller::setTestMode(bool test)
@@ -393,3 +410,12 @@ void Controller::removeConnection(const QString &userId)
SettingsGroup("Accounts"_ls).remove(userId);
}
}
bool Controller::ssssSupported() const
{
#if __has_include("Quotient/e2ee/sssshandler.h")
return true;
#else
return false;
#endif
}

View File

@@ -56,6 +56,8 @@ class Controller : public QObject
Q_PROPERTY(QStringList accountsLoading MEMBER m_accountsLoading NOTIFY accountsLoadingChanged)
Q_PROPERTY(bool ssssSupported READ ssssSupported CONSTANT)
public:
static Controller &instance();
static Controller *create(QQmlEngine *engine, QJSEngine *)
@@ -92,12 +94,16 @@ public:
*/
static void listenForNotifications();
Q_INVOKABLE QString loadFileContent(const QString &path) const;
Quotient::AccountRegistry &accounts();
static void setTestMode(bool testMode);
Q_INVOKABLE void removeConnection(const QString &userId);
bool ssssSupported() const;
private:
explicit Controller(QObject *parent = nullptr);

View File

@@ -40,6 +40,8 @@ public:
Code, /**< A code section. */
Quote, /**< A quote section. */
File, /**< A message that is a file. */
Itinerary, /**< A preview for a file that can integrate with KDE itinerary. */
Pdf, /**< A preview for a PDF 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). */
@@ -49,6 +51,7 @@ public:
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. */
Verification, /**< A user verification session start message. */
Other, /**< Anything that cannot be classified as another type. */
};
Q_ENUM(Type);

View File

@@ -29,7 +29,6 @@ public:
Deprioritized, /**< The room is set as low priority. */
Space, /**< The room is a space. */
AddDirect, /**< So we can show the add friend delegate. */
TypesCount, /**< Number of different types. */
};
Q_ENUM(Types);
@@ -41,8 +40,7 @@ public:
if (room->joinState() == Quotient::JoinState::Invite) {
return NeoChatRoomType::Invited;
}
// HACK for the unit tests
if (room->isFavourite() || room->property("isFavorite").toBool()) {
if (room->isFavourite()) {
return NeoChatRoomType::Favorite;
}
if (room->isLowPriority()) {

View File

@@ -280,6 +280,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()) {
@@ -640,6 +656,7 @@ QVariantMap EventHandler::getMediaInfoForEvent(const Quotient::RoomEvent *event)
// Get the file info for the event.
const EventContent::FileInfo *fileInfo;
bool isSticker = false;
if (event->is<RoomMessageEvent>()) {
auto roomMessageEvent = eventCast<const RoomMessageEvent>(event);
if (!roomMessageEvent->hasFileContent()) {
@@ -649,14 +666,15 @@ QVariantMap EventHandler::getMediaInfoForEvent(const Quotient::RoomEvent *event)
} else if (event->is<StickerEvent>()) {
auto stickerEvent = eventCast<const StickerEvent>(event);
fileInfo = &stickerEvent->image();
isSticker = true;
} else {
return {};
}
return getMediaInfoFromFileInfo(fileInfo, eventId);
return getMediaInfoFromFileInfo(fileInfo, eventId, false, isSticker);
}
QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail) const
QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail, bool isSticker) const
{
QVariantMap mediaInfo;
@@ -683,6 +701,8 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
// Add media size if available.
mediaInfo["size"_ls] = fileInfo->payloadSize;
mediaInfo["isSticker"_ls] = isSticker;
// Add parameter depending on media type.
if (mimeType.name().contains(QStringLiteral("image"))) {
if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileInfo)) {

View File

@@ -185,6 +185,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.
*
@@ -222,6 +229,7 @@ public:
* - width - The width in pixels of the audio media.
* - height - The height in pixels of the audio media.
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
* - isSticker - Whether the image is a sticker or not
*/
QVariantMap getMediaInfo() const;
@@ -313,6 +321,7 @@ public:
* - width - The width in pixels of the audio media.
* - height - The height in pixels of the audio media.
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
* - isSticker - Whether the image is a sticker or not
*/
QVariantMap getReplyMediaInfo() const;
@@ -398,5 +407,6 @@ private:
QString getMessageBody(const Quotient::RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const;
QVariantMap getMediaInfoForEvent(const Quotient::RoomEvent *event) const;
QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const;
QVariantMap
getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false, bool isSticker = false) const;
};

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

@@ -41,6 +41,12 @@ FileType::~FileType() noexcept
{
}
FileType &FileType::instance()
{
static FileType _instance;
return _instance;
}
QMimeType FileType::mimeTypeForName(const QString &nameOrAlias) const
{
Q_D(const FileType);
@@ -113,4 +119,10 @@ QStringList FileType::supportedAnimatedImageFormats() const
return d->supportedAnimatedImageFormats;
}
bool FileType::fileHasImage(const QUrl &file) const
{
const auto mimeType = mimeTypeForFile(file.toString());
return mimeType.isValid() && supportedImageFormats().contains(mimeType.preferredSuffix());
}
#include "moc_filetype.cpp"

View File

@@ -41,8 +41,13 @@ class FileType : public QObject
Q_PROPERTY(QStringList supportedAnimatedImageFormats READ supportedAnimatedImageFormats CONSTANT FINAL)
public:
explicit FileType(QObject *parent = nullptr);
~FileType();
static FileType &instance();
static FileType *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
/**
* @brief Returns a MIME type for nameOrAlias or an invalid one if none found.
@@ -120,7 +125,11 @@ public:
QStringList supportedImageFormats() const;
QStringList supportedAnimatedImageFormats() const;
bool fileHasImage(const QUrl &file) const;
private:
explicit FileType(QObject *parent = nullptr);
const QScopedPointer<FileTypePrivate> d_ptr;
Q_DECLARE_PRIVATE(FileType)
Q_DISABLE_COPY(FileType)

View File

@@ -11,6 +11,7 @@
#include <QQmlNetworkAccessManagerFactory>
#include <QQuickStyle>
#include <QQuickWindow>
#include <QtQml/QQmlExtensionPlugin>
#ifdef Q_OS_ANDROID
#include <QGuiApplication>
@@ -34,6 +35,11 @@
#include "neochat-version.h"
#include <Quotient/accountregistry.h>
#if __has_include("Quotient/e2ee/sssshandler.h")
#include <Quotient/e2ee/sssshandler.h>
#endif
#include <Quotient/keyverificationsession.h>
#include <Quotient/networkaccessmanager.h>
#include "blurhashimageprovider.h"
@@ -44,10 +50,16 @@
#include "neochatconfig.h"
#include "roommanager.h"
#include "windowcontroller.h"
#include "sharehandler.h"
#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
@@ -186,6 +198,9 @@ int main(int argc, char *argv[])
parser.addOption(dbusActivatedOption);
#endif
QCommandLineOption shareOption(QStringLiteral("share"), i18n("Share a URL to Matrix"), QStringLiteral("text"));
parser.addOption(shareOption);
about.setupCommandLine(&parser);
parser.process(app);
about.processCommandLine(&parser);
@@ -196,6 +211,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();
}
@@ -205,36 +228,48 @@ int main(int argc, char *argv[])
KDBusService service(KDBusService::Unique);
#endif
Q_IMPORT_QML_PLUGIN(org_kde_neochat_settingsPlugin)
Q_IMPORT_QML_PLUGIN(org_kde_neochat_timelinePlugin)
qml_register_types_org_kde_neochat();
qmlRegisterSingletonInstance("org.kde.neochat.config", 1, 0, "Config", NeoChatConfig::self());
qmlRegisterSingletonInstance("org.kde.neochat.accounts", 1, 0, "AccountRegistry", &Controller::instance().accounts());
qmlRegisterUncreatableType<KeyVerificationSession>("com.github.quotient_im.libquotient", 1, 0, "KeyVerificationSession", {});
#if __has_include("Quotient/e2ee/sssshandler.h")
qmlRegisterType<SSSSHandler>("com.github.quotient_im.libquotient", 1, 0, "SSSSHandler");
#endif
QQmlApplicationEngine engine;
#ifdef HAVE_KDBUSADDONS
service.connect(&service,
&KDBusService::activateRequested,
&RoomManager::instance(),
[&engine](const QStringList &arguments, const QString &workingDirectory) {
Q_UNUSED(workingDirectory);
&KDBusService::activateRequested,
&RoomManager::instance(),
[&engine](const QStringList &arguments, const QString &workingDirectory) {
Q_UNUSED(workingDirectory);
QWindow *window = windowFromEngine(&engine);
KWindowSystem::updateStartupId(window);
QWindow *window = windowFromEngine(&engine);
KWindowSystem::updateStartupId(window);
WindowController::instance().showAndRaiseWindow(QString());
WindowController::instance().showAndRaiseWindow(QString());
// Open matrix uri
if (arguments.isEmpty()) {
return;
}
auto args = arguments;
args.removeFirst();
for (const auto &arg : args) {
RoomManager::instance().resolveResource(arg);
}
});
// Open matrix uri
if (arguments.isEmpty()) {
return;
}
auto args = arguments;
args.removeFirst();
if (args.length() == 2 && args[0] == "--share"_ls) {
ShareHandler::instance().setText(args[1]);
return;
}
for (const auto &arg : args) {
RoomManager::instance().resolveResource(arg);
}
});
#endif
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
@@ -247,6 +282,10 @@ int main(int argc, char *argv[])
});
}
if (parser.isSet("share"_ls)) {
ShareHandler::instance().setText(parser.value(shareOption));
}
engine.addImageProvider(QLatin1String("mxc"), MatrixImageProvider::create(&engine, &engine));
engine.addImageProvider(QLatin1String("blurhash"), new BlurhashImageProvider);
@@ -255,7 +294,7 @@ int main(int argc, char *argv[])
return -1;
}
if (!parser.positionalArguments().isEmpty()) {
if (!parser.positionalArguments().isEmpty() && !parser.isSet("share"_ls)) {
RoomManager::instance().setUrlArgument(parser.positionalArguments()[0]);
}

View File

@@ -91,7 +91,9 @@ QVariant CompletionModel::data(const QModelIndex &index, int role) const
if (mediaId.isEmpty()) {
return QVariant();
}
return m_room->connection()->makeMediaUrl(QUrl(QStringLiteral("mxc://%1").arg(mediaId)));
if (m_room) {
return m_room->connection()->makeMediaUrl(QUrl(QStringLiteral("mxc://%1").arg(mediaId)));
}
}
}
if (m_autoCompletionType == Emoji) {

View File

@@ -118,7 +118,7 @@ private:
QString m_text;
QString m_fullText;
CompletionProxyModel *m_filterModel;
NeoChatRoom *m_room = nullptr;
QPointer<NeoChatRoom> m_room;
AutoCompletionType m_autoCompletionType = None;
void updateCompletion();

View File

@@ -3,6 +3,7 @@
#include "itinerarymodel.h"
#include <QJsonDocument>
#include <QProcess>
#include "config-neochat.h"
@@ -16,20 +17,6 @@ ItineraryModel::ItineraryModel(QObject *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()) {
@@ -39,20 +26,68 @@ QVariant ItineraryModel::data(const QModelIndex &index, int role) const
auto data = m_data[row];
if (role == NameRole) {
if (data[QStringLiteral("@type")] == QStringLiteral("TrainReservation")) {
return data[QStringLiteral("reservationFor")][QStringLiteral("trainNumber")];
auto trainName = QStringLiteral("%1 %2").arg(data[QStringLiteral("reservationFor")][QStringLiteral("trainName")].toString(),
data[QStringLiteral("reservationFor")][QStringLiteral("trainNumber")].toString());
if (trainName.trimmed().isEmpty()) {
return QStringLiteral("%1 to %2")
.arg(data[QStringLiteral("reservationFor")][QStringLiteral("departureStation")][QStringLiteral("name")].toString(),
data[QStringLiteral("reservationFor")][QStringLiteral("arrivalStation")][QStringLiteral("name")].toString());
;
}
return trainName;
}
if (data[QStringLiteral("@type")] == QStringLiteral("LodgingReservation")) {
return data[QStringLiteral("reservationFor")][QStringLiteral("name")];
}
if (data[QStringLiteral("@type")] == QStringLiteral("FoodEstablishmentReservation")) {
return data[QStringLiteral("reservationFor")][QStringLiteral("name")];
}
if (data[QStringLiteral("@type")] == QStringLiteral("FlightReservation")) {
return QStringLiteral("%1 %2 %3 → %4")
.arg(data[QStringLiteral("reservationFor")][QStringLiteral("airline")][QStringLiteral("iataCode")].toString(),
data[QStringLiteral("reservationFor")][QStringLiteral("flightNumber")].toString(),
data[QStringLiteral("reservationFor")][QStringLiteral("departureAirport")][QStringLiteral("iataCode")].toString(),
data[QStringLiteral("reservationFor")][QStringLiteral("arrivalAirport")][QStringLiteral("iataCode")].toString());
}
}
if (role == TypeRole) {
return data[QStringLiteral("@type")];
}
if (role == DepartureStationRole) {
return data[QStringLiteral("reservationFor")][QStringLiteral("departureStation")][QStringLiteral("name")];
if (role == DepartureLocationRole) {
if (data[QStringLiteral("@type")] == QStringLiteral("TrainReservation")) {
return data[QStringLiteral("reservationFor")][QStringLiteral("departureStation")][QStringLiteral("name")];
}
if (data[QStringLiteral("@type")] == QStringLiteral("FlightReservation")) {
return data[QStringLiteral("reservationFor")][QStringLiteral("departureAirport")][QStringLiteral("iataCode")];
}
}
if (role == ArrivalStationRole) {
return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalStation")][QStringLiteral("name")];
if (role == DepartureAddressRole) {
if (data[QStringLiteral("@type")] == QStringLiteral("TrainReservation")) {
return data[QStringLiteral("reservationFor")][QStringLiteral("departureStation")][QStringLiteral("address")][QStringLiteral("addressCountry")]
.toString();
}
if (data[QStringLiteral("@type")] == QStringLiteral("FlightReservation")) {
return data[QStringLiteral("reservationFor")][QStringLiteral("departureAirport")][QStringLiteral("address")][QStringLiteral("addressCountry")]
.toString();
}
}
if (role == ArrivalLocationRole) {
if (data[QStringLiteral("@type")] == QStringLiteral("TrainReservation")) {
return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalStation")][QStringLiteral("name")];
}
if (data[QStringLiteral("@type")] == QStringLiteral("FlightReservation")) {
return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalAirport")][QStringLiteral("iataCode")];
}
}
if (role == ArrivalAddressRole) {
if (data[QStringLiteral("@type")] == QStringLiteral("TrainReservation")) {
return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalStation")][QStringLiteral("address")][QStringLiteral("addressCountry")]
.toString();
}
if (data[QStringLiteral("@type")] == QStringLiteral("FlightReservation")) {
return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalAirport")][QStringLiteral("address")][QStringLiteral("addressCountry")]
.toString();
}
}
if (role == DepartureTimeRole) {
const auto &time = data[QStringLiteral("reservationFor")][QStringLiteral("departureTime")];
@@ -79,7 +114,16 @@ QVariant ItineraryModel::data(const QModelIndex &index, int role) const
addressData[QStringLiteral("addressCountry")].toString());
}
if (role == StartTimeRole) {
auto dateTime = data[QStringLiteral("checkinTime")][QStringLiteral("@value")].toVariant().toDateTime();
QDateTime dateTime;
if (data[QStringLiteral("@type")] == QStringLiteral("LodgingReservation")) {
dateTime = data[QStringLiteral("checkinTime")][QStringLiteral("@value")].toVariant().toDateTime();
}
if (data[QStringLiteral("@type")] == QStringLiteral("FoodEstablishmentReservation")) {
dateTime = data[QStringLiteral("startTime")][QStringLiteral("@value")].toVariant().toDateTime();
}
if (data[QStringLiteral("@type")] == QStringLiteral("FlightReservation")) {
dateTime = data[QStringLiteral("reservationFor")][QStringLiteral("boardingTime")][QStringLiteral("@value")].toVariant().toDateTime();
}
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
if (role == EndTimeRole) {
@@ -112,8 +156,10 @@ QHash<int, QByteArray> ItineraryModel::roleNames() const
return {
{NameRole, "name"},
{TypeRole, "type"},
{DepartureStationRole, "departureStation"},
{ArrivalStationRole, "arrivalStation"},
{DepartureLocationRole, "departureLocation"},
{DepartureAddressRole, "departureAddress"},
{ArrivalLocationRole, "arrivalLocation"},
{ArrivalAddressRole, "arrivalAddress"},
{DepartureTimeRole, "departureTime"},
{ArrivalTimeRole, "arrivalTime"},
{AddressRole, "address"},
@@ -133,11 +179,7 @@ QString ItineraryModel::path() const
void ItineraryModel::setPath(const QString &path)
{
if (path == m_path) {
return;
}
m_path = path;
Q_EMIT pathChanged();
loadData();
}
@@ -150,6 +192,11 @@ void ItineraryModel::loadData()
beginResetModel();
m_data = QJsonDocument::fromJson(data).array();
endResetModel();
Q_EMIT loaded();
});
connect(process, &QProcess::errorOccurred, this, [this]() {
Q_EMIT loadErrorOccurred();
});
}

View File

@@ -4,28 +4,27 @@
#pragma once
#include <QAbstractListModel>
#include <QJsonArray>
#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)
QML_UNCREATABLE("")
public:
enum Roles {
NameRole = Qt::DisplayRole,
TypeRole,
DepartureStationRole,
ArrivalStationRole,
DepartureLocationRole,
ArrivalLocationRole,
DepartureTimeRole,
DepartureAddressRole,
ArrivalTimeRole,
ArrivalAddressRole,
AddressRole,
StartTimeRole,
EndTimeRole,
@@ -37,9 +36,6 @@ public:
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;
@@ -51,11 +47,10 @@ public:
Q_INVOKABLE void sendToItinerary();
Q_SIGNALS:
void connectionChanged();
void pathChanged();
void loaded();
void loadErrorOccurred();
private:
QPointer<NeoChatConnection> m_connection;
QJsonArray m_data;
QString m_path;
void loadData();

View File

@@ -3,15 +3,25 @@
#include "messagecontentmodel.h"
#include <QImageReader>
#include <Quotient/events/redactionevent.h>
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/stickerevent.h>
#include <Quotient/room.h>
#include <KLocalizedString>
#ifndef Q_OS_ANDROID
#include <KSyntaxHighlighting/Definition>
#include <KSyntaxHighlighting/Repository>
#endif
#include "chatbarcache.h"
#include "enums/messagecomponenttype.h"
#include "eventhandler.h"
#include "filetype.h"
#include "itinerarymodel.h"
#include "linkpreviewer.h"
#include "neochatroom.h"
#include "texthandler.h"
@@ -64,11 +74,13 @@ MessageContentModel::MessageContentModel(const Quotient::RoomEvent *event, NeoCh
});
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_event->id()) {
updateComponents();
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()) {
updateComponents();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
@@ -152,6 +164,9 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return QVariant::fromValue(m_room->fileTransferInfo(event->id()));
}
}
if (role == ItineraryModelRole) {
return QVariant::fromValue<ItineraryModel *>(m_itineraryModel);
}
if (role == LatitudeRole) {
return eventHandler.getLatitude();
}
@@ -209,6 +224,7 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
roles[AuthorRole] = "author";
roles[MediaInfoRole] = "mediaInfo";
roles[FileTransferInfoRole] = "fileTransferInfo";
roles[ItineraryModelRole] = "itineraryModel";
roles[LatitudeRole] = "latitude";
roles[LongitudeRole] = "longitude";
roles[AssetRole] = "asset";
@@ -228,35 +244,113 @@ void MessageContentModel::updateComponents(bool isEditing)
beginResetModel();
m_components.clear();
EventHandler eventHandler(m_room, m_event);
if (eventHandler.hasReply()) {
if (m_room->findInTimeline(eventHandler.getReplyId()) == m_room->historyEdge()) {
m_components += MessageComponent{MessageComponentType::ReplyLoad, QString(), {}};
m_room->loadReply(m_event->id(), eventHandler.getReplyId());
} else {
m_components += MessageComponent{MessageComponentType::Reply, QString(), {}};
}
}
if (isEditing) {
m_components += MessageComponent{MessageComponentType::Edit, QString(), {}};
if (eventCast<const Quotient::RoomMessageEvent>(m_event)
&& eventCast<const Quotient::RoomMessageEvent>(m_event)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
m_components += MessageComponent{MessageComponentType::Verification, QString(), {}};
} else {
if (eventHandler.messageComponentType() == MessageComponentType::Text) {
const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event);
auto body = EventHandler::rawMessageBody(*event);
m_components.append(TextHandler().textComponents(body, EventHandler::messageBodyInputFormat(*event), m_room, event, event->isReplaced()));
} else {
m_components += MessageComponent{eventHandler.messageComponentType(), QString(), {}};
EventHandler eventHandler(m_room, m_event);
if (eventHandler.hasReply()) {
if (m_room->findInTimeline(eventHandler.getReplyId()) == m_room->historyEdge()) {
m_components += MessageComponent{MessageComponentType::ReplyLoad, QString(), {}};
m_room->loadReply(m_event->id(), eventHandler.getReplyId());
} else {
m_components += MessageComponent{MessageComponentType::Reply, QString(), {}};
}
}
}
if (m_linkPreviewer != nullptr) {
if (m_linkPreviewer->loaded()) {
m_components += MessageComponent{MessageComponentType::LinkPreview, QString(), {}};
if (isEditing) {
m_components += MessageComponent{MessageComponentType::Edit, QString(), {}};
} else if (m_event->isRedacted()) {
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
} else {
m_components += MessageComponent{MessageComponentType::LinkPreviewLoad, QString(), {}};
if (eventHandler.messageComponentType() == MessageComponentType::Text) {
const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event);
auto body = EventHandler::rawMessageBody(*event);
m_components.append(TextHandler().textComponents(body, EventHandler::messageBodyInputFormat(*event), m_room, event, event->isReplaced()));
} else if (eventHandler.messageComponentType() == MessageComponentType::File) {
m_components += MessageComponent{MessageComponentType::File, QString(), {}};
if (m_emptyItinerary) {
Quotient::FileTransferInfo fileTransferInfo;
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (event->hasFileContent()) {
fileTransferInfo = m_room->fileTransferInfo(event->id());
}
}
if (auto event = eventCast<const Quotient::StickerEvent>(m_event)) {
fileTransferInfo = m_room->fileTransferInfo(event->id());
}
#ifndef Q_OS_ANDROID
KSyntaxHighlighting::Repository repository;
const auto definitionForFile = repository.definitionForFileName(fileTransferInfo.localPath.toString());
if (definitionForFile.isValid() || QFileInfo(fileTransferInfo.localPath.path()).suffix() == QStringLiteral("txt")) {
QFile file(fileTransferInfo.localPath.path());
file.open(QIODevice::ReadOnly);
m_components += MessageComponent{MessageComponentType::Code,
QString::fromStdString(file.readAll().toStdString()),
{{QStringLiteral("class"), definitionForFile.name()}}};
}
#endif
if (FileType::instance().fileHasImage(fileTransferInfo.localPath)) {
QImageReader reader(fileTransferInfo.localPath.path());
m_components += MessageComponent{MessageComponentType::Pdf, QString(), {{QStringLiteral("size"), reader.size()}}};
}
} else {
updateItineraryModel();
if (m_itineraryModel != nullptr) {
m_components += MessageComponent{MessageComponentType::Itinerary, QString(), {}};
}
}
} else {
m_components += MessageComponent{eventHandler.messageComponentType(), QString(), {}};
}
}
if (m_linkPreviewer != nullptr) {
if (m_linkPreviewer->loaded()) {
m_components += MessageComponent{MessageComponentType::LinkPreview, QString(), {}};
} else {
m_components += MessageComponent{MessageComponentType::LinkPreviewLoad, QString(), {}};
}
}
}
endResetModel();
}
void MessageContentModel::updateItineraryModel()
{
if (m_room == nullptr || m_event == nullptr) {
return;
}
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (event->hasFileContent()) {
auto filePath = m_room->fileTransferInfo(event->id()).localPath;
if (filePath.isEmpty() && m_itineraryModel != nullptr) {
delete m_itineraryModel;
m_itineraryModel = nullptr;
} else if (!filePath.isEmpty()) {
if (m_itineraryModel == nullptr) {
m_itineraryModel = new ItineraryModel(this);
connect(m_itineraryModel, &ItineraryModel::loaded, this, [this]() {
if (m_itineraryModel->rowCount() == 0) {
m_itineraryModel->deleteLater();
m_itineraryModel = nullptr;
m_emptyItinerary = true;
updateComponents();
}
});
connect(m_itineraryModel, &ItineraryModel::loadErrorOccurred, this, [this]() {
m_itineraryModel->deleteLater();
m_itineraryModel = nullptr;
m_emptyItinerary = true;
updateComponents();
});
}
m_itineraryModel->setPath(filePath.toString());
}
}
}
}

View File

@@ -8,6 +8,7 @@
#include "enums/messagecomponenttype.h"
#include "eventhandler.h"
#include "itinerarymodel.h"
#include "linkpreviewer.h"
#include "neochatroom.h"
@@ -45,6 +46,7 @@ public:
AuthorRole, /**< The author of the event. */
MediaInfoRole, /**< The media info for the event. */
FileTransferInfoRole, /**< FileTransferInfo for any downloading files. */
ItineraryModelRole, /**< The itinerary model for a file. */
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. */
@@ -85,11 +87,15 @@ public:
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
NeoChatRoom *m_room = nullptr;
QPointer<NeoChatRoom> m_room;
const Quotient::RoomEvent *m_event = nullptr;
QList<MessageComponent> m_components;
void updateComponents(bool isEditing = false);
LinkPreviewer *m_linkPreviewer = nullptr;
ItineraryModel *m_itineraryModel = nullptr;
void updateItineraryModel();
bool m_emptyItinerary = false;
};

View File

@@ -629,6 +629,10 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
int MessageEventModel::eventIdToRow(const QString &eventID) const
{
if (m_currentRoom == nullptr) {
return -1;
}
const auto it = m_currentRoom->findInTimeline(eventID);
if (it == m_currentRoom->historyEdge()) {
// qWarning() << "Trying to find inexistent event:" << eventID;

View File

@@ -113,7 +113,7 @@ private Q_SLOTS:
void refreshRow(int row);
private:
NeoChatRoom *m_currentRoom = nullptr;
QPointer<NeoChatRoom> m_currentRoom = nullptr;
QString lastReadEventId;
QPersistentModelIndex m_lastReadEventIndex;
int rowBelowInserted = -1;

View File

@@ -16,7 +16,7 @@
#include <Quotient/user.h>
ReactionModel::ReactionModel(const Quotient::RoomMessageEvent *event, const NeoChatRoom *room)
ReactionModel::ReactionModel(const Quotient::RoomMessageEvent *event, NeoChatRoom *room)
: QAbstractListModel(nullptr)
, m_room(room)
, m_event(event)
@@ -162,6 +162,30 @@ QHash<int, QByteArray> ReactionModel::roleNames() const
};
}
bool isEmoji(const QString &text)
{
#ifdef HAVE_ICU
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);
int from = 0;
while (finder.toNextBoundary() != -1) {
auto to = finder.position();
if (text[from].isSpace()) {
from = to;
continue;
}
auto first = text.mid(from, to - from).toUcs4()[0];
if (!u_hasBinaryProperty(first, UCHAR_EMOJI)) {
return false;
}
from = to;
}
return true;
#else
return false;
#endif
}
QString ReactionModel::reactionText(QString text) const
{
text = text.toHtmlEscaped();
@@ -174,28 +198,6 @@ QString ReactionModel::reactionText(QString text) const
return QStringLiteral("<img src=\"%1\" width=\"%2\" height=\"%2\">")
.arg(m_room->connection()->makeMediaUrl(QUrl(text)).toString(), QString::number(size));
}
const auto isEmoji = [](const QString &text) {
#ifdef HAVE_ICU
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);
int from = 0;
while (finder.toNextBoundary() != -1) {
auto to = finder.position();
if (text[from].isSpace()) {
from = to;
continue;
}
auto first = text.mid(from, to - from).toUcs4()[0];
if (!u_hasBinaryProperty(first, UCHAR_EMOJI_PRESENTATION)) {
return false;
}
from = to;
}
return true;
#else
return false;
#endif
};
return isEmoji(text) ? QStringLiteral("<span style=\"font-family: 'emoji';\">") + text + QStringLiteral("</span>") : text;
}

View File

@@ -44,7 +44,7 @@ public:
HasLocalUser, /**< Whether the local user is in the list of authors. */
};
explicit ReactionModel(const Quotient::RoomMessageEvent *event, const NeoChatRoom *room);
explicit ReactionModel(const Quotient::RoomMessageEvent *event, NeoChatRoom *room);
/**
* @brief Get the given role value at the given index.
@@ -68,7 +68,7 @@ public:
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
const NeoChatRoom *m_room;
QPointer<NeoChatRoom> m_room;
const Quotient::RoomMessageEvent *m_event;
QList<Reaction> m_reactions;
QMap<QString, QString> m_shortcodes;

View File

@@ -13,177 +13,6 @@
using namespace Quotient;
TreeItem::TreeItem(TreeData treeData, TreeItem *parent)
: m_treeData(treeData)
, m_parentItem(parent)
{
}
void TreeItem::appendChild(std::unique_ptr<TreeItem> &&child)
{
m_childItems.push_back(std::move(child));
}
bool TreeItem::insertChildren(int position, int count, TreeData treeData)
{
if (position < 0 || position > qsizetype(m_childItems.size()))
return false;
for (int row = 0; row < count; ++row) {
m_childItems.insert(m_childItems.cbegin() + position, std::make_unique<TreeItem>(treeData, this));
}
return true;
}
bool TreeItem::removeChildren(int position, int count)
{
if (position < 0 || position + count > qsizetype(m_childItems.size())) {
return false;
}
for (int row = 0; row < count; ++row) {
m_childItems.erase(m_childItems.cbegin() + position);
qWarning() << "removing" << position;
}
return true;
}
TreeItem *TreeItem::child(int row)
{
return row >= 0 && row < childCount() ? m_childItems.at(row).get() : nullptr;
}
int TreeItem::childCount() const
{
return int(m_childItems.size());
}
int TreeItem::row() const
{
if (m_parentItem == nullptr) {
return 0;
}
const auto it = std::find_if(m_parentItem->m_childItems.cbegin(), m_parentItem->m_childItems.cend(), [this](const std::unique_ptr<TreeItem> &treeItem) {
return treeItem.get() == this;
});
if (it != m_parentItem->m_childItems.cend())
return std::distance(m_parentItem->m_childItems.cbegin(), it);
Q_ASSERT(false); // should not happen
return -1;
}
QVariant TreeItem::data(int role) const
{
if (!m_parentItem) {
return {};
}
if (std::holds_alternative<NeoChatRoomType::Types>(m_treeData)) {
const auto row = this->row();
switch (role) {
case RoomTreeModel::IsCategoryRole:
return true;
case RoomTreeModel::DisplayNameRole:
return NeoChatRoomType::typeName(row);
case RoomTreeModel::DelegateTypeRole:
if (row == NeoChatRoomType::Search) {
return QStringLiteral("search");
}
if (row == NeoChatRoomType::AddDirect) {
return QStringLiteral("addDirect");
}
return QStringLiteral("section");
case RoomTreeModel::IconRole:
return NeoChatRoomType::typeIconName(row);
case RoomTreeModel::CategoryRole:
return row;
default:
return {};
}
}
const auto room = std::get<NeoChatRoom *>(m_treeData);
switch (role) {
case RoomTreeModel::IsCategoryRole:
return false;
case RoomTreeModel::DisplayNameRole:
return room->displayName();
case RoomTreeModel::AvatarRole:
return room->avatarMediaId();
case RoomTreeModel::CanonicalAliasRole:
return room->canonicalAlias();
case RoomTreeModel::TopicRole:
return room->topic();
case RoomTreeModel::CategoryRole:
return NeoChatRoomType::typeForRoom(room);
case RoomTreeModel::ContextNotificationCountRole:
return room->contextAwareNotificationCount();
case RoomTreeModel::HasHighlightNotificationsRole:
return room->highlightCount() > 0 && room->contextAwareNotificationCount() > 0;
case RoomTreeModel::LastActiveTimeRole:
return room->lastActiveTime();
case RoomTreeModel::JoinStateRole:
if (!room->successorId().isEmpty()) {
return QStringLiteral("upgraded");
}
return QVariant::fromValue(room->joinState());
case RoomTreeModel::CurrentRoomRole:
return QVariant::fromValue(room);
case RoomTreeModel::SubtitleTextRole: {
if (room->lastEvent() == nullptr || room->lastEventIsSpoiler()) {
return QString();
}
EventHandler eventHandler(room, room->lastEvent());
return eventHandler.subtitleText();
}
case RoomTreeModel::AvatarImageRole:
return room->avatar(128);
case RoomTreeModel::RoomIdRole:
return room->id();
case RoomTreeModel::IsSpaceRole:
return room->isSpace();
case RoomTreeModel::IsChildSpaceRole:
return SpaceHierarchyCache::instance().isChild(room->id());
case RoomTreeModel::ReplacementIdRole:
return room->successorId();
case RoomTreeModel::IsDirectChat:
return room->isDirectChat();
case RoomTreeModel::DelegateTypeRole:
return QStringLiteral("normal");
}
return {};
}
TreeItem *TreeItem::parentItem() const
{
return m_parentItem;
}
std::optional<int> TreeItem::position(Quotient::Room *room) const
{
Q_ASSERT_X(std::holds_alternative<NeoChatRoomType::Types>(m_treeData), __FUNCTION__, "containsRoom only works in category items");
int i = 0;
for (const auto &child : m_childItems) {
if (std::get<NeoChatRoom *>(child->treeData()) == room) {
return i;
}
i++;
}
return std::nullopt;
}
TreeItem::TreeData TreeItem::treeData() const
{
return m_treeData;
}
RoomTreeModel::RoomTreeModel(QObject *parent)
: QAbstractItemModel(parent)
{
@@ -192,18 +21,15 @@ RoomTreeModel::RoomTreeModel(QObject *parent)
void RoomTreeModel::initializeCategories()
{
m_rootItem.reset(new TreeItem(nullptr, nullptr));
for (int i = 0; i < NeoChatRoomType::TypesCount; i++) {
m_rootItem->appendChild(std::make_unique<TreeItem>(NeoChatRoomType::Types(i), m_rootItem.get()));
for (const auto &key : m_rooms.keys()) {
for (const auto &room : m_rooms[key]) {
room->disconnect(this);
}
}
}
TreeItem *RoomTreeModel::getItem(const QModelIndex &index) const
{
if (index.isValid()) {
return static_cast<TreeItem *>(index.internalPointer());
m_rooms.clear();
for (int i = 0; i < 8; i++) {
m_rooms[NeoChatRoomType::Types(i)] = {};
}
return m_rootItem.get();
}
void RoomTreeModel::setConnection(NeoChatConnection *connection)
@@ -242,34 +68,24 @@ void RoomTreeModel::newRoom(Room *r)
return;
}
auto categoryItem = m_rootItem->child(type);
beginInsertRows(index(type, 0), categoryItem->childCount(), categoryItem->childCount());
categoryItem->appendChild(std::make_unique<TreeItem>(room, categoryItem));
beginInsertRows(index(type, 0), m_rooms[type].size(), m_rooms[type].size());
m_rooms[type].append(room);
connectRoomSignals(room);
endInsertRows();
qWarning() << "adding room" << type << "new count" << categoryItem->childCount();
}
void RoomTreeModel::leftRoom(Room *r)
{
const auto room = dynamic_cast<NeoChatRoom *>(r);
auto idx = indexForRoom(room);
if (!idx.isValid()) {
const auto type = NeoChatRoomType::typeForRoom(room);
auto row = m_rooms[type].indexOf(room);
if (row == -1) {
return;
}
auto parentItem = getItem(idx.parent());
Q_ASSERT(parentItem);
beginRemoveRows(idx.parent(), idx.row(), idx.row());
const bool success = parentItem->removeChildren(idx.row(), 1);
room->disconnect(this);
beginRemoveRows(index(type, 0), row, row);
m_rooms[type][row]->disconnect(this);
m_rooms[type].removeAt(row);
endRemoveRows();
if (success) {
qWarning() << "Unable to remove room";
}
}
void RoomTreeModel::moveRoom(Quotient::Room *room)
@@ -278,46 +94,31 @@ void RoomTreeModel::moveRoom(Quotient::Room *room)
// NeoChatRoomType::typeForRoom doesn't match it's current location. So find the room.
NeoChatRoomType::Types oldType;
int oldRow = -1;
for (int i = 0; i < NeoChatRoomType::TypesCount; i++) {
auto categoryItem = m_rootItem->child(i);
auto position = categoryItem->position(room);
if (position) {
oldType = static_cast<NeoChatRoomType::Types>(i);
oldRow = *position;
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;
}
auto neochatRoom = dynamic_cast<NeoChatRoom *>(room);
const auto newType = NeoChatRoomType::typeForRoom(neochatRoom);
const auto newType = NeoChatRoomType::typeForRoom(dynamic_cast<NeoChatRoom *>(room));
if (newType == oldType) {
return;
}
const auto oldParent = index(oldType, 0, {});
auto oldParentItem = getItem(oldParent);
Q_ASSERT(oldParentItem);
const auto newParent = index(newType, 0, {});
auto newParentItem = getItem(newParent);
Q_ASSERT(newParentItem);
// HACK: We're doing this as a remove then insert because moving doesn't work
// properly with DelegateChooser for whatever reason.
Q_ASSERT(checkIndex(index(oldRow, 0, oldParent), QAbstractItemModel::CheckIndexOption::IndexIsValid));
beginRemoveRows(oldParent, oldRow, oldRow);
const bool success = oldParentItem->removeChildren(oldRow, 1);
Q_ASSERT(success);
m_rooms[oldType].removeAt(oldRow);
endRemoveRows();
beginInsertRows(newParent, newParentItem->childCount(), newParentItem->childCount());
newParentItem->appendChild(std::make_unique<TreeItem>(neochatRoom, newParentItem));
beginInsertRows(newParent, m_rooms[newType].size(), m_rooms[newType].size());
m_rooms[newType].append(dynamic_cast<NeoChatRoom *>(room));
endInsertRows();
// Q_ASSERT(checkIndex(index(newParentItem->childCount() - 1, 0, newParent), QAbstractItemModel::CheckIndexOption::IndexIsValid));
}
void RoomTreeModel::connectRoomSignals(NeoChatRoom *room)
@@ -350,12 +151,14 @@ void RoomTreeModel::connectRoomSignals(NeoChatRoom *room)
void RoomTreeModel::refreshRoomRoles(NeoChatRoom *room, const QList<int> &roles)
{
const auto idx = indexForRoom(room);
if (!idx.isValid()) {
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);
}
@@ -366,42 +169,38 @@ NeoChatConnection *RoomTreeModel::connection() const
int RoomTreeModel::columnCount(const QModelIndex &parent) const
{
const TreeItem *parentItem = getItem(parent);
return parentItem ? 1 : 0;
Q_UNUSED(parent)
return 1;
}
int RoomTreeModel::rowCount(const QModelIndex &parent) const
{
const TreeItem *parentItem = getItem(parent);
return parentItem ? parentItem->childCount() : 0;
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.isValid()) {
if (!index.internalPointer()) {
return {};
}
TreeItem *childItem = getItem(index);
Q_ASSERT(childItem);
TreeItem *parentItem = childItem->parentItem();
return parentItem != m_rootItem.get() ? createIndex(parentItem->row(), 0, parentItem) : QModelIndex{};
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() && parent.column() != 0) {
if (!parent.isValid()) {
return createIndex(row, column, nullptr);
}
if (row >= rowCount(parent)) {
return {};
}
TreeItem *parentItem = getItem(parent);
Q_ASSERT(parentItem);
if (auto *childItem = parentItem->child(row)) {
return createIndex(row, column, childItem);
}
return {};
return createIndex(row, column, m_rooms[NeoChatRoomType::Types(parent.row())][row]);
}
QHash<int, QByteArray> RoomTreeModel::roleNames() const
@@ -433,12 +232,102 @@ QHash<int, QByteArray> RoomTreeModel::roleNames() const
QVariant RoomTreeModel::data(const QModelIndex &index, int role) const
{
if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)) {
qWarning() << index.row() << rowCount(index.parent());
Q_ASSERT(false);
return {};
return QVariant();
}
return getItem(index)->data(role);
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 == ContextNotificationCountRole) {
return int(room->contextAwareNotificationCount());
}
if (role == HasHighlightNotificationsRole) {
return room->highlightCount() > 0 && room->contextAwareNotificationCount() > 0;
}
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");
}
if (role == AttentionRole) {
return room->notificationCount() + room->highlightCount() > 0;
}
if (role == FavouriteRole) {
return room->isFavourite();
}
return {};
}
QModelIndex RoomTreeModel::indexForRoom(NeoChatRoom *room) const
@@ -447,18 +336,18 @@ QModelIndex RoomTreeModel::indexForRoom(NeoChatRoom *room) const
return {};
}
const auto roomType = NeoChatRoomType::typeForRoom(room);
const auto roomTypeItem = m_rootItem->child(roomType);
for (int i = 0, count = roomTypeItem->childCount(); i < count; i++) {
auto roomItem = roomTypeItem->child(i);
if (std::get<NeoChatRoom *>(roomItem->treeData()) == room) {
const auto parentIndex = index(roomType, 0, {});
const auto idx = index(i, 0, parentIndex);
return idx;
// Try and find by checking type.
const auto type = NeoChatRoomType::typeForRoom(room);
auto row = m_rooms[type].indexOf(room);
if (row >= 0) {
return index(row, 0, index(type, 0));
}
// Double check that the room isn't in the wrong category.
for (const auto &key : m_rooms.keys()) {
if (m_rooms[key].contains(room)) {
return index(m_rooms[key].indexOf(room), 0, index(key, 0));
}
}
return {};
}

View File

@@ -15,32 +15,6 @@ class Room;
class NeoChatConnection;
class NeoChatRoom;
class RoomTreeModelTest;
class TreeItem
{
public:
using TreeData = std::variant<NeoChatRoom *, NeoChatRoomType::Types>;
explicit TreeItem(TreeData data, TreeItem *parentItem);
TreeItem *child(int row);
int childCount() const;
QVariant data(int role) const;
void appendChild(std::unique_ptr<TreeItem> &&child);
bool insertChildren(int position, int count, TreeData treeData);
TreeItem *parentItem() const;
bool removeChildren(int position, int count);
bool removeColumns(int position, int columns);
std::optional<int> position(Quotient::Room *room) const;
int row() const;
TreeData treeData() const;
private:
std::vector<std::unique_ptr<TreeItem>> m_childItems;
TreeData m_treeData;
TreeItem *m_parentItem;
};
class RoomTreeModel : public QAbstractItemModel
{
@@ -75,7 +49,6 @@ public:
IconRole,
AttentionRole, /**< Whether there are any notifications. */
FavouriteRole, /**< Whether the room is favourited. */
IsCategoryRole, /**< Whether the item in the model is a category */
};
Q_ENUM(EventRoles)
explicit RoomTreeModel(QObject *parent = nullptr);
@@ -103,8 +76,6 @@ public:
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
TreeItem *getItem(const QModelIndex &index) const;
Q_INVOKABLE QModelIndex indexForRoom(NeoChatRoom *room) const;
Q_SIGNALS:
@@ -112,6 +83,7 @@ Q_SIGNALS:
private:
QPointer<NeoChatConnection> m_connection = nullptr;
QMap<NeoChatRoomType::Types, QList<QPointer<NeoChatRoom>>> m_rooms;
void initializeCategories();
void connectRoomSignals(NeoChatRoom *room);
@@ -121,8 +93,4 @@ private:
void moveRoom(Quotient::Room *room);
void refreshRoomRoles(NeoChatRoom *room, const QList<int> &roles = {});
std::unique_ptr<TreeItem> m_rootItem;
friend RoomTreeModelTest;
};

View File

@@ -121,7 +121,7 @@ private:
void setSearching(bool searching);
QString m_searchText;
NeoChatRoom *m_room = nullptr;
QPointer<NeoChatRoom> m_room;
Quotient::Omittable<Quotient::SearchJob::ResultRoomEvents> m_result = Quotient::none;
Quotient::SearchJob *m_job = nullptr;
bool m_searching = false;

View File

@@ -119,28 +119,20 @@ QString SortFilterRoomTreeModel::filterText() const
bool SortFilterRoomTreeModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
// root node
if (!source_parent.isValid()) {
return true;
}
const QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
if (!index.isValid()) {
qWarning() << source_row << source_parent << sourceModel()->rowCount(source_parent);
Q_ASSERT(false);
return true;
}
if (sourceModel()->data(index, RoomTreeModel::IsCategoryRole).toBool()) {
if (sourceModel()->data(index, RoomTreeModel::CategoryRole).toInt() == NeoChatRoomType::Search && NeoChatConfig::collapsed()) {
if (sourceModel()->data(sourceModel()->index(source_row, 0), RoomTreeModel::CategoryRole).toInt() == NeoChatRoomType::Search
&& NeoChatConfig::collapsed()) {
return true;
}
if (sourceModel()->data(index, RoomTreeModel::CategoryRole).toInt() == NeoChatRoomType::AddDirect && m_mode == DirectChats) {
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;

View File

@@ -8,11 +8,12 @@
#include <Quotient/room.h>
#include "neochatconnection.h"
#include "neochatroom.h"
SpaceChildrenModel::SpaceChildrenModel(QObject *parent)
: QAbstractItemModel(parent)
, m_rootItem(new SpaceTreeItem(nullptr))
{
m_rootItem = new SpaceTreeItem(nullptr);
}
SpaceChildrenModel::~SpaceChildrenModel()
@@ -32,18 +33,18 @@ void SpaceChildrenModel::setSpace(NeoChatRoom *space)
}
// disconnect the new room signal from the old connection in case it is different.
if (m_space != nullptr) {
disconnect(m_space->connection(), &Quotient::Connection::loadedRoomState, this, nullptr);
m_space->connection()->disconnect(this);
m_space->disconnect(this);
}
m_space = space;
Q_EMIT spaceChanged();
for (auto job : m_currentJobs) {
if (job) {
job->abandon();
}
refreshModel();
if (!m_space) {
return;
}
m_currentJobs.clear();
auto connection = m_space->connection();
connect(connection, &Quotient::Connection::loadedRoomState, this, [this](Quotient::Room *room) {
@@ -55,8 +56,6 @@ void SpaceChildrenModel::setSpace(NeoChatRoom *space)
connect(m_space, &Quotient::Room::changed, this, [this]() {
refreshModel();
});
refreshModel();
}
bool SpaceChildrenModel::loading() const
@@ -66,6 +65,21 @@ bool SpaceChildrenModel::loading() const
void SpaceChildrenModel::refreshModel()
{
for (auto job : m_currentJobs) {
if (job) {
job->abandon();
}
}
m_currentJobs.clear();
if (m_space == nullptr) {
beginResetModel();
delete m_rootItem;
m_rootItem = nullptr;
endResetModel();
return;
}
beginResetModel();
m_replacedRooms.clear();
delete m_rootItem;
@@ -112,6 +126,11 @@ void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJ
if (!successorId.isEmpty()) {
m_replacedRooms += successorId;
}
if (dynamic_cast<NeoChatRoom *>(room)->isSpace()) {
connect(room, &Quotient::Room::changed, this, [this]() {
refreshModel();
});
}
}
if (children[i].childrenState.size() > 0) {
auto job = m_space->connection()->callApi<Quotient::GetSpaceHierarchyJob>(children[i].roomId, Quotient::none, Quotient::none, 1);
@@ -120,19 +139,18 @@ void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJ
insertChildren(job->rooms(), index(insertRow, 0, parent));
});
}
parentItem->insertChild(insertRow,
new SpaceTreeItem(dynamic_cast<NeoChatConnection *>(m_space->connection()),
parentItem,
children[i].roomId,
children[i].name,
children[i].canonicalAlias,
children[i].topic,
children[i].numJoinedMembers,
children[i].avatarUrl,
children[i].guestCanJoin,
children[i].worldReadable,
children[i].roomType == QLatin1String("m.space"),
std::move(children[i].childrenState)));
parentItem->insertChild(std::make_unique<SpaceTreeItem>(dynamic_cast<NeoChatConnection *>(m_space->connection()),
parentItem,
children[i].roomId,
children[i].name,
children[i].canonicalAlias,
children[i].topic,
children[i].numJoinedMembers,
children[i].avatarUrl,
children[i].guestCanJoin,
children[i].worldReadable,
children[i].roomType == QLatin1String("m.space"),
std::move(children[i].childrenState)));
}
}
endInsertRows();

View File

@@ -131,7 +131,7 @@ Q_SIGNALS:
void loadingChanged();
private:
NeoChatRoom *m_space = nullptr;
QPointer<NeoChatRoom> m_space;
SpaceTreeItem *m_rootItem;
bool m_loading = false;

View File

@@ -60,4 +60,65 @@ bool SpaceChildSortFilterModel::filterAcceptsRow(int sourceRow, const QModelInde
return true;
}
void SpaceChildSortFilterModel::move(const QModelIndex &currentIndex, const QModelIndex &targetIndex)
{
const auto rootSpace = dynamic_cast<SpaceChildrenModel *>(sourceModel())->space();
if (rootSpace == nullptr) {
return;
}
const auto connection = rootSpace->connection();
const auto currentParent = currentIndex.parent();
auto targetParent = targetIndex.parent();
NeoChatRoom *currentParentSpace = nullptr;
if (!currentParent.isValid()) {
currentParentSpace = rootSpace;
} else {
currentParentSpace = static_cast<NeoChatRoom *>(connection->room(currentParent.data(SpaceChildrenModel::RoomIDRole).toString()));
}
NeoChatRoom *targetParentSpace = nullptr;
if (!targetParent.isValid()) {
targetParentSpace = rootSpace;
} else {
targetParentSpace = static_cast<NeoChatRoom *>(connection->room(targetParent.data(SpaceChildrenModel::RoomIDRole).toString()));
}
// If both parents are not resolvable to a room object we don't have the permissions
// required for this action.
if (currentParentSpace == nullptr || targetParentSpace == nullptr) {
return;
}
const auto currentRow = currentIndex.row();
auto targetRow = targetIndex.row();
const auto moveRoomId = currentIndex.data(SpaceChildrenModel::RoomIDRole).toString();
auto targetRoom = static_cast<NeoChatRoom *>(connection->room(targetIndex.data(SpaceChildrenModel::RoomIDRole).toString()));
// If the target room is a space, assume we want to drop the room into it.
if (targetRoom != nullptr && targetRoom->isSpace()) {
targetParent = targetIndex;
targetParentSpace = targetRoom;
targetRow = rowCount(targetParent);
}
const auto newRowCount = rowCount(targetParent) + (currentParentSpace != targetParentSpace ? 1 : 0);
for (int i = 0; i < newRowCount; i++) {
if (currentParentSpace == targetParentSpace && i == currentRow) {
continue;
}
targetParentSpace->setChildOrder(index(i, 0, targetParent).data(SpaceChildrenModel::RoomIDRole).toString(),
QString::number(i > targetRow ? i + 1 : i, 36));
if (i == targetRow) {
if (currentParentSpace != targetParentSpace) {
currentParentSpace->removeChild(moveRoomId, true);
targetParentSpace->addChild(moveRoomId, true, false, false, QString::number(i + 1, 36));
} else {
targetParentSpace->setChildOrder(currentIndex.data(SpaceChildrenModel::RoomIDRole).toString(), QString::number(i + 1, 36));
}
}
}
}
#include "moc_spacechildsortfiltermodel.cpp"

View File

@@ -46,6 +46,8 @@ protected:
*/
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
Q_INVOKABLE void move(const QModelIndex &currentIndex, const QModelIndex &targetIndex);
Q_SIGNALS:
void filterTextChanged();

View File

@@ -32,48 +32,62 @@ SpaceTreeItem::SpaceTreeItem(NeoChatConnection *connection,
{
}
SpaceTreeItem::~SpaceTreeItem()
bool SpaceTreeItem::operator==(const SpaceTreeItem &other) const
{
qDeleteAll(m_children);
return m_id == other.id();
}
SpaceTreeItem *SpaceTreeItem::child(int number)
SpaceTreeItem *SpaceTreeItem::child(int row)
{
if (number < 0 || number >= m_children.size()) {
return nullptr;
}
return m_children[number];
return row >= 0 && row < childCount() ? m_children.at(row).get() : nullptr;
}
int SpaceTreeItem::childCount() const
{
return m_children.count();
return int(m_children.size());
}
bool SpaceTreeItem::insertChild(int row, SpaceTreeItem *newChild)
bool SpaceTreeItem::insertChild(std::unique_ptr<SpaceTreeItem> newChild)
{
if (row < 0 || row > m_children.size()) {
if (newChild == nullptr) {
return false;
}
m_children.insert(row, newChild);
for (auto it = m_children.begin(), end = m_children.end(); it != end; ++it) {
if (*it == newChild) {
*it = std::move(newChild);
return true;
}
}
m_children.push_back(std::move(newChild));
return true;
}
bool SpaceTreeItem::removeChild(int row)
{
if (row < 0 || row >= m_children.size()) {
if (row < 0 || row >= childCount()) {
return false;
}
delete m_children.takeAt(row);
m_children.erase(m_children.begin() + row);
return true;
}
int SpaceTreeItem::row() const
{
if (m_parentItem) {
return m_parentItem->m_children.indexOf(const_cast<SpaceTreeItem *>(this));
if (m_parentItem == nullptr) {
return 0;
}
return 0;
const auto it = std::find_if(m_parentItem->m_children.cbegin(), m_parentItem->m_children.cend(), [this](const std::unique_ptr<SpaceTreeItem> &treeItem) {
return treeItem.get() == this;
});
if (it != m_parentItem->m_children.cend()) {
return std::distance(m_parentItem->m_children.cbegin(), it);
}
Q_ASSERT(false); // should not happen
return -1;
}
SpaceTreeItem *SpaceTreeItem::parentItem() const

View File

@@ -33,14 +33,15 @@ public:
bool worldReadable = {},
bool isSpace = {},
Quotient::StateEvents childStates = {});
~SpaceTreeItem();
bool operator==(const SpaceTreeItem &other) const;
/**
* @brief Return the child at the given row number.
*
* Nullptr is returned if there is no child at the given row number.
*/
SpaceTreeItem *child(int number);
SpaceTreeItem *child(int row);
/**
* @brief The number of children this item has.
@@ -48,9 +49,9 @@ public:
int childCount() const;
/**
* @brief Insert the given child at the given row number.
* @brief Insert the given child.
*/
bool insertChild(int row, SpaceTreeItem *newChild);
bool insertChild(std::unique_ptr<SpaceTreeItem> newChild);
/**
* @brief Remove the child at the given row number.
@@ -149,7 +150,7 @@ public:
private:
NeoChatConnection *m_connection;
QList<SpaceTreeItem *> m_children;
std::vector<std::unique_ptr<SpaceTreeItem>> m_children;
SpaceTreeItem *m_parentItem;
QString m_id;

View File

@@ -55,11 +55,14 @@ void StateKeysModel::setRoom(NeoChatRoom *room)
m_room = room;
Q_EMIT roomChanged();
loadState();
connect(room, &NeoChatRoom::changed, this, [this] {
if (room) {
loadState();
});
connect(room, &NeoChatRoom::changed, this, [this] {
loadState();
});
}
}
QString StateKeysModel::eventType() const

View File

@@ -76,7 +76,7 @@ Q_SIGNALS:
void eventTypeChanged();
private:
NeoChatRoom *m_room = nullptr;
QPointer<NeoChatRoom> m_room;
QString m_eventType;
QVector<const Quotient::StateEvent *> m_stateKeys;
void loadState();

View File

@@ -22,7 +22,9 @@ QVariant StickerModel::data(const QModelIndex &index, int role) const
const auto &row = index.row();
const auto &image = m_images[row];
if (role == UrlRole) {
return m_room->connection()->makeMediaUrl(image.url);
if (m_room) {
return m_room->connection()->makeMediaUrl(image.url);
}
}
if (role == BodyRole) {
if (image.body) {
@@ -108,6 +110,10 @@ void StickerModel::setRoom(NeoChatRoom *room)
void StickerModel::postSticker(int index)
{
if (!m_room) {
qWarning() << "No room";
}
const auto &image = m_images[index];
const auto &body = image.body ? *image.body : image.shortcode;
QJsonObject infoJson;

View File

@@ -101,6 +101,6 @@ private:
ImagePacksModel *m_model = nullptr;
int m_index = 0;
QList<Quotient::ImagePackEventContent::ImagePackImage> m_images;
NeoChatRoom *m_room;
QPointer<NeoChatRoom> m_room;
void reloadImages();
};

View File

@@ -61,7 +61,7 @@ public:
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
NeoChatRoom *m_room = nullptr;
QPointer<NeoChatRoom> m_room = nullptr;
};
/**

View File

@@ -24,6 +24,7 @@ Name[it]=NeoChat
Name[ka]=NeoChat
Name[ko]=NeoChat
Name[lt]=NeoChat
Name[lv]=NeoChat
Name[nl]=NeoChat
Name[nn]=NeoChat
Name[pa]=ਨਿਓ-ਚੈਟ
@@ -65,6 +66,7 @@ Comment[it]=Un client per matrix, il protocollo di comunicazione decentralizzato
Comment[ka]=კლიენტი Matrix-სთვის, დეცენტრალიზებული კომუნიკაციის პროტოკოლისთვის
Comment[ko]=Matrix, 분산 대화 프로토콜 클라이언트
Comment[lt]=Matrix decentralizuoto bendravimo protokolo kliento programa
Comment[lv]=Klients „Matrix“ protokolam — decentralizētam komunikācijas protokolam
Comment[nl]=Een client voor matrix, het gedecentraliseerde communicatieprotocol
Comment[nn]=Ein klient for Matrix  protokollen for desentralisert kommunikasjon
Comment[pa]=ਮੈਟਰਿਕਸ, ਸਰਬ-ਸਾਂਝੇ ਸੰਚਾਰ ਪਰੋਟੋਕਾਲ, ਲਈ ਕਲਾਈਂਟ ਹੈ
@@ -76,6 +78,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
@@ -107,6 +110,7 @@ Name[it]=Nuovo messaggio
Name[ka]=ახალი შეტყობინება
Name[ko]=새 메시지
Name[lt]=Nauja žinutė
Name[lv]=Jauns ziņojums
Name[nl]=Nieuw bericht
Name[nn]=Ny melding
Name[pa]=ਨਵਾਂ ਸੁਨੇਹਾ
@@ -146,6 +150,7 @@ Comment[it]=È presente un nuovo messaggio
Comment[ka]=გაქვთ ახალი შეტყობინება
Comment[ko]=새 메시지가 있음
Comment[lt]=Yra nauja žinutė
Comment[lv]=Ir pienācis jauns ziņojums
Comment[nl]=Er is een nieuw bericht
Comment[nn]=Du har ei ny melding
Comment[pa]=ਨਵਾਂ ਸੁਨੇਹਾ ਹੈ
@@ -189,6 +194,7 @@ Name[it]=Nuovo invito
Name[ka]=ახალი მოსაწვევი
Name[ko]=새 초대장
Name[lt]=Naujas pakvietimas
Name[lv]=Jauns uzaicinājums
Name[nl]=Nieuwe uitnodiging
Name[nn]=Ny invitasjon
Name[pa]=ਨਵਾਂ ਸੱਦਾ
@@ -227,6 +233,7 @@ Comment[it]=È presente un nuovo invito a una stanza
Comment[ka]=გაქვთ ახალი ოთახის მოსაწვევი
Comment[ko]=새로운 대화방 초대장을 받음
Comment[lt]=Yra naujas pakvietimas į kambarį
Comment[lv]=Istabā ir jauns uzaicinājums
Comment[nl]=Er is een nieuwe uitnodiging naar een room
Comment[nn]=Du har ein ny invitasjon til eit rom
Comment[pa]=ਰੂਮ ਲਈ ਨਵਾਂ ਸੱਦਾ ਹੈ
@@ -253,12 +260,15 @@ Name[eo]=Kundividi
Name[es]=Compartir
Name[eu]=Partekatu
Name[fr]=Partager
Name[hu]=Megosztás
Name[ia]=Comparti
Name[it]=Condivisione
Name[ka]=გაზიარება
Name[lv]=Kopīgot
Name[nl]=Gedeelde
Name[pl]=Udostępnij
Name[sl]=Deli
Name[ta]=பகிர்
Name[tr]=Paylaş
Name[uk]=Оприлюднення
Name[x-test]=xxSharexx
@@ -270,12 +280,15 @@ Comment[eo]=La rezulto el kundividado de enhavero
Comment[es]=El resultado de compartir una parte de contenido
Comment[eu]=Eduki pieza bat partekatzearen emaitza
Comment[fr]=Le résultat du partage d'une partie de contenu.
Comment[hu]=Tartalom megosztásának eredménye
Comment[ia]=Le exito de compartir un pecietta de contento
Comment[it]=Il risultato della condivisione di un contenuto
Comment[ka]=შემცველობის ნაწილის გაზიარების შედეგი
Comment[lv]=Satura kopīgošanas rezultāts
Comment[nl]=Het resultaat van het delen van een stukje inhoud
Comment[pl]=Wynik udostępniania kawałka treści
Comment[sl]=Rezultat deljenega kosa vsebine
Comment[ta]=எதையோ பகிர்ந்த‍தன் விளைவு
Comment[tr]=Bir parça içerik paylaşımının sonucu
Comment[uk]=Результат оприлюднення даних
Comment[x-test]=xxThe result of sharing a piece of contentxx

View File

@@ -161,6 +161,10 @@
<label>Enable threads</label>
<default>false</default>
</entry>
<entry name="SecretBackup" type="bool">
<label>Enable secret backup</label>
<default>false</default>
</entry>
</group>
</kcfg>

View File

@@ -153,14 +153,6 @@ void NeoChatConnection::logout(bool serverSideLogout)
job.start();
loop.exec();
if (Controller::instance().accounts().count() > 1) {
// Only set the connection if the the account being logged out is currently active
if (this == Controller::instance().activeConnection()) {
Controller::instance().setActiveConnection(dynamic_cast<NeoChatConnection *>(Controller::instance().accounts().accounts()[0]));
}
} else {
Controller::instance().setActiveConnection(nullptr);
}
if (!serverSideLogout) {
return;
}
@@ -344,6 +336,9 @@ void NeoChatConnection::openOrCreateDirectChat(User *user)
}
}
requestDirectChat(user);
connectSingleShot(this, &Connection::directChatAvailable, this, [=](auto room) {
room->activateEncryption();
});
}
qsizetype NeoChatConnection::directChatNotifications() const
@@ -482,9 +477,4 @@ QString NeoChatConnection::accountDataJsonString(const QString &type) const
return QString::fromUtf8(QJsonDocument(accountDataJson(type)).toJson());
}
void NeoChatConnection::addRoom(Quotient::Room *room)
{
Connection::addRoom(room, false);
}
#include "moc_neochatconnection.cpp"

View File

@@ -147,12 +147,6 @@ public:
bool isOnline() const;
/**
* Add room directly in the connection.
* @internal for tests
*/
void addRoom(Quotient::Room *room);
Q_SIGNALS:
void labelChanged();
void directChatNotificationsChanged();

View File

@@ -1298,9 +1298,7 @@ void NeoChatRoom::removeParent(const QString &parentId)
if (!currentState().contains("m.space.parent"_ls, parentId)) {
return;
}
if (auto parent = static_cast<NeoChatRoom *>(connection()->room(parentId))) {
setState("m.space.parent"_ls, parentId, {});
}
setState("m.space.parent"_ls, parentId, {});
}
bool NeoChatRoom::isSpace() const
@@ -1329,7 +1327,7 @@ bool NeoChatRoom::childrenHaveHighlightNotifications() const
return SpaceHierarchyCache::instance().spaceHasHighlightNotifications(id());
}
void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool canonical, bool suggested)
void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool canonical, bool suggested, const QString &order)
{
if (!isSpace()) {
return;
@@ -1337,7 +1335,9 @@ void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool can
if (!canSendEvent("m.space.child"_ls)) {
return;
}
setState("m.space.child"_ls, childId, QJsonObject{{QLatin1String("via"), QJsonArray{connection()->domain()}}, {"suggested"_ls, suggested}});
setState("m.space.child"_ls,
childId,
QJsonObject{{QLatin1String("via"), QJsonArray{connection()->domain()}}, {"suggested"_ls, suggested}, {"order"_ls, order}});
if (setChildParent) {
if (auto child = static_cast<NeoChatRoom *>(connection()->room(childId))) {
@@ -1403,6 +1403,28 @@ void NeoChatRoom::toggleChildSuggested(const QString &childId)
}
}
void NeoChatRoom::setChildOrder(const QString &childId, const QString &order)
{
if (!isSpace()) {
return;
}
if (!canSendEvent("m.space.child"_ls)) {
return;
}
if (const auto childEvent = currentState().get("m.space.child"_ls, childId)) {
auto content = childEvent->contentJson();
if (!content.contains("via"_ls)) {
return;
}
if (content.value("order"_ls).toString() == order) {
return;
}
content.insert("order"_ls, order);
setState("m.space.child"_ls, childId, content);
}
}
PushNotificationState::State NeoChatRoom::pushNotificationState() const
{
return m_currentPushNotificationState;

View File

@@ -560,7 +560,7 @@ public:
* Will fail if the user doesn't have the required privileges or this room is
* not a space.
*/
Q_INVOKABLE void addChild(const QString &childId, bool setChildParent = false, bool canonical = false, bool suggested = false);
Q_INVOKABLE void addChild(const QString &childId, bool setChildParent = false, bool canonical = false, bool suggested = false, const QString &order = {});
/**
* @brief Remove the given room as a child.
@@ -583,6 +583,8 @@ public:
*/
Q_INVOKABLE void toggleChildSuggested(const QString &childId);
void setChildOrder(const QString &childId, const QString &order = {});
bool isInvite() const;
bool readOnly() const;

View File

@@ -57,7 +57,7 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
if (job == nullptr) {
return;
}
if (connection == nullptr) {
if (connection == nullptr || !connection->isLoggedIn()) {
qWarning() << QStringLiteral("No connection for GetNotificationsJob %1").arg(job->objectName());
return;
}
@@ -150,7 +150,7 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
bool NotificationsManager::shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue &notification)
{
if (connection == nullptr) {
if (connection == nullptr || !connection->isLoggedIn()) {
return false;
}

View File

@@ -25,6 +25,7 @@ Name[it]=NeoChat
Name[ka]=NeoChat
Name[ko]=NeoChat
Name[lt]=NeoChat
Name[lv]=NeoChat
Name[nl]=NeoChat
Name[nn]=NeoChat
Name[pa]=ਨਿਓ-ਚੈਟ
@@ -64,6 +65,7 @@ Comment[it]=Trova stanze in NeoChat
Comment[ka]=იპოვე ოთახები NeoChat-ში
Comment[ko]=NeoChat에서 대화방 찾기
Comment[lt]=Rasti kambarius NeoChat
Comment[lv]=Atrast „NeoChat“ istabas
Comment[nl]=Rooms zoeken in NeoChat
Comment[nn]=Finn rom i NeoChat
Comment[pl]=Znajdź pokoje w NeoChat

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