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