Compare commits

...

16 Commits

Author SHA1 Message Date
Tobias Fella
3ede6b4526 More untested stuff :) 2024-06-26 22:56:33 +02:00
Tobias Fella
eda2881d6e Work 2024-06-21 21:57:26 +02:00
Tobias Fella
7ae553b2c3 Bunch o' work 2024-06-20 23:27:54 +02:00
Tobias Fella
94e650f7ad Create Cross-signing keys when required
This should be almost entirely in libquotient, but that's not prepared to actually use user-interactive authentication...
2024-06-20 21:48:22 +02:00
Tobias Fella
9a4a70178e Add basic cross-signing support 2024-06-08 15:20:08 +02:00
James Graham
33f505c06c Don't store RoomMembers in ReactionModel
Don't store RoomMember objects in the reaction model just grab them everytime incase the state event is replaced.
2024-06-08 15:20:08 +02:00
James Graham
47a5952c2a Use new libquotient functionality to encrypt direct chats by default
Needs https://github.com/quotient-im/libQuotient/pull/730
2024-06-08 15:20:08 +02:00
Tobias Fella
6babc1a479 Disable failing CIs 2024-06-08 15:20:08 +02:00
James Graham
58e4ccb680 Use the new libquotient version of the serveracl event
Use with https://github.com/quotient-im/libQuotient/pull/729
2024-06-08 15:20:08 +02:00
James Graham
607b6bcef0 Use updated membersTyping functions from libquotient 2024-06-08 15:20:08 +02:00
James Graham
bae9f39719 Request direct chat by userId to avoid breakage with upcoming libquotient changes 2024-06-08 15:20:07 +02:00
Tobias Fella
fa3fdca155 Unconditionally use SSSS 2024-06-08 15:20:07 +02:00
James Graham
8085d19eee Actually user.h needs to still be included in actionsmodel 2024-06-08 15:20:07 +02:00
James Graham
272f49876e Make use of new RoomMember item from libquotient
Depends on https://github.com/quotient-im/libQuotient/pull/695

Currently basic just to show a working implementation using RoomMember. Currently only the room event and search models are moved over. Will change everything else over once the dependent pr is complete.
2024-06-08 15:20:07 +02:00
James Graham
ece56a55e8 handle remaining stuff for Quotient::Omittable deprecation 2024-06-08 15:20:07 +02:00
James Graham
aec35222f9 Remove uses of Quotient:Omittable
Note this technically won't build for now because of the lack of RoomMember support but I'll push that at the quotient-next branch next. 

This is needed as well to get a branch that builds on dev.
2024-06-08 15:19:59 +02:00
77 changed files with 711 additions and 733 deletions

View File

@@ -5,11 +5,11 @@ include:
- project: sysadmin/ci-utilities - project: sysadmin/ci-utilities
file: file:
- /gitlab-templates/reuse-lint.yml - /gitlab-templates/reuse-lint.yml
- /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
- /gitlab-templates/craft-windows-x86-64-qt6.yml # - /gitlab-templates/craft-windows-x86-64-qt6.yml

View File

@@ -6,6 +6,7 @@
#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>
@@ -50,7 +51,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->getUser(nullptr)); QCOMPARE(chatBarCache->relationUser(), room->member(QString()));
QCOMPARE(chatBarCache->relationMessage(), QString()); QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), QString()); QCOMPARE(chatBarCache->attachmentPath(), QString());
} }
@@ -64,7 +65,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(), QVariantMap()); QCOMPARE(chatBarCache->relationUser(), Quotient::RoomMember());
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());
@@ -80,7 +81,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(), QVariantMap()); QCOMPARE(chatBarCache->relationUser(), Quotient::RoomMember());
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());
@@ -98,7 +99,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->getUser(room->user(QLatin1String("@example:example.org")))); QCOMPARE(chatBarCache->relationUser(), room->member(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());
} }
@@ -115,7 +116,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->getUser(room->user(QLatin1String("@example:example.org")))); QCOMPARE(chatBarCache->relationUser(), room->member(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());
} }
@@ -132,7 +133,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->getUser(nullptr)); QCOMPARE(chatBarCache->relationUser(), room->member(QString()));
QCOMPARE(chatBarCache->relationMessage(), QString()); QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), QLatin1String("some/path")); QCOMPARE(chatBarCache->attachmentPath(), QLatin1String("some/path"));
} }

View File

@@ -35,7 +35,7 @@
"content": { "content": {
"$153456789:example.org": { "$153456789:example.org": {
"m.read": { "m.read": {
"@alice:matrix.org": { "@alice:example.org": {
"ts": 1436451550453 "ts": 1436451550453
} }
} }

View File

@@ -37,16 +37,14 @@
"events": [ "events": [
{ {
"content": { "content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF", "displayname": "Example",
"displayname": "Alice Margatroid", "membership": "join"
"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": "@alice:example.org", "state_key": "@example:example.org",
"type": "m.room.member", "type": "m.room.member",
"unsigned": { "unsigned": {
"age": 1234 "age": 1234

View File

@@ -51,6 +51,21 @@
"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

@@ -101,28 +101,27 @@ void EventHandlerTest::nullEventId()
void EventHandlerTest::author() void EventHandlerTest::author()
{ {
auto event = room->messageEvents().at(0).get(); auto event = room->messageEvents().at(0).get();
auto author = room->user(event->senderId()); auto author = room->member(event->senderId());
EventHandler eventHandler(room, event); EventHandler eventHandler(room, event);
auto eventHandlerAuthor = eventHandler.getAuthor(); auto eventHandlerAuthor = eventHandler.getAuthor();
QCOMPARE(eventHandlerAuthor["isLocalUser"_ls], author->id() == room->localUser()->id()); QCOMPARE(eventHandlerAuthor.isLocalMember(), author.id() == room->localMember().id());
QCOMPARE(eventHandlerAuthor["id"_ls], author->id()); QCOMPARE(eventHandlerAuthor.id(), author.id());
QCOMPARE(eventHandlerAuthor["displayName"_ls], author->displayname(room)); QCOMPARE(eventHandlerAuthor.displayName(), author.displayName());
QCOMPARE(eventHandlerAuthor["avatarSource"_ls], room->avatarForMember(author)); QCOMPARE(eventHandlerAuthor.avatarUrl(), author.avatarUrl());
QCOMPARE(eventHandlerAuthor["avatarMediaId"_ls], author->avatarMediaId(room)); QCOMPARE(eventHandlerAuthor.avatarMediaId(), author.avatarMediaId());
QCOMPARE(eventHandlerAuthor["color"_ls], Utils::getUserColor(author->hueF())); QCOMPARE(eventHandlerAuthor.color(), author.color());
QCOMPARE(eventHandlerAuthor["object"_ls], QVariant::fromValue(author));
} }
void EventHandlerTest::nullAuthor() void EventHandlerTest::nullAuthor()
{ {
QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_room set to nullptr."); QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getAuthor(), QVariantMap()); QCOMPARE(emptyHandler.getAuthor(), RoomMember());
EventHandler noEventHandler(room, nullptr); EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_event set to nullptr. Returning empty user."); QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_event set to nullptr. Returning empty user.");
QCOMPARE(noEventHandler.getAuthor(), room->getUser(nullptr)); QCOMPARE(noEventHandler.getAuthor(), RoomMember());
} }
void EventHandlerTest::authorDisplayName() void EventHandlerTest::authorDisplayName()
@@ -393,31 +392,30 @@ 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->user(replyEvent->senderId()); auto replyAuthor = room->member(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["isLocalUser"_ls], replyAuthor->id() == room->localUser()->id()); QCOMPARE(eventHandlerReplyAuthor.isLocalMember(), replyAuthor.id() == room->localMember().id());
QCOMPARE(eventHandlerReplyAuthor["id"_ls], replyAuthor->id()); QCOMPARE(eventHandlerReplyAuthor.id(), replyAuthor.id());
QCOMPARE(eventHandlerReplyAuthor["displayName"_ls], replyAuthor->displayname(room)); QCOMPARE(eventHandlerReplyAuthor.displayName(), replyAuthor.displayName());
QCOMPARE(eventHandlerReplyAuthor["avatarSource"_ls], room->avatarForMember(replyAuthor)); QCOMPARE(eventHandlerReplyAuthor.avatarUrl(), replyAuthor.avatarUrl());
QCOMPARE(eventHandlerReplyAuthor["avatarMediaId"_ls], replyAuthor->avatarMediaId(room)); QCOMPARE(eventHandlerReplyAuthor.avatarMediaId(), replyAuthor.avatarMediaId());
QCOMPARE(eventHandlerReplyAuthor["color"_ls], Utils::getUserColor(replyAuthor->hueF())); QCOMPARE(eventHandlerReplyAuthor.color(), replyAuthor.color());
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(), room->getUser(nullptr)); QCOMPARE(eventHandlerNoAuthor.getReplyAuthor(), RoomMember());
} }
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(), QVariantMap()); QCOMPARE(emptyHandler.getReplyAuthor(), RoomMember());
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(), room->getUser(nullptr)); QCOMPARE(noEventHandler.getReplyAuthor(), RoomMember());
} }
void EventHandlerTest::replyBody() void EventHandlerTest::replyBody()
@@ -531,10 +529,10 @@ void EventHandlerTest::readMarkers()
auto readMarkers = eventHandler.getReadMarkers(); auto readMarkers = eventHandler.getReadMarkers();
QCOMPARE(readMarkers.size(), 1); QCOMPARE(readMarkers.size(), 1);
QCOMPARE(readMarkers[0].toMap()["id"_ls], QStringLiteral("@alice:matrix.org")); QCOMPARE(readMarkers[0].id(), QStringLiteral("@alice:example.org"));
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QString()); QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QString());
QCOMPARE(eventHandler.getReadMarkersString(), QStringLiteral("1 user: @alice:matrix.org")); QCOMPARE(eventHandler.getReadMarkersString(), QStringLiteral("1 user: Alice Margatroid"));
EventHandler eventHandler2(room, room->messageEvents().at(2).get()); EventHandler eventHandler2(room, room->messageEvents().at(2).get());
QCOMPARE(eventHandler2.hasReadMarkers(), true); QCOMPARE(eventHandler2.hasReadMarkers(), true);
@@ -554,7 +552,7 @@ void EventHandlerTest::nullReadMarkers()
QCOMPARE(emptyHandler.hasReadMarkers(), false); QCOMPARE(emptyHandler.hasReadMarkers(), false);
QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_room set to nullptr."); QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReadMarkers(), QVariantList()); QCOMPARE(emptyHandler.getReadMarkers(), QList<Quotient::RoomMember>());
QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_room set to nullptr."); QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getNumberExcessReadMarkers(), QString()); QCOMPARE(emptyHandler.getNumberExcessReadMarkers(), QString());
@@ -568,7 +566,7 @@ void EventHandlerTest::nullReadMarkers()
QCOMPARE(noEventHandler.hasReadMarkers(), false); QCOMPARE(noEventHandler.hasReadMarkers(), false);
QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_event set to nullptr."); QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReadMarkers(), QVariantList()); QCOMPARE(noEventHandler.getReadMarkers(), QList<Quotient::RoomMember>());
QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_event set to nullptr."); QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getNumberExcessReadMarkers(), QString()); QCOMPARE(noEventHandler.getNumberExcessReadMarkers(), QString());

View File

@@ -53,9 +53,7 @@ void ReactionModelTest::basicReaction()
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:matrix.org reacted with <span style=\"font-family: 'emoji';\">👍</span>")); QStringLiteral("@alice:matrix.org reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
auto authorList = QVariantList{room->getUser(room->user(QStringLiteral("@alice:matrix.org")))}; QCOMPARE(model.data(model.index(0), ReactionModel::HasLocalMember), false);
QCOMPARE(model.data(model.index(0), ReactionModel::AuthorsRole), authorList);
QCOMPARE(model.data(model.index(0), ReactionModel::HasLocalUser), false);
} }
void ReactionModelTest::newReaction() void ReactionModelTest::newReaction()

View File

@@ -156,7 +156,6 @@ 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
@@ -283,6 +282,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/ConsentDialog.qml qml/ConsentDialog.qml
qml/AskDirectChatConfirmation.qml qml/AskDirectChatConfirmation.qml
qml/HoverLinkIndicator.qml qml/HoverLinkIndicator.qml
qml/CrossSigningSetupDialog.qml
DEPENDENCIES DEPENDENCIES
QtCore QtCore
QtQuick QtQuick

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->localUser()->id() && event->hasTextContent()) { if (event->senderId() == m_room->localMember().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

@@ -3,6 +3,8 @@
#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"
@@ -84,7 +86,7 @@ void ChatBarCache::setEditId(const QString &editId)
Q_EMIT attachmentPathChanged(); Q_EMIT attachmentPathChanged();
} }
QVariantMap ChatBarCache::relationUser() const Quotient::RoomMember 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.";
@@ -96,9 +98,9 @@ QVariantMap ChatBarCache::relationUser() const
return {}; return {};
} }
if (m_relationId.isEmpty()) { if (m_relationId.isEmpty()) {
return room->getUser(nullptr); return room->member(QString());
} }
return room->getUser(room->user((*room->findInTimeline(m_relationId))->senderId())); return room->member((*room->findInTimeline(m_relationId))->senderId());
} }
QString ChatBarCache::relationMessage() const QString ChatBarCache::relationMessage() const

View File

@@ -10,6 +10,12 @@
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.
*/ */
@@ -88,26 +94,13 @@ 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 user for the message being replied to. * @brief Get the RoomMember object for the message being replied to.
* *
* This is different to getting a Quotient::User object * Returns an empty RoomMember if not replying to a message.
* as neither of those can provide details like the displayName or avatarMediaId
* without the room context as these can vary from room to room.
* *
* Returns an empty user if not replying to a message. * @sa Quotient::RoomMember
*
* 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(QVariantMap relationUser READ relationUser NOTIFY relationIdChanged) Q_PROPERTY(Quotient::RoomMember relationUser READ relationUser NOTIFY relationIdChanged)
/** /**
* @brief The content of the related message. * @brief The content of the related message.
@@ -161,7 +154,7 @@ public:
QString editId() const; QString editId() const;
void setEditId(const QString &editId); void setEditId(const QString &editId);
QVariantMap relationUser() const; Quotient::RoomMember relationUser() const;
QString relationMessage() const; QString relationMessage() const;

View File

@@ -406,12 +406,3 @@ void Controller::removeConnection(const QString &userId)
SettingsGroup("Accounts"_ls).remove(userId); SettingsGroup("Accounts"_ls).remove(userId);
} }
} }
bool Controller::ssssSupported() const
{
#if __has_include("Quotient/e2ee/sssshandler.h")
return true;
#else
return false;
#endif
}

View File

@@ -50,8 +50,6 @@ 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 ssssSupported READ ssssSupported CONSTANT)
public: public:
static Controller &instance(); static Controller &instance();
static Controller *create(QQmlEngine *engine, QJSEngine *) static Controller *create(QQmlEngine *engine, QJSEngine *)
@@ -96,8 +94,6 @@ public:
Q_INVOKABLE void removeConnection(const QString &userId); Q_INVOKABLE void removeConnection(const QString &userId);
bool ssssSupported() const;
private: private:
explicit Controller(QObject *parent = nullptr); explicit Controller(QObject *parent = nullptr);

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,20 +61,18 @@ MessageComponentType::Type EventHandler::messageComponentType() const
return MessageComponentType::typeForEvent(*m_event); return MessageComponentType::typeForEvent(*m_event);
} }
QVariantMap EventHandler::getAuthor(bool isPending) const Quotient::RoomMember EventHandler::getAuthor(bool isPending) const
{ {
if (m_room == nullptr) { if (m_room == nullptr) {
qCWarning(EventHandling) << "getAuthor called with m_room set to nullptr."; qCWarning(EventHandling) << "getAuthor 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) << "getAuthor called with m_event set to nullptr. Returning empty user."; qCWarning(EventHandling) << "getAuthor called with m_event set to nullptr. Returning empty user.";
return m_room->getUser(nullptr); return {};
} }
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId()); return isPending ? m_room->localMember() : m_room->member(m_event->senderId());
return m_room->getUser(author);
} }
QString EventHandler::getAuthorDisplayName(bool isPending) const QString EventHandler::getAuthorDisplayName(bool isPending) const
@@ -96,8 +94,8 @@ QString EventHandler::getAuthorDisplayName(bool isPending) const
} }
return previousDisplayName; return previousDisplayName;
} else { } else {
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId()); const auto author = isPending ? m_room->localMember() : m_room->member(m_event->senderId());
return m_room->htmlSafeMemberName(author->id()); return author.htmlSafeDisplayName();
} }
} }
@@ -112,8 +110,8 @@ QString EventHandler::singleLineAuthorDisplayname(bool isPending) const
return {}; return {};
} }
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId()); const auto author = isPending ? m_room->localMember() : m_room->member(m_event->senderId());
auto displayName = m_room->safeMemberName(author->id()); auto displayName = author.displayName();
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(" "));
@@ -220,7 +218,7 @@ bool EventHandler::isHidden()
} }
} }
if (m_room->connection()->isIgnored(m_room->user(m_event->senderId()))) { if (m_room->connection()->isIgnored(m_event->senderId())) {
return true; return true;
} }
@@ -255,7 +253,7 @@ QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event)
QString body; QString body;
if (event.hasTextContent() && event.content()) { if (event.hasTextContent() && event.content()) {
body = static_cast<const MessageEventContent::TextContent *>(event.content())->body; body = static_cast<const EventContent::TextContent *>(event.content())->body;
} else { } else {
body = event.plainBody(); body = event.plainBody();
} }
@@ -318,7 +316,7 @@ 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->htmlSafeMemberName(e.userId()); auto subjectName = m_room->member(e.userId()).htmlSafeDisplayName();
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).toHtmlEscaped(); subjectName = sanitized(*e.prevContent()->displayName).toHtmlEscaped();
@@ -326,7 +324,8 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
} }
if (prettyPrint) { if (prettyPrint) {
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\">%2</a>").arg(e.userId(), subjectName); subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
.arg(e.userId(), m_room->member(e.userId()).color().name(), subjectName);
} }
// The below code assumes senderName output in AuthorRole // The below code assumes senderName output in AuthorRole
@@ -440,7 +439,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 ServerAclEvent &) { [](const RoomServerAclEvent &) {
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) {
@@ -479,7 +478,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 MessageEventContent::TextContent *>(event.content())->body; body = static_cast<const EventContent::TextContent *>(event.content())->body;
} else { } else {
body = event.plainBody(); body = event.plainBody();
} }
@@ -609,7 +608,7 @@ QString EventHandler::getGenericBody() const
[](const LocationBeaconEvent &) { [](const LocationBeaconEvent &) {
return i18n("sent a live location beacon"); return i18n("sent a live location beacon");
}, },
[](const ServerAclEvent &) { [](const RoomServerAclEvent &) {
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) {
@@ -800,25 +799,21 @@ MessageComponentType::Type EventHandler::replyMessageComponentType() const
return MessageComponentType::typeForEvent(*replyEvent); return MessageComponentType::typeForEvent(*replyEvent);
} }
QVariantMap EventHandler::getReplyAuthor() const Quotient::RoomMember 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 m_room->getUser(nullptr); return {};
} }
auto replyPtr = m_room->getReplyForEvent(*m_event); if (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->getUser(nullptr); return m_room->member(QString());
} }
} }
@@ -966,11 +961,11 @@ bool EventHandler::hasReadMarkers() const
} }
auto userIds = m_room->userIdsAtEvent(m_event->id()); auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id()); userIds.remove(m_room->localMember().id());
return userIds.size() > 0; return userIds.size() > 0;
} }
QVariantList EventHandler::getReadMarkers(int maxMarkers) const QList<Quotient::RoomMember> EventHandler::getReadMarkers(int maxMarkers) const
{ {
if (m_room == nullptr) { if (m_room == nullptr) {
qCWarning(EventHandling) << "getReadMarkers called with m_room set to nullptr."; qCWarning(EventHandling) << "getReadMarkers called with m_room set to nullptr.";
@@ -982,18 +977,17 @@ QVariantList EventHandler::getReadMarkers(int maxMarkers) const
} }
auto userIds_temp = m_room->userIdsAtEvent(m_event->id()); auto userIds_temp = m_room->userIdsAtEvent(m_event->id());
userIds_temp.remove(m_room->localUser()->id()); userIds_temp.remove(m_room->localMember().id());
auto userIds = userIds_temp.values(); auto userIds = userIds_temp.values();
if (userIds.count() > maxMarkers) { if (userIds.count() > maxMarkers) {
userIds = userIds.mid(0, maxMarkers); userIds = userIds.mid(0, maxMarkers);
} }
QVariantList users; QList<Quotient::RoomMember> users;
users.reserve(userIds.size()); users.reserve(userIds.size());
for (const auto &userId : userIds) { for (const auto &userId : userIds) {
auto user = m_room->user(userId); users += m_room->member(userId);
users += m_room->getUser(user);
} }
return users; return users;
@@ -1011,7 +1005,7 @@ QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const
} }
auto userIds = m_room->userIdsAtEvent(m_event->id()); auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id()); userIds.remove(m_room->localMember().id());
if (userIds.count() > maxMarkers) { if (userIds.count() > maxMarkers) {
return QStringLiteral("+ ") + QString::number(userIds.count() - maxMarkers); return QStringLiteral("+ ") + QString::number(userIds.count() - maxMarkers);
@@ -1032,7 +1026,7 @@ QString EventHandler::getReadMarkersString() const
} }
auto userIds = m_room->userIdsAtEvent(m_event->id()); auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id()); userIds.remove(m_room->localMember().id());
/** /**
* The string ends up in the form * The string ends up in the form
@@ -1040,10 +1034,12 @@ QString EventHandler::getReadMarkersString() const
*/ */
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size()); QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
for (const auto &userId : userIds) { for (const auto &userId : userIds) {
auto user = m_room->user(userId); auto member = m_room->member(userId);
auto displayName = user->displayname(m_room); QString displayName;
if (displayName.isEmpty()) { if (member.isEmpty()) {
displayName = userId; displayName = i18nc("A member who is not in the room has been requested.", "unknown member");
} else {
displayName = member.displayName();
} }
readMarkersString += displayName + i18nc("list separator", ", "); readMarkersString += displayName + i18nc("list separator", ", ");
} }

View File

@@ -13,6 +13,11 @@
#include "enums/messagecomponenttype.h" #include "enums/messagecomponenttype.h"
namespace Quotient
{
class RoomMember;
}
class LinkPreviewer; class LinkPreviewer;
class NeoChatRoom; class NeoChatRoom;
class ReactionModel; class ReactionModel;
@@ -51,30 +56,17 @@ public:
/** /**
* @brief Get the author of the event in context of the room. * @brief Get the author of the event in context of the room.
* *
* This is different to getting a Quotient::User object * An empty Quotient::RoomMember will be returned if the EventHandler hasn't had
* as neither of those can provide details like the displayName or avatarMediaId * the room or event initialised.
* 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 * @param isPending if the event is pending, i.e. has not been confirmed by
* the server. * the server.
* *
* @return a QVariantMap for the user with the following properties: * @return a Quotient::RoomMember object for the user.
* - 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 * @sa Quotient::RoomMember
*/ */
QVariantMap getAuthor(bool isPending = false) const; Quotient::RoomMember getAuthor(bool isPending = false) const;
/** /**
* @brief Get the display name of the event author. * @brief Get the display name of the event author.
@@ -251,27 +243,17 @@ 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.
* *
* This is different to getting a Quotient::User object * An empty Quotient::RoomMember will be returned if the EventHandler hasn't had
* as neither of those can provide details like the displayName or avatarMediaId * the room or event initialised.
* 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 * @param isPending if the event is pending, i.e. has not been confirmed by
* intialised. An empty user (i.e. a QVariantMap with all the correct keys * the server.
* but empty values) will be returned if the room has been set but not an event.
* *
* @return a QVariantMap for the user with the following properties: * @return a Quotient::RoomMember object for the user.
* - 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 * @sa Quotient::RoomMember
*/ */
QVariantMap getReplyAuthor() const; Quotient::RoomMember 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
@@ -375,7 +357,7 @@ public:
* the number of users shown plus the excess number will be * the number of users shown plus the excess number will be
* the total number of other user read markers at an event. * the total number of other user read markers at an event.
*/ */
QVariantList getReadMarkers(int maxMarkers = 5) const; QList<Quotient::RoomMember> getReadMarkers(int maxMarkers = 5) const;
/** /**
* @brief Returns the number of excess user read markers for the event. * @brief Returns the number of excess user read markers for the event.

View File

@@ -10,29 +10,29 @@ ImagePackEventContent::ImagePackEventContent(const QJsonObject &json)
{ {
if (json.contains(QStringLiteral("pack"))) { if (json.contains(QStringLiteral("pack"))) {
pack = ImagePackEventContent::Pack{ pack = ImagePackEventContent::Pack{
fromJson<Omittable<QString>>(json["pack"_ls].toObject()["display_name"_ls]), fromJson<std::optional<QString>>(json["pack"_ls].toObject()["display_name"_ls]),
fromJson<Omittable<QUrl>>(json["pack"_ls].toObject()["avatar_url"_ls]), fromJson<std::optional<QUrl>>(json["pack"_ls].toObject()["avatar_url"_ls]),
fromJson<Omittable<QStringList>>(json["pack"_ls].toObject()["usage"_ls]), fromJson<std::optional<QStringList>>(json["pack"_ls].toObject()["usage"_ls]),
fromJson<Omittable<QString>>(json["pack"_ls].toObject()["attribution"_ls]), fromJson<std::optional<QString>>(json["pack"_ls].toObject()["attribution"_ls]),
}; };
} else { } else {
pack = none; pack = std::nullopt;
} }
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) {
Omittable<EventContent::ImageInfo> info; std::optional<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 = none; info = std::nullopt;
} }
images += ImagePackImage{ images += ImagePackImage{
k, k,
fromJson<QUrl>(json["images"_ls][k]["url"_ls].toString()), fromJson<QUrl>(json["images"_ls][k]["url"_ls].toString()),
fromJson<Omittable<QString>>(json["images"_ls][k]["body"_ls]), fromJson<std::optional<QString>>(json["images"_ls][k]["body"_ls]),
info, info,
fromJson<Omittable<QStringList>>(json["images"_ls][k]["usage"_ls]), fromJson<std::optional<QStringList>>(json["images"_ls][k]["usage"_ls]),
}; };
} }
} }

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 {
Quotient::Omittable<QString> displayName; /**< The display name of the pack. */ std::optional<QString> displayName; /**< The display name of the pack. */
Quotient::Omittable<QUrl> avatarUrl; /**< The source mxc URL for the pack avatar. */ std::optional<QUrl> avatarUrl; /**< The source mxc URL for the pack avatar. */
Quotient::Omittable<QStringList> usage; /**< An array of the usages for this pack. Possible usages are "emoticon" and "sticker". */ std::optional<QStringList> usage; /**< An array of the usages for this pack. Possible usages are "emoticon" and "sticker". */
Quotient::Omittable<QString> attribution; /**< The attribution for the pack author(s). */ std::optional<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. */
Quotient::Omittable<QString> body; /**< An optional text body for this image. */ std::optional<QString> body; /**< An optional text body for this image. */
Quotient::Omittable<Quotient::EventContent::ImageInfo> info; /**< The ImageInfo object used for the info block of m.sticker events. */ std::optional<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.
*/ */
Quotient::Omittable<QStringList> usage; std::optional<QStringList> usage;
}; };
/** /**
@@ -53,7 +53,7 @@ public:
* *
* @sa Pack * @sa Pack
*/ */
Quotient::Omittable<Pack> pack; std::optional<Pack> pack;
/** /**
* @brief Return a vector of images in the pack. * @brief Return a vector of images in the pack.

View File

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

View File

@@ -6,10 +6,8 @@
#include <QQmlEngine> #include <QQmlEngine>
#include <Quotient/accountregistry.h> #include <Quotient/accountregistry.h>
#include <Quotient/keyverificationsession.h>
#if __has_include("Quotient/e2ee/sssshandler.h")
#include <Quotient/e2ee/sssshandler.h> #include <Quotient/e2ee/sssshandler.h>
#endif #include <Quotient/keyverificationsession.h>
#include "controller.h" #include "controller.h"
#include "neochatconfig.h" #include "neochatconfig.h"
@@ -47,10 +45,8 @@ 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

@@ -5,7 +5,7 @@
using namespace Quotient; using namespace Quotient;
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth) NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const std::optional<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password") : BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
{ {
QJsonObject _data; QJsonObject _data;

View File

@@ -4,10 +4,9 @@
#pragma once #pragma once
#include <Quotient/jobs/basejob.h> #include <Quotient/jobs/basejob.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 = Quotient::none); explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const std::optional<QJsonObject> &auth = std::nullopt);
}; };

View File

@@ -5,7 +5,7 @@
using namespace Quotient; using namespace Quotient;
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const Omittable<QJsonObject> &auth) NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const std::optional<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("DisableDeviceJob"), "_matrix/client/v3/account/deactivate") : BaseJob(HttpVerb::Post, QStringLiteral("DisableDeviceJob"), "_matrix/client/v3/account/deactivate")
{ {
QJsonObject data; QJsonObject data;

View File

@@ -4,10 +4,9 @@
#pragma once #pragma once
#include <Quotient/jobs/basejob.h> #include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeoChatDeactivateAccountJob : public Quotient::BaseJob class NeoChatDeactivateAccountJob : public Quotient::BaseJob
{ {
public: public:
explicit NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth = Quotient::none); explicit NeoChatDeactivateAccountJob(const std::optional<QJsonObject> &auth = std::nullopt);
}; };

View File

@@ -5,7 +5,7 @@
using namespace Quotient; using namespace Quotient;
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth) NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const std::optional<QJsonObject> &auth)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1()) : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
{ {
QJsonObject _data; QJsonObject _data;

View File

@@ -4,10 +4,9 @@
#pragma once #pragma once
#include <Quotient/jobs/basejob.h> #include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatDeleteDeviceJob : public Quotient::BaseJob class NeochatDeleteDeviceJob : public Quotient::BaseJob
{ {
public: public:
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = Quotient::none); explicit NeochatDeleteDeviceJob(const QString &deviceId, const std::optional<QJsonObject> &auth = std::nullopt);
}; };

View File

@@ -13,6 +13,7 @@
#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>
@@ -174,6 +175,7 @@ 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

@@ -9,6 +9,7 @@
#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>
@@ -202,11 +203,11 @@ QList<ActionsModel::Action> actions{
Q_EMIT room->showMessage(NeoChatRoom::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->localUser()->id() == text) { if (room->localMember().id() == text) {
Q_EMIT room->showMessage(NeoChatRoom::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->users().contains(room->user(text))) { if (room->members().contains(room->member(text))) {
Q_EMIT room->showMessage(NeoChatRoom::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();
} }
@@ -359,7 +360,7 @@ QList<ActionsModel::Action> actions{
Q_EMIT room->showMessage(NeoChatRoom::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(room->connection()->user(text)); room->connection()->addToIgnoredUsers(text);
Q_EMIT room->showMessage(NeoChatRoom::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();
}, },
@@ -382,7 +383,7 @@ QList<ActionsModel::Action> actions{
Q_EMIT room->showMessage(NeoChatRoom::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(room->connection()->user(text)); room->connection()->removeFromIgnoredUsers(text);
Q_EMIT room->showMessage(NeoChatRoom::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();
}, },
@@ -431,11 +432,11 @@ QList<ActionsModel::Action> actions{
if (!plEvent) { if (!plEvent) {
return QString(); return QString();
} }
if (plEvent->ban() > plEvent->powerLevelForUser(room->localUser()->id())) { if (plEvent->ban() > plEvent->powerLevelForUser(room->localMember().id())) {
Q_EMIT room->showMessage(NeoChatRoom::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->localUser()->id()) <= plEvent->powerLevelForUser(parts[0])) { if (plEvent->powerLevelForUser(room->localMember().id()) <= plEvent->powerLevelForUser(parts[0])) {
Q_EMIT room->showMessage( Q_EMIT room->showMessage(
NeoChatRoom::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]));
@@ -464,7 +465,7 @@ QList<ActionsModel::Action> actions{
if (!plEvent) { if (!plEvent) {
return QString(); return QString();
} }
if (plEvent->ban() > plEvent->powerLevelForUser(room->localUser()->id())) { if (plEvent->ban() > plEvent->powerLevelForUser(room->localMember().id())) {
Q_EMIT room->showMessage(NeoChatRoom::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();
} }
@@ -495,7 +496,7 @@ QList<ActionsModel::Action> actions{
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->localUser()->id()) { if (parts[0] == room->localMember().id()) {
Q_EMIT room->showMessage(NeoChatRoom::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();
} }
@@ -508,11 +509,11 @@ QList<ActionsModel::Action> actions{
return QString(); return QString();
} }
auto kick = plEvent->kick(); auto kick = plEvent->kick();
if (plEvent->powerLevelForUser(room->localUser()->id()) < kick) { if (plEvent->powerLevelForUser(room->localMember().id()) < kick) {
Q_EMIT room->showMessage(NeoChatRoom::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->localUser()->id()) <= plEvent->powerLevelForUser(parts[0])) { if (plEvent->powerLevelForUser(room->localMember().id()) <= plEvent->powerLevelForUser(parts[0])) {
Q_EMIT room->showMessage( Q_EMIT room->showMessage(
NeoChatRoom::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]));

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 m_room->getUser(data.senderId); return QVariant::fromValue(m_room->member(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(),
.author = m_room->user(event->senderId()), .member = m_room->member(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 m_room->getUser(m_locations[row].author); return QVariant::fromValue(m_locations[row].member);
} }
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/user.h> #include <Quotient/roommember.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::User *author; Quotient::RoomMember member;
}; };
QList<LocationData> m_locations; QList<LocationData> m_locations;
void addLocation(const Quotient::RoomMessageEvent *event); void addLocation(const Quotient::RoomMessageEvent *event);

View File

@@ -194,7 +194,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return eventHandler.getId(); return eventHandler.getId();
} }
if (role == AuthorRole) { if (role == AuthorRole) {
return eventHandler.getAuthor(false); return QVariant::fromValue(eventHandler.getAuthor(false));
} }
if (role == MediaInfoRole) { if (role == MediaInfoRole) {
return eventHandler.getMediaInfo(); return eventHandler.getMediaInfo();
@@ -224,7 +224,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return eventHandler.getReplyId(); return eventHandler.getReplyId();
} }
if (role == ReplyAuthorRole) { if (role == ReplyAuthorRole) {
return eventHandler.getReplyAuthor(); return QVariant::fromValue(eventHandler.getReplyAuthor());
} }
if (role == ReplyContentModelRole) { if (role == ReplyContentModelRole) {
return QVariant::fromValue<MessageContentModel *>(m_replyModel); return QVariant::fromValue<MessageContentModel *>(m_replyModel);

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/user.h> #include <Quotient/roommember.h>
#include <QDebug> #include <QDebug>
#include <QGuiApplication> #include <QGuiApplication>
@@ -222,7 +222,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
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();
} }
@@ -460,7 +460,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
} }
if (role == AuthorRole) { if (role == AuthorRole) {
return eventHandler.getAuthor(isPending); return QVariant::fromValue(eventHandler.getAuthor(isPending));
} }
if (role == HighlightRole) { if (role == HighlightRole) {
@@ -539,7 +539,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
} }
if (role == ReadMarkersRole) { if (role == ReadMarkersRole) {
return eventHandler.getReadMarkers(); return QVariant::fromValue(eventHandler.getReadMarkers());
} }
if (role == ExcessReadMarkersRole) { if (role == ExcessReadMarkersRole) {
@@ -592,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->localUser()->id(); return eventHandler.messageComponentType() == MessageComponentType::Text && evt.senderId() == m_currentRoom->localMember().id();
} }
return {}; return {};

View File

@@ -116,7 +116,7 @@ void NotificationsModel::loadData()
if (!room) { if (!room) {
continue; continue;
} }
auto u = room->memberAvatarUrl(authorId); auto u = room->member(authorId).avatarUrl();
auto avatar = u.isEmpty() ? QUrl() : connection()->makeMediaUrl(u); auto avatar = u.isEmpty() ? QUrl() : connection()->makeMediaUrl(u);
const auto &authorAvatar = avatar.isValid() && avatar.scheme() == QStringLiteral("mxc") ? avatar : QUrl(); const auto &authorAvatar = avatar.isValid() && avatar.scheme() == QStringLiteral("mxc") ? avatar : QUrl();
@@ -125,9 +125,9 @@ void NotificationsModel::loadData()
beginInsertRows({}, m_notifications.length(), m_notifications.length()); beginInsertRows({}, m_notifications.length(), m_notifications.length());
m_notifications += Notification{ m_notifications += Notification{
.roomId = notification.roomId, .roomId = notification.roomId,
.text = room->htmlSafeMemberName(authorId) + (roomEvent->is<StateEvent>() ? QStringLiteral(" ") : QStringLiteral(": ")) .text = room->member(authorId).htmlSafeDisplayName() + (roomEvent->is<StateEvent>() ? QStringLiteral(" ") : QStringLiteral(": "))
+ eventHandler.getPlainBody(true), + eventHandler.getPlainBody(true),
.authorName = room->htmlSafeMemberName(authorId), .authorName = room->member(authorId).htmlSafeDisplayName(),
.authorAvatar = authorAvatar, .authorAvatar = authorAvatar,
.eventId = roomEvent->id(), .eventId = roomEvent->id(),
.roomDisplayName = room->displayName(), .roomDisplayName = room->displayName(),

View File

@@ -15,7 +15,7 @@
#include <KLocalizedString> #include <KLocalizedString>
#include <Quotient/user.h> #include <Quotient/roommember.h>
ReactionModel::ReactionModel(const Quotient::RoomMessageEvent *event, NeoChatRoom *room) ReactionModel::ReactionModel(const Quotient::RoomMessageEvent *event, NeoChatRoom *room)
: QAbstractListModel(nullptr) : QAbstractListModel(nullptr)
@@ -69,8 +69,7 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
text += i18nc("Separate the usernames of users", " and "); text += i18nc("Separate the usernames of users", " and ");
} }
} }
auto displayName = reaction.authors.at(i).toMap()[QStringLiteral("displayName")].toString(); text += m_room->member(reaction.authors.at(i)).displayName();
text += displayName.isEmpty() ? reaction.authors.at(i).toMap()[QStringLiteral("id")].toString() : displayName;
} }
if (reaction.authors.count() > 3) { if (reaction.authors.count() > 3) {
@@ -86,13 +85,9 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
return text; return text;
} }
if (role == AuthorsRole) { if (role == HasLocalMember) {
return reaction.authors;
}
if (role == HasLocalUser) {
for (auto author : reaction.authors) { for (auto author : reaction.authors) {
if (author.toMap()[QStringLiteral("id")] == m_room->localUser()->id()) { if (author == m_room->localMember().id()) {
return true; return true;
} }
} }
@@ -121,13 +116,13 @@ void ReactionModel::updateReactions()
return; return;
}; };
QMap<QString, QList<Quotient::User *>> reactions = {}; QMap<QString, QStringList> reactions = {};
for (const auto &a : annotations) { for (const auto &a : annotations) {
if (a->isRedacted()) { // Just in case? if (a->isRedacted()) { // Just in case?
continue; continue;
} }
if (const auto &e = eventCast<const Quotient::ReactionEvent>(a)) { if (const auto &e = eventCast<const Quotient::ReactionEvent>(a)) {
reactions[e->key()].append(m_room->user(e->senderId())); reactions[e->key()].append(e->senderId());
if (e->contentJson()[QStringLiteral("shortcode")].toString().length()) { if (e->contentJson()[QStringLiteral("shortcode")].toString().length()) {
m_shortcodes[e->key()] = e->contentJson()[QStringLiteral("shortcode")].toString().toHtmlEscaped(); m_shortcodes[e->key()] = e->contentJson()[QStringLiteral("shortcode")].toString().toHtmlEscaped();
} }
@@ -138,15 +133,14 @@ void ReactionModel::updateReactions()
endResetModel(); endResetModel();
return; return;
} }
auto i = reactions.constBegin(); auto i = reactions.constBegin();
while (i != reactions.constEnd()) { while (i != reactions.constEnd()) {
QVariantList authors; QStringList members;
for (const auto &author : i.value()) { for (const auto &member : i.value()) {
authors.append(m_room->getUser(author)); members.append(member);
} }
m_reactions.append(ReactionModel::Reaction{i.key(), authors}); m_reactions.append(ReactionModel::Reaction{i.key(), members});
++i; ++i;
} }
@@ -159,8 +153,7 @@ QHash<int, QByteArray> ReactionModel::roleNames() const
{TextContentRole, "textContent"}, {TextContentRole, "textContent"},
{ReactionRole, "reaction"}, {ReactionRole, "reaction"},
{ToolTipRole, "toolTip"}, {ToolTipRole, "toolTip"},
{AuthorsRole, "authors"}, {HasLocalMember, "hasLocalMember"},
{HasLocalUser, "hasLocalUser"},
}; };
} }

View File

@@ -5,13 +5,8 @@
#include "neochatroom.h" #include "neochatroom.h"
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QQmlEngine>
#include <Quotient/events/reactionevent.h> #include <Quotient/events/reactionevent.h>
#include <Quotient/roommember.h>
namespace Quotient
{
class User;
}
/** /**
* @class ReactionModel * @class ReactionModel
@@ -30,7 +25,7 @@ public:
*/ */
struct Reaction { struct Reaction {
QString reaction; /**< The reaction emoji. */ QString reaction; /**< The reaction emoji. */
QVariantList authors; /**< The list of authors who sent the given reaction. */ QStringList authors; /**< The list of authors who sent the given reaction. */
}; };
/** /**
@@ -40,8 +35,7 @@ public:
TextContentRole = Qt::DisplayRole, /**< The text to show in the reaction. */ TextContentRole = Qt::DisplayRole, /**< The text to show in the reaction. */
ReactionRole, /**< The reaction emoji. */ ReactionRole, /**< The reaction emoji. */
ToolTipRole, /**< The tool tip to show for the reaction. */ ToolTipRole, /**< The tool tip to show for the reaction. */
AuthorsRole, /**< The list of authors who sent the given reaction. */ HasLocalMember, /**< Whether the local member is in the list of authors. */
HasLocalUser, /**< Whether the local user is in the list of authors. */
}; };
explicit ReactionModel(const Quotient::RoomMessageEvent *event, NeoChatRoom *room); explicit ReactionModel(const Quotient::RoomMessageEvent *event, NeoChatRoom *room);

View File

@@ -44,7 +44,7 @@ void SearchModel::search()
} }
RoomEventFilter filter; RoomEventFilter filter;
filter.unreadThreadNotifications = none; filter.unreadThreadNotifications = std::nullopt;
filter.lazyLoadMembers = true; filter.lazyLoadMembers = true;
filter.includeRedundantMembers = false; filter.includeRedundantMembers = false;
filter.notRooms = QStringList(); filter.notRooms = QStringList();
@@ -58,7 +58,7 @@ void SearchModel::search()
.orderBy = "recent"_ls, .orderBy = "recent"_ls,
.eventContext = SearchJob::IncludeEventContext{3, 3, true}, .eventContext = SearchJob::IncludeEventContext{3, 3, true},
.includeState = false, .includeState = false,
.groupings = none, .groupings = std::nullopt,
}; };
@@ -85,7 +85,7 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
case ShowAuthorRole: case ShowAuthorRole:
return true; return true;
case AuthorRole: case AuthorRole:
return eventHandler.getAuthor(); return QVariant::fromValue(eventHandler.getAuthor());
case ShowSectionRole: case ShowSectionRole:
if (row == 0) { if (row == 0) {
return true; return true;

View File

@@ -87,7 +87,7 @@ void SpaceChildrenModel::refreshModel()
m_rootItem = m_rootItem =
new SpaceTreeItem(dynamic_cast<NeoChatConnection *>(m_space->connection()), nullptr, m_space->id(), m_space->displayName(), m_space->canonicalAlias()); new SpaceTreeItem(dynamic_cast<NeoChatConnection *>(m_space->connection()), nullptr, m_space->id(), m_space->displayName(), m_space->canonicalAlias());
endResetModel(); endResetModel();
auto job = m_space->connection()->callApi<Quotient::GetSpaceHierarchyJob>(m_space->id(), Quotient::none, Quotient::none, 1); auto job = m_space->connection()->callApi<Quotient::GetSpaceHierarchyJob>(m_space->id(), std::nullopt, std::nullopt, 1);
m_currentJobs.append(job); m_currentJobs.append(job);
connect(job, &Quotient::BaseJob::success, this, [this, job]() { connect(job, &Quotient::BaseJob::success, this, [this, job]() {
insertChildren(job->rooms()); insertChildren(job->rooms());
@@ -136,7 +136,7 @@ void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJ
} }
} }
if (children[i].childrenState.size() > 0) { if (children[i].childrenState.size() > 0) {
auto job = m_space->connection()->callApi<Quotient::GetSpaceHierarchyJob>(children[i].roomId, Quotient::none, Quotient::none, 1); auto job = m_space->connection()->callApi<Quotient::GetSpaceHierarchyJob>(children[i].roomId, std::nullopt, std::nullopt, 1);
m_currentJobs.append(job); m_currentJobs.append(job);
connect(job, &Quotient::BaseJob::success, this, [this, parent, insertRow, job]() { connect(job, &Quotient::BaseJob::success, this, [this, parent, insertRow, job]() {
insertChildren(job->rooms(), index(insertRow, 0, parent)); insertChildren(job->rooms(), index(insertRow, 0, parent));

View File

@@ -26,18 +26,26 @@ void UserListModel::setRoom(NeoChatRoom *room)
if (m_currentRoom) { if (m_currentRoom) {
m_currentRoom->disconnect(this); m_currentRoom->disconnect(this);
m_currentRoom->connection()->disconnect(this);
} }
m_currentRoom = room; m_currentRoom = room;
if (m_currentRoom) { if (m_currentRoom) {
connect(m_currentRoom, &Room::userAdded, this, &UserListModel::userAdded); connect(m_currentRoom, &Room::memberJoined, this, &UserListModel::memberJoined);
connect(m_currentRoom, &Room::userRemoved, this, &UserListModel::userRemoved); connect(m_currentRoom, &Room::memberLeft, this, &UserListModel::memberLeft);
connect(m_currentRoom, &Room::memberAboutToRename, this, &UserListModel::userRemoved); connect(m_currentRoom, &Room::memberNameUpdated, this, [this](RoomMember member) {
connect(m_currentRoom, &Room::memberRenamed, this, &UserListModel::userAdded); refreshMember(member, {DisplayNameRole});
connect(m_currentRoom, &Room::changed, this, &UserListModel::refreshAllUsers); });
connect(m_currentRoom, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
refreshMember(member, {AvatarRole});
});
connect(m_currentRoom, &Room::changed, this, &UserListModel::refreshAllMembers);
connect(m_currentRoom->connection(), &Connection::loggedOut, this, [this]() {
setRoom(nullptr);
});
} }
refreshAllUsers(); refreshAllMembers();
Q_EMIT roomChanged(); Q_EMIT roomChanged();
} }
@@ -46,44 +54,36 @@ NeoChatRoom *UserListModel::room() const
return m_currentRoom; return m_currentRoom;
} }
Quotient::User *UserListModel::userAt(QModelIndex index) const
{
if (index.row() < 0 || index.row() >= m_users.size()) {
return nullptr;
}
return m_users.at(index.row());
}
QVariant UserListModel::data(const QModelIndex &index, int role) const QVariant UserListModel::data(const QModelIndex &index, int role) const
{ {
if (!index.isValid()) { if (!index.isValid()) {
return QVariant(); return QVariant();
} }
if (index.row() >= m_users.count()) { if (index.row() >= m_members.count()) {
qDebug() << "UserListModel, something's wrong: index.row() >= " qDebug() << "UserListModel, something's wrong: index.row() >= "
"users.count()"; "users.count()";
return {}; return {};
} }
auto user = m_users.at(index.row()); auto member = m_members.at(index.row());
if (role == DisplayNameRole) { if (role == DisplayNameRole) {
return user->displayname(m_currentRoom); return member.disambiguatedName();
} }
if (role == UserIdRole) { if (role == UserIdRole) {
return user->id(); return member.id();
} }
if (role == AvatarRole) { if (role == AvatarRole) {
return m_currentRoom->avatarForMember(user); return member.avatarUrl();
} }
if (role == ObjectRole) { if (role == ObjectRole) {
return QVariant::fromValue(user); return QVariant::fromValue(member);
} }
if (role == PowerLevelRole) { if (role == PowerLevelRole) {
auto plEvent = m_currentRoom->currentState().get<RoomPowerLevelsEvent>(); auto plEvent = m_currentRoom->currentState().get<RoomPowerLevelsEvent>();
if (!plEvent) { if (!plEvent) {
return 0; return 0;
} }
return plEvent->powerLevelForUser(user->id()); return plEvent->powerLevelForUser(member.id());
} }
if (role == PowerLevelStringRole) { if (role == PowerLevelStringRole) {
auto pl = m_currentRoom->currentState().get<RoomPowerLevelsEvent>(); auto pl = m_currentRoom->currentState().get<RoomPowerLevelsEvent>();
@@ -93,7 +93,7 @@ QVariant UserListModel::data(const QModelIndex &index, int role) const
return QStringLiteral("Not Available"); return QStringLiteral("Not Available");
} }
auto userPl = pl->powerLevelForUser(user->id()); auto userPl = pl->powerLevelForUser(member.id());
return i18nc("%1 is the name of the power level, e.g. admin and %2 is the value that represents.", return i18nc("%1 is the name of the power level, e.g. admin and %2 is the value that represents.",
"%1 (%2)", "%1 (%2)",
@@ -109,79 +109,63 @@ int UserListModel::rowCount(const QModelIndex &parent) const
if (parent.isValid()) { if (parent.isValid()) {
return 0; return 0;
} }
return m_users.count(); return m_members.count();
} }
bool UserListModel::event(QEvent *event) bool UserListModel::event(QEvent *event)
{ {
if (event->type() == QEvent::ApplicationPaletteChange) { if (event->type() == QEvent::ApplicationPaletteChange) {
refreshAllUsers(); refreshAllMembers();
} }
return QObject::event(event); return QObject::event(event);
} }
void UserListModel::userAdded(Quotient::User *user) void UserListModel::memberJoined(const Quotient::RoomMember &member)
{ {
auto pos = findUserPos(user); auto pos = findUserPos(member);
beginInsertRows(QModelIndex(), pos, pos); beginInsertRows(QModelIndex(), pos, pos);
m_users.insert(pos, user); m_members.insert(pos, member);
endInsertRows(); endInsertRows();
connect(user, &User::defaultAvatarChanged, this, [this, user]() {
refreshUser(user, {AvatarRole});
});
} }
void UserListModel::userRemoved(Quotient::User *user) void UserListModel::memberLeft(const Quotient::RoomMember &member)
{ {
auto pos = findUserPos(user); auto pos = findUserPos(member);
if (pos != m_users.size()) { if (pos != m_members.size()) {
beginRemoveRows(QModelIndex(), pos, pos); beginRemoveRows(QModelIndex(), pos, pos);
m_users.removeAt(pos); m_members.removeAt(pos);
endRemoveRows(); endRemoveRows();
user->disconnect(this);
} else { } else {
qWarning() << "Trying to remove a room member not in the user list"; qWarning() << "Trying to remove a room member not in the user list";
} }
} }
void UserListModel::refreshUser(Quotient::User *user, const QList<int> &roles) void UserListModel::refreshMember(const Quotient::RoomMember &member, const QList<int> &roles)
{ {
auto pos = findUserPos(user); auto pos = findUserPos(member);
if (pos != m_users.size()) { if (pos != m_members.size()) {
Q_EMIT dataChanged(index(pos), index(pos), roles); Q_EMIT dataChanged(index(pos), index(pos), roles);
} else { } else {
qWarning() << "Trying to access a room member not in the user list"; qWarning() << "Trying to access a room member not in the user list";
} }
} }
void UserListModel::refreshAllUsers() void UserListModel::refreshAllMembers()
{ {
beginResetModel(); beginResetModel();
for (User *user : std::as_const(m_users)) { m_members.clear();
user->disconnect(this);
}
m_users.clear();
if (m_currentRoom != nullptr) { if (m_currentRoom != nullptr) {
m_users = m_currentRoom->users(); m_members = m_currentRoom->members();
std::sort(m_users.begin(), m_users.end(), m_currentRoom->memberSorter()); std::sort(m_members.begin(), m_members.end(), m_currentRoom->memberSorter());
for (User *user : std::as_const(m_users)) {
connect(user, &User::defaultAvatarChanged, this, [this, user]() {
refreshUser(user, {AvatarRole});
});
}
connect(m_currentRoom->connection(), &Connection::loggedOut, this, [this]() {
setRoom(nullptr);
});
} }
endResetModel(); endResetModel();
Q_EMIT usersRefreshed(); Q_EMIT usersRefreshed();
} }
int UserListModel::findUserPos(Quotient::User *user) const int UserListModel::findUserPos(const RoomMember &member) const
{ {
return findUserPos(m_currentRoom->safeMemberName(user->id())); return findUserPos(member.displayName());
} }
int UserListModel::findUserPos(const QString &username) const int UserListModel::findUserPos(const QString &username) const
@@ -189,7 +173,7 @@ int UserListModel::findUserPos(const QString &username) const
if (!m_currentRoom) { if (!m_currentRoom) {
return 0; return 0;
} }
return m_currentRoom->memberSorter().lowerBoundIndex(m_users, username); return m_currentRoom->memberSorter().lowerBoundIndex(m_members, username);
} }
QHash<int, QByteArray> UserListModel::roleNames() const QHash<int, QByteArray> UserListModel::roleNames() const

View File

@@ -56,11 +56,6 @@ public:
[[nodiscard]] NeoChatRoom *room() const; [[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room); void setRoom(NeoChatRoom *room);
/**
* @brief The user at the given index of the model.
*/
[[nodiscard]] Quotient::User *userAt(QModelIndex index) const;
/** /**
* @brief Get the given role value at the given index. * @brief Get the given role value at the given index.
* *
@@ -90,15 +85,15 @@ protected:
bool event(QEvent *event) override; bool event(QEvent *event) override;
private Q_SLOTS: private Q_SLOTS:
void userAdded(Quotient::User *user); void memberJoined(const Quotient::RoomMember &member);
void userRemoved(Quotient::User *user); void memberLeft(const Quotient::RoomMember &member);
void refreshUser(Quotient::User *user, const QList<int> &roles = {}); void refreshMember(const Quotient::RoomMember &member, const QList<int> &roles = {});
void refreshAllUsers(); void refreshAllMembers();
private: private:
QPointer<NeoChatRoom> m_currentRoom; QPointer<NeoChatRoom> m_currentRoom;
QList<Quotient::User *> m_users; QList<Quotient::RoomMember> m_members;
int findUserPos(Quotient::User *user) const; int findUserPos(const Quotient::RoomMember &member) const;
[[nodiscard]] int findUserPos(const QString &username) const; [[nodiscard]] int findUserPos(const QString &username) const;
}; };

View File

@@ -16,10 +16,15 @@
#include "spacehierarchycache.h" #include "spacehierarchycache.h"
#include <Quotient/connection.h> #include <Quotient/connection.h>
#include <Quotient/csapi/cross_signing.h>
#include <Quotient/e2ee/cryptoutils.h>
#include <Quotient/e2ee/e2ee_common.h>
#include <Quotient/jobs/basejob.h> #include <Quotient/jobs/basejob.h>
#include <Quotient/quotient_common.h> #include <Quotient/quotient_common.h>
#include <qt6keychain/keychain.h> #include <qt6keychain/keychain.h>
#include <olm/pk.h>
#include <KLocalizedString> #include <KLocalizedString>
#include <Quotient/csapi/content-repo.h> #include <Quotient/csapi/content-repo.h>
@@ -383,10 +388,7 @@ void NeoChatConnection::openOrCreateDirectChat(User *user)
return; return;
} }
} }
requestDirectChat(user); requestDirectChat(user->id());
connectSingleShot(this, &Connection::directChatAvailable, this, [=](auto room) {
room->activateEncryption();
});
} }
qsizetype NeoChatConnection::directChatNotifications() const qsizetype NeoChatConnection::directChatNotifications() const
@@ -541,4 +543,248 @@ LinkPreviewer *NeoChatConnection::previewerForLink(const QUrl &link)
return previewer; return previewer;
} }
void NeoChatConnection::setupCrossSigningKeys(const QString &password)
{
auto masterKeyPrivate = getRandom<32>();
auto masterKeyContext = makeCStruct(olm_pk_signing, olm_pk_signing_size, olm_clear_pk_signing);
QByteArray masterKeyPublic(olm_pk_signing_public_key_length(), 0);
olm_pk_signing_key_from_seed(masterKeyContext.get(),
masterKeyPublic.data(),
masterKeyPublic.length(),
masterKeyPrivate.data(),
masterKeyPrivate.viewAsByteArray().length());
auto selfSigningKeyPrivate = getRandom<32>();
auto selfSigningKeyContext = makeCStruct(olm_pk_signing, olm_pk_signing_size, olm_clear_pk_signing);
QByteArray selfSigningKeyPublic(olm_pk_signing_public_key_length(), 0);
olm_pk_signing_key_from_seed(selfSigningKeyContext.get(),
selfSigningKeyPublic.data(),
selfSigningKeyPublic.length(),
selfSigningKeyPrivate.data(),
selfSigningKeyPrivate.viewAsByteArray().length());
auto userSigningKeyPrivate = getRandom<32>();
auto userSigningKeyContext = makeCStruct(olm_pk_signing, olm_pk_signing_size, olm_clear_pk_signing);
QByteArray userSigningKeyPublic(olm_pk_signing_public_key_length(), 0);
olm_pk_signing_key_from_seed(userSigningKeyContext.get(),
userSigningKeyPublic.data(),
userSigningKeyPublic.length(),
userSigningKeyPrivate.data(),
userSigningKeyPrivate.viewAsByteArray().length());
database()->storeEncrypted("m.cross_signing.master"_ls, masterKeyPrivate.viewAsByteArray());
database()->storeEncrypted("m.cross_signing.self_signing"_ls, selfSigningKeyPrivate.viewAsByteArray());
database()->storeEncrypted("m.cross_signing.user_signing"_ls, userSigningKeyPrivate.viewAsByteArray());
auto masterKey = CrossSigningKey{
.userId = userId(),
.usage = {"master"_ls},
.keys = {{"ed25519:"_ls + QString::fromLatin1(masterKeyPublic), QString::fromLatin1(masterKeyPublic)}},
.signatures = {},
};
auto selfSigningKey = CrossSigningKey{
.userId = userId(),
.usage = {"self_signing"_ls},
.keys = {{"ed25519:"_ls + QString::fromLatin1(selfSigningKeyPublic), QString::fromLatin1(selfSigningKeyPublic)}},
};
auto userSigningKey = CrossSigningKey{
.userId = userId(),
.usage = {"user_signing"_ls},
.keys = {{"ed25519:"_ls + QString::fromLatin1(userSigningKeyPublic), QString::fromLatin1(userSigningKeyPublic)}},
};
auto selfSigningKeyJson = toJson(selfSigningKey);
selfSigningKeyJson.remove("signatures"_ls);
selfSigningKey.signatures = QJsonObject{
{userId(),
QJsonObject{{"ed25519:"_ls + QString::fromLatin1(masterKeyPublic),
QString::fromLatin1(sign(masterKeyPrivate.viewAsByteArray(), QJsonDocument(selfSigningKeyJson).toJson(QJsonDocument::Compact)))}}}};
auto userSigningKeyJson = toJson(userSigningKey);
userSigningKeyJson.remove("signatures"_ls);
userSigningKey.signatures = QJsonObject{
{userId(),
QJsonObject{{"ed25519:"_ls + QString::fromLatin1(masterKeyPublic),
QString::fromLatin1(sign(masterKeyPrivate.viewAsByteArray(), QJsonDocument(userSigningKeyJson).toJson(QJsonDocument::Compact)))}}}};
const auto encodedMasterKeyPrivate = viewAsByteArray(masterKeyPrivate).toBase64();
const auto encodedSelfSigningKeyPrivate = viewAsByteArray(selfSigningKeyPrivate).toBase64();
const auto encodedUserSigningKeyPrivate = viewAsByteArray(userSigningKeyPrivate).toBase64();
callApi<UploadCrossSigningKeysJob>(masterKey, selfSigningKey, userSigningKey, std::nullopt)
.then(
[this, encodedMasterKeyPrivate, encodedSelfSigningKeyPrivate, encodedUserSigningKeyPrivate, masterKeyPublic]() {
finishCrossSigningSetup(encodedMasterKeyPrivate,
encodedSelfSigningKeyPrivate,
encodedUserSigningKeyPrivate,
QString::fromLatin1(masterKeyPublic));
},
[this,
password,
masterKey,
selfSigningKey,
userSigningKey,
encodedMasterKeyPrivate,
encodedSelfSigningKeyPrivate,
encodedUserSigningKeyPrivate,
masterKeyPublic](const auto &job) {
callApi<UploadCrossSigningKeysJob>(masterKey,
selfSigningKey,
userSigningKey,
AuthenticationData{
.type = "m.login.password"_ls,
.session = job->jsonData()["session"_ls].toString(),
.authInfo =
QVariantHash{
{"password"_ls, password},
{"identifier"_ls,
QJsonObject{
{"type"_ls, "m.id.user"_ls},
{"user"_ls, userId()},
}},
},
})
.then(
[this, encodedMasterKeyPrivate, encodedSelfSigningKeyPrivate, encodedUserSigningKeyPrivate, masterKeyPublic]() {
finishCrossSigningSetup(encodedMasterKeyPrivate,
encodedSelfSigningKeyPrivate,
encodedUserSigningKeyPrivate,
QString::fromLatin1(masterKeyPublic));
},
[]() {
qWarning() << "Failed to setup cross-signing keys";
});
});
}
void NeoChatConnection::finishCrossSigningSetup(const QByteArray &encodedMasterKeyPrivate,
const QByteArray &encodedSelfSigningKeyPrivate,
const QByteArray &encodedUserSigningKeyPrivate,
const QString &masterKeyPublic)
{
auto key = getRandom(32);
QByteArray data = QByteArrayLiteral("\x8B\x01") + viewAsByteArray(key);
data[8] &= ~(1 << 7); // Byte 63 needs to be set to 0
data.append(std::accumulate(data.cbegin(), data.cend(), uint8_t{0}, std::bit_xor<>()));
data = base58Encode(data);
QList<QString> groups;
for (auto i = 0; i < data.size() / 4; i++) {
groups += QString::fromLatin1(data.mid(i * 4, i * 4 + 4));
}
// The key to be shown to the user
const auto formatted = groups.join(QStringLiteral(" "));
Q_EMIT showSecurityKey(formatted);
const auto identifier = QString::fromLatin1(QCryptographicHash::hash(QUuid::createUuid().toString().toLatin1(), QCryptographicHash::Sha256));
setAccountData("m.secret_storage.default_key"_ls,
{
{"key"_ls, identifier},
});
struct EncryptionData {
QString ciphertext;
QString iv;
QString mac;
};
auto encryptAccountData = [this, &key, identifier](QLatin1String info, const QByteArray &plainText) {
const auto iv = getRandom(16);
const auto &kdfKeys = hkdfSha256(byte_view_t<>(key).subspan<0, DefaultPbkdf2KeyLength>(), zeroes<32>(), asCBytes<>(info));
if (!kdfKeys.has_value()) {
qWarning() << "Key Setup: Failed to calculate HKDF" << info;
// Q_EMIT error(DecryptionError);
return EncryptionData{};
}
const auto &encrypted = aesCtr256Encrypt(plainText, kdfKeys.value().aes(), asCBytes<AesBlockSize>(iv));
if (!encrypted.has_value()) {
qWarning() << "Key Setup: Failed to encrypt test keys" << info;
// emit error(DecryptionError);
return EncryptionData{};
}
const auto &hmacResult = hmacSha256(kdfKeys.value().mac(), encrypted.value());
if (!hmacResult.has_value()) {
qWarning() << "Key Setup: Failed to calculate HMAC" << info;
// emit error(DecryptionError);
return EncryptionData{};
}
return EncryptionData{
.ciphertext = QString::fromLatin1(encrypted.value().toBase64()),
.iv = QString::fromLatin1(iv.viewAsByteArray()),
.mac = QString::fromLatin1(hmacResult.value().toBase64()),
};
};
auto testData = encryptAccountData({}, zeroedByteArray());
setAccountData("m.secret_storage.key.%1"_ls.arg(identifier),
{
{"algorithm"_ls, "m.secret_storage.v1.aes-hmac-sha2"_ls},
{"iv"_ls, testData.iv},
{"mac"_ls, testData.mac},
});
auto masterData = encryptAccountData("m.cross_signing.master"_ls, encodedMasterKeyPrivate);
setAccountData("m.cross_signing.master"_ls,
{{"encrypted"_ls,
QJsonObject{{identifier,
QJsonObject{
{"iv"_ls, masterData.iv},
{"ciphertext"_ls, masterData.ciphertext},
{"mac"_ls, masterData.mac},
}}}}});
auto selfSigningData = encryptAccountData("m.cross_signing.self_signing"_ls, encodedSelfSigningKeyPrivate);
setAccountData("m.cross_signing.self_signing"_ls,
{{"encrypted"_ls,
QJsonObject{{identifier,
QJsonObject{
{"iv"_ls, selfSigningData.iv},
{"ciphertext"_ls, selfSigningData.ciphertext},
{"mac"_ls, selfSigningData.mac},
}}}}});
auto userSigningData = encryptAccountData("m.cross_signing.user_signing"_ls, encodedUserSigningKeyPrivate);
setAccountData("m.cross_signing.user_signing"_ls,
{{"encrypted"_ls,
QJsonObject{{identifier,
QJsonObject{
{"iv"_ls, userSigningData.iv},
{"ciphertext"_ls, userSigningData.ciphertext},
{"mac"_ls, userSigningData.mac},
}}}}});
// Adding the verified master key manually so that we don't have to wait until we receive it from the server
auto query = database()->prepareQuery(QStringLiteral("INSERT INTO master_keys(userId, key, verified) VALUES (:userId, :key, :verified);"));
query.bindValue(":userId"_ls, userId());
query.bindValue(":key"_ls, masterKeyPublic);
query.bindValue(":verified"_ls, true);
database()->execute(query);
const auto selfSigningKey = database()->loadEncrypted("m.cross_signing.self_signing"_ls);
QHash<QString, QHash<QString, QJsonObject>> signatures;
auto json = QJsonObject{
{"keys"_ls,
QJsonObject{
{"ed25519:"_ls + deviceId(), edKeyForUserDevice(userId(), deviceId())},
{"curve25519:"_ls + deviceId(), curveKeyForUserDevice(userId(), deviceId())},
}},
{"algorithms"_ls, QJsonArray{"m.olm.v1.curve25519-aes-sha2"_ls, "m.megolm.v1.aes-sha2"_ls}},
{"device_id"_ls, deviceId()},
{"user_id"_ls, userId()},
};
auto signature = sign(selfSigningKey, QJsonDocument(json).toJson(QJsonDocument::Compact));
json["signatures"_ls] = QJsonObject{
{userId(),
QJsonObject{
{"ed25519:"_ls + database()->selfSigningPublicKey(), QString::fromLatin1(signature)},
}},
};
signatures[userId()][deviceId()] = json;
callApi<UploadCrossSigningSignaturesJob>(signatures).onFailure([](const auto &job) {
qWarning() << "Failed to upload self-signing signature" << job->error() << job->errorString();
});
// TODO start a key backup and store in account data
}
#include "moc_neochatconnection.cpp" #include "moc_neochatconnection.cpp"

View File

@@ -184,6 +184,8 @@ public:
LinkPreviewer *previewerForLink(const QUrl &link); LinkPreviewer *previewerForLink(const QUrl &link);
Q_INVOKABLE void setupCrossSigningKeys(const QString &password);
Q_SIGNALS: Q_SIGNALS:
void labelChanged(); void labelChanged();
void identityServerChanged(); void identityServerChanged();
@@ -196,6 +198,7 @@ Q_SIGNALS:
void passwordStatus(NeoChatConnection::PasswordStatus status); void passwordStatus(NeoChatConnection::PasswordStatus status);
void userConsentRequired(QUrl url); void userConsentRequired(QUrl url);
void badgeNotificationCountChanged(NeoChatConnection *connection, int count); void badgeNotificationCountChanged(NeoChatConnection *connection, int count);
void showSecurityKey(const QString &securityKey);
private: private:
bool m_isOnline = true; bool m_isOnline = true;
@@ -204,6 +207,10 @@ private:
ThreePIdModel *m_threePIdModel; ThreePIdModel *m_threePIdModel;
void connectSignals(); void connectSignals();
void finishCrossSigningSetup(const QByteArray &encodedMasterKeyPrivate,
const QByteArray &encodedSelfSigningKeyPrivate,
const QByteArray &encodedUserSigningKeyPrivate,
const QString &masterKeyPublic);
int m_badgeNotificationCount = 0; int m_badgeNotificationCount = 0;

View File

@@ -11,9 +11,9 @@
#include <Quotient/jobs/basejob.h> #include <Quotient/jobs/basejob.h>
#include <Quotient/quotient_common.h> #include <Quotient/quotient_common.h>
#include <Quotient/user.h>
#include <qcoro/qcorosignal.h> #include <qcoro/qcorosignal.h>
#include <Quotient/avatar.h>
#include <Quotient/connection.h> #include <Quotient/connection.h>
#include <Quotient/csapi/account-data.h> #include <Quotient/csapi/account-data.h>
#include <Quotient/csapi/directory.h> #include <Quotient/csapi/directory.h>
@@ -103,15 +103,15 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
if (this->joinState() != JoinState::Invite) { if (this->joinState() != JoinState::Invite) {
return; return;
} }
auto roomMemberEvent = currentState().get<RoomMemberEvent>(localUser()->id()); auto roomMemberEvent = currentState().get<RoomMemberEvent>(localMember().id());
QImage avatar_image; QImage avatar_image;
if (roomMemberEvent && !user(roomMemberEvent->senderId())->avatarUrl(this).isEmpty()) { if (roomMemberEvent && !member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
avatar_image = user(roomMemberEvent->senderId())->avatar(128, this); avatar_image = memberAvatar(roomMemberEvent->senderId()).get(this->connection(), 128, [] {});
} else { } else {
qWarning() << "using this room's avatar"; qWarning() << "using this room's avatar";
avatar_image = avatar(128); avatar_image = avatar(128);
} }
NotificationsManager::instance().postInviteNotification(this, displayName(), htmlSafeMemberName(roomMemberEvent->senderId()), avatar_image); NotificationsManager::instance().postInviteNotification(this, displayName(), member(roomMemberEvent->senderId()).htmlSafeDisplayName(), avatar_image);
}); });
connect(this, &Room::changed, this, [this] { connect(this, &Room::changed, this, [this] {
Q_EMIT canEncryptRoomChanged(); Q_EMIT canEncryptRoomChanged();
@@ -257,28 +257,9 @@ void NeoChatRoom::forget()
connection()->forgetRoom(id()); connection()->forgetRoom(id());
} }
QVariantList NeoChatRoom::getUsersTyping() const
{
auto users = usersTyping();
users.removeAll(localUser());
QVariantList userVariants;
for (const auto &user : users) {
if (connection()->isIgnored(user->id())) {
continue;
}
userVariants.append(QVariantMap{
{"id"_ls, user->id()},
{"avatarMediaId"_ls, user->avatarMediaId(this)},
{"displayName"_ls, user->displayname(this)},
{"display"_ls, user->name()},
});
}
return userVariants;
}
void NeoChatRoom::sendTypingNotification(bool isTyping) void NeoChatRoom::sendTypingNotification(bool isTyping)
{ {
connection()->callApi<SetTypingJob>(BackgroundRequest, localUser()->id(), id(), isTyping, 10000); connection()->callApi<SetTypingJob>(BackgroundRequest, localMember().id(), id(), isTyping, 10000);
} }
const RoomEvent *NeoChatRoom::lastEvent() const const RoomEvent *NeoChatRoom::lastEvent() const
@@ -316,7 +297,7 @@ const RoomEvent *NeoChatRoom::lastEvent() const
} }
} }
if (connection()->isIgnored(user(event->senderId()))) { if (connection()->isIgnored(event->senderId())) {
continue; continue;
} }
@@ -376,13 +357,13 @@ bool NeoChatRoom::isEventHighlighted(const RoomEvent *e) const
void NeoChatRoom::checkForHighlights(const Quotient::TimelineItem &ti) void NeoChatRoom::checkForHighlights(const Quotient::TimelineItem &ti)
{ {
auto localUserId = localUser()->id(); auto localMember = this->localMember();
if (ti->senderId() == localUserId) { if (ti->senderId() == localMember.id()) {
return; return;
} }
if (auto *e = ti.viewAs<RoomMessageEvent>()) { if (auto *e = ti.viewAs<RoomMessageEvent>()) {
const auto &text = e->plainBody(); const auto &text = e->plainBody();
if (text.contains(localUserId) || text.contains(safeMemberName(localUserId))) { if (text.contains(localMember.id()) || text.contains(localMember.disambiguatedName())) {
highlights.insert(e); highlights.insert(e);
} }
} }
@@ -428,40 +409,6 @@ QDateTime NeoChatRoom::lastActiveTime()
return messageEvents().rbegin()->get()->originTimestamp(); return messageEvents().rbegin()->get()->originTimestamp();
} }
// An empty user is useful for returning as a model value to avoid properties being undefined.
static const QVariantMap emptyUser = {
{"isLocalUser"_ls, false},
{"id"_ls, QString()},
{"displayName"_ls, QString()},
{"avatarSource"_ls, QUrl()},
{"avatarMediaId"_ls, QString()},
{"color"_ls, QColor()},
{"object"_ls, QVariant()},
};
QVariantMap NeoChatRoom::getUser(const QString &userID) const
{
return getUser(user(userID));
}
QVariantMap NeoChatRoom::getUser(User *user) const
{
if (user == nullptr) {
return emptyUser;
}
return QVariantMap{
{QStringLiteral("isLocalUser"), user->id() == localUser()->id()},
{QStringLiteral("id"), user->id()},
{QStringLiteral("displayName"), user->displayname(this)},
{QStringLiteral("escapedDisplayName"), htmlSafeMemberName(user->id())},
{QStringLiteral("avatarSource"), avatarForMember(user)},
{QStringLiteral("avatarMediaId"), user->avatarMediaId(this)},
{QStringLiteral("color"), Utils::getUserColor(user->hueF())},
{QStringLiteral("object"), QVariant::fromValue(user)},
};
}
QString NeoChatRoom::avatarMediaId() const QString NeoChatRoom::avatarMediaId() const
{ {
if (const auto avatar = Room::avatarMediaId(); !avatar.isEmpty()) { if (const auto avatar = Room::avatarMediaId(); !avatar.isEmpty()) {
@@ -469,10 +416,10 @@ QString NeoChatRoom::avatarMediaId() const
} }
// Use the first (excluding self) user's avatar for direct chats // Use the first (excluding self) user's avatar for direct chats
const auto dcUsers = directChatUsers(); const auto directChatMembers = this->directChatMembers();
for (const auto u : dcUsers) { for (const auto member : directChatMembers) {
if (u != localUser()) { if (member != localMember()) {
return u->avatarMediaId(this); return member.avatarMediaId();
} }
} }
@@ -639,7 +586,7 @@ void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction
continue; continue;
} }
if (e->senderId() == localUser()->id()) { if (e->senderId() == localMember().id()) {
redactEventIds.push_back(e->id()); redactEventIds.push_back(e->id());
break; break;
} }
@@ -668,7 +615,7 @@ bool NeoChatRoom::canSendEvent(const QString &eventType) const
return false; return false;
} }
auto pl = plEvent->powerLevelForEvent(eventType); auto pl = plEvent->powerLevelForEvent(eventType);
auto currentPl = plEvent->powerLevelForUser(localUser()->id()); auto currentPl = plEvent->powerLevelForUser(localMember().id());
return currentPl >= pl; return currentPl >= pl;
} }
@@ -680,7 +627,7 @@ bool NeoChatRoom::canSendState(const QString &eventType) const
return false; return false;
} }
auto pl = plEvent->powerLevelForState(eventType); auto pl = plEvent->powerLevelForState(eventType);
auto currentPl = plEvent->powerLevelForUser(localUser()->id()); auto currentPl = plEvent->powerLevelForUser(localMember().id());
return currentPl >= pl; return currentPl >= pl;
} }
@@ -858,7 +805,7 @@ void NeoChatRoom::setUrlPreviewEnabled(const bool &urlPreviewEnabled)
* "type": "org.matrix.room.preview_urls", * "type": "org.matrix.room.preview_urls",
* } * }
*/ */
connection()->callApi<SetAccountDataPerRoomJob>(localUser()->id(), connection()->callApi<SetAccountDataPerRoomJob>(localMember().id(),
id(), id(),
"org.matrix.room.preview_urls"_ls, "org.matrix.room.preview_urls"_ls,
QJsonObject{{"disable"_ls, !urlPreviewEnabled}}); QJsonObject{{"disable"_ls, !urlPreviewEnabled}});
@@ -1526,7 +1473,7 @@ void NeoChatRoom::editLastMessage()
} }
// check if the current message's sender's id is same as the user's id // check if the current message's sender's id is same as the user's id
if ((*it)->senderId() == localUser()->id()) { if ((*it)->senderId() == localMember().id()) {
auto content = (*it)->contentJson(); auto content = (*it)->contentJson();
if (e->msgtype() != MessageEventType::Unknown) { if (e->msgtype() != MessageEventType::Unknown) {
@@ -1651,13 +1598,9 @@ int NeoChatRoom::maxRoomVersion() const
return maxVersion; return maxVersion;
} }
Quotient::User *NeoChatRoom::directChatRemoteUser() const Quotient::RoomMember NeoChatRoom::directChatRemoteMember() const
{ {
auto users = connection()->directChatUsers(this); return directChatMembers()[0];
if (users.isEmpty()) {
return nullptr;
}
return users[0];
} }
void NeoChatRoom::sendLocation(float lat, float lon, const QString &description) void NeoChatRoom::sendLocation(float lat, float lon, const QString &description)
@@ -1689,20 +1632,6 @@ QByteArray NeoChatRoom::roomAcountDataJson(const QString &eventType)
return QJsonDocument(accountData(eventType)->fullJson()).toJson(); return QJsonDocument(accountData(eventType)->fullJson()).toJson();
} }
QUrl NeoChatRoom::avatarForMember(Quotient::User *user) const
{
const auto &url = memberAvatarUrl(user->id());
if (url.isEmpty() || url.scheme() != "mxc"_ls) {
return {};
}
auto avatar = connection()->makeMediaUrl(url);
if (avatar.isValid() && avatar.scheme() == QStringLiteral("mxc")) {
return avatar;
} else {
return QUrl();
}
}
void NeoChatRoom::downloadEventFromServer(const QString &eventId) void NeoChatRoom::downloadEventFromServer(const QString &eventId)
{ {
if (findInTimeline(eventId) != historyEdge()) { if (findInTimeline(eventId) != historyEdge()) {
@@ -1774,10 +1703,9 @@ void NeoChatRoom::cleanupExtraEvent(const QString &eventId)
m_extraEvents.erase(it); m_extraEvents.erase(it);
} }
} }
QString NeoChatRoom::invitingUserId() const
User *NeoChatRoom::invitingUser() const
{ {
return connection()->user(currentState().get<RoomMemberEvent>(connection()->userId())->senderId()); return currentState().get<RoomMemberEvent>(connection()->userId())->senderId();
} }
void NeoChatRoom::setRoomState(const QString &type, const QString &stateKey, const QByteArray &content) void NeoChatRoom::setRoomState(const QString &type, const QString &stateKey, const QByteArray &content)

View File

@@ -10,7 +10,7 @@
#include <QQmlEngine> #include <QQmlEngine>
#include <QCoroTask> #include <QCoroTask>
#include <Quotient/user.h> #include <Quotient/roommember.h>
#include "enums/pushrule.h" #include "enums/pushrule.h"
#include "pollhandler.h" #include "pollhandler.h"
@@ -40,27 +40,6 @@ class NeoChatRoom : public Quotient::Room
QML_ELEMENT QML_ELEMENT
QML_UNCREATABLE("") QML_UNCREATABLE("")
/**
* @brief A list of users currently typing in the room.
*
* The list does not include the local user.
*
* This is different to getting a list of Quotient::User objects
* as neither of those can provide details like the displayName or avatarMediaId
* without the room context as these can vary from room to room. This function
* provides the room context and puts the result as a list of QVariantMap objects.
*
* @return a QVariantMap for the user with the following
* parameters:
* - id - User ID.
* - avatarMediaId - Avatar id in the context of this room.
* - displayName - Display name in the context of this room.
* - display - Name in the context of this room.
*
* @sa Quotient::User
*/
Q_PROPERTY(QVariantList usersTyping READ getUsersTyping NOTIFY typingChanged)
/** /**
* @brief Convenience function to get the QDateTime of the last event. * @brief Convenience function to get the QDateTime of the last event.
* *
@@ -93,9 +72,9 @@ class NeoChatRoom : public Quotient::Room
Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
/** /**
* @brief Get a user object for the other person in a direct chat. * @brief Get a RoomMember object for the other person in a direct chat.
*/ */
Q_PROPERTY(Quotient::User *directChatRemoteUser READ directChatRemoteUser CONSTANT) Q_PROPERTY(Quotient::RoomMember directChatRemoteMember READ directChatRemoteMember CONSTANT)
/** /**
* @brief The Matrix IDs of this room's parents. * @brief The Matrix IDs of this room's parents.
@@ -235,60 +214,6 @@ public:
explicit NeoChatRoom(Quotient::Connection *connection, QString roomId, Quotient::JoinState joinState = {}); explicit NeoChatRoom(Quotient::Connection *connection, QString roomId, Quotient::JoinState joinState = {});
/**
* @brief Get a user in the context of this 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
* provides the room context and outputs the result as QVariantMap.
*
* Can be called with an empty QString to return an empty user, which is a useful return
* from models to avoid undefined properties.
*
* @param userID the ID of the user to output.
*
* @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
*/
Q_INVOKABLE [[nodiscard]] QVariantMap getUser(const QString &userID) const;
/**
* @brief Get a user in the context of this 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
* provides the room context and outputs the result as QVariantMap.
*
* Can be called with a nullptr to return an empty user, which is a useful return
* from models to avoid undefined properties.
*
* @param user the user to output.
*
* @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
*/
Q_INVOKABLE [[nodiscard]] QVariantMap getUser(Quotient::User *user) const;
[[nodiscard]] QVariantList getUsersTyping() const;
[[nodiscard]] QDateTime lastActiveTime(); [[nodiscard]] QDateTime lastActiveTime();
/** /**
@@ -400,7 +325,7 @@ public:
[[nodiscard]] QString avatarMediaId() const; [[nodiscard]] QString avatarMediaId() const;
Quotient::User *directChatRemoteUser() const; Quotient::RoomMember directChatRemoteMember() const;
/** /**
* @brief Whether this room has one or more parent spaces set. * @brief Whether this room has one or more parent spaces set.
@@ -630,8 +555,6 @@ public:
*/ */
Q_INVOKABLE QByteArray roomAcountDataJson(const QString &eventType); Q_INVOKABLE QByteArray roomAcountDataJson(const QString &eventType);
Q_INVOKABLE [[nodiscard]] QUrl avatarForMember(Quotient::User *user) const;
/** /**
* @brief Loads the event with the given id from the server and saves it locally. * @brief Loads the event with the given id from the server and saves it locally.
* *
@@ -660,7 +583,7 @@ public:
/** /**
* If we're invited to this room, the user that invited us. Undefined in other cases. * If we're invited to this room, the user that invited us. Undefined in other cases.
*/ */
Q_INVOKABLE Quotient::User *invitingUser() const; Q_INVOKABLE QString invitingUserId() const;
private: private:
QSet<const Quotient::RoomEvent *> highlights; QSet<const Quotient::RoomEvent *> highlights;

View File

@@ -109,7 +109,7 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
auto room = connection->room(notification["room_id"_ls].toString()); auto room = connection->room(notification["room_id"_ls].toString());
if (shouldPostNotification(connection, n)) { if (shouldPostNotification(connection, n)) {
// The room might have been deleted (for example rejected invitation). // The room might have been deleted (for example rejected invitation).
auto sender = room->user(notification["event"_ls].toObject()["sender"_ls].toString()); auto sender = room->member(notification["event"_ls].toObject()["sender"_ls].toString());
QString body; QString body;
@@ -133,13 +133,13 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
} }
QImage avatar_image; QImage avatar_image;
if (!sender->avatarUrl(room).isEmpty()) { if (!sender.avatarUrl().isEmpty()) {
avatar_image = sender->avatar(128, room); avatar_image = room->memberAvatar(sender.id()).get(connection, 128, {});
} else { } else {
avatar_image = room->avatar(128); avatar_image = room->avatar(128);
} }
postNotification(dynamic_cast<NeoChatRoom *>(room), postNotification(dynamic_cast<NeoChatRoom *>(room),
sender->displayname(room), sender.displayName(),
body, body,
avatar_image, avatar_image,
notification["event"_ls].toObject()["event_id"_ls].toString(), notification["event"_ls].toObject()["event_id"_ls].toString(),
@@ -213,7 +213,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
if (!room) { if (!room) {
return; return;
} }
auto connection = dynamic_cast<NeoChatConnection *>(Controller::instance().accounts().get(room->localUser()->id())); auto connection = dynamic_cast<NeoChatConnection *>(Controller::instance().accounts().get(room->localMember().id()));
Controller::instance().setActiveConnection(connection); Controller::instance().setActiveConnection(connection);
RoomManager::instance().setConnection(connection); RoomManager::instance().setConnection(connection);
RoomManager::instance().resolveResource(room->id()); RoomManager::instance().resolveResource(room->id());
@@ -230,7 +230,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
notification->setReplyAction(std::move(replyAction)); notification->setReplyAction(std::move(replyAction));
} }
notification->setHint(QStringLiteral("x-kde-origin-name"), room->localUser()->id()); notification->setHint(QStringLiteral("x-kde-origin-name"), room->localMember().id());
notification->sendEvent(); notification->sendEvent();
} }
@@ -276,7 +276,7 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *rawRoom, const QS
return; return;
} }
RoomManager::instance().leaveRoom(room); RoomManager::instance().leaveRoom(room);
room->connection()->addToIgnoredUsers(room->invitingUser()); room->connection()->addToIgnoredUsers(room->invitingUserId());
notification->close(); notification->close();
}); });
connect(notification, &KNotification::closed, this, [this, room]() { connect(notification, &KNotification::closed, this, [this, room]() {
@@ -286,7 +286,7 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *rawRoom, const QS
m_invitations.remove(room->id()); m_invitations.remove(room->id());
}); });
notification->setHint(QStringLiteral("x-kde-origin-name"), room->localUser()->id()); notification->setHint(QStringLiteral("x-kde-origin-name"), room->localMember().id());
notification->sendEvent(); notification->sendEvent();
m_invitations.insert(room->id(), notification); m_invitations.insert(room->id(), notification);

View File

@@ -154,7 +154,7 @@ void PollHandler::sendPollAnswer(const QString &eventId, const QString &answerId
return; return;
} }
QStringList ownAnswers; QStringList ownAnswers;
for (const auto &answer : m_answers[room->localUser()->id()].toArray()) { for (const auto &answer : m_answers[room->localMember().id()].toArray()) {
ownAnswers += answer.toString(); ownAnswers += answer.toString();
} }
if (ownAnswers.contains(answerId)) { if (ownAnswers.contains(answerId)) {
@@ -169,7 +169,7 @@ void PollHandler::sendPollAnswer(const QString &eventId, const QString &answerId
} }
auto response = new PollResponseEvent(eventId, ownAnswers); auto response = new PollResponseEvent(eventId, ownAnswers);
handleAnswer(response->contentJson(), room->localUser()->id(), QDateTime::currentDateTime()); handleAnswer(response->contentJson(), room->localMember().id(), QDateTime::currentDateTime());
room->postEvent(response); room->postEvent(response);
} }

View File

@@ -70,6 +70,11 @@ QQC2.Menu {
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog'), {}, { onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog'), {}, {
title: i18nc("@title:window", "Open Key Backup") title: i18nc("@title:window", "Open Key Backup")
}) })
}
QQC2.MenuItem {
text: i18nc("@action:inmenu", "Verify this Device")
icon.name: "security-low"
onTriggered: root.connection.startSelfVerification()
enabled: Controller.ssssSupported enabled: Controller.ssssSupported
} }
QQC2.MenuItem { QQC2.MenuItem {

View File

@@ -49,7 +49,7 @@ Loader {
text: room.isDirectChat() ? i18nc("@action:inmenu", "Copy user's Matrix ID to Clipboard") : i18nc("@action:inmenu", "Copy Address to Clipboard") text: room.isDirectChat() ? i18nc("@action:inmenu", "Copy user's Matrix ID to Clipboard") : i18nc("@action:inmenu", "Copy Address to Clipboard")
icon.name: "edit-copy" icon.name: "edit-copy"
onTriggered: if (room.isDirectChat()) { onTriggered: if (room.isDirectChat()) {
Clipboard.saveText(room.directChatRemoteUser.id); Clipboard.saveText(room.directChatRemoteMember.id);
} else if (room.canonicalAlias.length === 0) { } else if (room.canonicalAlias.length === 0) {
Clipboard.saveText(room.id); Clipboard.saveText(room.id);
} else { } else {

View File

@@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.neochat
Kirigami.PromptDialog {
id: root
required property NeoChatConnection connection
title: i18nc("@title:dialog", "Setup Encryption Keys")
dialogType: Kirigami.PromptDialog.Information
onRejected: {
root.close();
}
footer: QQC2.DialogButtonBox {
standardButtons: QQC2.Dialog.Cancel
QQC2.Button {
text: i18nc("@action:button", "Setup Keys")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
onClicked: {
root.connection.setupCrossSigningKeys("password123");
}
}
}
}

View File

@@ -40,18 +40,9 @@ Loader {
/** /**
* @brief The message author. * @brief The message author.
* *
* This should consist of the following: * A Quotient::RoomMember object.
* - id - The matrix ID of the author.
* - isLocalUser - Whether the author is the local user.
* - avatarSource - The mxc URL for the author's avatar in the current room.
* - avatarMediaId - The media ID of the author's avatar.
* - avatarUrl - The mxc URL for the author's avatar.
* - displayName - The display name of the author.
* - display - The name of the author.
* - color - The color for the author.
* - object - The Quotient::User object for the author.
* *
* @sa Quotient::User * @sa Quotient::RoomMember
*/ */
required property var author required property var author
@@ -90,7 +81,7 @@ Loader {
} }
component RemoveMessageAction: Kirigami.Action { component RemoveMessageAction: Kirigami.Action {
visible: author.isLocalUser || currentRoom.canSendState("redact") visible: author.isLocalMember || currentRoom.canSendState("redact")
text: i18n("Remove") text: i18n("Remove")
icon.name: "edit-delete-remove" icon.name: "edit-delete-remove"
icon.color: "red" icon.color: "red"
@@ -116,7 +107,7 @@ Loader {
component ReportMessageAction: Kirigami.Action { component ReportMessageAction: Kirigami.Action {
text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report") text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
icon.name: "dialog-warning-symbolic" icon.name: "dialog-warning-symbolic"
visible: !author.isLocalUser visible: !author.isLocalMember
onTriggered: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReportSheet'), { onTriggered: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReportSheet'), {
room: currentRoom, room: currentRoom,
eventId: eventId eventId: eventId

View File

@@ -33,7 +33,7 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
onClicked: { onClicked: {
RoomManager.resolveResource(root.room.directChatRemoteUser.id, "mention"); RoomManager.resolveResource(root.room.directChatRemoteMember.uri)
} }
contentItem: KirigamiComponents.Avatar { contentItem: KirigamiComponents.Avatar {
@@ -59,12 +59,27 @@ ColumnLayout {
} }
} }
Kirigami.Heading { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
type: Kirigami.Heading.Type.Primary Layout.alignment: Qt.AlignHCenter
wrapMode: QQC2.Label.Wrap Kirigami.Icon {
text: root.room.displayName id: securityIcon
textFormat: Text.PlainText //TODO figure out how to make this update
horizontalAlignment: Text.AlignHCenter source: room.connection.isUserVerified(root.room.directChatRemoteMember.id) ?
(room.connection.allSessionsSelfVerified(root.room.directChatRemoteMember.id) ? "security-high" : "security-medium")
: "security-low"
}
Kirigami.Heading {
type: Kirigami.Heading.Type.Primary
wrapMode: QQC2.Label.Wrap
text: root.room.displayName
textFormat: Text.PlainText
horizontalAlignment: Text.AlignHCenter
}
Item {
Layout.preferredWidth: visible ? securityIcon.width : 0
visible: securityIcon.visible
}
} }
} }

View File

@@ -63,7 +63,7 @@ DelegateContextMenu {
} }
}, },
Kirigami.Action { Kirigami.Action {
visible: author.id === currentRoom.localUser.id || currentRoom.canSendState("redact") visible: author.id === currentRoom.localMember.id || currentRoom.canSendState("redact")
text: i18n("Remove") text: i18n("Remove")
icon.name: "edit-delete-remove" icon.name: "edit-delete-remove"
icon.color: "red" icon.color: "red"

View File

@@ -22,7 +22,7 @@ Kirigami.PlaceholderMessage {
onClicked: { onClicked: {
RoomManager.leaveRoom(root.currentRoom); RoomManager.leaveRoom(root.currentRoom);
root.currentRoom.connection.addToIgnoredUsers(root.currentRoom.invitingUser()); root.currentRoom.connection.addToIgnoredUsers(root.currentRoom.invitingUserId());
} }
} }
QQC2.Button { QQC2.Button {

View File

@@ -55,7 +55,7 @@ MapQuickItem {
width: height width: height
height: parent.height / 3 + 1 height: parent.height / 3 + 1
name: root.author.displayName name: root.author.displayName
source: root.author.avatarSource source: root.author.avatarUrl
color: root.author.color color: root.author.color
} }

View File

@@ -309,6 +309,12 @@ Kirigami.ApplicationWindow {
url: url url: url
}).open(); }).open();
} }
function onCrossSigningSetupRequired() {
Qt.createComponent("org.kde.neochat", "CrossSigningSetupDialog").createObject(this, {
connection: root.connection
}).open();
}
} }
HoverLinkIndicator { HoverLinkIndicator {
@@ -354,7 +360,7 @@ Kirigami.ApplicationWindow {
function showUserDetail(user) { function showUserDetail(user) {
Qt.createComponent("org.kde.neochat", "UserDetailDialog").createObject(root.QQC2.ApplicationWindow.window, { Qt.createComponent("org.kde.neochat", "UserDetailDialog").createObject(root.QQC2.ApplicationWindow.window, {
room: RoomManager.currentRoom ? RoomManager.currentRoom : null, room: RoomManager.currentRoom ? RoomManager.currentRoom : null,
user: RoomManager.currentRoom ? RoomManager.currentRoom.getUser(user.id) : QmlUtils.getUser(user), user: user,
connection: root.connection connection: root.connection
}).open(); }).open();
} }

View File

@@ -40,7 +40,7 @@ DelegateContextMenu {
currentRoom.editCache.editId = eventId; currentRoom.editCache.editId = eventId;
currentRoom.mainCache.replyId = ""; currentRoom.mainCache.replyId = "";
} }
visible: author.isLocalUser && (root.messageComponentType === MessageComponentType.Emote || root.messageComponentType === MessageComponentType.Message) visible: author.isLocalMember && (root.messageComponentType === MessageComponentType.Emote || root.messageComponentType === MessageComponentType.Message)
}, },
DelegateContextMenu.ReplyMessageAction {}, DelegateContextMenu.ReplyMessageAction {},
Kirigami.Action { Kirigami.Action {

View File

@@ -89,6 +89,16 @@ QQC2.ScrollView {
} }
} }
Delegates.RoundedItemDelegate {
visible: root.room.isDirectChat()
icon.name: "security-low-symbolic"
text: i18nc("@action:button", "Verify user")
onClicked: root.room.startVerification()
Layout.fillWidth: true
}
Delegates.RoundedItemDelegate { Delegates.RoundedItemDelegate {
id: favouriteButton id: favouriteButton
visible: !root.room.isSpace visible: !root.room.isSpace
@@ -219,7 +229,7 @@ QQC2.ScrollView {
onClicked: { onClicked: {
userDelegate.highlighted = true; userDelegate.highlighted = true;
RoomManager.resolveResource(userDelegate.userId, "mention"); RoomManager.resolveResource(root.room.member(userDelegate.userId).uri)
} }
contentItem: RowLayout { contentItem: RowLayout {

View File

@@ -263,8 +263,8 @@ QQC2.ScrollView {
TypingPane { TypingPane {
id: typingPane id: typingPane
visible: root.currentRoom && root.currentRoom.usersTyping.length > 0 visible: root.currentRoom && root.currentRoom.otherMembersTyping.length > 0
labelText: visible ? i18ncp("Message displayed when some users are typing", "%2 is typing", "%2 are typing", root.currentRoom.usersTyping.length, root.currentRoom.usersTyping.map(user => user.displayName).join(", ")) : "" labelText: visible ? i18ncp("Message displayed when some users are typing", "%2 is typing", "%2 are typing", root.currentRoom.otherMembersTyping.length, root.currentRoom.otherMembersTyping.map(member => member.displayName).join(", ")) : ""
anchors.left: parent.left anchors.left: parent.left
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
height: visible ? implicitHeight : 0 height: visible ? implicitHeight : 0

View File

@@ -19,8 +19,21 @@ Kirigami.Dialog {
// This dialog is sometimes used outside the context of a room, e.g., when scanning a user's QR code. // This dialog is sometimes used outside the context of a room, e.g., when scanning a user's QR code.
// Make sure that code is prepared to deal with this property being null // Make sure that code is prepared to deal with this property being null
property NeoChatRoom room property NeoChatRoom room
/**
* @brief The user's profile object.
*
* Required to interact with the profile and perform action like ignoring.
*/
property var user property var user
/**
* @brief The RoomMember object for the user in the current room.
*
* Required to visualise the user.
*/
property var member: root.room.member(user.id)
property NeoChatConnection connection property NeoChatConnection connection
leftPadding: 0 leftPadding: 0
@@ -48,9 +61,9 @@ Kirigami.Dialog {
Layout.preferredWidth: Kirigami.Units.iconSizes.huge Layout.preferredWidth: Kirigami.Units.iconSizes.huge
Layout.preferredHeight: Kirigami.Units.iconSizes.huge Layout.preferredHeight: Kirigami.Units.iconSizes.huge
name: root.user.displayName name: root.member.displayName
source: root.user.avatarSource source: root.member.avatarUrl
color: root.user.color color: root.member.color
} }
ColumnLayout { ColumnLayout {
@@ -69,7 +82,7 @@ Kirigami.Dialog {
Kirigami.SelectableLabel { Kirigami.SelectableLabel {
textFormat: TextEdit.PlainText textFormat: TextEdit.PlainText
text: root.user.id text: root.member.id
} }
} }
QQC2.AbstractButton { QQC2.AbstractButton {
@@ -78,16 +91,16 @@ Kirigami.Dialog {
contentItem: Barcode { contentItem: Barcode {
id: barcode id: barcode
barcodeType: Barcode.QRCode barcodeType: Barcode.QRCode
content: "https://matrix.to/#/" + root.user.id content: "https://matrix.to/#/" + root.member.id
} }
onClicked: { onClicked: {
let map = qrMaximizeComponent.createObject(parent, { let map = qrMaximizeComponent.createObject(parent, {
text: barcode.content, text: barcode.content,
title: root.user.displayName, title: root.member.displayName,
subtitle: root.user.id, subtitle: root.member.id,
avatarColor: root.user.color, avatarColor: root.member.color,
avatarSource: root.user.avatarSource avatarSource: root.member.avatarUrl,
}); });
root.close(); root.close();
map.open(); map.open();
@@ -104,46 +117,46 @@ Kirigami.Dialog {
} }
FormCard.FormButtonDelegate { FormCard.FormButtonDelegate {
visible: !root.user.isLocalUser && !!root.user.object visible: !root.member.isLocalMember
action: Kirigami.Action { action: Kirigami.Action {
text: !!root.user.object && root.connection.isIgnored(root.user.object) ? i18n("Unignore this user") : i18n("Ignore this user") text: !!root.user && root.connection.isIgnored(root.user) ? i18n("Unignore this user") : i18n("Ignore this user")
icon.name: "im-invisible-user" icon.name: "im-invisible-user"
onTriggered: { onTriggered: {
root.close(); root.close();
root.connection.isIgnored(root.user.object) ? root.connection.removeFromIgnoredUsers(root.user.object) : root.connection.addToIgnoredUsers(root.user.object); root.connection.isIgnored(root.user) ? root.connection.removeFromIgnoredUsers(root.user) : root.connection.addToIgnoredUsers(root.user);
} }
} }
} }
FormCard.FormButtonDelegate { FormCard.FormButtonDelegate {
visible: root.room && !root.user.isLocalUser && room.canSendState("kick") && room.containsUser(root.user.id) && room.getUserPowerLevel(root.user.id) < room.getUserPowerLevel(root.connection.localUser.id) visible: root.room && !root.member.isLocalMember && room.canSendState("kick") && room.containsUser(root.member.id) && room.getUserPowerLevel(root.member.id) < room.getUserPowerLevel(root.room.localMember.id)
action: Kirigami.Action { action: Kirigami.Action {
text: i18n("Kick this user") text: i18n("Kick this user")
icon.name: "im-kick-user" icon.name: "im-kick-user"
onTriggered: { onTriggered: {
root.room.kickMember(root.user.id); root.room.kickMember(root.member.id);
root.close(); root.close();
} }
} }
} }
FormCard.FormButtonDelegate { FormCard.FormButtonDelegate {
visible: root.room && !root.user.isLocalUser && room.canSendState("invite") && !room.containsUser(root.user.id) visible: root.room && !root.member.isLocalMember && room.canSendState("invite") && !room.containsUser(root.member.id)
action: Kirigami.Action { action: Kirigami.Action {
enabled: root.room && !root.room.isUserBanned(root.user.id) enabled: root.room && !root.room.isUserBanned(root.member.id)
text: i18n("Invite this user") text: i18n("Invite this user")
icon.name: "list-add-user" icon.name: "list-add-user"
onTriggered: { onTriggered: {
root.room.inviteToRoom(root.user.id); root.room.inviteToRoom(root.member.id);
root.close(); root.close();
} }
} }
} }
FormCard.FormButtonDelegate { FormCard.FormButtonDelegate {
visible: root.room && !root.user.isLocalUser && room.canSendState("ban") && !room.isUserBanned(root.user.id) && room.getUserPowerLevel(root.user.id) < room.getUserPowerLevel(root.room.connection.localUser.id) visible: root.room && !root.member.isLocalMember && room.canSendState("ban") && !room.isUserBanned(root.member.id) && room.getUserPowerLevel(root.member.id) < room.getUserPowerLevel(root.room.localMember.id)
action: Kirigami.Action { action: Kirigami.Action {
text: i18n("Ban this user") text: i18n("Ban this user")
@@ -152,7 +165,7 @@ Kirigami.Dialog {
onTriggered: { onTriggered: {
(root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'BanSheet'), { (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'BanSheet'), {
room: root.room, room: root.room,
userId: root.user.id userId: root.member.id
}, { }, {
title: i18nc("@title", "Ban User"), title: i18nc("@title", "Ban User"),
width: Kirigami.Units.gridUnit * 25 width: Kirigami.Units.gridUnit * 25
@@ -163,14 +176,14 @@ Kirigami.Dialog {
} }
FormCard.FormButtonDelegate { FormCard.FormButtonDelegate {
visible: root.room && !root.user.isLocalUser && room.canSendState("ban") && room.isUserBanned(root.user.id) visible: root.room && !root.member.isLocalMember && room.canSendState("ban") && room.isUserBanned(root.member.id)
action: Kirigami.Action { action: Kirigami.Action {
text: i18n("Unban this user") text: i18n("Unban this user")
icon.name: "im-irc" icon.name: "im-irc"
icon.color: Kirigami.Theme.negativeTextColor icon.color: Kirigami.Theme.negativeTextColor
onTriggered: { onTriggered: {
root.room.unban(root.user.id); root.room.unban(root.member.id);
root.close(); root.close();
} }
} }
@@ -184,8 +197,8 @@ Kirigami.Dialog {
onTriggered: { onTriggered: {
let dialog = powerLevelDialog.createObject(this, { let dialog = powerLevelDialog.createObject(this, {
room: root.room, room: root.room,
userId: root.user.id, userId: root.member.id,
powerLevel: root.room.getUserPowerLevel(root.user.id) powerLevel: root.room.getUserPowerLevel(root.member.id)
}); });
dialog.open(); dialog.open();
root.close(); root.close();
@@ -201,7 +214,7 @@ Kirigami.Dialog {
} }
FormCard.FormButtonDelegate { FormCard.FormButtonDelegate {
visible: root.room && (root.user.isLocalUser || room.canSendState("redact")) visible: root.room && (root.member.isLocalUser || room.canSendState("redact"))
action: Kirigami.Action { action: Kirigami.Action {
text: i18n("Remove recent messages by this user") text: i18n("Remove recent messages by this user")
@@ -210,7 +223,7 @@ Kirigami.Dialog {
onTriggered: { onTriggered: {
applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RemoveSheet'), { applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RemoveSheet'), {
room: root.room, room: root.room,
userId: root.user.id userId: root.member.id
}, { }, {
title: i18nc("@title", "Remove Messages"), title: i18nc("@title", "Remove Messages"),
width: Kirigami.Units.gridUnit * 25 width: Kirigami.Units.gridUnit * 25
@@ -221,12 +234,12 @@ Kirigami.Dialog {
} }
FormCard.FormButtonDelegate { FormCard.FormButtonDelegate {
visible: !root.user.isLocalUser visible: !root.member.isLocalMember
action: Kirigami.Action { action: Kirigami.Action {
text: root.connection.directChatExists(root.user.object) ? i18nc("%1 is the name of the user.", "Chat with %1", root.user.escapedDisplayName) : i18n("Invite to private chat") text: root.connection.directChatExists(root.user.object) ? i18nc("%1 is the name of the user.", "Chat with %1", root.user.escapedDisplayName) : i18n("Invite to private chat")
icon.name: "document-send" icon.name: "document-send"
onTriggered: { onTriggered: {
root.connection.openOrCreateDirectChat(root.user.object); root.room.connection.openOrCreateDirectChat(root.user);
root.close(); root.close();
} }
} }
@@ -237,7 +250,7 @@ Kirigami.Dialog {
text: i18n("Copy link") text: i18n("Copy link")
icon.name: "username-copy" icon.name: "username-copy"
onTriggered: { onTriggered: {
Clipboard.saveText("https://matrix.to/#/" + root.user.id); Clipboard.saveText("https://matrix.to/#/" + root.member.id);
} }
} }
} }

View File

@@ -63,7 +63,7 @@ QString Registration::recaptchaSiteKey() const
void Registration::registerAccount() void Registration::registerAccount()
{ {
setStatus(Working); setStatus(Working);
Omittable<QJsonObject> authData = none; std::optional<QJsonObject> authData = std::nullopt;
if (nextStep() == "m.login.recaptcha"_ls) { if (nextStep() == "m.login.recaptcha"_ls) {
authData = QJsonObject{ authData = QJsonObject{
{"type"_ls, "m.login.recaptcha"_ls}, {"type"_ls, "m.login.recaptcha"_ls},
@@ -176,7 +176,7 @@ void Registration::testHomeserver()
if (m_testServerJob) { if (m_testServerJob) {
delete m_testServerJob; delete m_testServerJob;
} }
m_testServerJob = m_connection->callApi<NeoChatRegisterJob>("user"_ls, none, "user"_ls, QString(), QString(), QString(), false); m_testServerJob = m_connection->callApi<NeoChatRegisterJob>("user"_ls, std::nullopt, "user"_ls, QString(), QString(), QString(), false);
connect(m_testServerJob.data(), &BaseJob::finished, this, [this]() { connect(m_testServerJob.data(), &BaseJob::finished, this, [this]() {
if (m_testServerJob->error() == BaseJob::StatusCode::ContentAccessError) { if (m_testServerJob->error() == BaseJob::StatusCode::ContentAccessError) {
setStatus(ServerNoRegistration); setStatus(ServerNoRegistration);
@@ -244,12 +244,12 @@ void Registration::setPassword(const QString &password)
} }
NeoChatRegisterJob::NeoChatRegisterJob(const QString &kind, NeoChatRegisterJob::NeoChatRegisterJob(const QString &kind,
const Omittable<QJsonObject> &auth, const std::optional<QJsonObject> &auth,
const QString &username, const QString &username,
const QString &password, const QString &password,
const QString &deviceId, const QString &deviceId,
const QString &initialDeviceDisplayName, const QString &initialDeviceDisplayName,
Omittable<bool> inhibitLogin) std::optional<bool> inhibitLogin)
: BaseJob(HttpVerb::Post, "RegisterJob"_ls, QByteArrayLiteral("/_matrix/client/r0/register"), false) : BaseJob(HttpVerb::Post, "RegisterJob"_ls, QByteArrayLiteral("/_matrix/client/r0/register"), false)
{ {
QJsonObject _data; QJsonObject _data;

View File

@@ -27,12 +27,12 @@ class NeoChatRegisterJob : public Quotient::BaseJob
{ {
public: public:
explicit NeoChatRegisterJob(const QString &kind = QStringLiteral("user"), explicit NeoChatRegisterJob(const QString &kind = QStringLiteral("user"),
const Quotient::Omittable<QJsonObject> &auth = Quotient::none, const std::optional<QJsonObject> &auth = std::nullopt,
const QString &username = {}, const QString &username = {},
const QString &password = {}, const QString &password = {},
const QString &deviceId = {}, const QString &deviceId = {},
const QString &initialDeviceDisplayName = {}, const QString &initialDeviceDisplayName = {},
Quotient::Omittable<bool> inhibitLogin = Quotient::none); std::optional<bool> inhibitLogin = std::nullopt);
QString userId() const QString userId() const
{ {

View File

@@ -251,7 +251,6 @@ void RoomManager::openRoomForActiveConnection()
UriResolveResult RoomManager::visitUser(User *user, const QString &action) UriResolveResult RoomManager::visitUser(User *user, const QString &action)
{ {
if (action == "mention"_ls || action.isEmpty()) { if (action == "mention"_ls || action.isEmpty()) {
// send it has QVariantMap because the properties in the
user->load(); user->load();
Q_EMIT showUserDetail(user); Q_EMIT showUserDetail(user);
} else if (action == "_interactive"_ls) { } else if (action == "_interactive"_ls) {

View File

@@ -8,6 +8,7 @@
#include <QObject> #include <QObject>
#include <QQmlEngine> #include <QQmlEngine>
#include <Quotient/room.h> #include <Quotient/room.h>
#include <Quotient/roommember.h>
#include <Quotient/uriresolver.h> #include <Quotient/uriresolver.h>
#include "chatdocumenthandler.h" #include "chatdocumenthandler.h"
@@ -290,7 +291,7 @@ Q_SIGNALS:
* @brief Request to show a menu for the given event. * @brief Request to show a menu for the given event.
*/ */
void showMessageMenu(const QString &eventId, void showMessageMenu(const QString &eventId,
const QVariantMap &author, const Quotient::RoomMember &author,
MessageComponentType::Type messageComponentType, MessageComponentType::Type messageComponentType,
const QString &plainText, const QString &plainText,
const QString &htmlText, const QString &htmlText,
@@ -300,7 +301,7 @@ Q_SIGNALS:
* @brief Request to show a menu for the given media event. * @brief Request to show a menu for the given media event.
*/ */
void showFileMenu(const QString &eventId, void showFileMenu(const QString &eventId,
const QVariantMap &author, const Quotient::RoomMember &author,
MessageComponentType::Type messageComponentType, MessageComponentType::Type messageComponentType,
const QString &plainText, const QString &plainText,
const QString &mimeType, const QString &mimeType,

View File

@@ -61,8 +61,8 @@ FormCard.FormCardPage {
spacing: Kirigami.Units.largeSpacing spacing: Kirigami.Units.largeSpacing
QQC2.Label { QQC2.Label {
id: powerLevelLabel id: powerLevelLabel
visible: !room.canSendState("m.room.power_levels") || (room.getUserPowerLevel(room.localUser.id) <= privilegedUserDelegate.powerLevel && privilegedUserDelegate.userId != room.localUser.id)
text: privilegedUserDelegate.powerLevelString text: privilegedUserDelegate.powerLevelString
visible: !room.canSendState("m.room.power_levels") || (room.getUserPowerLevel(room.localMember.id) <= privilegedUserDelegate.powerLevel && privilegedUserDelegate.userId != room.localMember.id)
color: Kirigami.Theme.disabledTextColor color: Kirigami.Theme.disabledTextColor
} }
QQC2.ComboBox { QQC2.ComboBox {

View File

@@ -57,7 +57,7 @@ void SpaceHierarchyCache::populateSpaceHierarchy(const QString &spaceId)
} }
m_nextBatchTokens[spaceId] = QString(); m_nextBatchTokens[spaceId] = QString();
auto job = m_connection->callApi<GetSpaceHierarchyJob>(spaceId, none, none, none, *m_nextBatchTokens[spaceId]); auto job = m_connection->callApi<GetSpaceHierarchyJob>(spaceId, std::nullopt, std::nullopt, std::nullopt, *m_nextBatchTokens[spaceId]);
auto group = KConfigGroup(KSharedConfig::openStateConfig("SpaceHierarchy"_ls), "Cache"_ls); auto group = KConfigGroup(KSharedConfig::openStateConfig("SpaceHierarchy"_ls), "Cache"_ls);
m_spaceHierarchy.insert(spaceId, group.readEntry(spaceId, QStringList())); m_spaceHierarchy.insert(spaceId, group.readEntry(spaceId, QStringList()));
@@ -86,7 +86,7 @@ void SpaceHierarchyCache::addBatch(const QString &spaceId, Quotient::GetSpaceHie
const auto nextBatchToken = job->nextBatch(); const auto nextBatchToken = job->nextBatch();
if (!nextBatchToken.isEmpty() && nextBatchToken != *m_nextBatchTokens[spaceId]) { if (!nextBatchToken.isEmpty() && nextBatchToken != *m_nextBatchTokens[spaceId]) {
*m_nextBatchTokens[spaceId] = nextBatchToken; *m_nextBatchTokens[spaceId] = nextBatchToken;
auto nextJob = m_connection->callApi<GetSpaceHierarchyJob>(spaceId, none, none, none, *m_nextBatchTokens[spaceId]); auto nextJob = m_connection->callApi<GetSpaceHierarchyJob>(spaceId, std::nullopt, std::nullopt, std::nullopt, *m_nextBatchTokens[spaceId]);
connect(nextJob, &BaseJob::success, this, [this, nextJob, spaceId]() { connect(nextJob, &BaseJob::success, this, [this, nextJob, spaceId]() {
addBatch(spaceId, nextJob); addBatch(spaceId, nextJob);
}); });

View File

@@ -691,9 +691,9 @@ QString TextHandler::emoteString(const NeoChatRoom *room, const Quotient::RoomEv
} }
auto e = eventCast<const Quotient::RoomMessageEvent>(event); auto e = eventCast<const Quotient::RoomMessageEvent>(event);
auto author = room->user(e->senderId()); auto author = room->member(e->senderId());
return QStringLiteral("* <a href=\"https://matrix.to/#/") + e->senderId() + QStringLiteral("\" style=\"color:") + Utils::getUserColor(author->hueF()).name() return QStringLiteral("* <a href=\"https://matrix.to/#/") + e->senderId() + QStringLiteral("\" style=\"color:") + author.color().name()
+ QStringLiteral("\">") + author->displayname(room) + QStringLiteral("</a> "); + QStringLiteral("\">") + author.htmlSafeDisplayName() + QStringLiteral("</a> ");
} }
QString TextHandler::convertCodeLanguageString(const QString &languageString) QString TextHandler::convertCodeLanguageString(const QString &languageString)

View File

@@ -25,7 +25,7 @@ Flow {
implicitHeight: root.avatarSize implicitHeight: root.avatarSize
name: modelData.displayName name: modelData.displayName
source: modelData.avatarSource source: modelData.avatarUrl
color: modelData.color color: modelData.color
} }
} }

View File

@@ -35,18 +35,9 @@ QQC2.Control {
/** /**
* @brief The message author. * @brief The message author.
* *
* This should consist of the following: * A Quotient::RoomMember object.
* - id - The matrix ID of the author.
* - isLocalUser - Whether the author is the local user.
* - avatarSource - The mxc URL for the author's avatar in the current room.
* - avatarMediaId - The media ID of the author's avatar.
* - avatarUrl - The mxc URL for the author's avatar.
* - displayName - The display name of the author.
* - display - The name of the author.
* - color - The color for the author.
* - object - The Quotient::User object for the author.
* *
* @sa Quotient::User * @sa Quotient::RoomMember
*/ */
property var author property var author
@@ -125,14 +116,14 @@ QQC2.Control {
id: nameButton id: nameButton
Layout.fillWidth: true Layout.fillWidth: true
contentItem: QQC2.Label { contentItem: QQC2.Label {
text: root.author.displayName text: root.author.disambiguatedName
color: root.author.color color: root.author.color
textFormat: Text.PlainText textFormat: Text.PlainText
font.weight: Font.Bold font.weight: Font.Bold
elide: Text.ElideRight elide: Text.ElideRight
} }
Accessible.name: contentItem.text Accessible.name: contentItem.text
onClicked: RoomManager.resolveResource(root.author.id, "mention") onClicked: RoomManager.resolveResource(root.author.uri)
} }
QQC2.Label { QQC2.Label {
id: timeLabel id: timeLabel
@@ -176,7 +167,7 @@ QQC2.Control {
visible: root.showBackground visible: root.showBackground
Kirigami.Theme.colorSet: Kirigami.Theme.View Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
color: if (root.author.isLocalUser) { color: if (root.author.isLocalMember) {
return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15); return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15);
} else if (root.showHighlight) { } else if (root.showHighlight) {
return Kirigami.Theme.positiveBackgroundColor; return Kirigami.Theme.positiveBackgroundColor;

View File

@@ -16,18 +16,9 @@ QQC2.Control {
/** /**
* @brief The message author. * @brief The message author.
* *
* This should consist of the following: * A Quotient::RoomMember object.
* - id - The matrix ID of the author.
* - isLocalUser - Whether the author is the local user.
* - avatarSource - The mxc URL for the author's avatar in the current room.
* - avatarMediaId - The media ID of the author's avatar.
* - avatarUrl - The mxc URL for the author's avatar.
* - displayName - The display name of the author.
* - display - The name of the author.
* - color - The color for the author.
* - object - The Quotient::User object for the author.
* *
* @sa Quotient::User * @sa Quotient::RoomMember
*/ */
required property var author required property var author

View File

@@ -19,18 +19,9 @@ ColumnLayout {
/** /**
* @brief The message author. * @brief The message author.
* *
* This should consist of the following: * A Quotient::RoomMember object.
* - id - The matrix ID of the author.
* - isLocalUser - Whether the author is the local user.
* - avatarSource - The mxc URL for the author's avatar in the current room.
* - avatarMediaId - The media ID of the author's avatar.
* - avatarUrl - The mxc URL for the author's avatar.
* - displayName - The display name of the author.
* - display - The name of the author.
* - color - The color for the author.
* - object - The Quotient::User object for the author.
* *
* @sa Quotient::User * @sa Quotient::RoomMember
*/ */
required property var author required property var author

View File

@@ -56,18 +56,9 @@ TimelineDelegate {
/** /**
* @brief The message author. * @brief The message author.
* *
* This should consist of the following: * A Quotient::RoomMember object.
* - id - The matrix ID of the author.
* - isLocalUser - Whether the author is the local user.
* - avatarSource - The mxc URL for the author's avatar in the current room.
* - avatarMediaId - The media ID of the author's avatar.
* - avatarUrl - The mxc URL for the author's avatar.
* - displayName - The display name of the author.
* - display - The name of the author.
* - color - The color for the author.
* - object - The Quotient::User object for the author.
* *
* @sa Quotient::User * @sa Quotient::RoomMember
*/ */
required property var author required property var author
@@ -281,11 +272,11 @@ TimelineDelegate {
visible: (root.showAuthor || root.alwaysShowAuthor) && Config.showAvatarInTimeline && (Config.compactLayout || !_private.showUserMessageOnRight) visible: (root.showAuthor || root.alwaysShowAuthor) && Config.showAvatarInTimeline && (Config.compactLayout || !_private.showUserMessageOnRight)
name: root.author.displayName name: root.author.displayName
source: root.author.avatarSource source: root.author.avatarUrl
color: root.author.color color: root.author.color
QQC2.ToolTip.text: root.author.escapedDisplayName QQC2.ToolTip.text: root.author.htmlSafeDisambiguatedName
onClicked: RoomManager.resolveResource(root.author.id, "mention") onClicked: RoomManager.resolveResource(root.author.uri)
} }
Bubble { Bubble {
id: bubble id: bubble
@@ -411,7 +402,7 @@ TimelineDelegate {
/** /**
* @brief Whether local user messages should be aligned right. * @brief Whether local user messages should be aligned right.
*/ */
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && root.author.isLocalUser && !Config.compactLayout && !root.alwaysFillWidth property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && root.author.isLocalMember && !Config.compactLayout && !root.alwaysFillWidth
function showMessageMenu() { function showMessageMenu() {
RoomManager.viewEventMenu(root.eventId, root.room, root.selectedText); RoomManager.viewEventMenu(root.eventId, root.room, root.selectedText);

View File

@@ -52,7 +52,7 @@ ColumnLayout {
delegate: RowLayout { delegate: RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
CheckBox { CheckBox {
checked: root.pollHandler.answers[root.room.localUser.id] ? root.pollHandler.answers[root.room.localUser.id].includes(modelData["id"]) : false checked: root.pollHandler.answers[root.room.localember.id] ? root.pollHandler.answers[root.room.localMember.id].includes(modelData["id"]) : false
onClicked: root.pollHandler.sendPollAnswer(root.eventId, modelData["id"]) onClicked: root.pollHandler.sendPollAnswer(root.eventId, modelData["id"])
enabled: !root.pollHandler.hasEnded enabled: !root.pollHandler.hasEnded
} }

View File

@@ -35,7 +35,7 @@ Flow {
required property string textContent required property string textContent
required property string reaction required property string reaction
required property string toolTip required property string toolTip
required property bool hasLocalUser required property bool hasLocalMember
width: Math.max(contentItem.implicitWidth + leftPadding + rightPadding, height) width: Math.max(contentItem.implicitWidth + leftPadding + rightPadding, height)
height: Math.round(Kirigami.Units.gridUnit * 1.5) height: Math.round(Kirigami.Units.gridUnit * 1.5)
@@ -54,13 +54,13 @@ Flow {
padding: Kirigami.Units.smallSpacing padding: Kirigami.Units.smallSpacing
background: Kirigami.ShadowedRectangle { background: Kirigami.ShadowedRectangle {
color: reactionDelegate.hasLocalUser ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor color: reactionDelegate.hasLocalMember ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Config.compactLayout ? Kirigami.Theme.Window : Kirigami.Theme.View Kirigami.Theme.colorSet: Config.compactLayout ? Kirigami.Theme.Window : Kirigami.Theme.View
radius: height / 2 radius: height / 2
shadow { shadow {
size: Kirigami.Units.smallSpacing size: Kirigami.Units.smallSpacing
color: !reactionDelegate.hasLocalUser ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10) color: !reactionDelegate.hasLocalMember ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
} }
} }

View File

@@ -32,18 +32,9 @@ RowLayout {
/** /**
* @brief The reply author. * @brief The reply author.
* *
* This should consist of the following: * A Quotient::RoomMember object.
* - id - The matrix ID of the reply author.
* - isLocalUser - Whether the reply author is the local user.
* - avatarSource - The mxc URL for the reply author's avatar in the current room.
* - avatarMediaId - The media ID of the reply author's avatar.
* - avatarUrl - The mxc URL for the reply author's avatar.
* - displayName - The display name of the reply author.
* - display - The name of the reply author.
* - color - The color for the reply author.
* - object - The Quotient::User object for the reply author.
* *
* @sa Quotient::User * @sa Quotient::RoomMember
*/ */
required property var replyAuthor required property var replyAuthor
@@ -87,7 +78,7 @@ RowLayout {
implicitWidth: Kirigami.Units.iconSizes.small implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: Kirigami.Units.iconSizes.small implicitHeight: Kirigami.Units.iconSizes.small
source: root.replyAuthor.avatarSource source: root.replyAuthor.avatarUrl
name: root.replyAuthor.displayName name: root.replyAuthor.displayName
color: root.replyAuthor.color color: root.replyAuthor.color
} }

View File

@@ -24,18 +24,9 @@ RowLayout {
/** /**
* @brief The message author. * @brief The message author.
* *
* This should consist of the following: * A Quotient::RoomMember object.
* - id - The matrix ID of the author.
* - isLocalUser - Whether the author is the local user.
* - avatarSource - The mxc URL for the author's avatar in the current room.
* - avatarMediaId - The media ID of the author's avatar.
* - avatarUrl - The mxc URL for the author's avatar.
* - displayName - The display name of the author.
* - display - The name of the author.
* - color - The color for the author.
* - object - The Quotient::User object for the author.
* *
* @sa Quotient::User * @sa Quotient::RoomMember
*/ */
property var author: modelData.author property var author: modelData.author

View File

@@ -113,7 +113,7 @@ TimelineDelegate {
implicitHeight: Kirigami.Units.iconSizes.small implicitHeight: Kirigami.Units.iconSizes.small
name: parent.modelData.displayName name: parent.modelData.displayName
source: parent.modelData.avatarSource source: parent.modelData.avatarUrl
color: parent.modelData.color color: parent.modelData.color
} }
} }

View File

@@ -7,45 +7,6 @@
#include <QJsonDocument> #include <QJsonDocument>
using namespace Quotient;
static const QVariantMap emptyUser = {
{"isLocalUser"_ls, false},
{"id"_ls, QString()},
{"displayName"_ls, QString()},
{"avatarSource"_ls, QUrl()},
{"avatarMediaId"_ls, QString()},
{"color"_ls, QColor()},
{"object"_ls, QVariant()},
};
QVariantMap QmlUtils::getUser(User *user) const
{
if (user == nullptr) {
return emptyUser;
}
const auto &url = user->avatarUrl();
if (url.isEmpty() || url.scheme() != "mxc"_ls) {
return {};
}
auto avatarSource = user->connection()->makeMediaUrl(url);
if (!avatarSource.isValid() || avatarSource.scheme() != QStringLiteral("mxc")) {
avatarSource = {};
}
return QVariantMap{
{QStringLiteral("isLocalUser"), user->id() == user->connection()->user()->id()},
{QStringLiteral("id"), user->id()},
{QStringLiteral("displayName"), user->displayname()},
{QStringLiteral("escapedDisplayName"), user->displayname().toHtmlEscaped()},
{QStringLiteral("avatarSource"), avatarSource},
{QStringLiteral("avatarMediaId"), user->avatarMediaId()},
{QStringLiteral("color"), Utils::getUserColor(user->hueF())},
{QStringLiteral("object"), QVariant::fromValue(user)},
};
}
bool QmlUtils::isValidJson(const QByteArray &json) bool QmlUtils::isValidJson(const QByteArray &json)
{ {
return !QJsonDocument::fromJson(json).isNull(); return !QJsonDocument::fromJson(json).isNull();

View File

@@ -3,14 +3,9 @@
#pragma once #pragma once
#include <QColor>
#include <QGuiApplication>
#include <QPalette>
#include <QQmlEngine> #include <QQmlEngine>
#include <QRegularExpression> #include <QRegularExpression>
#include <Quotient/user.h>
class QmlUtils : public QObject class QmlUtils : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -30,31 +25,12 @@ public:
return _instance; return _instance;
} }
Q_INVOKABLE QVariantMap getUser(Quotient::User *user) const;
Q_INVOKABLE bool isValidJson(const QByteArray &json); Q_INVOKABLE bool isValidJson(const QByteArray &json);
private: private:
QmlUtils() = default; QmlUtils() = default;
}; };
namespace Utils
{
/**
* @brief Get a color for a user from a hueF value.
*
* The lightness of the color will be modified depending on the current palette in
* order to maintain contrast.
*/
inline QColor getUserColor(qreal hueF)
{
const auto lightness = static_cast<QGuiApplication *>(QGuiApplication::instance())->palette().color(QPalette::Active, QPalette::Window).lightnessF();
// https://github.com/quotient-im/libQuotient/wiki/User-color-coding-standard-draft-proposal
return QColor::fromHslF(hueF, 1, -0.7 * lightness + 0.9, 1);
}
}
namespace TextRegex namespace TextRegex
{ {
static const QRegularExpression endTagType{QStringLiteral("(>| )")}; static const QRegularExpression endTagType{QStringLiteral("(>| )")};