Files
neochat/src/roommanager.cpp
Joshua Goins 2379e3d83b Don't scroll up when clicking on the same room over and over
If you try to click on your current room in the list, it scrolls up the
messages a bit. This is because in RoomManager::visitRoom it's being
called with an empty eventId and we will happily emit a goToEvent. This
is despite there being nothing to go to.

Fixes #677.
2024-11-16 14:18:32 +00:00

544 lines
16 KiB
C++

// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
// SPDX-FileCopyrightText: 2021 Alexey Rusakov <Kitsune-Ral@users.sf.net>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "roommanager.h"
#include "chatbarcache.h"
#include "controller.h"
#include "eventhandler.h"
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include "spacehierarchycache.h"
#include "urlhelper.h"
#include <KLocalizedString>
#include <QDesktopServices>
#include <QQuickTextDocument>
#include <QStandardPaths>
#include <Quotient/csapi/joining.h>
#include <Quotient/csapi/knocking.h>
#include <Quotient/qt_connection_util.h>
#include <Quotient/user.h>
#ifndef Q_OS_ANDROID
#include <KIO/OpenUrlJob>
#endif
RoomManager::RoomManager(QObject *parent)
: QObject(parent)
, m_config(KSharedConfig::openStateConfig())
, m_roomListModel(new RoomListModel(this))
, m_sortFilterRoomListModel(new SortFilterRoomListModel(m_roomListModel, this))
, m_sortFilterSpaceListModel(new SortFilterSpaceListModel(m_roomListModel, this))
, m_roomTreeModel(new RoomTreeModel(this))
, m_sortFilterRoomTreeModel(new SortFilterRoomTreeModel(m_roomTreeModel, this))
, m_timelineModel(new TimelineModel(this))
, m_messageFilterModel(new MessageFilterModel(this, m_timelineModel))
, m_mediaMessageFilterModel(new MediaMessageFilterModel(this, m_messageFilterModel))
, m_userListModel(new UserListModel(this))
{
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(UBUNTU_TOUCH)
m_isMobile = true;
#else
// Mostly for debug purposes and for platforms which are always mobile,
// such as Plasma Mobile
if (qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MOBILE")) {
m_isMobile = QByteArrayList{"1", "true"}.contains(qgetenv("QT_QUICK_CONTROLS_MOBILE"));
}
#endif
m_lastRoomConfig = m_config->group(QStringLiteral("LastOpenRoom"));
m_lastSpaceConfig = m_config->group(QStringLiteral("LastOpenSpace"));
m_directChatsConfig = m_config->group(QStringLiteral("DirectChatsActive"));
connect(this, &RoomManager::currentRoomChanged, this, [this]() {
m_timelineModel->setRoom(m_currentRoom);
m_userListModel->setRoom(m_currentRoom);
});
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this](NeoChatConnection *connection) {
setConnection(connection);
});
connect(this, &RoomManager::connectionChanged, this, [this]() {
m_roomListModel->setConnection(m_connection);
m_roomTreeModel->setConnection(m_connection);
});
connect(m_sortFilterSpaceListModel, &SortFilterSpaceListModel::layoutChanged, m_sortFilterRoomTreeModel, &SortFilterRoomTreeModel::invalidate);
}
RoomManager::~RoomManager()
{
}
RoomManager &RoomManager::instance()
{
static RoomManager _instance;
return _instance;
}
NeoChatRoom *RoomManager::currentRoom() const
{
return m_currentRoom;
}
RoomListModel *RoomManager::roomListModel() const
{
return m_roomListModel;
}
SortFilterRoomListModel *RoomManager::sortFilterRoomListModel() const
{
return m_sortFilterRoomListModel;
}
SortFilterSpaceListModel *RoomManager::sortFilterSpaceListModel() const
{
return m_sortFilterSpaceListModel;
}
RoomTreeModel *RoomManager::roomTreeModel() const
{
return m_roomTreeModel;
}
SortFilterRoomTreeModel *RoomManager::sortFilterRoomTreeModel() const
{
return m_sortFilterRoomTreeModel;
}
TimelineModel *RoomManager::timelineModel() const
{
return m_timelineModel;
}
MessageFilterModel *RoomManager::messageFilterModel() const
{
return m_messageFilterModel;
}
MediaMessageFilterModel *RoomManager::mediaMessageFilterModel() const
{
return m_mediaMessageFilterModel;
}
UserListModel *RoomManager::userListModel() const
{
return m_userListModel;
}
void RoomManager::activateUserModel()
{
m_userListModel->activate();
}
UriResolveResult RoomManager::resolveResource(const Uri &uri)
{
return UriResolverBase::visitResource(m_connection, uri);
}
void RoomManager::resolveResource(const QString &idOrUri, const QString &action)
{
Uri uri{idOrUri};
if (!uri.isValid()) {
Q_EMIT showMessage(MessageType::Warning, i18n("Malformed or empty Matrix id<br />%1 is not a correct Matrix identifier", idOrUri));
return;
}
if (uri.type() == Uri::NonMatrix && action == "qr"_ls) {
Q_EMIT externalUrl(uri.toUrl());
return;
}
if (uri.type() != Uri::NonMatrix) {
if (!m_connection) {
return;
}
if (!action.isEmpty() && (uri.type() != Uri::UserId || action != "join"_ls)) {
uri.setAction(action);
}
// TODO we should allow the user to select a connection.
}
const auto result = visitResource(m_connection, uri);
if (result == Quotient::CouldNotResolve) {
if ((uri.type() == Uri::RoomAlias || uri.type() == Uri::RoomId) && action != "no_join"_ls) {
Q_EMIT askJoinRoom(uri.primaryId());
}
}
}
void RoomManager::maximizeMedia(int index)
{
if (index < -1 || index > m_mediaMessageFilterModel->rowCount()) {
return;
}
Q_EMIT showMaximizedMedia(index);
}
void RoomManager::maximizeCode(NeochatRoomMember *author, const QDateTime &time, const QString &codeText, const QString &language)
{
if (codeText.isEmpty()) {
return;
}
Q_EMIT showMaximizedCode(author, time, codeText, language);
}
void RoomManager::requestFullScreenClose()
{
Q_EMIT closeFullScreen();
}
void RoomManager::viewEventSource(const QString &eventId)
{
Q_EMIT showEventSource(eventId);
}
void RoomManager::viewEventMenu(const QString &eventId, NeoChatRoom *room, NeochatRoomMember *sender, const QString &selectedText)
{
const auto &event = **room->findInTimeline(eventId);
if (EventHandler::mediaInfo(room, &event).contains("mimeType"_ls)) {
Q_EMIT showFileMenu(eventId,
sender,
MessageComponentType::typeForEvent(event),
EventHandler::plainBody(room, &event),
EventHandler::mediaInfo(room, &event)["mimeType"_ls].toString(),
room->fileTransferInfo(eventId));
return;
}
Q_EMIT showMessageMenu(eventId,
sender,
MessageComponentType::typeForEvent(event),
EventHandler::plainBody(room, &event),
EventHandler::richBody(room, &event),
selectedText);
}
bool RoomManager::hasOpenRoom() const
{
return m_currentRoom != nullptr;
}
void RoomManager::setUrlArgument(const QString &arg)
{
m_arg = arg;
}
void RoomManager::loadInitialRoom()
{
Q_ASSERT(m_connection);
if (!m_arg.isEmpty()) {
resolveResource(m_arg);
}
if (m_isMobile) {
// We still need to remember the last space on mobile
setCurrentSpace(m_lastSpaceConfig.readEntry(m_connection->userId(), QString()), false);
// We don't want to open a room on startup on mobile
return;
}
if (m_currentRoom) {
// we opened a room with the arg parsing already
return;
}
openRoomForActiveConnection();
connect(this, &RoomManager::connectionChanged, this, &RoomManager::openRoomForActiveConnection);
}
void RoomManager::openRoomForActiveConnection()
{
if (!m_connection) {
setCurrentRoom({});
setCurrentSpace({}, false);
return;
}
const auto &lastRoom = m_lastRoomConfig.readEntry(m_connection->userId(), QString());
if (lastRoom.isEmpty() || !m_connection->room(lastRoom)) {
setCurrentRoom({});
} else {
m_currentRoom = nullptr;
resolveResource(lastRoom);
}
setCurrentSpace(m_lastSpaceConfig.readEntry(m_connection->userId(), QString()), false);
}
UriResolveResult RoomManager::visitUser(User *user, const QString &action)
{
if (action == "mention"_ls || action == "qr"_ls || action.isEmpty()) {
user->load();
Q_EMIT showUserDetail(user, action == "qr"_ls ? nullptr : currentRoom());
} else if (action == "_interactive"_ls) {
user->requestDirectChat();
} else if (action == "chat"_ls) {
user->load();
Q_EMIT askDirectChatConfirmation(user);
} else {
return Quotient::IncorrectAction;
}
return Quotient::UriResolved;
}
void RoomManager::visitRoom(Room *r, const QString &eventId)
{
auto room = qobject_cast<NeoChatRoom *>(r);
if (m_currentRoom && !m_currentRoom->editCache()->editId().isEmpty()) {
m_currentRoom->editCache()->setEditId({});
}
if (m_currentRoom && !m_currentRoom->isSpace() && m_chatDocumentHandler) {
// We're doing these things here because it is critical that they are switched at the same time
if (m_chatDocumentHandler->document()) {
m_currentRoom->mainCache()->setSavedText(m_chatDocumentHandler->document()->textDocument()->toPlainText());
m_chatDocumentHandler->setRoom(room);
if (room) {
m_chatDocumentHandler->document()->textDocument()->setPlainText(room->mainCache()->savedText());
room->mainCache()->setText(room->mainCache()->savedText());
}
} else {
m_chatDocumentHandler->setRoom(room);
}
}
if (!room) {
setCurrentRoom({});
return;
}
// It's important that we compare room *objects* here, not just room *ids*, since we need to deal with the object changing when going invite -> joined
if (m_currentRoom && m_currentRoom == room) {
if (!eventId.isEmpty()) {
Q_EMIT goToEvent(eventId);
}
} else {
setCurrentRoom(room->id());
}
}
void RoomManager::joinRoom(Quotient::Connection *account, const QString &roomAliasOrId, const QStringList &viaServers)
{
auto job = account->joinRoom(roomAliasOrId, viaServers);
connect(
job.get(),
&Quotient::BaseJob::finished,
this,
[this, account](Quotient::BaseJob *finish) {
if (finish->status() == Quotient::BaseJob::Success) {
connect(
account,
&NeoChatConnection::newRoom,
this,
[this](Quotient::Room *room) {
resolveResource(room->id());
},
Qt::SingleShotConnection);
} else {
Q_EMIT showMessage(MessageType::Warning, i18n("Failed to join room<br />%1", finish->errorString()));
}
},
Qt::SingleShotConnection);
}
void RoomManager::knockRoom(NeoChatConnection *account, const QString &roomAliasOrId, const QString &reason, const QStringList &viaServers)
{
auto job = account->callApi<KnockRoomJob>(roomAliasOrId, viaServers, viaServers, reason);
// Upon completion, ensure a room object is created in case it hasn't come
// with a sync yet. If the room object is not there, provideRoom() will
// create it in Join state. finished() is used here instead of success()
// to overtake clients that may add their own slots to finished().
connect(
job.get(),
&BaseJob::finished,
this,
[this, job, account] {
if (job->status() == Quotient::BaseJob::Success) {
connect(
account,
&NeoChatConnection::newRoom,
this,
[this](Quotient::Room *room) {
Q_EMIT showMessage(MessageType::Information, i18n("You requested to join '%1'", room->name()));
},
Qt::SingleShotConnection);
} else {
Q_EMIT showMessage(MessageType::Warning, i18n("Failed to request joining room<br />%1", job->errorString()));
}
},
Qt::SingleShotConnection);
}
bool RoomManager::visitNonMatrix(const QUrl &url)
{
UrlHelper().openUrl(url);
return true;
}
void RoomManager::leaveRoom(NeoChatRoom *room)
{
if (!room) {
return;
}
if (m_currentRoom && m_currentRoom->id() == room->id()) {
setCurrentRoom({});
}
if (m_currentSpaceId == room->id()) {
setCurrentSpace({});
}
room->forget();
}
ChatDocumentHandler *RoomManager::chatDocumentHandler() const
{
return m_chatDocumentHandler;
}
void RoomManager::setChatDocumentHandler(ChatDocumentHandler *handler)
{
m_chatDocumentHandler = handler;
m_chatDocumentHandler->setRoom(m_currentRoom);
Q_EMIT chatDocumentHandlerChanged();
}
void RoomManager::setConnection(NeoChatConnection *connection)
{
if (m_connection == connection) {
return;
}
if (m_connection != nullptr) {
m_connection->disconnect(this);
}
m_connection = connection;
if (m_connection != nullptr) {
connect(m_connection, &NeoChatConnection::showMessage, this, &RoomManager::showMessage);
connect(m_connection, &NeoChatConnection::createdRoom, this, [this](Quotient::Room *room) {
resolveResource(room->id());
});
connect(m_connection, &NeoChatConnection::directChatAvailable, this, [this](Quotient::Room *directChat) {
resolveResource(directChat->id());
});
connect(m_connection, &NeoChatConnection::joinedRoom, this, [this](const auto &room, const auto &previousRoom) {
if (m_currentRoom == previousRoom) {
resolveResource(room->id());
}
});
}
Q_EMIT connectionChanged();
}
void RoomManager::setCurrentSpace(const QString &spaceId, bool setRoom)
{
m_currentSpaceId = spaceId;
// This need to happen before the signal so TreeView.expandRecursively() can work nicely.
m_sortFilterRoomTreeModel->setActiveSpaceId(m_currentSpaceId);
m_sortFilterRoomTreeModel->setMode(m_currentSpaceId == QLatin1String("DM") ? SortFilterRoomTreeModel::DirectChats : SortFilterRoomTreeModel::Rooms);
Q_EMIT currentSpaceChanged();
if (m_connection) {
m_lastSpaceConfig.writeEntry(m_connection->userId(), spaceId);
}
if (!setRoom) {
return;
}
if (spaceId.length() > 3) {
resolveResource(spaceId, "no_join"_ls);
} else if (!m_isMobile) {
visitRoom({}, {});
}
}
void RoomManager::setCurrentRoom(const QString &roomId)
{
if (m_currentRoom != nullptr) {
m_currentRoom->disconnect(this);
}
if (roomId.isEmpty()) {
m_currentRoom = nullptr;
} else {
m_currentRoom = dynamic_cast<NeoChatRoom *>(m_connection->room(roomId));
}
if (m_currentRoom != nullptr) {
connect(m_currentRoom, &NeoChatRoom::showMessage, this, &RoomManager::showMessage);
}
Q_EMIT currentRoomChanged();
if (m_connection) {
m_lastRoomConfig.writeEntry(m_connection->userId(), roomId);
}
if (roomId.isEmpty()) {
return;
}
if (m_currentRoom->isSpace()) {
return;
}
if (m_currentRoom->isDirectChat()) {
if (m_currentSpaceId != "DM"_ls) {
setCurrentSpace("DM"_ls, false);
}
return;
}
const auto &parentSpaces = SpaceHierarchyCache::instance().parentSpaces(roomId);
if (parentSpaces.contains(m_currentSpaceId)) {
return;
}
static auto config = NeoChatConfig::self();
if (config->allRoomsInHome()) {
setCurrentSpace({}, false);
return;
}
if (const auto &parent = m_connection->room(m_currentRoom->canonicalParent())) {
for (const auto &parentParent : SpaceHierarchyCache::instance().parentSpaces(parent->id())) {
if (SpaceHierarchyCache::instance().parentSpaces(parentParent).isEmpty()) {
setCurrentSpace(parentParent, false);
return;
}
}
setCurrentSpace(parent->id(), false);
return;
}
for (const auto &space : parentSpaces) {
if (SpaceHierarchyCache::instance().parentSpaces(space).isEmpty()) {
setCurrentSpace(space, false);
return;
}
}
setCurrentSpace({}, false);
}
void RoomManager::clearCurrentRoom()
{
setCurrentRoom(QString());
}
QString RoomManager::currentSpace() const
{
return m_currentSpaceId;
}
void RoomManager::resetState()
{
setCurrentRoom({});
setCurrentSpace({});
}
#include "moc_roommanager.cpp"