From 516beab06759d7f4b79e939c9fff13787cdb5114 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 20 Sep 2024 12:05:32 +0200 Subject: [PATCH] Port to libQuotient 0.9 --- client/chatedit.cpp | 70 ++++++--- client/chatedit.h | 18 ++- client/chatroomwidget.cpp | 217 +++++++++++++++++----------- client/chatroomwidget.h | 21 ++- client/logindialog.cpp | 17 +-- client/mainwindow.cpp | 23 ++- client/models/messageeventmodel.cpp | 43 +++--- client/models/roomlistmodel.cpp | 14 +- client/models/userlistmodel.cpp | 113 +++++++-------- client/models/userlistmodel.h | 23 ++- client/profiledialog.cpp | 4 +- client/qml/Avatar.qml | 7 +- client/qml/Timeline.qml | 14 +- client/quaternionroom.cpp | 39 ++--- client/quaternionroom.h | 66 ++++----- client/roomdialogs.cpp | 143 ++++++++---------- client/thumbnailprovider.cpp | 142 +++++------------- client/thumbnailprovider.h | 1 - client/timelinewidget.cpp | 33 ++--- client/userlistdock.cpp | 40 ++--- client/userlistdock.h | 9 +- lib | 2 +- 22 files changed, 507 insertions(+), 552 deletions(-) diff --git a/client/chatedit.cpp b/client/chatedit.cpp index 6c8d0637..7ca11678 100644 --- a/client/chatedit.cpp +++ b/client/chatedit.cpp @@ -86,17 +86,50 @@ void ChatEdit::contextMenuEvent(QContextMenuEvent *event) menu->popup(event->globalPos()); } +void ChatEdit::insertFromMimeData(const QMimeData* source) { acceptMimeData(source); } + void ChatEdit::switchContext(QObject* contextKey) { cancelCompletion(); KChatEdit::switchContext(contextKey); } -bool ChatEdit::canInsertFromMimeData(const QMimeData *source) const +bool ChatEdit::canInsertFromMimeData(const QMimeData* source) const { + if (!source) + return false; + // When not in a room, only allow dropping plain text (for commands) + if (!chatRoomWidget->currentRoom()) + return source->hasText(); + return source->hasImage() || KChatEdit::canInsertFromMimeData(source); } +QString ChatEdit::checkDndEvent(QDropEvent* event) +{ + if (const auto* data = event->mimeData(); data->hasHtml()) { + const auto [cleanHtml, errorPos, errorString] = + HtmlFilter::fromLocalHtml(data->html()); + if (errorPos != -1) { + qCWarning(MSGINPUT) << "HTML validation failed at position" + << errorPos << "with error" << errorString; + event->ignore(); + return tr( + "Cannot insert HTML - it's either invalid or unsupported"); + } + } + event->setDropAction(Qt::CopyAction); + event->accept(); + return {}; +} + +void ChatEdit::dragEnterEvent(QDragEnterEvent* event) +{ + KChatEdit::dragEnterEvent(event); + if (event->source() != this) + checkDndEvent(event); +} + void ChatEdit::alternatePaste() { m_pastePlaintext = !pastePlaintextByDefault(); @@ -104,16 +137,15 @@ void ChatEdit::alternatePaste() m_pastePlaintext = pastePlaintextByDefault(); } -void ChatEdit::insertFromMimeData(const QMimeData *source) +bool ChatEdit::acceptMimeData(const QMimeData* source) { if (!source) { - qCWarning(MSGINPUT) << "Nothing to insert"; - return; + qCWarning(MSGINPUT) << "Nothing to insert from the drop event"; + return true; // Treat it as nothing to do, not an error } if (source->hasImage()) - chatRoomWidget->attachImage(source->imageData().value(), - source->urls()); + chatRoomWidget->attachImage(source->imageData().value(), source->urls()); else if (source->hasHtml()) { if (m_pastePlaintext) { QTextDocument document; @@ -126,29 +158,23 @@ void ChatEdit::insertFromMimeData(const QMimeData *source) if (errorPos != -1) { qCWarning(MSGINPUT) << "HTML insertion failed at pos" << errorPos << "with error" << errorString; - // FIXME: Come on... It should be app->showStatusMessage() or smth - emit chatRoomWidget->timelineWidget()->showStatusMessage( - tr("Could not insert HTML - it's either invalid or unsupported"), - 5000); - return; + chatRoomWidget->showStatusMessage( + tr("Could not insert HTML - it's either invalid or unsupported"), 5000); + return false; } insertHtml(cleanHtml); } ensureCursorVisible(); } else if (source->hasUrls()) { - bool hasAnyProcessed = false; - for (const QUrl &url : source->urls()) - if (url.isLocalFile()) { - chatRoomWidget->dropFile(url.toLocalFile()); - hasAnyProcessed = true; - // Only the first url is processed for now - break; - } - if (!hasAnyProcessed) { + const auto& urls = source->urls(); + // Only the first local url is processed for now + if (auto urlIt = std::ranges::find(urls, true, &QUrl::isLocalFile); urlIt != urls.cend()) + chatRoomWidget->dropFile(urlIt->toLocalFile()); + else KChatEdit::insertFromMimeData(source); - } } else KChatEdit::insertFromMimeData(source); + return true; } void ChatEdit::appendMentionAt(QTextCursor& cursor, QString mention, @@ -243,7 +269,7 @@ void ChatEdit::triggerCompletion() QStringList matchesForSignal; for (const auto& p: completionMatches) matchesForSignal.push_back(p.first); - emit proposedCompletion(matchesForSignal, matchesListPosition); + chatRoomWidget->showCompletions(matchesForSignal, matchesListPosition); matchesListPosition = (matchesListPosition + 1) % completionMatches.length(); } diff --git a/client/chatedit.h b/client/chatedit.h index 42892f7b..efa45470 100644 --- a/client/chatedit.h +++ b/client/chatedit.h @@ -27,19 +27,21 @@ class ChatEdit : public KChatEdit bool isCompletionActive(); void insertMention(QString author, QUrl url); + bool acceptMimeData(const QMimeData* source); + QString checkDndEvent(QDropEvent* event); + + // NB: the following virtual functions are protected in QTextEdit but + // ChatRoomWidget delegates to them + + bool canInsertFromMimeData(const QMimeData* source) const override; public slots: void switchContext(QObject* contextKey) override; void alternatePaste(); signals: - void proposedCompletion(const QStringList& allCompletions, int curIndex); void cancelledCompletion(); - protected: - bool canInsertFromMimeData(const QMimeData* source) const override; - void insertFromMimeData(const QMimeData* source) override; - private: ChatRoomWidget* chatRoomWidget; @@ -60,7 +62,9 @@ class ChatEdit : public KChatEdit QUrl mentionUrl, bool select); void keyPressEvent(QKeyEvent* event) override; void contextMenuEvent(QContextMenuEvent* event) override; - bool pastePlaintextByDefault(); -}; + void insertFromMimeData(const QMimeData* source) override; + void dragEnterEvent(QDragEnterEvent* event) override; + static bool pastePlaintextByDefault(); +}; diff --git a/client/chatroomwidget.cpp b/client/chatroomwidget.cpp index 49f7f840..a0e97d9c 100644 --- a/client/chatroomwidget.cpp +++ b/client/chatroomwidget.cpp @@ -107,38 +107,7 @@ ChatRoomWidget::ChatRoomWidget(MainWindow* parent) ? m_chatEdit->textCursor().selectedText() : m_timelineWidget->selectedText()); }); - connect(m_chatEdit, &ChatEdit::proposedCompletion, this, - [this](QStringList matches, int pos) { - Q_ASSERT(pos >= 0 && pos < matches.size()); - // If the completion list is MaxNamesToShow or shorter, show all - // of it; if it's longer, show SampleSizeForHud entries and - // append how many more matches are there. - // #344: in any case, drop the current match from the list - // ("Next completion:" showing the current match looks wrong) - - switch (matches.size()) { - case 0: - setHudHtml(tr("No completions")); - return; - case 1: - setHudHtml({}); // That one match is already in the text - return; - default:; - } - matches.removeAt(pos); // Drop the current match (#344) - - // Replenish the tail of the list from the beginning, if needed - std::rotate(matches.begin(), matches.begin() + pos, - matches.end()); - if (matches.size() > MaxNamesToShow) { - const auto moreIt = matches.begin() + SampleSizeForHud; - *moreIt = tr("%Ln more completions", "", - matches.size() - SampleSizeForHud); - matches.erase(moreIt + 1, matches.end()); - } - setHudHtml(tr("Next completion:"), matches); - }); - // When completion is cancelled, show typing users, if any + // When completion is cancelled, revert to showing typing users, if any connect(m_chatEdit, &ChatEdit::cancelledCompletion, this, &ChatRoomWidget::typingChanged); @@ -155,6 +124,7 @@ ChatRoomWidget::ChatRoomWidget(MainWindow* parent) if (!styleSheet.isEmpty()) setStyleSheet(styleSheet); } + setAcceptDrops(true); // see dragEnteredEvent(), dropEvent() auto* layout = new QVBoxLayout(); layout->addWidget(m_timelineWidget); @@ -183,6 +153,11 @@ QuaternionRoom* ChatRoomWidget::currentRoom() const return m_timelineWidget->currentRoom(); } +Quotient::Connection* ChatRoomWidget::currentConnection() const +{ + return currentRoom()->connection(); +} + void ChatRoomWidget::setRoom(QuaternionRoom* newRoom) { if (currentRoom() == newRoom) { @@ -191,7 +166,7 @@ void ChatRoomWidget::setRoom(QuaternionRoom* newRoom) } if (currentRoom()) { - currentRoom()->connection()->disconnect(this); + currentConnection()->disconnect(this); currentRoom()->disconnect(this); } cancelAttaching(); @@ -217,24 +192,24 @@ void ChatRoomWidget::setRoom(QuaternionRoom* newRoom) void ChatRoomWidget::typingChanged() { - if (!currentRoom() || currentRoom()->usersTyping().isEmpty()) + if (!currentRoom() || currentRoom()->membersTyping().isEmpty()) { setHudHtml({}); return; } - const auto& usersTyping = currentRoom()->usersTyping(); + const auto& membersTyping = currentRoom()->membersTyping(); + const auto endIt = membersTyping.size() > MaxNamesToShow + ? membersTyping.cbegin() + SampleSizeForHud + : membersTyping.cend(); QStringList typingNames; typingNames.reserve(MaxNamesToShow); - const auto endIt = usersTyping.size() > MaxNamesToShow - ? usersTyping.cbegin() + SampleSizeForHud - : usersTyping.cend(); - for (auto it = usersTyping.cbegin(); it != endIt; ++it) - typingNames << currentRoom()->safeMemberName((*it)->id()); + std::transform(membersTyping.cbegin(), endIt, std::back_inserter(typingNames), + std::mem_fn(&Quotient::RoomMember::disambiguatedName)); - if (usersTyping.size() > MaxNamesToShow) { + if (membersTyping.size() > MaxNamesToShow) { typingNames.push_back( //: The number of users in the typing or completion list - tr("%L1 more").arg(usersTyping.size() - SampleSizeForHud)); + tr("%L1 more").arg(membersTyping.size() - SampleSizeForHud)); } setHudHtml(tr("Currently typing:"), typingNames); } @@ -245,7 +220,7 @@ void ChatRoomWidget::encryptionChanged() currentRoom() ? tr("Send a message (over %1) or enter a command...", "%1 is the protocol used by the server (usually HTTPS)") - .arg(currentRoom()->connection()->homeserver().scheme().toUpper()) + .arg(currentConnection()->homeserver().scheme().toUpper()) : DefaultPlaceholderText()); } @@ -284,12 +259,48 @@ void ChatRoomWidget::setHudHtml(const QString& htmlCaption, m_hudCaption->setText(hudText); } -void ChatRoomWidget::insertMention(Quotient::User* user) +void ChatRoomWidget::showStatusMessage(const QString& message, int timeout) +{ + mainWindow()->showStatusMessage(message, timeout); +} + +void ChatRoomWidget::showCompletions(QStringList matches, int pos) +{ + Q_ASSERT(pos >= 0 && pos < matches.size()); + // If the completion list is MaxNamesToShow or shorter, show all + // of it; if it's longer, show SampleSizeForHud entries and + // append how many more matches are there. + // #344: in any case, drop the current match from the list + // ("Next completion:" showing the current match looks wrong) + + switch (matches.size()) { + case 0: + setHudHtml(tr("No completions")); + return; + case 1: + setHudHtml({}); // That one match is already in the text + return; + default:; + } + matches.removeAt(pos); // Drop the current match (#344) + + // Replenish the tail of the list from the beginning, if needed + std::rotate(matches.begin(), matches.begin() + pos, + matches.end()); + if (matches.size() > MaxNamesToShow) { + matches[SampleSizeForHud] = + tr("%Ln more completions", "", static_cast(matches.size() - SampleSizeForHud)); + matches.resize(SampleSizeForHud); + } + setHudHtml(tr("Next completion:"), matches); +} + +void ChatRoomWidget::insertMention(const QString& userId) { Q_ASSERT(currentRoom() != nullptr); - m_chatEdit->insertMention( - user->displayname(currentRoom()), - Quotient::Uri(user->id()).toUrl(Quotient::Uri::MatrixToUri)); + const auto member = currentRoom()->member(userId); + m_chatEdit->insertMention(member.displayName(), + Quotient::Uri(member.id()).toUrl(Quotient::Uri::MatrixToUri)); m_chatEdit->setFocus(); } @@ -312,12 +323,12 @@ void ChatRoomWidget::attachImage(const QImage& img, const QList& sources) m_attachAction->setChecked(true); m_chatEdit->setPlaceholderText(AttachedPlaceholderText()); mainWindow()->showStatusMessage(tr("Attaching the pasted image")); - // TODO, 0.0.97: tr("... from %1").arg(localPath) } QString ChatRoomWidget::attachFile(const QString& localPath) { - Q_ASSERT(currentRoom() != nullptr); + if (QUO_ALARM(currentRoom() == nullptr)) + return tr("Can't attach a file without a selected room"); qCDebug(MSGINPUT) << "Trying to attach" << localPath; m_fileToAttach = std::make_unique(localPath); @@ -341,13 +352,11 @@ void ChatRoomWidget::dropFile(const QString& localPath) QString ChatRoomWidget::checkAttachment() { Q_ASSERT(m_fileToAttach != nullptr); - if (m_fileToAttach->isReadable() - || m_fileToAttach->open(QIODevice::ReadOnly)) + if (m_fileToAttach->isReadable() || m_fileToAttach->open(QIODevice::ReadOnly)) return {}; // Form the message in advance while the file name is still there - const auto msg = - tr("%1 is not readable or not a file").arg(m_fileToAttach->fileName()); + const auto msg = tr("%1 is not readable or not a file").arg(m_fileToAttach->fileName()); cancelAttaching(); return msg; } @@ -388,35 +397,36 @@ QVector lazySplitRef(const QString& s, QChar sep, int maxParts) return parts; } -Quotient::EventContent::TypedBase* contentFromFile(const QFileInfo& file) +std::unique_ptr contentFromFile(const QFileInfo& file) { using namespace Quotient::EventContent; + using namespace Quotient::Literals; auto filePath = file.absoluteFilePath(); auto localUrl = QUrl::fromLocalFile(filePath); auto mimeType = QMimeDatabase().mimeTypeForFile(file); auto mimeTypeName = mimeType.name(); - if (mimeTypeName.startsWith("image/")) - return new ImageContent(localUrl, file.size(), mimeType, - QImageReader(filePath).size(), file.fileName()); + if (mimeTypeName.startsWith("image/"_L1)) + return std::make_unique(localUrl, file.size(), mimeType, + QImageReader(filePath).size(), file.fileName()); - if (mimeTypeName.startsWith("audio/")) - return new AudioContent(localUrl, file.size(), mimeType, - file.fileName()); + if (mimeTypeName.startsWith("audio/"_L1)) + return std::make_unique(localUrl, file.size(), mimeType, file.fileName()); // TODO: video files support - return new FileContent(localUrl, file.size(), mimeType, file.fileName()); + return std::make_unique(localUrl, file.size(), mimeType, file.fileName()); } QString ChatRoomWidget::sendFile() { Q_ASSERT(currentRoom() != nullptr); const auto& description = m_chatEdit->toPlainText(); - if (const auto error = checkAttachment(); !error.isEmpty()) - return error; - QFileInfo fileInfo(*m_fileToAttach); + if (!fileInfo.isReadable() || !fileInfo.isFile()) + return tr("%1 is not readable or not a file") + .arg(m_fileToAttach->fileName()); + currentRoom()->postFile(description.isEmpty() ? fileInfo.fileName() : description, contentFromFile(fileInfo)); @@ -519,7 +529,7 @@ QString ChatRoomWidget::sendCommand(QStringView command, return tr("%1 doesn't look like a room id or alias").arg(argString); // Forget the specified room using the current room's connection - currentRoom()->connection()->forgetRoom(argString); + currentConnection()->forgetRoom(argString); return {}; } if (command == u"invite") @@ -569,7 +579,7 @@ QString ChatRoomWidget::sendCommand(QStringView command, if (!argString.contains(UserIdRE)) return tr("/ignore argument doesn't look like a user ID"); - if (auto* user = currentRoom()->user(argString)) + if (auto* user = currentConnection()->user(argString)) { if (command == u"ignore") user->ignore(); @@ -612,12 +622,12 @@ QString ChatRoomWidget::sendCommand(QStringView command, } if (command == u"nick" || command == u"mynick") { - currentRoom()->localUser()->rename(argString); + currentConnection()->user()->rename(argString); return {}; } if (command == u"roomnick" || command == u"myroomnick") { - currentRoom()->localUser()->rename(argString, currentRoom()); + currentConnection()->user()->rename(argString, currentRoom()); return {}; } if (command == u"pm" || command == u"msg") @@ -634,15 +644,13 @@ QString ChatRoomWidget::sendCommand(QStringView command, return {}; } return tr("%1 doesn't seem to have joined room %2") - .arg(currentRoom()->localUser()->id(), args.front()); + .arg(currentRoom()->localMember().id(), args.front()); } if (UserIdRE.match(args.front()).hasMatch()) { - if (args.back().isEmpty()) - currentRoom()->connection()->requestDirectChat(args.front()); - else - currentRoom()->connection()->doInDirectChat(args.front(), - [msg=args.back()] (Room* dc) { dc->postPlainText(msg); }); + auto futureChat = currentConnection()->getDirectChat(args.front()); + if (!args.back().isEmpty()) + futureChat.then([msg=args.back()] (Room* dc) { dc->postPlainText(msg); }); return {}; } @@ -695,7 +703,7 @@ QString ChatRoomWidget::sendCommand(QStringView command, if (!argString.contains(UserIdRE)) return tr("%1 doesn't look like a user id").arg(argString); - currentRoom()->connection()->requestDirectChat(argString); + currentConnection()->requestDirectChat(argString); return {}; } // --- Add more room commands here @@ -726,7 +734,7 @@ void ChatRoomWidget::sendInput() sendMessage(); } if (!error.isEmpty()) { - mainWindow()->showStatusMessage(error, 5000); + showStatusMessage(error, 5000); return; } m_chatEdit->setPlaceholderText(DefaultPlaceholderText()); @@ -738,14 +746,13 @@ ChatRoomWidget::findCompletionMatches(const QString& pattern) const { completions_t matches; if (currentRoom()) { - const auto& users = currentRoom()->users(); - for (auto user: users) { + const auto& members = currentRoom()->joinedMembers(); + for (const auto& m: members) { using Quotient::Uri; - if (user->displayname(currentRoom()) + if (m.displayName() .startsWith(pattern, Qt::CaseInsensitive) - || user->id().startsWith(pattern, Qt::CaseInsensitive)) - matches.push_back({ user->displayname(currentRoom()), - Uri(user->id()).toUrl(Uri::MatrixToUri) }); + || m.id().startsWith(pattern, Qt::CaseInsensitive)) + matches.push_back({ m.displayName(), Uri(m.id()).toUrl(Uri::MatrixToUri) }); } std::sort(matches.begin(), matches.end(), [] (const auto& p1, const auto& p2) @@ -808,6 +815,52 @@ void ChatRoomWidget::keyPressEvent(QKeyEvent* event) } } +void ChatRoomWidget::dragEnterEvent(QDragEnterEvent* event) +{ + if (event->source() == m_chatEdit + || !m_chatEdit->canInsertFromMimeData(event->mimeData())) { + event->ignore(); + return; + } + m_chatEdit->checkDndEvent(event); +} + +void ChatRoomWidget::dropEvent(QDropEvent* event) +{ + Q_ASSERT(event != nullptr); // Something very wrong with Qt if that fails + auto* source = event->mimeData(); + if (!source) { + qCWarning(MSGINPUT) << "Nothing to insert from the drop event"; + return; + } + + event->setDropAction(Qt::CopyAction); // A default, but you never know + qCDebug(MSGINPUT) << "MIME arrived:" << source->formats().join(u','); + if (source->hasUrls()) + qCDebug(MSGINPUT) << "MIME URLs:" << source->urls(); + if (source->hasImage()) { + attachImage(source->imageData().value(), source->urls()); + event->accept(); + } else if (source->hasHtml()) { + if (m_chatEdit->acceptMimeData(source)) + event->accept(); + return; + } else if (source->hasUrls()) { + bool hasAnyProcessed = false; + for (const QUrl& url : source->urls()) + if (url.isLocalFile()) { + attachFile(url.toLocalFile()); + hasAnyProcessed = true; + // Only the first url is processed for now + break; + } + if (hasAnyProcessed) + event->accept(); + } + if (m_chatEdit->acceptMimeData(source)) + event->accept(); +} + int ChatRoomWidget::maximumChatEditHeight() const { return height() / 3; diff --git a/client/chatroomwidget.h b/client/chatroomwidget.h index 8661c048..044bb050 100644 --- a/client/chatroomwidget.h +++ b/client/chatroomwidget.h @@ -15,6 +15,10 @@ #include #include +namespace Quotient { +class Connection; +} + class TimelineWidget; class QuaternionRoom; class MainWindow; @@ -22,10 +26,6 @@ class MainWindow; class QLabel; class QAction; -namespace Quotient { -class User; -} - class ChatRoomWidget : public QWidget { Q_OBJECT @@ -34,12 +34,13 @@ class ChatRoomWidget : public QWidget explicit ChatRoomWidget(MainWindow* parent = nullptr); TimelineWidget* timelineWidget() const; + QuaternionRoom* currentRoom() const; completions_t findCompletionMatches(const QString& pattern) const; public slots: void setRoom(QuaternionRoom* newRoom); - void insertMention(Quotient::User* user); + void insertMention(const QString &userId); void attachImage(const QImage& img, const QList& sources); QString attachFile(const QString& localPath); void dropFile(const QString& localPath); @@ -47,11 +48,13 @@ class ChatRoomWidget : public QWidget void cancelAttaching(); void focusInput(); - /// Set a line just above the message input, with optional list of - /// member displaynames + //! Set a line above the message input, with optional list of member displaynames void setHudHtml(const QString& htmlCaption, const QStringList& plainTextNames = {}); + void showStatusMessage(const QString& message, int timeout = 0); + void showCompletions(QStringList matches, int pos); + void typingChanged(); void quote(const QString& htmlText); @@ -69,7 +72,7 @@ class ChatRoomWidget : public QWidget Quotient::SettingsGroup m_uiSettings; MainWindow* mainWindow() const; - QuaternionRoom* currentRoom() const; + Quotient::Connection* currentConnection() const; QString sendFile(); void sendMessage(); @@ -78,6 +81,8 @@ class ChatRoomWidget : public QWidget void resizeEvent(QResizeEvent*) override; void keyPressEvent(QKeyEvent* event) override; + void dragEnterEvent(QDragEnterEvent* event) override; + void dropEvent(QDropEvent* event) override; int maximumChatEditHeight() const; }; diff --git a/client/logindialog.cpp b/client/logindialog.cpp index 9f8adeb8..9c452cdf 100644 --- a/client/logindialog.cpp +++ b/client/logindialog.cpp @@ -170,10 +170,10 @@ void LoginDialog::setup(const QString& statusMessage) }); connect(m_connection.get(), &Connection::loginFlowsChanged, this, [this] { serverEdit->setText(m_connection->homeserver().toString()); - setStatusMessage(m_connection->isUsable() + setStatusMessage(!m_connection->loginFlows().empty() ? tr("The homeserver is available") : tr("Could not connect to the homeserver")); - button(QDialogButtonBox::Ok)->setEnabled(m_connection->isUsable()); + button(QDialogButtonBox::Ok)->setEnabled(!m_connection->loginFlows().isEmpty()); }); // This overrides the above in case of an unsuccessful attempt to resolve // the server URL from a changed MXID @@ -234,15 +234,10 @@ void LoginDialog::apply() else if (!url.isValid()) applyFailed(MalformedServerUrl); else { - m_connection->setHomeserver(url); - - // Wait for new flows and check them - connectSingleShot(m_connection.get(), &Connection::loginFlowsChanged, - this, [this] { - qCDebug(MAIN) - << "Received login flows, trying to login"; - loginWithBestFlow(); - }); + m_connection->setHomeserver(url).then([this](auto) { + qCDebug(MAIN) << "Received login flows, trying to login"; + loginWithBestFlow(); + }); } } diff --git a/client/mainwindow.cpp b/client/mainwindow.cpp index 5e33c54f..06684138 100644 --- a/client/mainwindow.cpp +++ b/client/mainwindow.cpp @@ -829,7 +829,7 @@ class ConnectionInitiator : public QObject { void tryConnection() { - connection->assumeIdentity(userId, accessToken); + connection->assumeIdentity(userId, deviceId, accessToken); } void onNetworkError(const QString& error) { @@ -987,7 +987,7 @@ Quotient::UriResolveResult MainWindow::visitUser(Quotient::User* user, const QString& action) { if (action == "mention" || action.isEmpty()) - chatRoomWidget->insertMention(user); + chatRoomWidget->insertMention(user->id()); // action=_interactive is checked in openResource() and // converted to "chat" in openUserInput() else if (action == "_interactive" @@ -1014,16 +1014,13 @@ void MainWindow::joinRoom(Quotient::Connection* account, const QString& roomAliasOrId, const QStringList& viaServers) { - auto* job = account->joinRoom(roomAliasOrId, viaServers); // Connection::joinRoom() already connected to success() the code that // initialises the room in the library, which in turn causes RoomListModel // to update the room list. So the below connection to success() will be // triggered after all the initialisation have happened. - connect(job, &Quotient::BaseJob::success, this, - [this, account, roomAliasOrId] { - statusBar()->showMessage( - tr("Joined %1 as %2").arg(roomAliasOrId, account->userId())); - }); + account->joinRoom(roomAliasOrId, viaServers).then(this, [this, account, roomAliasOrId] { + statusBar()->showMessage(tr("Joined %1 as %2").arg(roomAliasOrId, account->userId())); + }); } bool MainWindow::visitNonMatrix(const QUrl& url) @@ -1244,23 +1241,21 @@ void MainWindow::openUserInput(bool forJoining) } QStringList completions; const auto& allRooms = connection->allRooms(); - const auto& users = connection->users(); + const auto& userIds = connection->userIds(); // Assuming that roughly half of rooms in the room list have // a canonical alias; this may be quite a bit off but is better // than not reserving at all - completions.reserve(allRooms.size() * 3 / 2 + users.size()); + completions.reserve(allRooms.size() * 3 / 2 + userIds.size()); for (auto* room: allRooms) { completions << room->id(); if (!room->canonicalAlias().isEmpty()) completions << room->canonicalAlias(); } - for (auto* user: users) - completions << user->id(); + std::ranges::copy(userIds, std::back_inserter(completions)); completions.sort(); - completions.erase(std::unique(completions.begin(), completions.end()), - completions.end()); + completions.erase(std::ranges::unique(completions).begin(), completions.end()); auto* completer = new QCompleter(completions); completer->setFilterMode(Qt::MatchContains); diff --git a/client/models/messageeventmodel.cpp b/client/models/messageeventmodel.cpp index 270fe3a7..10d76bf4 100644 --- a/client/models/messageeventmodel.cpp +++ b/client/models/messageeventmodel.cpp @@ -180,10 +180,10 @@ void MessageEventModel::changeRoom(QuaternionRoom* room) this, &MessageEventModel::refreshEvent); qCDebug(EVENTMODEL) << "Event model connected to room" << room->objectName() // - << "as" << room->localUser()->id(); + << "as" << room->localMember().id(); // If the timeline isn't loaded, ask for at least something right away if (room->timelineSize() == 0) - room->getHistory(30); + room->getPreviousContent(30); } endResetModel(); emit readMarkerUpdated(); @@ -472,12 +472,11 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const return switchOnType(evt , [this] (const RoomMessageEvent& e) { // clang-format on - using namespace MessageEventContent; + using namespace Quotient::EventContent; - if (e.hasTextContent() && e.mimeType().name() != "text/plain") { + if (e.has() && e.mimeType().name() != "text/plain") { // Naïvely assume that it's HTML - auto htmlBody = - static_cast(e.content())->body; + auto htmlBody = e.get()->body; auto [cleanHtml, errorPos, errorString] = HtmlFilter::fromMatrixHtml(htmlBody, m_currentRoom); // If HTML is bad (or it's not HTML at all), fall back @@ -493,9 +492,8 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const } return cleanHtml; } - if (e.hasFileContent()) { - auto fileCaption = - e.content()->fileInfo()->originalName.toHtmlEscaped(); + if (const auto fileContent = e.get()) { + auto fileCaption = fileContent->commonInfo().originalName.toHtmlEscaped(); if (fileCaption.isEmpty()) fileCaption = m_currentRoom->prettyPrint(e.plainBody()); return !fileCaption.isEmpty() ? fileCaption : tr("a file"); @@ -507,7 +505,7 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const // clang-format on // FIXME: Rewind to the name that was at the time of this event const auto subjectName = - m_currentRoom->safeMemberName(e.userId()).toHtmlEscaped(); + m_currentRoom->member(e.userId()).htmlSafeDisambiguatedName(); // The below code assumes senderName output in AuthorRole switch( e.membership() ) { @@ -671,7 +669,7 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const return settings.get(QStringLiteral("UI/highlight_color"), QStringLiteral("orange")); - if (isPending || evt.senderId() == m_currentRoom->localUser()->id()) + if (isPending || evt.senderId() == m_currentRoom->localMember().id()) normalTextColor = mixColors(normalTextColor, settings.get(QStringLiteral("UI/outgoing_color"), QStringLiteral("#4A8780")), 0.5); @@ -700,7 +698,7 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const case MessageEventType::Image: return "image"; default: - return e->hasFileContent() ? "file" : "message"; + return e->has() ? "file" : "message"; } } if (evt.isStateEvent()) @@ -714,14 +712,13 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const if( role == AuthorRole ) { - // FIXME: It shouldn't be User, it should be its state "as of event" - return QVariant::fromValue(isPending - ? m_currentRoom->localUser() - : m_currentRoom->user(evt.senderId())); + // TODO: It should be RoomMember state "as of event", not "as of now" + return QVariant::fromValue(isPending ? m_currentRoom->localMember() + : m_currentRoom->member(evt.senderId())); } if (role == AuthorHasAvatarRole) { - return m_currentRoom->memberAvatarUrl(evt.senderId()).isValid(); + return m_currentRoom->member(evt.senderId()).avatarUrl().isValid(); } if (role == ContentTypeRole) @@ -750,7 +747,7 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const // Cannot use e.contentJson() here because some // EventContent classes inject values into the copy of the // content JSON stored in EventContent::Base - return e->hasFileContent() + return e->has() ? QVariant::fromValue(e->content()->originalJson) : QVariant(); } @@ -809,8 +806,8 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const } } if (memberEvent || evt.isRedacted()) { - if (evt.senderId() != m_currentRoom->localUser()->id() - && evt.stateKey() != m_currentRoom->localUser()->id() + if (evt.senderId() != m_currentRoom->localMember().id() + && evt.stateKey() != m_currentRoom->localMember().id() && !settings.get("UI/show_spammy")) { // QElapsedTimer et; et.start(); auto hide = !isUserActivityNotable(timelineIt); @@ -842,7 +839,7 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const if( role == LongOperationRole ) { if (auto e = eventCast(&evt)) - if (e->hasFileContent()) + if (e->has()) return QVariant::fromValue( m_currentRoom->fileTransferInfo( isPending ? e->transactionId() : e->id())); @@ -871,9 +868,9 @@ QVariant MessageEventModel::data(const QModelIndex& idx, int role) const if (rIt == reactions.end()) rIt = reactions.insert(reactions.end(), { e->key() }); - rIt->authorsList << m_currentRoom->safeMemberName(e->senderId()); + rIt->authorsList << m_currentRoom->member(e->senderId()).displayName(); rIt->includesLocalUser |= - e->senderId() == m_currentRoom->localUser()->id(); + e->senderId() == m_currentRoom->localMember().id(); } // Prepare the QML model data // NB: Strings are NOT HTML-escaped; QML code must take care to use diff --git a/client/models/roomlistmodel.cpp b/client/models/roomlistmodel.cpp index 1a88abf0..c8803f14 100644 --- a/client/models/roomlistmodel.cpp +++ b/client/models/roomlistmodel.cpp @@ -403,7 +403,7 @@ QVariant RoomListModel::data(const QModelIndex& index, int role) const && c->room(room->id(), room->joinState())) disambiguatedName = RoomNameTemplate.arg(room->displayName(), - room->localUser()->id()); + room->localMember().id()); using Quotient::JoinState; switch (role) @@ -483,12 +483,12 @@ QVariant RoomListModel::data(const QModelIndex& index, int role) const result += //: The number of invited users "
" % tr("Invited: %L1").arg(room->invitedCount()); - const auto directChatUsers = room->directChatUsers(); - if (!directChatUsers.isEmpty()) { + const auto directChatMembers = room->directChatMembers(); + if (!directChatMembers.isEmpty()) { QStringList userNames; - userNames.reserve(directChatUsers.size()); - for (auto* user: directChatUsers) - userNames.push_back(user->displayname(room).toHtmlEscaped()); + userNames.reserve(directChatMembers.size()); + for (const auto& m: directChatMembers) + userNames.push_back(m.htmlSafeDisplayName()); result += "
" % tr("Direct chat with %1") .arg(QLocale().createSeparatedList(userNames)); @@ -531,7 +531,7 @@ QVariant RoomListModel::data(const QModelIndex& index, int role) const : room->joinState() == JoinState::Invite ? tr("You were invited into this room as %1") : tr("You left this room as %1")) - .arg(room->localUser()->id().toHtmlEscaped()); + .arg(room->localMember().id().toHtmlEscaped()); return result; } case HasUnreadRole: diff --git a/client/models/userlistmodel.cpp b/client/models/userlistmodel.cpp index 7f701b55..9542ef93 100644 --- a/client/models/userlistmodel.cpp +++ b/client/models/userlistmodel.cpp @@ -22,12 +22,12 @@ #include #include +#include + UserListModel::UserListModel(QAbstractItemView* parent) : QAbstractListModel(parent), m_currentRoom(nullptr) { } -UserListModel::~UserListModel() = default; - void UserListModel::setRoom(Quotient::Room* room) { if (m_currentRoom == room) @@ -38,32 +38,30 @@ void UserListModel::setRoom(Quotient::Room* room) if (m_currentRoom) { m_currentRoom->connection()->disconnect(this); m_currentRoom->disconnect(this); - for (auto* user: std::as_const(m_users)) - user->disconnect(this); - m_users.clear(); + m_memberIds.clear(); } 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::memberJoined, this, &UserListModel::userAdded); + connect(m_currentRoom, &Room::memberLeft, this, &UserListModel::userRemoved); + connect(m_currentRoom, &Room::memberNameAboutToUpdate, this, &UserListModel::userRemoved); + connect(m_currentRoom, &Room::memberNameUpdated, this, &UserListModel::userAdded); connect(m_currentRoom, &Room::memberListChanged, this, &UserListModel::membersChanged); - connect(m_currentRoom, &Room::memberAvatarChanged, this, &UserListModel::avatarChanged); + connect(m_currentRoom, &Room::memberAvatarUpdated, this, &UserListModel::avatarChanged); connect(m_currentRoom->connection(), &Connection::loggedOut, this, [this] { setRoom(nullptr); }); filter({}); - qCDebug(MODELS) << m_users.count() << "user(s) in the room"; + qCDebug(MODELS) << m_memberIds.count() << "member(s) in the room"; } endResetModel(); } -Quotient::User* UserListModel::userAt(QModelIndex index) +Quotient::RoomMember UserListModel::userAt(QModelIndex index) const { - if (index.row() < 0 || index.row() >= m_users.size()) - return nullptr; - return m_users.at(index.row()); + if (index.row() < 0 || index.row() >= m_memberIds.size()) + return {}; + return m_currentRoom->member(m_memberIds.at(index.row())); } QVariant UserListModel::data(const QModelIndex& index, int role) const @@ -71,23 +69,22 @@ QVariant UserListModel::data(const QModelIndex& index, int role) const if( !index.isValid() ) return QVariant(); - if( index.row() >= m_users.count() ) + if( index.row() >= m_memberIds.count() ) { qCWarning(MODELS) << "UserListModel, something's wrong: index.row() >= " "m_users.count()"; return QVariant(); } - auto user = m_users.at(index.row()); + auto m = userAt(index); if( role == Qt::DisplayRole ) { - return user->displayname(m_currentRoom); + return m.displayName(); } const auto* view = static_cast(parent()); if (role == Qt::DecorationRole) { - // Make user avatars 150% high compared to display names + // Convert avatar image to QIcon const auto dpi = view->devicePixelRatioF(); - if (auto av = user->avatar(int(view->iconSize().height() * dpi), - m_currentRoom); + if (auto av = m.avatar(static_cast(view->iconSize().height() * dpi), [] {}); !av.isNull()) { av.setDevicePixelRatio(dpi); return QIcon(QPixmap::fromImage(av)); @@ -99,9 +96,8 @@ QVariant UserListModel::data(const QModelIndex& index, int role) const if (role == Qt::ToolTipRole) { - auto tooltip = QStringLiteral("%1
%2") - .arg(user->name(m_currentRoom).toHtmlEscaped(), - user->id().toHtmlEscaped()); + auto tooltip = + QStringLiteral("%1
%2").arg(m.name().toHtmlEscaped(), m.id().toHtmlEscaped()); // TODO: Find a new way to determine that the user is bridged // if (!user->bridged().isEmpty()) // tooltip += "
" + tr("Bridged from: %1").arg(user->bridged()); @@ -111,10 +107,10 @@ QVariant UserListModel::data(const QModelIndex& index, int role) const if (role == Qt::ForegroundRole) { // FIXME: boilerplate with TimelineItem.qml:57 const auto& palette = view->palette(); - return QColor::fromHslF(user->hueF(), - 1 - palette.color(QPalette::Window).saturationF(), - 0.9 - 0.7 * palette.color(QPalette::Window).lightnessF(), - palette.color(QPalette::ButtonText).alphaF()); + return QColor::fromHslF(static_cast(m.hueF()), + 1 - palette.color(QPalette::Window).saturationF(), + 0.9f - 0.7f * palette.color(QPalette::Window).lightnessF(), + palette.color(QPalette::ButtonText).alphaF()); } return QVariant(); @@ -125,38 +121,37 @@ int UserListModel::rowCount(const QModelIndex& parent) const if( parent.isValid() ) return 0; - return m_users.count(); + return m_memberIds.count(); } -void UserListModel::userAdded(Quotient::User* user) +void UserListModel::userAdded(const RoomMember& member) { - auto pos = findUserPos(user); - if (pos != m_users.size() && m_users[pos] == user) + auto pos = findUserPos(member.id()); + if (pos != m_memberIds.size() && m_memberIds[pos] == member.id()) { - qCWarning(MODELS) << "Trying to add the user" << user->id() + qCWarning(MODELS) << "Trying to add the user" << member.id() << "but it's already in the user list"; return; } beginInsertRows(QModelIndex(), pos, pos); - m_users.insert(pos, user); + m_memberIds.insert(pos, member.id()); endInsertRows(); } -void UserListModel::userRemoved(Quotient::User* user) +void UserListModel::userRemoved(const RoomMember& member) { - auto pos = findUserPos(user); - if (pos == m_users.size()) + auto pos = findUserPos(member); + if (pos == m_memberIds.size()) { qCWarning(MODELS) << "Trying to remove a room member not in the user list:" - << user->id(); + << member.id(); return; } beginRemoveRows(QModelIndex(), pos, pos); - m_users.removeAt(pos); + m_memberIds.removeAt(pos); endRemoveRows(); - user->disconnect(this); } void UserListModel::filter(const QString& filterString) @@ -167,41 +162,45 @@ void UserListModel::filter(const QString& filterString) QElapsedTimer et; et.start(); beginResetModel(); - m_users.clear(); - const auto all = m_currentRoom->users(); - std::remove_copy_if(all.begin(), all.end(), std::back_inserter(m_users), - [&](User* u) { - return !(u->name(m_currentRoom).contains(filterString) || - u->id().contains(filterString)); - }); - std::sort(m_users.begin(), m_users.end(), m_currentRoom->memberSorter()); + // TODO: use std::ranges::to() once it's available from all stdlibs Quotient builds with + auto filteredMembersView = + std::views::filter(m_currentRoom->joinedMembers(), + Quotient::memberMatcher(filterString, Qt::CaseInsensitive)); + QList filteredMembers(filteredMembersView.begin(), filteredMembersView.end()); + std::ranges::sort(filteredMembers, Quotient::MemberSorter()); + const auto sortedIds = std::views::transform(filteredMembers, &RoomMember::id); +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + m_memberIds.assign(sortedIds.begin(), sortedIds.end()); +#else + std::exchange(m_memberIds, QList(sortedIds.begin(), sortedIds.end())); +#endif endResetModel(); - qCDebug(MODELS) << "Filtering" << m_users.size() << "user(s) in" + qCDebug(MODELS) << "Filtering" << m_memberIds.size() << "user(s) in" << m_currentRoom->displayName() << "took" << et; } -void UserListModel::refresh(Quotient::User* user, QVector roles) +void UserListModel::refresh(const RoomMember& member, QVector roles) { - auto pos = findUserPos(user); - if ( pos != m_users.size() ) + auto pos = findUserPos(member); + if ( pos != m_memberIds.size() ) emit dataChanged(index(pos), index(pos), roles); else qCWarning(MODELS) << "Trying to access a room member not in the user list"; } -void UserListModel::avatarChanged(Quotient::User* user) +void UserListModel::avatarChanged(const RoomMember& m) { - refresh(user, {Qt::DecorationRole}); + refresh(m, {Qt::DecorationRole}); } -int UserListModel::findUserPos(User* user) const +int UserListModel::findUserPos(const Quotient::RoomMember& m) const { - return findUserPos(m_currentRoom->disambiguatedMemberName(user->id())); + return findUserPos(m.disambiguatedName()); } int UserListModel::findUserPos(const QString& username) const { - return m_currentRoom->memberSorter().lowerBoundIndex(m_users, username); + return static_cast(Quotient::lowerBoundMemberIndex(m_memberIds, username, m_currentRoom)); } diff --git a/client/models/userlistmodel.h b/client/models/userlistmodel.h index 9eaa0dc3..b2d566b1 100644 --- a/client/models/userlistmodel.h +++ b/client/models/userlistmodel.h @@ -16,40 +16,39 @@ namespace Quotient { class Connection; class Room; - class User; + class RoomMember; } class UserListModel: public QAbstractListModel { Q_OBJECT public: - using User = Quotient::User; + using RoomMember = Quotient::RoomMember; UserListModel(QAbstractItemView* parent); - virtual ~UserListModel(); void setRoom(Quotient::Room* room); - User* userAt(QModelIndex index); + RoomMember userAt(QModelIndex index) const; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex& parent=QModelIndex()) const override; - + signals: - void membersChanged(); //< Reflection of Room::memberListChanged + void membersChanged(); //!< Reflection of Room::memberListChanged public slots: void filter(const QString& filterString); private slots: - void userAdded(User* user); - void userRemoved(User* user); - void refresh(User* user, QVector roles = {}); - void avatarChanged(User* user); + void userAdded(const RoomMember& member); + void userRemoved(const RoomMember& member); + void refresh(const RoomMember& member, QVector roles = {}); + void avatarChanged(const RoomMember& m); private: Quotient::Room* m_currentRoom; - QList m_users; + QList m_memberIds; - int findUserPos(User* user) const; + int findUserPos(const RoomMember &m) const; int findUserPos(const QString& username) const; }; diff --git a/client/profiledialog.cpp b/client/profiledialog.cpp index c3ba7a89..3230c601 100644 --- a/client/profiledialog.cpp +++ b/client/profiledialog.cpp @@ -126,7 +126,7 @@ ProfileDialog::DeviceTable::DeviceTable() void updateAvatarButton(Quotient::User* user, QPushButton* btn) { - const auto img = user->avatar(128); + const auto img = user->avatar(128, [] {}); if (img.isNull()) { btn->setText(ProfileDialog::tr("No avatar")); btn->setIcon({}); @@ -273,7 +273,7 @@ void ProfileDialog::load() auto* user = m_currentAccount->user(); updateAvatarButton(user, m_avatar); connect(user, &User::defaultAvatarChanged, this, - [this] { updateAvatarButton(account()->user(), m_avatar); }); + [this, user] { updateAvatarButton(user, m_avatar); }); m_displayName->setText(user->name()); m_displayName->setFocus(); diff --git a/client/qml/Avatar.qml b/client/qml/Avatar.qml index f31ee875..dba98905 100644 --- a/client/qml/Avatar.qml +++ b/client/qml/Avatar.qml @@ -5,8 +5,7 @@ Image { readonly property var forRoom: root.room /* readonly */ property var forMember - property string sourceId: - forRoom ? "image://avatar/" + (forMember ? forMember : forRoom).id : "" + property string sourceId: forMember?.avatarUrl ?? forRoom?.avatarUrl ?? "" source: sourceId cache: false // Quotient::Avatar takes care of caching fillMode: Image.PreserveAspectFit @@ -19,8 +18,8 @@ Image { Connections { target: forRoom function onAvatarChanged() { avatar.reload() } - function onMemberAvatarChanged(member) { - if (member === avatar.forMember) + function onMemberAvatarUpdated(member) { + if (avatar.forMember && member?.id === avatar.forMember.id) avatar.reload() } } diff --git a/client/qml/Timeline.qml b/client/qml/Timeline.qml index 81f7034f..26a5fc0c 100644 --- a/client/qml/Timeline.qml +++ b/client/qml/Timeline.qml @@ -101,7 +101,7 @@ Page { (roomNameMetrics.text != roomNameMetrics.elidedText || roomName.lineCount > 1) ToolTip.visible: hovered - ToolTip.text: room ? room.htmlSafeDisplayName : "" + ToolTip.text: room?.displayNameForHtml ?? "" } Label { @@ -284,7 +284,7 @@ Page { // 2 seconds and if yes, request the amount of messages // enough to scroll at this rate for 3 more seconds if (velocity > 0 && contentY - velocity*2 < originY) - room.getHistory(velocity * eventDensity * 3) + room.getPreviousContent(velocity * eventDensity * 3) } onContentYChanged: ensurePreviousContent() onContentHeightChanged: ensurePreviousContent() @@ -470,8 +470,7 @@ Page { id: cachedEventsBar // A proxy property for animation - property int requestedHistoryEventsCount: - room ? room.requestedEventsCount : 0 + property int requestedHistoryEventsCount: room?.requestedHistorySize ?? 0 AnimationBehavior on requestedHistoryEventsCount { NormalNumberAnimation { } } @@ -640,10 +639,9 @@ Page { chatView.bottommostVisibleIndex)) + "\n" + qsTr("%Ln events cached", "", chatView.count) : "") - + (room && room.requestedEventsCount > 0 + + (room?.requestedHistorySize > 0 ? (chatView.count > 0 ? "\n" : "") - + qsTr("%Ln events requested from the server", - "", room.requestedEventsCount) + + qsTr("%Ln events requested from the server", "", room.requestedHistorySize) : "") horizontalAlignment: Label.AlignRight } @@ -712,7 +710,7 @@ Page { scrollFinisher.scrollViewTo(messageModel.readMarkerVisualIndex, ListView.Center) else - room.getHistory(chatView.count / 2) // FIXME, #799 + room.getPreviousContent(chatView.count / 2) // FIXME, #799 } } } diff --git a/client/quaternionroom.cpp b/client/quaternionroom.cpp index 2fd666b7..168cabef 100644 --- a/client/quaternionroom.cpp +++ b/client/quaternionroom.cpp @@ -16,15 +16,9 @@ using namespace Quotient; -QuaternionRoom::QuaternionRoom(Connection* connection, QString roomId, - JoinState joinState) +QuaternionRoom::QuaternionRoom(Connection* connection, QString roomId, JoinState joinState) : Room(connection, std::move(roomId), joinState) -{ - connect(this, &Room::namesChanged, - this, &QuaternionRoom::htmlSafeDisplayNameChanged); - connect(this, &Room::eventsHistoryJobChanged, - this, &QuaternionRoom::requestedEventsCountChanged); -} +{} const QString& QuaternionRoom::cachedUserFilter() const { @@ -77,20 +71,19 @@ void QuaternionRoom::saveViewport(int topIndex, int bottomIndex, bool force) setLastDisplayedEvent(maxTimelineIndex() - bottomIndex); } -QString QuaternionRoom::htmlSafeDisplayName() const -{ - return displayName().toHtmlEscaped(); -} - -int QuaternionRoom::requestedEventsCount() const +bool QuaternionRoom::canRedact(const Quotient::EventId& eventId) const { - return eventsHistoryJob() != nullptr ? m_requestedEventsCount : 0; -} - -void QuaternionRoom::getHistory(int limit) -{ - m_requestedEventsCount = limit; - getPreviousContent(m_requestedEventsCount); + if (const auto it = findInTimeline(eventId); it != historyEdge()) { + const auto localMemberId = localMember().id(); + const auto memberId = it->event()->senderId(); + if (localMemberId == memberId) + return true; + + const auto& ple = currentState().get(); + const auto currentUserPl = ple->powerLevelForUser(localMemberId); + return currentUserPl >= ple->redact() && currentUserPl >= ple->powerLevelForUser(memberId); + } + return false; } void QuaternionRoom::onAddNewTimelineEvents(timeline_iter_t from) @@ -107,7 +100,7 @@ void QuaternionRoom::onAddHistoricalTimelineEvents(rev_iter_t from) void QuaternionRoom::checkForHighlights(const Quotient::TimelineItem& ti) { - const auto localUserId = localUser()->id(); + const auto localUserId = localMember().id(); if (ti->senderId() == localUserId) return; if (auto* e = ti.viewAs()) { @@ -124,7 +117,7 @@ void QuaternionRoom::checkForHighlights(const Quotient::TimelineItem& ti) localUserExpressions[localUserId] = QRegularExpression("(\\W|^)" + localUserId + "(\\W|$)", ReOpt); } - const auto memberName = disambiguatedMemberName(localUserId); + const auto memberName = member(localUserId).disambiguatedName(); if (!roomMemberExpressions.contains(memberName)) { // FIXME: unravels if the room member name contains characters special // to regexp($, e.g.) diff --git a/client/quaternionroom.h b/client/quaternionroom.h index 6d1007e9..0936a1fd 100644 --- a/client/quaternionroom.h +++ b/client/quaternionroom.h @@ -12,45 +12,29 @@ class QuaternionRoom: public Quotient::Room { - Q_OBJECT - Q_PROPERTY(QString htmlSafeDisplayName READ htmlSafeDisplayName NOTIFY htmlSafeDisplayNameChanged) - Q_PROPERTY(int requestedEventsCount READ requestedEventsCount NOTIFY requestedEventsCountChanged) - public: - QuaternionRoom(Quotient::Connection* connection, - QString roomId, Quotient::JoinState joinState); - - const QString& cachedUserFilter() const; - void setCachedUserFilter(const QString& input); - - bool isEventHighlighted(const Quotient::RoomEvent* e) const; - - Q_INVOKABLE int savedTopVisibleIndex() const; - Q_INVOKABLE int savedBottomVisibleIndex() const; - Q_INVOKABLE void saveViewport(int topIndex, int bottomIndex, - bool force = false); - - QString htmlSafeDisplayName() const; - int requestedEventsCount() const; - - public slots: - // TODO, 0.0.96: move logic to libQuotient 0.9 and get rid of it here - void getHistory(int limit); - - signals: - // Gotta wrap the Room::namesChanged signal because it has parameters - // and moc cannot use signals with parameters defined in the parent - // class as NOTIFY targets - void htmlSafeDisplayNameChanged(); - // TODO, 0.0.96: same as for getHistory() - void requestedEventsCountChanged(); - - private: - QSet highlights; - QString m_cachedUserFilter; - int m_requestedEventsCount = 0; - - void onAddNewTimelineEvents(timeline_iter_t from) override; - void onAddHistoricalTimelineEvents(rev_iter_t from) override; - - void checkForHighlights(const Quotient::TimelineItem& ti); + Q_OBJECT +public: + QuaternionRoom(Quotient::Connection* connection, QString roomId, + Quotient::JoinState joinState); + + const QString& cachedUserFilter() const; + void setCachedUserFilter(const QString& input); + + bool isEventHighlighted(const Quotient::RoomEvent* e) const; + + Q_INVOKABLE int savedTopVisibleIndex() const; + Q_INVOKABLE int savedBottomVisibleIndex() const; + Q_INVOKABLE void saveViewport(int topIndex, int bottomIndex, bool force = false); + + bool canRedact(const Quotient::EventId& eventId) const; + +private: + QSet highlights; + QString m_cachedUserFilter; + int m_requestedEventsCount = 0; + + void onAddNewTimelineEvents(timeline_iter_t from) override; + void onAddHistoricalTimelineEvents(rev_iter_t from) override; + + void checkForHighlights(const Quotient::TimelineItem& ti); }; diff --git a/client/roomdialogs.cpp b/client/roomdialogs.cpp index aab8f252..40f300d1 100644 --- a/client/roomdialogs.cpp +++ b/client/roomdialogs.cpp @@ -6,34 +6,36 @@ #include "roomdialogs.h" -#include "mainwindow.h" -#include "quaternionroom.h" #include "accountselector.h" -#include "models/orderbytag.h" // For tagToCaption() #include "logging_categories.h" +#include "mainwindow.h" +#include "models/orderbytag.h" // For tagToCaption() +#include "quaternionroom.h" + +#include + +#include // Only needed because loadCapabilities() implies it #include -#include #include #include -#include -#include +#include +#include #include +#include +#include +#include +#include #include -#include -#include #include -#include -#include -#include -#include -#include #include -#include +#include +#include #include #include +#include RoomDialogBase::RoomDialogBase(const QString& title, const QString& applyButtonText, @@ -106,42 +108,32 @@ void RoomDialogBase::refillVersionSelector(QComboBox* selector, Connection* account) { selector->clear(); - if (account->loadingCapabilities()) - { - selector->addItem( - tr("(loading)", "Loading room versions from the server"), - QString()); - selector->setEnabled(false); - // FIXME: It should be connectSingleShot - // but sadly connectSingleShot doesn't work with lambdas yet - connectUntil(account, &Connection::capabilitiesLoaded, this, - [this,selector,account] { - refillVersionSelector(selector, account); - return true; - }); - return; - } - const auto& versions = account->availableRoomVersions(); - for (const auto& v: versions) - { - const bool isDefault = v.id == account->defaultRoomVersion(); - const auto postfix = - isDefault ? tr("default", "Default room version") : - v.isStable() ? tr("stable", "Stable room version") : - v.status; - selector->addItem(v.id % " (" % postfix % ")", v.id); - const auto idx = selector->count() - 1; - if (isDefault) - { - auto font = selector->itemData(idx, Qt::FontRole).value(); - font.setBold(true); - selector->setItemData(idx, font, Qt::FontRole); - selector->setCurrentIndex(idx); - } - if (!v.isStable()) - selector->setItemData(idx, QColor(Qt::red), Qt::ForegroundRole); - } - selector->setEnabled(true); + selector->addItem(tr("(loading)", "Loading room versions from the server"), QString()); + selector->setEnabled(false); + account->loadCapabilities().then([selector, account] { + selector->clear(); + const auto& versions = account->availableRoomVersions(); + if (versions.empty()) { + selector->addItem(tr("(no available room versions)"), QString()); + } else + for (const auto& v : versions) { + const bool isDefault = v.id == account->defaultRoomVersion(); + const auto postfix = isDefault ? tr("default", "Default room version") + : v.isStable() ? tr("stable", "Stable room version") + : v.status; + selector->addItem(v.id % " (" % postfix % ")", v.id); + const auto idx = selector->count() - 1; + if (isDefault) { + auto font = selector->itemData(idx, Qt::FontRole).value(); + font.setBold(true); + selector->setItemData(idx, font, Qt::FontRole); + selector->setCurrentIndex(idx); + } + if (!v.isStable()) + selector->setItemData(idx, QColor(Qt::red), Qt::ForegroundRole); + } + selector->setEnabled(!versions.isEmpty()); + }); } void RoomDialogBase::addEssentials(QWidget* accountControl, @@ -232,7 +224,7 @@ void RoomSettingsDialog::load() { if (const auto* plEvt = room->currentState().get()) { - const int userPl = plEvt->powerLevelForUser(room->localUser()->id()); + const int userPl = plEvt->powerLevelForUser(room->localMember().id()); roomName->setText(room->name()); roomName->setReadOnly(plEvt->powerLevelForState("m.room.name") > userPl); @@ -283,8 +275,7 @@ void RoomSettingsDialog::apply() static_cast(parent())->selectRoom(newRoom); return true; }); - connectSingleShot(room, &Room::upgradeFailed, - this, &Dialog::applyFailed); + connect(room, &Room::upgradeFailed, this, &Dialog::applyFailed, Qt::SingleShotConnection); room->switchVersion(version->text()); return; // It's either a version upgrade or everything else } @@ -375,8 +366,6 @@ CreateRoomDialog::CreateRoomDialog(Quotient::AccountRegistry* accounts, nextInvitee->setCompleter(completer); connect(nextInvitee, &NextInvitee::currentTextChanged, this, &CreateRoomDialog::updatePushButtons); -// connect(nextInvitee, &NextInvitee::editTextChanged, -// this, &CreateRoomDialog::updateUserList); inviteButton->setFocusPolicy(Qt::NoFocus); inviteButton->setDisabled(true); connect(inviteButton, &QPushButton::clicked, [this] { @@ -387,10 +376,7 @@ CreateRoomDialog::CreateRoomDialog(Quotient::AccountRegistry* accounts, if (userName.indexOf(':') == -1) userName += ':' + accountChooser->currentAccount()->domain(); } - auto* item = new QListWidgetItem(userName); - if (nextInvitee->currentIndex() != -1) - item->setData(Qt::UserRole, nextInvitee->currentData(Qt::UserRole)); - invitees->addItem(item); + invitees->addItem(userName); nextInvitee->clear(); }); invitees->setSizeAdjustPolicy( @@ -448,23 +434,22 @@ bool CreateRoomDialog::validate() void CreateRoomDialog::apply() { using namespace Quotient; + auto* const account = accountChooser->currentAccount(); QStringList userIds; for (int i = 0; i < invitees->count(); ++i) - if (auto* user = invitees->item(i)->data(Qt::UserRole).value()) - userIds.push_back(user->id()); + if (const auto& userId = invitees->item(i)->text(); account->user(userId)) + userIds.push_back(userId); else - userIds.push_back(invitees->item(i)->text()); + qCWarning(MAIN).nospace() << std::source_location::current().function_name() << ": " + << userId << "is not a correct user id, skipping"; - auto* job = accountChooser->currentAccount()->createRoom( - publishRoom->isChecked() ? - Connection::PublishRoom : Connection::UnpublishRoom, - alias->text(), roomName->text(), topic->toPlainText(), - userIds, "", version->currentData().toString(), false); - connect(job, &BaseJob::success, this, &Dialog::accept); - connect(job, &BaseJob::failure, this, [this,job] { - applyFailed(job->errorString()); - }); + account + ->createRoom(publishRoom->isChecked() ? Connection::PublishRoom : Connection::UnpublishRoom, + alias->text(), roomName->text(), topic->toPlainText(), userIds, "", + version->currentData().toString(), false) + .then(this, &Dialog::accept, + [this](const BaseJob* job) { applyFailed(job->errorString()); }); } void CreateRoomDialog::accountSwitched() @@ -487,21 +472,19 @@ void CreateRoomDialog::accountSwitched() // if (prefix.size() >= 3) // { QElapsedTimer et; et.start(); - for (auto* u: connection->users()) + for (const auto& uId: connection->userIds()) { - if (!u->isGuest()) + if (!Quotient::isGuestUserId(uId)) { - // It would be great to show u->fullName() rather than - // just u->id(); unfortunately, this implies fetching profiles - // for the whole list of users known to a given account, which - // is terribly inefficient - auto* item = new QStandardItem(u->id()); - item->setData(QVariant::fromValue(u)); + // It would be great to show a user's full name rather than MXID; unfortunately, + // this implies fetching profiles for the whole list of users known to a given + // account, one by one, and that can easily be thousands. + auto* item = new QStandardItem(uId); model->appendRow(item); } } qCDebug(MAIN) << "Completion candidates:" << model->rowCount() - << "out of" << connection->users().size() << "filled in" + << "out of" << connection->userIds().size() << "filled in" << et; // } } diff --git a/client/thumbnailprovider.cpp b/client/thumbnailprovider.cpp index 31cac0ce..16dbe78f 100644 --- a/client/thumbnailprovider.cpp +++ b/client/thumbnailprovider.cpp @@ -38,28 +38,22 @@ inline QDebug operator<<(QDebug dbg, const auto&& size) class AbstractThumbnailResponse : public QQuickImageResponse { Q_OBJECT public: - AbstractThumbnailResponse(const TimelineWidget* timeline, QString id, - QSize size) + AbstractThumbnailResponse(const TimelineWidget* timeline, QString id, QSize size) : timeline(timeline) , mediaId(std::move(id)) - , requestedSize( - { checkDimension(size.width()), checkDimension(size.height()) }) + , requestedSize({ checkDimension(size.width()), checkDimension(size.height()) }) { - qCDebug(THUMBNAILS).noquote() - << mediaId << '@' << requestedSize << "requested"; + qCDebug(THUMBNAILS).noquote() << mediaId << '@' << requestedSize << "requested"; if (mediaId.isEmpty() || requestedSize.isEmpty()) { qCDebug(THUMBNAILS) << "Returning an empty thumbnail"; - image = { requestedSize, QImage::Format_Invalid }; - emit finished(); + finish(QImage(requestedSize, QImage::Format_Invalid)); return; } - errorStr = tr("Image request is pending"); + result = tr("Image request is pending"); // Start a request on the main thread, concluding the initialisation moveToThread(qApp->thread()); - QMetaObject::invokeMethod(this, - &AbstractThumbnailResponse::startRequest); - // From this point, access to `image` and `errorStr` must be guarded - // by `lock` + QMetaObject::invokeMethod(this, &AbstractThumbnailResponse::startRequest); + // From this point, access to `result` must be guarded by `lock` } protected: @@ -67,12 +61,13 @@ class AbstractThumbnailResponse : public QQuickImageResponse { virtual void startRequest() = 0; virtual void doCancel() {} - void finish(const QImage& result, const QString& error = {}) + using result_type = Quotient::Expected; + + void finish(const result_type& r) { { QWriteLocker _(&lock); - image = result; - errorStr = error; + result = r; } emit finished(); } @@ -82,22 +77,21 @@ class AbstractThumbnailResponse : public QQuickImageResponse { const QSize requestedSize{}; private: - QImage image{}; - QString errorStr{}; - mutable QReadWriteLock lock{}; // Guards ONLY these two above + Quotient::Expected result{}; + mutable QReadWriteLock lock{}; // Guards ONLY the above // The following overrides run in QML thread QQuickTextureFactory* textureFactory() const override { QReadLocker _(&lock); - return QQuickTextureFactory::textureFactoryForImage(image); + return QQuickTextureFactory::textureFactoryForImage(result.value_or(QImage())); } QString errorString() const override { QReadLocker _(&lock); - return errorStr; + return result.has_value() ? QString() : result.error(); } void cancel() override @@ -125,93 +119,42 @@ private slots: const auto* currentRoom = timeline->currentRoom(); if (!currentRoom) { - finish({}, NoConnectionError); + finish(NoConnectionError); return; } - job = currentRoom->connection()->getThumbnail(mediaId, requestedSize); - - // Connect to any possible outcome including abandonment - // to make sure the QML thread is not left stuck forever. - connect(job, &BaseJob::finished, this, [this] { - Q_ASSERT(job->error() != BaseJob::Pending); - if (job->error() == BaseJob::Success) { - qCDebug(THUMBNAILS).noquote() - << "Thumbnail for" << mediaId - << "ready, actual size:" << job->thumbnail().size(); - finish(job->thumbnail()); - } else if (job->error() == BaseJob::Abandoned) { + // Save the future so that we could cancel it + futureResult = + Quotient::JobHandle(currentRoom->connection()->getThumbnail(mediaId, requestedSize)) + .then(this, + [this](const QImage& thumbnail) { + qCDebug(THUMBNAILS).noquote() + << "Thumbnail for" << mediaId + << "ready, actual size:" << thumbnail.size(); + return result_type { thumbnail }; + }, + [this](const Quotient::MediaThumbnailJob* job) { + qCWarning(THUMBNAILS).nospace() + << "No valid thumbnail for" << mediaId << ": " << job->errorString(); + return result_type { job->errorString() }; + }); + // NB: Make sure to connect to any possible outcome including cancellation so that + // the QML thread is not left stuck forever. + futureResult + .onCanceled([this] { qCDebug(THUMBNAILS) << "Request cancelled for" << mediaId; - finish({}, tr("Image request has been cancelled")); - } else { - qCWarning(THUMBNAILS).nospace() - << "No valid thumbnail for" << mediaId << ": " - << job->errorString(); - finish({}, job->errorString()); - } - job = nullptr; - }); + return tr("Image request has been cancelled"); // Turn it to an error + }) + .then([this] (const result_type& r) { finish(r); }); } void doCancel() override { - if (job) { - Q_ASSERT(QThread::currentThread() == job->thread()); - job->abandon(); - } + futureResult.cancel(); } private: - QPointer job = nullptr; -}; - -class AvatarResponse : public AbstractThumbnailResponse { - Q_OBJECT -public: - using AbstractThumbnailResponse::AbstractThumbnailResponse; - -private: - void startRequest() override - { - Q_ASSERT(QThread::currentThread() == qApp->thread()); - - Quotient::Room* currentRoom = timeline->currentRoom(); - if (!currentRoom) { - finish({}, NoConnectionError); - return; - } - - // NB: both Room:avatar() and User::avatar() invocations return an image - // available right now and, if needed, request one with the better - // resolution asynchronously. To get this better resolution image, - // Avatar elements in QML should call Avatar.reload() in response to - // Room::avatarChanged() and Room::memberAvatarChanged() (sic!) - // respectively. - const auto& w = requestedSize.width(); - const auto& h = requestedSize.height(); - if (mediaId.startsWith(u'!')) { - if (mediaId != currentRoom->id()) { - currentRoom = currentRoom->connection()->room(mediaId); - Q_ASSERT(currentRoom != nullptr); - } - // As of libQuotient 0.8, Room::avatar() is the only call in the - // Room::avatar*() family that substitutes the counterpart's - // avatar for a direct chat avatar. - prepareResult(currentRoom->avatar(w, h)); - return; - } - - auto* user = currentRoom->user(mediaId); - Q_ASSERT(user != nullptr); - prepareResult(user->avatar(w, h, currentRoom)); - } - - void prepareResult(const QImage& avatar) - { - qCDebug(THUMBNAILS).noquote() << "Returning avatar for" << mediaId - << "with size:" << avatar.size(); - finish(avatar); - } + QFuture> futureResult; }; #include "thumbnailprovider.moc" // Because we define a Q_OBJECT in the cpp file @@ -232,11 +175,6 @@ class ImageProviderTemplate : public QQuickAsyncImageProvider { Q_DISABLE_COPY(ImageProviderTemplate) }; -QQuickAsyncImageProvider* makeAvatarProvider(TimelineWidget* parent) -{ - return new ImageProviderTemplate(parent); -} - QQuickAsyncImageProvider* makeThumbnailProvider(TimelineWidget* parent) { return new ImageProviderTemplate(parent); diff --git a/client/thumbnailprovider.h b/client/thumbnailprovider.h index e2797f27..e12cc5ac 100644 --- a/client/thumbnailprovider.h +++ b/client/thumbnailprovider.h @@ -12,5 +12,4 @@ class TimelineWidget; -QQuickAsyncImageProvider* makeAvatarProvider(TimelineWidget* parent); QQuickAsyncImageProvider* makeThumbnailProvider(TimelineWidget* parent); diff --git a/client/timelinewidget.cpp b/client/timelinewidget.cpp index 6e67dfe9..d6de0507 100644 --- a/client/timelinewidget.cpp +++ b/client/timelinewidget.cpp @@ -1,24 +1,28 @@ #include "timelinewidget.h" #include "chatroomwidget.h" +#include "logging_categories.h" #include "models/messageeventmodel.h" #include "thumbnailprovider.h" -#include "logging_categories.h" -#include -#include #include +#include + #include + +#include +#include #include #include #include +#include #include #include -#include -#include -#include + #include +#include +#include using Quotient::operator""_ls; @@ -33,18 +37,13 @@ TimelineWidget::TimelineWidget(ChatRoomWidget* chatRoomWidget) qmlRegisterUncreatableType( "Quotient", 1, 0, "Room", "Room objects can only be created by libQuotient"); - qmlRegisterUncreatableType( - "Quotient", 1, 0, "User", - "User objects can only be created by libQuotient"); + qmlRegisterAnonymousType("Quotient", 1); qmlRegisterAnonymousType("Quotient", 1); qmlRegisterAnonymousType("Quotient", 1); - qRegisterMetaType("GetRoomEventsJob*"); - qRegisterMetaType("User*"); qmlRegisterType("Quotient", 1, 0, "Settings"); setResizeMode(SizeRootObjectToView); - engine()->addImageProvider("avatar"_ls, makeAvatarProvider(this)); engine()->addImageProvider("thumbnail"_ls, makeThumbnailProvider(this)); auto* ctxt = rootContext(); @@ -179,13 +178,7 @@ void TimelineWidget::showMenu(int index, const QString& hoveredLink, auto menu = new QMenu(this); menu->setAttribute(Qt::WA_DeleteOnClose); - const auto* plEvt = - currentRoom()->currentState().get(); - const auto localUserId = currentRoom()->localUser()->id(); - const int userPl = plEvt ? plEvt->powerLevelForUser(localUserId) : 0; - const auto* modelUser = - modelIndex.data(MessageEventModel::AuthorRole).value(); - if (!plEvt || userPl >= plEvt->redact() || localUserId == modelUser->id()) + if (currentRoom()->canRedact(eventId)) menu->addAction(QIcon::fromTheme("edit-delete"), tr("Redact"), this, [this, eventId] { currentRoom()->redactEvent(eventId); }); @@ -265,7 +258,7 @@ void TimelineWidget::reactionButtonClicked(const QString& eventId, for (const auto& a: annotations) if (auto* e = eventCast(a); e != nullptr && e->key() == key - && a->senderId() == currentRoom()->localUser()->id()) // + && a->senderId() == currentRoom()->localMember().id()) // { currentRoom()->redactEvent(a->id()); return; diff --git a/client/userlistdock.cpp b/client/userlistdock.cpp index 1265c5c5..4b7d4014 100644 --- a/client/userlistdock.cpp +++ b/client/userlistdock.cpp @@ -99,7 +99,7 @@ void UserListDock::refreshTitle() void UserListDock::showContextMenu(QPoint pos) { - if (!getSelectedUser()) + if (getSelectedUser().isEmpty()) return; auto* contextMenu = new QMenu(this); @@ -117,7 +117,7 @@ void UserListDock::showContextMenu(QPoint pos) const auto* plEvt = m_currentRoom->currentState().get(); const int userPl = - plEvt ? plEvt->powerLevelForUser(m_currentRoom->localUser()->id()) : 0; + plEvt ? plEvt->powerLevelForUser(m_currentRoom->localMember().id()) : 0; if (!plEvt || userPl >= plEvt->kick()) { contextMenu->addAction(QIcon::fromTheme("im-ban-kick-user"), @@ -134,47 +134,47 @@ void UserListDock::showContextMenu(QPoint pos) void UserListDock::startChatSelected() { - if (auto* user = getSelectedUser()) - user->requestDirectChat(); + if (auto userId = getSelectedUser(); !userId.isEmpty()) + m_currentRoom->connection()->requestDirectChat(userId); } void UserListDock::requestUserMention() { - if (auto* user = getSelectedUser()) - emit userMentionRequested(user); + if (auto userId = getSelectedUser(); !userId.isEmpty()) + emit userMentionRequested(userId); } void UserListDock::kickUser() { - if (auto* user = getSelectedUser()) + if (auto userId = getSelectedUser(); !userId.isEmpty()) { bool ok; const auto reason = QInputDialog::getText(this, - tr("Kick %1").arg(user->id()), tr("Reason"), + tr("Kick %1").arg(userId), tr("Reason"), QLineEdit::Normal, nullptr, &ok); if (ok) { - m_currentRoom->kickMember(user->id(), reason); + m_currentRoom->kickMember(userId, reason); } } } void UserListDock::banUser() { - if (auto* user = getSelectedUser()) + if (auto userId = getSelectedUser(); !userId.isEmpty()) { bool ok; const auto reason = QInputDialog::getText(this, - tr("Ban %1").arg(user->id()), tr("Reason"), + tr("Ban %1").arg(userId), tr("Reason"), QLineEdit::Normal, nullptr, &ok); if (ok) { - m_currentRoom->ban(user->id(), reason); + m_currentRoom->ban(userId, reason); } } } void UserListDock::ignoreUser() { - if (auto* user = getSelectedUser()) { + if (auto* user = m_currentRoom->connection()->user(getSelectedUser())) { if (!user->isIgnored()) user->ignore(); else @@ -184,17 +184,17 @@ void UserListDock::ignoreUser() bool UserListDock::isIgnored() { - if (auto* user = getSelectedUser()) - return user->isIgnored(); + if (auto memberId = getSelectedUser(); !memberId.isEmpty()) + return m_currentRoom->connection()->isIgnored(memberId); return false; } -Quotient::User* UserListDock::getSelectedUser() const +QString UserListDock::getSelectedUser() const { auto index = m_view->currentIndex(); if (!index.isValid()) - return nullptr; - auto* const user = m_model->userAt(index); - Q_ASSERT(user); - return user; + return {}; + const auto member = m_model->userAt(index); + Q_ASSERT(!member.isEmpty()); + return member.id(); } diff --git a/client/userlistdock.h b/client/userlistdock.h index 179a5755..b201e8c4 100644 --- a/client/userlistdock.h +++ b/client/userlistdock.h @@ -11,11 +11,6 @@ #include #include -namespace Quotient -{ - class User; -} - class UserListModel; class QuaternionRoom; class QTableView; @@ -30,7 +25,7 @@ class UserListDock: public QDockWidget void setRoom( QuaternionRoom* room ); signals: - void userMentionRequested(Quotient::User* u); + void userMentionRequested(QString userId); private slots: void refreshTitle(); @@ -50,5 +45,5 @@ class UserListDock: public QDockWidget UserListModel* m_model; QuaternionRoom* m_currentRoom = nullptr; - Quotient::User* getSelectedUser() const; + QString getSelectedUser() const; }; diff --git a/lib b/lib index a313422d..e70e242a 160000 --- a/lib +++ b/lib @@ -1 +1 @@ -Subproject commit a313422d05818d241fc7f86087d250b184b26324 +Subproject commit e70e242a9b436532bc57569a76e57330cad53896