Compare commits

...

13 Commits

Author SHA1 Message Date
Tobias Fella
27f4dfbf8e Work 2024-05-26 14:43:41 +02:00
Tobias Fella
1235e46575 Add basic cross-signing support 2024-05-25 19:40:13 +02:00
James Graham
31dd981104 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-05-25 19:40:13 +02:00
James Graham
645438525f Use new libquotient functionality to encrypt direct chats by default
Needs https://github.com/quotient-im/libQuotient/pull/730
2024-05-25 19:40:13 +02:00
Tobias Fella
a29d3790c8 Disable failing CIs 2024-05-25 19:40:13 +02:00
James Graham
a47cbeca81 Use the new libquotient version of the serveracl event
Use with https://github.com/quotient-im/libQuotient/pull/729
2024-05-25 19:40:12 +02:00
James Graham
ab06d24f66 Use updated membersTyping functions from libquotient 2024-05-25 19:40:12 +02:00
James Graham
6f4a52024b Request direct chat by userId to avoid breakage with upcoming libquotient changes 2024-05-25 19:40:12 +02:00
Tobias Fella
83d21634c3 Unconditionally use SSSS 2024-05-25 19:40:12 +02:00
James Graham
8d6340f84c Actually user.h needs to still be included in actionsmodel 2024-05-25 19:40:12 +02:00
James Graham
a9713b1f1c 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-05-25 19:40:10 +02:00
James Graham
9e05f17cb7 handle remaining stuff for Quotient::Omittable deprecation 2024-05-25 19:38:32 +02:00
James Graham
15b7c04834 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-05-25 19:38:32 +02:00
80 changed files with 592 additions and 730 deletions

View File

@@ -5,11 +5,11 @@ include:
- project: sysadmin/ci-utilities
file:
- /gitlab-templates/reuse-lint.yml
- /gitlab-templates/android-qt6.yml
- /gitlab-templates/linux-qt6.yml
- /gitlab-templates/windows-qt6.yml
- /gitlab-templates/freebsd-qt6.yml
# - /gitlab-templates/android-qt6.yml
# - /gitlab-templates/linux-qt6.yml
# - /gitlab-templates/windows-qt6.yml
# - /gitlab-templates/freebsd-qt6.yml
- /gitlab-templates/flatpak.yml
- /gitlab-templates/craft-android-qt6-apks.yml
- /gitlab-templates/craft-appimage-qt6.yml
- /gitlab-templates/craft-windows-x86-64-qt6.yml
# - /gitlab-templates/craft-android-qt6-apks.yml
# - /gitlab-templates/craft-appimage-qt6.yml
# - /gitlab-templates/craft-windows-x86-64-qt6.yml

View File

@@ -6,6 +6,7 @@
#include <QObject>
#include <QTest>
#include <Quotient/roommember.h>
#include <Quotient/syncdata.h>
#include <qtestcase.h>
@@ -50,7 +51,7 @@ void ChatBarCacheTest::empty()
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationUser(), room->getUser(nullptr));
QCOMPARE(chatBarCache->relationUser(), room->member(QString()));
QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
@@ -64,7 +65,7 @@ void ChatBarCacheTest::noRoom()
// ChatBarCache has no parent.
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.");
QCOMPARE(chatBarCache->relationMessage(), QString());
@@ -80,7 +81,7 @@ void ChatBarCacheTest::badParent()
// ChatBarCache has no parent.
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.");
QCOMPARE(chatBarCache->relationMessage(), QString());
@@ -98,7 +99,7 @@ void ChatBarCacheTest::reply()
QCOMPARE(chatBarCache->replyId(), QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->isEditing(), false);
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->attachmentPath(), QString());
}
@@ -115,7 +116,7 @@ void ChatBarCacheTest::edit()
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), true);
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->attachmentPath(), QString());
}
@@ -132,7 +133,7 @@ void ChatBarCacheTest::attachment()
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationUser(), room->getUser(nullptr));
QCOMPARE(chatBarCache->relationUser(), room->member(QString()));
QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), QLatin1String("some/path"));
}

View File

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

View File

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

View File

@@ -51,6 +51,21 @@
"unsigned": {
"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()
{
auto event = room->messageEvents().at(0).get();
auto author = room->user(event->senderId());
auto author = room->member(event->senderId());
EventHandler eventHandler(room, event);
auto eventHandlerAuthor = eventHandler.getAuthor();
QCOMPARE(eventHandlerAuthor["isLocalUser"_ls], author->id() == room->localUser()->id());
QCOMPARE(eventHandlerAuthor["id"_ls], author->id());
QCOMPARE(eventHandlerAuthor["displayName"_ls], author->displayname(room));
QCOMPARE(eventHandlerAuthor["avatarSource"_ls], room->avatarForMember(author));
QCOMPARE(eventHandlerAuthor["avatarMediaId"_ls], author->avatarMediaId(room));
QCOMPARE(eventHandlerAuthor["color"_ls], Utils::getUserColor(author->hueF()));
QCOMPARE(eventHandlerAuthor["object"_ls], QVariant::fromValue(author));
QCOMPARE(eventHandlerAuthor.isLocalMember(), author.id() == room->localMember().id());
QCOMPARE(eventHandlerAuthor.id(), author.id());
QCOMPARE(eventHandlerAuthor.displayName(), author.displayName());
QCOMPARE(eventHandlerAuthor.avatarUrl(), author.avatarUrl());
QCOMPARE(eventHandlerAuthor.avatarMediaId(), author.avatarMediaId());
QCOMPARE(eventHandlerAuthor.color(), author.color());
}
void EventHandlerTest::nullAuthor()
{
QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getAuthor(), QVariantMap());
QCOMPARE(emptyHandler.getAuthor(), RoomMember());
EventHandler noEventHandler(room, nullptr);
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()
@@ -393,31 +392,30 @@ void EventHandlerTest::nullReplyId()
void EventHandlerTest::replyAuthor()
{
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());
auto eventHandlerReplyAuthor = eventHandler.getReplyAuthor();
QCOMPARE(eventHandlerReplyAuthor["isLocalUser"_ls], replyAuthor->id() == room->localUser()->id());
QCOMPARE(eventHandlerReplyAuthor["id"_ls], replyAuthor->id());
QCOMPARE(eventHandlerReplyAuthor["displayName"_ls], replyAuthor->displayname(room));
QCOMPARE(eventHandlerReplyAuthor["avatarSource"_ls], room->avatarForMember(replyAuthor));
QCOMPARE(eventHandlerReplyAuthor["avatarMediaId"_ls], replyAuthor->avatarMediaId(room));
QCOMPARE(eventHandlerReplyAuthor["color"_ls], Utils::getUserColor(replyAuthor->hueF()));
QCOMPARE(eventHandlerReplyAuthor["object"_ls], QVariant::fromValue(replyAuthor));
QCOMPARE(eventHandlerReplyAuthor.isLocalMember(), replyAuthor.id() == room->localMember().id());
QCOMPARE(eventHandlerReplyAuthor.id(), replyAuthor.id());
QCOMPARE(eventHandlerReplyAuthor.displayName(), replyAuthor.displayName());
QCOMPARE(eventHandlerReplyAuthor.avatarUrl(), replyAuthor.avatarUrl());
QCOMPARE(eventHandlerReplyAuthor.avatarMediaId(), replyAuthor.avatarMediaId());
QCOMPARE(eventHandlerReplyAuthor.color(), replyAuthor.color());
EventHandler eventHandlerNoAuthor(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoAuthor.getReplyAuthor(), room->getUser(nullptr));
QCOMPARE(eventHandlerNoAuthor.getReplyAuthor(), RoomMember());
}
void EventHandlerTest::nullReplyAuthor()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyAuthor(), QVariantMap());
QCOMPARE(emptyHandler.getReplyAuthor(), RoomMember());
EventHandler noEventHandler(room, nullptr);
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()
@@ -531,10 +529,10 @@ void EventHandlerTest::readMarkers()
auto readMarkers = eventHandler.getReadMarkers();
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.getReadMarkersString(), QStringLiteral("1 user: @alice:matrix.org"));
QCOMPARE(eventHandler.getReadMarkersString(), QStringLiteral("1 user: Alice Margatroid"));
EventHandler eventHandler2(room, room->messageEvents().at(2).get());
QCOMPARE(eventHandler2.hasReadMarkers(), true);
@@ -554,7 +552,7 @@ void EventHandlerTest::nullReadMarkers()
QCOMPARE(emptyHandler.hasReadMarkers(), false);
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.");
QCOMPARE(emptyHandler.getNumberExcessReadMarkers(), QString());
@@ -568,7 +566,7 @@ void EventHandlerTest::nullReadMarkers()
QCOMPARE(noEventHandler.hasReadMarkers(), false);
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.");
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::ToolTipRole),
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::AuthorsRole), authorList);
QCOMPARE(model.data(model.index(0), ReactionModel::HasLocalUser), false);
QCOMPARE(model.data(model.index(0), ReactionModel::HasLocalMember), false);
}
void ReactionModelTest::newReaction()

View File

@@ -156,7 +156,6 @@ add_library(neochat STATIC
models/linemodel.cpp
models/linemodel.h
events/locationbeaconevent.h
events/serveraclevent.h
events/widgetevent.h
enums/messagecomponenttype.h
models/messagecontentmodel.cpp

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++) {
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;
if (event->content()) {
originalString = static_cast<const Quotient::EventContent::TextContent *>(event->content())->body;

View File

@@ -3,6 +3,8 @@
#include "chatbarcache.h"
#include <Quotient/roommember.h>
#include "chatdocumenthandler.h"
#include "eventhandler.h"
#include "neochatroom.h"
@@ -84,7 +86,7 @@ void ChatBarCache::setEditId(const QString &editId)
Q_EMIT attachmentPathChanged();
}
QVariantMap ChatBarCache::relationUser() const
Quotient::RoomMember ChatBarCache::relationUser() const
{
if (parent() == nullptr) {
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 {};
}
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

View File

@@ -10,6 +10,12 @@
class ChatDocumentHandler;
namespace Quotient
{
class RoomMember;
}
/**
* @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)
/**
* @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
* 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 RoomMember if not replying to a message.
*
* Returns an empty user if not replying to a message.
*
* The user QVariantMap has the following properties:
* - isLocalUser - Whether the user is the local user.
* - id - The matrix ID of the user.
* - displayName - Display name in the context of this room.
* - avatarSource - The mxc URL for the user's avatar in the current room.
* - avatarMediaId - Avatar id in the context of this room.
* - color - Color for the user.
* - object - The Quotient::User object for the user.
*
* @sa getUser, Quotient::User
* @sa Quotient::RoomMember
*/
Q_PROPERTY(QVariantMap relationUser READ relationUser NOTIFY relationIdChanged)
Q_PROPERTY(Quotient::RoomMember relationUser READ relationUser NOTIFY relationIdChanged)
/**
* @brief The content of the related message.
@@ -161,7 +154,7 @@ public:
QString editId() const;
void setEditId(const QString &editId);
QVariantMap relationUser() const;
Quotient::RoomMember relationUser() const;
QString relationMessage() const;

View File

@@ -407,11 +407,11 @@ void Controller::removeConnection(const QString &userId)
}
}
bool Controller::ssssSupported() const
void Controller::saveFileContent(const QString &path, const QByteArray &data) const
{
#if __has_include("Quotient/e2ee/sssshandler.h")
return true;
#else
return false;
#endif
QUrl url(path);
QFile file(url.toLocalFile());
file.open(QFile::WriteOnly);
file.write(data);
file.close();
}

View File

@@ -50,8 +50,6 @@ class Controller : public QObject
Q_PROPERTY(QStringList accountsLoading MEMBER m_accountsLoading NOTIFY accountsLoadingChanged)
Q_PROPERTY(bool ssssSupported READ ssssSupported CONSTANT)
public:
static Controller &instance();
static Controller *create(QQmlEngine *engine, QJSEngine *)
@@ -89,6 +87,7 @@ public:
static void listenForNotifications();
Q_INVOKABLE QString loadFileContent(const QString &path) const;
Q_INVOKABLE void saveFileContent(const QString &path, const QByteArray &data) const;
Quotient::AccountRegistry &accounts();
@@ -96,8 +95,6 @@ public:
Q_INVOKABLE void removeConnection(const QString &userId);
bool ssssSupported() const;
private:
explicit Controller(QObject *parent = nullptr);

View File

@@ -19,11 +19,11 @@
#include <Quotient/events/simplestateevents.h>
#include <Quotient/events/stickerevent.h>
#include <Quotient/quotient_common.h>
#include <Quotient/roommember.h>
#include "eventhandler_logging.h"
#include "events/locationbeaconevent.h"
#include "events/pollevent.h"
#include "events/serveraclevent.h"
#include "events/widgetevent.h"
#include "linkpreviewer.h"
#include "messagecomponenttype.h"
@@ -61,20 +61,18 @@ MessageComponentType::Type EventHandler::messageComponentType() const
return MessageComponentType::typeForEvent(*m_event);
}
QVariantMap EventHandler::getAuthor(bool isPending) const
Quotient::RoomMember EventHandler::getAuthor(bool isPending) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getAuthor called with m_room set to nullptr.";
return {};
}
// If we have a room we can return an empty user by handing nullptr to m_room->getUser.
if (m_event == nullptr) {
qCWarning(EventHandling) << "getAuthor called with m_event set to nullptr. Returning empty user.";
return m_room->getUser(nullptr);
return {};
}
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
return m_room->getUser(author);
return isPending ? m_room->localMember() : m_room->member(m_event->senderId());
}
QString EventHandler::getAuthorDisplayName(bool isPending) const
@@ -96,8 +94,8 @@ QString EventHandler::getAuthorDisplayName(bool isPending) const
}
return previousDisplayName;
} else {
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
return m_room->htmlSafeMemberName(author->id());
const auto author = isPending ? m_room->localMember() : m_room->member(m_event->senderId());
return author.htmlSafeDisplayName();
}
}
@@ -112,8 +110,8 @@ QString EventHandler::singleLineAuthorDisplayname(bool isPending) const
return {};
}
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
auto displayName = m_room->safeMemberName(author->id());
const auto author = isPending ? m_room->localMember() : m_room->member(m_event->senderId());
auto displayName = author.displayName();
displayName.replace(QStringLiteral("<br>\n"), QStringLiteral(" "));
displayName.replace(QStringLiteral("<br>"), QStringLiteral(" "));
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;
}
@@ -255,7 +253,7 @@ QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event)
QString body;
if (event.hasTextContent() && event.content()) {
body = static_cast<const MessageEventContent::TextContent *>(event.content())->body;
body = static_cast<const EventContent::TextContent *>(event.content())->body;
} else {
body = event.plainBody();
}
@@ -318,7 +316,7 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
},
[this, prettyPrint](const RoomMemberEvent &e) {
// 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.prevContent() && e.prevContent()->displayName) {
subjectName = sanitized(*e.prevContent()->displayName).toHtmlEscaped();
@@ -326,7 +324,8 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
}
if (prettyPrint) {
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\">%2</a>").arg(e.userId(), subjectName);
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
.arg(e.userId(), m_room->member(e.userId()).color().name(), subjectName);
}
// 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) {
return e.contentJson()["description"_ls].toString();
},
[](const ServerAclEvent &) {
[](const RoomServerAclEvent &) {
return i18n("changed the server access control lists for this room");
},
[](const WidgetEvent &e) {
@@ -479,7 +478,7 @@ QString EventHandler::getMessageBody(const RoomMessageEvent &event, Qt::TextForm
QString body;
if (event.hasTextContent() && event.content()) {
body = static_cast<const MessageEventContent::TextContent *>(event.content())->body;
body = static_cast<const EventContent::TextContent *>(event.content())->body;
} else {
body = event.plainBody();
}
@@ -609,7 +608,7 @@ QString EventHandler::getGenericBody() const
[](const LocationBeaconEvent &) {
return i18n("sent a live location beacon");
},
[](const ServerAclEvent &) {
[](const RoomServerAclEvent &) {
return i18n("changed the server access control lists for this room");
},
[](const WidgetEvent &e) {
@@ -800,25 +799,21 @@ MessageComponentType::Type EventHandler::replyMessageComponentType() const
return MessageComponentType::typeForEvent(*replyEvent);
}
QVariantMap EventHandler::getReplyAuthor() const
Quotient::RoomMember EventHandler::getReplyAuthor() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyAuthor called with m_room set to nullptr.";
return {};
}
// If we have a room we can return an empty user by handing nullptr to m_room->getUser.
if (m_event == nullptr) {
qCWarning(EventHandling) << "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 (replyPtr) {
auto replyUser = m_room->user(replyPtr->senderId());
return m_room->getUser(replyUser);
if (auto replyPtr = m_room->getReplyForEvent(*m_event)) {
return m_room->member(replyPtr->senderId());
} 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());
userIds.remove(m_room->localUser()->id());
userIds.remove(m_room->localMember().id());
return userIds.size() > 0;
}
QVariantList EventHandler::getReadMarkers(int maxMarkers) const
QList<Quotient::RoomMember> EventHandler::getReadMarkers(int maxMarkers) const
{
if (m_room == 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());
userIds_temp.remove(m_room->localUser()->id());
userIds_temp.remove(m_room->localMember().id());
auto userIds = userIds_temp.values();
if (userIds.count() > maxMarkers) {
userIds = userIds.mid(0, maxMarkers);
}
QVariantList users;
QList<Quotient::RoomMember> users;
users.reserve(userIds.size());
for (const auto &userId : userIds) {
auto user = m_room->user(userId);
users += m_room->getUser(user);
users += m_room->member(userId);
}
return users;
@@ -1011,7 +1005,7 @@ QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const
}
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
userIds.remove(m_room->localMember().id());
if (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());
userIds.remove(m_room->localUser()->id());
userIds.remove(m_room->localMember().id());
/**
* The string ends up in the form
@@ -1040,10 +1034,12 @@ QString EventHandler::getReadMarkersString() const
*/
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
for (const auto &userId : userIds) {
auto user = m_room->user(userId);
auto displayName = user->displayname(m_room);
if (displayName.isEmpty()) {
displayName = userId;
auto member = m_room->member(userId);
QString displayName;
if (member.isEmpty()) {
displayName = i18nc("A member who is not in the room has been requested.", "unknown member");
} else {
displayName = member.displayName();
}
readMarkersString += displayName + i18nc("list separator", ", ");
}

View File

@@ -13,6 +13,11 @@
#include "enums/messagecomponenttype.h"
namespace Quotient
{
class RoomMember;
}
class LinkPreviewer;
class NeoChatRoom;
class ReactionModel;
@@ -51,30 +56,17 @@ public:
/**
* @brief Get the author of the event in context of the room.
*
* This is different to getting a Quotient::User object
* as neither of those can provide details like the displayName or avatarMediaId
* without the room context as these can vary from room to room. This function
* uses the room context and outputs the result as QVariantMap.
*
* An empty QVariantMap will be returned if the EventHandler hasn't had the room
* intialised. An empty user (i.e. a QVariantMap with all the correct keys
* but empty values) will be returned if the room has been set but not an event.
* An empty Quotient::RoomMember will be returned if the EventHandler hasn't had
* the room or event initialised.
*
* @param isPending if the event is pending, i.e. has not been confirmed by
* the server.
*
* @return a QVariantMap for the user with the following properties:
* - isLocalUser - Whether the user is the local user.
* - id - The matrix ID of the user.
* - displayName - Display name in the context of this room.
* - avatarSource - The mxc URL for the user's avatar in the current room.
* - avatarMediaId - Avatar id in the context of this room.
* - color - Color for the user.
* - object - The Quotient::User object for the user.
* @return a Quotient::RoomMember 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.
@@ -251,27 +243,17 @@ public:
/**
* @brief Get the author of the event replied to in context of the room.
*
* This is different to getting a Quotient::User object
* as neither of those can provide details like the displayName or avatarMediaId
* without the room context as these can vary from room to room. This function
* uses the room context and outputs the result as QVariantMap.
* An empty Quotient::RoomMember will be returned if the EventHandler hasn't had
* the room or event initialised.
*
* An empty QVariantMap will be returned if the EventHandler hasn't had the room
* intialised. An empty user (i.e. a QVariantMap with all the correct keys
* but empty values) will be returned if the room has been set but not an event.
* @param isPending if the event is pending, i.e. has not been confirmed by
* the server.
*
* @return a QVariantMap for the user with the following properties:
* - isLocalUser - Whether the user is the local user.
* - id - The matrix ID of the user.
* - displayName - Display name in the context of this room.
* - avatarSource - The mxc URL for the user's avatar in the current room.
* - avatarMediaId - Avatar id in the context of this room.
* - color - Color for the user.
* - object - The Quotient::User object for the user.
* @return a Quotient::RoomMember 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
@@ -375,7 +357,7 @@ public:
* the number of users shown plus the excess number will be
* the total number of other user read markers at an event.
*/
QVariantList getReadMarkers(int maxMarkers = 5) const;
QList<Quotient::RoomMember> getReadMarkers(int maxMarkers = 5) const;
/**
* @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"))) {
pack = ImagePackEventContent::Pack{
fromJson<Omittable<QString>>(json["pack"_ls].toObject()["display_name"_ls]),
fromJson<Omittable<QUrl>>(json["pack"_ls].toObject()["avatar_url"_ls]),
fromJson<Omittable<QStringList>>(json["pack"_ls].toObject()["usage"_ls]),
fromJson<Omittable<QString>>(json["pack"_ls].toObject()["attribution"_ls]),
fromJson<std::optional<QString>>(json["pack"_ls].toObject()["display_name"_ls]),
fromJson<std::optional<QUrl>>(json["pack"_ls].toObject()["avatar_url"_ls]),
fromJson<std::optional<QStringList>>(json["pack"_ls].toObject()["usage"_ls]),
fromJson<std::optional<QString>>(json["pack"_ls].toObject()["attribution"_ls]),
};
} else {
pack = none;
pack = std::nullopt;
}
const auto &keys = json["images"_ls].toObject().keys();
for (const auto &k : keys) {
Omittable<EventContent::ImageInfo> info;
std::optional<EventContent::ImageInfo> 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);
} else {
info = none;
info = std::nullopt;
}
images += ImagePackImage{
k,
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,
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.
*/
struct Pack {
Quotient::Omittable<QString> displayName; /**< The display name of the pack. */
Quotient::Omittable<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". */
Quotient::Omittable<QString> attribution; /**< The attribution for the pack author(s). */
std::optional<QString> displayName; /**< The display name of the pack. */
std::optional<QUrl> avatarUrl; /**< The source mxc URL for the pack avatar. */
std::optional<QStringList> usage; /**< An array of the usages for this pack. Possible usages are "emoticon" and "sticker". */
std::optional<QString> attribution; /**< The attribution for the pack author(s). */
};
/**
@@ -38,14 +38,14 @@ public:
struct ImagePackImage {
QString shortcode; /**< The shortcode for the image. */
QUrl url; /**< The mxc URL for this image. */
Quotient::Omittable<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<QString> body; /**< An optional text body for this image. */
std::optional<Quotient::EventContent::ImageInfo> info; /**< The ImageInfo object used for the info block of m.sticker events. */
/**
* @brief An array of the usages for this image.
*
* 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
*/
Quotient::Omittable<Pack> pack;
std::optional<Pack> 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,9 @@
#include <QQmlEngine>
#include <Quotient/accountregistry.h>
#include <Quotient/keyverificationsession.h>
#if __has_include("Quotient/e2ee/sssshandler.h")
#include <Quotient/e2ee/sssshandler.h>
#endif
#include <Quotient/keyimport.h>
#include <Quotient/keyverificationsession.h>
#include "controller.h"
#include "neochatconfig.h"
@@ -47,10 +46,15 @@ struct ForeignKeyVerificationSession {
QML_UNCREATABLE("")
};
#if __has_include("Quotient/e2ee/sssshandler.h")
struct ForeignSSSSHandler {
Q_GADGET
QML_FOREIGN(Quotient::SSSSHandler)
QML_NAMED_ELEMENT(SSSSHandler)
};
#endif
struct ForeignKeyImport {
Q_GADGET
QML_FOREIGN(Quotient::KeyImport)
QML_NAMED_ELEMENT(KeyImport)
QML_SINGLETON
};

View File

@@ -5,7 +5,7 @@
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")
{
QJsonObject _data;

View File

@@ -4,10 +4,9 @@
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatChangePasswordJob : public Quotient::BaseJob
{
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;
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const Omittable<QJsonObject> &auth)
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const std::optional<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("DisableDeviceJob"), "_matrix/client/v3/account/deactivate")
{
QJsonObject data;

View File

@@ -4,10 +4,9 @@
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeoChatDeactivateAccountJob : public Quotient::BaseJob
{
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;
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())
{
QJsonObject _data;

View File

@@ -4,10 +4,9 @@
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatDeleteDeviceJob : public Quotient::BaseJob
{
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 <QQuickWindow>
#include <QtQml/QQmlExtensionPlugin>
#include <Quotient/connection.h>
#ifdef Q_OS_ANDROID
#include <QGuiApplication>
@@ -174,6 +175,7 @@ int main(int argc, char *argv[])
initLogging();
Connection::setEncryptionDefault(true);
Connection::setDirectChatEncryptionDefault(true);
#ifdef NEOCHAT_FLATPAK
// Copy over the included FontConfig configuration to the

View File

@@ -9,6 +9,7 @@
#include "roommanager.h"
#include <Quotient/events/roommemberevent.h>
#include <Quotient/events/roompowerlevelsevent.h>
#include <Quotient/user.h>
#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));
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."));
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));
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));
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));
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));
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));
return QString();
},
@@ -431,11 +432,11 @@ QList<ActionsModel::Action> actions{
if (!plEvent) {
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."));
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(
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]));
@@ -464,7 +465,7 @@ QList<ActionsModel::Action> actions{
if (!plEvent) {
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."));
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]));
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."));
return QString();
}
@@ -508,11 +509,11 @@ QList<ActionsModel::Action> actions{
return QString();
}
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."));
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(
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]));

View File

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

View File

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

View File

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

View File

@@ -158,7 +158,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return eventHandler.getId();
}
if (role == AuthorRole) {
return eventHandler.getAuthor(false);
return QVariant::fromValue(eventHandler.getAuthor(false));
}
if (role == MediaInfoRole) {
return eventHandler.getMediaInfo();
@@ -191,7 +191,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return eventHandler.getReplyId();
}
if (role == ReplyAuthorRole) {
return eventHandler.getReplyAuthor();
return QVariant::fromValue(eventHandler.getReplyAuthor());
}
if (role == ReplyDisplayRole) {
return eventHandler.getReplyRichBody();

View File

@@ -11,7 +11,7 @@
#include <Quotient/events/redactionevent.h>
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/stickerevent.h>
#include <Quotient/user.h>
#include <Quotient/roommember.h>
#include <QDebug>
#include <QGuiApplication>
@@ -222,7 +222,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
beginResetModel();
endResetModel();
});
qCDebug(MessageEvent) << "Connected to room" << room->id() << "as" << room->localUser()->id();
qCDebug(MessageEvent) << "Connected to room" << room->id() << "as" << room->localMember().id();
} else {
lastReadEventId.clear();
}
@@ -460,7 +460,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == AuthorRole) {
return eventHandler.getAuthor(isPending);
return QVariant::fromValue(eventHandler.getAuthor(isPending));
}
if (role == HighlightRole) {
@@ -539,7 +539,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == ReadMarkersRole) {
return eventHandler.getReadMarkers();
return QVariant::fromValue(eventHandler.getReadMarkers());
}
if (role == ExcessReadMarkersRole) {
@@ -592,7 +592,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
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 {};

View File

@@ -116,7 +116,7 @@ void NotificationsModel::loadData()
if (!room) {
continue;
}
auto u = room->memberAvatarUrl(authorId);
auto u = room->member(authorId).avatarUrl();
auto avatar = u.isEmpty() ? QUrl() : connection()->makeMediaUrl(u);
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());
m_notifications += Notification{
.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),
.authorName = room->htmlSafeMemberName(authorId),
.authorName = room->member(authorId).htmlSafeDisplayName(),
.authorAvatar = authorAvatar,
.eventId = roomEvent->id(),
.roomDisplayName = room->displayName(),

View File

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

View File

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

View File

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

View File

@@ -87,7 +87,7 @@ void SpaceChildrenModel::refreshModel()
m_rootItem =
new SpaceTreeItem(dynamic_cast<NeoChatConnection *>(m_space->connection()), nullptr, m_space->id(), m_space->displayName(), m_space->canonicalAlias());
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);
connect(job, &Quotient::BaseJob::success, this, [this, job]() {
insertChildren(job->rooms());
@@ -136,7 +136,7 @@ void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJ
}
}
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);
connect(job, &Quotient::BaseJob::success, this, [this, parent, insertRow, job]() {
insertChildren(job->rooms(), index(insertRow, 0, parent));

View File

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

View File

@@ -56,11 +56,6 @@ public:
[[nodiscard]] NeoChatRoom *room() const;
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.
*
@@ -90,15 +85,15 @@ protected:
bool event(QEvent *event) override;
private Q_SLOTS:
void userAdded(Quotient::User *user);
void userRemoved(Quotient::User *user);
void refreshUser(Quotient::User *user, const QList<int> &roles = {});
void refreshAllUsers();
void memberJoined(const Quotient::RoomMember &member);
void memberLeft(const Quotient::RoomMember &member);
void refreshMember(const Quotient::RoomMember &member, const QList<int> &roles = {});
void refreshAllMembers();
private:
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;
};

View File

@@ -17,6 +17,7 @@
#include <Quotient/connection.h>
#include <Quotient/jobs/basejob.h>
#include <Quotient/keyimport.h>
#include <Quotient/quotient_common.h>
#include <qt6keychain/keychain.h>
@@ -345,10 +346,7 @@ void NeoChatConnection::openOrCreateDirectChat(User *user)
return;
}
}
requestDirectChat(user);
connectSingleShot(this, &Connection::directChatAvailable, this, [=](auto room) {
room->activateEncryption();
});
requestDirectChat(user->id());
}
qsizetype NeoChatConnection::directChatNotifications() const
@@ -503,4 +501,15 @@ LinkPreviewer *NeoChatConnection::previewerForLink(const QUrl &link)
return previewer;
}
void NeoChatConnection::exportMegolmSessions(const QString &passphrase, const QString &path)
{
KeyImport keyImport;
auto result = keyImport.exportKeys(passphrase, this);
if (!result.has_value()) {
qWarning() << "error TODO";
return;
}
Controller::instance().saveFileContent(path, result.value());
}
#include "moc_neochatconnection.cpp"

View File

@@ -160,6 +160,8 @@ public:
LinkPreviewer *previewerForLink(const QUrl &link);
Q_INVOKABLE void exportMegolmSessions(const QString &passphrase, const QString &path);
Q_SIGNALS:
void labelChanged();
void directChatNotificationsChanged();

View File

@@ -11,9 +11,9 @@
#include <Quotient/jobs/basejob.h>
#include <Quotient/quotient_common.h>
#include <Quotient/user.h>
#include <qcoro/qcorosignal.h>
#include <Quotient/avatar.h>
#include <Quotient/connection.h>
#include <Quotient/csapi/account-data.h>
#include <Quotient/csapi/directory.h>
@@ -101,15 +101,15 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
if (this->joinState() != JoinState::Invite) {
return;
}
auto roomMemberEvent = currentState().get<RoomMemberEvent>(localUser()->id());
auto roomMemberEvent = currentState().get<RoomMemberEvent>(localMember().id());
QImage avatar_image;
if (roomMemberEvent && !user(roomMemberEvent->senderId())->avatarUrl(this).isEmpty()) {
avatar_image = user(roomMemberEvent->senderId())->avatar(128, this);
if (roomMemberEvent && !member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
avatar_image = memberAvatar(roomMemberEvent->senderId()).get(this->connection(), 128, [] {});
} else {
qWarning() << "using this room's avatar";
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] {
Q_EMIT canEncryptRoomChanged();
@@ -255,28 +255,9 @@ void NeoChatRoom::forget()
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)
{
connection()->callApi<SetTypingJob>(BackgroundRequest, localUser()->id(), id(), isTyping, 10000);
connection()->callApi<SetTypingJob>(BackgroundRequest, localMember().id(), id(), isTyping, 10000);
}
const RoomEvent *NeoChatRoom::lastEvent() const
@@ -314,7 +295,7 @@ const RoomEvent *NeoChatRoom::lastEvent() const
}
}
if (connection()->isIgnored(user(event->senderId()))) {
if (connection()->isIgnored(event->senderId())) {
continue;
}
@@ -374,13 +355,13 @@ bool NeoChatRoom::isEventHighlighted(const RoomEvent *e) const
void NeoChatRoom::checkForHighlights(const Quotient::TimelineItem &ti)
{
auto localUserId = localUser()->id();
if (ti->senderId() == localUserId) {
auto localMember = this->localMember();
if (ti->senderId() == localMember.id()) {
return;
}
if (auto *e = ti.viewAs<RoomMessageEvent>()) {
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);
}
}
@@ -426,40 +407,6 @@ QDateTime NeoChatRoom::lastActiveTime()
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
{
if (const auto avatar = Room::avatarMediaId(); !avatar.isEmpty()) {
@@ -467,10 +414,10 @@ QString NeoChatRoom::avatarMediaId() const
}
// Use the first (excluding self) user's avatar for direct chats
const auto dcUsers = directChatUsers();
for (const auto u : dcUsers) {
if (u != localUser()) {
return u->avatarMediaId(this);
const auto directChatMembers = this->directChatMembers();
for (const auto member : directChatMembers) {
if (member != localMember()) {
return member.avatarMediaId();
}
}
@@ -637,7 +584,7 @@ void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction
continue;
}
if (e->senderId() == localUser()->id()) {
if (e->senderId() == localMember().id()) {
redactEventIds.push_back(e->id());
break;
}
@@ -666,7 +613,7 @@ bool NeoChatRoom::canSendEvent(const QString &eventType) const
return false;
}
auto pl = plEvent->powerLevelForEvent(eventType);
auto currentPl = plEvent->powerLevelForUser(localUser()->id());
auto currentPl = plEvent->powerLevelForUser(localMember().id());
return currentPl >= pl;
}
@@ -678,7 +625,7 @@ bool NeoChatRoom::canSendState(const QString &eventType) const
return false;
}
auto pl = plEvent->powerLevelForState(eventType);
auto currentPl = plEvent->powerLevelForUser(localUser()->id());
auto currentPl = plEvent->powerLevelForUser(localMember().id());
return currentPl >= pl;
}
@@ -856,7 +803,7 @@ void NeoChatRoom::setUrlPreviewEnabled(const bool &urlPreviewEnabled)
* "type": "org.matrix.room.preview_urls",
* }
*/
connection()->callApi<SetAccountDataPerRoomJob>(localUser()->id(),
connection()->callApi<SetAccountDataPerRoomJob>(localMember().id(),
id(),
"org.matrix.room.preview_urls"_ls,
QJsonObject{{"disable"_ls, !urlPreviewEnabled}});
@@ -1524,7 +1471,7 @@ void NeoChatRoom::editLastMessage()
}
// 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();
if (e->msgtype() != MessageEventType::Unknown) {
@@ -1649,13 +1596,9 @@ int NeoChatRoom::maxRoomVersion() const
return maxVersion;
}
Quotient::User *NeoChatRoom::directChatRemoteUser() const
Quotient::RoomMember NeoChatRoom::directChatRemoteMember() const
{
auto users = connection()->directChatUsers(this);
if (users.isEmpty()) {
return nullptr;
}
return users[0];
return directChatMembers()[0];
}
void NeoChatRoom::sendLocation(float lat, float lon, const QString &description)
@@ -1687,20 +1630,6 @@ QByteArray NeoChatRoom::roomAcountDataJson(const QString &eventType)
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();
}
}
const RoomEvent *NeoChatRoom::getReplyForEvent(const RoomEvent &event) const
{
const QString &replyEventId = event.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
@@ -1729,10 +1658,9 @@ void NeoChatRoom::loadReply(const QString &eventId, const QString &replyId)
Q_EMIT replyLoaded(eventId, replyId);
});
}
User *NeoChatRoom::invitingUser() const
QString NeoChatRoom::invitingUserId() 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)

View File

@@ -10,7 +10,7 @@
#include <QQmlEngine>
#include <QCoroTask>
#include <Quotient/user.h>
#include <Quotient/roommember.h>
#include "enums/pushrule.h"
#include "pollhandler.h"
@@ -40,27 +40,6 @@ class NeoChatRoom : public Quotient::Room
QML_ELEMENT
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.
*
@@ -93,9 +72,9 @@ class NeoChatRoom : public Quotient::Room
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.
@@ -235,60 +214,6 @@ public:
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();
/**
@@ -400,7 +325,7 @@ public:
[[nodiscard]] QString avatarMediaId() const;
Quotient::User *directChatRemoteUser() const;
Quotient::RoomMember directChatRemoteMember() const;
/**
* @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 [[nodiscard]] QUrl avatarForMember(Quotient::User *user) const;
/**
* @brief Returns the event that is being replied to. This includes events that were manually loaded using NeoChatRoom::loadReply.
*/
@@ -647,7 +570,7 @@ public:
/**
* 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:
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());
if (shouldPostNotification(connection, n)) {
// 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;
@@ -133,13 +133,13 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
}
QImage avatar_image;
if (!sender->avatarUrl(room).isEmpty()) {
avatar_image = sender->avatar(128, room);
if (!sender.avatarUrl().isEmpty()) {
avatar_image = room->memberAvatar(sender.id()).get(connection, 128, {});
} else {
avatar_image = room->avatar(128);
}
postNotification(dynamic_cast<NeoChatRoom *>(room),
sender->displayname(room),
sender.displayName(),
body,
avatar_image,
notification["event"_ls].toObject()["event_id"_ls].toString(),
@@ -213,7 +213,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
if (!room) {
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);
RoomManager::instance().setConnection(connection);
RoomManager::instance().resolveResource(room->id());
@@ -230,7 +230,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
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();
}
@@ -276,7 +276,7 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *rawRoom, const QS
return;
}
RoomManager::instance().leaveRoom(room);
room->connection()->addToIgnoredUsers(room->invitingUser());
room->connection()->addToIgnoredUsers(room->invitingUserId());
notification->close();
});
connect(notification, &KNotification::closed, this, [this, room]() {
@@ -286,7 +286,7 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *rawRoom, const QS
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();
m_invitations.insert(room->id(), notification);

View File

@@ -154,7 +154,7 @@ void PollHandler::sendPollAnswer(const QString &eventId, const QString &answerId
return;
}
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();
}
if (ownAnswers.contains(answerId)) {
@@ -169,7 +169,7 @@ void PollHandler::sendPollAnswer(const QString &eventId, const QString &answerId
}
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);
}

View File

@@ -70,6 +70,11 @@ QQC2.Menu {
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog'), {}, {
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
}
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")
icon.name: "edit-copy"
onTriggered: if (room.isDirectChat()) {
Clipboard.saveText(room.directChatRemoteUser.id);
Clipboard.saveText(room.directChatRemoteMember.id);
} else if (room.canonicalAlias.length === 0) {
Clipboard.saveText(room.id);
} else {

View File

@@ -40,18 +40,9 @@ Loader {
/**
* @brief The message author.
*
* This should consist of the following:
* - 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.
* A Quotient::RoomMember object.
*
* @sa Quotient::User
* @sa Quotient::RoomMember
*/
required property var author
@@ -90,7 +81,7 @@ Loader {
}
component RemoveMessageAction: Kirigami.Action {
visible: author.isLocalUser || currentRoom.canSendState("redact")
visible: author.isLocalMember || currentRoom.canSendState("redact")
text: i18n("Remove")
icon.name: "edit-delete-remove"
icon.color: "red"
@@ -116,7 +107,7 @@ Loader {
component ReportMessageAction: Kirigami.Action {
text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
icon.name: "dialog-warning-symbolic"
visible: !author.isLocalUser
visible: !author.isLocalMember
onTriggered: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReportSheet'), {
room: currentRoom,
eventId: eventId

View File

@@ -33,7 +33,7 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter
onClicked: {
RoomManager.resolveResource(root.room.directChatRemoteUser.id, "mention");
RoomManager.resolveResource(root.room.directChatRemoteMember.uri)
}
contentItem: KirigamiComponents.Avatar {
@@ -59,12 +59,27 @@ ColumnLayout {
}
}
Kirigami.Heading {
RowLayout {
Layout.fillWidth: true
type: Kirigami.Heading.Type.Primary
wrapMode: QQC2.Label.Wrap
text: root.room.displayName
textFormat: Text.PlainText
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
Kirigami.Icon {
id: securityIcon
//TODO figure out how to make this update
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 {
visible: author.id === currentRoom.localUser.id || currentRoom.canSendState("redact")
visible: author.id === currentRoom.localMember.id || currentRoom.canSendState("redact")
text: i18n("Remove")
icon.name: "edit-delete-remove"
icon.color: "red"

View File

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

View File

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

View File

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

View File

@@ -40,7 +40,7 @@ DelegateContextMenu {
currentRoom.editCache.editId = eventId;
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 {},
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 {
id: favouriteButton
visible: !root.room.isSpace
@@ -219,7 +229,7 @@ QQC2.ScrollView {
onClicked: {
userDelegate.highlighted = true;
RoomManager.resolveResource(userDelegate.userId, "mention");
RoomManager.resolveResource(root.room.member(userDelegate.userId).uri)
}
contentItem: RowLayout {

View File

@@ -263,8 +263,8 @@ QQC2.ScrollView {
TypingPane {
id: typingPane
visible: root.currentRoom && root.currentRoom.usersTyping.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(", ")) : ""
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.otherMembersTyping.length, root.currentRoom.otherMembersTyping.map(member => member.displayName).join(", ")) : ""
anchors.left: parent.left
anchors.bottom: parent.bottom
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.
// Make sure that code is prepared to deal with this property being null
property NeoChatRoom room
/**
* @brief The user's profile object.
*
* Required to interact with the profile and perform action like ignoring.
*/
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
leftPadding: 0
@@ -48,9 +61,9 @@ Kirigami.Dialog {
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
name: root.user.displayName
source: root.user.avatarSource
color: root.user.color
name: root.member.displayName
source: root.member.avatarUrl
color: root.member.color
}
ColumnLayout {
@@ -69,7 +82,7 @@ Kirigami.Dialog {
Kirigami.SelectableLabel {
textFormat: TextEdit.PlainText
text: root.user.id
text: root.member.id
}
}
QQC2.AbstractButton {
@@ -78,16 +91,16 @@ Kirigami.Dialog {
contentItem: Barcode {
id: barcode
barcodeType: Barcode.QRCode
content: "https://matrix.to/#/" + root.user.id
content: "https://matrix.to/#/" + root.member.id
}
onClicked: {
let map = qrMaximizeComponent.createObject(parent, {
text: barcode.content,
title: root.user.displayName,
subtitle: root.user.id,
avatarColor: root.user.color,
avatarSource: root.user.avatarSource
title: root.member.displayName,
subtitle: root.member.id,
avatarColor: root.member.color,
avatarSource: root.member.avatarUrl,
});
root.close();
map.open();
@@ -104,46 +117,46 @@ Kirigami.Dialog {
}
FormCard.FormButtonDelegate {
visible: !root.user.isLocalUser && !!root.user.object
visible: !root.member.isLocalMember
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"
onTriggered: {
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 {
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 {
text: i18n("Kick this user")
icon.name: "im-kick-user"
onTriggered: {
root.room.kickMember(root.user.id);
root.room.kickMember(root.member.id);
root.close();
}
}
}
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 {
enabled: root.room && !root.room.isUserBanned(root.user.id)
enabled: root.room && !root.room.isUserBanned(root.member.id)
text: i18n("Invite this user")
icon.name: "list-add-user"
onTriggered: {
root.room.inviteToRoom(root.user.id);
root.room.inviteToRoom(root.member.id);
root.close();
}
}
}
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 {
text: i18n("Ban this user")
@@ -152,7 +165,7 @@ Kirigami.Dialog {
onTriggered: {
(root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'BanSheet'), {
room: root.room,
userId: root.user.id
userId: root.member.id
}, {
title: i18nc("@title", "Ban User"),
width: Kirigami.Units.gridUnit * 25
@@ -163,14 +176,14 @@ Kirigami.Dialog {
}
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 {
text: i18n("Unban this user")
icon.name: "im-irc"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
root.room.unban(root.user.id);
root.room.unban(root.member.id);
root.close();
}
}
@@ -184,8 +197,8 @@ Kirigami.Dialog {
onTriggered: {
let dialog = powerLevelDialog.createObject(this, {
room: root.room,
userId: root.user.id,
powerLevel: root.room.getUserPowerLevel(root.user.id)
userId: root.member.id,
powerLevel: root.room.getUserPowerLevel(root.member.id)
});
dialog.open();
root.close();
@@ -201,7 +214,7 @@ Kirigami.Dialog {
}
FormCard.FormButtonDelegate {
visible: root.room && (root.user.isLocalUser || room.canSendState("redact"))
visible: root.room && (root.member.isLocalUser || room.canSendState("redact"))
action: Kirigami.Action {
text: i18n("Remove recent messages by this user")
@@ -210,7 +223,7 @@ Kirigami.Dialog {
onTriggered: {
applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RemoveSheet'), {
room: root.room,
userId: root.user.id
userId: root.member.id
}, {
title: i18nc("@title", "Remove Messages"),
width: Kirigami.Units.gridUnit * 25
@@ -221,12 +234,12 @@ Kirigami.Dialog {
}
FormCard.FormButtonDelegate {
visible: !root.user.isLocalUser
visible: !root.member.isLocalMember
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")
icon.name: "document-send"
onTriggered: {
root.connection.openOrCreateDirectChat(root.user.object);
root.room.connection.openOrCreateDirectChat(root.user);
root.close();
}
}
@@ -237,7 +250,7 @@ Kirigami.Dialog {
text: i18n("Copy link")
icon.name: "username-copy"
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()
{
setStatus(Working);
Omittable<QJsonObject> authData = none;
std::optional<QJsonObject> authData = std::nullopt;
if (nextStep() == "m.login.recaptcha"_ls) {
authData = QJsonObject{
{"type"_ls, "m.login.recaptcha"_ls},
@@ -176,7 +176,7 @@ void Registration::testHomeserver()
if (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]() {
if (m_testServerJob->error() == BaseJob::StatusCode::ContentAccessError) {
setStatus(ServerNoRegistration);
@@ -244,12 +244,12 @@ void Registration::setPassword(const QString &password)
}
NeoChatRegisterJob::NeoChatRegisterJob(const QString &kind,
const Omittable<QJsonObject> &auth,
const std::optional<QJsonObject> &auth,
const QString &username,
const QString &password,
const QString &deviceId,
const QString &initialDeviceDisplayName,
Omittable<bool> inhibitLogin)
std::optional<bool> inhibitLogin)
: BaseJob(HttpVerb::Post, "RegisterJob"_ls, QByteArrayLiteral("/_matrix/client/r0/register"), false)
{
QJsonObject _data;

View File

@@ -27,12 +27,12 @@ class NeoChatRegisterJob : public Quotient::BaseJob
{
public:
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 &password = {},
const QString &deviceId = {},
const QString &initialDeviceDisplayName = {},
Quotient::Omittable<bool> inhibitLogin = Quotient::none);
std::optional<bool> inhibitLogin = std::nullopt);
QString userId() const
{

View File

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

View File

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

View File

@@ -35,4 +35,6 @@ qt_add_qml_module(settings
PasswordSheet.qml
ThemeRadioButton.qml
ThreePIdCard.qml
ImportKeysDialog.qml
ExportKeysDialog.qml
)

View File

@@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtQuick.Dialogs
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.neochat
FormCard.FormCardPage {
id: root
title: i18nc("@title", "Export Keys")
required property NeoChatConnection connection
FormCard.FormCard {
Layout.topMargin: Kirigami.Units.largeSpacing
FormCard.FormTextFieldDelegate {
id: passphraseField
label: i18nc("@label:textbox", "Passphrase:")
echoMode: TextInput.Password
}
FormCard.FormButtonDelegate {
enabled: passphraseField.text.length > 0
text: i18nc("@action:button", "Export keys")
onClicked: {
let dialog = saveDialog.createObject(root);
dialog.accepted.connect(() => {
root.connection.exportMegolmSessions(passphraseField.text, dialog.selectedFile);
});
dialog.open();
}
}
}
Component {
id: saveDialog
FileDialog {
fileMode: FileDialog.SaveFile
currentFolder: Config.lastSaveDirectory.length > 0 ? Config.lastSaveDirectory : StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
}
}
}

View File

@@ -0,0 +1,73 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat
FormCard.FormCardPage {
id: root
required property NeoChatConnection connection
title: i18nc("@title", "Import Keys")
header: KirigamiComponents.Banner {
id: banner
showCloseButton: true
visible: false
type: Kirigami.MessageType.Error
}
FormCard.FormCard {
Layout.topMargin: Kirigami.Units.largeSpacing
FormCard.FormButtonDelegate {
id: fileButton
text: i18nc("@action:button", "Choose backup file")
description: ""
icon.name: "cloud-upload"
onClicked: {
let dialog = Qt.createComponent("org.kde.neochat", "OpenFileDialog").createObject(root);
dialog.chosen.connect(path => {
fileButton.description = path.substring(7);
});
dialog.open();
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextFieldDelegate {
id: passphraseField
label: i18nc("@label:textbox", "Passphrase:")
echoMode: TextInput.Password
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Import keys")
enabled: fileButton.description.length > 0 && passphraseField.text.length > 0
onClicked: {
banner.visible = false;
let error = KeyImport.importKeys(Controller.loadFileContent(fileButton.description), passphraseField.text, root.connection);
if (error === KeyImport.Success) {
root.closeDialog();
} else if (error === KeyImport.InvalidPassphrase) {
banner.text = i18nc("@info", "Invalid passphrase");
banner.visible = true;
} else if (error === KeyImport.InvalidData) {
banner.text = i18nc("@info", "Invalid key backup data");
banner.visible = true;
} else {
banner.text = i18nc("@info", "Unknown error");
banner.visible = true;
}
}
}
}
}

View File

@@ -33,6 +33,31 @@ FormCard.FormCardPage {
description: i18n("Device id")
}
}
FormCard.FormHeader {
title: i18nc("@title", "Encryption Keys")
}
FormCard.FormCard {
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Import Encryption Keys")
icon.name: "document-import"
onClicked: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat.settings", "ImportKeysDialog"), {
connection: root.connection
}, {
title: i18nc("@title", "Import Keys")
})
}
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Export Encryption Keys")
icon.name: "document-export"
onClicked: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat.settings", "ExportKeysDialog"), {
connection: root.connection
}, {
title: i18nc("@title", "Export Keys")
})
}
}
FormCard.FormHeader {
title: i18nc("@title:group", "Ignored Users")
}

View File

@@ -61,8 +61,8 @@ FormCard.FormCardPage {
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
id: powerLevelLabel
visible: !room.canSendState("m.room.power_levels") || (room.getUserPowerLevel(room.localUser.id) <= privilegedUserDelegate.powerLevel && privilegedUserDelegate.userId != room.localUser.id)
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
}
QQC2.ComboBox {

View File

@@ -57,7 +57,7 @@ void SpaceHierarchyCache::populateSpaceHierarchy(const QString &spaceId)
}
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);
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();
if (!nextBatchToken.isEmpty() && nextBatchToken != *m_nextBatchTokens[spaceId]) {
*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]() {
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 author = room->user(e->senderId());
return QStringLiteral("* <a href=\"https://matrix.to/#/") + e->senderId() + QStringLiteral("\" style=\"color:") + Utils::getUserColor(author->hueF()).name()
+ QStringLiteral("\">") + author->displayname(room) + QStringLiteral("</a> ");
auto author = room->member(e->senderId());
return QStringLiteral("* <a href=\"https://matrix.to/#/") + e->senderId() + QStringLiteral("\" style=\"color:") + author.color().name()
+ QStringLiteral("\">") + author.htmlSafeDisplayName() + QStringLiteral("</a> ");
}
QString TextHandler::convertCodeLanguageString(const QString &languageString)

View File

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

View File

@@ -35,18 +35,9 @@ QQC2.Control {
/**
* @brief The message author.
*
* This should consist of the following:
* - 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.
* A Quotient::RoomMember object.
*
* @sa Quotient::User
* @sa Quotient::RoomMember
*/
property var author
@@ -125,14 +116,14 @@ QQC2.Control {
id: nameButton
Layout.fillWidth: true
contentItem: QQC2.Label {
text: root.author.displayName
text: root.author.disambiguatedName
color: root.author.color
textFormat: Text.PlainText
font.weight: Font.Bold
elide: Text.ElideRight
}
Accessible.name: contentItem.text
onClicked: RoomManager.resolveResource(root.author.id, "mention")
onClicked: RoomManager.resolveResource(root.author.uri)
}
QQC2.Label {
id: timeLabel
@@ -176,7 +167,7 @@ QQC2.Control {
visible: root.showBackground
Kirigami.Theme.colorSet: Kirigami.Theme.View
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);
} else if (root.showHighlight) {
return Kirigami.Theme.positiveBackgroundColor;

View File

@@ -16,18 +16,9 @@ QQC2.Control {
/**
* @brief The message author.
*
* This should consist of the following:
* - 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.
* A Quotient::RoomMember object.
*
* @sa Quotient::User
* @sa Quotient::RoomMember
*/
required property var author

View File

@@ -19,18 +19,9 @@ ColumnLayout {
/**
* @brief The message author.
*
* This should consist of the following:
* - 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.
* A Quotient::RoomMember object.
*
* @sa Quotient::User
* @sa Quotient::RoomMember
*/
required property var author

View File

@@ -56,18 +56,9 @@ TimelineDelegate {
/**
* @brief The message author.
*
* This should consist of the following:
* - 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.
* A Quotient::RoomMember object.
*
* @sa Quotient::User
* @sa Quotient::RoomMember
*/
required property var author
@@ -281,11 +272,11 @@ TimelineDelegate {
visible: (root.showAuthor || root.alwaysShowAuthor) && Config.showAvatarInTimeline && (Config.compactLayout || !_private.showUserMessageOnRight)
name: root.author.displayName
source: root.author.avatarSource
source: root.author.avatarUrl
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 {
id: bubble
@@ -411,7 +402,7 @@ TimelineDelegate {
/**
* @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() {
RoomManager.viewEventMenu(root.eventId, root.room, root.selectedText);

View File

@@ -52,7 +52,7 @@ ColumnLayout {
delegate: RowLayout {
Layout.fillWidth: true
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"])
enabled: !root.pollHandler.hasEnded
}

View File

@@ -35,7 +35,7 @@ Flow {
required property string textContent
required property string reaction
required property string toolTip
required property bool hasLocalUser
required property bool hasLocalMember
width: Math.max(contentItem.implicitWidth + leftPadding + rightPadding, height)
height: Math.round(Kirigami.Units.gridUnit * 1.5)
@@ -54,13 +54,13 @@ Flow {
padding: Kirigami.Units.smallSpacing
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.colorSet: Config.compactLayout ? Kirigami.Theme.Window : Kirigami.Theme.View
radius: height / 2
shadow {
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

@@ -37,18 +37,9 @@ RowLayout {
/**
* @brief The reply author.
*
* This should consist of the following:
* - 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.
* A Quotient::RoomMember object.
*
* @sa Quotient::User
* @sa Quotient::RoomMember
*/
required property var replyAuthor
@@ -116,7 +107,7 @@ RowLayout {
implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: Kirigami.Units.iconSizes.small
source: root.replyAuthor.avatarSource
source: root.replyAuthor.avatarUrl
name: root.replyAuthor.displayName
color: root.replyAuthor.color
}

View File

@@ -24,18 +24,9 @@ RowLayout {
/**
* @brief The message author.
*
* This should consist of the following:
* - 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.
* A Quotient::RoomMember object.
*
* @sa Quotient::User
* @sa Quotient::RoomMember
*/
property var author: modelData.author

View File

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

View File

@@ -7,45 +7,6 @@
#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)
{
return !QJsonDocument::fromJson(json).isNull();

View File

@@ -3,14 +3,9 @@
#pragma once
#include <QColor>
#include <QGuiApplication>
#include <QPalette>
#include <QQmlEngine>
#include <QRegularExpression>
#include <Quotient/user.h>
class QmlUtils : public QObject
{
Q_OBJECT
@@ -30,31 +25,12 @@ public:
return _instance;
}
Q_INVOKABLE QVariantMap getUser(Quotient::User *user) const;
Q_INVOKABLE bool isValidJson(const QByteArray &json);
private:
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
{
static const QRegularExpression endTagType{QStringLiteral("(>| )")};