File indexing completed on 2024-09-15 04:28:35
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"