File indexing completed on 2024-04-21 04:59:30

0001 // SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
0002 // SPDX-FileCopyrightText: 2021 Alexey Rusakov <TODO>
0003 // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 
0005 #include "roommanager.h"
0006 
0007 #include "chatbarcache.h"
0008 #include "enums/delegatetype.h"
0009 #include "models/timelinemodel.h"
0010 #include "neochatconfig.h"
0011 #include "neochatconnection.h"
0012 #include "neochatroom.h"
0013 
0014 #include <KLocalizedString>
0015 #include <QDesktopServices>
0016 #include <QQuickTextDocument>
0017 #include <QStandardPaths>
0018 
0019 #include <Quotient/csapi/joining.h>
0020 #include <Quotient/csapi/knocking.h>
0021 #include <Quotient/qt_connection_util.h>
0022 #include <Quotient/user.h>
0023 
0024 #ifndef Q_OS_ANDROID
0025 #include <KIO/OpenUrlJob>
0026 #endif
0027 
0028 RoomManager::RoomManager(QObject *parent)
0029     : QObject(parent)
0030     , m_currentRoom(nullptr)
0031     , m_lastCurrentRoom(nullptr)
0032     , m_config(KSharedConfig::openStateConfig())
0033     , m_timelineModel(new TimelineModel(this))
0034     , m_messageFilterModel(new MessageFilterModel(this, m_timelineModel))
0035     , m_mediaMessageFilterModel(new MediaMessageFilterModel(this, m_messageFilterModel))
0036 {
0037     m_lastRoomConfig = m_config->group(QStringLiteral("LastOpenRoom"));
0038     m_lastSpaceConfig = m_config->group(QStringLiteral("LastOpenSpace"));
0039 
0040     connect(this, &RoomManager::currentRoomChanged, this, [this]() {
0041         m_timelineModel->setRoom(m_currentRoom);
0042     });
0043 }
0044 
0045 RoomManager::~RoomManager()
0046 {
0047 }
0048 
0049 RoomManager &RoomManager::instance()
0050 {
0051     static RoomManager _instance;
0052     return _instance;
0053 }
0054 
0055 NeoChatRoom *RoomManager::currentRoom() const
0056 {
0057     return m_currentRoom;
0058 }
0059 
0060 TimelineModel *RoomManager::timelineModel() const
0061 {
0062     return m_timelineModel;
0063 }
0064 
0065 MessageFilterModel *RoomManager::messageFilterModel() const
0066 {
0067     return m_messageFilterModel;
0068 }
0069 
0070 MediaMessageFilterModel *RoomManager::mediaMessageFilterModel() const
0071 {
0072     return m_mediaMessageFilterModel;
0073 }
0074 
0075 UriResolveResult RoomManager::resolveResource(const Uri &uri)
0076 {
0077     return UriResolverBase::visitResource(m_connection, uri);
0078 }
0079 
0080 void RoomManager::resolveResource(const QString &idOrUri, const QString &action)
0081 {
0082     Uri uri{idOrUri};
0083     if (!uri.isValid()) {
0084         Q_EMIT warning(i18n("Malformed or empty Matrix id"), i18n("%1 is not a correct Matrix identifier", idOrUri));
0085         return;
0086     }
0087 
0088     if (uri.type() != Uri::NonMatrix) {
0089         if (!m_connection) {
0090             return;
0091         }
0092         if (!action.isEmpty()) {
0093             uri.setAction(action);
0094         }
0095         // TODO we should allow the user to select a connection.
0096     }
0097 
0098     const auto result = visitResource(m_connection, uri);
0099     if (result == Quotient::CouldNotResolve) {
0100         Q_EMIT warning(i18n("Room not found"), i18n("There's no room %1 in the room list. Check the spelling and the account.", idOrUri));
0101     } else { // Invalid cases should have been eliminated earlier
0102         Q_ASSERT(result == Quotient::UriResolved);
0103 
0104         if (uri.type() == Uri::RoomAlias || uri.type() == Uri::RoomId) {
0105             connectSingleShot(m_connection, &Connection::newRoom, this, [this, uri](Room *room) {
0106                 resolveResource(room->id());
0107             });
0108         }
0109     }
0110 }
0111 
0112 void RoomManager::maximizeMedia(int index)
0113 {
0114     if (index < -1 || index > m_mediaMessageFilterModel->rowCount()) {
0115         return;
0116     }
0117     Q_EMIT showMaximizedMedia(index);
0118 }
0119 
0120 void RoomManager::requestFullScreenClose()
0121 {
0122     Q_EMIT closeFullScreen();
0123 }
0124 
0125 void RoomManager::viewEventSource(const QString &eventId)
0126 {
0127     Q_EMIT showEventSource(eventId);
0128 }
0129 
0130 void RoomManager::viewEventMenu(const QString &eventId,
0131                                 const QVariantMap &author,
0132                                 DelegateType::Type delegateType,
0133                                 const QString &plainText,
0134                                 const QString &htmlText,
0135                                 const QString &selectedText,
0136                                 const QString &mimeType,
0137                                 const FileTransferInfo &progressInfo)
0138 {
0139     if (delegateType == DelegateType::Image || delegateType == DelegateType::Video || delegateType == DelegateType::Audio
0140         || delegateType == DelegateType::File) {
0141         Q_EMIT showFileMenu(eventId, author, delegateType, plainText, mimeType, progressInfo);
0142         return;
0143     }
0144 
0145     Q_EMIT showMessageMenu(eventId, author, delegateType, plainText, htmlText, selectedText);
0146 }
0147 
0148 bool RoomManager::hasOpenRoom() const
0149 {
0150     return m_currentRoom != nullptr;
0151 }
0152 
0153 void RoomManager::setUrlArgument(const QString &arg)
0154 {
0155     m_arg = arg;
0156 }
0157 
0158 void RoomManager::loadInitialRoom()
0159 {
0160     Q_ASSERT(m_connection);
0161 
0162     if (!m_arg.isEmpty()) {
0163         resolveResource(m_arg);
0164     }
0165 
0166     if (m_currentRoom) {
0167         // we opened a room with the arg parsing already
0168         return;
0169     }
0170 
0171     openRoomForActiveConnection();
0172 
0173     connect(this, &RoomManager::connectionChanged, this, &RoomManager::openRoomForActiveConnection);
0174 }
0175 
0176 QString RoomManager::lastSpaceId()
0177 {
0178     if (!m_connection) {
0179         return {};
0180     }
0181     return m_lastSpaceConfig.readEntry(m_connection->userId(), QString());
0182 }
0183 
0184 void RoomManager::openRoomForActiveConnection()
0185 {
0186     if (!m_connection) {
0187         return;
0188     }
0189     // Read from last open room
0190     QString roomId = m_lastRoomConfig.readEntry(m_connection->userId(), QString());
0191 
0192     // TODO remove legacy check at some point.
0193     if (roomId.isEmpty()) {
0194         roomId = NeoChatConfig::self()->openRoom();
0195     }
0196 
0197     if (!roomId.isEmpty()) {
0198         // Here we can cast because the controller has been configured to
0199         // return NeoChatRoom instead of simple Quotient::Room
0200         const auto room = qobject_cast<NeoChatRoom *>(m_connection->room(roomId));
0201 
0202         if (room) {
0203             resolveResource(room->id());
0204         }
0205     }
0206 }
0207 
0208 void RoomManager::openWindow(NeoChatRoom *room)
0209 {
0210     // forward the call to QML
0211     Q_EMIT openRoomInNewWindow(room);
0212 }
0213 
0214 UriResolveResult RoomManager::visitUser(User *user, const QString &action)
0215 {
0216     if (action == "mention"_ls || action.isEmpty()) {
0217         // send it has QVariantMap because the properties in the
0218         user->load();
0219         Q_EMIT showUserDetail(user);
0220     } else if (action == "_interactive"_ls) {
0221         user->requestDirectChat();
0222     } else if (action == "chat"_ls) {
0223         user->load();
0224         Q_EMIT askDirectChatConfirmation(user);
0225     } else {
0226         return Quotient::IncorrectAction;
0227     }
0228 
0229     return Quotient::UriResolved;
0230 }
0231 
0232 void RoomManager::visitRoom(Room *room, const QString &eventId)
0233 {
0234     auto neoChatRoom = qobject_cast<NeoChatRoom *>(room);
0235     Q_ASSERT(neoChatRoom != nullptr);
0236 
0237     if (m_currentRoom && !m_currentRoom->editCache()->editId().isEmpty()) {
0238         m_currentRoom->editCache()->setEditId({});
0239     }
0240     if (m_currentRoom && !m_currentRoom->isSpace() && m_chatDocumentHandler) {
0241         // We're doing these things here because it is critical that they are switched at the same time
0242         if (m_chatDocumentHandler->document()) {
0243             m_currentRoom->mainCache()->setSavedText(m_chatDocumentHandler->document()->textDocument()->toPlainText());
0244             m_chatDocumentHandler->setRoom(neoChatRoom);
0245             m_chatDocumentHandler->document()->textDocument()->setPlainText(neoChatRoom->mainCache()->savedText());
0246             neoChatRoom->mainCache()->setText(neoChatRoom->mainCache()->savedText());
0247         } else {
0248             m_chatDocumentHandler->setRoom(neoChatRoom);
0249         }
0250     }
0251 
0252     if (m_currentRoom) {
0253         if (m_currentRoom->id() == room->id()) {
0254             Q_EMIT goToEvent(eventId);
0255         } else {
0256             m_lastCurrentRoom = std::exchange(m_currentRoom, neoChatRoom);
0257             Q_EMIT currentRoomChanged();
0258 
0259             if (neoChatRoom->isSpace()) {
0260                 m_lastSpaceConfig.writeEntry(m_connection->userId(), room->id());
0261                 Q_EMIT replaceSpaceHome(neoChatRoom);
0262             } else {
0263                 Q_EMIT replaceRoom(neoChatRoom, eventId);
0264             }
0265         }
0266     } else {
0267         m_lastCurrentRoom = std::exchange(m_currentRoom, neoChatRoom);
0268         Q_EMIT currentRoomChanged();
0269         if (neoChatRoom->isSpace()) {
0270             m_lastSpaceConfig.writeEntry(m_connection->userId(), room->id());
0271             Q_EMIT pushSpaceHome(neoChatRoom);
0272         } else {
0273             Q_EMIT pushRoom(neoChatRoom, eventId);
0274         }
0275     }
0276 
0277     // Save last open room
0278     m_lastRoomConfig.writeEntry(m_connection->userId(), room->id());
0279 }
0280 
0281 void RoomManager::joinRoom(Quotient::Connection *account, const QString &roomAliasOrId, const QStringList &viaServers)
0282 {
0283     auto job = account->joinRoom(roomAliasOrId, viaServers);
0284     connectSingleShot(job, &Quotient::BaseJob::finished, this, [this, account](Quotient::BaseJob *finish) {
0285         if (finish->status() == Quotient::BaseJob::Success) {
0286             connectSingleShot(account, &Quotient::Connection::newRoom, this, [this](Quotient::Room *room) {
0287                 resolveResource(room->id());
0288             });
0289         } else {
0290             Q_EMIT warning(i18n("Failed to join room"), finish->errorString());
0291         }
0292     });
0293 }
0294 
0295 void RoomManager::knockRoom(Quotient::Connection *account, const QString &roomAliasOrId, const QString &reason, const QStringList &viaServers)
0296 {
0297     auto *const job = account->callApi<KnockRoomJob>(roomAliasOrId, viaServers, reason);
0298     // Upon completion, ensure a room object is created in case it hasn't come
0299     // with a sync yet. If the room object is not there, provideRoom() will
0300     // create it in Join state. finished() is used here instead of success()
0301     // to overtake clients that may add their own slots to finished().
0302     connectSingleShot(job, &BaseJob::finished, this, [this, job, account] {
0303         if (job->status() == Quotient::BaseJob::Success) {
0304             connectSingleShot(account, &Quotient::Connection::newRoom, this, [this](Quotient::Room *room) {
0305                 Q_EMIT currentRoom()->showMessage(NeoChatRoom::Info, i18n("You requested to join '%1'", room->name()));
0306             });
0307         } else {
0308             Q_EMIT warning(i18n("Failed to request joining room"), job->errorString());
0309         }
0310     });
0311 }
0312 
0313 bool RoomManager::visitNonMatrix(const QUrl &url)
0314 {
0315 #ifdef Q_OS_ANDROID
0316     if (!QDesktopServices::openUrl(url)) {
0317         Q_EMIT warning(i18n("No application for the link"), i18n("Your operating system could not find an application for the link."));
0318     }
0319 #else
0320     auto *job = new KIO::OpenUrlJob(url);
0321     connect(job, &KJob::finished, this, [this](KJob *job) {
0322         if (job->error()) {
0323             Q_EMIT warning(i18n("Could not open URL"), job->errorString());
0324         }
0325     });
0326     job->start();
0327 #endif
0328     return true;
0329 }
0330 
0331 void RoomManager::reset()
0332 {
0333     m_arg = QString();
0334     m_currentRoom = nullptr;
0335     m_lastCurrentRoom = nullptr;
0336     Q_EMIT currentRoomChanged();
0337 }
0338 
0339 void RoomManager::leaveRoom(NeoChatRoom *room)
0340 {
0341     if (!room) {
0342         return;
0343     }
0344     if (m_lastCurrentRoom && room->id() == m_lastCurrentRoom->id()) {
0345         m_lastCurrentRoom = nullptr;
0346     }
0347     if (m_currentRoom && m_currentRoom->id() == room->id()) {
0348         m_currentRoom = m_lastCurrentRoom;
0349         m_lastCurrentRoom = nullptr;
0350         Q_EMIT currentRoomChanged();
0351     }
0352 
0353     room->forget();
0354 }
0355 
0356 ChatDocumentHandler *RoomManager::chatDocumentHandler() const
0357 {
0358     return m_chatDocumentHandler;
0359 }
0360 
0361 void RoomManager::setChatDocumentHandler(ChatDocumentHandler *handler)
0362 {
0363     m_chatDocumentHandler = handler;
0364     m_chatDocumentHandler->setRoom(m_currentRoom);
0365     Q_EMIT chatDocumentHandlerChanged();
0366 }
0367 
0368 NeoChatConnection *RoomManager::connection() const
0369 {
0370     return m_connection;
0371 }
0372 
0373 void RoomManager::setConnection(NeoChatConnection *connection)
0374 {
0375     if (m_connection == connection) {
0376         return;
0377     }
0378     m_connection = connection;
0379     Q_EMIT connectionChanged();
0380 }
0381 
0382 #include "moc_roommanager.cpp"