Compare commits

..

31 Commits

Author SHA1 Message Date
l10n daemon script
5b3e650854 GIT_SILENT Sync po/docbooks with svn 2024-08-16 03:04:39 +00:00
Heiko Becker
86d85c6ce7 GIT_SILENT Update Appstream for new release 2024-08-16 00:35:00 +02:00
Heiko Becker
863e20394a GIT_SILENT Upgrade release service version to 24.08.0. 2024-08-15 23:34:44 +02:00
l10n daemon script
908dcea75e GIT_SILENT made messages (after extraction) 2024-08-11 02:34:30 +00:00
l10n daemon script
c59d5bc75d 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-08-10 03:12:41 +00:00
l10n daemon script
ff4ba4df9c GIT_SILENT made messages (after extraction) 2024-08-10 02:39:29 +00:00
l10n daemon script
0c1494e74a 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-08-09 03:01:15 +00:00
Albert Astals Cid
18d4da8b42 GIT_SILENT Upgrade release service version to 24.07.90. 2024-08-08 10:00:41 +02:00
l10n daemon script
9680cbbb38 GIT_SILENT Sync po/docbooks with svn 2024-08-06 03:07:30 +00:00
l10n daemon script
3b5ba470b6 GIT_SILENT Sync po/docbooks with svn 2024-08-04 03:12:15 +00:00
l10n daemon script
e1066aede2 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-08-04 03:08:29 +00:00
l10n daemon script
2bf4ed26f0 GIT_SILENT Sync po/docbooks with svn 2024-08-03 03:09:58 +00:00
l10n daemon script
b460b588a8 GIT_SILENT Sync po/docbooks with svn 2024-08-02 03:10:44 +00:00
l10n daemon script
05270c38bf GIT_SILENT Sync po/docbooks with svn 2024-07-31 03:05:10 +00:00
l10n daemon script
18afad83db GIT_SILENT Sync po/docbooks with svn 2024-07-30 03:09:21 +00:00
Tobias Fella
d390433b2b Show time without seconds in timeline
(cherry picked from commit 2df2e39d43)
2024-07-29 18:00:10 +02:00
Tobias Fella
0017be1c0f Show time without timezone in tooltip
Constructing the timezone string is relatively heavy and is done for each delegate, even when the tooltip is never shown

(cherry picked from commit 42cec7d5ba)
2024-07-29 18:00:08 +02:00
l10n daemon script
63eda3796d GIT_SILENT Sync po/docbooks with svn 2024-07-29 03:09:38 +00:00
l10n daemon script
3246076a0b GIT_SILENT Sync po/docbooks with svn 2024-07-28 03:10:41 +00:00
James Graham
e905cdd151 Cherrypick: Create NeochatRoomMember as a shim for RoomMember
The intention is that NeochatRoomMember can be created passed to QML and then be fully managed by it. It effectively just grabs the current RoomMember, calls the correct function then discards it so that we don't end up trying to access an already deleted state event.


(cherry picked from commit 11fd4f88ec)

a2a8ad09 Create NeochatRoomMember as a shim for RoomMember so it can be safely passed to QML
0867eef5 Fix showAuthor
0f72ccd0 Mamange the creation of NeochatRoomMembers and only create one per member rather than event.
dba88fe2 REmove getAuthor as no longer needed
4e3a61d1 Update include
32d4d9f7 Pass NeochatRoomMembers rather than RoomMembers to menus
8e4b2034 Don't leak memory
c2f2bb26 Fix code component regression.
5aee89be Make sure the sender Id is intialised properly for pending events
c10c2677 Tweak intialisation
b3146034 Make sure event objects are created for new pending events
e4fab6d9 Pass an empty NeochatRoomMember when not in the map

Co-authored-by: James Graham <james.h.graham@protonmail.com>
2024-07-27 09:33:14 +00:00
l10n daemon script
0372074beb GIT_SILENT Sync po/docbooks with svn 2024-07-27 03:10:43 +00:00
Joshua Goins
09e97f2bdb Make the "Unignore this user" button work on the Ignored Users page
This function now takes a QString. This should be fine to use since it's
in the 0.8.2 release which we require.

(cherry picked from commit 9a4504ce61)
2024-07-26 19:09:57 +00:00
Joshua Goins
8e324c16f3 Fix the emoji page not doing anything
This is yet more stuff broken due to referencing a implicit pageStack
that no longer exists.


(cherry picked from commit cb8ed02e82)

Co-authored-by: Joshua Goins <josh@redstrate.com>
2024-07-26 19:00:47 +00:00
Joshua Goins
2bb55eece7 Don't flag invite notifications as persistent
These really don't need to be persistent, as they even stick around
when NeoChat is closed. This also spams the user's notification system
usually, if they get lots of invitations at once which don't go away
automatically.

(cherry picked from commit 51197d7c1a)
2024-07-25 16:38:13 -04:00
Joshua Goins
2877e40647 Hint that the user can auto-reject invitations on the invite page
This should help make the new setting more discoverable.

(cherry picked from commit 2a2a2e0c05)
2024-07-25 16:38:13 -04:00
Joshua Goins
768ec242fa Allow blocking invites from people you don't share a room with
Matrix currently has a significant moderation loophole, thanks to
invites. Right now, anyone can invite anyone to a room - and clients
like NeoChat will gladly display these rooms to them and even give you
a notification.

However, this creates a pretty easy attack since room names and avatars
are arbitrary and this is a known vector of harassment in the Matrix
community. There's currently no tools to block this server-side, so
let's try to improve the situation where we can.

This adds a new setting to the Security page, wherein it allows you to
block invites from people you don't share a room with. This prevents the
notification from appearing and NeoChat will attempt to leave the room
immediately.

Since this depends on MSC 2666 - a currently unstable feature - the
server may not support it and NeoChat will disable the setting in this
case.

(cherry picked from commit 07fee30cc0)
2024-07-25 16:38:13 -04:00
Joshua Goins
194751627f Don't display notifications for invite rooms
Sometimes a ghost notification will appear, this is sometimes a stray
m.room.invite notification we didn't handle or some other event. Let's
outright reject all notifications from Invite-type rooms to prevent this
from happening altogether.

(cherry picked from commit 83c6ce0ace)
2024-07-25 16:38:13 -04:00
l10n daemon script
bc701c51d9 GIT_SILENT Sync po/docbooks with svn 2024-07-25 03:13:15 +00:00
l10n daemon script
35939b4af4 GIT_SILENT Sync po/docbooks with svn 2024-07-23 03:14:18 +00:00
l10n daemon script
a178b8b6ca 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-07-23 03:04:52 +00:00
Albert Astals Cid
a6994318de GIT_SILENT Upgrade release service version to 24.07.80. 2024-07-21 12:25:07 +02:00
174 changed files with 22348 additions and 33899 deletions

View File

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

View File

@@ -13,4 +13,3 @@ include:
- /gitlab-templates/craft-android-qt6-apks.yml
- /gitlab-templates/craft-appimage-qt6.yml
- /gitlab-templates/craft-windows-x86-64-qt6.yml
- /gitlab-templates/craft-windows-appx-qt6.yml

View File

@@ -8,13 +8,13 @@ cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "24")
set(RELEASE_SERVICE_VERSION_MINOR "11")
set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION_MINOR "08")
set(RELEASE_SERVICE_VERSION_MICRO "0")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
set(KF_MIN_VERSION "6.5")
set(KF_MIN_VERSION "6.0")
set(QT_MIN_VERSION "6.5")
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
@@ -38,7 +38,6 @@ include(KDEGitCommitHooks)
include(ECMCheckOutboundLicense)
include(ECMQtDeclareLoggingCategory)
include(ECMAddAndroidApk)
include(ECMQmlModule)
if (NOT ANDROID)
include(KDEClangFormat)
endif()
@@ -60,6 +59,7 @@ set_package_properties(Qt6 PROPERTIES
PURPOSE "Basic application components"
)
qt_policy(SET QTP0001 NEW)
if (QT_KNOWN_POLICY_QTP0004)
qt_policy(SET QTP0004 NEW)
endif ()

View File

@@ -107,13 +107,8 @@ void ChatBarCacheTest::reply()
void ChatBarCacheTest::edit()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(QLatin1String("some text"));
chatBarCache->setAttachmentPath(QLatin1String("some/path"));
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [](const QString &oldEventId, const QString &newEventId) {
QCOMPARE(oldEventId, QString());
QCOMPARE(newEventId, QString(QLatin1String("$153456789:example.org")));
});
chatBarCache->setEditId(QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->text(), QLatin1String("some text"));

View File

@@ -29,6 +29,8 @@ private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
EventHandler emptyHandler = EventHandler(nullptr, nullptr);
private Q_SLOTS:
void initTestCase();
@@ -41,6 +43,7 @@ private Q_SLOTS:
void time();
void nullTime();
void timeString();
void nullTimeString();
void highlighted();
void nullHighlighted();
void hidden();
@@ -62,6 +65,10 @@ private Q_SLOTS:
void nullReplyId();
void replyAuthor();
void nullReplyAuthor();
void replyBody();
void nullReplyBody();
void replyMediaInfo();
void nullReplyMediaInfo();
void thread();
void nullThread();
void location();
@@ -76,136 +83,157 @@ void EventHandlerTest::initTestCase()
void EventHandlerTest::eventId()
{
QCOMPARE(EventHandler::id(room->messageEvents().at(0).get()), QStringLiteral("$153456789:example.org"));
EventHandler eventHandler(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandler.getId(), QStringLiteral("$153456789:example.org"));
}
void EventHandlerTest::nullEventId()
{
QTest::ignoreMessage(QtWarningMsg, "id called with event set to nullptr.");
QCOMPARE(EventHandler::id(nullptr), QString());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getId called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getId(), QString());
}
void EventHandlerTest::authorDisplayName()
{
QCOMPARE(EventHandler::authorDisplayName(room, room->messageEvents().at(1).get()), QStringLiteral("before"));
EventHandler eventHandler(room, room->messageEvents().at(1).get());
QCOMPARE(eventHandler.getAuthorDisplayName(), QStringLiteral("before"));
}
void EventHandlerTest::nullAuthorDisplayName()
{
QTest::ignoreMessage(QtWarningMsg, "authorDisplayName called with room set to nullptr.");
QCOMPARE(EventHandler::authorDisplayName(nullptr, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getAuthorDisplayName(), QString());
QTest::ignoreMessage(QtWarningMsg, "authorDisplayName called with event set to nullptr.");
QCOMPARE(EventHandler::authorDisplayName(room, nullptr), QString());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getAuthorDisplayName(), QString());
}
void EventHandlerTest::singleLineSidplayName()
{
QCOMPARE(EventHandler::singleLineAuthorDisplayname(room, room->messageEvents().at(11).get()),
QStringLiteral("Look at me I put newlines in my display name"));
EventHandler eventHandler(room, room->messageEvents().at(11).get());
QCOMPARE(eventHandler.singleLineAuthorDisplayname(), QStringLiteral("Look at me I put newlines in my display name"));
}
void EventHandlerTest::nullSingleLineDisplayName()
{
QTest::ignoreMessage(QtWarningMsg, "singleLineAuthorDisplayname called with room set to nullptr.");
QCOMPARE(EventHandler::singleLineAuthorDisplayname(nullptr, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_room set to nullptr.");
QCOMPARE(emptyHandler.singleLineAuthorDisplayname(), QString());
QTest::ignoreMessage(QtWarningMsg, "singleLineAuthorDisplayname called with event set to nullptr.");
QCOMPARE(EventHandler::singleLineAuthorDisplayname(room, nullptr), QString());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_event set to nullptr.");
QCOMPARE(noEventHandler.singleLineAuthorDisplayname(), QString());
}
void EventHandlerTest::time()
{
const auto event = room->messageEvents().at(0).get();
EventHandler eventHandler(room, room->messageEvents().at(0).get());
QCOMPARE(EventHandler::time(event), QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC));
QCOMPARE(EventHandler::time(event, true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC));
QCOMPARE(eventHandler.getTime(), QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC));
QCOMPARE(eventHandler.getTime(true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC));
}
void EventHandlerTest::nullTime()
{
QTest::ignoreMessage(QtWarningMsg, "time called with event set to nullptr.");
QCOMPARE(EventHandler::time(nullptr), QDateTime());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getTime called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getTime(), QDateTime());
EventHandler eventHandler(room, room->messageEvents().at(0).get());
QTest::ignoreMessage(QtWarningMsg, "a value must be provided for lastUpdated for a pending event.");
QCOMPARE(EventHandler::time(room->messageEvents().at(0).get(), true), QDateTime());
QCOMPARE(eventHandler.getTime(true), QDateTime());
}
void EventHandlerTest::timeString()
{
const auto event = room->messageEvents().at(0).get();
EventHandler eventHandler(room, room->messageEvents().at(0).get());
KFormat format;
QCOMPARE(EventHandler::timeString(event, false),
QCOMPARE(eventHandler.getTimeString(false),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(event, true),
QCOMPARE(eventHandler.getTimeString(true),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(event, false, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QCOMPARE(eventHandler.getTimeString(false, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(event, true, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QCOMPARE(eventHandler.getTimeString(true, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(event, false, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QCOMPARE(eventHandler.getTimeString(false, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::LongFormat));
QCOMPARE(EventHandler::timeString(event, true, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QCOMPARE(eventHandler.getTimeString(true, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::LongFormat));
QCOMPARE(EventHandler::timeString(event, QStringLiteral("hh:mm")),
QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toString(QStringLiteral("hh:mm")));
QCOMPARE(eventHandler.getTimeString(QStringLiteral("hh:mm")), QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toString(QStringLiteral("hh:mm")));
}
void EventHandlerTest::nullTimeString()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getTimeString called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getTimeString(false), QString());
EventHandler eventHandler(room, room->messageEvents().at(0).get());
QTest::ignoreMessage(QtWarningMsg, "a value must be provided for lastUpdated for a pending event.");
QCOMPARE(eventHandler.getTimeString(false, QLocale::ShortFormat, true), QString());
}
void EventHandlerTest::highlighted()
{
QCOMPARE(EventHandler::isHighlighted(room, room->messageEvents().at(2).get()), true);
QCOMPARE(EventHandler::isHighlighted(room, room->messageEvents().at(0).get()), false);
EventHandler eventHandlerHighlight(room, room->messageEvents().at(2).get());
QCOMPARE(eventHandlerHighlight.isHighlighted(), true);
EventHandler eventHandlerNoHighlight(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoHighlight.isHighlighted(), false);
}
void EventHandlerTest::nullHighlighted()
{
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with room set to nullptr.");
QCOMPARE(EventHandler::isHighlighted(nullptr, nullptr), false);
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with m_room set to nullptr.");
QCOMPARE(emptyHandler.isHighlighted(), false);
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with event set to nullptr.");
QCOMPARE(EventHandler::isHighlighted(room, nullptr), false);
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with m_event set to nullptr.");
QCOMPARE(noEventHandler.isHighlighted(), false);
}
void EventHandlerTest::hidden()
{
QCOMPARE(EventHandler::isHidden(room, room->messageEvents().at(3).get()), true);
QCOMPARE(EventHandler::isHidden(room, room->messageEvents().at(0).get()), false);
EventHandler eventHandlerHidden(room, room->messageEvents().at(3).get());
QCOMPARE(eventHandlerHidden.isHidden(), true);
EventHandler eventHandlerNoHidden(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoHidden.isHidden(), false);
}
void EventHandlerTest::nullHidden()
{
QTest::ignoreMessage(QtWarningMsg, "isHidden called with room set to nullptr.");
QCOMPARE(EventHandler::isHidden(nullptr, nullptr), false);
QTest::ignoreMessage(QtWarningMsg, "isHidden called with m_room set to nullptr.");
QCOMPARE(emptyHandler.isHidden(), false);
QTest::ignoreMessage(QtWarningMsg, "isHidden called with event set to nullptr.");
QCOMPARE(EventHandler::isHidden(room, nullptr), false);
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "isHidden called with m_event set to nullptr.");
QCOMPARE(noEventHandler.isHidden(), false);
}
void EventHandlerTest::body()
{
const auto event = room->messageEvents().at(0).get();
EventHandler eventHandler(room, room->messageEvents().at(0).get());
QCOMPARE(EventHandler::richBody(room, event), QStringLiteral("<b>This is an example<br>text message</b>"));
QCOMPARE(EventHandler::richBody(room, event, true), QStringLiteral("<b>This is an example text message</b>"));
QCOMPARE(EventHandler::plainBody(room, event), QStringLiteral("This is an example\ntext message"));
QCOMPARE(EventHandler::plainBody(room, event, true), QStringLiteral("This is an example text message"));
QCOMPARE(eventHandler.getRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
QCOMPARE(eventHandler.getRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
QCOMPARE(eventHandler.getPlainBody(), QStringLiteral("This is an example\ntext message"));
QCOMPARE(eventHandler.getPlainBody(true), QStringLiteral("This is an example text message"));
}
void EventHandlerTest::nullBody()
{
QTest::ignoreMessage(QtWarningMsg, "richBody called with room set to nullptr.");
QCOMPARE(EventHandler::richBody(nullptr, nullptr), QString());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "richBody called with event set to nullptr.");
QCOMPARE(EventHandler::richBody(room, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "getRichBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getRichBody(), QString());
QTest::ignoreMessage(QtWarningMsg, "plainBody called with room set to nullptr.");
QCOMPARE(EventHandler::plainBody(nullptr, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "plainBody called with event set to nullptr.");
QCOMPARE(EventHandler::plainBody(room, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "getPlainBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getPlainBody(), QString());
}
void EventHandlerTest::genericBody_data()
@@ -225,45 +253,54 @@ void EventHandlerTest::genericBody()
QFETCH(int, eventNum);
QFETCH(QString, output);
QCOMPARE(EventHandler::genericBody(room->messageEvents().at(eventNum).get()), output);
EventHandler eventHandler(room, room->messageEvents().at(eventNum).get());
QCOMPARE(eventHandler.getGenericBody(), output);
}
void EventHandlerTest::nullGenericBody()
{
QTest::ignoreMessage(QtWarningMsg, "genericBody called with event set to nullptr.");
QCOMPARE(EventHandler::genericBody(nullptr), QString());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getGenericBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getGenericBody(), QString());
}
void EventHandlerTest::markdownBody()
{
QCOMPARE(EventHandler::markdownBody(room->messageEvents().at(0).get()), QStringLiteral("This is an example\ntext message"));
EventHandler eventHandler(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandler.getMarkdownBody(), QStringLiteral("This is an example\ntext message"));
}
void EventHandlerTest::markdownBodyReply()
{
QCOMPARE(EventHandler::markdownBody(room->messageEvents().at(5).get()), QStringLiteral("reply"));
EventHandler eventHandler(room, room->messageEvents().at(5).get());
QCOMPARE(eventHandler.getMarkdownBody(), QStringLiteral("reply"));
}
void EventHandlerTest::subtitle()
{
QCOMPARE(EventHandler::subtitleText(room, room->messageEvents().at(0).get()), QStringLiteral("after: This is an example text message"));
QCOMPARE(EventHandler::subtitleText(room, room->messageEvents().at(2).get()),
QStringLiteral("after: This is a highlight @bob:kde.org and this is a link https://kde.org"));
EventHandler eventHandler(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandler.subtitleText(), QStringLiteral("after: This is an example text message"));
EventHandler eventHandler2(room, room->messageEvents().at(2).get());
QCOMPARE(eventHandler2.subtitleText(), QStringLiteral("after: This is a highlight @bob:kde.org and this is a link https://kde.org"));
}
void EventHandlerTest::nullSubtitle()
{
QTest::ignoreMessage(QtWarningMsg, "subtitleText called with room set to nullptr.");
QCOMPARE(EventHandler::subtitleText(nullptr, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "subtitleText called with event set to nullptr.");
QCOMPARE(EventHandler::subtitleText(room, nullptr), QString());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "subtitleText called with m_event set to nullptr.");
QCOMPARE(noEventHandler.subtitleText(), QString());
}
void EventHandlerTest::mediaInfo()
{
auto event = room->messageEvents().at(4).get();
auto mediaInfo = EventHandler::mediaInfo(room, event);
EventHandler eventHandler(room, event);
auto mediaInfo = eventHandler.getMediaInfo();
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
QCOMPARE(mediaInfo["source"_ls], room->makeMediaUrl(event->id(), QUrl("mxc://kde.org/1234567"_ls)));
@@ -283,42 +320,53 @@ void EventHandlerTest::mediaInfo()
void EventHandlerTest::nullMediaInfo()
{
QTest::ignoreMessage(QtWarningMsg, "mediaInfo called with room set to nullptr.");
QCOMPARE(EventHandler::mediaInfo(nullptr, nullptr), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "getMediaInfo called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getMediaInfo(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "mediaInfo called with event set to nullptr.");
QCOMPARE(EventHandler::mediaInfo(room, nullptr), QVariantMap());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getMediaInfo called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getMediaInfo(), QVariantMap());
}
void EventHandlerTest::hasReply()
{
QCOMPARE(EventHandler::hasReply(room->messageEvents().at(5).get()), true);
QCOMPARE(EventHandler::hasReply(room->messageEvents().at(0).get()), false);
EventHandler eventHandlerReply(room, room->messageEvents().at(5).get());
QCOMPARE(eventHandlerReply.hasReply(), true);
EventHandler eventHandlerNoReply(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoReply.hasReply(), false);
}
void EventHandlerTest::nullHasReply()
{
QTest::ignoreMessage(QtWarningMsg, "hasReply called with event set to nullptr.");
QCOMPARE(EventHandler::hasReply(nullptr), false);
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "hasReply called with m_event set to nullptr.");
QCOMPARE(noEventHandler.hasReply(), false);
}
void EventHandlerTest::replyId()
{
QCOMPARE(EventHandler::replyId(room->messageEvents().at(5).get()), QStringLiteral("$153456789:example.org"));
QCOMPARE(EventHandler::replyId(room->messageEvents().at(0).get()), QStringLiteral(""));
EventHandler eventHandlerReply(room, room->messageEvents().at(5).get());
QCOMPARE(eventHandlerReply.getReplyId(), QStringLiteral("$153456789:example.org"));
EventHandler eventHandlerNoReply(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoReply.getReplyId(), QStringLiteral(""));
}
void EventHandlerTest::nullReplyId()
{
QTest::ignoreMessage(QtWarningMsg, "replyId called with event set to nullptr.");
QCOMPARE(EventHandler::replyId(nullptr), QString());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getReplyId called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyId(), QString());
}
void EventHandlerTest::replyAuthor()
{
auto replyEvent = room->messageEvents().at(0).get();
auto replyAuthor = room->member(replyEvent->senderId());
auto eventHandlerReplyAuthor = EventHandler::replyAuthor(room, room->messageEvents().at(5).get());
EventHandler eventHandler(room, room->messageEvents().at(5).get());
auto eventHandlerReplyAuthor = eventHandler.getReplyAuthor();
QCOMPARE(eventHandlerReplyAuthor.isLocalMember(), replyAuthor.id() == room->localMember().id());
QCOMPARE(eventHandlerReplyAuthor.id(), replyAuthor.id());
@@ -327,58 +375,121 @@ void EventHandlerTest::replyAuthor()
QCOMPARE(eventHandlerReplyAuthor.avatarMediaId(), replyAuthor.avatarMediaId());
QCOMPARE(eventHandlerReplyAuthor.color(), replyAuthor.color());
QCOMPARE(EventHandler::replyAuthor(room, room->messageEvents().at(0).get()), RoomMember());
EventHandler eventHandlerNoAuthor(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoAuthor.getReplyAuthor(), RoomMember());
}
void EventHandlerTest::nullReplyAuthor()
{
QTest::ignoreMessage(QtWarningMsg, "replyAuthor called with room set to nullptr.");
QCOMPARE(EventHandler::replyAuthor(nullptr, nullptr), RoomMember());
QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyAuthor(), RoomMember());
QTest::ignoreMessage(QtWarningMsg, "replyAuthor called with event set to nullptr. Returning empty user.");
QCOMPARE(EventHandler::replyAuthor(room, nullptr), RoomMember());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_event set to nullptr. Returning empty user.");
QCOMPARE(noEventHandler.getReplyAuthor(), RoomMember());
}
void EventHandlerTest::replyBody()
{
EventHandler eventHandler(room, room->messageEvents().at(5).get());
QCOMPARE(eventHandler.getReplyRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
QCOMPARE(eventHandler.getReplyRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
QCOMPARE(eventHandler.getReplyPlainBody(), QStringLiteral("This is an example\ntext message"));
QCOMPARE(eventHandler.getReplyPlainBody(true), QStringLiteral("This is an example text message"));
}
void EventHandlerTest::nullReplyBody()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getReplyRichBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyRichBody(), QString());
QTest::ignoreMessage(QtWarningMsg, "getReplyPlainBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyPlainBody(), QString());
}
void EventHandlerTest::replyMediaInfo()
{
auto event = room->messageEvents().at(6).get();
auto replyEvent = room->messageEvents().at(4).get();
EventHandler eventHandler(room, event);
auto mediaInfo = eventHandler.getReplyMediaInfo();
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
QCOMPARE(mediaInfo["source"_ls], room->makeMediaUrl(replyEvent->id(), QUrl("mxc://kde.org/1234567"_ls)));
QCOMPARE(mediaInfo["mimeType"_ls], QStringLiteral("video/mp4"));
QCOMPARE(mediaInfo["mimeIcon"_ls], QStringLiteral("video-mp4"));
QCOMPARE(mediaInfo["size"_ls], 62650636);
QCOMPARE(mediaInfo["duration"_ls], 10);
QCOMPARE(mediaInfo["width"_ls], 1920);
QCOMPARE(mediaInfo["height"_ls], 1080);
QCOMPARE(thumbnailInfo["source"_ls], room->makeMediaUrl(replyEvent->id(), QUrl("mxc://kde.org/2234567"_ls)));
QCOMPARE(thumbnailInfo["mimeType"_ls], QStringLiteral("image/jpeg"));
QCOMPARE(thumbnailInfo["mimeIcon"_ls], QStringLiteral("image-jpeg"));
QCOMPARE(thumbnailInfo["size"_ls], 382249);
QCOMPARE(thumbnailInfo["width"_ls], 800);
QCOMPARE(thumbnailInfo["height"_ls], 450);
}
void EventHandlerTest::nullReplyMediaInfo()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyMediaInfo called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyMediaInfo(), QVariantMap());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getReplyMediaInfo called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyMediaInfo(), QVariantMap());
}
void EventHandlerTest::thread()
{
QCOMPARE(EventHandler::isThreaded(room->messageEvents().at(0).get()), false);
QCOMPARE(EventHandler::threadRoot(room->messageEvents().at(0).get()), QString());
EventHandler eventHandlerNoThread(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoThread.isThreaded(), false);
QCOMPARE(eventHandlerNoThread.threadRoot(), QString());
QCOMPARE(EventHandler::isThreaded(room->messageEvents().at(9).get()), true);
QCOMPARE(EventHandler::threadRoot(room->messageEvents().at(9).get()), QStringLiteral("$threadroot:example.org"));
QCOMPARE(EventHandler::replyId(room->messageEvents().at(9).get()), QStringLiteral("$threadroot:example.org"));
EventHandler eventHandlerThreadRoot(room, room->messageEvents().at(9).get());
QCOMPARE(eventHandlerThreadRoot.isThreaded(), true);
QCOMPARE(eventHandlerThreadRoot.threadRoot(), QStringLiteral("$threadroot:example.org"));
QCOMPARE(eventHandlerThreadRoot.getReplyId(), QStringLiteral("$threadroot:example.org"));
QCOMPARE(EventHandler::isThreaded(room->messageEvents().at(10).get()), true);
QCOMPARE(EventHandler::threadRoot(room->messageEvents().at(10).get()), QStringLiteral("$threadroot:example.org"));
QCOMPARE(EventHandler::replyId(room->messageEvents().at(10).get()), QStringLiteral("$threadmessage1:example.org"));
EventHandler eventHandlerThreadReply(room, room->messageEvents().at(10).get());
QCOMPARE(eventHandlerThreadReply.isThreaded(), true);
QCOMPARE(eventHandlerThreadReply.threadRoot(), QStringLiteral("$threadroot:example.org"));
QCOMPARE(eventHandlerThreadReply.getReplyId(), QStringLiteral("$threadmessage1:example.org"));
}
void EventHandlerTest::nullThread()
{
QTest::ignoreMessage(QtWarningMsg, "isThreaded called with event set to nullptr.");
QCOMPARE(EventHandler::isThreaded(nullptr), false);
QTest::ignoreMessage(QtWarningMsg, "isThreaded called with m_event set to nullptr.");
QCOMPARE(emptyHandler.isThreaded(), false);
QTest::ignoreMessage(QtWarningMsg, "threadRoot called with event set to nullptr.");
QCOMPARE(EventHandler::threadRoot(nullptr), QString());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "threadRoot called with m_event set to nullptr.");
QCOMPARE(noEventHandler.threadRoot(), QString());
}
void EventHandlerTest::location()
{
QCOMPARE(EventHandler::latitude(room->messageEvents().at(7).get()), QStringLiteral("51.7035").toFloat());
QCOMPARE(EventHandler::longitude(room->messageEvents().at(7).get()), QStringLiteral("-1.14394").toFloat());
QCOMPARE(EventHandler::locationAssetType(room->messageEvents().at(7).get()), QStringLiteral("m.pin"));
EventHandler eventHandler(room, room->messageEvents().at(7).get());
QCOMPARE(eventHandler.getLatitude(), QStringLiteral("51.7035").toFloat());
QCOMPARE(eventHandler.getLongitude(), QStringLiteral("-1.14394").toFloat());
QCOMPARE(eventHandler.getLocationAssetType(), QStringLiteral("m.pin"));
}
void EventHandlerTest::nullLocation()
{
QTest::ignoreMessage(QtWarningMsg, "latitude called with event set to nullptr.");
QCOMPARE(EventHandler::latitude(nullptr), -100.0);
QTest::ignoreMessage(QtWarningMsg, "getLatitude called with m_event set to nullptr.");
QCOMPARE(emptyHandler.getLatitude(), -100.0);
QTest::ignoreMessage(QtWarningMsg, "longitude called with event set to nullptr.");
QCOMPARE(EventHandler::longitude(nullptr), -200.0);
QTest::ignoreMessage(QtWarningMsg, "getLongitude called with m_event set to nullptr.");
QCOMPARE(emptyHandler.getLongitude(), -200.0);
QTest::ignoreMessage(QtWarningMsg, "locationAssetType called with event set to nullptr.");
QCOMPARE(EventHandler::locationAssetType(nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "getLocationAssetType called with m_event set to nullptr.");
QCOMPARE(emptyHandler.getLocationAssetType(), QString());
}
QTEST_MAIN(EventHandlerTest)

View File

@@ -46,7 +46,6 @@ private Q_SLOTS:
void sendCustomEmojiCode_data();
void sendCustomEmojiCode();
void receiveSpacelessSelfClosingTag();
void receiveStripReply();
void receivePlainTextIn();
@@ -253,19 +252,6 @@ void TextHandlerTest::sendCustomEmojiCode()
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::receiveSpacelessSelfClosingTag()
{
const QString testInputString = QStringLiteral("Test...<br/>...ing");
const QString testRichOutputString = QStringLiteral("Test...<br/>...ing");
const QString testPlainOutputString = QStringLiteral("Test...\n...ing");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testRichOutputString);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testPlainOutputString);
}
void TextHandlerTest::receiveStripReply()
{
const QString testInputString = QStringLiteral(
@@ -288,7 +274,6 @@ void TextHandlerTest::receiveRichInPlainOut_data()
QTest::newRow("ampersand") << QStringLiteral("a &amp; b") << QStringLiteral("a & b");
QTest::newRow("quote") << QStringLiteral("&quot;a and b&quot;") << QStringLiteral("\"a and b\"");
QTest::newRow("new line") << QStringLiteral("new<br>line") << QStringLiteral("new\nline");
QTest::newRow("unescape") << QStringLiteral("can&#x27;t") << QStringLiteral("can't");
}
void TextHandlerTest::receiveRichInPlainOut()
@@ -463,9 +448,6 @@ void TextHandlerTest::receiveRichPlainUrl()
QString testOutputStringMxId = QStringLiteral(
"<b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a></b>");
QString testInputStringMxIdWithPrefix = QStringLiteral("a @user:kde.org b");
QString testOutputStringMxIdWithPrefix = QStringLiteral("a <b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> b");
TextHandler testTextHandler;
testTextHandler.setData(testInputStringLink1);
@@ -479,9 +461,6 @@ void TextHandlerTest::receiveRichPlainUrl()
testTextHandler.setData(testInputStringMxId);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxId);
testTextHandler.setData(testInputStringMxIdWithPrefix);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxIdWithPrefix);
}
void TextHandlerTest::receiveRichEdited_data()
@@ -556,7 +535,7 @@ void TextHandlerTest::componentOutput_data()
"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<br/>"), {}},
MessageComponent{MessageComponentType::Text, QStringLiteral("Ah, you mean something like"), {}},
MessageComponent{
MessageComponentType::Code,
QStringLiteral(

View File

@@ -17,6 +17,7 @@ class WindowControllerTest : public QObject
private Q_SLOTS:
void nullWindow();
void geometry();
void showAndRaise();
void toggle();
@@ -29,10 +30,32 @@ void WindowControllerTest::nullWindow()
auto &instance = WindowController::instance();
QCOMPARE(instance.window(), nullptr);
instance.restoreGeometry();
instance.saveGeometry();
instance.showAndRaiseWindow({});
instance.toggleWindow();
}
void WindowControllerTest::geometry()
{
auto &instance = WindowController::instance();
QWindow window;
window.setGeometry(0, 0, 200, 200);
instance.setWindow(&window);
QCOMPARE(instance.window(), &window);
instance.saveGeometry();
const auto stateConfig = KSharedConfig::openStateConfig();
KConfigGroup windowGroup = stateConfig->group(QStringLiteral("Window"));
QCOMPARE(KWindowConfig::hasSavedWindowSize(windowGroup), true);
window.setGeometry(0, 0, 400, 400);
QCOMPARE(window.geometry(), QRect(0, 0, 400, 400));
instance.restoreGeometry();
QCOMPARE(window.geometry(), QRect(0, 0, 200, 200));
}
void WindowControllerTest::showAndRaise()
{
auto &instance = WindowController::instance();

View File

@@ -429,7 +429,6 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="24.08.1" date="2024-09-12"/>
<release version="24.08.0" date="2024-08-22"/>
<release version="24.05.2" date="2024-07-04"/>
<release version="24.05.1" date="2024-06-13"/>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

@@ -192,8 +192,6 @@ add_library(neochat STATIC
models/readmarkermodel.h
neochatroommember.cpp
neochatroommember.h
models/threadmodel.cpp
models/threadmodel.h
)
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
@@ -242,9 +240,11 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/EditMenu.qml
qml/MessageDelegateContextMenu.qml
qml/FileDelegateContextMenu.qml
qml/FileDelegateContextMenuMobile.qml
qml/MessageSourceSheet.qml
qml/ReportSheet.qml
qml/ConfirmEncryptionDialog.qml
qml/RemoveSheet.qml
qml/BanSheet.qml
qml/RoomSearchPage.qml
qml/LocationChooser.qml
qml/TimelineView.qml
@@ -288,8 +288,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/ConsentDialog.qml
qml/AskDirectChatConfirmation.qml
qml/HoverLinkIndicator.qml
qml/AvatarNotification.qml
qml/ReasonDialog.qml
DEPENDENCIES
QtCore
QtQuick
@@ -307,7 +305,7 @@ add_subdirectory(devtools)
add_subdirectory(login)
add_subdirectory(chatbar)
if(NOT ANDROID AND NOT WIN32)
if(UNIX)
qt_target_qml_sources(neochat QML_FILES qml/ShareAction.qml)
else()
set_source_files_properties(qml/ShareActionStub.qml PROPERTIES
@@ -427,7 +425,7 @@ if (TARGET KF6::Crash)
target_link_libraries(neochat PUBLIC KF6::Crash)
endif()
kconfig_target_kcfg_file(neochat FILE neochatconfig.kcfg CLASS_NAME NeoChatConfig MUTATORS GENERATE_PROPERTIES DEFAULT_VALUE_GETTERS PARENT_IN_CONSTRUCTOR SINGLETON GENERATE_MOC QML_REGISTRATION)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
if(NEOCHAT_FLATPAK)
target_compile_definitions(neochat PUBLIC NEOCHAT_FLATPAK)

View File

@@ -56,7 +56,7 @@ void ActionsHandler::handleMessageEvent(ChatBarCache *chatBarCache)
QString handledText = chatBarCache->text();
handledText = handleMentions(handledText, chatBarCache->mentions());
handleMessage(chatBarCache->text(), handledText, chatBarCache);
handleMessage(m_room->mainCache()->text(), handledText, chatBarCache);
}
QString ActionsHandler::handleMentions(QString handledText, QList<Mention> *mentions)

View File

@@ -219,7 +219,7 @@ QQC2.Control {
}
onTextChanged: {
if (!repeatTimer.running && NeoChatConfig.typingNotifications) {
if (!repeatTimer.running && Config.typingNotifications) {
var textExists = text.length > 0;
root.currentRoom.sendTypingNotification(textExists);
textExists ? repeatTimer.start() : repeatTimer.stop();
@@ -353,8 +353,8 @@ QQC2.Control {
startBreakpoint: Kirigami.Units.gridUnit * 46
endBreakpoint: Kirigami.Units.gridUnit * 66
startPercentWidth: 100
endPercentWidth: NeoChatConfig.compactLayout ? 100 : 85
maxWidth: NeoChatConfig.compactLayout ? -1 : Kirigami.Units.gridUnit * 60
endPercentWidth: Config.compactLayout ? 100 : 85
maxWidth: Config.compactLayout ? -1 : Kirigami.Units.gridUnit * 60
parentWidth: root.width
}
@@ -395,7 +395,7 @@ QQC2.Control {
repeatTimer.stop();
root.currentRoom.markAllMessagesAsRead();
textField.clear();
_private.chatBarCache.clearRelations();
_private.chatBarCache.replyId = "";
messageSent();
}

View File

@@ -119,7 +119,8 @@ QString ChatBarCache::relationMessage() const
}
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
return EventHandler::markdownBody(&**event);
EventHandler eventhandler(room, &**event);
return eventhandler.getMarkdownBody();
}
return {};
}
@@ -139,8 +140,8 @@ void ChatBarCache::setThreadId(const QString &threadId)
if (m_threadId == threadId) {
return;
}
const auto oldThreadId = std::exchange(m_threadId, threadId);
Q_EMIT threadIdChanged(oldThreadId, m_threadId);
m_threadId = threadId;
Q_EMIT threadIdChanged();
}
QString ChatBarCache::attachmentPath() const
@@ -160,16 +161,6 @@ void ChatBarCache::setAttachmentPath(const QString &attachmentPath)
Q_EMIT relationIdChanged(oldEventId, m_relationId);
}
void ChatBarCache::clearRelations()
{
const auto oldEventId = std::exchange(m_relationId, QString());
const auto oldThreadId = std::exchange(m_threadId, QString());
m_attachmentPath = QString();
Q_EMIT relationIdChanged(oldEventId, m_relationId);
Q_EMIT threadIdChanged(oldThreadId, m_threadId);
Q_EMIT attachmentPathChanged();
}
QList<Mention> *ChatBarCache::mentions()
{
return &m_mentions;

View File

@@ -33,7 +33,7 @@ struct Mention {
* A class to cache data from a chat bar.
*
* A chat bar can be anything that allows users to compose or edit message, it doesn't
* necessarily have to use the ChatBar component, e.g. ChatBarComponent.
* necessarily have to use the ChatBar component, e.g. MessageEditComponent.
*
* This object is intended to allow the current contents of a chat bar to be cached
* between different rooms, i.e. there is an expectation that each NeoChatRoom could
@@ -43,7 +43,7 @@ struct Mention {
* as it's parent. This is necessary for certain functions which need to get
* relevant room information.
*
* @sa ChatBar, ChatBarComponent, NeoChatRoom
* @sa ChatBar, MessageEditComponent, NeoChatRoom
*/
class ChatBarCache : public QObject
{
@@ -165,13 +165,6 @@ public:
QString attachmentPath() const;
void setAttachmentPath(const QString &attachmentPath);
/**
* @brief Clear all relations in the cache.
*
* This includes relation ID, thread root ID and attachment path.
*/
Q_INVOKABLE void clearRelations();
/**
* @brief Retrieve the mentions for the current chat bar text.
*/
@@ -195,7 +188,7 @@ public:
Q_SIGNALS:
void textChanged();
void relationIdChanged(const QString &oldEventId, const QString &newEventId);
void threadIdChanged(const QString &oldThreadId, const QString &newThreadId);
void threadIdChanged();
void attachmentPathChanged();
private:

View File

@@ -26,10 +26,23 @@ void ColorSchemer::apply(int idx)
c->activateScheme(c->model()->index(idx, 0));
}
int ColorSchemer::indexForCurrentScheme()
void ColorSchemer::apply(const QString &name)
{
return -1;
// return c->indexForSchemeId(c->activeSchemeId()).row();
c->activateScheme(c->indexForScheme(name));
}
int ColorSchemer::indexForScheme(const QString &name) const
{
auto index = c->indexForScheme(name).row();
if (index == -1) {
index = 0;
}
return index;
}
QString ColorSchemer::nameForIndex(int index) const
{
return c->model()->data(c->model()->index(index, 0), Qt::DisplayRole).toString();
}
#include "moc_colorschemer.cpp"

View File

@@ -44,11 +44,21 @@ public:
Q_INVOKABLE void apply(int idx);
/**
* @brief Get the row for the current color scheme.
* @brief Activates the KColorScheme with the given name.
*
* @sa KColorScheme
*/
Q_INVOKABLE int indexForCurrentScheme();
Q_INVOKABLE void apply(const QString &name);
/**
* @brief Returns the index for the scheme with the given name.
*/
Q_INVOKABLE int indexForScheme(const QString &name) const;
/**
* @brief Returns the name for the scheme with the given index.
*/
Q_INVOKABLE QString nameForIndex(int index) const;
private:
KColorSchemeManager *c;

View File

@@ -13,11 +13,13 @@
#include <signal.h>
#include <Quotient/accountregistry.h>
#include <Quotient/csapi/notifications.h>
#include <Quotient/qt_connection_util.h>
#include <Quotient/settings.h>
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include "notificationsmanager.h"
#include "proxycontroller.h"
@@ -62,11 +64,7 @@ Controller::Controller(QObject *parent)
});
} else {
auto c = new NeoChatConnection(this);
#if Quotient_VERSION_MINOR > 8
c->assumeIdentity(QStringLiteral("@user:localhost:1234"), QStringLiteral("device_1234"), QStringLiteral("token_1234"));
#else
c->assumeIdentity(QStringLiteral("@user:localhost:1234"), QStringLiteral("token_1234"));
#endif
connect(c, &Connection::connected, this, [c, this]() {
m_accountRegistry.add(c);
c->syncLoop();
@@ -204,33 +202,15 @@ void Controller::invokeLogin()
m_connectionsLoading[accountId] = connection;
connect(connection, &NeoChatConnection::connected, this, [this, connection, accountId] {
connection->loadState();
if (connection->allRooms().size() == 0 || connection->allRooms()[0]->currentState().get<RoomCreateEvent>()) {
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
m_connectionsLoading.remove(accountId);
Q_EMIT accountsLoadingChanged();
} else {
connect(
connection->allRooms()[0],
&Room::baseStateLoaded,
this,
[this, connection, accountId]() {
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
m_connectionsLoading.remove(accountId);
Q_EMIT accountsLoadingChanged();
},
Qt::SingleShotConnection);
}
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
m_connectionsLoading.remove(accountId);
Q_EMIT accountsLoadingChanged();
});
connect(connection, &NeoChatConnection::networkError, this, [this](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured(i18n("Network Error: %1", error), {});
});
#if Quotient_VERSION_MINOR > 8
connection->assumeIdentity(account.userId(), account.deviceId(), accessToken);
#else
connection->assumeIdentity(account.userId(), accessToken);
#endif
});
}
}
@@ -271,19 +251,23 @@ QKeychain::ReadPasswordJob *Controller::loadAccessTokenFromKeyChain(const QStrin
return job;
}
void Controller::saveAccessTokenToKeyChain(const QString &userId, const QByteArray &accessToken)
bool Controller::saveAccessTokenToKeyChain(const QString &userId, const QByteArray &accessToken)
{
qDebug() << "Save the access token to the keychain for " << userId;
auto job = new QKeychain::WritePasswordJob(qAppName());
job->setAutoDelete(true);
job->setKey(userId);
job->setBinaryData(accessToken);
connect(job, &QKeychain::WritePasswordJob::finished, this, [job]() {
if (job->error()) {
qWarning() << "Could not save access token to the keychain: " << qPrintable(job->errorString());
}
});
job->start();
QKeychain::WritePasswordJob job(qAppName());
job.setAutoDelete(false);
job.setKey(userId);
job.setBinaryData(accessToken);
QEventLoop loop;
QKeychain::WritePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
if (job.error()) {
qWarning() << "Could not save access token to the keychain: " << qPrintable(job.errorString());
return false;
}
return true;
}
bool Controller::supportSystemTray() const
@@ -425,28 +409,11 @@ void Controller::removeConnection(const QString &userId)
bool Controller::csSupported() const
{
#if Quotient_VERSION_MINOR > 8
#if Quotient_VERSION_MINOR > 9
return true;
#else
return false;
#endif
}
void Controller::revertToDefaultConfig()
{
const auto config = NeoChatConfig::self();
config->setDefaults();
config->save();
}
bool Controller::isImageShown(const QString &eventId)
{
return m_shownImages.contains(eventId);
}
void Controller::markImageShown(const QString &eventId)
{
m_shownImages.append(eventId);
}
#include "moc_controller.cpp"

View File

@@ -86,7 +86,7 @@ public:
/**
* @brief Save an access token to the keychain for the given account.
*/
void saveAccessTokenToKeyChain(const QString &userId, const QByteArray &accessToken);
bool saveAccessTokenToKeyChain(const QString &userId, const QByteArray &accessToken);
[[nodiscard]] bool supportSystemTray() const;
@@ -108,17 +108,6 @@ public:
bool csSupported() const;
/**
* @brief Revert all configuration values to their default.
*
* The parameters along with their defaults are specified in the config file
* neochatconfig.kcfg.
*/
Q_INVOKABLE void revertToDefaultConfig();
Q_INVOKABLE bool isImageShown(const QString &eventId);
Q_INVOKABLE void markImageShown(const QString &eventId);
private:
explicit Controller(QObject *parent = nullptr);
@@ -127,11 +116,13 @@ private:
QKeychain::ReadPasswordJob *loadAccessTokenFromKeyChain(const QString &account);
void loadSettings();
void saveSettings() const;
Quotient::AccountRegistry m_accountRegistry;
QStringList m_accountsLoading;
QMap<QString, QPointer<NeoChatConnection>> m_connectionsLoading;
QString m_endpoint;
QStringList m_shownImages;
private Q_SLOTS:
void invokeLogin();

View File

@@ -17,25 +17,25 @@ FormCard.FormCardPage {
FormCard.FormCheckDelegate {
text: i18nc("@option:check", "Show hidden events in the timeline")
checked: NeoChatConfig.showAllEvents
checked: Config.showAllEvents
onToggled: NeoChatConfig.showAllEvents = checked
onToggled: Config.showAllEvents = checked
}
FormCard.FormCheckDelegate {
id: roomAccountDataVisibleCheck
text: i18nc("@option:check Enable the matrix 'threads' feature", "Always allow device verification")
description: i18n("Allow the user to start a verification session with devices that were already verified")
checked: NeoChatConfig.alwaysVerifyDevice
checked: Config.alwaysVerifyDevice
onToggled: NeoChatConfig.alwaysVerifyDevice = checked
onToggled: Config.alwaysVerifyDevice = checked
}
FormCard.FormCheckDelegate {
text: i18nc("@option:check", "Show focus in window header")
checked: NeoChatConfig.windowTitleFocus
checked: Config.windowTitleFocus
onToggled: {
NeoChatConfig.windowTitleFocus = checked;
NeoChatConfig.save();
Config.windowTitleFocus = checked;
Config.save();
}
}
}

View File

@@ -18,23 +18,23 @@ FormCard.FormCardPage {
FormCard.FormCheckDelegate {
id: roomAccountDataVisibleCheck
text: i18nc("@option:check Enable the matrix 'threads' feature", "Threads")
checked: NeoChatConfig.threads
checked: Config.threads
onToggled: NeoChatConfig.threads = checked
onToggled: Config.threads = checked
}
FormCard.FormCheckDelegate {
text: i18nc("@option:check Enable the matrix 'secret backup' feature", "Secret Backup")
checked: NeoChatConfig.secretBackup
checked: Config.secretBackup
onToggled: NeoChatConfig.secretBackup = checked
onToggled: Config.secretBackup = checked
}
FormCard.FormCheckDelegate {
text: i18nc("@option:check Enable the matrix feature to add a phone number as a third party ID", "Add phone numbers as 3PIDs")
checked: NeoChatConfig.phone3PId
checked: Config.phone3PId
onToggled: {
NeoChatConfig.phone3PId = checked
NeoChatConfig.save();
Config.phone3PId = checked
Config.save();
}
}
}

View File

@@ -38,8 +38,6 @@ public:
ReadMarker, /**< The local user read marker. */
Loading, /**< A delegate to tell the user more messages are being loaded. */
TimelineEnd, /**< A delegate to inform that all messages are loaded. */
Predecessor, /**< A delegate to show a room predecessor. */
Successor, /**< A delegate to show a room successor. */
Other, /**< Anything that cannot be classified as another type. */
};
Q_ENUM(Type);

View File

@@ -50,7 +50,7 @@ public:
Reply, /**< A component to show a replied-to message. */
LinkPreview, /**< A preview of a URL in the message. */
LinkPreviewLoad, /**< A loading dialog for a link preview. */
ChatBar, /**< A text edit for editing a message. */
Edit, /**< A text edit for editing a message. */
Verification, /**< A user verification session start message. */
Loading, /**< The component is loading. */
Other, /**< Anything that cannot be classified as another type. */

View File

@@ -5,17 +5,16 @@
#include <QMovie>
#include <KFormat>
#include <KLocalizedString>
#include <Quotient/eventitem.h>
#include <Quotient/events/encryptionevent.h>
#include <Quotient/events/event.h>
#include <Quotient/events/reactionevent.h>
#include <Quotient/events/redactionevent.h>
#include <Quotient/events/roomavatarevent.h>
#include <Quotient/events/roomcanonicalaliasevent.h>
#include <Quotient/events/roomevent.h>
#include <Quotient/events/roommemberevent.h>
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/roompowerlevelsevent.h>
#include <Quotient/events/simplestateevents.h>
#include <Quotient/events/stickerevent.h>
@@ -26,6 +25,9 @@
#include "events/locationbeaconevent.h"
#include "events/pollevent.h"
#include "events/widgetevent.h"
#include "linkpreviewer.h"
#include "messagecomponenttype.h"
#include "models/reactionmodel.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "texthandler.h"
@@ -33,52 +35,68 @@
using namespace Quotient;
QString EventHandler::id(const Quotient::RoomEvent *event)
EventHandler::EventHandler(const NeoChatRoom *room, const RoomEvent *event)
: m_room(room)
, m_event(event)
{
if (event == nullptr) {
qCWarning(EventHandling) << "id called with event set to nullptr.";
return {};
}
return !event->id().isEmpty() ? event->id() : event->transactionId();
}
QString EventHandler::authorDisplayName(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending)
QString EventHandler::getId() const
{
if (room == nullptr) {
qCWarning(EventHandling) << "authorDisplayName called with room set to nullptr.";
return {};
}
if (event == nullptr) {
qCWarning(EventHandling) << "authorDisplayName called with event set to nullptr.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "getId called with m_event set to nullptr.";
return {};
}
if (is<RoomMemberEvent>(*event) && !event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].isNull()
&& event->stateKey() == event->senderId()) {
auto previousDisplayName = event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].toString().toHtmlEscaped();
return !m_event->id().isEmpty() ? m_event->id() : m_event->transactionId();
}
MessageComponentType::Type EventHandler::messageComponentType() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "messageComponentType called with m_event set to nullptr.";
return MessageComponentType::Other;
}
return MessageComponentType::typeForEvent(*m_event);
}
QString EventHandler::getAuthorDisplayName(bool isPending) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getAuthorDisplayName called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getAuthorDisplayName called with m_event set to nullptr.";
return {};
}
if (is<RoomMemberEvent>(*m_event) && !m_event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].isNull()
&& m_event->stateKey() == m_event->senderId()) {
auto previousDisplayName = m_event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].toString().toHtmlEscaped();
if (previousDisplayName.isEmpty()) {
previousDisplayName = event->senderId();
previousDisplayName = m_event->senderId();
}
return previousDisplayName;
} else {
const auto author = isPending ? room->localMember() : room->member(event->senderId());
const auto author = isPending ? m_room->localMember() : m_room->member(m_event->senderId());
return author.htmlSafeDisplayName();
}
}
QString EventHandler::singleLineAuthorDisplayname(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending)
QString EventHandler::singleLineAuthorDisplayname(bool isPending) const
{
if (room == nullptr) {
qCWarning(EventHandling) << "singleLineAuthorDisplayname called with room set to nullptr.";
if (m_room == nullptr) {
qCWarning(EventHandling) << "getAuthorDisplayName called with m_room set to nullptr.";
return {};
}
if (event == nullptr) {
qCWarning(EventHandling) << "singleLineAuthorDisplayname called with event set to nullptr.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "getAuthorDisplayName called with m_event set to nullptr.";
return {};
}
const auto author = isPending ? room->localMember() : room->member(event->senderId());
const auto author = isPending ? m_room->localMember() : m_room->member(m_event->senderId());
auto displayName = author.displayName();
displayName.replace(QStringLiteral("<br>\n"), QStringLiteral(" "));
displayName.replace(QStringLiteral("<br>"), QStringLiteral(" "));
@@ -89,10 +107,10 @@ QString EventHandler::singleLineAuthorDisplayname(const NeoChatRoom *room, const
return displayName;
}
QDateTime EventHandler::time(const Quotient::RoomEvent *event, bool isPending, QDateTime lastUpdated)
QDateTime EventHandler::getTime(bool isPending, QDateTime lastUpdated) const
{
if (event == nullptr) {
qCWarning(EventHandling) << "time called with event set to nullptr.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "getTime called with m_event set to nullptr.";
return {};
}
if (isPending && lastUpdated == QDateTime()) {
@@ -100,16 +118,24 @@ QDateTime EventHandler::time(const Quotient::RoomEvent *event, bool isPending, Q
return {};
}
return isPending ? lastUpdated : event->originTimestamp();
return isPending ? lastUpdated : m_event->originTimestamp();
}
QString EventHandler::timeString(const Quotient::RoomEvent *event, bool relative, QLocale::FormatType format, bool isPending, QDateTime lastUpdated)
QString EventHandler::getTimeString(bool relative, QLocale::FormatType format, bool isPending, QDateTime lastUpdated) const
{
auto ts = time(event, isPending, lastUpdated);
if (m_event == nullptr) {
qCWarning(EventHandling) << "getTimeString called with m_event set to nullptr.";
return {};
}
if (isPending && lastUpdated == QDateTime()) {
qCWarning(EventHandling) << "a value must be provided for lastUpdated for a pending event.";
return {};
}
auto ts = getTime(isPending, lastUpdated);
if (ts.isValid()) {
if (relative) {
KFormat formatter;
return formatter.formatRelativeDate(ts.toLocalTime().date(), format);
return m_format.formatRelativeDate(ts.toLocalTime().date(), format);
} else {
return QLocale().toString(ts.toLocalTime().time(), format);
}
@@ -117,45 +143,44 @@ QString EventHandler::timeString(const Quotient::RoomEvent *event, bool relative
return {};
}
QString EventHandler::timeString(const Quotient::RoomEvent *event, const QString &format, bool isPending, const QDateTime &lastUpdated)
QString EventHandler::getTimeString(const QString &format, bool isPending, const QDateTime &lastUpdated)
{
return time(event, isPending, lastUpdated).toLocalTime().toString(format);
return getTime(isPending, lastUpdated).toLocalTime().toString(format);
}
bool EventHandler::isHighlighted(const NeoChatRoom *room, const Quotient::RoomEvent *event)
bool EventHandler::isHighlighted()
{
if (room == nullptr) {
qCWarning(EventHandling) << "isHighlighted called with room set to nullptr.";
if (m_room == nullptr) {
qCWarning(EventHandling) << "isHighlighted called with m_room set to nullptr.";
return false;
}
if (event == nullptr) {
qCWarning(EventHandling) << "isHighlighted called with event set to nullptr.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "isHighlighted called with m_event set to nullptr.";
return false;
}
return !room->isDirectChat() && room->isEventHighlighted(event);
return !m_room->isDirectChat() && m_room->isEventHighlighted(m_event);
}
bool EventHandler::isHidden(const NeoChatRoom *room, const Quotient::RoomEvent *event)
bool EventHandler::isHidden()
{
if (room == nullptr) {
qCWarning(EventHandling) << "isHidden called with room set to nullptr.";
if (m_room == nullptr) {
qCWarning(EventHandling) << "isHidden called with m_room set to nullptr.";
return false;
}
if (event == nullptr) {
qCWarning(EventHandling) << "isHidden called with event set to nullptr.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "isHidden called with m_event set to nullptr.";
return false;
}
if (event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
if (m_event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
return true;
}
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(event)) {
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(m_event)) {
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
return true;
} else if (roomMemberEvent->isRename() && roomMemberEvent->prevContent() && roomMemberEvent->prevContent()->membership == roomMemberEvent->membership()
&& !NeoChatConfig::self()->showRename()) {
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
return true;
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
&& !NeoChatConfig::self()->showAvatarUpdate()) {
@@ -163,33 +188,33 @@ bool EventHandler::isHidden(const NeoChatRoom *room, const Quotient::RoomEvent *
}
}
if (event->isStateEvent() && eventCast<const StateEvent>(event)->repeatsState()) {
if (m_event->isStateEvent() && eventCast<const StateEvent>(m_event)->repeatsState()) {
return true;
}
// isReplacement?
if (auto e = eventCast<const RoomMessageEvent>(event)) {
if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
if (!e->replacedEvent().isEmpty()) {
return true;
}
}
if (is<RedactionEvent>(*event) || is<ReactionEvent>(*event)) {
if (is<RedactionEvent>(*m_event) || is<ReactionEvent>(*m_event)) {
return true;
}
if (auto e = eventCast<const RoomMessageEvent>(event)) {
if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
if (!e->replacedEvent().isEmpty() && e->replacedEvent() != e->id()) {
return true;
}
}
if (room->connection()->isIgnored(event->senderId())) {
if (m_room->connection()->isIgnored(m_event->senderId())) {
return true;
}
// hide ending live location beacons
if (event->isStateEvent() && event->matrixType() == "org.matrix.msc3672.beacon_info"_ls && !event->contentJson()["live"_ls].toBool()) {
if (m_event->isStateEvent() && m_event->matrixType() == "org.matrix.msc3672.beacon_info"_ls && !m_event->contentJson()["live"_ls].toBool()) {
return true;
}
@@ -228,52 +253,44 @@ QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event)
return body;
}
QString EventHandler::richBody(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool stripNewlines)
QString EventHandler::getRichBody(bool stripNewlines) const
{
if (room == nullptr) {
qCWarning(EventHandling) << "richBody called with room set to nullptr.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "getRichBody called with m_event set to nullptr.";
return {};
}
if (event == nullptr) {
qCWarning(EventHandling) << "richBody called with event set to nullptr.";
return {};
}
return getBody(room, event, Qt::RichText, stripNewlines);
return getBody(m_event, Qt::RichText, stripNewlines);
}
QString EventHandler::plainBody(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool stripNewlines)
QString EventHandler::getPlainBody(bool stripNewlines) const
{
if (room == nullptr) {
qCWarning(EventHandling) << "plainBody called with room set to nullptr.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "getPlainBody called with m_event set to nullptr.";
return {};
}
if (event == nullptr) {
qCWarning(EventHandling) << "plainBody called with event set to nullptr.";
return {};
}
return getBody(room, event, Qt::PlainText, stripNewlines);
return getBody(m_event, Qt::PlainText, stripNewlines);
}
QString EventHandler::markdownBody(const Quotient::RoomEvent *event)
QString EventHandler::getMarkdownBody() const
{
if (event == nullptr) {
qCWarning(EventHandling) << "markdownBody called with event set to nullptr.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "getMarkdownBody called with m_event set to nullptr.";
return {};
}
if (!event->is<RoomMessageEvent>()) {
qCWarning(EventHandling) << "markdownBody called when event isn't a RoomMessageEvent.";
if (!m_event->is<RoomMessageEvent>()) {
qCWarning(EventHandling) << "getMarkdownBody called when m_event isn't a RoomMessageEvent.";
return {};
}
const auto roomMessageEvent = eventCast<const RoomMessageEvent>(event);
const auto roomMessageEvent = eventCast<const RoomMessageEvent>(m_event);
QString plainBody = roomMessageEvent->plainBody();
plainBody.remove(TextRegex::removeReply);
return plainBody;
}
QString EventHandler::getBody(const NeoChatRoom *room, const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines)
QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const
{
if (event->isRedacted()) {
auto reason = event->redactedBecause()->reason();
@@ -284,15 +301,15 @@ QString EventHandler::getBody(const NeoChatRoom *room, const Quotient::RoomEvent
return switchOnType(
*event,
[room, format, stripNewlines](const RoomMessageEvent &event) {
return getMessageBody(room, event, format, stripNewlines);
[this, format, stripNewlines](const RoomMessageEvent &event) {
return getMessageBody(event, format, stripNewlines);
},
[](const StickerEvent &e) {
return e.body();
},
[room, prettyPrint](const RoomMemberEvent &e) {
[this, prettyPrint](const RoomMemberEvent &e) {
// FIXME: Rewind to the name that was at the time of this event
auto subjectName = room->member(e.userId()).htmlSafeDisplayName();
auto subjectName = m_room->member(e.userId()).htmlSafeDisplayName();
if (e.membership() == Membership::Leave) {
if (e.prevContent() && e.prevContent()->displayName) {
subjectName = sanitized(*e.prevContent()->displayName);
@@ -304,7 +321,7 @@ QString EventHandler::getBody(const NeoChatRoom *room, const Quotient::RoomEvent
if (prettyPrint) {
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
.arg(e.userId(), room->member(e.userId()).color().name(), subjectName);
.arg(e.userId(), m_room->member(e.userId()).color().name(), subjectName);
}
// The below code assumes senderName output in AuthorRole
@@ -367,13 +384,9 @@ QString EventHandler::getBody(const NeoChatRoom *room, const Quotient::RoomEvent
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
return (e.senderId() != e.userId()) ? i18n("unbanned %1", subjectName) : i18n("self-unbanned");
}
if (e.senderId() == e.userId()) {
return i18n("left the room");
}
if (const auto &reason = e.contentJson()["reason"_ls].toString().toHtmlEscaped(); !reason.isEmpty()) {
return i18n("has put %1 out of the room: %2", subjectName, reason);
}
return i18n("has put %1 out of the room", subjectName);
return (e.senderId() != e.userId())
? i18n("has put %1 out of the room: %2", subjectName, e.contentJson()["reason"_ls].toString().toHtmlEscaped())
: i18n("left the room");
case Membership::Ban:
if (e.senderId() != e.userId()) {
if (e.reason().isEmpty()) {
@@ -444,7 +457,7 @@ QString EventHandler::getBody(const NeoChatRoom *room, const Quotient::RoomEvent
i18n("Unknown event"));
}
QString EventHandler::getMessageBody(const NeoChatRoom *room, const RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines)
QString EventHandler::getMessageBody(const RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const
{
TextHandler textHandler;
@@ -476,24 +489,24 @@ QString EventHandler::getMessageBody(const NeoChatRoom *room, const RoomMessageE
}
if (format == Qt::RichText) {
return textHandler.handleRecieveRichText(inputFormat, room, &event, stripNewlines, event.isReplaced());
return textHandler.handleRecieveRichText(inputFormat, m_room, &event, stripNewlines, event.isReplaced());
} else {
return textHandler.handleRecievePlainText(inputFormat, stripNewlines);
}
}
QString EventHandler::genericBody(const Quotient::RoomEvent *event)
QString EventHandler::getGenericBody() const
{
if (event == nullptr) {
qCWarning(EventHandling) << "genericBody called with event set to nullptr.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "getGenericBody called with m_event set to nullptr.";
return {};
}
if (event->isRedacted()) {
if (m_event->isRedacted()) {
return i18n("<i>[This message was deleted]</i>");
}
return switchOnType(
*event,
*m_event,
[](const RoomMessageEvent &e) {
Q_UNUSED(e)
return i18n("sent a message");
@@ -613,33 +626,29 @@ QString EventHandler::genericBody(const Quotient::RoomEvent *event)
i18n("Unknown event"));
}
QString EventHandler::subtitleText(const NeoChatRoom *room, const Quotient::RoomEvent *event)
QString EventHandler::subtitleText() const
{
if (room == nullptr) {
qCWarning(EventHandling) << "subtitleText called with room set to nullptr.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "subtitleText called with m_event set to nullptr.";
return {};
}
if (event == nullptr) {
qCWarning(EventHandling) << "subtitleText called with event set to nullptr.";
return {};
}
return singleLineAuthorDisplayname(room, event) + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": ")) + plainBody(room, event, true);
return singleLineAuthorDisplayname() + (m_event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": ")) + getPlainBody(true);
}
QVariantMap EventHandler::mediaInfo(const NeoChatRoom *room, const Quotient::RoomEvent *event)
QVariantMap EventHandler::getMediaInfo() const
{
if (room == nullptr) {
qCWarning(EventHandling) << "mediaInfo called with room set to nullptr.";
if (m_room == nullptr) {
qCWarning(EventHandling) << "getMediaInfo called with m_room set to nullptr.";
return {};
}
if (event == nullptr) {
qCWarning(EventHandling) << "mediaInfo called with event set to nullptr.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "getMediaInfo called with m_event set to nullptr.";
return {};
}
return getMediaInfoForEvent(room, event);
return getMediaInfoForEvent(m_event);
}
QVariantMap EventHandler::getMediaInfoForEvent(const NeoChatRoom *room, const Quotient::RoomEvent *event)
QVariantMap EventHandler::getMediaInfoForEvent(const Quotient::RoomEvent *event) const
{
QString eventId = event->id();
@@ -652,7 +661,7 @@ QVariantMap EventHandler::getMediaInfoForEvent(const NeoChatRoom *room, const Qu
const EventContent::FileInfo *fileInfo;
fileInfo = roomMessageEvent->content()->fileInfo();
QVariantMap mediaInfo = getMediaInfoFromFileInfo(room, fileInfo, eventId, false, false);
QVariantMap mediaInfo = getMediaInfoFromFileInfo(fileInfo, eventId, false, false);
// if filename isn't specifically given, it is in body
// https://spec.matrix.org/latest/client-server-api/#mfile
mediaInfo["filename"_ls] = (fileInfo->originalName.isEmpty()) ? roomMessageEvent->plainBody() : fileInfo->originalName;
@@ -664,17 +673,13 @@ QVariantMap EventHandler::getMediaInfoForEvent(const NeoChatRoom *room, const Qu
auto stickerEvent = eventCast<const StickerEvent>(event);
fileInfo = &stickerEvent->image();
return getMediaInfoFromFileInfo(room, fileInfo, eventId, false, true);
return getMediaInfoFromFileInfo(fileInfo, eventId, false, true);
} else {
return {};
}
}
QVariantMap EventHandler::getMediaInfoFromFileInfo(const NeoChatRoom *room,
const EventContent::FileInfo *fileInfo,
const QString &eventId,
bool isThumbnail,
bool isSticker)
QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail, bool isSticker) const
{
QVariantMap mediaInfo;
@@ -682,7 +687,7 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const NeoChatRoom *room,
if (!fileInfo->url().isValid() || fileInfo->url().scheme() != QStringLiteral("mxc") || eventId.isEmpty()) {
mediaInfo["source"_ls] = QUrl();
} else {
QUrl source = room->makeMediaUrl(eventId, fileInfo->url());
QUrl source = m_room->makeMediaUrl(eventId, fileInfo->url());
if (source.isValid()) {
mediaInfo["source"_ls] = source;
@@ -714,7 +719,7 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const NeoChatRoom *room,
if (!isThumbnail) {
QVariantMap tempInfo;
auto thumbnailInfo = getMediaInfoFromFileInfo(room, castInfo->thumbnailInfo(), eventId, true);
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
tempInfo = thumbnailInfo;
} else {
@@ -737,7 +742,7 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const NeoChatRoom *room,
if (!isThumbnail) {
QVariantMap tempInfo;
auto thumbnailInfo = getMediaInfoFromFileInfo(room, castInfo->thumbnailInfo(), eventId, true);
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
tempInfo = thumbnailInfo;
} else {
@@ -761,89 +766,156 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const NeoChatRoom *room,
return mediaInfo;
}
bool EventHandler::hasReply(const Quotient::RoomEvent *event, bool showFallbacks)
bool EventHandler::hasReply() const
{
if (event == nullptr) {
qCWarning(EventHandling) << "hasReply called with event set to nullptr.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "hasReply called with m_event set to nullptr.";
return false;
}
const auto relations = event->contentPart<QJsonObject>("m.relates_to"_ls);
if (!relations.isEmpty()) {
const bool hasReplyRelation = relations.contains("m.in_reply_to"_ls);
bool isFallingBack = relations["is_falling_back"_ls].toBool();
return hasReplyRelation && (showFallbacks ? true : !isFallingBack);
}
return false;
return !m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
}
QString EventHandler::replyId(const Quotient::RoomEvent *event)
QString EventHandler::getReplyId() const
{
if (event == nullptr) {
qCWarning(EventHandling) << "replyId called with event set to nullptr.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyId called with m_event set to nullptr.";
return {};
}
return event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
return m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
}
Quotient::RoomMember EventHandler::replyAuthor(const NeoChatRoom *room, const Quotient::RoomEvent *event)
MessageComponentType::Type EventHandler::replyMessageComponentType() const
{
if (room == nullptr) {
qCWarning(EventHandling) << "replyAuthor called with room set to nullptr.";
if (m_room == nullptr) {
qCWarning(EventHandling) << "replyMessageComponentType called with m_room set to nullptr.";
return MessageComponentType::Other;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "replyMessageComponentType called with m_event set to nullptr.";
return MessageComponentType::Other;
}
auto replyEvent = m_room->getReplyForEvent(*m_event);
if (replyEvent == nullptr) {
return MessageComponentType::Other;
}
return MessageComponentType::typeForEvent(*replyEvent);
}
Quotient::RoomMember EventHandler::getReplyAuthor() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyAuthor called with m_room set to nullptr.";
return {};
}
if (event == nullptr) {
qCWarning(EventHandling) << "replyAuthor called with event set to nullptr. Returning empty user.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyAuthor called with m_event set to nullptr. Returning empty user.";
return {};
}
if (auto replyPtr = room->getReplyForEvent(*event)) {
return room->member(replyPtr->senderId());
if (auto replyPtr = m_room->getReplyForEvent(*m_event)) {
return m_room->member(replyPtr->senderId());
} else {
return room->member(QString());
return m_room->member(QString());
}
}
bool EventHandler::isThreaded(const Quotient::RoomEvent *event)
QString EventHandler::getReplyRichBody(bool stripNewlines) const
{
if (event == nullptr) {
qCWarning(EventHandling) << "isThreaded called with event set to nullptr.";
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyRichBody called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyRichBody called with m_event set to nullptr.";
return {};
}
auto replyEvent = m_room->getReplyForEvent(*m_event);
if (replyEvent == nullptr) {
return {};
}
return getBody(replyEvent, Qt::RichText, stripNewlines);
}
QString EventHandler::getReplyPlainBody(bool stripNewlines) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyPlainBody called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyPlainBody called with m_event set to nullptr.";
return {};
}
auto replyEvent = m_room->getReplyForEvent(*m_event);
if (replyEvent == nullptr) {
return {};
}
return getBody(replyEvent, Qt::PlainText, stripNewlines);
}
QVariantMap EventHandler::getReplyMediaInfo() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyMediaInfo called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyMediaInfo called with m_event set to nullptr.";
return {};
}
auto replyPtr = m_room->getReplyForEvent(*m_event);
if (!replyPtr) {
return {};
}
return getMediaInfoForEvent(replyPtr);
}
bool EventHandler::isThreaded() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "isThreaded called with m_event set to nullptr.";
return false;
}
return (event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls)
|| (!event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls));
return (m_event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& m_event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls)
|| (!m_event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && m_event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls));
}
QString EventHandler::threadRoot(const Quotient::RoomEvent *event)
QString EventHandler::threadRoot() const
{
if (event == nullptr) {
qCWarning(EventHandling) << "threadRoot called with event set to nullptr.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "threadRoot called with m_event set to nullptr.";
return {};
}
// Get the thread root ID from m.relates_to if it exists.
if (event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls) {
return event->contentPart<QJsonObject>("m.relates_to"_ls)["event_id"_ls].toString();
if (m_event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& m_event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls) {
return m_event->contentPart<QJsonObject>("m.relates_to"_ls)["event_id"_ls].toString();
}
// For thread root events they have an m.relations in the unsigned part with a m.thread object.
// If so return the event ID as it is the root.
if (!event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls)) {
return id(event);
if (!m_event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && m_event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls)) {
return getId();
}
return {};
}
float EventHandler::latitude(const Quotient::RoomEvent *event)
float EventHandler::getLatitude() const
{
if (event == nullptr) {
qCWarning(EventHandling) << "latitude called with event set to nullptr.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLatitude called with m_event set to nullptr.";
return -100.0;
}
const auto geoUri = event->contentJson()["geo_uri"_ls].toString();
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) {
return -100.0; // latitude runs from -90deg to +90deg so -100 is out of range.
}
@@ -851,14 +923,14 @@ float EventHandler::latitude(const Quotient::RoomEvent *event)
return latitude.toFloat();
}
float EventHandler::longitude(const Quotient::RoomEvent *event)
float EventHandler::getLongitude() const
{
if (event == nullptr) {
qCWarning(EventHandling) << "longitude called with event set to nullptr.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLongitude called with m_event set to nullptr.";
return -200.0;
}
const auto geoUri = event->contentJson()["geo_uri"_ls].toString();
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) {
return -200.0; // longitude runs from -180deg to +180deg so -200 is out of range.
}
@@ -866,14 +938,14 @@ float EventHandler::longitude(const Quotient::RoomEvent *event)
return latitude.toFloat();
}
QString EventHandler::locationAssetType(const Quotient::RoomEvent *event)
QString EventHandler::getLocationAssetType() const
{
if (event == nullptr) {
qCWarning(EventHandling) << "locationAssetType called with event set to nullptr.";
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLocationAssetType called with m_event set to nullptr.";
return {};
}
const auto assetType = event->contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
const auto assetType = m_event->contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
if (assetType.isEmpty()) {
return {};
}

View File

@@ -3,22 +3,24 @@
#pragma once
#include <QDateTime>
#include <QString>
#include <Quotient/events/eventcontent.h>
#include <QObject>
#include <KFormat>
#include <Quotient/eventitem.h>
#include <Quotient/events/roomevent.h>
#include <Quotient/events/roommessageevent.h>
#include "enums/messagecomponenttype.h"
namespace Quotient
{
namespace EventContent
{
class FileInfo;
}
class RoomEvent;
class RoomMember;
class RoomMessageEvent;
}
class LinkPreviewer;
class NeoChatRoom;
class ReactionModel;
/**
* @class EventHandler
@@ -36,14 +38,20 @@ class NeoChatRoom;
*/
class EventHandler
{
Q_GADGET
public:
EventHandler(const NeoChatRoom *room, const Quotient::RoomEvent *event);
/**
* @brief Return the ID of the event.
*
* Returns the transaction ID if the Matrix ID is empty, which may be the case
* for a pending event.
* @brief Return the Matrix ID of the event.
*/
static QString id(const Quotient::RoomEvent *event);
QString getId() const;
/**
* @brief The MessageComponentType to use to visualise the main event content.
*/
MessageComponentType::Type messageComponentType() const;
/**
* @brief Get the display name of the event author.
@@ -56,7 +64,7 @@ public:
* @param isPending whether the event is pending as this cannot be derived from
* just the event object.
*/
static QString authorDisplayName(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending = false);
QString getAuthorDisplayName(bool isPending = false) const;
/**
* @brief Get the display name of the event author but with any newlines removed.
@@ -67,12 +75,12 @@ public:
* @param isPending whether the event is pending as this cannot be derived from
* just the event object.
*/
static QString singleLineAuthorDisplayname(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending = false);
QString singleLineAuthorDisplayname(bool isPending = false) const;
/**
* @brief Return a QDateTime object for the event timestamp.
*/
static QDateTime time(const Quotient::RoomEvent *event, bool isPending = false, QDateTime lastUpdated = {});
QDateTime getTime(bool isPending = false, QDateTime lastUpdated = {}) const;
/**
* @brief Return a QString for the event timestamp.
@@ -88,32 +96,16 @@ public:
* @param lastUpdated the time the event was last updated locally as this cannot be
* obtained from the event.
*/
static QString timeString(const Quotient::RoomEvent *event,
bool relative,
QLocale::FormatType format = QLocale::ShortFormat,
bool isPending = false,
QDateTime lastUpdated = {});
QString getTimeString(bool relative, QLocale::FormatType format = QLocale::ShortFormat, bool isPending = false, QDateTime lastUpdated = {}) const;
/**
* @brief Return a QString for the event timestamp.
*
* This is intended to return a string that is read for display in the UI without
* any further manipulation required.
*
* @param format the format to use as a string.
* @param isPending whether the event is pending as this cannot be derived from
* just the event object.
* @param lastUpdated the time the event was last updated locally as this cannot be
* obtained from the event.
*/
static QString timeString(const Quotient::RoomEvent *event, const QString &format, bool isPending = false, const QDateTime &lastUpdated = {});
QString getTimeString(const QString &format, bool isPending = false, const QDateTime &lastUpdated = {});
/**
* @brief Whether the event should be highlighted in the timeline.
*
* @note Messages in direct chats are never highlighted.
*/
static bool isHighlighted(const NeoChatRoom *room, const Quotient::RoomEvent *event);
bool isHighlighted();
/**
* @brief Whether the event should be hidden in the timeline.
@@ -122,7 +114,7 @@ public:
* user has hidden all state events or if the sender has been ignored by the local
* user.
*/
static bool isHidden(const NeoChatRoom *room, const Quotient::RoomEvent *event);
bool isHidden();
/**
* @brief The input format of the body in the message.
@@ -154,7 +146,7 @@ public:
*
* @param stripNewlines whether the output should have new lines in it.
*/
static QString richBody(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool stripNewlines = false);
QString getRichBody(bool stripNewlines = false) const;
/**
* @brief Output a string for the message content ready for display in a plain text field.
@@ -170,14 +162,14 @@ public:
*
* @param stripNewlines whether the output should have new lines in it.
*/
static QString plainBody(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool stripNewlines = false);
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.
*/
static QString markdownBody(const Quotient::RoomEvent *event);
QString getMarkdownBody() const;
/**
* @brief Output a generic string for the message content ready for display.
@@ -190,9 +182,9 @@ public:
* E.g. For a message the text will be:
* "sent a message"
*
* @sa richBody(), plainBody()
* @sa getRichBody(), getPlainBody()
*/
static QString genericBody(const Quotient::RoomEvent *event);
QString getGenericBody() const;
/**
* @brief Output a string for the event to be used as a RoomList subtitle.
@@ -200,7 +192,7 @@ public:
* The output includes the username followed by the plain message, all with no
* line breaks.
*/
static QString subtitleText(const NeoChatRoom *room, const Quotient::RoomEvent *event);
QString subtitleText() const;
/**
* @brief Return the media info for the event.
@@ -218,21 +210,22 @@ public:
* - 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
*/
static QVariantMap mediaInfo(const NeoChatRoom *room, const Quotient::RoomEvent *event);
QVariantMap getMediaInfo() const;
/**
* @brief Whether the event is a reply to another in the timeline.
*
* @param showFallbacks whether message that have is_falling_back set true should
* show the fallback reply. Leave true for non-threaded
* timelines.
*/
static bool hasReply(const Quotient::RoomEvent *event, bool showFallbacks = true);
bool hasReply() const;
/**
* @brief Return the Matrix ID of the event replied to.
*/
static QString replyId(const Quotient::RoomEvent *event);
QString getReplyId() const;
/**
* @brief The MessageComponentType to use to visualise the reply content.
*/
MessageComponentType::Type replyMessageComponentType() const;
/**
* @brief Get the author of the event replied to in context of the room.
@@ -247,21 +240,73 @@ public:
*
* @sa Quotient::RoomMember
*/
static Quotient::RoomMember replyAuthor(const NeoChatRoom *room, const Quotient::RoomEvent *event);
Quotient::RoomMember getReplyAuthor() const;
/**
* @brief Output a string for the message content of the event replied to ready
* for display in a rich text field.
*
* The output string is dependant upon the event type and the desired output format.
*
* For most messages this is the body content of the message. For media messages
* this will be the caption and for state events it will be a string specific
* to that event with some dynamic details about the event added.
*
* E.g. For a room topic state event the text will be:
* "set the topic to: <new topic text>"
*
* @param stripNewlines whether the output should have new lines in it.
*/
QString getReplyRichBody(bool stripNewlines = false) const;
/**
* @brief Output a string for the message content of the event replied to ready
* for display in a plain text field.
*
* The output string is dependant upon the event type and the desired output format.
*
* For most messages this is the body content of the message. For media messages
* this will be the caption and for state events it will be a string specific
* to that event with some dynamic details about the event added.
*
* E.g. For a room topic state event the text will be:
* "set the topic to: <new topic text>"
*
* @param stripNewlines whether the output should have new lines in it.
*/
QString getReplyPlainBody(bool stripNewlines = false) const;
/**
* @brief Return the media info for the event replied to.
*
* An empty QVariantMap will be returned for any event that doesn't have any
* media info.
*
* @return This should consist of the following:
* - source - The mxc URL for the media.
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
* - mimeIcon - The MIME icon name (should be image-xxx).
* - size - The file size in bytes.
* - 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;
/**
* @brief Whether the message is part of a thread.
*
* i.e. There is a rel_type of m.thread.
*/
static bool isThreaded(const Quotient::RoomEvent *event);
bool isThreaded() const;
/**
* @brief Return the Matrix ID of the thread's root message.
*
* Empty if this not part of a thread.
*/
static QString threadRoot(const Quotient::RoomEvent *event);
QString threadRoot() const;
/**
* @brief Return the latitude for the event.
@@ -269,7 +314,7 @@ public:
* Returns -100.0 if the event doesn't have a location (latitudes are in the
* range -90deg to +90deg so -100 is out of range).
*/
static float latitude(const Quotient::RoomEvent *event);
float getLatitude() const;
/**
* @brief Return the longitude for the event.
@@ -277,21 +322,23 @@ public:
* Returns -200.0 if the event doesn't have a location (latitudes are in the
* range -180deg to +180deg so -200 is out of range).
*/
static float longitude(const Quotient::RoomEvent *event);
float getLongitude() const;
/**
* @brief Return the type of location marker for the event.
*/
static QString locationAssetType(const Quotient::RoomEvent *event);
QString getLocationAssetType() const;
private:
static QString getBody(const NeoChatRoom *room, const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines);
static QString getMessageBody(const NeoChatRoom *room, const Quotient::RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines);
const NeoChatRoom *m_room = nullptr;
const Quotient::RoomEvent *m_event = nullptr;
static QVariantMap getMediaInfoForEvent(const NeoChatRoom *room, const Quotient::RoomEvent *event);
QVariantMap static getMediaInfoFromFileInfo(const NeoChatRoom *room,
const Quotient::EventContent::FileInfo *fileInfo,
const QString &eventId,
bool isThumbnail = false,
bool isSticker = false);
KFormat m_format;
QString getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const;
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, bool isSticker = false) const;
};

View File

@@ -10,13 +10,22 @@
#include <Quotient/keyverificationsession.h>
#include <Quotient/roommember.h>
#if Quotient_VERSION_MINOR > 8
#include <Quotient/keyimport.h>
#endif
#include "controller.h"
#include "neochatconfig.h"
struct ForeignConfig {
Q_GADGET
QML_FOREIGN(NeoChatConfig)
QML_NAMED_ELEMENT(Config)
QML_SINGLETON
public:
static NeoChatConfig *create(QQmlEngine *, QJSEngine *)
{
QQmlEngine::setObjectOwnership(NeoChatConfig::self(), QQmlEngine::CppOwnership);
return NeoChatConfig::self();
}
};
struct ForeignAccountRegistry {
Q_GADGET
QML_FOREIGN(Quotient::AccountRegistry)
@@ -42,12 +51,3 @@ struct ForeignSSSSHandler {
QML_FOREIGN(Quotient::SSSSHandler)
QML_NAMED_ELEMENT(SSSSHandler)
};
#if Quotient_VERSION_MINOR > 8
struct ForeignKeyImport {
Q_GADGET
QML_SINGLETON
QML_FOREIGN(Quotient::KeyImport)
QML_NAMED_ELEMENT(KeyImport)
};
#endif

View File

@@ -40,7 +40,7 @@ float LocationHelper::zoomToFit(const QRectF &r, float mapWidth, float mapHeight
const auto zy = std::log2((mapHeight / (p2.y() - p1.y())));
const auto z = std::min(zx, zy);
return z;
return std::clamp(z, 5.0, 18.0);
}
#include "moc_locationhelper.cpp"

View File

@@ -78,7 +78,9 @@ void LoginHelper::init()
account.setHomeserver(m_connection->homeserver());
account.setDeviceId(m_connection->deviceId());
account.setDeviceName(m_deviceName);
Controller::instance().saveAccessTokenToKeyChain(account.userId(), m_connection->accessToken());
if (!Controller::instance().saveAccessTokenToKeyChain(account.userId(), m_connection->accessToken())) {
qWarning() << "Couldn't save access token";
}
account.sync();
Controller::instance().addConnection(m_connection);
Controller::instance().setActiveConnection(m_connection);

View File

@@ -11,7 +11,7 @@ import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.neochat
import org.kde.neochat.settings
Kirigami.Page {
FormCard.FormCardPage {
id: root
property bool showExisting: false
@@ -23,237 +23,202 @@ Kirigami.Page {
signal connectionChosen
title: i18n("Welcome")
globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None
header: QQC2.Control {
topPadding: 0
bottomPadding: 0
leftPadding: 0
rightPadding: 0
contentItem: Kirigami.InlineMessage {
id: headerMessage
type: Kirigami.MessageType.Error
showCloseButton: true
visible: false
}
}
contentItem: ColumnLayout {
spacing: 0
Kirigami.Icon {
source: "org.kde.neochat"
Layout.alignment: Qt.AlignHCenter
implicitWidth: Math.round(Kirigami.Units.iconSizes.huge * 1.5)
implicitHeight: Math.round(Kirigami.Units.iconSizes.huge * 1.5)
}
Kirigami.Separator {
Layout.fillWidth: true
Kirigami.Heading {
id: welcomeMessage
text: i18n("Welcome to NeoChat")
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Kirigami.Units.largeSpacing
}
FormCard.FormHeader {
id: existingAccountsHeader
title: i18nc("@title", "Continue with an existing account")
visible: (loadedAccounts.count > 0 || loadingAccounts.count > 0) && root._showExisting
}
FormCard.FormCard {
visible: existingAccountsHeader.visible
Repeater {
id: loadedAccounts
model: AccountRegistry
delegate: FormCard.FormButtonDelegate {
text: model.userId
onClicked: {
Controller.activeConnection = model.connection;
root.connectionChosen();
}
}
}
Repeater {
id: loadingAccounts
model: Controller.accountsLoading
delegate: FormCard.AbstractFormDelegate {
id: loadingDelegate
Kirigami.InlineMessage {
id: headerMessage
type: Kirigami.MessageType.Error
showCloseButton: true
visible: false
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
background: null
contentItem: RowLayout {
spacing: 0
QQC2.Label {
Layout.fillWidth: true
text: i18nc("As in 'this account is still loading'", "%1 (loading)", modelData)
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
color: Kirigami.Theme.disabledTextColor
Accessible.ignored: true // base class sets this text on root already
}
QQC2.ToolButton {
text: i18nc("@action:button", "Log out of this account")
icon.name: "edit-delete-remove"
onClicked: Controller.removeConnection(modelData)
display: QQC2.Button.IconOnly
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
enabled: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
}
FormCard.FormArrow {
Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
direction: Qt.RightArrow
visible: root.background.visible
}
}
}
onCountChanged: {
if (loadingAccounts.count === 0 && loadedAccounts.count === 1 && showExisting) {
Controller.activeConnection = AccountRegistry.data(AccountRegistry.index(0, 0), 257);
root.connectionChosen();
}
}
}
}
contentItem: Item {
ColumnLayout {
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
FormCard.FormHeader {
title: i18nc("@title", "Log in or Create a New Account")
}
spacing: 0
FormCard.FormCard {
Loader {
id: module
Layout.fillWidth: true
sourceComponent: Qt.createComponent('org.kde.neochat.login', root.initialStep)
Kirigami.Icon {
source: "org.kde.neochat"
Layout.alignment: Qt.AlignHCenter
implicitWidth: Math.round(Kirigami.Units.iconSizes.huge * 1.5)
implicitHeight: Math.round(Kirigami.Units.iconSizes.huge * 1.5)
}
Connections {
id: stepConnections
target: currentStep
Kirigami.Heading {
id: welcomeMessage
text: i18n("NeoChat")
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Kirigami.Units.largeSpacing
}
FormCard.FormHeader {
id: existingAccountsHeader
title: i18nc("@title", "Continue with an existing account")
visible: (loadedAccounts.count > 0 || loadingAccounts.count > 0) && root._showExisting
maximumWidth: Kirigami.Units.gridUnit * 20
}
FormCard.FormCard {
visible: existingAccountsHeader.visible
maximumWidth: Kirigami.Units.gridUnit * 20
Repeater {
id: loadedAccounts
model: AccountRegistry
delegate: FormCard.FormButtonDelegate {
text: model.userId
onClicked: {
Controller.activeConnection = model.connection;
root.connectionChosen();
}
}
}
Repeater {
id: loadingAccounts
model: Controller.accountsLoading
delegate: FormCard.AbstractFormDelegate {
id: loadingDelegate
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
background: null
contentItem: RowLayout {
spacing: 0
QQC2.Label {
Layout.fillWidth: true
text: i18nc("As in 'this account is still loading'", "%1 (loading)", modelData)
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
color: Kirigami.Theme.disabledTextColor
Accessible.ignored: true // base class sets this text on root already
}
QQC2.ToolButton {
text: i18nc("@action:button", "Log out of this account")
icon.name: "im-kick-user"
onClicked: Controller.removeConnection(modelData)
display: QQC2.Button.IconOnly
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
enabled: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
}
FormCard.FormArrow {
Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
direction: Qt.RightArrow
visible: root.background.visible
}
}
}
onCountChanged: {
if (loadingAccounts.count === 0 && loadedAccounts.count === 1 && showExisting) {
Controller.activeConnection = AccountRegistry.data(AccountRegistry.index(0, 0), 257);
root.connectionChosen();
}
}
}
}
FormCard.FormHeader {
title: i18nc("@title", "Log in or Create a New Account")
maximumWidth: Kirigami.Units.gridUnit * 20
}
FormCard.FormCard {
maximumWidth: Kirigami.Units.gridUnit * 20
Loader {
id: module
Layout.fillWidth: true
sourceComponent: Qt.createComponent('org.kde.neochat.login', root.initialStep)
Connections {
id: stepConnections
target: currentStep
function onProcessed(nextStep: string): void {
module.source = nextStep + ".qml";
root.currentStepString = nextStep;
headerMessage.text = "";
headerMessage.visible = false;
if (!module.item.noControls) {
module.item.forceActiveFocus();
} else {
continueButton.forceActiveFocus();
}
}
function onShowMessage(message: string): void {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Information;
}
function onClearError(): void {
headerMessage.text = "";
headerMessage.visible = false;
}
function onCloseDialog(): void {
root.closeDialog();
}
}
Connections {
target: Registration
function onNextStepChanged() {
if (Registration.nextStep === "m.login.recaptcha") {
stepConnections.onProcessed("Captcha");
}
if (Registration.nextStep === "m.login.terms") {
stepConnections.onProcessed("Terms");
}
if (Registration.nextStep === "m.login.email.identity") {
stepConnections.onProcessed("Email");
}
if (Registration.nextStep === "loading") {
stepConnections.onProcessed("Loading");
}
}
}
Connections {
target: LoginHelper
function onErrorOccured(message) {
headerMessage.text = message;
headerMessage.visible = message.length > 0;
headerMessage.type = Kirigami.MessageType.Error;
}
function onProcessed(nextStep: string): void {
module.source = nextStep + ".qml";
root.currentStepString = nextStep;
headerMessage.text = "";
headerMessage.visible = false;
if (!module.item.noControls) {
module.item.forceActiveFocus();
} else {
continueButton.forceActiveFocus();
}
}
FormCard.FormDelegateSeparator {
below: continueButton
visible: root.currentStep.nextAction
function onShowMessage(message: string): void {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Information;
}
FormCard.FormButtonDelegate {
id: continueButton
text: root.currentStep.nextAction && root.currentStep.nextAction.text ? root.currentStep.nextAction.text : i18nc("@action:button", "Continue")
visible: root.currentStep.nextAction
onClicked: root.currentStep.nextAction.trigger()
icon.name: "arrow-right"
enabled: root.currentStep.nextAction ? root.currentStep.nextAction.enabled : false
function onClearError(): void {
headerMessage.text = "";
headerMessage.visible = false;
}
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Go back")
visible: root.currentStep.previousAction
onClicked: root.currentStep.previousAction.trigger()
icon.name: "arrow-left"
enabled: root.currentStep.previousAction ? root.currentStep.previousAction.enabled : false
function onCloseDialog(): void {
root.closeDialog();
}
}
FormCard.FormCard {
Layout.topMargin: Kirigami.Units.largeSpacing * 2
maximumWidth: Kirigami.Units.gridUnit * 20
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Settings")
icon.name: "settings-configure"
onClicked: NeoChatSettingsView.open()
Connections {
target: Registration
function onNextStepChanged() {
if (Registration.nextStep === "m.login.recaptcha") {
stepConnections.onProcessed("Captcha");
}
if (Registration.nextStep === "m.login.terms") {
stepConnections.onProcessed("Terms");
}
if (Registration.nextStep === "m.login.email.identity") {
stepConnections.onProcessed("Email");
}
if (Registration.nextStep === "loading") {
stepConnections.onProcessed("Loading");
}
}
}
Connections {
target: LoginHelper
function onErrorOccured(message) {
headerMessage.text = message;
headerMessage.visible = message.length > 0;
headerMessage.type = Kirigami.MessageType.Error;
}
}
}
FormCard.FormDelegateSeparator {
below: continueButton
}
FormCard.FormButtonDelegate {
id: continueButton
text: root.currentStep.nextAction && root.currentStep.nextAction.text ? root.currentStep.nextAction.text : i18nc("@action:button", "Continue")
visible: root.currentStep.nextAction
onClicked: root.currentStep.nextAction.trigger()
icon.name: "arrow-right"
enabled: root.currentStep.nextAction ? root.currentStep.nextAction.enabled : false
}
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Go back")
visible: root.currentStep.previousAction
onClicked: root.currentStep.previousAction.trigger()
icon.name: "arrow-left"
enabled: root.currentStep.previousAction ? root.currentStep.previousAction.enabled : false
}
}
FormCard.FormCard {
Layout.topMargin: Kirigami.Units.largeSpacing
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Open proxy settings")
icon.name: "settings-configure"
onClicked: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat.settings", "NetworkProxyPage"), {}, {
title: i18nc("@title:window", "Proxy Settings")
});
}
}

View File

@@ -148,7 +148,7 @@ int main(int argc, char *argv[])
i18n("Maintainer"),
QStringLiteral("carl@carlschwan.eu"),
QStringLiteral("https://carlschwan.eu"),
QUrl(QStringLiteral("https://carlschwan.eu/avatar.png")));
QStringLiteral("https://carlschwan.eu/avatar.png"));
about.addAuthor(i18n("Tobias Fella"), i18n("Maintainer"), QStringLiteral("tobias.fella@kde.org"), QStringLiteral("https://tobiasfella.de"));
about.addAuthor(i18n("James Graham"), i18n("Maintainer"), QStringLiteral("james.h.graham@protonmail.com"));
about.addCredit(i18n("Black Hat"), i18n("Original author of Spectral"), QStringLiteral("bhat@encom.eu.org"));
@@ -185,6 +185,9 @@ int main(int argc, char *argv[])
#endif
ColorSchemer colorScheme;
if (!NeoChatConfig::self()->colorScheme().isEmpty()) {
colorScheme.apply(NeoChatConfig::self()->colorScheme());
}
QCommandLineParser parser;
parser.setApplicationDescription(i18n("Client for the matrix communication protocol"));
@@ -304,6 +307,7 @@ int main(int argc, char *argv[])
QWindow *window = windowFromEngine(&engine);
WindowController::instance().setWindow(window);
WindowController::instance().restoreGeometry();
return app.exec();
}

124
src/matriximageprovider.cpp Normal file
View File

@@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2019 Kitsune Ral <kitsune-ral@users.sf.net>
// SPDX-License-Identifier: GPL-3.0-only
#include "matriximageprovider.h"
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
#include <QThread>
#include <KLocalizedString>
#include "neochatconnection.h"
using namespace Quotient;
ThumbnailResponse::ThumbnailResponse(QString id, QSize size, NeoChatConnection *connection)
: mediaId(std::move(id))
, requestedSize(size)
, localFile(QStringLiteral("%1/image_provider/%2-%3x%4.png")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation),
mediaId,
QString::number(requestedSize.width()),
QString::number(requestedSize.height())))
, m_connection(connection)
, errorStr("Image request hasn't started"_ls)
{
if (requestedSize.isEmpty()) {
requestedSize.setHeight(100);
requestedSize.setWidth(100);
}
if (mediaId.count(QLatin1Char('/')) != 1) {
if (mediaId.startsWith(QLatin1Char('/'))) {
mediaId = mediaId.mid(1);
} else {
errorStr = i18n("Media id '%1' doesn't follow server/mediaId pattern", mediaId);
Q_EMIT finished();
return;
}
}
mediaId = mediaId.split(QLatin1Char('?'))[0];
QImage cachedImage;
if (cachedImage.load(localFile)) {
image = cachedImage;
errorStr.clear();
Q_EMIT finished();
return;
}
if (!m_connection) {
qWarning() << "Current connection is null";
return;
}
// Execute a request on the main thread asynchronously
moveToThread(m_connection->thread());
QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest, Qt::QueuedConnection);
}
void ThumbnailResponse::startRequest()
{
if (!m_connection) {
return;
}
// Runs in the main thread, not QML thread
Q_ASSERT(QThread::currentThread() == m_connection->thread());
job = m_connection->getThumbnail(mediaId, requestedSize);
// Connect to any possible outcome including abandonment
// to make sure the QML thread is not left stuck forever.
connect(job, &BaseJob::finished, this, &ThumbnailResponse::prepareResult);
}
void ThumbnailResponse::prepareResult()
{
Q_ASSERT(QThread::currentThread() == job->thread());
Q_ASSERT(job->error() != BaseJob::Pending);
{
QWriteLocker _(&lock);
if (job->error() == BaseJob::Success) {
image = job->thumbnail();
QString localPath = QFileInfo(localFile).absolutePath();
QDir dir;
if (!dir.exists(localPath)) {
dir.mkpath(localPath);
}
image.save(localFile);
errorStr.clear();
} else if (job->error() == BaseJob::Abandoned) {
errorStr = i18n("Image request has been cancelled");
// qDebug() << "ThumbnailResponse: cancelled for" << mediaId;
} else {
errorStr = job->errorString();
qWarning() << "ThumbnailResponse: no valid image for" << mediaId << "-" << errorStr;
}
job = nullptr;
}
Q_EMIT finished();
}
QQuickTextureFactory *ThumbnailResponse::textureFactory() const
{
QReadLocker _(&lock);
return QQuickTextureFactory::textureFactoryForImage(image);
}
QString ThumbnailResponse::errorString() const
{
QReadLocker _(&lock);
return errorStr;
}
QQuickImageResponse *MatrixImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{
return new ThumbnailResponse(id, requestedSize, m_connection);
}
#include "moc_matriximageprovider.cpp"

80
src/matriximageprovider.h Normal file
View File

@@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2019 Kitsune Ral <kitsune-ral@users.sf.net>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QQuickAsyncImageProvider>
#include <Quotient/jobs/mediathumbnailjob.h>
#include <QReadWriteLock>
class NeoChatConnection;
/**
* @class ThumbnailResponse
*
* A QQuickImageResponse for an mxc image.
*
* @sa QQuickImageResponse
*/
class ThumbnailResponse : public QQuickImageResponse
{
Q_OBJECT
public:
explicit ThumbnailResponse(QString mediaId, QSize requestedSize, NeoChatConnection *m_connection);
~ThumbnailResponse() override = default;
private Q_SLOTS:
void startRequest();
void prepareResult();
private:
QString mediaId;
QSize requestedSize;
const QString localFile;
Quotient::MediaThumbnailJob *job = nullptr;
QPointer<NeoChatConnection> m_connection;
QImage image;
QString errorStr;
mutable QReadWriteLock lock; // Guards ONLY these two members above
QQuickTextureFactory *textureFactory() const override;
QString errorString() const override;
};
/**
* @class MatrixImageProvider
*
* A QQuickAsyncImageProvider for mxc images.
*
* @sa QQuickAsyncImageProvider
*/
class MatrixImageProvider : public QQuickAsyncImageProvider
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(NeoChatConnection *connection MEMBER m_connection)
public:
static MatrixImageProvider *create(QQmlEngine *engine, QJSEngine *)
{
static MatrixImageProvider *instance = new MatrixImageProvider;
engine->setObjectOwnership(instance, QQmlEngine::CppOwnership);
return instance;
}
/**
* @brief Return a job to provide the image with the given ID.
*
* @sa QQuickAsyncImageProvider::requestImageResponse
*/
QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
private:
QPointer<NeoChatConnection> m_connection;
MatrixImageProvider() = default;
};

View File

@@ -211,7 +211,7 @@ QList<ActionsModel::Action> actions{
Q_EMIT Controller::instance().showMessage(Controller::Positive, i18n("You are already in this room."));
return QString();
}
if (room->joinedMemberIds().contains(text)) {
if (room->members().contains(room->member(text))) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("<user> is already in this room.", "%1 is already in this room.", text));
return QString();
}

View File

@@ -2,18 +2,17 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "messagecontentmodel.h"
#include "eventhandler.h"
#include "neochatconfig.h"
#include "neochatroommember.h"
#include <QImageReader>
#include <Quotient/events/redactionevent.h>
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/stickerevent.h>
#include <Quotient/qt_connection_util.h>
#include <KLocalizedString>
#include <Kirigami/Platform/PlatformTheme>
#include <Quotient/qt_connection_util.h>
#ifndef Q_OS_ANDROID
#include <KSyntaxHighlighting/Definition>
@@ -21,7 +20,10 @@
#endif
#include "chatbarcache.h"
#include "enums/messagecomponenttype.h"
#include "eventhandler.h"
#include "filetype.h"
#include "itinerarymodel.h"
#include "linkpreviewer.h"
#include "neochatconnection.h"
#include "neochatroom.h"
@@ -29,8 +31,8 @@
using namespace Quotient;
MessageContentModel::MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply, bool isPending, MessageContentModel *parent)
: QAbstractListModel(parent)
MessageContentModel::MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply, bool isPending)
: QAbstractListModel(nullptr)
, m_room(room)
, m_eventId(event != nullptr ? event->id() : QString())
, m_eventSenderId(event != nullptr ? event->senderId() : QString())
@@ -41,8 +43,8 @@ MessageContentModel::MessageContentModel(NeoChatRoom *room, const Quotient::Room
initializeModel();
}
MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending, MessageContentModel *parent)
: QAbstractListModel(parent)
MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending)
: QAbstractListModel(nullptr)
, m_room(room)
, m_eventId(eventId)
, m_isPending(isPending)
@@ -56,27 +58,15 @@ void MessageContentModel::initializeModel()
Q_ASSERT(m_room != nullptr);
// Allow making a model for an event that is being downloaded but will appear later
// e.g. a reply, but we need an ID to know when it has arrived.
// Also note that a pending event may not have an event ID yet but as long as we have an event
// pointer we can pass out the transaction ID until it is set.
Q_ASSERT(!m_eventId.isEmpty() || m_event != nullptr);
Q_ASSERT(!m_eventId.isEmpty());
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventLoaded, this, [this](const QString &eventId) {
if (m_room != nullptr) {
if (eventId == m_eventId) {
m_notFound = false;
intiializeEvent(m_room->getEvent(eventId));
m_event = loadEvent<RoomEvent>(m_room->getEvent(eventId)->fullJson());
Q_EMIT eventUpdated();
updateReplyModel();
resetModel();
return true;
}
}
return false;
});
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventNotFound, this, [this](const QString &eventId) {
if (m_room != nullptr) {
if (eventId == m_eventId) {
m_notFound = true;
resetModel();
resetContent();
return true;
}
}
@@ -84,15 +74,12 @@ void MessageContentModel::initializeModel()
});
if (m_event == nullptr) {
intiializeEvent(m_room->getEvent(m_eventId));
if (m_event == nullptr) {
m_room->downloadEventFromServer(m_eventId);
}
m_room->downloadEventFromServer(m_eventId);
}
connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) {
if (m_room != nullptr && m_event != nullptr) {
if (m_eventId == serverEvent->id() || m_eventId == serverEvent->transactionId()) {
if (m_eventId == serverEvent->id()) {
beginResetModel();
m_isPending = false;
intiializeEvent(serverEvent);
@@ -105,7 +92,6 @@ void MessageContentModel::initializeModel()
if (m_eventId == newEvent->id()) {
beginResetModel();
intiializeEvent(newEvent);
resetContent();
endResetModel();
}
}
@@ -140,13 +126,6 @@ void MessageContentModel::initializeModel()
endResetModel();
}
});
connect(m_room->threadCache(), &ChatBarCache::threadIdChanged, this, [this](const QString &oldThreadId, const QString &newThreadId) {
if (m_event != nullptr && (oldThreadId == m_eventId || newThreadId == m_eventId)) {
beginResetModel();
resetContent(false, newThreadId == m_eventId);
endResetModel();
}
});
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() {
resetContent();
});
@@ -168,11 +147,6 @@ void MessageContentModel::initializeModel()
}
});
connect(NeoChatConfig::self(), &NeoChatConfig::ThreadsChanged, this, [this]() {
updateReplyModel();
resetModel();
});
if (m_event != nullptr) {
updateReplyModel();
}
@@ -189,15 +163,8 @@ void MessageContentModel::intiializeEvent(const QString &eventId)
void MessageContentModel::intiializeEvent(const Quotient::RoomEvent *event)
{
if (event == nullptr) {
return;
}
m_event = loadEvent<RoomEvent>(event->fullJson());
// a pending event may not previously have had an event ID so update.
m_eventId = EventHandler::id(m_event.get());
auto senderId = m_event->senderId();
auto senderId = event->senderId();
// A pending event might not have a sender ID set yet but in that case it must
// be the local member.
if (senderId.isEmpty()) {
@@ -221,7 +188,7 @@ void MessageContentModel::setShowAuthor(bool showAuthor)
}
m_showAuthor = showAuthor;
if (m_event != nullptr && m_room->connection()->isIgnored(m_event->senderId())) {
if (m_event != nullptr) {
if (showAuthor) {
beginInsertRows({}, 0, 0);
m_components.prepend(MessageComponent{MessageComponentType::Author, QString(), {}});
@@ -248,23 +215,10 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return {};
}
EventHandler eventHandler(m_room, m_event.get());
const auto component = m_components[index.row()];
if (role == DisplayRole) {
if (m_notFound || (m_event && m_room->connection()->isIgnored(m_event->senderId()))) {
Kirigami::Platform::PlatformTheme *theme =
static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
QString disabledTextColor;
if (theme != nullptr) {
disabledTextColor = theme->disabledTextColor().name();
} else {
disabledTextColor = QStringLiteral("#000000");
}
return QString(QStringLiteral("<span style=\"color:%1\">").arg(disabledTextColor)
+ i18nc("@info", "This message was either not found, you do not have permission to view it, or it was sent by an ignored user")
+ QStringLiteral("</span>"));
}
if (component.type == MessageComponentType::Loading && m_isReply) {
return i18n("Loading reply");
}
@@ -279,7 +233,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
if (!component.content.isEmpty()) {
return component.content;
}
return EventHandler::richBody(m_room, m_event.get());
return eventHandler.getRichBody();
}
if (role == ComponentTypeRole) {
return component.type;
@@ -288,7 +242,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return component.attributes;
}
if (role == EventIdRole) {
return EventHandler::id(m_event.get());
return eventHandler.getId();
}
if (role == TimeRole) {
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [this](const PendingEventItem &pendingEvent) {
@@ -296,7 +250,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
});
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated();
return EventHandler::time(m_event.get(), m_isPending, lastUpdated);
return eventHandler.getTime(m_isPending, lastUpdated);
}
if (role == TimeStringRole) {
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [this](const PendingEventItem &pendingEvent) {
@@ -304,13 +258,13 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
});
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated();
return EventHandler::timeString(m_event.get(), QStringLiteral("hh:mm"), m_isPending, lastUpdated);
return eventHandler.getTimeString(QStringLiteral("hh:mm"), m_isPending, lastUpdated);
}
if (role == AuthorRole) {
return QVariant::fromValue<NeochatRoomMember *>(m_eventSenderObject.get());
}
if (role == MediaInfoRole) {
return EventHandler::mediaInfo(m_room, m_event.get());
return eventHandler.getMediaInfo();
}
if (role == FileTransferInfoRole) {
return QVariant::fromValue(m_room->cachedFileTransferInfo(m_event.get()));
@@ -319,22 +273,25 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return QVariant::fromValue<ItineraryModel *>(m_itineraryModel);
}
if (role == LatitudeRole) {
return EventHandler::latitude(m_event.get());
return eventHandler.getLatitude();
}
if (role == LongitudeRole) {
return EventHandler::longitude(m_event.get());
return eventHandler.getLongitude();
}
if (role == AssetRole) {
return EventHandler::locationAssetType(m_event.get());
return eventHandler.getLocationAssetType();
}
if (role == PollHandlerRole) {
return QVariant::fromValue<PollHandler *>(m_room->poll(m_eventId));
}
if (role == IsReplyRole) {
return eventHandler.hasReply();
}
if (role == ReplyEventIdRole) {
return EventHandler::replyId(m_event.get());
return eventHandler.getReplyId();
}
if (role == ReplyAuthorRole) {
return QVariant::fromValue(EventHandler::replyAuthor(m_room, m_event.get()));
return QVariant::fromValue(eventHandler.getReplyAuthor());
}
if (role == ReplyContentModelRole) {
return QVariant::fromValue<MessageContentModel *>(m_replyModel);
@@ -347,12 +304,6 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview);
}
}
if (role == ChatBarCacheRole) {
if (m_room->threadCache()->threadId() == m_eventId) {
return QVariant::fromValue<ChatBarCache *>(m_room->threadCache());
}
return QVariant::fromValue<ChatBarCache *>(m_room->editCache());
}
return {};
}
@@ -380,11 +331,11 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
roles[LongitudeRole] = "longitude";
roles[AssetRole] = "asset";
roles[PollHandlerRole] = "pollHandler";
roles[IsReplyRole] = "isReply";
roles[ReplyEventIdRole] = "replyEventId";
roles[ReplyAuthorRole] = "replyAuthor";
roles[ReplyContentModelRole] = "replyContentModel";
roles[LinkPreviewerRole] = "linkPreviewer";
roles[ChatBarCacheRole] = "chatBarCache";
return roles;
}
@@ -393,12 +344,6 @@ void MessageContentModel::resetModel()
beginResetModel();
m_components.clear();
if ((m_event && m_room->connection()->isIgnored(m_event->senderId())) || m_notFound) {
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
endResetModel();
return;
}
if (m_event == nullptr) {
m_components += MessageComponent{MessageComponentType::Loading, QString(), {}};
endResetModel();
@@ -413,7 +358,7 @@ void MessageContentModel::resetModel()
endResetModel();
}
void MessageContentModel::resetContent(bool isEditing, bool isThreading)
void MessageContentModel::resetContent(bool isEditing)
{
Q_ASSERT(m_event != nullptr);
@@ -422,7 +367,7 @@ void MessageContentModel::resetContent(bool isEditing, bool isThreading)
m_components.remove(startRow, rowCount() - startRow);
endRemoveRows();
const auto newComponents = messageContentComponents(isEditing, isThreading);
const auto newComponents = messageContentComponents(isEditing);
if (newComponents.size() == 0) {
return;
}
@@ -431,7 +376,7 @@ void MessageContentModel::resetContent(bool isEditing, bool isThreading)
endInsertRows();
}
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing, bool isThreading)
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing)
{
QList<MessageComponent> newComponents;
@@ -451,45 +396,35 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
}
if (isEditing) {
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
newComponents += MessageComponent{MessageComponentType::Edit, QString(), {}};
} else {
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*m_event.get())));
EventHandler eventHandler(m_room, m_event.get());
newComponents.append(componentsForType(eventHandler.messageComponentType()));
}
if (m_room->urlPreviewEnabled()) {
newComponents = addLinkPreviews(newComponents);
}
// If the event is already threaded the ThreadModel will handle displaying a chat bar.
if (isThreading && !EventHandler::isThreaded(m_event.get())) {
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
}
return newComponents;
}
void MessageContentModel::updateReplyModel()
{
if (m_event == nullptr || m_isReply) {
if (m_event == nullptr || m_replyModel != nullptr || m_isReply) {
return;
}
if (!EventHandler::hasReply(m_event.get()) || (EventHandler::isThreaded(m_event.get()) && NeoChatConfig::self()->threads())) {
if (m_replyModel) {
delete m_replyModel;
}
EventHandler eventHandler(m_room, m_event.get());
if (!eventHandler.hasReply()) {
return;
}
if (m_replyModel != nullptr) {
return;
}
const auto replyEvent = m_room->findInTimeline(EventHandler::replyId(m_event.get()));
const auto replyEvent = m_room->findInTimeline(eventHandler.getReplyId());
if (replyEvent == m_room->historyEdge()) {
m_replyModel = new MessageContentModel(m_room, EventHandler::replyId(m_event.get()), true, false, this);
m_replyModel = new MessageContentModel(m_room, eventHandler.getReplyId(), true);
} else {
m_replyModel = new MessageContentModel(m_room, replyEvent->get(), true, false, this);
m_replyModel = new MessageContentModel(m_room, replyEvent->get(), true);
}
connect(m_replyModel, &MessageContentModel::eventUpdated, this, [this]() {

View File

@@ -10,6 +10,7 @@
#include <Quotient/room.h>
#include "enums/messagecomponenttype.h"
#include "eventhandler.h"
#include "itinerarymodel.h"
#include "neochatroommember.h"
@@ -65,21 +66,17 @@ public:
AssetRole, /**< Type of location event, e.g. self pin of the user location. */
PollHandlerRole, /**< The PollHandler for the event, if any. */
IsReplyRole, /**< Is the message a reply to another event. */
ReplyEventIdRole, /**< The matrix ID of the message that was replied to. */
ReplyAuthorRole, /**< The author of the event that was replied to. */
ReplyContentModelRole, /**< The MessageContentModel for the reply event. */
LinkPreviewerRole, /**< The link preview details. */
ChatBarCacheRole, /**< The ChatBarCache to use. */
};
Q_ENUM(Roles)
explicit MessageContentModel(NeoChatRoom *room,
const Quotient::RoomEvent *event,
bool isReply = false,
bool isPending = false,
MessageContentModel *parent = nullptr);
MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply = false, bool isPending = false, MessageContentModel *parent = nullptr);
explicit MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply = false, bool isPending = false);
MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply = false, bool isPending = false);
bool showAuthor() const;
void setShowAuthor(bool showAuthor);
@@ -126,7 +123,6 @@ private:
bool m_isPending;
bool m_showAuthor = true;
bool m_isReply;
bool m_notFound = false;
void initializeModel();
void intiializeEvent(const QString &eventId);
@@ -134,8 +130,8 @@ private:
QList<MessageComponent> m_components;
void resetModel();
void resetContent(bool isEditing = false, bool isThreading = false);
QList<MessageComponent> messageContentComponents(bool isEditing = false, bool isThreading = false);
void resetContent(bool isEditing = false);
QList<MessageComponent> messageContentComponents(bool isEditing = false);
QPointer<MessageContentModel> m_replyModel;
void updateReplyModel();

View File

@@ -22,8 +22,13 @@
#include "enums/delegatetype.h"
#include "eventhandler.h"
#include "events/pollevent.h"
#include "linkpreviewer.h"
#include "messagecontentmodel.h"
#include "models/messagefiltermodel.h"
#include "models/reactionmodel.h"
#include "neochatroom.h"
#include "neochatroommember.h"
#include "readmarkermodel.h"
#include "texthandler.h"
using namespace Quotient;
@@ -66,11 +71,6 @@ MessageEventModel::MessageEventModel(QObject *parent)
connect(this, &MessageEventModel::modelReset, this, [this]() {
resetting = false;
});
connect(NeoChatConfig::self(), &NeoChatConfig::ThreadsChanged, this, [this]() {
beginResetModel();
endResetModel();
});
}
NeoChatRoom *MessageEventModel::room() const
@@ -88,6 +88,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
// HACK: Reset the model to a null room first to make sure QML dismantles
// last room's objects before the room is actually changed
beginResetModel();
m_readMarkerModels.clear();
m_currentRoom->disconnect(this);
m_currentRoom = nullptr;
endResetModel();
@@ -95,9 +96,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
// Don't clear the member objects until the model has been fully reset and all
// refs cleared.
m_memberObjects.clear();
m_contentModels.clear();
m_reactionModels.clear();
m_readMarkerModels.clear();
}
beginResetModel();
@@ -432,30 +430,31 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
const auto pendingIt = m_currentRoom->pendingEvents().crbegin() + std::min(row, timelineBaseIndex());
const auto &evt = isPending ? **pendingIt : **timelineIt;
EventHandler eventHandler(m_currentRoom, &evt);
if (role == Qt::DisplayRole) {
if (evt.isRedacted()) {
auto reason = evt.redactedBecause()->reason();
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>")
: i18n("<i>[This message was deleted: %1]</i>", evt.redactedBecause()->reason());
}
return EventHandler::richBody(m_currentRoom, &evt);
return eventHandler.getRichBody();
}
if (role == ContentModelRole) {
QString modelId;
if (!evt.id().isEmpty() && m_contentModels.contains(evt.id())) {
modelId = evt.id();
} else if (!evt.transactionId().isEmpty() && m_contentModels.contains(evt.transactionId())) {
modelId = evt.transactionId();
if (!evt.isStateEvent() && !evt.id().isEmpty()) {
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_currentRoom, &evt));
}
if (!modelId.isEmpty()) {
return QVariant::fromValue<MessageContentModel *>(m_contentModels.at(modelId).get());
if (evt.isStateEvent()) {
if (evt.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_currentRoom, &evt));
}
}
return {};
}
if (role == GenericDisplayRole) {
return EventHandler::genericBody(&evt);
return eventHandler.getGenericBody();
}
if (role == DelegateTypeRole) {
@@ -478,7 +477,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == HighlightRole) {
return EventHandler::isHighlighted(m_currentRoom, &evt);
return eventHandler.isHighlighted();
}
if (role == SpecialMarksRole) {
@@ -491,11 +490,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return pendingIt->deliveryStatus();
}
if (EventHandler::isHidden(m_currentRoom, &evt)) {
return EventStatus::Hidden;
}
if (EventHandler::isThreaded(&evt) && EventHandler::threadRoot(&evt) != EventHandler::id(&evt) && NeoChatConfig::threads()) {
if (eventHandler.isHidden()) {
return EventStatus::Hidden;
}
@@ -503,7 +498,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == EventIdRole) {
return EventHandler::id(&evt);
return eventHandler.getId();
}
if (role == ProgressInfoRole) {
@@ -519,20 +514,20 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
if (role == TimeRole) {
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
return EventHandler::time(&evt, isPending, lastUpdated);
return eventHandler.getTime(isPending, lastUpdated);
}
if (role == SectionRole) {
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
return EventHandler::timeString(&evt, true, QLocale::ShortFormat, isPending, lastUpdated);
return eventHandler.getTimeString(true, QLocale::ShortFormat, isPending, lastUpdated);
}
if (role == IsThreadedRole) {
return EventHandler::isThreaded(&evt);
return eventHandler.isThreaded();
}
if (role == ThreadRootRole) {
return EventHandler::threadRoot(&evt);
return eventHandler.threadRoot();
}
if (role == ShowSectionRole) {
@@ -585,7 +580,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == AuthorDisplayNameRole) {
return EventHandler::authorDisplayName(m_currentRoom, &evt, isPending);
return eventHandler.getAuthorDisplayName(isPending);
}
if (role == IsRedactedRole) {
@@ -597,11 +592,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == MediaInfoRole) {
return EventHandler::mediaInfo(m_currentRoom, &evt);
return eventHandler.getMediaInfo();
}
if (role == IsEditableRole) {
return MessageComponentType::typeForEvent(evt) == MessageComponentType::Text && evt.senderId() == m_currentRoom->localMember().id();
return eventHandler.messageComponentType() == MessageComponentType::Text && evt.senderId() == m_currentRoom->localMember().id();
}
return {};
@@ -629,9 +624,6 @@ void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event)
auto eventId = event->id();
auto senderId = event->senderId();
if (eventId.isEmpty()) {
eventId = event->transactionId();
}
// A pending event might not have a sender ID set yet but in that case it must
// be the local member.
if (senderId.isEmpty()) {
@@ -642,16 +634,6 @@ void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event)
m_memberObjects[senderId] = std::unique_ptr<NeochatRoomMember>(new NeochatRoomMember(m_currentRoom, senderId));
}
if (!m_contentModels.contains(eventId) && !m_contentModels.contains(event->transactionId())) {
if (!event->isStateEvent() || event->matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
m_contentModels[eventId] = std::unique_ptr<MessageContentModel>(new MessageContentModel(m_currentRoom, event));
}
}
if (EventHandler::isThreaded(event) && !m_threadModels.contains(EventHandler::threadRoot(event))) {
m_threadModels[EventHandler::threadRoot(event)] = QSharedPointer<ThreadModel>(new ThreadModel(EventHandler::threadRoot(event), m_currentRoom));
}
// ReadMarkerModel handles updates to add and remove markers, we only need to
// handle adding and removing whole models here.
if (m_readMarkerModels.contains(eventId)) {
@@ -711,9 +693,4 @@ bool MessageEventModel::event(QEvent *event)
return QObject::event(event);
}
ThreadModel *MessageEventModel::threadModelForRootId(const QString &threadRootId) const
{
return m_threadModels[threadRootId].data();
}
#include "moc_messageeventmodel.cpp"

View File

@@ -8,12 +8,10 @@
#include <QQmlEngine>
#include "linkpreviewer.h"
#include "messagecontentmodel.h"
#include "neochatroom.h"
#include "neochatroommember.h"
#include "pollhandler.h"
#include "readmarkermodel.h"
#include "threadmodel.h"
class ReactionModel;
@@ -56,8 +54,8 @@ public:
ContentModelRole, /**< The MessageContentModel for the event. */
IsThreadedRole, /**< Whether the message is in a thread. */
ThreadRootRole, /**< The Matrix ID of the thread root message, if any . */
IsThreadedRole,
ThreadRootRole,
ShowSectionRole, /**< Whether the section header should be shown. */
@@ -106,8 +104,6 @@ public:
*/
Q_INVOKABLE [[nodiscard]] int eventIdToRow(const QString &eventID) const;
Q_INVOKABLE ThreadModel *threadModelForRootId(const QString &threadRootId) const;
protected:
bool event(QEvent *event) override;
@@ -121,9 +117,7 @@ private:
KFormat m_format;
std::map<QString, std::unique_ptr<NeochatRoomMember>> m_memberObjects;
std::map<QString, std::unique_ptr<MessageContentModel>> m_contentModels;
QMap<QString, QSharedPointer<ReadMarkerModel>> m_readMarkerModels;
QMap<QString, QSharedPointer<ThreadModel>> m_threadModels;
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
[[nodiscard]] int timelineBaseIndex() const;

View File

@@ -8,8 +8,10 @@
#include "enums/delegatetype.h"
#include "messagecontentmodel.h"
#include "messageeventmodel.h"
#include "neochatconfig.h"
#include "neochatroommember.h"
#include "timelinemodel.h"
using namespace Quotient;

View File

@@ -121,11 +121,12 @@ void NotificationsModel::loadData()
const auto &authorAvatar = avatar.isValid() && avatar.scheme() == QStringLiteral("mxc") ? avatar : QUrl();
const auto &roomEvent = eventCast<const RoomEvent>(notification.event.get());
EventHandler eventHandler(dynamic_cast<NeoChatRoom *>(room), roomEvent);
beginInsertRows({}, m_notifications.length(), m_notifications.length());
m_notifications += Notification{
.roomId = notification.roomId,
.text = room->member(authorId).htmlSafeDisplayName() + (roomEvent->is<StateEvent>() ? QStringLiteral(" ") : QStringLiteral(": "))
+ EventHandler::plainBody(dynamic_cast<NeoChatRoom *>(room), roomEvent, true),
+ eventHandler.getPlainBody(true),
.authorName = room->member(authorId).htmlSafeDisplayName(),
.authorAvatar = authorAvatar,
.eventId = roomEvent->id(),

View File

@@ -2,40 +2,20 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "permissionsmodel.h"
#include "powerlevel.h"
#include <Quotient/events/roompowerlevelsevent.h>
#include <KLazyLocalizedString>
#include "powerlevel.h"
using namespace Qt::Literals::StringLiterals;
namespace
{
constexpr auto UsersDefaultKey = "users_default"_L1;
constexpr auto StateDefaultKey = "state_default"_L1;
constexpr auto EventsDefaultKey = "events_default"_L1;
constexpr auto InviteKey = "invite"_L1;
constexpr auto KickKey = "kick"_L1;
constexpr auto BanKey = "ban"_L1;
constexpr auto RedactKey = "redact"_L1;
static const QStringList defaultPermissions = {
UsersDefaultKey,
StateDefaultKey,
EventsDefaultKey,
};
static const QStringList basicPermissions = {
InviteKey,
KickKey,
BanKey,
RedactKey,
};
static const QStringList knownPermissions = {
QStringLiteral("users_default"),
QStringLiteral("state_default"),
QStringLiteral("events_default"),
QStringLiteral("invite"),
QStringLiteral("kick"),
QStringLiteral("ban"),
QStringLiteral("redact"),
QStringLiteral("m.reaction"),
QStringLiteral("m.room.redaction"),
QStringLiteral("m.room.power_levels"),
@@ -53,14 +33,14 @@ static const QStringList knownPermissions = {
};
// Alternate name text for default permissions.
static const QHash<QString, KLazyLocalizedString> permissionNames = {
{UsersDefaultKey, kli18nc("Room permission type", "Default user power level")},
{StateDefaultKey, kli18nc("Room permission type", "Default power level to set the room state")},
{EventsDefaultKey, kli18nc("Room permission type", "Default power level to send messages")},
{InviteKey, kli18nc("Room permission type", "Invite users")},
{KickKey, kli18nc("Room permission type", "Kick users")},
{BanKey, kli18nc("Room permission type", "Ban users")},
{RedactKey, kli18nc("Room permission type", "Remove messages sent by other users")},
static const QHash<QString, KLazyLocalizedString> defaultPermissionNames = {
{QStringLiteral("users_default"), kli18nc("Room permission type", "Default user power level")},
{QStringLiteral("state_default"), kli18nc("Room permission type", "Default power level to set the room state")},
{QStringLiteral("events_default"), kli18nc("Room permission type", "Default power level to send messages")},
{QStringLiteral("invite"), kli18nc("Room permission type", "Invite users")},
{QStringLiteral("kick"), kli18nc("Room permission type", "Kick users")},
{QStringLiteral("ban"), kli18nc("Room permission type", "Ban users")},
{QStringLiteral("redact"), kli18nc("Room permission type", "Remove messages sent by other users")},
{QStringLiteral("m.reaction"), kli18nc("Room permission type", "Send reactions")},
{QStringLiteral("m.room.redaction"), kli18nc("Room permission type", "Remove their own messages")},
{QStringLiteral("m.room.power_levels"), kli18nc("Room permission type", "Change user permissions")},
@@ -78,10 +58,10 @@ static const QHash<QString, KLazyLocalizedString> permissionNames = {
};
// Subtitles for the default values.
static const QHash<QString, KLazyLocalizedString> permissionSubtitles = {
{UsersDefaultKey, kli18nc("Room permission type", "This is the power level for all new users when joining the room")},
{StateDefaultKey, kli18nc("Room permission type", "This is used for all state events that do not have their own entry here")},
{EventsDefaultKey, kli18nc("Room permission type", "This is used for all message events that do not have their own entry here")},
static const QHash<QString, KLazyLocalizedString> defaultSubtitles = {
{QStringLiteral("users_default"), kli18nc("Room permission type", "This is the power level for all new users when joining the room")},
{QStringLiteral("state_default"), kli18nc("Room permission type", "This is used for all state events that do not have their own entry here")},
{QStringLiteral("events_default"), kli18nc("Room permission type", "This is used for all message events that do not have their own entry here")},
};
// Permissions that should use the event default.
@@ -90,7 +70,6 @@ static const QStringList eventPermissions = {
QStringLiteral("m.reaction"),
QStringLiteral("m.room.redaction"),
};
};
PermissionsModel::PermissionsModel(QObject *parent)
: QAbstractListModel(parent)
@@ -130,8 +109,6 @@ void PermissionsModel::initializeModel()
}
m_permissions.append(defaultPermissions);
m_permissions.append(basicPermissions);
m_permissions.append(knownPermissions);
for (const auto &event : currentPowerLevelEvent->events().keys()) {
if (!m_permissions.contains(event)) {
@@ -154,17 +131,17 @@ QVariant PermissionsModel::data(const QModelIndex &index, int role) const
const auto permission = m_permissions.value(index.row());
if (role == NameRole) {
if (permissionNames.keys().contains(permission)) {
return permissionNames.value(permission).toString();
if (defaultPermissionNames.keys().contains(permission)) {
return defaultPermissionNames.value(permission).toString();
}
return permission;
}
if (role == SubtitleRole) {
if (knownPermissions.contains(permission) && permissionNames.keys().contains(permission)) {
if (permission.startsWith(QLatin1String("m.")) && defaultPermissionNames.keys().contains(permission)) {
return permission;
}
if (permissionSubtitles.contains(permission)) {
return permissionSubtitles.value(permission).toString();
if (defaultSubtitles.contains(permission)) {
return defaultSubtitles.value(permission).toString();
}
return QString();
}
@@ -189,10 +166,11 @@ QVariant PermissionsModel::data(const QModelIndex &index, int role) const
return QString();
}
if (role == IsDefaultValueRole) {
return defaultPermissions.contains(permission);
return permission.contains(QLatin1String("default"));
}
if (role == IsBasicPermissionRole) {
return basicPermissions.contains(permission);
return permission == QStringLiteral("invite") || permission == QStringLiteral("kick") || permission == QStringLiteral("ban")
|| permission == QStringLiteral("redact");
}
return {};
}
@@ -223,19 +201,19 @@ std::optional<int> PermissionsModel::powerLevel(const QString &permission) const
}
if (const auto currentPowerLevelEvent = m_room->currentState().get<Quotient::RoomPowerLevelsEvent>()) {
if (permission == BanKey) {
if (permission == QStringLiteral("ban")) {
return currentPowerLevelEvent->ban();
} else if (permission == KickKey) {
} else if (permission == QStringLiteral("kick")) {
return currentPowerLevelEvent->kick();
} else if (permission == InviteKey) {
} else if (permission == QStringLiteral("invite")) {
return currentPowerLevelEvent->invite();
} else if (permission == RedactKey) {
} else if (permission == QStringLiteral("redact")) {
return currentPowerLevelEvent->redact();
} else if (permission == UsersDefaultKey) {
} else if (permission == QStringLiteral("users_default")) {
return currentPowerLevelEvent->usersDefault();
} else if (permission == StateDefaultKey) {
} else if (permission == QStringLiteral("state_default")) {
return currentPowerLevelEvent->stateDefault();
} else if (permission == EventsDefaultKey) {
} else if (permission == QStringLiteral("events_default")) {
return currentPowerLevelEvent->eventsDefault();
} else if (eventPermissions.contains(permission)) {
return currentPowerLevelEvent->powerLevelForEvent(permission);
@@ -263,9 +241,6 @@ void PermissionsModel::setPowerLevel(const QString &permission, const int &newPo
auto powerLevelContent = currentPowerLevelEvent->contentJson();
if (powerLevelContent.contains(permission)) {
powerLevelContent[permission] = clampPowerLevel;
// Deal with the case where a default or basic permission is missing from the event content erroneously.
} else if (defaultPermissions.contains(permission) || basicPermissions.contains(permission)) {
powerLevelContent[permission] = clampPowerLevel;
} else {
auto eventPowerLevels = powerLevelContent[QLatin1String("events")].toObject();
eventPowerLevels[permission] = clampPowerLevel;

View File

@@ -11,6 +11,7 @@
#include <Quotient/jobs/basejob.h>
#include "neochatconfig.h"
#include "neochatconnection.h"
#include <KLazyLocalizedString>

View File

@@ -2,13 +2,21 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "reactionmodel.h"
#include "utils.h"
#include "neochatroom.h"
#include <QDebug>
#include <QFont>
#ifdef HAVE_ICU
#include <QTextBoundaryFinder>
#include <QTextCharFormat>
#include <unicode/uchar.h>
#include <unicode/urename.h>
#endif
#include <KLocalizedString>
#include <Quotient/roommember.h>
ReactionModel::ReactionModel(const Quotient::RoomMessageEvent *event, NeoChatRoom *room)
: QAbstractListModel(nullptr)
, m_room(room)
@@ -149,6 +157,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();
@@ -162,7 +194,7 @@ QString ReactionModel::reactionText(QString text) const
.arg(m_room->connection()->makeMediaUrl(QUrl(text)).toString(), QString::number(size));
}
return Utils::isEmoji(text) ? QStringLiteral("<span style=\"font-family: 'emoji';\">") + text + QStringLiteral("</span>") : text;
return isEmoji(text) ? QStringLiteral("<span style=\"font-family: 'emoji';\">") + text + QStringLiteral("</span>") : text;
}
#include "moc_reactionmodel.cpp"

View File

@@ -245,7 +245,8 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
if (room->lastEvent() == nullptr || room->lastEventIsSpoiler()) {
return QString();
}
return EventHandler::subtitleText(room, room->lastEvent());
EventHandler eventHandler(room, room->lastEvent());
return eventHandler.subtitleText();
}
if (role == AvatarImageRole) {
return room->avatar(128);

View File

@@ -357,7 +357,8 @@ QVariant RoomTreeModel::data(const QModelIndex &index, int role) const
if (room->lastEvent() == nullptr || room->lastEventIsSpoiler()) {
return QString();
}
return EventHandler::subtitleText(room, room->lastEvent());
EventHandler eventHandler(room, room->lastEvent());
return eventHandler.subtitleText();
}
if (role == AvatarImageRole) {
return room->avatar(128);

View File

@@ -7,6 +7,7 @@
#include "eventhandler.h"
#include "models/messagecontentmodel.h"
#include "neochatroom.h"
#include "neochatroommember.h"
#include <QGuiApplication>
@@ -89,6 +90,8 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
auto row = index.row();
const auto &event = *m_result->results[row].result;
EventHandler eventHandler(m_room, &event);
switch (role) {
case AuthorRole:
return QVariant::fromValue<NeochatRoomMember *>(m_memberObjects.at(event.senderId()).get());
@@ -98,7 +101,7 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
}
return event.originTimestamp().date() != m_result->results[row - 1].result->originTimestamp().date();
case SectionRole:
return EventHandler::timeString(&event, true);
return eventHandler.getTimeString(true);
case ShowReactionsRole:
return false;
case ShowReadMarkersRole:
@@ -106,13 +109,13 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
case IsPendingRole:
return false;
case HighlightRole:
return EventHandler::isHighlighted(m_room, &event);
return eventHandler.isHighlighted();
case EventIdRole:
return EventHandler::id(&event);
return eventHandler.getId();
case IsThreadedRole:
return EventHandler::isThreaded(&event);
return eventHandler.isThreaded();
case ThreadRootRole:
return EventHandler::threadRoot(&event);
return eventHandler.threadRoot();
case ContentModelRole: {
if (!event.isStateEvent()) {
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_room, &event));

View File

@@ -7,6 +7,7 @@
#include <Quotient/room.h>
#include "neochatconnection.h"
#include "neochatroom.h"
SpaceChildrenModel::SpaceChildrenModel(QObject *parent)
: QAbstractItemModel(parent)

View File

@@ -1,190 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "threadmodel.h"
#include <Quotient/events/event.h>
#include <Quotient/events/stickerevent.h>
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
#include <memory>
#include "chatbarcache.h"
#include "eventhandler.h"
#include "messagecomponenttype.h"
#include "neochatroom.h"
ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
: QConcatenateTablesProxyModel(room)
, m_threadRootId(threadRootId)
, m_threadChatBarModel(new ThreadChatBarModel(this, room))
{
Q_ASSERT(!m_threadRootId.isEmpty());
Q_ASSERT(room);
m_threadRootContentModel = std::unique_ptr<MessageContentModel>(new MessageContentModel(room, threadRootId));
connect(room, &Quotient::Room::pendingEventAboutToAdd, this, [this](Quotient::RoomEvent *event) {
if (auto roomEvent = eventCast<const Quotient::RoomMessageEvent>(event)) {
if (EventHandler::isThreaded(roomEvent) && EventHandler::threadRoot(roomEvent) == m_threadRootId) {
addNewEvent(event);
addModels();
}
}
});
connect(room, &Quotient::Room::aboutToAddNewMessages, this, [this](Quotient::RoomEventsRange events) {
for (const auto &event : events) {
if (auto roomEvent = eventCast<const Quotient::RoomMessageEvent>(event)) {
if (EventHandler::isThreaded(roomEvent) && EventHandler::threadRoot(roomEvent) == m_threadRootId) {
addNewEvent(roomEvent);
}
}
}
addModels();
});
fetchMore({});
addModels();
}
QString ThreadModel::threadRootId() const
{
return m_threadRootId;
}
MessageContentModel *ThreadModel::threadRootContentModel() const
{
return m_threadRootContentModel.get();
}
QHash<int, QByteArray> ThreadModel::roleNames() const
{
return m_threadRootContentModel->roleNames();
}
bool ThreadModel::canFetchMore(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return !m_currentJob && m_nextBatch.has_value();
}
void ThreadModel::fetchMore(const QModelIndex &parent)
{
Q_UNUSED(parent);
if (!m_currentJob && m_nextBatch.has_value()) {
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
const auto connection = room->connection();
m_currentJob =
connection->callApi<Quotient::GetRelatingEventsWithRelTypeJob>(room->id(), m_threadRootId, QLatin1String("m.thread"), *m_nextBatch, QString(), 5);
connect(m_currentJob, &Quotient::BaseJob::success, this, [this]() {
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
auto newEvents = m_currentJob->chunk();
for (auto &event : newEvents) {
m_contentModels.push_back(new MessageContentModel(room, event.get()));
}
addModels();
const auto newNextBatch = m_currentJob->nextBatch();
if (!newNextBatch.isEmpty() && *m_nextBatch != newNextBatch) {
*m_nextBatch = newNextBatch;
} else {
// Insert the thread root at the end.
beginInsertRows({}, rowCount(), rowCount());
endInsertRows();
m_nextBatch.reset();
}
m_currentJob.clear();
});
}
}
void ThreadModel::addNewEvent(const Quotient::RoomEvent *event)
{
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
m_contentModels.push_front(new MessageContentModel(room, event));
}
void ThreadModel::addModels()
{
if (!sourceModels().isEmpty()) {
clearModels();
}
addSourceModel(m_threadRootContentModel.get());
for (auto it = m_contentModels.crbegin(); it != m_contentModels.crend(); ++it) {
addSourceModel(*it);
}
addSourceModel(m_threadChatBarModel);
beginResetModel();
endResetModel();
}
void ThreadModel::clearModels()
{
removeSourceModel(m_threadRootContentModel.get());
for (const auto &model : m_contentModels) {
if (sourceModels().contains(model)) {
removeSourceModel(model);
}
}
removeSourceModel(m_threadChatBarModel);
}
ThreadChatBarModel::ThreadChatBarModel(QObject *parent, NeoChatRoom *room)
: QAbstractListModel(parent)
, m_room(room)
{
if (m_room != nullptr) {
connect(m_room->threadCache(), &ChatBarCache::threadIdChanged, this, [this](const QString &oldThreadId, const QString &newThreadId) {
const auto threadModel = dynamic_cast<ThreadModel *>(this->parent());
if (threadModel != nullptr && (oldThreadId == threadModel->threadRootId() || newThreadId == threadModel->threadRootId())) {
beginResetModel();
endResetModel();
}
});
}
}
QVariant ThreadChatBarModel::data(const QModelIndex &idx, int role) const
{
if (idx.row() > 1) {
return {};
}
if (role == ComponentTypeRole) {
return MessageComponentType::ChatBar;
}
if (role == ChatBarCacheRole) {
if (m_room == nullptr) {
return {};
}
return QVariant::fromValue<ChatBarCache *>(m_room->threadCache());
}
return {};
}
int ThreadChatBarModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
if (m_room == nullptr) {
return 0;
}
const auto threadModel = dynamic_cast<ThreadModel *>(this->parent());
if (threadModel != nullptr) {
return m_room->threadCache()->threadId() == threadModel->threadRootId() ? 1 : 0;
}
return 0;
}
QHash<int, QByteArray> ThreadChatBarModel::roleNames() const
{
return {
{ComponentTypeRole, "componentType"},
{ChatBarCacheRole, "chatBarCache"},
};
}
#include "moc_threadmodel.cpp"

View File

@@ -1,141 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <Quotient/util.h>
#include <QConcatenateTablesProxyModel>
#include <QQmlEngine>
#include <QPointer>
#include <Quotient/csapi/relations.h>
#include <Quotient/events/roomevent.h>
#include <Quotient/events/roommessageevent.h>
#include <deque>
#include <optional>
#include "linkpreviewer.h"
#include "messagecontentmodel.h"
class NeoChatRoom;
class ReactionModel;
/**
* @class ThreadChatBarModel
*
* A model to provide a chat bar component to send new messages in a thread.
*/
class ThreadChatBarModel : public QAbstractListModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*
* The role values need to match MessageContentModel not to blow up.
*
* @sa MessageContentModel
*/
enum Roles {
ComponentTypeRole = MessageContentModel::ComponentTypeRole, /**< The type of component to visualise the message. */
ChatBarCacheRole = MessageContentModel::ChatBarCacheRole, /**< The ChatBarCache to use. */
};
Q_ENUM(Roles)
explicit ThreadChatBarModel(QObject *parent, NeoChatRoom *room);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief 1 or 0, depending on whether a chat bar should be shown.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a map with ComponentTypeRole it's the only one.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
QPointer<NeoChatRoom> m_room;
};
/**
* @class ThreadModel
*
* This class defines the model for visualising a thread.
*
* The class also provides functions to access the data of the root event, typically
* used to visualise the thread in a list of room threads.
*/
class ThreadModel : public QConcatenateTablesProxyModel
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
explicit ThreadModel(const QString &threadRootId, NeoChatRoom *room);
QString threadRootId() const;
/**
* @brief The content model for the thread root event.
*/
MessageContentModel *threadRootContentModel() const;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Whether there is more data available for the model to fetch.
*
* @sa QAbstractItemModel::canFetchMore()
*/
bool canFetchMore(const QModelIndex &parent) const override;
/**
* @brief Fetches the next batch of model data if any is available.
*
* @sa QAbstractItemModel::fetchMore()
*/
void fetchMore(const QModelIndex &parent) override;
private:
QString m_threadRootId;
std::unique_ptr<MessageContentModel> m_threadRootContentModel;
std::deque<MessageContentModel *> m_contentModels;
ThreadChatBarModel *m_threadChatBarModel;
QList<QString> m_events;
QList<QString> m_pendingEvents;
std::unordered_map<QString, std::unique_ptr<Quotient::RoomEvent>> m_unloadedEvents;
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
QPointer<Quotient::GetRelatingEventsWithRelTypeJob> m_currentJob = nullptr;
std::optional<QString> m_nextBatch = QString();
bool m_addingPending = false;
void addNewEvent(const Quotient::RoomEvent *event);
void addModels();
void clearModels();
};

View File

@@ -3,15 +3,11 @@
#include "timelinemodel.h"
#include <Quotient/qt_connection_util.h>
#include "delegatetype.h"
TimelineModel::TimelineModel(QObject *parent)
: QConcatenateTablesProxyModel(parent)
{
m_timelineBeginningModel = new TimelineBeginningModel(this);
addSourceModel(m_timelineBeginningModel);
m_messageEventModel = new MessageEventModel(this);
addSourceModel(m_messageEventModel);
m_timelineEndModel = new TimelineEndModel(this);
@@ -27,7 +23,6 @@ void TimelineModel::setRoom(NeoChatRoom *room)
{
// Both models do their own null checking so just pass along.
m_messageEventModel->setRoom(room);
m_timelineBeginningModel->setRoom(room);
m_timelineEndModel->setRoom(room);
}
@@ -41,69 +36,6 @@ QHash<int, QByteArray> TimelineModel::roleNames() const
return m_messageEventModel->roleNames();
}
TimelineBeginningModel::TimelineBeginningModel(QObject *parent)
: QAbstractListModel(parent)
{
}
void TimelineBeginningModel::setRoom(NeoChatRoom *room)
{
if (room == m_room) {
return;
}
beginResetModel();
if (m_room != nullptr) {
m_room->disconnect(this);
}
m_room = room;
if (m_room != nullptr) {
Quotient::connectUntil(m_room.get(), &Quotient::Room::eventsHistoryJobChanged, this, [this]() {
if (m_room && m_room->allHistoryLoaded()) {
// HACK: We have to do it this way because DelegateChooser doesn't update dynamically.
beginRemoveRows({}, 0, 0);
endRemoveRows();
beginInsertRows({}, 0, 0);
endInsertRows();
return true;
}
return false;
});
}
endResetModel();
}
QVariant TimelineBeginningModel::data(const QModelIndex &idx, int role) const
{
Q_UNUSED(idx)
if (m_room == nullptr) {
return {};
}
if (role == DelegateTypeRole) {
return DelegateType::Successor;
}
return {};
}
int TimelineBeginningModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
if (m_room == nullptr) {
return 0;
}
return m_room->successorId().isEmpty() ? 0 : 1;
}
QHash<int, QByteArray> TimelineBeginningModel::roleNames() const
{
return {{DelegateTypeRole, "delegateType"}};
}
TimelineEndModel::TimelineEndModel(QObject *parent)
: QAbstractListModel(parent)
{
@@ -146,11 +78,7 @@ QVariant TimelineEndModel::data(const QModelIndex &idx, int role) const
}
if (role == DelegateTypeRole) {
if (idx.row() == 1 || rowCount() == 1) {
return m_room->allHistoryLoaded() ? DelegateType::TimelineEnd : DelegateType::Loading;
} else {
return DelegateType::Predecessor;
}
return m_room->allHistoryLoaded() ? DelegateType::TimelineEnd : DelegateType::Loading;
}
return {};
}
@@ -158,10 +86,7 @@ QVariant TimelineEndModel::data(const QModelIndex &idx, int role) const
int TimelineEndModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
if (m_room == nullptr) {
return 0;
}
return m_room->predecessorId().isEmpty() ? 1 : (m_room->allHistoryLoaded() ? 2 : 1);
return 1;
}
QHash<int, QByteArray> TimelineEndModel::roleNames() const

View File

@@ -10,57 +10,6 @@
#include "messageeventmodel.h"
#include "neochatroom.h"
/**
* @class TimelineBeginningModel
*
* A model to provide a delegate at the start of the timeline to show upgrades.
*/
class TimelineBeginningModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
DelegateTypeRole = MessageEventModel::DelegateTypeRole, /**< The delegate type of the message. */
};
Q_ENUM(Roles)
explicit TimelineBeginningModel(QObject *parent = nullptr);
/**
* @brief Set the room for the timeline.
*/
void setRoom(NeoChatRoom *room);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief 1, the answer is always 1.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a map with DelegateTypeRole it's the only one.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
QPointer<NeoChatRoom> m_room = nullptr;
};
/**
* @class TimelineEndModel
*
@@ -159,6 +108,5 @@ Q_SIGNALS:
private:
MessageEventModel *m_messageEventModel = nullptr;
TimelineBeginningModel *m_timelineBeginningModel = nullptr;
TimelineEndModel *m_timelineEndModel = nullptr;
};

View File

@@ -7,6 +7,7 @@
#include <Quotient/avatar.h>
#include <Quotient/events/roompowerlevelsevent.h>
#include <Quotient/room.h>
#include "enums/powerlevel.h"
#include "neochatroom.h"

View File

@@ -125,7 +125,7 @@ void WebShortcutModel::trigger(const QString &data)
void WebShortcutModel::configureWebShortcuts()
{
#ifdef HAVE_KIO
auto job = new KIO::CommandLauncherJob(QStringLiteral("kcmshell6"), QStringList() << QStringLiteral("webshortcuts"), this);
auto job = new KIO::CommandLauncherJob(QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("webshortcuts"), this);
job->exec();
#endif
}

View File

@@ -261,7 +261,7 @@ Action=Popup
Name=Share
Name[ar]=شارك
Name[ca]=Compartició
Name[ca@valencia]=Compartició
Name[ca@valencia]=Compartiu
Name[cs]=Sdílet
Name[en_GB]=Share
Name[eo]=Kundividi

View File

@@ -15,6 +15,9 @@
<entry name="OpenRoom" type="String">
<label>Latest opened room</label>
</entry>
<entry name="ColorScheme" type="String">
<label>Color scheme</label>
</entry>
<entry name="Blur" type="bool">
<label>Make NeoChat blurry</label>
<default>false</default>
@@ -86,10 +89,6 @@
<label>Show deleted messages in the timeline</label>
<default>false</default>
</entry>
<entry name="HideImages" type="bool">
<label>Hide images in the timeline</label>
<default>false</default>
</entry>
<entry name="ShowLinkPreview" type="bool">
<label>Show preview of the links in the chat messages</label>
</entry>

9
src/neochatconfig.kcfgc Normal file
View File

@@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
# SPDX-License-Identifier: LGPL-2.1-or-later
File=neochatconfig.kcfg
ClassName=NeoChatConfig
Mutators=true
DefaultValueGetters=true
GenerateProperties=true
ParentInConstructor=true
Singleton=true

View File

@@ -9,12 +9,14 @@
#include "controller.h"
#include "jobs/neochatchangepasswordjob.h"
#include "jobs/neochatdeactivateaccountjob.h"
#include "linkpreviewer.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "notificationsmanager.h"
#include "roommanager.h"
#include "spacehierarchycache.h"
#include <Quotient/connection.h>
#include <Quotient/jobs/basejob.h>
#include <Quotient/quotient_common.h>
#include <qt6keychain/keychain.h>
@@ -44,7 +46,6 @@ NeoChatConnection::NeoChatConnection(QObject *parent)
: Connection(parent)
, m_threePIdModel(new ThreePIdModel(this))
{
m_linkPreviewers.setMaxCost(20);
connectSignals();
}
@@ -52,7 +53,6 @@ NeoChatConnection::NeoChatConnection(const QUrl &server, QObject *parent)
: Connection(server, parent)
, m_threePIdModel(new ThreePIdModel(this))
{
m_linkPreviewers.setMaxCost(20);
connectSignals();
}
@@ -564,31 +564,14 @@ LinkPreviewer *NeoChatConnection::previewerForLink(const QUrl &link)
return nullptr;
}
auto previewer = m_linkPreviewers.object(link);
auto previewer = m_linkPreviewers.value(link, nullptr);
if (previewer != nullptr) {
return previewer;
}
previewer = new LinkPreviewer(link, this);
m_linkPreviewers.insert(link, previewer);
m_linkPreviewers[link] = previewer;
return previewer;
}
#if Quotient_VERSION_MINOR > 8
KeyImport::Error NeoChatConnection::exportMegolmSessions(const QString &passphrase, const QString &path)
{
KeyImport keyImport;
auto result = keyImport.exportKeys(passphrase, this);
if (!result.has_value()) {
return result.error();
}
QUrl url(path);
QFile file(url.toLocalFile());
file.open(QFile::WriteOnly);
file.write(result.value());
file.close();
return KeyImport::Success;
}
#endif
#include "moc_neochatconnection.cpp"

View File

@@ -3,20 +3,16 @@
#pragma once
#include <QCache>
#include <QObject>
#include <QQmlEngine>
#include <QCoroTask>
#include <Quotient/connection.h>
#if Quotient_VERSION_MINOR > 8
#include <Quotient/keyimport.h>
#endif
#include "linkpreviewer.h"
#include "models/threepidmodel.h"
class LinkPreviewer;
class NeoChatConnection : public Quotient::Connection
{
Q_OBJECT
@@ -173,10 +169,6 @@ public:
*/
Q_INVOKABLE QString accountDataJsonString(const QString &type) const;
#if Quotient_VERSION_MINOR > 8
Q_INVOKABLE Quotient::KeyImport::Error exportMegolmSessions(const QString &passphrase, const QString &path);
#endif
qsizetype directChatNotifications() const;
bool directChatsHaveHighlightNotifications() const;
qsizetype homeNotifications() const;
@@ -222,7 +214,7 @@ private:
int m_badgeNotificationCount = 0;
QCache<QUrl, LinkPreviewer> m_linkPreviewers;
QHash<QUrl, LinkPreviewer *> m_linkPreviewers;
bool m_canCheckMutualRooms = false;
};

View File

@@ -64,7 +64,6 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
{
m_mainCache = new ChatBarCache(this);
m_editCache = new ChatBarCache(this);
m_threadCache = new ChatBarCache(this);
connect(connection, &Connection::accountDataChanged, this, &NeoChatRoom::updatePushNotificationState);
connect(this, &Room::fileTransferCompleted, this, [this] {
@@ -149,7 +148,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
if (NeoChatConfig::rejectUnknownInvites()) {
auto job = this->connection()->callApi<NeochatGetCommonRoomsJob>(roomMemberEvent->senderId());
connect(job, &BaseJob::result, this, [this, job, showNotification] {
connect(job, &BaseJob::result, this, [this, job, roomMemberEvent, showNotification] {
QJsonObject replyData = job->jsonData();
if (replyData.contains(QStringLiteral("joined"))) {
const bool inAnyOfOurRooms = !replyData[QStringLiteral("joined")].toArray().isEmpty();
@@ -303,16 +302,10 @@ void NeoChatRoom::acceptInvitation()
void NeoChatRoom::forget()
{
QStringList roomIds{id()};
NeoChatRoom *predecessor = this;
while (predecessor = dynamic_cast<NeoChatRoom *>(predecessor->predecessor(JoinState::Join)), predecessor && !roomIds.contains(predecessor->id())) {
roomIds += predecessor->id();
}
for (const auto &id : roomIds) {
connection()->forgetRoom(id);
if (const auto &predecessor = dynamic_cast<NeoChatRoom *>(this->predecessor(JoinState::Join))) {
predecessor->forget();
}
connection()->forgetRoom(id());
}
void NeoChatRoom::sendTypingNotification(bool isTyping)
@@ -523,10 +516,9 @@ void NeoChatRoom::postMessage(const QString &rawText,
MessageEventType type,
const QString &replyEventId,
const QString &relateToEventId,
const QString &threadRootId,
const QString &fallbackId)
const QString &threadRootId)
{
postHtmlMessage(rawText, text, type, replyEventId, relateToEventId, threadRootId, fallbackId);
postHtmlMessage(rawText, text, type, replyEventId, relateToEventId, threadRootId);
}
void NeoChatRoom::postHtmlMessage(const QString &text,
@@ -534,8 +526,7 @@ void NeoChatRoom::postHtmlMessage(const QString &text,
MessageEventType type,
const QString &replyEventId,
const QString &relateToEventId,
const QString &threadRootId,
const QString &fallbackId)
const QString &threadRootId)
{
bool isReply = !replyEventId.isEmpty();
bool isEdit = !relateToEventId.isEmpty();
@@ -546,19 +537,9 @@ void NeoChatRoom::postHtmlMessage(const QString &text,
}
if (isThread) {
bool isFallingBack = !fallbackId.isEmpty();
QString replyEventId = isFallingBack ? fallbackId : QString();
if (isReply) {
isFallingBack = false;
replyEventId = EventHandler::id(replyIt->get());
}
EventHandler eventHandler(this, &**replyIt);
// If we are not replying and there is no fallback ID it means a new thread
// is being created.
if (!isFallingBack && !isReply) {
isFallingBack = true;
replyEventId = threadRootId;
}
const bool isFallingBack = !eventHandler.isThreaded();
// clang-format off
QJsonObject json{
@@ -603,10 +584,12 @@ void NeoChatRoom::postHtmlMessage(const QString &text,
if (isReply) {
const auto &replyEvt = **replyIt;
EventHandler eventHandler(this, &**replyIt);
// clang-format off
QJsonObject json{
{"msgtype"_ls, msgTypeToString(type)},
{"body"_ls, "> <%1> %2\n\n%3"_ls.arg(replyEvt.senderId(), EventHandler::plainBody(this, &replyEvt), text)},
{"body"_ls, "> <%1> %2\n\n%3"_ls.arg(replyEvt.senderId(), eventHandler.getPlainBody(), text)},
{"format"_ls, "org.matrix.custom.html"_ls},
{"m.relates_to"_ls,
QJsonObject {
@@ -618,7 +601,7 @@ void NeoChatRoom::postHtmlMessage(const QString &text,
}
},
{"formatted_body"_ls,
"<mx-reply><blockquote><a href=\"https://matrix.to/#/%1/%2\">In reply to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br>%5</blockquote></mx-reply>%6"_ls.arg(id(), replyEventId, replyEvt.senderId(), replyEvt.senderId(), EventHandler::richBody(this, &replyEvt), html)
"<mx-reply><blockquote><a href=\"https://matrix.to/#/%1/%2\">In reply to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br>%5</blockquote></mx-reply>%6"_ls.arg(id(), replyEventId, replyEvt.senderId(), replyEvt.senderId(), eventHandler.getRichBody(), html)
}
};
// clang-format on
@@ -1549,11 +1532,6 @@ ChatBarCache *NeoChatRoom::editCache() const
return m_editCache;
}
ChatBarCache *NeoChatRoom::threadCache() const
{
return m_threadCache;
}
void NeoChatRoom::replyLastMessage()
{
const auto &timelineBottom = messageEvents().rbegin();
@@ -1770,7 +1748,6 @@ void NeoChatRoom::downloadEventFromServer(const QString &eventId)
connect(job, &BaseJob::success, this, [this, job, eventId] {
// The event may have arrived in the meantime so check it's not in the timeline.
if (findInTimeline(eventId) != historyEdge()) {
Q_EMIT extraEventLoaded(eventId);
return;
}
@@ -1778,11 +1755,6 @@ void NeoChatRoom::downloadEventFromServer(const QString &eventId)
m_extraEvents.push_back(std::move(event));
Q_EMIT extraEventLoaded(eventId);
});
connect(job, &BaseJob::failure, this, [this, job, eventId] {
if (job->error() == BaseJob::NotFound) {
Q_EMIT extraEventNotFound(eventId);
}
});
}
const RoomEvent *NeoChatRoom::getEvent(const QString &eventId) const

View File

@@ -201,11 +201,6 @@ class NeoChatRoom : public Quotient::Room
*/
Q_PROPERTY(ChatBarCache *editCache READ editCache CONSTANT)
/**
* @brief The cache for the thread chat bar in the room.
*/
Q_PROPERTY(ChatBarCache *threadCache READ threadCache CONSTANT)
#if Quotient_VERSION_MINOR == 8
Q_PROPERTY(QList<Quotient::RoomMember> otherMembersTyping READ otherMembersTyping NOTIFY typingChanged)
#endif
@@ -516,8 +511,6 @@ public:
ChatBarCache *editCache() const;
ChatBarCache *threadCache() const;
/**
* @brief Reply to the last message sent in the timeline.
*
@@ -616,7 +609,6 @@ private:
ChatBarCache *m_mainCache;
ChatBarCache *m_editCache;
ChatBarCache *m_threadCache;
QCache<QString, PollHandler> m_polls;
std::vector<Quotient::event_ptr_tt<Quotient::RoomEvent>> m_extraEvents;
@@ -655,7 +647,6 @@ Q_SIGNALS:
void urlPreviewEnabledChanged();
void maxRoomVersionChanged();
void extraEventLoaded(const QString &eventId);
void extraEventNotFound(const QString &eventId);
public Q_SLOTS:
/**
@@ -700,8 +691,7 @@ public Q_SLOTS:
Quotient::MessageEventType type = Quotient::MessageEventType::Text,
const QString &replyEventId = QString(),
const QString &relateToEventId = QString(),
const QString &threadRootId = QString(),
const QString &fallbackId = QString());
const QString &threadRootId = QString());
/**
* @brief Send an html message to the room.
@@ -717,8 +707,7 @@ public Q_SLOTS:
Quotient::MessageEventType type = Quotient::MessageEventType::Text,
const QString &replyEventId = QString(),
const QString &relateToEventId = QString(),
const QString &threadRootId = QString(),
const QString &fallbackId = QString());
const QString &threadRootId = QString());
/**
* @brief Set the room avatar.

View File

@@ -4,34 +4,8 @@
{
"Name": "Tobias Fella",
"Name[ar]": "توبياس فلة",
"Name[ca@valencia]": "Tobias Fella",
"Name[ca]": "Tobias Fella",
"Name[cs]": "Tobias Fella",
"Name[de]": "Tobias Fella",
"Name[en_GB]": "Tobias Fella",
"Name[eo]": "Tobias Fella",
"Name[es]": "Tobias Fella",
"Name[eu]": "Tobias Fella",
"Name[fi]": "Tobias Fella",
"Name[fr]": "Tobias Fella",
"Name[gl]": "Tobias Fella",
"Name[he]": "טוביאס פלה",
"Name[hu]": "Tobias Fella",
"Name[ia]": "Tobias Fella",
"Name[it]": "Tobias Fella",
"Name[ka]": "Tobias Fella",
"Name[lv]": "Tobias Fella",
"Name[nl]": "Tobias Fella",
"Name[nn]": "Tobias Fella",
"Name[pl]": "Tobias Fella",
"Name[ru]": "Tobias Fella",
"Name[sl]": "Tobias Fella",
"Name[sv]": "Tobias Fella",
"Name[ta]": "டோபியாஸ் ஃபெல்லா",
"Name[tr]": "Tobias Fella",
"Name[uk]": "Tobias Fella",
"Name[x-test]": "xxTobias Fellaxx",
"Name[zh_TW]": "Tobias Fella"
"Name[x-test]": "xxTobias Fellaxx"
}
],
"Category": "Utilities",
@@ -40,14 +14,12 @@
"Description[ca@valencia]": "Compartix a través de NeoChat",
"Description[ca]": "Comparteix a través del NeoChat",
"Description[de]": "Über NeoChat teilen",
"Description[en_GB]": "Share via NeoChat",
"Description[eo]": "Kundividi per NeoChat",
"Description[es]": "Compartir mediante NeoChat",
"Description[eu]": "Partekatu NeoChat bidez",
"Description[fi]": "Jaa NeoChatillä",
"Description[fr]": "Partager grâce à NeoChat",
"Description[gl]": "Compartir por NeoChat",
"Description[he]": "שיתוף דרך NeoChat",
"Description[hu]": "Megosztás NeoChatben",
"Description[ia]": "Comparti via NeoChat",
"Description[it]": "Condividi tramite NeoChat",
@@ -56,6 +28,7 @@
"Description[nl]": "Delen via NeoChat",
"Description[nn]": "Del via NeoChat",
"Description[pl]": "Udostępnij przez NeoChat",
"Description[pt_BR]": "Compartilhar via NeoChat",
"Description[ru]": "Опубликовать в NeoChat",
"Description[sl]": "Deli prek NeoChat",
"Description[sv]": "Dela via NeoChat",
@@ -68,35 +41,9 @@
"License": "GPL",
"Name": "NeoChat",
"Name[ar]": "نيوتشات",
"Name[ast]": "NeoChat",
"Name[ca@valencia]": "NeoChat",
"Name[ca]": "NeoChat",
"Name[cs]": "NeoChat",
"Name[de]": "NeoChat",
"Name[en_GB]": "NeoChat",
"Name[eo]": "NeoChat",
"Name[es]": "NeoChat",
"Name[eu]": "NeoChat",
"Name[fi]": "NeoChat",
"Name[fr]": "NeoChat",
"Name[gl]": "NeoChat",
"Name[he]": "NeoChat",
"Name[hu]": "NeoChat",
"Name[ia]": "Neochat",
"Name[it]": "NeoChat",
"Name[ka]": "NeoChat",
"Name[lv]": "NeoChat",
"Name[nl]": "NeoChat",
"Name[nn]": "NeoChat",
"Name[pl]": "NeoChat",
"Name[ru]": "NeoChat",
"Name[sl]": "NeoChat",
"Name[sv]": "NeoChat",
"Name[ta]": "நியோச்சாட்",
"Name[tr]": "NeoChat",
"Name[uk]": "NeoChat",
"Name[x-test]": "xxNeoChatxx",
"Name[zh_TW]": "NeoChat",
"X-Purpose-ActionDisplay": "NeoChat"
},
"X-Purpose-PluginTypes": [

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