Compare commits

..

2 Commits

Author SHA1 Message Date
James Graham
90829e4959 Refactor based on better implementation 2024-05-28 19:15:54 +01:00
James Graham
6a1ff37011 Use standard ItemDelegates rather than the kirigami-addons special ones 2024-05-27 18:41:53 +01:00
224 changed files with 32832 additions and 40781 deletions

View File

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

View File

@@ -8,7 +8,7 @@ include:
- /gitlab-templates/android-qt6.yml - /gitlab-templates/android-qt6.yml
- /gitlab-templates/linux-qt6.yml - /gitlab-templates/linux-qt6.yml
- /gitlab-templates/windows-qt6.yml - /gitlab-templates/windows-qt6.yml
# - /gitlab-templates/freebsd-qt6.yml - /gitlab-templates/freebsd-qt6.yml
- /gitlab-templates/flatpak.yml - /gitlab-templates/flatpak.yml
- /gitlab-templates/craft-android-qt6-apks.yml - /gitlab-templates/craft-android-qt6-apks.yml
- /gitlab-templates/craft-appimage-qt6.yml - /gitlab-templates/craft-appimage-qt6.yml

View File

@@ -40,4 +40,4 @@ Dependencies:
Options: Options:
per-test-timeout: 90 per-test-timeout: 90
require-passing-tests-on: [ 'Linux', 'Android', 'FreeBSD' ] require-passing-tests-on: [ '@all' ]

View File

@@ -8,8 +8,8 @@ cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script. # KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "24") set(RELEASE_SERVICE_VERSION_MAJOR "24")
set(RELEASE_SERVICE_VERSION_MINOR "08") set(RELEASE_SERVICE_VERSION_MINOR "07")
set(RELEASE_SERVICE_VERSION_MICRO "0") set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}") set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION}) project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
@@ -60,9 +60,6 @@ set_package_properties(Qt6 PROPERTIES
) )
qt_policy(SET QTP0001 NEW) qt_policy(SET QTP0001 NEW)
if (QT_KNOWN_POLICY_QTP0004)
qt_policy(SET QTP0004 NEW)
endif ()
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme) find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme)
set_package_properties(KF6 PROPERTIES set_package_properties(KF6 PROPERTIES
@@ -105,7 +102,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED) find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
endif() endif()
find_package(QuotientQt6 0.8.2) find_package(QuotientQt6 0.7)
set_package_properties(QuotientQt6 PROPERTIES set_package_properties(QuotientQt6 PROPERTIES
TYPE REQUIRED TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API" DESCRIPTION "Qt wrapper around Matrix API"

View File

@@ -38,8 +38,8 @@ Due to the nature of the Matrix specification development NeoChat also supports
Details where to find stable releases for NeoChat can be found on its [homepage](https://apps.kde.org/neochat). Details where to find stable releases for NeoChat can be found on its [homepage](https://apps.kde.org/neochat).
Nightly builds for Linux and Windows can be downloaded from [cdn.kde.org](https://cdn.kde.org/ci-builds/network/neochat/). Nightly builds for linux and windows can be downloaded from [cdn.kde.org](https://cdn.kde.org/ci-builds/network/neochat/).
Nightly builds for Android are available from [KDE's nightly F-Droid repository](https://community.kde.org/Android/F-Droid). Nightly builds for android are available from [KDE's nightly F-Droid repository](https://community.kde.org/Android/F-Droid).
Nightly Flatpaks are available from [KDE's nightly Flatpak repository](https://userbase.kde.org/Tutorials/Flatpak). Nightly Flatpaks are available from [KDE's nightly Flatpak repository](https://userbase.kde.org/Tutorials/Flatpak).
## Building NeoChat ## Building NeoChat

View File

@@ -6,7 +6,6 @@
#include <QObject> #include <QObject>
#include <QTest> #include <QTest>
#include <Quotient/roommember.h>
#include <Quotient/syncdata.h> #include <Quotient/syncdata.h>
#include <qtestcase.h> #include <qtestcase.h>
@@ -51,7 +50,7 @@ void ChatBarCacheTest::empty()
QCOMPARE(chatBarCache->replyId(), QString()); QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false); QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString()); QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationUser(), room->member(QString())); QCOMPARE(chatBarCache->relationUser(), room->getUser(nullptr));
QCOMPARE(chatBarCache->relationMessage(), QString()); QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), QString()); QCOMPARE(chatBarCache->attachmentPath(), QString());
} }
@@ -65,7 +64,7 @@ void ChatBarCacheTest::noRoom()
// ChatBarCache has no parent. // ChatBarCache has no parent.
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation."); QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationUser(), Quotient::RoomMember()); QCOMPARE(chatBarCache->relationUser(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation."); QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString()); QCOMPARE(chatBarCache->relationMessage(), QString());
@@ -81,7 +80,7 @@ void ChatBarCacheTest::badParent()
// ChatBarCache has no parent. // ChatBarCache has no parent.
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation."); QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationUser(), Quotient::RoomMember()); QCOMPARE(chatBarCache->relationUser(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation."); QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString()); QCOMPARE(chatBarCache->relationMessage(), QString());
@@ -99,7 +98,7 @@ void ChatBarCacheTest::reply()
QCOMPARE(chatBarCache->replyId(), QLatin1String("$153456789:example.org")); QCOMPARE(chatBarCache->replyId(), QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->isEditing(), false); QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString()); QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationUser(), room->member(QLatin1String("@example:example.org"))); QCOMPARE(chatBarCache->relationUser(), room->getUser(room->user(QLatin1String("@example:example.org"))));
QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message")); QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message"));
QCOMPARE(chatBarCache->attachmentPath(), QString()); QCOMPARE(chatBarCache->attachmentPath(), QString());
} }
@@ -116,7 +115,7 @@ void ChatBarCacheTest::edit()
QCOMPARE(chatBarCache->replyId(), QString()); QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), true); QCOMPARE(chatBarCache->isEditing(), true);
QCOMPARE(chatBarCache->editId(), QLatin1String("$153456789:example.org")); QCOMPARE(chatBarCache->editId(), QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->relationUser(), room->member(QLatin1String("@example:example.org"))); QCOMPARE(chatBarCache->relationUser(), room->getUser(room->user(QLatin1String("@example:example.org"))));
QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message")); QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message"));
QCOMPARE(chatBarCache->attachmentPath(), QString()); QCOMPARE(chatBarCache->attachmentPath(), QString());
} }
@@ -133,7 +132,7 @@ void ChatBarCacheTest::attachment()
QCOMPARE(chatBarCache->replyId(), QString()); QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false); QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString()); QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationUser(), room->member(QString())); QCOMPARE(chatBarCache->relationUser(), room->getUser(nullptr));
QCOMPARE(chatBarCache->relationMessage(), QString()); QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), QLatin1String("some/path")); QCOMPARE(chatBarCache->attachmentPath(), QLatin1String("some/path"));
} }

View File

@@ -25,7 +25,7 @@
"content": { "content": {
"user_ids": [ "user_ids": [
"@alice:matrix.org", "@alice:matrix.org",
"@bob:kde.org" "@bob:example.com"
] ]
}, },
"room_id": "!jEsUZKDJdhlrceRyVU:example.org", "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
@@ -35,7 +35,7 @@
"content": { "content": {
"$153456789:example.org": { "$153456789:example.org": {
"m.read": { "m.read": {
"@alice:example.org": { "@alice:matrix.org": {
"ts": 1436451550453 "ts": 1436451550453
} }
} }
@@ -47,7 +47,7 @@
"content": { "content": {
"$1532735824654:example.org": { "$1532735824654:example.org": {
"m.read": { "m.read": {
"@bob:kde.org": { "@bob:example.com": {
"ts": 1436451550453 "ts": 1436451550453
} }
} }
@@ -67,18 +67,6 @@
}, },
"type": "m.receipt" "type": "m.receipt"
}, },
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@tim2:example.com": {
"ts": 1436451550454
}
}
}
},
"type": "m.receipt"
},
{ {
"content": { "content": {
"$1532735824654:example.org": { "$1532735824654:example.org": {
@@ -148,22 +136,6 @@
"age": 1234 "age": 1234
} }
}, },
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Bob",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "bob:kde.org",
"state_key": "@bob:kde.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{ {
"content": { "content": {
"displayname": "Look\nat\nme\nI\nput\nnewlines\nin\nmy\ndisplay name", "displayname": "Look\nat\nme\nI\nput\nnewlines\nin\nmy\ndisplay name",
@@ -184,7 +156,7 @@
"summary": { "summary": {
"m.heroes": [ "m.heroes": [
"@alice:example.com", "@alice:example.com",
"@bob:kde.org" "@bob:example.com"
], ],
"m.invited_member_count": 0, "m.invited_member_count": 0,
"m.joined_member_count": 2 "m.joined_member_count": 2

View File

@@ -37,14 +37,16 @@
"events": [ "events": [
{ {
"content": { "content": {
"displayname": "Example", "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"membership": "join" "displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
}, },
"event_id": "$143273582443PhrSn:example.org", "event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653, "origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org", "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org", "sender": "@example:example.org",
"state_key": "@example:example.org", "state_key": "@alice:example.org",
"type": "m.room.member", "type": "m.room.member",
"unsigned": { "unsigned": {
"age": 1234 "age": 1234

View File

@@ -130,23 +130,7 @@
"origin_server_ts": 1432735824653, "origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org", "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org", "sender": "@example:example.org",
"state_key": "@alice:matrix.org", "state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Bob",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "bob:example.org",
"state_key": "@bob:example.org",
"type": "m.room.member", "type": "m.room.member",
"unsigned": { "unsigned": {
"age": 1234 "age": 1234

View File

@@ -51,21 +51,6 @@
"unsigned": { "unsigned": {
"age": 1234 "age": 1234
} }
},
{
"content": {
"displayname": "Example",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@example:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
} }
] ]
}, },

View File

@@ -36,6 +36,8 @@ private Q_SLOTS:
void eventId(); void eventId();
void nullEventId(); void nullEventId();
void author();
void nullAuthor();
void authorDisplayName(); void authorDisplayName();
void nullAuthorDisplayName(); void nullAuthorDisplayName();
void singleLineSidplayName(); void singleLineSidplayName();
@@ -73,6 +75,8 @@ private Q_SLOTS:
void nullThread(); void nullThread();
void location(); void location();
void nullLocation(); void nullLocation();
void readMarkers();
void nullReadMarkers();
}; };
void EventHandlerTest::initTestCase() void EventHandlerTest::initTestCase()
@@ -94,6 +98,33 @@ void EventHandlerTest::nullEventId()
QCOMPARE(noEventHandler.getId(), QString()); QCOMPARE(noEventHandler.getId(), QString());
} }
void EventHandlerTest::author()
{
auto event = room->messageEvents().at(0).get();
auto author = room->user(event->senderId());
EventHandler eventHandler(room, event);
auto eventHandlerAuthor = eventHandler.getAuthor();
QCOMPARE(eventHandlerAuthor["isLocalUser"_ls], author->id() == room->localUser()->id());
QCOMPARE(eventHandlerAuthor["id"_ls], author->id());
QCOMPARE(eventHandlerAuthor["displayName"_ls], author->displayname(room));
QCOMPARE(eventHandlerAuthor["avatarSource"_ls], room->avatarForMember(author));
QCOMPARE(eventHandlerAuthor["avatarMediaId"_ls], author->avatarMediaId(room));
QCOMPARE(eventHandlerAuthor["color"_ls], Utils::getUserColor(author->hueF()));
QCOMPARE(eventHandlerAuthor["object"_ls], QVariant::fromValue(author));
}
void EventHandlerTest::nullAuthor()
{
QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getAuthor(), QVariantMap());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_event set to nullptr. Returning empty user.");
QCOMPARE(noEventHandler.getAuthor(), room->getUser(nullptr));
}
void EventHandlerTest::authorDisplayName() void EventHandlerTest::authorDisplayName()
{ {
EventHandler eventHandler(room, room->messageEvents().at(1).get()); EventHandler eventHandler(room, room->messageEvents().at(1).get());
@@ -163,7 +194,6 @@ void EventHandlerTest::timeString()
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::LongFormat)); QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::LongFormat));
QCOMPARE(eventHandler.getTimeString(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)); format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::LongFormat));
QCOMPARE(eventHandler.getTimeString(QStringLiteral("hh:mm")), QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toString(QStringLiteral("hh:mm")));
} }
void EventHandlerTest::nullTimeString() void EventHandlerTest::nullTimeString()
@@ -363,30 +393,31 @@ void EventHandlerTest::nullReplyId()
void EventHandlerTest::replyAuthor() void EventHandlerTest::replyAuthor()
{ {
auto replyEvent = room->messageEvents().at(0).get(); auto replyEvent = room->messageEvents().at(0).get();
auto replyAuthor = room->member(replyEvent->senderId()); auto replyAuthor = room->user(replyEvent->senderId());
EventHandler eventHandler(room, room->messageEvents().at(5).get()); EventHandler eventHandler(room, room->messageEvents().at(5).get());
auto eventHandlerReplyAuthor = eventHandler.getReplyAuthor(); auto eventHandlerReplyAuthor = eventHandler.getReplyAuthor();
QCOMPARE(eventHandlerReplyAuthor.isLocalMember(), replyAuthor.id() == room->localMember().id()); QCOMPARE(eventHandlerReplyAuthor["isLocalUser"_ls], replyAuthor->id() == room->localUser()->id());
QCOMPARE(eventHandlerReplyAuthor.id(), replyAuthor.id()); QCOMPARE(eventHandlerReplyAuthor["id"_ls], replyAuthor->id());
QCOMPARE(eventHandlerReplyAuthor.displayName(), replyAuthor.displayName()); QCOMPARE(eventHandlerReplyAuthor["displayName"_ls], replyAuthor->displayname(room));
QCOMPARE(eventHandlerReplyAuthor.avatarUrl(), replyAuthor.avatarUrl()); QCOMPARE(eventHandlerReplyAuthor["avatarSource"_ls], room->avatarForMember(replyAuthor));
QCOMPARE(eventHandlerReplyAuthor.avatarMediaId(), replyAuthor.avatarMediaId()); QCOMPARE(eventHandlerReplyAuthor["avatarMediaId"_ls], replyAuthor->avatarMediaId(room));
QCOMPARE(eventHandlerReplyAuthor.color(), replyAuthor.color()); QCOMPARE(eventHandlerReplyAuthor["color"_ls], Utils::getUserColor(replyAuthor->hueF()));
QCOMPARE(eventHandlerReplyAuthor["object"_ls], QVariant::fromValue(replyAuthor));
EventHandler eventHandlerNoAuthor(room, room->messageEvents().at(0).get()); EventHandler eventHandlerNoAuthor(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoAuthor.getReplyAuthor(), RoomMember()); QCOMPARE(eventHandlerNoAuthor.getReplyAuthor(), room->getUser(nullptr));
} }
void EventHandlerTest::nullReplyAuthor() void EventHandlerTest::nullReplyAuthor()
{ {
QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_room set to nullptr."); QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyAuthor(), RoomMember()); QCOMPARE(emptyHandler.getReplyAuthor(), QVariantMap());
EventHandler noEventHandler(room, nullptr); EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_event set to nullptr. Returning empty user."); QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_event set to nullptr. Returning empty user.");
QCOMPARE(noEventHandler.getReplyAuthor(), RoomMember()); QCOMPARE(noEventHandler.getReplyAuthor(), room->getUser(nullptr));
} }
void EventHandlerTest::replyBody() void EventHandlerTest::replyBody()
@@ -492,5 +523,59 @@ void EventHandlerTest::nullLocation()
QCOMPARE(emptyHandler.getLocationAssetType(), QString()); QCOMPARE(emptyHandler.getLocationAssetType(), QString());
} }
void EventHandlerTest::readMarkers()
{
EventHandler eventHandler(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandler.hasReadMarkers(), true);
auto readMarkers = eventHandler.getReadMarkers();
QCOMPARE(readMarkers.size(), 1);
QCOMPARE(readMarkers[0].toMap()["id"_ls], QStringLiteral("@alice:matrix.org"));
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QString());
QCOMPARE(eventHandler.getReadMarkersString(), QStringLiteral("1 user: @alice:matrix.org"));
EventHandler eventHandler2(room, room->messageEvents().at(2).get());
QCOMPARE(eventHandler2.hasReadMarkers(), true);
readMarkers = eventHandler2.getReadMarkers();
QCOMPARE(readMarkers.size(), 5);
QCOMPARE(eventHandler2.getNumberExcessReadMarkers(), QStringLiteral("+ 1"));
// There are no guarantees on the order of the users it will be different every time so don't match the whole string.
QCOMPARE(eventHandler2.getReadMarkersString().startsWith(QStringLiteral("6 users:")), true);
}
void EventHandlerTest::nullReadMarkers()
{
QTest::ignoreMessage(QtWarningMsg, "hasReadMarkers called with m_room set to nullptr.");
QCOMPARE(emptyHandler.hasReadMarkers(), false);
QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReadMarkers(), QVariantList());
QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getNumberExcessReadMarkers(), QString());
QTest::ignoreMessage(QtWarningMsg, "getReadMarkersString called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReadMarkersString(), QString());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "hasReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.hasReadMarkers(), false);
QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReadMarkers(), QVariantList());
QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getNumberExcessReadMarkers(), QString());
QTest::ignoreMessage(QtWarningMsg, "getReadMarkersString called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReadMarkersString(), QString());
}
QTEST_MAIN(EventHandlerTest) QTEST_MAIN(EventHandlerTest)
#include "eventhandlertest.moc" #include "eventhandlertest.moc"

View File

@@ -52,8 +52,10 @@ void ReactionModelTest::basicReaction()
QCOMPARE(model.data(model.index(0), ReactionModel::TextContentRole), QStringLiteral("<span style=\"font-family: 'emoji';\">👍</span>")); QCOMPARE(model.data(model.index(0), ReactionModel::TextContentRole), QStringLiteral("<span style=\"font-family: 'emoji';\">👍</span>"));
QCOMPARE(model.data(model.index(0), ReactionModel::ReactionRole), QStringLiteral("👍")); QCOMPARE(model.data(model.index(0), ReactionModel::ReactionRole), QStringLiteral("👍"));
QCOMPARE(model.data(model.index(0), ReactionModel::ToolTipRole), QCOMPARE(model.data(model.index(0), ReactionModel::ToolTipRole),
QStringLiteral("Alice Margatroid reacted with <span style=\"font-family: 'emoji';\">👍</span>")); QStringLiteral("@alice:matrix.org reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
QCOMPARE(model.data(model.index(0), ReactionModel::HasLocalMember), false); auto authorList = QVariantList{room->getUser(room->user(QStringLiteral("@alice:matrix.org")))};
QCOMPARE(model.data(model.index(0), ReactionModel::AuthorsRole), authorList);
QCOMPARE(model.data(model.index(0), ReactionModel::HasLocalUser), false);
} }
void ReactionModelTest::newReaction() void ReactionModelTest::newReaction()
@@ -63,7 +65,7 @@ void ReactionModelTest::newReaction()
QCOMPARE(model->rowCount(), 1); QCOMPARE(model->rowCount(), 1);
QCOMPARE(model->data(model->index(0), ReactionModel::ToolTipRole), QCOMPARE(model->data(model->index(0), ReactionModel::ToolTipRole),
QStringLiteral("Alice Margatroid reacted with <span style=\"font-family: 'emoji';\">👍</span>")); QStringLiteral("@alice:matrix.org reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
QSignalSpy spy(model, SIGNAL(modelReset())); QSignalSpy spy(model, SIGNAL(modelReset()));
@@ -72,7 +74,7 @@ void ReactionModelTest::newReaction()
QCOMPARE(spy.count(), 2); // Once for each of the 2 new reactions. QCOMPARE(spy.count(), 2); // Once for each of the 2 new reactions.
QCOMPARE(model->data(model->index(1), ReactionModel::ReactionRole), QStringLiteral("😆")); QCOMPARE(model->data(model->index(1), ReactionModel::ReactionRole), QStringLiteral("😆"));
QCOMPARE(model->data(model->index(0), ReactionModel::ToolTipRole), QCOMPARE(model->data(model->index(0), ReactionModel::ToolTipRole),
QStringLiteral("Alice Margatroid and Bob reacted with <span style=\"font-family: 'emoji';\">👍</span>")); QStringLiteral("@alice:matrix.org and @bob:example.org reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
delete model; delete model;
} }

View File

@@ -25,7 +25,6 @@
<name xml:lang="fi">NeoChat</name> <name xml:lang="fi">NeoChat</name>
<name xml:lang="fr">NeoChat</name> <name xml:lang="fr">NeoChat</name>
<name xml:lang="gl">NeoChat</name> <name xml:lang="gl">NeoChat</name>
<name xml:lang="he">NeoChat</name>
<name xml:lang="hu">NeoChat</name> <name xml:lang="hu">NeoChat</name>
<name xml:lang="ia">Neochat</name> <name xml:lang="ia">Neochat</name>
<name xml:lang="id">NeoChat</name> <name xml:lang="id">NeoChat</name>
@@ -62,7 +61,6 @@
<summary xml:lang="fi">Keskustelu ystäviesi kanssa Matrixissa</summary> <summary xml:lang="fi">Keskustelu ystäviesi kanssa Matrixissa</summary>
<summary xml:lang="fr">Discuter avec vos ami(e)s sur le réseau Matrix</summary> <summary xml:lang="fr">Discuter avec vos ami(e)s sur le réseau Matrix</summary>
<summary xml:lang="gl">Charle coas súas amizades en Matrix.</summary> <summary xml:lang="gl">Charle coas súas amizades en Matrix.</summary>
<summary xml:lang="he">התכתבות עם החברים שלך ב־matrix</summary>
<summary xml:lang="hu">Csevegjen barátaival a matrixon</summary> <summary xml:lang="hu">Csevegjen barátaival a matrixon</summary>
<summary xml:lang="ia">Starta Conversation con tu amicos sur matrix</summary> <summary xml:lang="ia">Starta Conversation con tu amicos sur matrix</summary>
<summary xml:lang="it">Conversa con i tuoi contatti su matrix</summary> <summary xml:lang="it">Conversa con i tuoi contatti su matrix</summary>
@@ -82,27 +80,21 @@
<summary xml:lang="zh-TW">在 Matrix 上與您的朋友聊天</summary> <summary xml:lang="zh-TW">在 Matrix 上與您的朋友聊天</summary>
<description> <description>
<p>NeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.</p> <p>NeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.</p>
<p xml:lang="ar">نيوتشات هو تطبيق دردشة يتيح لك الاستفادة الكاملة من شبكة Matrix. فهو يوفر لك طريقة آمنة لإرسال الرسائل النصية ومقاطع الفيديو والملفات الصوتية إلى عائلتك وزملائك وأصدقائك.</p>
<p xml:lang="ca">El NeoChat és una aplicació de xat que us permet aprofitar plenament la xarxa Matrix. Proporciona una manera segura d'enviar missatges de text, vídeos i arxius d'àudio a la vostra família, companys i amics.</p> <p xml:lang="ca">El NeoChat és una aplicació de xat que us permet aprofitar plenament la xarxa Matrix. Proporciona una manera segura d'enviar missatges de text, vídeos i arxius d'àudio a la vostra família, companys i amics.</p>
<p xml:lang="ca-valencia">NeoChat és una aplicació de xat que us permet aprofitar plenament la xarxa Matrix. Proporciona una manera segura d'enviar missatges de text, vídeos i arxius d'àudio a la vostra família, companys i amics.</p> <p xml:lang="ca-valencia">NeoChat és una aplicació de xat que us permet aprofitar plenament la xarxa Matrix. Proporciona una manera segura d'enviar missatges de text, vídeos i arxius d'àudio a la vostra família, companys i amics.</p>
<p xml:lang="en-GB">NeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.</p> <p xml:lang="en-GB">NeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.</p>
<p xml:lang="eo">NeoChat estas babilej-apo, kiu ebligas al vi plene profiti de la Matrix-reto. Ĝi provizas al vi sekuran manieron sendi tekstmesaĝojn, filmetojn kaj sondosierojn al via familio, kolegoj kaj amikoj.</p> <p xml:lang="eo">NeoChat estas babilej-apo, kiu ebligas al vi plene profiti de la Matrix-reto. Ĝi provizas al vi sekuran manieron sendi tekstmesaĝojn, filmetojn kaj sondosierojn al via familio, kolegoj kaj amikoj.</p>
<p xml:lang="es">NeoChat es una aplicación de chat que le permite aprovechar al máximo la red Matrix. Le proporciona un modo seguro de enviar mensajes de texto, vídeos y archivos de sonido a su familia, colegas y amigos.</p> <p xml:lang="es">NeoChat es una aplicación de chat que le permite aprovechar al máximo la red Matrix. Le proporciona un modo seguro de enviar mensajes de texto, vídeos y archivos de sonido a su familia, colegas y amigos.</p>
<p xml:lang="eu">NeoChat, Matrix sarearen abantaila guztiei probetsua ateratzeko aukera ematen dizun berriketa aplikaizo bat da. Zure familiari, kideei eta lagunei testu mezuak, bideoak eta audio fitxategiak era seguruan bidaltzeko aukera ematen dizu.</p> <p xml:lang="eu">NeoChat, Matrix sarearen abantaila guztiei probetsua ateratzeko aukera ematen dizun berriketa aplikaizo bat da. Zure familiari, kideei eta lagunei testu mezuak, bideoak eta audio fitxategiak era seguruan bidaltzeko aukera ematen dizu.</p>
<p xml:lang="fi">NeoChat on keskustelusovellus, jolla Matrix-verkosta saa täyden hyödyn. Se tarjoaa salatun kanavan lähettää perheelle, työkavereille ja ystäville tekstiviestejä sekä video- ja äänitiedostoja.</p>
<p xml:lang="fr">NeoChat est une application de discussions vous permettant de profiter pleinement du réseau Matrix. Elle vous offre un moyen sécurisé d'envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos ami(e)s.</p> <p xml:lang="fr">NeoChat est une application de discussions vous permettant de profiter pleinement du réseau Matrix. Elle vous offre un moyen sécurisé d'envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos ami(e)s.</p>
<p xml:lang="gl">NeoChat é unha aplicación de conversa que lle permite usar todas as funcionalidades da rede Matrix. Fornece unha forma segura de enviar mensaxes de texto e ficheiros de vídeo e son a familiares, amizades ou no traballo.</p>
<p xml:lang="he">NeoChat הוא יישום התכתבות שמאפשר לך לנצל את רשת Matrix במלואה. הוא מספק דרך מאובטחת לשליחת הודעות כתובות, סרטונים וקובצי שמע למשפחה, לעמיתים לעבודה ולחברים.</p>
<p xml:lang="hu">A NeoChat egy olyan csevegőalkalmazás, amellyel teljes mértékben kihasználhatja a Matrix hálózatot. Biztonságos módot biztosít szöveges üzenetek, videók és hangfájlok küldéséhez családtagjainak, kollégáinak és barátainak.</p> <p xml:lang="hu">A NeoChat egy olyan csevegőalkalmazás, amellyel teljes mértékben kihasználhatja a Matrix hálózatot. Biztonságos módot biztosít szöveges üzenetek, videók és hangfájlok küldéséhez családtagjainak, kollégáinak és barátainak.</p>
<p xml:lang="ia">NeoChat es un app de conversation que te permitte prender avantage plen del rete Matrix. Il te forni un modo secur de inviar messages de texto, videos e files audio a tui familia, collegas e amicos.</p> <p xml:lang="ia">NeoChat es un app de conversation que te permitte prender avantage plen del rete Matrix. Il te forni un modo secur de inviar messages de texto, videos e files audio a tui familia, collegas e amicos.</p>
<p xml:lang="it">NeoChat è un'applicazione di chat che ti consente di sfruttare appieno la rete Matrix. Ti fornisce un modo sicuro per inviare messaggi di testo, video e file audio a familiari, colleghi e amici.</p> <p xml:lang="it">NeoChat è un'applicazione di chat che ti consente di sfruttare appieno la rete Matrix. Ti fornisce un modo sicuro per inviare messaggi di testo, video e file audio a familiari, colleghi e amici.</p>
<p xml:lang="ka">NeoChat ჩატის აპია, რომელიც საშუალება გაძლევთ, Matrix-ის ქსელის საშუალებები ბოლომდე გამოიყენოთ. ის გაძლევთ უსაფრთხო გზას, გააგზავნოთ ტექსტური შეტყობინებები, ვიდეოებ და აუდიოფაილები თქვენს ოჯახთან, კოლეგებთან და მეგობრებთან.</p> <p xml:lang="ka">NeoChat ჩატის აპია, რომელიც საშუალება გაძლევთ, Matrix-ის ქსელის საშუალებები ბოლომდე გამოიყენოთ. ის გაძლევთ უსაფრთხო გზას, გააგზავნოთ ტექსტური შეტყობინებები, ვიდეოებ და აუდიოფაილები თქვენს ოჯახთან, კოლეგებთან და მეგობრებთან.</p>
<p xml:lang="lv">„NeoChat“ ir tērzēšanas programma, kas ļauj pilnvērtīgi izmantot „Matrix“ tīklu. Tā sniedz drošu veidu teksta ziņu, video un audio sūtīšanai ģimenes locekļiem, kolēģiem un draugiem.</p> <p xml:lang="lv">„NeoChat“ ir tērzēšanas programma, kas ļauj pilnvērtīgi izmantot „Matrix“ tīklu. Tā sniedz drošu veidu teksta ziņu, video un audio sūtīšanai ģimenes locekļiem, kolēģiem un draugiem.</p>
<p xml:lang="nl">NeoChat is een chat-toepassing die u het volledige voordeel van het Matrix-netwerk laat genieten. Het levert u op een veilige manier tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden te verzenden.</p> <p xml:lang="nl">NeoChat is een chat-toepassing die u het volledige voordeel van het Matrix-netwerk laat genieten. Het levert u op een veilige manier tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden te verzenden.</p>
<p xml:lang="nn">NeoChat er ein prateapp som lèt deg bruka all funksjonalitet i Matrix-nettverket. Du kan utveksla tekst, lyd og videoar med vennar, familie og kollegaar på ein trygg måte.</p>
<p xml:lang="pl">NoeChat to aplikacja do rozmów, która umożliwia wykorzystanie wszystkich możliwości Matriksa. Umożliwia wysyłanie wiadomości tekstowych, filmów i dźwięków w bezpieczny sposób do twojej rodziny, kolegów i przyjaciół.</p> <p xml:lang="pl">NoeChat to aplikacja do rozmów, która umożliwia wykorzystanie wszystkich możliwości Matriksa. Umożliwia wysyłanie wiadomości tekstowych, filmów i dźwięków w bezpieczny sposób do twojej rodziny, kolegów i przyjaciół.</p>
<p xml:lang="sl">NeoChat je aplikacija za klepet, ki vam omogoča, da v celoti izkoristite omrežje Matrix. Zagotavlja vam varen način za pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek vaši družini, sodelavcem in prijateljem.</p> <p xml:lang="sl">NeoChat je aplikacija za klepet, ki vam omogoča, da v celoti izkoristite omrežje Matrix. Zagotavlja vam varen način za pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek vaši družini, sodelavcem in prijateljem.</p>
<p xml:lang="sv">NeoChat är ett chattprogram som låter dig dra full nytta av Matrix-nätverket. Det ger dig ett säkert sätt att skicka textmeddelanden, videor och ljudfiler till din familj, kollegor och vänner.</p>
<p xml:lang="tr">NeoChat, Matrix ağının tüm özelliklerini kullanan bir sohbet uygulamasıdır. Ailenize, arkadaşlarınıza ve iş arkadaşlarınıza metin iletileri, ses ve video dosyaları göndermenin kolay bir yolunu sunar.</p> <p xml:lang="tr">NeoChat, Matrix ağının tüm özelliklerini kullanan bir sohbet uygulamasıdır. Ailenize, arkadaşlarınıza ve iş arkadaşlarınıza metin iletileri, ses ve video dosyaları göndermenin kolay bir yolunu sunar.</p>
<p xml:lang="uk">NeoChat є програмою для спілкування, за допомогою якої ви можете скористатися усіма перевагами мережі Matrix. За її допомогою ви можете безпечно надсилати текстові повідомлення, відео та звукові файли вашим родичам, колегам та друзям.</p> <p xml:lang="uk">NeoChat є програмою для спілкування, за допомогою якої ви можете скористатися усіма перевагами мережі Matrix. За її допомогою ви можете безпечно надсилати текстові повідомлення, відео та звукові файли вашим родичам, колегам та друзям.</p>
<p xml:lang="x-test">xxNeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.xx</p> <p xml:lang="x-test">xxNeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.xx</p>
@@ -118,9 +110,8 @@
<p xml:lang="fi">NeoChat pyrkii olemaan Matrix-määritelmän täysominaisuuksinen sovellus, joten se tukee kaikkea nykyisessä vakaassa määritelmässä muutamaa huomattavaa poikkeusta lukuun ottamatta (VoIP, säikeet ja jotkin piirteet päästä päähän -salauksessa). Joitakin pienempiäkin puutteita on Matrix-määritelmän jatkuvan kehityksen vuoksi, mutta lopputavoitteena on tarjota määritelmän täysi tuki.</p> <p xml:lang="fi">NeoChat pyrkii olemaan Matrix-määritelmän täysominaisuuksinen sovellus, joten se tukee kaikkea nykyisessä vakaassa määritelmässä muutamaa huomattavaa poikkeusta lukuun ottamatta (VoIP, säikeet ja jotkin piirteet päästä päähän -salauksessa). Joitakin pienempiäkin puutteita on Matrix-määritelmän jatkuvan kehityksen vuoksi, mutta lopputavoitteena on tarjota määritelmän täysi tuki.</p>
<p xml:lang="fr">L'objectif de NeoChat est d'être une application complète pour le protocole Matrix. En tant que tel, tout dans la spécification stable actuelle avec les exceptions notables de VoIP, les processus et certains aspects du chiffrement de bout en bout sont pris en charge. Il y a quelques autres petites omissions en raison du fait que la spécification du protocole Matrix est en constante évolution. Cependant, l'objectif reste de fournir un soutien éventuel pour l'ensemble de la spécification.</p> <p xml:lang="fr">L'objectif de NeoChat est d'être une application complète pour le protocole Matrix. En tant que tel, tout dans la spécification stable actuelle avec les exceptions notables de VoIP, les processus et certains aspects du chiffrement de bout en bout sont pris en charge. Il y a quelques autres petites omissions en raison du fait que la spécification du protocole Matrix est en constante évolution. Cependant, l'objectif reste de fournir un soutien éventuel pour l'ensemble de la spécification.</p>
<p xml:lang="gl">NeoChat pretende ser unha aplicación completa para a especificación de Matrix. Coas excepcións de VoIP, conversas fiadas e algúns aspectos da cifraxe de extremo a extremo, a versión estábel segue as especificacións. Existen algunhas outras pequenas omisións debido ao feito de que Matrix está en continua evolución pero a intención é implementar a especificación completa.</p> <p xml:lang="gl">NeoChat pretende ser unha aplicación completa para a especificación de Matrix. Coas excepcións de VoIP, conversas fiadas e algúns aspectos da cifraxe de extremo a extremo, a versión estábel segue as especificacións. Existen algunhas outras pequenas omisións debido ao feito de que Matrix está en continua evolución pero a intención é implementar a especificación completa.</p>
<p xml:lang="he">NeoChat מתיימר להיות יישום עתיר יכולות לפי מפרט Matrix. כיוון שזה ייעודו, כל מה שבמפרט היציב עם חריגות משמעותיות כגון VoIP, שרשורים ועוד מגוון היבטים של הצפנה מקצה לקצה נתמכים גם הם. יש מספר השמטות קטן עקב העובדה שהמפרט של Matrix ממשיך להתפתח אך המטרה היא להמשיך לספק תמיכה בסופו של דבר לכל המפרט.</p>
<p xml:lang="hu">A NeoChat célja, hogy a Matrix specifikációnak megfelelő teljes funkcionalitású alkalmazás legyen. Mint ilyen, a jelenlegi stabil specifikáció támogatott a VoIP, a szálak és a végpontok közötti titkosítás egyes elemeinek kivételével. Van még néhány kisebb hiányosság annak köszönhetően, hogy a Matrix specifikáció folyamatosan fejlődik, de végső cél a teljes specifikáció megvalósítása.</p> <p xml:lang="hu">A NeoChat célja, hogy a Matrix specifikációnak megfelelő teljes funkcionalitású alkalmazás legyen. Mint ilyen, a jelenlegi stabil specifikáció támogatott a VoIP, a szálak és a végpontok közötti titkosítás egyes elemeinek kivételével. Van még néhány kisebb hiányosság annak köszönhetően, hogy a Matrix specifikáció folyamatosan fejlődik, de végső cél a teljes specifikáció megvalósítása.</p>
<p xml:lang="ia">NeoChat aspira a esser un application plenmente eminente per le specification de Matrix. Tal como omne cosas in le specification currentemente stabile con le exceptiones notabile de VOIP, threads e alcun aspectos del cryptation End-to-End es supportate. Il ha ltere pauc omissiones, debite al facto que le specification de Matrix es in evolution constante ma le aspiration remane a fornir supporto eventual per le integre specification.</p> <p xml:lang="ia">NeoChat aspira a esser un application plenemente eminente per le specification de Matrix. Tal como omne cosas in le specification currentemente stabile con le exceptiones notabile de VOIP, threads e alcun aspectos del cryptation End-to-End es supportate. Il ha ltere pauc omissiones, debite al facto que le specification de Matrix es in evolution constante ma le aspiration remane a fornir supporto eventual per le integre specification.</p>
<p xml:lang="it">NeoChat mira ad essere un'applicazione completa per le specifiche Matrix. Pertanto, sono supportati tutti gli elementi dell'attuale specifica stabile con le notevoli eccezioni di VoIP, conversazioni e alcuni aspetti della cifratura end-to-end. Ci sono alcune altre piccole omissioni dovute al fatto che le specifiche Matrix sono in continua evoluzione, ma l'obiettivo rimane quello di fornire un eventuale supporto per l'intera specifica.</p> <p xml:lang="it">NeoChat mira ad essere un'applicazione completa per le specifiche Matrix. Pertanto, sono supportati tutti gli elementi dell'attuale specifica stabile con le notevoli eccezioni di VoIP, conversazioni e alcuni aspetti della cifratura end-to-end. Ci sono alcune altre piccole omissioni dovute al fatto che le specifiche Matrix sono in continua evoluzione, ma l'obiettivo rimane quello di fornire un eventuale supporto per l'intera specifica.</p>
<p xml:lang="ka">NeoChat მიზნად ისახავს Matrix სპეციფიკაციის სრული განხორციელება ჰქონდეს. როგორც ასეთი, ყველაფერი მიმდინარე სპეციფიკაციიდან, VoIP-ის, ძაფებისა და გამჭოლი დაშიფვრის ზოგიერთი ასპექტის გარდა, მხარდაჭერილია. შეძლება ასევე იყოს მცირე ლაფსუსებიც იმის გამო, რომ Matrix-ის სპეციფიკაცია მუდმივად ვითარდება, მაგრამ ჩვენი მიზანი მისი სრული მხარდაჭერაა.</p> <p xml:lang="ka">NeoChat მიზნად ისახავს Matrix სპეციფიკაციის სრული განხორციელება ჰქონდეს. როგორც ასეთი, ყველაფერი მიმდინარე სპეციფიკაციიდან, VoIP-ის, ძაფებისა და გამჭოლი დაშიფვრის ზოგიერთი ასპექტის გარდა, მხარდაჭერილია. შეძლება ასევე იყოს მცირე ლაფსუსებიც იმის გამო, რომ Matrix-ის სპეციფიკაცია მუდმივად ვითარდება, მაგრამ ჩვენი მიზანი მისი სრული მხარდაჭერაა.</p>
<p xml:lang="ko">NeoChat은 Matrix 표준을 따르는 프로그램을 목표로 합니다. 현재 안정 버전의 표준에서 제공하는 기능의 대부분을 지원하며, VoIP, 스레드, 일부 종단간 암호화와 같은 기능은 아직 지원하지 않습니다. Matrix 표준은 계속하여 진화 중이기 때문에 일부 기능이 빠져 있을 수도 있지만 장기적으로는 전체 표준을 지원하는 것이 목표입니다.</p> <p xml:lang="ko">NeoChat은 Matrix 표준을 따르는 프로그램을 목표로 합니다. 현재 안정 버전의 표준에서 제공하는 기능의 대부분을 지원하며, VoIP, 스레드, 일부 종단간 암호화와 같은 기능은 아직 지원하지 않습니다. Matrix 표준은 계속하여 진화 중이기 때문에 일부 기능이 빠져 있을 수도 있지만 장기적으로는 전체 표준을 지원하는 것이 목표입니다.</p>
@@ -147,7 +138,6 @@
<p xml:lang="fi">Matrix-määritelmän kehittyessä NeoChat tukee myös monia epävakaita ominaisuuksia. Tällä hetkellä näitä ovat:</p> <p xml:lang="fi">Matrix-määritelmän kehittyessä NeoChat tukee myös monia epävakaita ominaisuuksia. Tällä hetkellä näitä ovat:</p>
<p xml:lang="fr">En raison de la nature du développement des spécifications du protocole Matrix, NeoChat prend également en charge de nombreuses fonctionnalités instables. Actuellement, ce sont :</p> <p xml:lang="fr">En raison de la nature du développement des spécifications du protocole Matrix, NeoChat prend également en charge de nombreuses fonctionnalités instables. Actuellement, ce sont :</p>
<p xml:lang="gl">Debido á natureza do desenvolvemento da especificación de Matrix, NeoChat tamén inclúe varias funcionalidades non estábeis:</p> <p xml:lang="gl">Debido á natureza do desenvolvemento da especificación de Matrix, NeoChat tamén inclúe varias funcionalidades non estábeis:</p>
<p xml:lang="he">מטבע הדברים, הפיתוח של NeoChat תומך במגוון יכולות מפוקפקות כתלות בהתפתחות המפרט הטכני של Matrix. היכולות האלה הן:</p>
<p xml:lang="hu">A Matrix specifikáció fejlesztésének jellegéből adódóan a NeoChat számos instabil funkciót is támogat. Jelenleg a következőket:</p> <p xml:lang="hu">A Matrix specifikáció fejlesztésének jellegéből adódóan a NeoChat számos instabil funkciót is támogat. Jelenleg a következőket:</p>
<p xml:lang="ia">Debite al natura del disveloppamento de specification de Matrix NeoChat tamben supporta numerose characteristicas instabile. Currentemente istes es:</p> <p xml:lang="ia">Debite al natura del disveloppamento de specification de Matrix NeoChat tamben supporta numerose characteristicas instabile. Currentemente istes es:</p>
<p xml:lang="it">A causa della natura dello sviluppo delle specifiche Matrix, NeoChat supporta anche numerose funzionalità instabili. Attualmente queste sono:</p> <p xml:lang="it">A causa della natura dello sviluppo delle specifiche Matrix, NeoChat supporta anche numerose funzionalità instabili. Attualmente queste sono:</p>
@@ -178,7 +168,6 @@
<li xml:lang="fi">Kyselyt MSC3381</li> <li xml:lang="fi">Kyselyt MSC3381</li>
<li xml:lang="fr">Sondages - MSC3381</li> <li xml:lang="fr">Sondages - MSC3381</li>
<li xml:lang="gl">Enquisas — MSC3381</li> <li xml:lang="gl">Enquisas — MSC3381</li>
<li xml:lang="he">סקרים - MSC3381</li>
<li xml:lang="hu">Szavazások - MSC3381</li> <li xml:lang="hu">Szavazások - MSC3381</li>
<li xml:lang="ia">Inquestas - MSC3381</li> <li xml:lang="ia">Inquestas - MSC3381</li>
<li xml:lang="it">Sondaggi - MSC3381</li> <li xml:lang="it">Sondaggi - MSC3381</li>
@@ -207,7 +196,6 @@
<li xml:lang="fi">Tarrapakkaukset MSC2545</li> <li xml:lang="fi">Tarrapakkaukset MSC2545</li>
<li xml:lang="fr">Paquets d'auto-collants - MSC2545</li> <li xml:lang="fr">Paquets d'auto-collants - MSC2545</li>
<li xml:lang="gl">Paquetes de adhesivos — MSC2545</li> <li xml:lang="gl">Paquetes de adhesivos — MSC2545</li>
<li xml:lang="he">חבילות מדבקות - MSC2545</li>
<li xml:lang="hu">Matricacsomagok - MSC2545</li> <li xml:lang="hu">Matricacsomagok - MSC2545</li>
<li xml:lang="ia">Etiquetta gummate (sticker) -MSC2545</li> <li xml:lang="ia">Etiquetta gummate (sticker) -MSC2545</li>
<li xml:lang="it">Pacchetti di adesivi - MSC2545</li> <li xml:lang="it">Pacchetti di adesivi - MSC2545</li>
@@ -222,7 +210,7 @@
<li xml:lang="sl">Sticker Packs - MSC2545</li> <li xml:lang="sl">Sticker Packs - MSC2545</li>
<li xml:lang="sv">Sticker Packs - MSC2545</li> <li xml:lang="sv">Sticker Packs - MSC2545</li>
<li xml:lang="ta">ஒட்டி தொகுப்புகள் - MSC2545</li> <li xml:lang="ta">ஒட்டி தொகுப்புகள் - MSC2545</li>
<li xml:lang="tr">Çıkartma Paketleri — MSC2545</li> <li xml:lang="tr">Yapışkan Paketleri — MSC2545</li>
<li xml:lang="uk">Пакунки наліпок - MSC2545</li> <li xml:lang="uk">Пакунки наліпок - MSC2545</li>
<li xml:lang="x-test">xxSticker Packs - MSC2545xx</li> <li xml:lang="x-test">xxSticker Packs - MSC2545xx</li>
<li xml:lang="zh-TW">貼圖包 - MSC2545</li> <li xml:lang="zh-TW">貼圖包 - MSC2545</li>
@@ -237,7 +225,6 @@
<li xml:lang="fi">Sijaintitapahtumat MSC3488</li> <li xml:lang="fi">Sijaintitapahtumat MSC3488</li>
<li xml:lang="fr">Événements de lieu - MSC3488</li> <li xml:lang="fr">Événements de lieu - MSC3488</li>
<li xml:lang="gl">Localización de eventos — MSC3488</li> <li xml:lang="gl">Localización de eventos — MSC3488</li>
<li xml:lang="he">אירועי מקום - MSC3488</li>
<li xml:lang="hu">Események helyadatai - MSC3488</li> <li xml:lang="hu">Események helyadatai - MSC3488</li>
<li xml:lang="ia">Eventos de Location - MSC3488</li> <li xml:lang="ia">Eventos de Location - MSC3488</li>
<li xml:lang="it">Località eventi - MSC3488</li> <li xml:lang="it">Località eventi - MSC3488</li>
@@ -301,7 +288,6 @@
<caption xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</caption> <caption xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</caption>
<caption xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</caption> <caption xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</caption>
<caption xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</caption> <caption xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</caption>
<caption xml:lang="he">תצוגה ראשית עם רשימת חדרים, צ׳אט ופרטי חדר</caption>
<caption xml:lang="hu">A fő nézet a szobalistával, csevegéssel és szobainformációkkal</caption> <caption xml:lang="hu">A fő nézet a szobalistával, csevegéssel és szobainformációkkal</caption>
<caption xml:lang="ia">Vista principal con lista de sala, chat e information de sala</caption> <caption xml:lang="ia">Vista principal con lista de sala, chat e information de sala</caption>
<caption xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption> <caption xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption>
@@ -324,27 +310,21 @@
<screenshot type="default"> <screenshot type="default">
<image>https://cdn.kde.org/screenshots/neochat/spaces.png</image> <image>https://cdn.kde.org/screenshots/neochat/spaces.png</image>
<caption>Discover new communities with Matrix Spaces</caption> <caption>Discover new communities with Matrix Spaces</caption>
<caption xml:lang="ar">اكتشف مجتمعات جديدة مع فضاءات ماتركس</caption>
<caption xml:lang="ca">Descobriu comunitats noves amb els espais de Matrix</caption> <caption xml:lang="ca">Descobriu comunitats noves amb els espais de Matrix</caption>
<caption xml:lang="ca-valencia">Descobriu comunitats noves amb els espais de Matrix</caption> <caption xml:lang="ca-valencia">Descobriu comunitats noves amb els espais de Matrix</caption>
<caption xml:lang="en-GB">Discover new communities with Matrix Spaces</caption> <caption xml:lang="en-GB">Discover new communities with Matrix Spaces</caption>
<caption xml:lang="eo">Malkovru novajn komunumojn per Matrix Spaces</caption> <caption xml:lang="eo">Malkovru novajn komunumojn per Matrix Spaces</caption>
<caption xml:lang="es">Descubra nuevas comunidades con los espacios de Matrix</caption> <caption xml:lang="es">Descubra nuevas comunidades con los espacios de Matrix</caption>
<caption xml:lang="eu">Ezagutu komunitate berriak Matrixeko Tokiak erabiliz</caption> <caption xml:lang="eu">Ezagutu komunitate berriak Matrixeko Tokiak erabiliz</caption>
<caption xml:lang="fi">Löydä uusia yhteisöjä Matrix Spacesillä</caption>
<caption xml:lang="fr">Découvrez de nouvelles communautés avec les espaces sous Matrix</caption> <caption xml:lang="fr">Découvrez de nouvelles communautés avec les espaces sous Matrix</caption>
<caption xml:lang="gl">Descubra novas comunidades dos espazos de Matrix.</caption>
<caption xml:lang="he">אפשר להיחשף לקהילות חדשות דרך Matrix Spaces</caption>
<caption xml:lang="hu">Fedezzen fel új közösségeket a Matrix Terek segítségével</caption> <caption xml:lang="hu">Fedezzen fel új közösségeket a Matrix Terek segítségével</caption>
<caption xml:lang="ia">Discoperi nove communitate con Matrix Spaces (Spatios de Matrix)</caption> <caption xml:lang="ia">Discoperi nove communitate con Matrix Spaces (Spatios de Matrix)</caption>
<caption xml:lang="it">Scopri nuove comunità con Matrix Spaces</caption> <caption xml:lang="it">Scopri nuove comunità con Matrix Spaces</caption>
<caption xml:lang="ka">აღმოაჩინეთ ახალი საზოგადოებები Matrix Spaces-თან ერთად</caption> <caption xml:lang="ka">აღმოაჩინეთ ახალი საზოგადოებები Matrix Spaces-თან ერთად</caption>
<caption xml:lang="lv">Atklājiet jaunas kopienas ar „Matrix“ telpām</caption> <caption xml:lang="lv">Atklājiet jaunas kopienas ar „Matrix“ telpām</caption>
<caption xml:lang="nl">Ontdek nieuwe gemeenschappen met Matrix-ruimten</caption> <caption xml:lang="nl">Ontdek nieuwe gemeenschappen met Matrix-ruimten</caption>
<caption xml:lang="nn">Oppdag nye fellesskap med Matrix Spaces</caption>
<caption xml:lang="pl">Odkrywaj nowe społeczności w Przestrzeniach Matriksa</caption> <caption xml:lang="pl">Odkrywaj nowe społeczności w Przestrzeniach Matriksa</caption>
<caption xml:lang="sl">Odkrijte nove skupnosti z Matrix Spaces</caption> <caption xml:lang="sl">Odkrijte nove skupnosti z Matrix Spaces</caption>
<caption xml:lang="sv">Upptäck nya gemenskaper med Matrix Spaces</caption>
<caption xml:lang="tr">Matrix Alanlar ile yeni topluluklar keşfedin</caption> <caption xml:lang="tr">Matrix Alanlar ile yeni topluluklar keşfedin</caption>
<caption xml:lang="uk">Пошук нових спільнот за допомогою Matrix Spaces</caption> <caption xml:lang="uk">Пошук нових спільнот за допомогою Matrix Spaces</caption>
<caption xml:lang="x-test">xxDiscover new communities with Matrix Spacesxx</caption> <caption xml:lang="x-test">xxDiscover new communities with Matrix Spacesxx</caption>
@@ -370,7 +350,6 @@
<caption xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</caption> <caption xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</caption>
<caption xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</caption> <caption xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</caption>
<caption xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</caption> <caption xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</caption>
<caption xml:lang="he">תצוגה ראשית עם רשימת חדרים, צ׳אט ופרטי חדר</caption>
<caption xml:lang="hu">A fő nézet a szobalistával, csevegéssel és szobainformációkkal</caption> <caption xml:lang="hu">A fő nézet a szobalistával, csevegéssel és szobainformációkkal</caption>
<caption xml:lang="ia">Vista principal con lista de sala, chat e information de sala</caption> <caption xml:lang="ia">Vista principal con lista de sala, chat e information de sala</caption>
<caption xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption> <caption xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption>
@@ -404,7 +383,6 @@
<caption xml:lang="fi">Kirjautumisnäkymä</caption> <caption xml:lang="fi">Kirjautumisnäkymä</caption>
<caption xml:lang="fr">Écran de connexion</caption> <caption xml:lang="fr">Écran de connexion</caption>
<caption xml:lang="gl">Pantalla de identificación.</caption> <caption xml:lang="gl">Pantalla de identificación.</caption>
<caption xml:lang="he">מסך כניסה</caption>
<caption xml:lang="hu">Bejelentkező képernyő</caption> <caption xml:lang="hu">Bejelentkező képernyő</caption>
<caption xml:lang="ia">Schermo de accesso</caption> <caption xml:lang="ia">Schermo de accesso</caption>
<caption xml:lang="it">Schermata di accesso</caption> <caption xml:lang="it">Schermata di accesso</caption>
@@ -429,9 +407,6 @@
<content_attribute id="social-chat">intense</content_attribute> <content_attribute id="social-chat">intense</content_attribute>
</content_rating> </content_rating>
<releases> <releases>
<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"/>
<release version="24.05.0" date="2024-05-23"/> <release version="24.05.0" date="2024-05-23"/>
<release version="24.02.2" date="2024-04-11"/> <release version="24.02.2" date="2024-04-11"/>
<release version="24.02.1" date="2024-03-21"/> <release version="24.02.1" date="2024-03-21"/>

View File

@@ -18,7 +18,6 @@ Name[eu]=NeoChat
Name[fi]=NeoChat Name[fi]=NeoChat
Name[fr]=NeoChat Name[fr]=NeoChat
Name[gl]=NeoChat Name[gl]=NeoChat
Name[he]=NeoChat
Name[hu]=NeoChat Name[hu]=NeoChat
Name[ia]=Neochat Name[ia]=Neochat
Name[id]=NeoChat Name[id]=NeoChat
@@ -60,7 +59,6 @@ GenericName[eu]=Matrix bezeroa
GenericName[fi]=Matrix-asiakas GenericName[fi]=Matrix-asiakas
GenericName[fr]=Client « Matrix » GenericName[fr]=Client « Matrix »
GenericName[gl]=Cliente de Matrix GenericName[gl]=Cliente de Matrix
GenericName[he]=לקוח Matrix
GenericName[hu]=Matrix kliens GenericName[hu]=Matrix kliens
GenericName[ia]=Cliente de Matrice GenericName[ia]=Cliente de Matrice
GenericName[id]=Klien Matrix GenericName[id]=Klien Matrix
@@ -101,7 +99,6 @@ Comment[eu]=Matrix protokolorako bezeroa
Comment[fi]=Asiakas Matrix-yhteyskäytännölle Comment[fi]=Asiakas Matrix-yhteyskäytännölle
Comment[fr]=Client pour le protocole « Matrix » Comment[fr]=Client pour le protocole « Matrix »
Comment[gl]=Cliente para o protocolo Matrix. Comment[gl]=Cliente para o protocolo Matrix.
Comment[he]=לקוח לפרוטוקול Matrix
Comment[hu]=Kliens a Matrix protokollhoz Comment[hu]=Kliens a Matrix protokollhoz
Comment[ia]=Cliente per le protocollo de Matrix Comment[ia]=Cliente per le protocollo de Matrix
Comment[id]=Klien untuk protokol Matrix Comment[id]=Klien untuk protokol Matrix

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -77,7 +77,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
></term> ></term>
<listitem> <listitem>
<para <para
>L'URI de Matrix per a un usuari o una sala. P. ex. matrix:u/usuari:example.org o matrix:r/root:example.org. Això farà que el NeoChat intenti obrir la sala o conversa indicada. </para> >L'URI de Matrix per a un usuari o una sala. P. ex. matrix:u/usuari:exemple.org o matrix:r/root:exemple.org. Això farà que el NeoChat intenti obrir la sala o conversa indicada. </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
</variablelist> </variablelist>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

@@ -134,8 +134,6 @@ add_library(neochat STATIC
jobs/neochatdeletedevicejob.h jobs/neochatdeletedevicejob.h
jobs/neochatchangepasswordjob.cpp jobs/neochatchangepasswordjob.cpp
jobs/neochatchangepasswordjob.h jobs/neochatchangepasswordjob.h
jobs/neochatgetcommonroomsjob.cpp
jobs/neochatgetcommonroomsjob.h
mediasizehelper.cpp mediasizehelper.cpp
mediasizehelper.h mediasizehelper.h
eventhandler.cpp eventhandler.cpp
@@ -158,6 +156,7 @@ add_library(neochat STATIC
models/linemodel.cpp models/linemodel.cpp
models/linemodel.h models/linemodel.h
events/locationbeaconevent.h events/locationbeaconevent.h
events/serveraclevent.h
events/widgetevent.h events/widgetevent.h
enums/messagecomponenttype.h enums/messagecomponenttype.h
models/messagecontentmodel.cpp models/messagecontentmodel.cpp
@@ -186,19 +185,13 @@ add_library(neochat STATIC
enums/powerlevel.h enums/powerlevel.h
models/permissionsmodel.cpp models/permissionsmodel.cpp
models/permissionsmodel.h models/permissionsmodel.h
threepidbindhelper.cpp
threepidbindhelper.h
models/readmarkermodel.cpp
models/readmarkermodel.h
neochatroommember.cpp
neochatroommember.h
) )
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
QT_QML_SINGLETON_TYPE TRUE QT_QML_SINGLETON_TYPE TRUE
) )
ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat
QML_FILES QML_FILES
qml/Main.qml qml/Main.qml
@@ -288,15 +281,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/ConsentDialog.qml qml/ConsentDialog.qml
qml/AskDirectChatConfirmation.qml qml/AskDirectChatConfirmation.qml
qml/HoverLinkIndicator.qml qml/HoverLinkIndicator.qml
DEPENDENCIES
QtCore
QtQuick
IMPORTS
org.kde.neochat.timeline
org.kde.neochat.settings
org.kde.neochat.devtools
org.kde.neochat.login
org.kde.neochat.chatbar
) )
add_subdirectory(settings) add_subdirectory(settings)

View File

@@ -91,7 +91,7 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, Cha
for (auto it = m_room->messageEvents().crbegin(); it != m_room->messageEvents().crend(); it++) { for (auto it = m_room->messageEvents().crbegin(); it != m_room->messageEvents().crend(); it++) {
if (const auto event = eventCast<const RoomMessageEvent>(&**it)) { if (const auto event = eventCast<const RoomMessageEvent>(&**it)) {
if (event->senderId() == m_room->localMember().id() && event->hasTextContent()) { if (event->senderId() == m_room->localUser()->id() && event->hasTextContent()) {
QString originalString; QString originalString;
if (event->content()) { if (event->content()) {
originalString = static_cast<const Quotient::EventContent::TextContent *>(event->content())->body; originalString = static_cast<const Quotient::EventContent::TextContent *>(event->content())->body;

View File

@@ -13,15 +13,13 @@ import org.kde.neochat
QQC2.Popup { QQC2.Popup {
id: root id: root
padding: Kirigami.Units.largeSpacing padding: 16
signal chosen(string path) signal chosen(string path)
contentItem: RowLayout { contentItem: RowLayout {
spacing: Kirigami.Units.smallSpacing
QQC2.ToolButton { QQC2.ToolButton {
Layout.preferredWidth: 160
Layout.fillHeight: true Layout.fillHeight: true
icon.name: 'mail-attachment' icon.name: 'mail-attachment'
@@ -30,7 +28,7 @@ QQC2.Popup {
onClicked: { onClicked: {
root.close(); root.close();
var fileDialog = openFileDialog.createObject(QQC2.Overlay.overlay); var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay);
fileDialog.chosen.connect(path => root.chosen(path)); fileDialog.chosen.connect(path => root.chosen(path));
fileDialog.open(); fileDialog.open();
} }
@@ -39,8 +37,11 @@ QQC2.Popup {
Kirigami.Separator {} Kirigami.Separator {}
QQC2.ToolButton { QQC2.ToolButton {
Layout.preferredWidth: 160
Layout.fillHeight: true Layout.fillHeight: true
padding: 16
icon.name: 'insert-image' icon.name: 'insert-image'
text: i18n("Clipboard image") text: i18n("Clipboard image")
onClicked: { onClicked: {

View File

@@ -2,7 +2,7 @@
# SPDX-License-Identifier: BSD-2-Clause # SPDX-License-Identifier: BSD-2-Clause
qt_add_library(chatbar STATIC) qt_add_library(chatbar STATIC)
ecm_add_qml_module(chatbar GENERATE_PLUGIN_SOURCE qt_add_qml_module(chatbar
URI org.kde.neochat.chatbar URI org.kde.neochat.chatbar
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/chatbar OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/chatbar
QML_FILES QML_FILES

View File

@@ -115,7 +115,7 @@ QQC2.Control {
displayHint: QQC2.AbstractButton.IconOnly displayHint: QQC2.AbstractButton.IconOnly
onTriggered: { onTriggered: {
locationChooser.createObject(QQC2.Overlay.overlay, { locationChooser.createObject(QQC2.ApplicationWindow.overlay, {
room: root.currentRoom room: root.currentRoom
}).open(); }).open();
} }
@@ -364,7 +364,7 @@ QQC2.Control {
ReplyPane { ReplyPane {
userName: _private.chatBarCache.relationUser.displayName userName: _private.chatBarCache.relationUser.displayName
userColor: _private.chatBarCache.relationUser.color userColor: _private.chatBarCache.relationUser.color
userAvatar: _private.chatBarCache.relationUser.avatarUrl userAvatar: _private.chatBarCache.relationUser.avatarSource
text: _private.chatBarCache.relationMessage text: _private.chatBarCache.relationMessage
onCancel: { onCancel: {

View File

@@ -3,8 +3,6 @@
#include "chatbarcache.h" #include "chatbarcache.h"
#include <Quotient/roommember.h>
#include "chatdocumenthandler.h" #include "chatdocumenthandler.h"
#include "eventhandler.h" #include "eventhandler.h"
#include "neochatroom.h" #include "neochatroom.h"
@@ -86,7 +84,7 @@ void ChatBarCache::setEditId(const QString &editId)
Q_EMIT attachmentPathChanged(); Q_EMIT attachmentPathChanged();
} }
Quotient::RoomMember ChatBarCache::relationUser() const QVariantMap ChatBarCache::relationUser() const
{ {
if (parent() == nullptr) { if (parent() == nullptr) {
qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation."; qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.";
@@ -98,9 +96,9 @@ Quotient::RoomMember ChatBarCache::relationUser() const
return {}; return {};
} }
if (m_relationId.isEmpty()) { if (m_relationId.isEmpty()) {
return room->member(QString()); return room->getUser(nullptr);
} }
return room->member((*room->findInTimeline(m_relationId))->senderId()); return room->getUser(room->user((*room->findInTimeline(m_relationId))->senderId()));
} }
QString ChatBarCache::relationMessage() const QString ChatBarCache::relationMessage() const

View File

@@ -10,12 +10,6 @@
class ChatDocumentHandler; class ChatDocumentHandler;
namespace Quotient
{
class RoomMember;
}
/** /**
* @brief Defines a user mention in the current chat or edit text. * @brief Defines a user mention in the current chat or edit text.
*/ */
@@ -94,13 +88,26 @@ class ChatBarCache : public QObject
Q_PROPERTY(QString editId READ editId WRITE setEditId NOTIFY relationIdChanged) Q_PROPERTY(QString editId READ editId WRITE setEditId NOTIFY relationIdChanged)
/** /**
* @brief Get the RoomMember object for the message being replied to. * @brief Get the user for the message being replied to.
* *
* Returns an empty RoomMember if not replying to a message. * This is different to getting a Quotient::User object
* as neither of those can provide details like the displayName or avatarMediaId
* without the room context as these can vary from room to room.
* *
* @sa Quotient::RoomMember * Returns an empty user if not replying to a message.
*
* The user QVariantMap has the following properties:
* - isLocalUser - Whether the user is the local user.
* - id - The matrix ID of the user.
* - displayName - Display name in the context of this room.
* - avatarSource - The mxc URL for the user's avatar in the current room.
* - avatarMediaId - Avatar id in the context of this room.
* - color - Color for the user.
* - object - The Quotient::User object for the user.
*
* @sa getUser, Quotient::User
*/ */
Q_PROPERTY(Quotient::RoomMember relationUser READ relationUser NOTIFY relationIdChanged) Q_PROPERTY(QVariantMap relationUser READ relationUser NOTIFY relationIdChanged)
/** /**
* @brief The content of the related message. * @brief The content of the related message.
@@ -154,7 +161,7 @@ public:
QString editId() const; QString editId() const;
void setEditId(const QString &editId); void setEditId(const QString &editId);
Quotient::RoomMember relationUser() const; QVariantMap relationUser() const;
QString relationMessage() const; QString relationMessage() const;

View File

@@ -103,16 +103,14 @@ Controller::Controller(QObject *parent)
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() { connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
if (m_accountRegistry.size() > oldAccountCount) { if (m_accountRegistry.size() > oldAccountCount) {
auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]); auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]);
connect( connect(connection, &NeoChatConnection::syncDone, this, [connection]() {
connection, NotificationsManager::instance().handleNotifications(connection);
&NeoChatConnection::syncDone, });
this, connectSingleShot(connection, &NeoChatConnection::syncDone, this, [this, connection] {
[this, connection] { if (!m_endpoint.isEmpty()) {
if (!m_endpoint.isEmpty()) { connection->setupPushNotifications(m_endpoint);
connection->setupPushNotifications(m_endpoint); }
} });
},
Qt::SingleShotConnection);
} }
oldAccountCount = m_accountRegistry.size(); oldAccountCount = m_accountRegistry.size();
}); });
@@ -392,6 +390,8 @@ QString Controller::loadFileContent(const QString &path) const
return QString::fromLatin1(file.readAll()); return QString::fromLatin1(file.readAll());
} }
#include "moc_controller.cpp"
void Controller::setTestMode(bool test) void Controller::setTestMode(bool test)
{ {
testMode = test; testMode = test;
@@ -407,13 +407,11 @@ void Controller::removeConnection(const QString &userId)
} }
} }
bool Controller::csSupported() const bool Controller::ssssSupported() const
{ {
#if Quotient_VERSION_MINOR > 9 #if __has_include("Quotient/e2ee/sssshandler.h")
return true; return true;
#else #else
return false; return false;
#endif #endif
} }
#include "moc_controller.cpp"

View File

@@ -50,19 +50,9 @@ class Controller : public QObject
Q_PROPERTY(QStringList accountsLoading MEMBER m_accountsLoading NOTIFY accountsLoadingChanged) Q_PROPERTY(QStringList accountsLoading MEMBER m_accountsLoading NOTIFY accountsLoadingChanged)
Q_PROPERTY(bool csSupported READ csSupported CONSTANT) Q_PROPERTY(bool ssssSupported READ ssssSupported CONSTANT)
public: public:
/**
* @brief Define the types on inline messages that can be shown.
*/
enum MessageType {
Positive, /**< Positive message, typically green. */
Info, /**< Info message, typically highlight color. */
Error, /**< Error message, typically red. */
};
Q_ENUM(MessageType)
static Controller &instance(); static Controller &instance();
static Controller *create(QQmlEngine *engine, QJSEngine *) static Controller *create(QQmlEngine *engine, QJSEngine *)
{ {
@@ -106,7 +96,7 @@ public:
Q_INVOKABLE void removeConnection(const QString &userId); Q_INVOKABLE void removeConnection(const QString &userId);
bool csSupported() const; bool ssssSupported() const;
private: private:
explicit Controller(QObject *parent = nullptr); explicit Controller(QObject *parent = nullptr);
@@ -135,5 +125,4 @@ Q_SIGNALS:
void connectionDropped(NeoChatConnection *connection); void connectionDropped(NeoChatConnection *connection);
void activeConnectionChanged(NeoChatConnection *connection); void activeConnectionChanged(NeoChatConnection *connection);
void accountsLoadingChanged(); void accountsLoadingChanged();
void showMessage(MessageType messageType, const QString &message);
}; };

View File

@@ -2,7 +2,7 @@
# SPDX-License-Identifier: BSD-2-Clause # SPDX-License-Identifier: BSD-2-Clause
qt_add_library(devtools STATIC) qt_add_library(devtools STATIC)
ecm_add_qml_module(devtools GENERATE_PLUGIN_SOURCE qt_add_qml_module(devtools
URI org.kde.neochat.devtools URI org.kde.neochat.devtools
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/devtools OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/devtools
QML_FILES QML_FILES

View File

@@ -33,7 +33,6 @@ public:
* a room message. * a room message.
*/ */
enum Type { enum Type {
Author, /**< The message sender and time. */
Text, /**< A text message. */ Text, /**< A text message. */
Image, /**< A message that is an image. */ Image, /**< A message that is an image. */
Audio, /**< A message that is an audio recording. */ Audio, /**< A message that is an audio recording. */
@@ -48,11 +47,11 @@ public:
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */ LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
Encrypted, /**< An encrypted message that cannot be decrypted. */ Encrypted, /**< An encrypted message that cannot be decrypted. */
Reply, /**< A component to show a replied-to message. */ Reply, /**< A component to show a replied-to message. */
ReplyLoad, /**< A loading dialog for a reply. */
LinkPreview, /**< A preview of a URL in the message. */ LinkPreview, /**< A preview of a URL in the message. */
LinkPreviewLoad, /**< A loading dialog for a link preview. */ LinkPreviewLoad, /**< A loading dialog for a link preview. */
Edit, /**< A text edit for editing a message. */ Edit, /**< A text edit for editing a message. */
Verification, /**< A user verification session start message. */ Verification, /**< A user verification session start message. */
Loading, /**< The component is loading. */
Other, /**< Anything that cannot be classified as another type. */ Other, /**< Anything that cannot be classified as another type. */
}; };
Q_ENUM(Type); Q_ENUM(Type);

View File

@@ -19,11 +19,11 @@
#include <Quotient/events/simplestateevents.h> #include <Quotient/events/simplestateevents.h>
#include <Quotient/events/stickerevent.h> #include <Quotient/events/stickerevent.h>
#include <Quotient/quotient_common.h> #include <Quotient/quotient_common.h>
#include <Quotient/roommember.h>
#include "eventhandler_logging.h" #include "eventhandler_logging.h"
#include "events/locationbeaconevent.h" #include "events/locationbeaconevent.h"
#include "events/pollevent.h" #include "events/pollevent.h"
#include "events/serveraclevent.h"
#include "events/widgetevent.h" #include "events/widgetevent.h"
#include "linkpreviewer.h" #include "linkpreviewer.h"
#include "messagecomponenttype.h" #include "messagecomponenttype.h"
@@ -61,6 +61,22 @@ MessageComponentType::Type EventHandler::messageComponentType() const
return MessageComponentType::typeForEvent(*m_event); return MessageComponentType::typeForEvent(*m_event);
} }
QVariantMap EventHandler::getAuthor(bool isPending) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getAuthor called with m_room set to nullptr.";
return {};
}
// If we have a room we can return an empty user by handing nullptr to m_room->getUser.
if (m_event == nullptr) {
qCWarning(EventHandling) << "getAuthor called with m_event set to nullptr. Returning empty user.";
return m_room->getUser(nullptr);
}
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
return m_room->getUser(author);
}
QString EventHandler::getAuthorDisplayName(bool isPending) const QString EventHandler::getAuthorDisplayName(bool isPending) const
{ {
if (m_room == nullptr) { if (m_room == nullptr) {
@@ -80,8 +96,8 @@ QString EventHandler::getAuthorDisplayName(bool isPending) const
} }
return previousDisplayName; return previousDisplayName;
} else { } else {
const auto author = isPending ? m_room->localMember() : m_room->member(m_event->senderId()); const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
return author.htmlSafeDisplayName(); return m_room->htmlSafeMemberName(author->id());
} }
} }
@@ -96,8 +112,8 @@ QString EventHandler::singleLineAuthorDisplayname(bool isPending) const
return {}; return {};
} }
const auto author = isPending ? m_room->localMember() : m_room->member(m_event->senderId()); const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
auto displayName = author.displayName(); auto displayName = m_room->safeMemberName(author->id());
displayName.replace(QStringLiteral("<br>\n"), QStringLiteral(" ")); displayName.replace(QStringLiteral("<br>\n"), QStringLiteral(" "));
displayName.replace(QStringLiteral("<br>"), QStringLiteral(" ")); displayName.replace(QStringLiteral("<br>"), QStringLiteral(" "));
displayName.replace(QStringLiteral("<br />\n"), QStringLiteral(" ")); displayName.replace(QStringLiteral("<br />\n"), QStringLiteral(" "));
@@ -143,11 +159,6 @@ QString EventHandler::getTimeString(bool relative, QLocale::FormatType format, b
return {}; return {};
} }
QString EventHandler::getTimeString(const QString &format, bool isPending, const QDateTime &lastUpdated)
{
return getTime(isPending, lastUpdated).toLocalTime().toString(format);
}
bool EventHandler::isHighlighted() bool EventHandler::isHighlighted()
{ {
if (m_room == nullptr) { if (m_room == nullptr) {
@@ -209,7 +220,7 @@ bool EventHandler::isHidden()
} }
} }
if (m_room->connection()->isIgnored(m_event->senderId())) { if (m_room->connection()->isIgnored(m_room->user(m_event->senderId()))) {
return true; return true;
} }
@@ -232,21 +243,19 @@ Qt::TextFormat EventHandler::messageBodyInputFormat(const Quotient::RoomMessageE
QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event) QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event)
{ {
QString body;
if (event.hasFileContent()) { if (event.hasFileContent()) {
// if filename is given or body is equal to filename, auto fileCaption = event.content()->fileInfo()->originalName;
// then body is a caption if (fileCaption.isEmpty()) {
QString filename = event.content()->fileInfo()->originalName; fileCaption = event.plainBody();
QString body = event.plainBody(); } else if (event.content()->fileInfo()->originalName != event.plainBody()) {
if (filename.isEmpty() || filename == body) { fileCaption = event.plainBody() + " | "_ls + fileCaption;
return QString();
} }
return body; return fileCaption;
} }
QString body;
if (event.hasTextContent() && event.content()) { if (event.hasTextContent() && event.content()) {
body = static_cast<const EventContent::TextContent *>(event.content())->body; body = static_cast<const MessageEventContent::TextContent *>(event.content())->body;
} else { } else {
body = event.plainBody(); body = event.plainBody();
} }
@@ -309,19 +318,15 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
}, },
[this, prettyPrint](const RoomMemberEvent &e) { [this, prettyPrint](const RoomMemberEvent &e) {
// FIXME: Rewind to the name that was at the time of this event // FIXME: Rewind to the name that was at the time of this event
auto subjectName = m_room->member(e.userId()).htmlSafeDisplayName(); auto subjectName = m_room->htmlSafeMemberName(e.userId());
if (e.membership() == Membership::Leave) { if (e.membership() == Membership::Leave) {
if (e.prevContent() && e.prevContent()->displayName) { if (e.prevContent() && e.prevContent()->displayName) {
subjectName = sanitized(*e.prevContent()->displayName); subjectName = sanitized(*e.prevContent()->displayName).toHtmlEscaped();
if (prettyPrint) {
subjectName = subjectName.toHtmlEscaped();
}
} }
} }
if (prettyPrint) { if (prettyPrint) {
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>") subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\">%2</a>").arg(e.userId(), subjectName);
.arg(e.userId(), m_room->member(e.userId()).color().name(), subjectName);
} }
// The below code assumes senderName output in AuthorRole // The below code assumes senderName output in AuthorRole
@@ -435,7 +440,7 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
[](const LocationBeaconEvent &e) { [](const LocationBeaconEvent &e) {
return e.contentJson()["description"_ls].toString(); return e.contentJson()["description"_ls].toString();
}, },
[](const RoomServerAclEvent &) { [](const ServerAclEvent &) {
return i18n("changed the server access control lists for this room"); return i18n("changed the server access control lists for this room");
}, },
[](const WidgetEvent &e) { [](const WidgetEvent &e) {
@@ -474,7 +479,7 @@ QString EventHandler::getMessageBody(const RoomMessageEvent &event, Qt::TextForm
QString body; QString body;
if (event.hasTextContent() && event.content()) { if (event.hasTextContent() && event.content()) {
body = static_cast<const EventContent::TextContent *>(event.content())->body; body = static_cast<const MessageEventContent::TextContent *>(event.content())->body;
} else { } else {
body = event.plainBody(); body = event.plainBody();
} }
@@ -604,7 +609,7 @@ QString EventHandler::getGenericBody() const
[](const LocationBeaconEvent &) { [](const LocationBeaconEvent &) {
return i18n("sent a live location beacon"); return i18n("sent a live location beacon");
}, },
[](const RoomServerAclEvent &) { [](const ServerAclEvent &) {
return i18n("changed the server access control lists for this room"); return i18n("changed the server access control lists for this room");
}, },
[](const WidgetEvent &e) { [](const WidgetEvent &e) {
@@ -653,30 +658,23 @@ QVariantMap EventHandler::getMediaInfoForEvent(const Quotient::RoomEvent *event)
QString eventId = event->id(); QString eventId = event->id();
// Get the file info for the event. // Get the file info for the event.
const EventContent::FileInfo *fileInfo;
bool isSticker = false;
if (event->is<RoomMessageEvent>()) { if (event->is<RoomMessageEvent>()) {
auto roomMessageEvent = eventCast<const RoomMessageEvent>(event); auto roomMessageEvent = eventCast<const RoomMessageEvent>(event);
if (!roomMessageEvent->hasFileContent()) { if (!roomMessageEvent->hasFileContent()) {
return {}; return {};
} }
const EventContent::FileInfo *fileInfo;
fileInfo = roomMessageEvent->content()->fileInfo(); fileInfo = roomMessageEvent->content()->fileInfo();
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;
return mediaInfo;
} else if (event->is<StickerEvent>()) { } else if (event->is<StickerEvent>()) {
const EventContent::FileInfo *fileInfo;
auto stickerEvent = eventCast<const StickerEvent>(event); auto stickerEvent = eventCast<const StickerEvent>(event);
fileInfo = &stickerEvent->image(); fileInfo = &stickerEvent->image();
isSticker = true;
return getMediaInfoFromFileInfo(fileInfo, eventId, false, true);
} else { } else {
return {}; return {};
} }
return getMediaInfoFromFileInfo(fileInfo, eventId, false, isSticker);
} }
QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail, bool isSticker) const QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail, bool isSticker) const
@@ -802,21 +800,25 @@ MessageComponentType::Type EventHandler::replyMessageComponentType() const
return MessageComponentType::typeForEvent(*replyEvent); return MessageComponentType::typeForEvent(*replyEvent);
} }
Quotient::RoomMember EventHandler::getReplyAuthor() const QVariantMap EventHandler::getReplyAuthor() const
{ {
if (m_room == nullptr) { if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyAuthor called with m_room set to nullptr."; qCWarning(EventHandling) << "getReplyAuthor called with m_room set to nullptr.";
return {}; return {};
} }
// If we have a room we can return an empty user by handing nullptr to m_room->getUser.
if (m_event == nullptr) { if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyAuthor called with m_event set to nullptr. Returning empty user."; qCWarning(EventHandling) << "getReplyAuthor called with m_event set to nullptr. Returning empty user.";
return {}; return m_room->getUser(nullptr);
} }
if (auto replyPtr = m_room->getReplyForEvent(*m_event)) { auto replyPtr = m_room->getReplyForEvent(*m_event);
return m_room->member(replyPtr->senderId());
if (replyPtr) {
auto replyUser = m_room->user(replyPtr->senderId());
return m_room->getUser(replyUser);
} else { } else {
return m_room->member(QString()); return m_room->getUser(nullptr);
} }
} }
@@ -952,4 +954,101 @@ QString EventHandler::getLocationAssetType() const
return assetType; return assetType;
} }
bool EventHandler::hasReadMarkers() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "hasReadMarkers called with m_room set to nullptr.";
return false;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "hasReadMarkers called with m_event set to nullptr.";
return false;
}
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
return userIds.size() > 0;
}
QVariantList EventHandler::getReadMarkers(int maxMarkers) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReadMarkers called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReadMarkers called with m_event set to nullptr.";
return {};
}
auto userIds_temp = m_room->userIdsAtEvent(m_event->id());
userIds_temp.remove(m_room->localUser()->id());
auto userIds = userIds_temp.values();
if (userIds.count() > maxMarkers) {
userIds = userIds.mid(0, maxMarkers);
}
QVariantList users;
users.reserve(userIds.size());
for (const auto &userId : userIds) {
auto user = m_room->user(userId);
users += m_room->getUser(user);
}
return users;
}
QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getNumberExcessReadMarkers called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getNumberExcessReadMarkers called with m_event set to nullptr.";
return {};
}
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
if (userIds.count() > maxMarkers) {
return QStringLiteral("+ ") + QString::number(userIds.count() - maxMarkers);
} else {
return QString();
}
}
QString EventHandler::getReadMarkersString() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReadMarkersString called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReadMarkersString called with m_event set to nullptr.";
return {};
}
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
/**
* The string ends up in the form
* "x users: user1DisplayName, user2DisplayName, etc."
*/
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
for (const auto &userId : userIds) {
auto user = m_room->user(userId);
auto displayName = user->displayname(m_room);
if (displayName.isEmpty()) {
displayName = userId;
}
readMarkersString += displayName + i18nc("list separator", ", ");
}
readMarkersString.chop(2);
return readMarkersString;
}
#include "moc_eventhandler.cpp" #include "moc_eventhandler.cpp"

View File

@@ -13,11 +13,6 @@
#include "enums/messagecomponenttype.h" #include "enums/messagecomponenttype.h"
namespace Quotient
{
class RoomMember;
}
class LinkPreviewer; class LinkPreviewer;
class NeoChatRoom; class NeoChatRoom;
class ReactionModel; class ReactionModel;
@@ -53,10 +48,38 @@ public:
*/ */
MessageComponentType::Type messageComponentType() const; MessageComponentType::Type messageComponentType() const;
/**
* @brief Get the author of the event in context of the room.
*
* This is different to getting a Quotient::User object
* as neither of those can provide details like the displayName or avatarMediaId
* without the room context as these can vary from room to room. This function
* uses the room context and outputs the result as QVariantMap.
*
* An empty QVariantMap will be returned if the EventHandler hasn't had the room
* intialised. An empty user (i.e. a QVariantMap with all the correct keys
* but empty values) will be returned if the room has been set but not an event.
*
* @param isPending if the event is pending, i.e. has not been confirmed by
* the server.
*
* @return a QVariantMap for the user with the following properties:
* - isLocalUser - Whether the user is the local user.
* - id - The matrix ID of the user.
* - displayName - Display name in the context of this room.
* - avatarSource - The mxc URL for the user's avatar in the current room.
* - avatarMediaId - Avatar id in the context of this room.
* - color - Color for the user.
* - object - The Quotient::User object for the user.
*
* @sa Quotient::User
*/
QVariantMap getAuthor(bool isPending = false) const;
/** /**
* @brief Get the display name of the event author. * @brief Get the display name of the event author.
* *
* This method is special in that it will return * This method is separate from getAuthor() and special in that it will return
* the old display name of the author if the current event is one that caused it * the old display name of the author if the current event is one that caused it
* to change. This allows for scenarios where the UI wishes to notify that a * to change. This allows for scenarios where the UI wishes to notify that a
* user's display name has changed and what it changed from. * user's display name has changed and what it changed from.
@@ -98,8 +121,6 @@ public:
*/ */
QString getTimeString(bool relative, QLocale::FormatType format = QLocale::ShortFormat, bool isPending = false, QDateTime lastUpdated = {}) const; QString getTimeString(bool relative, QLocale::FormatType format = QLocale::ShortFormat, bool isPending = false, QDateTime lastUpdated = {}) const;
QString getTimeString(const QString &format, bool isPending = false, const QDateTime &lastUpdated = {});
/** /**
* @brief Whether the event should be highlighted in the timeline. * @brief Whether the event should be highlighted in the timeline.
* *
@@ -230,17 +251,27 @@ public:
/** /**
* @brief Get the author of the event replied to in context of the room. * @brief Get the author of the event replied to in context of the room.
* *
* An empty Quotient::RoomMember will be returned if the EventHandler hasn't had * This is different to getting a Quotient::User object
* the room or event initialised. * as neither of those can provide details like the displayName or avatarMediaId
* without the room context as these can vary from room to room. This function
* uses the room context and outputs the result as QVariantMap.
* *
* @param isPending if the event is pending, i.e. has not been confirmed by * An empty QVariantMap will be returned if the EventHandler hasn't had the room
* the server. * intialised. An empty user (i.e. a QVariantMap with all the correct keys
* but empty values) will be returned if the room has been set but not an event.
* *
* @return a Quotient::RoomMember object for the user. * @return a QVariantMap for the user with the following properties:
* - isLocalUser - Whether the user is the local user.
* - id - The matrix ID of the user.
* - displayName - Display name in the context of this room.
* - avatarSource - The mxc URL for the user's avatar in the current room.
* - avatarMediaId - Avatar id in the context of this room.
* - color - Color for the user.
* - object - The Quotient::User object for the user.
* *
* @sa Quotient::RoomMember * @sa Quotient::User
*/ */
Quotient::RoomMember getReplyAuthor() const; QVariantMap getReplyAuthor() const;
/** /**
* @brief Output a string for the message content of the event replied to ready * @brief Output a string for the message content of the event replied to ready
@@ -329,6 +360,43 @@ public:
*/ */
QString getLocationAssetType() const; QString getLocationAssetType() const;
/**
* @brief Whether the event has any read marker for other users.
*/
bool hasReadMarkers() const;
/**
* @brief Returns a list of user read marker for the event.
*
* @param maxMarkers the maximum number of users to return. Usually the number
* of user read makers shown is limited to not clutter the UI.
* This needs to be the same as used in getNumberExcessReadMarkers
* so that the markers line up with the number displayed, i.e.
* the number of users shown plus the excess number will be
* the total number of other user read markers at an event.
*/
QVariantList getReadMarkers(int maxMarkers = 5) const;
/**
* @brief Returns the number of excess user read markers for the event.
*
* This returns a string in the form "+ x" ready for use in the UI.
*
* @param maxMarkers the maximum number of markers shown in the UI. This needs to
* be the same as used in getReadMarkers so that the value lines
* up with the number displayed, i.e. the number of users shown
* plus the excess number will be the total number of other user
* read markers at an event.
*/
QString getNumberExcessReadMarkers(int maxMarkers = 5) const;
/**
* @brief Returns a string with the names of the read markers at the event.
*
* This is in the form "x users: name 1, name 2, ...".
*/
QString getReadMarkersString() const;
private: private:
const NeoChatRoom *m_room = nullptr; const NeoChatRoom *m_room = nullptr;
const Quotient::RoomEvent *m_event = nullptr; const Quotient::RoomEvent *m_event = nullptr;

View File

@@ -3,7 +3,6 @@
#include "imagepackevent.h" #include "imagepackevent.h"
#include <QJsonObject> #include <QJsonObject>
#include <Quotient/omittable.h>
using namespace Quotient; using namespace Quotient;
@@ -17,16 +16,16 @@ ImagePackEventContent::ImagePackEventContent(const QJsonObject &json)
fromJson<Omittable<QString>>(json["pack"_ls].toObject()["attribution"_ls]), fromJson<Omittable<QString>>(json["pack"_ls].toObject()["attribution"_ls]),
}; };
} else { } else {
pack = std::nullopt; pack = none;
} }
const auto &keys = json["images"_ls].toObject().keys(); const auto &keys = json["images"_ls].toObject().keys();
for (const auto &k : keys) { for (const auto &k : keys) {
std::optional<EventContent::ImageInfo> info; Omittable<EventContent::ImageInfo> info;
if (json["images"_ls][k].toObject().contains(QStringLiteral("info"))) { if (json["images"_ls][k].toObject().contains(QStringLiteral("info"))) {
info = EventContent::ImageInfo(QUrl(json["images"_ls][k]["url"_ls].toString()), json["images"_ls][k]["info"_ls].toObject(), k); info = EventContent::ImageInfo(QUrl(json["images"_ls][k]["url"_ls].toString()), json["images"_ls][k]["info"_ls].toObject(), k);
} else { } else {
info = std::nullopt; info = none;
} }
images += ImagePackImage{ images += ImagePackImage{
k, k,

View File

@@ -26,10 +26,10 @@ public:
* @brief Defines the properties of an image pack. * @brief Defines the properties of an image pack.
*/ */
struct Pack { struct Pack {
std::optional<QString> displayName; /**< The display name of the pack. */ Quotient::Omittable<QString> displayName; /**< The display name of the pack. */
std::optional<QUrl> avatarUrl; /**< The source mxc URL for the pack avatar. */ Quotient::Omittable<QUrl> avatarUrl; /**< The source mxc URL for the pack avatar. */
std::optional<QStringList> usage; /**< An array of the usages for this pack. Possible usages are "emoticon" and "sticker". */ Quotient::Omittable<QStringList> usage; /**< An array of the usages for this pack. Possible usages are "emoticon" and "sticker". */
std::optional<QString> attribution; /**< The attribution for the pack author(s). */ Quotient::Omittable<QString> attribution; /**< The attribution for the pack author(s). */
}; };
/** /**
@@ -38,14 +38,14 @@ public:
struct ImagePackImage { struct ImagePackImage {
QString shortcode; /**< The shortcode for the image. */ QString shortcode; /**< The shortcode for the image. */
QUrl url; /**< The mxc URL for this image. */ QUrl url; /**< The mxc URL for this image. */
std::optional<QString> body; /**< An optional text body for this image. */ Quotient::Omittable<QString> body; /**< An optional text body for this image. */
std::optional<Quotient::EventContent::ImageInfo> info; /**< The ImageInfo object used for the info block of m.sticker events. */ Quotient::Omittable<Quotient::EventContent::ImageInfo> info; /**< The ImageInfo object used for the info block of m.sticker events. */
/** /**
* @brief An array of the usages for this image. * @brief An array of the usages for this image.
* *
* The possible values match those of the usage key of a pack object. * The possible values match those of the usage key of a pack object.
*/ */
std::optional<QStringList> usage; Quotient::Omittable<QStringList> usage;
}; };
/** /**
@@ -53,7 +53,7 @@ public:
* *
* @sa Pack * @sa Pack
*/ */
std::optional<Pack> pack; Quotient::Omittable<Pack> pack;
/** /**
* @brief Return a vector of images in the pack. * @brief Return a vector of images in the pack.

View File

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

View File

@@ -14,15 +14,12 @@ Q_SCRIPTABLE RemoteActions FakeRunner::Actions()
Q_SCRIPTABLE RemoteMatches FakeRunner::Match(const QString &searchTerm) Q_SCRIPTABLE RemoteMatches FakeRunner::Match(const QString &searchTerm)
{ {
Q_UNUSED(searchTerm);
QCoreApplication::quit(); QCoreApplication::quit();
return {}; return {};
} }
Q_SCRIPTABLE void FakeRunner::Run(const QString &id, const QString &actionId) Q_SCRIPTABLE void FakeRunner::Run(const QString &id, const QString &actionId)
{ {
Q_UNUSED(id);
Q_UNUSED(actionId);
QCoreApplication::quit(); QCoreApplication::quit();
} }

View File

@@ -6,9 +6,10 @@
#include <QQmlEngine> #include <QQmlEngine>
#include <Quotient/accountregistry.h> #include <Quotient/accountregistry.h>
#include <Quotient/e2ee/sssshandler.h>
#include <Quotient/keyverificationsession.h> #include <Quotient/keyverificationsession.h>
#include <Quotient/roommember.h> #if __has_include("Quotient/e2ee/sssshandler.h")
#include <Quotient/e2ee/sssshandler.h>
#endif
#include "controller.h" #include "controller.h"
#include "neochatconfig.h" #include "neochatconfig.h"
@@ -46,8 +47,10 @@ struct ForeignKeyVerificationSession {
QML_UNCREATABLE("") QML_UNCREATABLE("")
}; };
#if __has_include("Quotient/e2ee/sssshandler.h")
struct ForeignSSSSHandler { struct ForeignSSSSHandler {
Q_GADGET Q_GADGET
QML_FOREIGN(Quotient::SSSSHandler) QML_FOREIGN(Quotient::SSSSHandler)
QML_NAMED_ELEMENT(SSSSHandler) QML_NAMED_ELEMENT(SSSSHandler)
}; };
#endif

View File

@@ -33,6 +33,43 @@ void IdentityServerHelper::setConnection(NeoChatConnection *connection)
m_connection = connection; m_connection = connection;
Q_EMIT connectionChanged(); Q_EMIT connectionChanged();
Q_EMIT currentServerChanged();
connect(m_connection, &NeoChatConnection::accountDataChanged, this, [this](const QString &type) {
if (type == QLatin1String("m.identity_server")) {
Q_EMIT currentServerChanged();
}
});
}
QString IdentityServerHelper::currentServer() const
{
if (m_connection == nullptr) {
return {};
}
if (!m_connection->hasAccountData(QLatin1String("m.identity_server"))) {
return i18nc("@info", "No identity server configured");
}
const auto url = m_connection->accountData(QLatin1String("m.identity_server"))->contentPart<QUrl>(QLatin1String("base_url"));
if (!url.isEmpty()) {
return url.toString();
}
return i18nc("@info", "No identity server configured");
}
bool IdentityServerHelper::hasCurrentServer() const
{
if (m_connection == nullptr && !m_connection->hasAccountData(QLatin1String("m.identity_server"))) {
return false;
}
const auto url = m_connection->accountData(QLatin1String("m.identity_server"))->contentPart<QUrl>(QLatin1String("base_url"));
if (!url.isEmpty()) {
return true;
}
return false;
} }
QString IdentityServerHelper::url() const QString IdentityServerHelper::url() const
@@ -63,7 +100,7 @@ void IdentityServerHelper::checkUrl()
m_idServerCheckRequest.clear(); m_idServerCheckRequest.clear();
} }
if (m_url == m_connection->identityServer().toString()) { if (m_url == currentServer()) {
m_status = Match; m_status = Match;
Q_EMIT statusChanged(); Q_EMIT statusChanged();
return; return;
@@ -97,7 +134,7 @@ void IdentityServerHelper::checkUrl()
void IdentityServerHelper::setIdentityServer() void IdentityServerHelper::setIdentityServer()
{ {
if (m_url == m_connection->identityServer().toString()) { if (m_url == currentServer()) {
return; return;
} }
@@ -108,7 +145,7 @@ void IdentityServerHelper::setIdentityServer()
void IdentityServerHelper::clearIdentityServer() void IdentityServerHelper::clearIdentityServer()
{ {
if (m_connection->identityServer().isEmpty()) { if (currentServer().isEmpty()) {
return; return;
} }
m_connection->setAccountData(QLatin1String("m.identity_server"), {{QLatin1String("base_url"), QString()}}); m_connection->setAccountData(QLatin1String("m.identity_server"), {{QLatin1String("base_url"), QString()}});

View File

@@ -26,6 +26,16 @@ class IdentityServerHelper : public QObject
*/ */
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged) Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
/**
* @brief The current identity server.
*/
Q_PROPERTY(QString currentServer READ currentServer NOTIFY currentServerChanged)
/**
* @brief Whether an identity server is currently configured.
*/
Q_PROPERTY(bool hasCurrentServer READ hasCurrentServer NOTIFY currentServerChanged)
/** /**
* @brief The URL for the desired server. * @brief The URL for the desired server.
*/ */
@@ -54,6 +64,10 @@ public:
[[nodiscard]] NeoChatConnection *connection() const; [[nodiscard]] NeoChatConnection *connection() const;
void setConnection(NeoChatConnection *connection); void setConnection(NeoChatConnection *connection);
[[nodiscard]] QString currentServer() const;
[[nodiscard]] bool hasCurrentServer() const;
[[nodiscard]] QString url() const; [[nodiscard]] QString url() const;
void setUrl(const QString &url); void setUrl(const QString &url);
@@ -73,6 +87,7 @@ public:
Q_SIGNALS: Q_SIGNALS:
void connectionChanged(); void connectionChanged();
void currentServerChanged();
void urlChanged(); void urlChanged();
void statusChanged(); void statusChanged();

View File

@@ -9,5 +9,5 @@
class NeochatAdd3PIdJob : public Quotient::BaseJob class NeochatAdd3PIdJob : public Quotient::BaseJob
{ {
public: public:
explicit NeochatAdd3PIdJob(const QString &clientSecret, const QString &sid, const Quotient::Omittable<QJsonObject> &auth = {}); explicit NeochatAdd3PIdJob(const QString &clientSecret, const QString &sid, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
}; };

View File

@@ -4,11 +4,10 @@
#pragma once #pragma once
#include <Quotient/jobs/basejob.h> #include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h> #include <Quotient/omittable.h>
class NeochatChangePasswordJob : public Quotient::BaseJob class NeochatChangePasswordJob : public Quotient::BaseJob
{ {
public: public:
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Quotient::Omittable<QJsonObject> &auth = {}); explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
}; };

View File

@@ -9,5 +9,5 @@
class NeoChatDeactivateAccountJob : public Quotient::BaseJob class NeoChatDeactivateAccountJob : public Quotient::BaseJob
{ {
public: public:
explicit NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth = {}); explicit NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
}; };

View File

@@ -9,5 +9,5 @@
class NeochatDeleteDeviceJob : public Quotient::BaseJob class NeochatDeleteDeviceJob : public Quotient::BaseJob
{ {
public: public:
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = {}); explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
}; };

View File

@@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatgetcommonroomsjob.h"
using namespace Quotient;
NeochatGetCommonRoomsJob::NeochatGetCommonRoomsJob(const QString &userId)
: BaseJob(HttpVerb::Get,
QStringLiteral("GetCommonRoomsJob"),
QStringLiteral("/_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms").toLatin1(),
QUrlQuery({{QStringLiteral("user_id"), userId}}))
{
}

View File

@@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
// TODO: Upstream to libQuotient
class NeochatGetCommonRoomsJob : public Quotient::BaseJob
{
public:
explicit NeochatGetCommonRoomsJob(const QString &userId);
};

View File

@@ -54,19 +54,14 @@ void LoginHelper::init()
m_connection = new NeoChatConnection(); m_connection = new NeoChatConnection();
} }
m_connection->resolveServer(m_matrixId); m_connection->resolveServer(m_matrixId);
connect( connectSingleShot(m_connection.get(), &Connection::loginFlowsChanged, this, [this]() {
m_connection.get(), setHomeserverReachable(true);
&Connection::loginFlowsChanged, m_testing = false;
this, Q_EMIT testingChanged();
[this]() { m_supportsSso = m_connection->supportsSso();
setHomeserverReachable(true); m_supportsPassword = m_connection->supportsPasswordAuth();
m_testing = false; Q_EMIT loginFlowsChanged();
Q_EMIT testingChanged(); });
m_supportsSso = m_connection->supportsSso();
m_supportsPassword = m_connection->supportsPasswordAuth();
Q_EMIT loginFlowsChanged();
},
Qt::SingleShotConnection);
}); });
connect(m_connection, &Connection::connected, this, [this] { connect(m_connection, &Connection::connected, this, [this] {
Q_EMIT connected(); Q_EMIT connected();
@@ -105,14 +100,9 @@ void LoginHelper::init()
Q_EMIT Controller::instance().errorOccured(i18n("Network Error"), std::move(error)); Q_EMIT Controller::instance().errorOccured(i18n("Network Error"), std::move(error));
}); });
connect( connectSingleShot(m_connection.get(), &Connection::syncDone, this, [this]() {
m_connection.get(), Q_EMIT loaded();
&Connection::syncDone, });
this,
[this]() {
Q_EMIT loaded();
},
Qt::SingleShotConnection);
} }
void LoginHelper::setHomeserverReachable(bool reachable) void LoginHelper::setHomeserverReachable(bool reachable)
@@ -192,16 +182,11 @@ QUrl LoginHelper::ssoUrl() const
void LoginHelper::loginWithSso() void LoginHelper::loginWithSso()
{ {
m_connection->resolveServer(m_matrixId); m_connection->resolveServer(m_matrixId);
connect( connectSingleShot(m_connection.get(), &Connection::loginFlowsChanged, this, [this]() {
m_connection.get(), SsoSession *session = m_connection->prepareForSso(m_deviceName);
&Connection::loginFlowsChanged, m_ssoUrl = session->ssoUrl();
this, Q_EMIT ssoUrlChanged();
[this]() { });
SsoSession *session = m_connection->prepareForSso(m_deviceName);
m_ssoUrl = session->ssoUrl();
Q_EMIT ssoUrlChanged();
},
Qt::SingleShotConnection);
} }
bool LoginHelper::testing() const bool LoginHelper::testing() const

View File

@@ -2,7 +2,7 @@
# SPDX-License-Identifier: BSD-2-Clause # SPDX-License-Identifier: BSD-2-Clause
qt_add_library(login STATIC) qt_add_library(login STATIC)
ecm_add_qml_module(login GENERATE_PLUGIN_SOURCE qt_add_qml_module(login
URI org.kde.neochat.login URI org.kde.neochat.login
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/login OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/login
QML_FILES QML_FILES

View File

@@ -13,7 +13,7 @@ LoginStep {
id: root id: root
FormCard.FormTextDelegate { FormCard.FormTextDelegate {
text: i18n("Please wait while your messages are loaded from the server. This might take a little while.") text: i18n("Please wait. This might take a little while.")
} }
FormCard.AbstractFormDelegate { FormCard.AbstractFormDelegate {
contentItem: QQC2.BusyIndicator {} contentItem: QQC2.BusyIndicator {}

View File

@@ -92,7 +92,7 @@ FormCard.FormCardPage {
} }
QQC2.ToolButton { QQC2.ToolButton {
text: i18nc("@action:button", "Log out of this account") text: i18nc("@action:button", "Remove this account")
icon.name: "edit-delete-remove" icon.name: "edit-delete-remove"
onClicked: Controller.removeConnection(modelData) onClicked: Controller.removeConnection(modelData)
display: QQC2.Button.IconOnly display: QQC2.Button.IconOnly

View File

@@ -13,7 +13,6 @@
#include <QQuickStyle> #include <QQuickStyle>
#include <QQuickWindow> #include <QQuickWindow>
#include <QtQml/QQmlExtensionPlugin> #include <QtQml/QQmlExtensionPlugin>
#include <Quotient/connection.h>
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
#include <QGuiApplication> #include <QGuiApplication>
@@ -175,7 +174,6 @@ int main(int argc, char *argv[])
initLogging(); initLogging();
Connection::setEncryptionDefault(true); Connection::setEncryptionDefault(true);
Connection::setDirectChatEncryptionDefault(true);
#ifdef NEOCHAT_FLATPAK #ifdef NEOCHAT_FLATPAK
// Copy over the included FontConfig configuration to the // Copy over the included FontConfig configuration to the

View File

@@ -4,13 +4,11 @@
#include "actionsmodel.h" #include "actionsmodel.h"
#include "chatbarcache.h" #include "chatbarcache.h"
#include "controller.h"
#include "neochatconnection.h" #include "neochatconnection.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "roommanager.h" #include "roommanager.h"
#include <Quotient/events/roommemberevent.h> #include <Quotient/events/roommemberevent.h>
#include <Quotient/events/roompowerlevelsevent.h> #include <Quotient/events/roompowerlevelsevent.h>
#include <Quotient/user.h>
#include <KLocalizedString> #include <KLocalizedString>
@@ -24,15 +22,14 @@ QStringList rainbowColors{"#ff2b00"_ls, "#ff5500"_ls, "#ff8000"_ls, "#ffaa00"_ls
auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) { auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) {
if (text.isEmpty()) { if (text.isEmpty()) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18n("Leaving this room.")); Q_EMIT room->showMessage(NeoChatRoom::Info, i18n("Leaving this room."));
room->connection()->leaveRoom(room); room->connection()->leaveRoom(room);
} else { } else {
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)")); QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text); auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) { if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage( Q_EMIT room->showMessage(NeoChatRoom::Error,
Controller::Error, i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString(); return QString();
} }
auto leaving = room->connection()->room(text); auto leaving = room->connection()->room(text);
@@ -40,10 +37,10 @@ auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *
leaving = room->connection()->roomByAlias(text); leaving = room->connection()->roomByAlias(text);
} }
if (leaving) { if (leaving) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("Leaving room <roomname>.", "Leaving room %1.", text)); Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
room->connection()->leaveRoom(leaving); room->connection()->leaveRoom(leaving);
} else { } else {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("Room <roomname> not found", "Room %1 not found.", text)); Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Room <roomname> not found", "Room %1 not found.", text));
} }
} }
return QString(); return QString();
@@ -51,7 +48,7 @@ auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *
auto roomNickLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) { auto roomNickLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) {
if (text.isEmpty()) { if (text.isEmpty()) {
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("No new nickname provided, no changes will happen.")); Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen."));
} else { } else {
room->connection()->user()->rename(text, room); room->connection()->user()->rename(text, room);
} }
@@ -193,31 +190,28 @@ QList<ActionsModel::Action> actions{
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))")); QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text); auto regexMatch = mxidRegex.match(text);
if (!regexMatch.hasMatch()) { if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage(Controller::Error, Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString(); return QString();
} }
const RoomMemberEvent *roomMemberEvent = room->currentState().get<RoomMemberEvent>(text); const RoomMemberEvent *roomMemberEvent = room->currentState().get<RoomMemberEvent>(text);
if (roomMemberEvent && roomMemberEvent->membership() == Membership::Invite) { if (roomMemberEvent && roomMemberEvent->membership() == Membership::Invite) {
Q_EMIT Controller::instance().showMessage(Controller::Info, Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already invited to this room.", "%1 is already invited to this room.", text));
i18nc("<user> is already invited to this room.", "%1 is already invited to this room.", text));
return QString(); return QString();
} }
if (roomMemberEvent && roomMemberEvent->membership() == Membership::Ban) { if (roomMemberEvent && roomMemberEvent->membership() == Membership::Ban) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("<user> is banned from this room.", "%1 is banned from this room.", text)); Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is banned from this room.", "%1 is banned from this room.", text));
return QString(); return QString();
} }
if (room->localMember().id() == text) { if (room->localUser()->id() == text) {
Q_EMIT Controller::instance().showMessage(Controller::Positive, i18n("You are already in this room.")); Q_EMIT room->showMessage(NeoChatRoom::Positive, i18n("You are already in this room."));
return QString(); return QString();
} }
if (room->members().contains(room->member(text))) { if (room->users().contains(room->user(text))) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("<user> is already in this room.", "%1 is already in this room.", text)); Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already in this room.", "%1 is already in this room.", text));
return QString(); return QString();
} }
room->inviteToRoom(text); room->inviteToRoom(text);
Q_EMIT Controller::instance().showMessage(Controller::Positive, Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> was invited into this room", "%1 was invited into this room", text));
i18nc("<username> was invited into this room", "%1 was invited into this room", text));
return QString(); return QString();
}, },
false, false,
@@ -231,9 +225,8 @@ QList<ActionsModel::Action> actions{
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)")); QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text); auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) { if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage( Q_EMIT room->showMessage(NeoChatRoom::Error,
Controller::Error, i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString(); return QString();
} }
auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text); auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text);
@@ -241,7 +234,7 @@ QList<ActionsModel::Action> actions{
RoomManager::instance().resolveResource(targetRoom->id()); RoomManager::instance().resolveResource(targetRoom->id());
return QString(); return QString();
} }
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text)); Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
RoomManager::instance().resolveResource(text, "join"_ls); RoomManager::instance().resolveResource(text, "join"_ls);
return QString(); return QString();
}, },
@@ -258,9 +251,8 @@ QList<ActionsModel::Action> actions{
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)")); QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(roomName); auto regexMatch = roomRegex.match(roomName);
if (!regexMatch.hasMatch()) { if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage( Q_EMIT room->showMessage(NeoChatRoom::Error,
Controller::Error, i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString(); return QString();
} }
auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text); auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text);
@@ -268,7 +260,7 @@ QList<ActionsModel::Action> actions{
RoomManager::instance().resolveResource(targetRoom->id()); RoomManager::instance().resolveResource(targetRoom->id());
return QString(); return QString();
} }
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("Knocking room <roomname>.", "Knocking room %1.", text)); Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Knocking room <roomname>.", "Knocking room %1.", text));
auto connection = dynamic_cast<NeoChatConnection *>(room->connection()); auto connection = dynamic_cast<NeoChatConnection *>(room->connection());
const auto knownServer = roomName.mid(roomName.indexOf(":"_ls) + 1); const auto knownServer = roomName.mid(roomName.indexOf(":"_ls) + 1);
if (parts.length() >= 2) { if (parts.length() >= 2) {
@@ -289,16 +281,15 @@ QList<ActionsModel::Action> actions{
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)")); QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text); auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) { if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage( Q_EMIT room->showMessage(NeoChatRoom::Error,
Controller::Error, i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString(); return QString();
} }
if (room->connection()->room(text) || room->connection()->roomByAlias(text)) { if (room->connection()->room(text) || room->connection()->roomByAlias(text)) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("You are already in room <roomname>.", "You are already in room %1.", text)); Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("You are already in room <roomname>.", "You are already in room %1.", text));
return QString(); return QString();
} }
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text)); Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
RoomManager::instance().resolveResource(text, "join"_ls); RoomManager::instance().resolveResource(text, "join"_ls);
return QString(); return QString();
}, },
@@ -327,7 +318,7 @@ QList<ActionsModel::Action> actions{
QStringLiteral("nick"), QStringLiteral("nick"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *) { [](const QString &text, NeoChatRoom *room, ChatBarCache *) {
if (text.isEmpty()) { if (text.isEmpty()) {
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("No new nickname provided, no changes will happen.")); Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen."));
} else { } else {
room->connection()->user()->rename(text); room->connection()->user()->rename(text);
} }
@@ -361,17 +352,15 @@ QList<ActionsModel::Action> actions{
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))")); QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text); auto regexMatch = mxidRegex.match(text);
if (!regexMatch.hasMatch()) { if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage(Controller::Error, Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString(); return QString();
} }
if (room->connection()->ignoredUsers().contains(text)) { if (room->connection()->ignoredUsers().contains(text)) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("<username> is already ignored.", "%1 is already ignored.", text)); Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<username> is already ignored.", "%1 is already ignored.", text));
return QString(); return QString();
} }
room->connection()->addToIgnoredUsers(text); room->connection()->addToIgnoredUsers(room->connection()->user(text));
Q_EMIT Controller::instance().showMessage(Controller::Positive, i18nc("<username> is now ignored", "%1 is now ignored.", text)); Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is now ignored", "%1 is now ignored.", text));
return QString(); return QString();
}, },
false, false,
@@ -386,16 +375,15 @@ QList<ActionsModel::Action> actions{
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))")); QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text); auto regexMatch = mxidRegex.match(text);
if (!regexMatch.hasMatch()) { if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage(Controller::Error, Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString(); return QString();
} }
if (!room->connection()->ignoredUsers().contains(text)) { if (!room->connection()->ignoredUsers().contains(text)) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("<username> is not ignored.", "%1 is not ignored.", text)); Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<username> is not ignored.", "%1 is not ignored.", text));
return QString(); return QString();
} }
room->connection()->removeFromIgnoredUsers(text); room->connection()->removeFromIgnoredUsers(room->connection()->user(text));
Q_EMIT Controller::instance().showMessage(Controller::Positive, i18nc("<username> is no longer ignored.", "%1 is no longer ignored.", text)); Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> is no longer ignored.", "%1 is no longer ignored.", text));
return QString(); return QString();
}, },
false, false,
@@ -431,33 +419,30 @@ QList<ActionsModel::Action> actions{
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))")); QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(parts[0]); auto regexMatch = mxidRegex.match(parts[0]);
if (!regexMatch.hasMatch()) { if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage(Controller::Error, Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString(); return QString();
} }
auto state = room->currentState().get<RoomMemberEvent>(parts[0]); auto state = room->currentState().get<RoomMemberEvent>(parts[0]);
if (state && state->membership() == Membership::Ban) { if (state && state->membership() == Membership::Ban) {
Q_EMIT Controller::instance().showMessage(Controller::Info, Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already banned from this room.", "%1 is already banned from this room.", text));
i18nc("<user> is already banned from this room.", "%1 is already banned from this room.", text));
return QString(); return QString();
} }
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>(); auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
if (!plEvent) { if (!plEvent) {
return QString(); return QString();
} }
if (plEvent->ban() > plEvent->powerLevelForUser(room->localMember().id())) { if (plEvent->ban() > plEvent->powerLevelForUser(room->localUser()->id())) {
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("You are not allowed to ban users from this room.")); Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("You are not allowed to ban users from this room."));
return QString(); return QString();
} }
if (plEvent->powerLevelForUser(room->localMember().id()) <= plEvent->powerLevelForUser(parts[0])) { if (plEvent->powerLevelForUser(room->localUser()->id()) <= plEvent->powerLevelForUser(parts[0])) {
Q_EMIT Controller::instance().showMessage( Q_EMIT room->showMessage(
Controller::Error, NeoChatRoom::Error,
i18nc("You are not allowed to ban <username> from this room.", "You are not allowed to ban %1 from this room.", parts[0])); i18nc("You are not allowed to ban <username> from this room.", "You are not allowed to ban %1 from this room.", parts[0]));
return QString(); return QString();
} }
room->ban(parts[0], parts.size() > 1 ? parts.mid(1).join(QLatin1Char(' ')) : QString()); room->ban(parts[0], parts.size() > 1 ? parts.mid(1).join(QLatin1Char(' ')) : QString());
Q_EMIT Controller::instance().showMessage(Controller::Positive, Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> was banned from this room.", "%1 was banned from this room.", parts[0]));
i18nc("<username> was banned from this room.", "%1 was banned from this room.", parts[0]));
return QString(); return QString();
}, },
false, false,
@@ -472,27 +457,24 @@ QList<ActionsModel::Action> actions{
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))")); QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text); auto regexMatch = mxidRegex.match(text);
if (!regexMatch.hasMatch()) { if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage(Controller::Error, Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString(); return QString();
} }
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>(); auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
if (!plEvent) { if (!plEvent) {
return QString(); return QString();
} }
if (plEvent->ban() > plEvent->powerLevelForUser(room->localMember().id())) { if (plEvent->ban() > plEvent->powerLevelForUser(room->localUser()->id())) {
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("You are not allowed to unban users from this room.")); Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("You are not allowed to unban users from this room."));
return QString(); return QString();
} }
auto state = room->currentState().get<RoomMemberEvent>(text); auto state = room->currentState().get<RoomMemberEvent>(text);
if (state && state->membership() != Membership::Ban) { if (state && state->membership() != Membership::Ban) {
Q_EMIT Controller::instance().showMessage(Controller::Info, Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is not banned from this room.", "%1 is not banned from this room.", text));
i18nc("<user> is not banned from this room.", "%1 is not banned from this room.", text));
return QString(); return QString();
} }
room->unban(text); room->unban(text);
Q_EMIT Controller::instance().showMessage(Controller::Positive, Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> was unbanned from this room.", "%1 was unbanned from this room.", text));
i18nc("<username> was unbanned from this room.", "%1 was unbanned from this room.", text));
return QString(); return QString();
}, },
@@ -509,16 +491,16 @@ QList<ActionsModel::Action> actions{
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))")); QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(parts[0]); auto regexMatch = mxidRegex.match(parts[0]);
if (!regexMatch.hasMatch()) { if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage(Controller::Error, Q_EMIT room->showMessage(NeoChatRoom::Error,
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", parts[0])); i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", parts[0]));
return QString(); return QString();
} }
if (parts[0] == room->localMember().id()) { if (parts[0] == room->localUser()->id()) {
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("You cannot kick yourself from the room.")); Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("You cannot kick yourself from the room."));
return QString(); return QString();
} }
if (!room->isMember(parts[0])) { if (!room->isMember(parts[0])) {
Q_EMIT Controller::instance().showMessage(Controller::Error, i18nc("<username> is not in this room", "%1 is not in this room.", parts[0])); Q_EMIT room->showMessage(NeoChatRoom::Error, i18nc("<username> is not in this room", "%1 is not in this room.", parts[0]));
return QString(); return QString();
} }
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>(); auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
@@ -526,19 +508,18 @@ QList<ActionsModel::Action> actions{
return QString(); return QString();
} }
auto kick = plEvent->kick(); auto kick = plEvent->kick();
if (plEvent->powerLevelForUser(room->localMember().id()) < kick) { if (plEvent->powerLevelForUser(room->localUser()->id()) < kick) {
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("You are not allowed to kick users from this room.")); Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("You are not allowed to kick users from this room."));
return QString(); return QString();
} }
if (plEvent->powerLevelForUser(room->localMember().id()) <= plEvent->powerLevelForUser(parts[0])) { if (plEvent->powerLevelForUser(room->localUser()->id()) <= plEvent->powerLevelForUser(parts[0])) {
Q_EMIT Controller::instance().showMessage( Q_EMIT room->showMessage(
Controller::Error, NeoChatRoom::Error,
i18nc("You are not allowed to kick <username> from this room", "You are not allowed to kick %1 from this room.", parts[0])); i18nc("You are not allowed to kick <username> from this room", "You are not allowed to kick %1 from this room.", parts[0]));
return QString(); return QString();
} }
room->kickMember(parts[0], parts.size() > 1 ? parts.mid(1).join(QLatin1Char(' ')) : QString()); room->kickMember(parts[0], parts.size() > 1 ? parts.mid(1).join(QLatin1Char(' ')) : QString());
Q_EMIT Controller::instance().showMessage(Controller::Positive, Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> was kicked from this room.", "%1 was kicked from this room.", parts[0]));
i18nc("<username> was kicked from this room.", "%1 was kicked from this room.", parts[0]));
return QString(); return QString();
}, },
false, false,

View File

@@ -9,16 +9,18 @@
#include "customemojimodel.h" #include "customemojimodel.h"
#include "emojimodel.h" #include "emojimodel.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "roommanager.h"
#include "userlistmodel.h" #include "userlistmodel.h"
CompletionModel::CompletionModel(QObject *parent) CompletionModel::CompletionModel(QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
, m_filterModel(new CompletionProxyModel()) , m_filterModel(new CompletionProxyModel())
, m_userListModel(RoomManager::instance().userListModel()) , m_userListModel(new UserListModel(this))
, m_emojiModel(new QConcatenateTablesProxyModel(this)) , m_emojiModel(new QConcatenateTablesProxyModel(this))
{ {
connect(this, &CompletionModel::textChanged, this, &CompletionModel::updateCompletion); connect(this, &CompletionModel::textChanged, this, &CompletionModel::updateCompletion);
connect(this, &CompletionModel::roomChanged, this, [this]() {
m_userListModel->setRoom(m_room);
});
m_emojiModel->addSourceModel(&CustomEmojiModel::instance()); m_emojiModel->addSourceModel(&CustomEmojiModel::instance());
m_emojiModel->addSourceModel(&EmojiModel::instance()); m_emojiModel->addSourceModel(&EmojiModel::instance());
} }

View File

@@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "customemojimodel.h"
#include <QRegularExpression>
class NeoChatConnection;
struct CustomEmoji {
QString name; // with :semicolons:
QString url; // mxc://
QRegularExpression regexp;
};
struct CustomEmojiModel::Private {
QPointer<NeoChatConnection> connection;
QList<CustomEmoji> emojies;
};

View File

@@ -26,7 +26,7 @@ int EmojiModel::rowCount(const QModelIndex &parent) const
{ {
Q_UNUSED(parent); Q_UNUSED(parent);
int total = 0; int total = 0;
for (const auto &category : std::as_const(_emojis)) { for (const auto &category : _emojis) {
total += category.count(); total += category.count();
} }
return total; return total;
@@ -35,7 +35,7 @@ int EmojiModel::rowCount(const QModelIndex &parent) const
QVariant EmojiModel::data(const QModelIndex &index, int role) const QVariant EmojiModel::data(const QModelIndex &index, int role) const
{ {
auto row = index.row(); auto row = index.row();
for (const auto &category : std::as_const(_emojis)) { for (const auto &category : _emojis) {
if (row >= category.count()) { if (row >= category.count()) {
row -= category.count(); row -= category.count();
continue; continue;
@@ -79,8 +79,7 @@ QVariantList EmojiModel::filterModelNoCustom(const QString &filter, bool limit)
{ {
QVariantList result; QVariantList result;
const auto &values = _emojis.values(); for (const auto &e : _emojis.values()) {
for (const auto &e : values) {
for (const auto &variant : e) { for (const auto &variant : e) {
const auto &emoji = qvariant_cast<Emoji>(variant); const auto &emoji = qvariant_cast<Emoji>(variant);
if (emoji.shortName.contains(filter, Qt::CaseInsensitive)) { if (emoji.shortName.contains(filter, Qt::CaseInsensitive)) {
@@ -122,8 +121,7 @@ QVariantList EmojiModel::emojis(Category category) const
} }
if (category == HistoryNoCustom) { if (category == HistoryNoCustom) {
QVariantList list; QVariantList list;
const auto &history = emojiHistory(); for (const auto &e : emojiHistory()) {
for (const auto &e : history) {
auto emoji = qvariant_cast<Emoji>(e); auto emoji = qvariant_cast<Emoji>(e);
if (!emoji.isCustom) { if (!emoji.isCustom) {
list.append(e); list.append(e);
@@ -226,9 +224,8 @@ QVariantList EmojiModel::categoriesWithCustom() const
QVariantList EmojiModel::emojiHistory() const QVariantList EmojiModel::emojiHistory() const
{ {
QVariantList list; QVariantList list;
const auto &lastUsed = lastUsedEmojis(); for (const auto &historicEmoji : lastUsedEmojis()) {
for (const auto &historicEmoji : lastUsed) { for (const auto &emojiCategory : _emojis) {
for (const auto &emojiCategory : std::as_const(_emojis)) {
for (const auto &emoji : emojiCategory) { for (const auto &emoji : emojiCategory) {
if (qvariant_cast<Emoji>(emoji).shortName == historicEmoji) { if (qvariant_cast<Emoji>(emoji).shortName == historicEmoji) {
list.append(emoji); list.append(emoji);

View File

@@ -80,7 +80,7 @@ QVariant LiveLocationsModel::data(const QModelIndex &index, int roleName) const
case AssetRole: case AssetRole:
return data.beaconInfo["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString(); return data.beaconInfo["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
case AuthorRole: case AuthorRole:
return QVariant::fromValue(m_room->member(data.senderId)); return m_room->getUser(data.senderId);
case IsLiveRole: { case IsLiveRole: {
if (!data.beaconInfo["live"_ls].toBool()) { if (!data.beaconInfo["live"_ls].toBool()) {
return false; return false;

View File

@@ -63,7 +63,7 @@ void LocationsModel::addLocation(const RoomMessageEvent *event)
.latitude = latitude, .latitude = latitude,
.longitude = longitude, .longitude = longitude,
.content = event->contentJson(), .content = event->contentJson(),
.member = m_room->member(event->senderId()), .author = m_room->user(event->senderId()),
}; };
endInsertRows(); endInsertRows();
} }
@@ -105,7 +105,7 @@ QVariant LocationsModel::data(const QModelIndex &index, int roleName) const
} else if (roleName == AssetRole) { } else if (roleName == AssetRole) {
return m_locations[row].content["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString(); return m_locations[row].content["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
} else if (roleName == AuthorRole) { } else if (roleName == AuthorRole) {
return QVariant::fromValue(m_locations[row].member); return m_room->getUser(m_locations[row].author);
} }
return {}; return {};
} }

View File

@@ -11,7 +11,7 @@
#include "neochatroom.h" #include "neochatroom.h"
#include <Quotient/events/roommessageevent.h> #include <Quotient/events/roommessageevent.h>
#include <Quotient/roommember.h> #include <Quotient/user.h>
class LocationsModel : public QAbstractListModel class LocationsModel : public QAbstractListModel
{ {
@@ -57,7 +57,7 @@ private:
float latitude; float latitude;
float longitude; float longitude;
QJsonObject content; QJsonObject content;
Quotient::RoomMember member; Quotient::User *author;
}; };
QList<LocationData> m_locations; QList<LocationData> m_locations;
void addLocation(const Quotient::RoomMessageEvent *event); void addLocation(const Quotient::RoomMessageEvent *event);

View File

@@ -6,7 +6,6 @@
#include <Quotient/events/roommessageevent.h> #include <Quotient/events/roommessageevent.h>
#include <Quotient/room.h> #include <Quotient/room.h>
#include "messagecontentmodel.h"
#include "messageeventmodel.h" #include "messageeventmodel.h"
#include "messagefiltermodel.h" #include "messagefiltermodel.h"
@@ -30,6 +29,40 @@ bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex
QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
{ {
if (role == SourceRole) {
if (mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("image"))) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("source")].toUrl();
} else if (mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("video"))) {
auto progressInfo = mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>();
if (progressInfo.completed()) {
return mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>().localPath;
} else {
return QUrl();
}
} else {
return QUrl();
}
}
if (role == TempSourceRole) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("tempInfo")].toMap()[QStringLiteral("source")].toUrl();
}
if (role == TypeRole) {
if (mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("image"))) {
return MediaType::Image;
} else {
return MediaType::Video;
}
}
if (role == CaptionRole) {
return mapToSource(index).data(Qt::DisplayRole);
}
if (role == SourceWidthRole) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("width")].toFloat();
}
if (role == SourceHeightRole) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("height")].toFloat();
}
// We need to catch this one and return true if the next media object was // We need to catch this one and return true if the next media object was
// on a different day. // on a different day.
if (role == MessageEventModel::ShowSectionRole) { if (role == MessageEventModel::ShowSectionRole) {
@@ -37,45 +70,6 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
const auto previousEventDay = mapToSource(this->index(index.row() + 1, 0)).data(MessageEventModel::TimeRole).toDateTime().toLocalTime().date(); const auto previousEventDay = mapToSource(this->index(index.row() + 1, 0)).data(MessageEventModel::TimeRole).toDateTime().toLocalTime().date();
return day != previousEventDay; return day != previousEventDay;
} }
// Catch and force the author to be shown for all rows
if (role == MessageEventModel::ContentModelRole) {
const auto model = qvariant_cast<MessageContentModel *>(mapToSource(index).data(MessageEventModel::ContentModelRole));
if (model != nullptr) {
model->setShowAuthor(true);
}
return QVariant::fromValue<MessageContentModel *>(model);
}
QVariantMap mediaInfo = mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap();
if (role == TempSourceRole) {
return mediaInfo[QStringLiteral("tempInfo")].toMap()[QStringLiteral("source")].toUrl();
}
if (role == CaptionRole) {
return mapToSource(index).data(Qt::DisplayRole);
}
if (role == SourceWidthRole) {
return mediaInfo[QStringLiteral("width")].toFloat();
}
if (role == SourceHeightRole) {
return mediaInfo[QStringLiteral("height")].toFloat();
}
bool isVideo = mediaInfo[QStringLiteral("mimeType")].toString().contains(QStringLiteral("video"));
if (role == TypeRole) {
return (isVideo) ? MediaType::Video : MediaType::Image;
}
if (role == SourceRole) {
if (isVideo) {
auto progressInfo = mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>();
if (progressInfo.completed()) {
return mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>().localPath;
}
} else {
return mediaInfo[QStringLiteral("source")].toUrl();
}
}
return sourceModel()->data(mapToSource(index), role); return sourceModel()->data(mapToSource(index), role);
} }

View File

@@ -3,7 +3,6 @@
#include "messagecontentmodel.h" #include "messagecontentmodel.h"
#include "neochatconfig.h" #include "neochatconfig.h"
#include "neochatroommember.h"
#include <QImageReader> #include <QImageReader>
@@ -12,7 +11,6 @@
#include <Quotient/events/stickerevent.h> #include <Quotient/events/stickerevent.h>
#include <KLocalizedString> #include <KLocalizedString>
#include <Quotient/qt_connection_util.h>
#ifndef Q_OS_ANDROID #ifndef Q_OS_ANDROID
#include <KSyntaxHighlighting/Definition> #include <KSyntaxHighlighting/Definition>
@@ -31,175 +29,96 @@
using namespace Quotient; using namespace Quotient;
MessageContentModel::MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply, bool isPending) MessageContentModel::MessageContentModel(const Quotient::RoomEvent *event, NeoChatRoom *room)
: QAbstractListModel(nullptr) : QAbstractListModel(nullptr)
, m_room(room) , m_room(room)
, m_eventId(event != nullptr ? event->id() : QString()) , m_event(event)
, m_eventSenderId(event != nullptr ? event->senderId() : QString())
, m_isPending(isPending)
, m_isReply(isReply)
{ {
intiializeEvent(event); if (m_room != nullptr) {
initializeModel(); connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) {
} if (m_room != nullptr && m_event != nullptr) {
if (m_event->id() == serverEvent->id()) {
MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending) beginResetModel();
: QAbstractListModel(nullptr) m_event = serverEvent;
, m_room(room) endResetModel();
, m_eventId(eventId) }
, m_isPending(isPending)
, m_isReply(isReply)
{
initializeModel();
}
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.
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_event = loadEvent<RoomEvent>(m_room->getEvent(eventId)->fullJson());
Q_EMIT eventUpdated();
updateReplyModel();
resetContent();
return true;
} }
} });
return false; connect(m_room, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) {
}); if (m_room != nullptr && m_event != nullptr) {
if (m_event->id() == newEvent->id()) {
beginResetModel();
m_event = newEvent;
endResetModel();
}
}
});
connect(m_room, &NeoChatRoom::replyLoaded, this, [this](const QString &eventId, const QString &replyId) {
Q_UNUSED(eventId)
if (m_event != nullptr && m_room != nullptr) {
const auto eventHandler = EventHandler(m_room, m_event);
if (replyId == eventHandler.getReplyId()) {
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
beginResetModel();
m_components[0].type = MessageComponentType::Reply;
endResetModel();
}
}
});
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_event->id()) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_event->id()) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_event->id()) {
updateComponents();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
if (m_event == nullptr) { QString mxcUrl;
m_room->downloadEventFromServer(m_eventId); if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
} if (event->hasFileContent()) {
mxcUrl = event->content()->fileInfo()->url().toString();
connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) { }
if (m_room != nullptr && m_event != nullptr) { } else if (auto event = eventCast<const Quotient::StickerEvent>(m_event)) {
if (m_eventId == serverEvent->id()) { mxcUrl = event->image().fileInfo()->url().toString();
}
if (mxcUrl.isEmpty()) {
return;
}
auto localPath = m_room->fileTransferInfo(m_event->id()).localPath.toLocalFile();
auto config = KSharedConfig::openStateConfig(QStringLiteral("neochatdownloads"))->group(QStringLiteral("downloads"));
config.writePathEntry(mxcUrl.mid(6), localPath);
}
});
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_event->id()) {
updateComponents();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room->editCache(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
if (m_event != nullptr && (oldEventId == m_event->id() || newEventId == m_event->id())) {
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
beginResetModel(); beginResetModel();
m_isPending = false; updateComponents(newEventId == m_event->id());
intiializeEvent(serverEvent);
endResetModel(); endResetModel();
} }
} });
}); connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() {
connect(m_room, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) { updateComponents();
if (m_room != nullptr && m_event != nullptr) { });
if (m_eventId == newEvent->id()) { connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, [this]() {
beginResetModel(); updateComponents();
intiializeEvent(newEvent); });
endResetModel();
}
}
});
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_eventId) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_eventId) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
if (m_room != nullptr && m_event != nullptr && eventId == m_eventId) {
resetContent();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_eventId) {
resetContent();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room->editCache(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
if (m_event != nullptr && (oldEventId == m_eventId || newEventId == m_eventId)) {
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
beginResetModel();
resetContent(newEventId == m_eventId);
endResetModel();
}
});
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() {
resetContent();
});
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, [this]() {
resetContent();
});
connect(m_room, &Room::memberNameUpdated, this, [this](RoomMember member) {
if (m_room != nullptr && m_event != nullptr) {
if (m_eventSenderId.isEmpty() || m_eventSenderId == member.id()) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
}
}
});
connect(m_room, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
if (m_room != nullptr && m_event != nullptr) {
if (m_eventSenderId.isEmpty() || m_eventSenderId == member.id()) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
}
}
});
if (m_event != nullptr) {
updateReplyModel();
} }
resetModel();
}
void MessageContentModel::intiializeEvent(const QString &eventId) updateComponents();
{
const auto newEvent = m_room->getEvent(eventId);
if (newEvent != nullptr) {
intiializeEvent(newEvent);
}
}
void MessageContentModel::intiializeEvent(const Quotient::RoomEvent *event)
{
m_event = loadEvent<RoomEvent>(event->fullJson());
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()) {
senderId = m_room->localMember().id();
}
if (m_eventSenderObject == nullptr) {
m_eventSenderObject = std::unique_ptr<NeochatRoomMember>(new NeochatRoomMember(m_room, senderId));
}
Q_EMIT eventUpdated();
}
bool MessageContentModel::showAuthor() const
{
return m_showAuthor;
}
void MessageContentModel::setShowAuthor(bool showAuthor)
{
if (showAuthor == m_showAuthor) {
return;
}
m_showAuthor = showAuthor;
if (m_event != nullptr) {
if (showAuthor) {
beginInsertRows({}, 0, 0);
m_components.prepend(MessageComponent{MessageComponentType::Author, QString(), {}});
endInsertRows();
} else {
beginRemoveRows({}, 0, 0);
m_components.remove(0, 1);
endRemoveRows();
}
}
Q_EMIT showAuthorChanged();
} }
static LinkPreviewer *emptyLinkPreview = new LinkPreviewer; static LinkPreviewer *emptyLinkPreview = new LinkPreviewer;
@@ -215,16 +134,10 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return {}; return {};
} }
EventHandler eventHandler(m_room, m_event.get()); EventHandler eventHandler(m_room, m_event);
const auto component = m_components[index.row()]; const auto component = m_components[index.row()];
if (role == DisplayRole) { if (role == DisplayRole) {
if (component.type == MessageComponentType::Loading && m_isReply) {
return i18n("Loading reply");
}
if (m_event == nullptr) {
return QString();
}
if (m_event->isRedacted()) { if (m_event->isRedacted()) {
auto reason = m_event->redactedBecause()->reason(); auto reason = m_event->redactedBecause()->reason();
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>") return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>")
@@ -244,30 +157,14 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
if (role == EventIdRole) { if (role == EventIdRole) {
return eventHandler.getId(); return eventHandler.getId();
} }
if (role == TimeRole) {
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [this](const PendingEventItem &pendingEvent) {
return m_event->transactionId() == pendingEvent->transactionId();
});
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->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) {
return m_event->transactionId() == pendingEvent->transactionId();
});
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated();
return eventHandler.getTimeString(QStringLiteral("hh:mm"), m_isPending, lastUpdated);
}
if (role == AuthorRole) { if (role == AuthorRole) {
return QVariant::fromValue<NeochatRoomMember *>(m_eventSenderObject.get()); return eventHandler.getAuthor(false);
} }
if (role == MediaInfoRole) { if (role == MediaInfoRole) {
return eventHandler.getMediaInfo(); return eventHandler.getMediaInfo();
} }
if (role == FileTransferInfoRole) { if (role == FileTransferInfoRole) {
return QVariant::fromValue(m_room->cachedFileTransferInfo(m_event.get())); return QVariant::fromValue(fileInfo());
} }
if (role == ItineraryModelRole) { if (role == ItineraryModelRole) {
return QVariant::fromValue<ItineraryModel *>(m_itineraryModel); return QVariant::fromValue<ItineraryModel *>(m_itineraryModel);
@@ -282,19 +179,25 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return eventHandler.getLocationAssetType(); return eventHandler.getLocationAssetType();
} }
if (role == PollHandlerRole) { if (role == PollHandlerRole) {
return QVariant::fromValue<PollHandler *>(m_room->poll(m_eventId)); return QVariant::fromValue<PollHandler *>(m_room->poll(m_event->id()));
} }
if (role == IsReplyRole) { if (role == IsReplyRole) {
return eventHandler.hasReply(); return eventHandler.hasReply();
} }
if (role == ReplyComponentType) {
return eventHandler.replyMessageComponentType();
}
if (role == ReplyEventIdRole) { if (role == ReplyEventIdRole) {
return eventHandler.getReplyId(); return eventHandler.getReplyId();
} }
if (role == ReplyAuthorRole) { if (role == ReplyAuthorRole) {
return QVariant::fromValue(eventHandler.getReplyAuthor()); return eventHandler.getReplyAuthor();
} }
if (role == ReplyContentModelRole) { if (role == ReplyDisplayRole) {
return QVariant::fromValue<MessageContentModel *>(m_replyModel); return eventHandler.getReplyRichBody();
}
if (role == ReplyMediaInfoRole) {
return eventHandler.getReplyMediaInfo();
} }
if (role == LinkPreviewerRole) { if (role == LinkPreviewerRole) {
if (component.type == MessageComponentType::LinkPreview) { if (component.type == MessageComponentType::LinkPreview) {
@@ -321,8 +224,6 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
roles[ComponentTypeRole] = "componentType"; roles[ComponentTypeRole] = "componentType";
roles[ComponentAttributesRole] = "componentAttributes"; roles[ComponentAttributesRole] = "componentAttributes";
roles[EventIdRole] = "eventId"; roles[EventIdRole] = "eventId";
roles[TimeRole] = "time";
roles[TimeStringRole] = "timeString";
roles[AuthorRole] = "author"; roles[AuthorRole] = "author";
roles[MediaInfoRole] = "mediaInfo"; roles[MediaInfoRole] = "mediaInfo";
roles[FileTransferInfoRole] = "fileTransferInfo"; roles[FileTransferInfoRole] = "fileTransferInfo";
@@ -332,104 +233,54 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
roles[AssetRole] = "asset"; roles[AssetRole] = "asset";
roles[PollHandlerRole] = "pollHandler"; roles[PollHandlerRole] = "pollHandler";
roles[IsReplyRole] = "isReply"; roles[IsReplyRole] = "isReply";
roles[ReplyComponentType] = "replyComponentType";
roles[ReplyEventIdRole] = "replyEventId"; roles[ReplyEventIdRole] = "replyEventId";
roles[ReplyAuthorRole] = "replyAuthor"; roles[ReplyAuthorRole] = "replyAuthor";
roles[ReplyContentModelRole] = "replyContentModel"; roles[ReplyDisplayRole] = "replyDisplay";
roles[ReplyMediaInfoRole] = "replyMediaInfo";
roles[LinkPreviewerRole] = "linkPreviewer"; roles[LinkPreviewerRole] = "linkPreviewer";
return roles; return roles;
} }
void MessageContentModel::resetModel() void MessageContentModel::updateComponents(bool isEditing)
{ {
beginResetModel(); beginResetModel();
m_components.clear(); m_components.clear();
if (m_event == nullptr) { if (eventCast<const Quotient::RoomMessageEvent>(m_event)
m_components += MessageComponent{MessageComponentType::Loading, QString(), {}}; && eventCast<const Quotient::RoomMessageEvent>(m_event)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
m_components += MessageComponent{MessageComponentType::Verification, QString(), {}};
endResetModel(); endResetModel();
return; return;
} }
if (m_showAuthor) { if (m_event->isRedacted()) {
m_components += MessageComponent{MessageComponentType::Author, QString(), {}}; m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
} endResetModel();
m_components += messageContentComponents();
endResetModel();
}
void MessageContentModel::resetContent(bool isEditing)
{
Q_ASSERT(m_event != nullptr);
const auto startRow = m_components[0].type == MessageComponentType::Author ? 1 : 0;
beginRemoveRows({}, startRow, rowCount() - 1);
m_components.remove(startRow, rowCount() - startRow);
endRemoveRows();
const auto newComponents = messageContentComponents(isEditing);
if (newComponents.size() == 0) {
return; return;
} }
beginInsertRows({}, startRow, startRow + newComponents.size() - 1);
m_components += newComponents;
endInsertRows();
}
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing) EventHandler eventHandler(m_room, m_event);
{ if (eventHandler.hasReply()) {
QList<MessageComponent> newComponents; if (m_room->findInTimeline(eventHandler.getReplyId()) == m_room->historyEdge()) {
m_components += MessageComponent{MessageComponentType::ReplyLoad, QString(), {}};
if (eventCast<const Quotient::RoomMessageEvent>(m_event) m_room->loadReply(m_event->id(), eventHandler.getReplyId());
&& eventCast<const Quotient::RoomMessageEvent>(m_event)->rawMsgtype() == QStringLiteral("m.key.verification.request")) { } else {
newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}}; m_components += MessageComponent{MessageComponentType::Reply, QString(), {}};
return newComponents; }
}
if (m_event->isRedacted()) {
newComponents += MessageComponent{MessageComponentType::Text, QString(), {}};
return newComponents;
}
if (m_replyModel != nullptr) {
newComponents += MessageComponent{MessageComponentType::Reply, QString(), {}};
} }
if (isEditing) { if (isEditing) {
newComponents += MessageComponent{MessageComponentType::Edit, QString(), {}}; m_components += MessageComponent{MessageComponentType::Edit, QString(), {}};
} else { } else {
EventHandler eventHandler(m_room, m_event.get()); m_components.append(componentsForType(eventHandler.messageComponentType()));
newComponents.append(componentsForType(eventHandler.messageComponentType()));
} }
if (m_room->urlPreviewEnabled()) { if (m_room->urlPreviewEnabled()) {
newComponents = addLinkPreviews(newComponents); addLinkPreviews();
} }
return newComponents; endResetModel();
}
void MessageContentModel::updateReplyModel()
{
if (m_event == nullptr || m_replyModel != nullptr || m_isReply) {
return;
}
EventHandler eventHandler(m_room, m_event.get());
if (!eventHandler.hasReply()) {
return;
}
const auto replyEvent = m_room->findInTimeline(eventHandler.getReplyId());
if (replyEvent == m_room->historyEdge()) {
m_replyModel = new MessageContentModel(m_room, eventHandler.getReplyId(), true);
} else {
m_replyModel = new MessageContentModel(m_room, replyEvent->get(), true);
}
connect(m_replyModel, &MessageContentModel::eventUpdated, this, [this]() {
Q_EMIT dataChanged(index(0), index(0), {ReplyAuthorRole});
});
} }
QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentType::Type type) QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentType::Type type)
@@ -444,52 +295,36 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
QList<MessageComponent> components; QList<MessageComponent> components;
components += MessageComponent{MessageComponentType::File, QString(), {}}; components += MessageComponent{MessageComponentType::File, QString(), {}};
const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event); const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event);
auto body = EventHandler::rawMessageBody(*event);
components += TextHandler().textComponents(body, EventHandler::messageBodyInputFormat(*event), m_room, event, event->isReplaced());
if (m_emptyItinerary) { if (m_emptyItinerary) {
if (!m_isReply) { auto fileTransferInfo = fileInfo();
auto fileTransferInfo = m_room->cachedFileTransferInfo(m_event.get());
#ifndef Q_OS_ANDROID #ifndef Q_OS_ANDROID
Q_ASSERT(event->content() != nullptr && event->content()->fileInfo() != nullptr); KSyntaxHighlighting::Repository repository;
const QMimeType mimeType = event->content()->fileInfo()->mimeType; const auto definitionForFile = repository.definitionForFileName(fileTransferInfo.localPath.toString());
if (mimeType.name() == QStringLiteral("text/plain") || mimeType.parentMimeTypes().contains(QStringLiteral("text/plain"))) { if (definitionForFile.isValid() || QFileInfo(fileTransferInfo.localPath.path()).suffix() == QStringLiteral("txt")) {
QString originalName = event->content()->fileInfo()->originalName; QFile file(fileTransferInfo.localPath.path());
if (originalName.isEmpty()) { file.open(QIODevice::ReadOnly);
originalName = event->plainBody(); components += MessageComponent{MessageComponentType::Code,
} QString::fromStdString(file.readAll().toStdString()),
KSyntaxHighlighting::Repository repository; {{QStringLiteral("class"), definitionForFile.name()}}};
KSyntaxHighlighting::Definition definitionForFile = repository.definitionForFileName(originalName); }
if (!definitionForFile.isValid()) {
definitionForFile = repository.definitionForMimeType(mimeType.name());
}
QFile file(fileTransferInfo.localPath.path());
file.open(QIODevice::ReadOnly);
components += MessageComponent{MessageComponentType::Code,
QString::fromStdString(file.readAll().toStdString()),
{{QStringLiteral("class"), definitionForFile.name()}}};
}
#endif #endif
if (FileType::instance().fileHasImage(fileTransferInfo.localPath)) { if (FileType::instance().fileHasImage(fileTransferInfo.localPath)) {
QImageReader reader(fileTransferInfo.localPath.path()); QImageReader reader(fileTransferInfo.localPath.path());
components += MessageComponent{MessageComponentType::Pdf, QString(), {{QStringLiteral("size"), reader.size()}}}; components += MessageComponent{MessageComponentType::Pdf, QString(), {{QStringLiteral("size"), reader.size()}}};
}
}
} else if (m_itineraryModel != nullptr) {
components += MessageComponent{MessageComponentType::Itinerary, QString(), {}};
if (m_itineraryModel->rowCount() > 0) {
updateItineraryModel();
} }
} else { } else {
updateItineraryModel(); updateItineraryModel();
if (m_itineraryModel != nullptr) {
components += MessageComponent{MessageComponentType::Itinerary, QString(), {}};
}
} }
auto body = EventHandler::rawMessageBody(*event);
components += TextHandler().textComponents(body, EventHandler::messageBodyInputFormat(*event), m_room, event, event->isReplaced());
return components; return components;
} }
case MessageComponentType::Image: case MessageComponentType::Image:
case MessageComponentType::Audio:
case MessageComponentType::Video: { case MessageComponentType::Video: {
if (!m_event->is<StickerEvent>()) { if (!m_event->is<StickerEvent>()) {
const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event); const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event);
@@ -531,26 +366,24 @@ MessageComponent MessageContentModel::linkPreviewComponent(const QUrl &link)
} }
} }
QList<MessageComponent> MessageContentModel::addLinkPreviews(QList<MessageComponent> inputComponents) void MessageContentModel::addLinkPreviews()
{ {
int i = 0; int i = 0;
while (i < inputComponents.size()) { while (i < m_components.size()) {
const auto component = inputComponents.at(i); const auto component = m_components.at(i);
if (component.type == MessageComponentType::Text || component.type == MessageComponentType::Quote) { if (component.type == MessageComponentType::Text || component.type == MessageComponentType::Quote) {
if (LinkPreviewer::hasPreviewableLinks(component.content)) { if (LinkPreviewer::hasPreviewableLinks(component.content)) {
const auto links = LinkPreviewer::linkPreviews(component.content); const auto links = LinkPreviewer::linkPreviews(component.content);
for (qsizetype j = 0; j < links.size(); ++j) { for (qsizetype j = 0; j < links.size(); ++j) {
const auto linkPreview = linkPreviewComponent(links[j]); const auto linkPreview = linkPreviewComponent(links[j]);
if (!m_removedLinkPreviews.contains(links[j]) && !linkPreview.isEmpty()) { if (!m_removedLinkPreviews.contains(links[j]) && !linkPreview.isEmpty()) {
inputComponents.insert(i + j + 1, linkPreview); m_components.insert(i + j + 1, linkPreview);
} }
}; };
} }
} }
i++; i++;
} }
return inputComponents;
} }
void MessageContentModel::closeLinkPreview(int row) void MessageContentModel::closeLinkPreview(int row)
@@ -560,7 +393,8 @@ void MessageContentModel::closeLinkPreview(int row)
m_removedLinkPreviews += m_components[row].attributes["link"_ls].toUrl(); m_removedLinkPreviews += m_components[row].attributes["link"_ls].toUrl();
m_components.remove(row); m_components.remove(row);
m_components.squeeze(); m_components.squeeze();
resetContent(); updateComponents();
endResetModel();
} }
} }
@@ -572,7 +406,7 @@ void MessageContentModel::updateItineraryModel()
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) { if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (event->hasFileContent()) { if (event->hasFileContent()) {
auto filePath = m_room->cachedFileTransferInfo(m_event.get()).localPath; auto filePath = fileInfo().localPath;
if (filePath.isEmpty() && m_itineraryModel != nullptr) { if (filePath.isEmpty() && m_itineraryModel != nullptr) {
delete m_itineraryModel; delete m_itineraryModel;
m_itineraryModel = nullptr; m_itineraryModel = nullptr;
@@ -581,17 +415,17 @@ void MessageContentModel::updateItineraryModel()
m_itineraryModel = new ItineraryModel(this); m_itineraryModel = new ItineraryModel(this);
connect(m_itineraryModel, &ItineraryModel::loaded, this, [this]() { connect(m_itineraryModel, &ItineraryModel::loaded, this, [this]() {
if (m_itineraryModel->rowCount() == 0) { if (m_itineraryModel->rowCount() == 0) {
m_emptyItinerary = true;
m_itineraryModel->deleteLater(); m_itineraryModel->deleteLater();
m_itineraryModel = nullptr; m_itineraryModel = nullptr;
resetContent(); m_emptyItinerary = true;
updateComponents();
} }
}); });
connect(m_itineraryModel, &ItineraryModel::loadErrorOccurred, this, [this]() { connect(m_itineraryModel, &ItineraryModel::loadErrorOccurred, this, [this]() {
m_emptyItinerary = true;
m_itineraryModel->deleteLater(); m_itineraryModel->deleteLater();
m_itineraryModel = nullptr; m_itineraryModel = nullptr;
resetContent(); m_emptyItinerary = true;
updateComponents();
}); });
} }
m_itineraryModel->setPath(filePath.toString()); m_itineraryModel->setPath(filePath.toString());
@@ -600,4 +434,42 @@ void MessageContentModel::updateItineraryModel()
} }
} }
FileTransferInfo MessageContentModel::fileInfo() const
{
if (m_room == nullptr || m_event == nullptr) {
return {};
}
QString mxcUrl;
int total;
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (event->hasFileContent()) {
mxcUrl = event->content()->fileInfo()->url().toString();
total = event->content()->fileInfo()->payloadSize;
}
} else if (auto event = eventCast<const Quotient::StickerEvent>(m_event)) {
mxcUrl = event->image().fileInfo()->url().toString();
total = event->image().fileInfo()->payloadSize;
}
auto config = KSharedConfig::openStateConfig(QStringLiteral("neochatdownloads"))->group(QStringLiteral("downloads"));
if (!config.hasKey(mxcUrl.mid(6))) {
return m_room->fileTransferInfo(m_event->id());
}
const auto path = config.readPathEntry(mxcUrl.mid(6), QString());
QFileInfo info(path);
if (!info.isFile()) {
config.deleteEntry(mxcUrl);
return m_room->fileTransferInfo(m_event->id());
}
// TODO: we could check the hash here
return FileTransferInfo{
.status = FileTransferInfo::Completed,
.isUpload = false,
.progress = total,
.total = total,
.localDir = QUrl(info.dir().path()),
.localPath = QUrl::fromLocalFile(path),
};
}
#include "moc_messagecontentmodel.cpp" #include "moc_messagecontentmodel.cpp"

View File

@@ -6,13 +6,11 @@
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QQmlEngine> #include <QQmlEngine>
#include <Quotient/events/roomevent.h>
#include <Quotient/room.h> #include <Quotient/room.h>
#include "enums/messagecomponenttype.h" #include "enums/messagecomponenttype.h"
#include "eventhandler.h" #include "eventhandler.h"
#include "itinerarymodel.h" #include "itinerarymodel.h"
#include "neochatroommember.h"
struct MessageComponent { struct MessageComponent {
MessageComponentType::Type type = MessageComponentType::Other; MessageComponentType::Type type = MessageComponentType::Other;
@@ -41,11 +39,6 @@ class MessageContentModel : public QAbstractListModel
QML_ELEMENT QML_ELEMENT
QML_UNCREATABLE("") QML_UNCREATABLE("")
/**
* @brief Whether the author component is being shown.
*/
Q_PROPERTY(bool showAuthor READ showAuthor WRITE setShowAuthor NOTIFY showAuthorChanged)
public: public:
/** /**
* @brief Defines the model roles. * @brief Defines the model roles.
@@ -55,8 +48,6 @@ public:
ComponentTypeRole, /**< The type of component to visualise the message. */ ComponentTypeRole, /**< The type of component to visualise the message. */
ComponentAttributesRole, /**< The attributes of the component. */ ComponentAttributesRole, /**< The attributes of the component. */
EventIdRole, /**< The matrix event ID of the event. */ EventIdRole, /**< The matrix event ID of the event. */
TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */
TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */
AuthorRole, /**< The author of the event. */ AuthorRole, /**< The author of the event. */
MediaInfoRole, /**< The media info for the event. */ MediaInfoRole, /**< The media info for the event. */
FileTransferInfoRole, /**< FileTransferInfo for any downloading files. */ FileTransferInfoRole, /**< FileTransferInfo for any downloading files. */
@@ -67,19 +58,17 @@ public:
PollHandlerRole, /**< The PollHandler for the event, if any. */ PollHandlerRole, /**< The PollHandler for the event, if any. */
IsReplyRole, /**< Is the message a reply to another event. */ IsReplyRole, /**< Is the message a reply to another event. */
ReplyComponentType, /**< The type of component to visualise the reply message. */
ReplyEventIdRole, /**< The matrix ID of the message that was replied to. */ ReplyEventIdRole, /**< The matrix ID of the message that was replied to. */
ReplyAuthorRole, /**< The author of the event that was replied to. */ ReplyAuthorRole, /**< The author of the event that was replied to. */
ReplyContentModelRole, /**< The MessageContentModel for the reply event. */ ReplyDisplayRole, /**< The body of the message that was replied to. */
ReplyMediaInfoRole, /**< The media info of the message that was replied to. */
LinkPreviewerRole, /**< The link preview details. */ LinkPreviewerRole, /**< The link preview details. */
}; };
Q_ENUM(Roles) Q_ENUM(Roles)
explicit MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply = false, bool isPending = false); explicit MessageContentModel(const Quotient::RoomEvent *event, NeoChatRoom *room);
MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply = false, bool isPending = false);
bool showAuthor() const;
void setShowAuthor(bool showAuthor);
/** /**
* @brief Get the given role value at the given index. * @brief Get the given role value at the given index.
@@ -109,41 +98,23 @@ public:
*/ */
Q_INVOKABLE void closeLinkPreview(int row); Q_INVOKABLE void closeLinkPreview(int row);
Q_SIGNALS:
void showAuthorChanged();
void eventUpdated();
private: private:
QPointer<NeoChatRoom> m_room; QPointer<NeoChatRoom> m_room;
QString m_eventId; const Quotient::RoomEvent *m_event = nullptr;
QString m_eventSenderId;
std::unique_ptr<NeochatRoomMember> m_eventSenderObject = nullptr;
Quotient::RoomEventPtr m_event;
bool m_isPending;
bool m_showAuthor = true;
bool m_isReply;
void initializeModel();
void intiializeEvent(const QString &eventId);
void intiializeEvent(const Quotient::RoomEvent *event);
QList<MessageComponent> m_components; QList<MessageComponent> m_components;
void resetModel(); void updateComponents(bool isEditing = false);
void resetContent(bool isEditing = false);
QList<MessageComponent> messageContentComponents(bool isEditing = false);
QPointer<MessageContentModel> m_replyModel;
void updateReplyModel();
ItineraryModel *m_itineraryModel = nullptr; ItineraryModel *m_itineraryModel = nullptr;
QList<MessageComponent> componentsForType(MessageComponentType::Type type); QList<MessageComponent> componentsForType(MessageComponentType::Type type);
MessageComponent linkPreviewComponent(const QUrl &link); MessageComponent linkPreviewComponent(const QUrl &link);
QList<MessageComponent> addLinkPreviews(QList<MessageComponent> inputComponents); void addLinkPreviews();
QList<QUrl> m_removedLinkPreviews; QList<QUrl> m_removedLinkPreviews;
void updateItineraryModel(); void updateItineraryModel();
bool m_emptyItinerary = false; bool m_emptyItinerary = false;
Quotient::FileTransferInfo fileInfo() const;
}; };

View File

@@ -11,7 +11,7 @@
#include <Quotient/events/redactionevent.h> #include <Quotient/events/redactionevent.h>
#include <Quotient/events/roommessageevent.h> #include <Quotient/events/roommessageevent.h>
#include <Quotient/events/stickerevent.h> #include <Quotient/events/stickerevent.h>
#include <Quotient/roommember.h> #include <Quotient/user.h>
#include <QDebug> #include <QDebug>
#include <QGuiApplication> #include <QGuiApplication>
@@ -26,9 +26,6 @@
#include "messagecontentmodel.h" #include "messagecontentmodel.h"
#include "models/messagefiltermodel.h" #include "models/messagefiltermodel.h"
#include "models/reactionmodel.h" #include "models/reactionmodel.h"
#include "neochatroom.h"
#include "neochatroommember.h"
#include "readmarkermodel.h"
#include "texthandler.h" #include "texthandler.h"
using namespace Quotient; using namespace Quotient;
@@ -39,6 +36,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[DelegateTypeRole] = "delegateType"; roles[DelegateTypeRole] = "delegateType";
roles[EventIdRole] = "eventId"; roles[EventIdRole] = "eventId";
roles[TimeRole] = "time"; roles[TimeRole] = "time";
roles[TimeStringRole] = "timeString";
roles[SectionRole] = "section"; roles[SectionRole] = "section";
roles[AuthorRole] = "author"; roles[AuthorRole] = "author";
roles[HighlightRole] = "isHighlighted"; roles[HighlightRole] = "isHighlighted";
@@ -48,6 +46,8 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[ThreadRootRole] = "threadRoot"; roles[ThreadRootRole] = "threadRoot";
roles[ShowSectionRole] = "showSection"; roles[ShowSectionRole] = "showSection";
roles[ReadMarkersRole] = "readMarkers"; roles[ReadMarkersRole] = "readMarkers";
roles[ExcessReadMarkersRole] = "excessReadMarkers";
roles[ReadMarkersStringRole] = "readMarkersString";
roles[ShowReadMarkersRole] = "showReadMarkers"; roles[ShowReadMarkersRole] = "showReadMarkers";
roles[ReactionRole] = "reaction"; roles[ReactionRole] = "reaction";
roles[ShowReactionsRole] = "showReactions"; roles[ShowReactionsRole] = "showReactions";
@@ -84,21 +84,12 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
return; return;
} }
beginResetModel();
if (m_currentRoom) { if (m_currentRoom) {
// 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->disconnect(this);
m_currentRoom = nullptr; m_reactionModels.clear();
endResetModel();
// Don't clear the member objects until the model has been fully reset and all
// refs cleared.
m_memberObjects.clear();
} }
beginResetModel();
m_currentRoom = room; m_currentRoom = room;
Q_EMIT roomChanged(); Q_EMIT roomChanged();
if (room) { if (room) {
@@ -106,7 +97,9 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
room->setDisplayed(); room->setDisplayed();
for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) { for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) {
createEventObjects(&*event->viewAs<RoomEvent>()); if (const auto &roomMessageEvent = &*event->viewAs<RoomMessageEvent>()) {
createEventObjects(roomMessageEvent);
}
if (event->event()->is<PollStartEvent>()) { if (event->event()->is<PollStartEvent>()) {
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event->event())); m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event->event()));
} }
@@ -119,7 +112,11 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) { connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
for (auto &&event : events) { for (auto &&event : events) {
createEventObjects(event.get()); const RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
if (message != nullptr) {
createEventObjects(message);
}
if (event->is<PollStartEvent>()) { if (event->is<PollStartEvent>()) {
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event.get())); m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event.get()));
} }
@@ -129,7 +126,9 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
}); });
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) { connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
for (auto &event : events) { for (auto &event : events) {
createEventObjects(event.get()); if (const auto &roomMessageEvent = dynamic_cast<RoomMessageEvent *>(event.get())) {
createEventObjects(roomMessageEvent);
}
if (event->is<PollStartEvent>()) { if (event->is<PollStartEvent>()) {
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event.get())); m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event.get()));
} }
@@ -150,15 +149,14 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
} }
if (biggest < m_currentRoom->maxTimelineIndex()) { if (biggest < m_currentRoom->maxTimelineIndex()) {
auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - biggest + timelineBaseIndex() - 1; auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - biggest + timelineBaseIndex() - 1;
refreshEventRoles(rowBelowInserted, {ContentModelRole}); refreshEventRoles(rowBelowInserted, {MessageFilterModel::ShowAuthorRole});
} }
for (auto i = m_currentRoom->maxTimelineIndex() - biggest; i <= m_currentRoom->maxTimelineIndex() - lowest; ++i) { for (auto i = m_currentRoom->maxTimelineIndex() - biggest; i <= m_currentRoom->maxTimelineIndex() - lowest; ++i) {
refreshLastUserEvents(i); refreshLastUserEvents(i);
} }
}); });
connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, [this](Quotient::RoomEvent *event) { connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, [this] {
m_initialized = true; m_initialized = true;
createEventObjects(event);
beginInsertRows({}, 0, 0); beginInsertRows({}, 0, 0);
}); });
connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows); connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows);
@@ -178,13 +176,13 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
endMoveRows(); endMoveRows();
movingEvent = false; movingEvent = false;
} }
fullEventRefresh(timelineBaseIndex()); refreshRow(timelineBaseIndex()); // Refresh the looks
refreshLastUserEvents(0); refreshLastUserEvents(0);
if (timelineBaseIndex() > 0) { // Refresh below, see #312 if (timelineBaseIndex() > 0) { // Refresh below, see #312
refreshEventRoles(timelineBaseIndex() - 1, {ContentModelRole}); refreshEventRoles(timelineBaseIndex() - 1, {MessageFilterModel::ShowAuthorRole});
} }
}); });
connect(m_currentRoom, &Room::pendingEventChanged, this, &MessageEventModel::fullEventRefresh); connect(m_currentRoom, &Room::pendingEventChanged, this, &MessageEventModel::refreshRow);
connect(m_currentRoom, &Room::pendingEventAboutToDiscard, this, [this](int i) { connect(m_currentRoom, &Room::pendingEventAboutToDiscard, this, [this](int i) {
beginRemoveRows({}, i, i); beginRemoveRows({}, i, i);
}); });
@@ -194,7 +192,10 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
moveReadMarker(toEventId); moveReadMarker(toEventId);
}); });
connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) { connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) {
createEventObjects(newEvent); const RoomMessageEvent *message = eventCast<const RoomMessageEvent>(newEvent);
if (message != nullptr) {
createEventObjects(message);
}
}); });
connect(m_currentRoom, &Room::updatedEvent, this, [this](const QString &eventId) { connect(m_currentRoom, &Room::updatedEvent, this, [this](const QString &eventId) {
if (eventId.isEmpty()) { // How did we get here? if (eventId.isEmpty()) { // How did we get here?
@@ -202,27 +203,26 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
} }
const auto eventIt = m_currentRoom->findInTimeline(eventId); const auto eventIt = m_currentRoom->findInTimeline(eventId);
if (eventIt != m_currentRoom->historyEdge()) { if (eventIt != m_currentRoom->historyEdge()) {
createEventObjects(eventIt->event()); if (const auto &event = dynamic_cast<const RoomMessageEvent *>(&**eventIt)) {
createEventObjects(event);
}
if (eventIt->event()->is<PollStartEvent>()) { if (eventIt->event()->is<PollStartEvent>()) {
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(eventIt->event())); m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(eventIt->event()));
} }
} }
refreshEventRoles(eventId, {Qt::DisplayRole}); refreshEventRoles(eventId, {Qt::DisplayRole});
}); });
connect(m_currentRoom, &Room::changed, this, [this](Room::Changes changes) { connect(m_currentRoom, &Room::changed, this, [this]() {
if (changes.testFlag(Quotient::Room::Change::Other)) { for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
// this is slow auto event = it->event();
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) { refreshEventRoles(event->id(), {ReadMarkersRole, ReadMarkersStringRole, ExcessReadMarkersRole});
createEventObjects(it->event());
}
} }
}); });
connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [this] { connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [this] {
beginResetModel(); beginResetModel();
endResetModel(); endResetModel();
}); });
qCDebug(MessageEvent) << "Connected to room" << room->id() << "as" << room->localUser()->id();
qCDebug(MessageEvent) << "Connected to room" << room->id() << "as" << room->localMember().id();
} else { } else {
lastReadEventId.clear(); lastReadEventId.clear();
} }
@@ -235,15 +235,14 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
} }
} }
void MessageEventModel::fullEventRefresh(int row) int MessageEventModel::refreshEvent(const QString &eventId)
{ {
auto roles = roleNames().keys(); return refreshEventRoles(eventId);
// The author of an event never changes so should only be updated when a member }
// changed signal is emitted.
// This also avoids any race conditions where a member is updating and this refresh void MessageEventModel::refreshRow(int row)
// tries to access a member event that has already been deleted. {
roles.removeAll(AuthorRole); refreshEventRoles(row);
refreshEventRoles(row, roles);
} }
int MessageEventModel::timelineBaseIndex() const int MessageEventModel::timelineBaseIndex() const
@@ -336,11 +335,11 @@ QDateTime MessageEventModel::makeMessageTimestamp(const Quotient::Room::rev_iter
using Quotient::TimelineItem; using Quotient::TimelineItem;
auto rit = std::find_if(baseIt, timeline.rend(), hasValidTimestamp); auto rit = std::find_if(baseIt, timeline.rend(), hasValidTimestamp);
if (rit != timeline.rend()) { if (rit != timeline.rend()) {
return {rit->event()->originTimestamp().date(), {0, 0}, QTimeZone::LocalTime}; return {rit->event()->originTimestamp().date(), {0, 0}, Qt::LocalTime};
}; };
auto it = std::find_if(baseIt.base(), timeline.end(), hasValidTimestamp); auto it = std::find_if(baseIt.base(), timeline.end(), hasValidTimestamp);
if (it != timeline.end()) { if (it != timeline.end()) {
return {it->event()->originTimestamp().date(), {0, 0}, QTimeZone::LocalTime}; return {it->event()->originTimestamp().date(), {0, 0}, Qt::LocalTime};
}; };
// What kind of room is that?.. // What kind of room is that?..
@@ -359,7 +358,8 @@ void MessageEventModel::refreshLastUserEvents(int baseTimelineRow)
const auto limit = timelineBottom + std::min(baseTimelineRow + 10, m_currentRoom->timelineSize()); const auto limit = timelineBottom + std::min(baseTimelineRow + 10, m_currentRoom->timelineSize());
for (auto it = timelineBottom + std::max(baseTimelineRow - 10, 0); it != limit; ++it) { for (auto it = timelineBottom + std::max(baseTimelineRow - 10, 0); it != limit; ++it) {
if ((*it)->senderId() == lastSender) { if ((*it)->senderId() == lastSender) {
fullEventRefresh(it - timelineBottom); auto idx = index(it - timelineBottom);
Q_EMIT dataChanged(idx, idx);
} }
} }
} }
@@ -388,8 +388,6 @@ void MessageEventModel::fetchMore(const QModelIndex &parent)
} }
} }
static NeochatRoomMember *emptyNeochatRoomMember = new NeochatRoomMember;
QVariant MessageEventModel::data(const QModelIndex &idx, int role) const QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
{ {
if (!checkIndex(idx, QAbstractItemModel::CheckIndexOption::IndexIsValid)) { if (!checkIndex(idx, QAbstractItemModel::CheckIndexOption::IndexIsValid)) {
@@ -442,12 +440,12 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
} }
if (role == ContentModelRole) { if (role == ContentModelRole) {
if (!evt.isStateEvent() && !evt.id().isEmpty()) { if (!evt.isStateEvent()) {
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_currentRoom, &evt)); return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(&evt, m_currentRoom));
} }
if (evt.isStateEvent()) { if (evt.isStateEvent()) {
if (evt.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) { if (evt.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_currentRoom, &evt)); return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(&evt, m_currentRoom));
} }
} }
return {}; return {};
@@ -462,18 +460,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
} }
if (role == AuthorRole) { if (role == AuthorRole) {
QString mId; return eventHandler.getAuthor(isPending);
if (isPending) {
mId = m_currentRoom->localMember().id();
} else {
mId = evt.senderId();
}
if (!m_memberObjects.contains(mId)) {
return QVariant::fromValue<NeochatRoomMember *>(emptyNeochatRoomMember);
}
return QVariant::fromValue<NeochatRoomMember *>(m_memberObjects.at(mId).get());
} }
if (role == HighlightRole) { if (role == HighlightRole) {
@@ -504,11 +491,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
if (role == ProgressInfoRole) { if (role == ProgressInfoRole) {
if (auto e = eventCast<const RoomMessageEvent>(&evt)) { if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if (e->hasFileContent()) { if (e->hasFileContent()) {
return QVariant::fromValue(m_currentRoom->cachedFileTransferInfo(&evt)); return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id()));
} }
} }
if (eventCast<const StickerEvent>(&evt)) { if (auto e = eventCast<const StickerEvent>(&evt)) {
return QVariant::fromValue(m_currentRoom->cachedFileTransferInfo(&evt)); return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id()));
} }
} }
@@ -517,6 +504,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return eventHandler.getTime(isPending, lastUpdated); return eventHandler.getTime(isPending, lastUpdated);
} }
if (role == TimeStringRole) {
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
return eventHandler.getTimeString(false, QLocale::ShortFormat, isPending, lastUpdated);
}
if (role == SectionRole) { if (role == SectionRole) {
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime(); auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
return eventHandler.getTimeString(true, QLocale::ShortFormat, isPending, lastUpdated); return eventHandler.getTimeString(true, QLocale::ShortFormat, isPending, lastUpdated);
@@ -547,15 +539,19 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
} }
if (role == ReadMarkersRole) { if (role == ReadMarkersRole) {
if (m_readMarkerModels.contains(evt.id())) { return eventHandler.getReadMarkers();
return QVariant::fromValue<ReadMarkerModel *>(m_readMarkerModels[evt.id()].get()); }
} else {
return QVariantList(); if (role == ExcessReadMarkersRole) {
} return eventHandler.getNumberExcessReadMarkers();
}
if (role == ReadMarkersStringRole) {
return eventHandler.getReadMarkersString();
} }
if (role == ShowReadMarkersRole) { if (role == ShowReadMarkersRole) {
return m_readMarkerModels.contains(evt.id()); return eventHandler.hasReadMarkers();
} }
if (role == ReactionRole) { if (role == ReactionRole) {
@@ -596,7 +592,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
} }
if (role == IsEditableRole) { if (role == IsEditableRole) {
return eventHandler.messageComponentType() == MessageComponentType::Text && evt.senderId() == m_currentRoom->localMember().id(); return eventHandler.messageComponentType() == MessageComponentType::Text && evt.senderId() == m_currentRoom->localUser()->id();
} }
return {}; return {};
@@ -616,71 +612,30 @@ int MessageEventModel::eventIdToRow(const QString &eventID) const
return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex(); return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex();
} }
void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event) void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *event)
{ {
if (event == nullptr) {
return;
}
auto eventId = event->id(); auto eventId = event->id();
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()) {
senderId = m_currentRoom->localMember().id();
}
if (!m_memberObjects.contains(senderId)) { // ReactionModel handles updates to add and remove reactions, we only need to
m_memberObjects[senderId] = std::unique_ptr<NeochatRoomMember>(new NeochatRoomMember(m_currentRoom, senderId));
}
// ReadMarkerModel handles updates to add and remove markers, we only need to
// handle adding and removing whole models here. // handle adding and removing whole models here.
if (m_readMarkerModels.contains(eventId)) { if (m_reactionModels.contains(eventId)) {
// If a model already exists but now has no reactions remove it // If a model already exists but now has no reactions remove it
if (m_readMarkerModels[eventId]->rowCount() <= 0) { if (m_reactionModels[eventId]->rowCount() <= 0) {
m_readMarkerModels.remove(eventId); m_reactionModels.remove(eventId);
if (!resetting) { if (!resetting) {
refreshEventRoles(eventId, {ReadMarkersRole, ShowReadMarkersRole}); refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole});
} }
} }
} else { } else {
auto memberIds = m_currentRoom->userIdsAtEvent(eventId); if (m_currentRoom->relatedEvents(*event, Quotient::EventRelation::AnnotationType).count() > 0) {
memberIds.remove(m_currentRoom->localMember().id());
if (memberIds.size() > 0) {
// If a model doesn't exist and there are reactions add it. // If a model doesn't exist and there are reactions add it.
auto newModel = QSharedPointer<ReadMarkerModel>(new ReadMarkerModel(eventId, m_currentRoom)); auto reactionModel = QSharedPointer<ReactionModel>(new ReactionModel(event, m_currentRoom));
if (newModel->rowCount() > 0) { if (reactionModel->rowCount() > 0) {
m_readMarkerModels[eventId] = newModel; m_reactionModels[eventId] = reactionModel;
if (!resetting) {
refreshEventRoles(eventId, {ReadMarkersRole, ShowReadMarkersRole});
}
}
}
}
if (const auto roomEvent = eventCast<const RoomMessageEvent>(event)) {
// ReactionModel handles updates to add and remove reactions, we only need to
// handle adding and removing whole models here.
if (m_reactionModels.contains(eventId)) {
// If a model already exists but now has no reactions remove it
if (m_reactionModels[eventId]->rowCount() <= 0) {
m_reactionModels.remove(eventId);
if (!resetting) { if (!resetting) {
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole}); refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole});
} }
} }
} else {
if (m_currentRoom->relatedEvents(*event, Quotient::EventRelation::AnnotationType).count() > 0) {
// If a model doesn't exist and there are reactions add it.
auto reactionModel = QSharedPointer<ReactionModel>(new ReactionModel(roomEvent, m_currentRoom));
if (reactionModel->rowCount() > 0) {
m_reactionModels[eventId] = reactionModel;
if (!resetting) {
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole});
}
}
}
} }
} }
} }

View File

@@ -9,9 +9,7 @@
#include "linkpreviewer.h" #include "linkpreviewer.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "neochatroommember.h"
#include "pollhandler.h" #include "pollhandler.h"
#include "readmarkermodel.h"
class ReactionModel; class ReactionModel;
@@ -44,6 +42,7 @@ public:
DelegateTypeRole = Qt::UserRole + 1, /**< The delegate type of the message. */ DelegateTypeRole = Qt::UserRole + 1, /**< The delegate type of the message. */
EventIdRole, /**< The matrix event ID of the event. */ EventIdRole, /**< The matrix event ID of the event. */
TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */ TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */
TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */
SectionRole, /**< The date of the event as a string. */ SectionRole, /**< The date of the event as a string. */
AuthorRole, /**< The author of the event. */ AuthorRole, /**< The author of the event. */
HighlightRole, /**< Whether the event should be highlighted. */ HighlightRole, /**< Whether the event should be highlighted. */
@@ -60,6 +59,8 @@ public:
ShowSectionRole, /**< Whether the section header should be shown. */ ShowSectionRole, /**< Whether the section header should be shown. */
ReadMarkersRole, /**< The first 5 other users at the event for read marker tracking. */ ReadMarkersRole, /**< The first 5 other users at the event for read marker tracking. */
ExcessReadMarkersRole, /**< The number of other users at the event after the first 5. */
ReadMarkersStringRole, /**< String with the display name and mxID of the users at the event. */
ShowReadMarkersRole, /**< Whether there are any other user read markers to be shown. */ ShowReadMarkersRole, /**< Whether there are any other user read markers to be shown. */
ReactionRole, /**< List model for this event. */ ReactionRole, /**< List model for this event. */
ShowReactionsRole, /**< Whether there are any reactions to be shown. */ ShowReactionsRole, /**< Whether there are any reactions to be shown. */
@@ -107,6 +108,10 @@ public:
protected: protected:
bool event(QEvent *event) override; bool event(QEvent *event) override;
private Q_SLOTS:
int refreshEvent(const QString &eventId);
void refreshRow(int row);
private: private:
QPointer<NeoChatRoom> m_currentRoom = nullptr; QPointer<NeoChatRoom> m_currentRoom = nullptr;
QString lastReadEventId; QString lastReadEventId;
@@ -116,8 +121,6 @@ private:
bool movingEvent = false; bool movingEvent = false;
KFormat m_format; KFormat m_format;
std::map<QString, std::unique_ptr<NeochatRoomMember>> m_memberObjects;
QMap<QString, QSharedPointer<ReadMarkerModel>> m_readMarkerModels;
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels; QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
[[nodiscard]] int timelineBaseIndex() const; [[nodiscard]] int timelineBaseIndex() const;
@@ -126,13 +129,12 @@ private:
bool canFetchMore(const QModelIndex &parent) const override; bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override; void fetchMore(const QModelIndex &parent) override;
void fullEventRefresh(int row);
void refreshLastUserEvents(int baseTimelineRow); void refreshLastUserEvents(int baseTimelineRow);
void refreshEventRoles(int row, const QList<int> &roles = {}); void refreshEventRoles(int row, const QList<int> &roles = {});
int refreshEventRoles(const QString &eventId, const QList<int> &roles = {}); int refreshEventRoles(const QString &eventId, const QList<int> &roles = {});
void moveReadMarker(const QString &toEventId); void moveReadMarker(const QString &toEventId);
void createEventObjects(const Quotient::RoomEvent *event); void createEventObjects(const Quotient::RoomMessageEvent *event);
// Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows // Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows
bool m_initialized = false; bool m_initialized = false;

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