Files
neochat/src/roommanager.cpp
James Graham ecdad9f965 Space Home Page
Add a space homepage with the ability to both create new room and add existing rooms to the space. This uses a tree model for the space hierarchy and will go to any number of levels. The user should only see the add options if they have appropriate permissions.

This MR also combines the create space and room pages and adds a lot of optional functionality for managing space children.

![image](/uploads/1764b0319241ff870dc39b18b39f5d51/image.png)
2023-09-28 17:36:23 +00:00

383 lines
12 KiB
C++

// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
// SPDX-FileCopyrightText: 2021 Alexey Rusakov <TODO>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "roommanager.h"
#include "controller.h"
#include "enums/delegatetype.h"
#include "models/messageeventmodel.h"
#include "neochatconfig.h"
#include "neochatroom.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_currentRoom(nullptr)
, m_lastCurrentRoom(nullptr)
, m_config(KConfig(QStringLiteral("data"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation))
, m_messageEventModel(new MessageEventModel(this))
, m_messageFilterModel(new MessageFilterModel(this, m_messageEventModel))
, m_mediaMessageFilterModel(new MediaMessageFilterModel(this, m_messageFilterModel))
{
m_lastRoomConfig = m_config.group(QStringLiteral("LastOpenRoom"));
connect(this, &RoomManager::currentRoomChanged, this, [this]() {
m_messageEventModel->setRoom(m_currentRoom);
});
}
RoomManager::~RoomManager()
{
}
RoomManager &RoomManager::instance()
{
static RoomManager _instance;
return _instance;
}
NeoChatRoom *RoomManager::currentRoom() const
{
return m_currentRoom;
}
MessageEventModel *RoomManager::messageEventModel() const
{
return m_messageEventModel;
}
MessageFilterModel *RoomManager::messageFilterModel() const
{
return m_messageFilterModel;
}
MediaMessageFilterModel *RoomManager::mediaMessageFilterModel() const
{
return m_mediaMessageFilterModel;
}
void RoomManager::openResource(const QString &idOrUri, const QString &action)
{
Uri uri{idOrUri};
if (!uri.isValid()) {
Q_EMIT warning(i18n("Malformed or empty Matrix id"), i18n("%1 is not a correct Matrix identifier", idOrUri));
return;
}
auto account = Controller::instance().activeConnection();
if (uri.type() != Uri::NonMatrix) {
if (!account) {
return;
}
if (!action.isEmpty()) {
uri.setAction(action);
}
// TODO we should allow the user to select a connection.
}
const auto result = visitResource(account, uri);
if (result == Quotient::CouldNotResolve) {
Q_EMIT warning(i18n("Room not found"), i18n("There's no room %1 in the room list. Check the spelling and the account.", idOrUri));
} else { // Invalid cases should have been eliminated earlier
Q_ASSERT(result == Quotient::UriResolved);
}
}
void RoomManager::maximizeMedia(int index)
{
if (index < -1 || index > m_mediaMessageFilterModel->rowCount()) {
return;
}
Q_EMIT showMaximizedMedia(index);
}
void RoomManager::requestFullScreenClose()
{
Q_EMIT closeFullScreen();
}
void RoomManager::viewEventSource(const QString &eventId)
{
Q_EMIT showEventSource(eventId);
}
void RoomManager::viewEventMenu(const QString &eventId,
const QVariantMap &author,
DelegateType::Type delegateType,
const QString &plainText,
const QString &htmlText,
const QString &selectedText,
const QString &mimeType,
const FileTransferInfo &progressInfo)
{
if (delegateType == DelegateType::Image || delegateType == DelegateType::Video || delegateType == DelegateType::Audio
|| delegateType == DelegateType::File) {
Q_EMIT showFileMenu(eventId, author, delegateType, plainText, mimeType, progressInfo);
return;
}
Q_EMIT showMessageMenu(eventId, author, delegateType, plainText, htmlText, selectedText);
}
bool RoomManager::hasOpenRoom() const
{
return m_currentRoom != nullptr;
}
void RoomManager::setUrlArgument(const QString &arg)
{
m_arg = arg;
}
void RoomManager::loadInitialRoom()
{
Q_ASSERT(Controller::instance().activeConnection());
if (!m_arg.isEmpty()) {
openResource(m_arg);
}
if (m_currentRoom) {
// we opened a room with the arg parsing already
return;
}
openRoomForActiveConnection();
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, &RoomManager::openRoomForActiveConnection);
}
void RoomManager::openRoomForActiveConnection()
{
if (!Controller::instance().activeConnection()) {
return;
}
// Read from last open room
QString roomId = m_lastRoomConfig.readEntry(Controller::instance().activeConnection()->userId(), QString());
// TODO remove legacy check at some point.
if (roomId.isEmpty()) {
roomId = NeoChatConfig::self()->openRoom();
}
if (!roomId.isEmpty()) {
// Here we can cast because the controller has been configured to
// return NeoChatRoom instead of simple Quotient::Room
const auto room = qobject_cast<NeoChatRoom *>(Controller::instance().activeConnection()->room(roomId));
if (room) {
if (room->isSpace()) {
enterSpaceHome(room);
} else {
enterRoom(room);
}
}
}
}
void RoomManager::enterRoom(NeoChatRoom *room)
{
if (m_currentRoom && !m_currentRoom->chatBoxEditId().isEmpty()) {
m_currentRoom->setChatBoxEditId({});
}
if (m_currentRoom && 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->setSavedText(m_chatDocumentHandler->document()->textDocument()->toPlainText());
m_chatDocumentHandler->setRoom(room);
m_chatDocumentHandler->document()->textDocument()->setPlainText(room->savedText());
} else {
m_chatDocumentHandler->setRoom(room);
}
}
m_lastCurrentRoom = std::exchange(m_currentRoom, room);
Q_EMIT currentRoomChanged();
if (!m_lastCurrentRoom) {
Q_EMIT pushRoom(room, QString());
} else {
Q_EMIT replaceRoom(m_currentRoom, QString());
}
if (room && room->timelineSize() == 0) {
room->getPreviousContent(20);
}
// Save last open room
m_lastRoomConfig.writeEntry(Controller::instance().activeConnection()->userId(), room->id());
}
void RoomManager::openWindow(NeoChatRoom *room)
{
// forward the call to QML
Q_EMIT openRoomInNewWindow(room);
}
void RoomManager::enterSpaceHome(NeoChatRoom *spaceRoom)
{
if (!spaceRoom->isSpace()) {
return;
}
// If replacing a normal room message timeline make sure any edit is cancelled.
if (m_currentRoom && !m_currentRoom->chatBoxEditId().isEmpty()) {
m_currentRoom->setChatBoxEditId({});
}
// Save the chatbar text for the current room if any before switching
if (m_currentRoom && m_chatDocumentHandler) {
if (m_chatDocumentHandler->document()) {
m_currentRoom->setSavedText(m_chatDocumentHandler->document()->textDocument()->toPlainText());
}
}
m_lastCurrentRoom = std::exchange(m_currentRoom, spaceRoom);
Q_EMIT currentRoomChanged();
if (!m_lastCurrentRoom) {
Q_EMIT pushSpaceHome(spaceRoom);
} else {
Q_EMIT replaceSpaceHome(m_currentRoom);
}
// Save last open room
m_lastRoomConfig.writeEntry(Controller::instance().activeConnection()->userId(), spaceRoom->id());
}
UriResolveResult RoomManager::visitUser(User *user, const QString &action)
{
if (action == "mention"_ls || action.isEmpty()) {
// send it has QVariantMap because the properties in the
user->load();
Q_EMIT showUserDetail(user);
} 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 *room, const QString &eventId)
{
auto neoChatRoom = qobject_cast<NeoChatRoom *>(room);
Q_ASSERT(neoChatRoom != nullptr);
if (m_currentRoom) {
if (m_currentRoom->id() == room->id()) {
Q_EMIT goToEvent(eventId);
} else {
m_lastCurrentRoom = std::exchange(m_currentRoom, neoChatRoom);
Q_EMIT currentRoomChanged();
Q_EMIT replaceRoom(neoChatRoom, eventId);
}
} else {
m_lastCurrentRoom = std::exchange(m_currentRoom, neoChatRoom);
Q_EMIT currentRoomChanged();
Q_EMIT pushRoom(neoChatRoom, eventId);
}
}
void RoomManager::joinRoom(Quotient::Connection *account, const QString &roomAliasOrId, const QStringList &viaServers)
{
auto job = account->joinRoom(QString::fromLatin1(QUrl::toPercentEncoding(roomAliasOrId)), viaServers);
connectSingleShot(job, &Quotient::BaseJob::finished, this, [this, account](Quotient::BaseJob *finish) {
if (finish->status() == Quotient::BaseJob::Success) {
connectSingleShot(account, &Quotient::Connection::newRoom, this, [this](Quotient::Room *room) {
enterRoom(dynamic_cast<NeoChatRoom *>(room));
});
} else {
Q_EMIT warning(i18n("Failed to join room"), finish->errorString());
}
});
}
void RoomManager::knockRoom(Quotient::Connection *account, const QString &roomAliasOrId, const QString &reason, const QStringList &viaServers)
{
auto *const job = account->callApi<KnockRoomJob>(roomAliasOrId, 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().
connectSingleShot(job, &BaseJob::finished, this, [this, job, account] {
if (job->status() == Quotient::BaseJob::Success) {
connectSingleShot(account, &Quotient::Connection::newRoom, this, [this](Quotient::Room *room) {
Q_EMIT currentRoom()->showMessage(NeoChatRoom::Info, i18n("You requested to join '%1'", room->name()));
});
} else {
Q_EMIT warning(i18n("Failed to request joining room"), job->errorString());
}
});
}
bool RoomManager::visitNonMatrix(const QUrl &url)
{
#ifdef Q_OS_ANDROID
if (!QDesktopServices::openUrl(url)) {
Q_EMIT warning(i18n("No application for the link"), i18n("Your operating system could not find an application for the link."));
}
#else
auto *job = new KIO::OpenUrlJob(url);
connect(job, &KJob::finished, this, [this](KJob *job) {
if (job->error()) {
Q_EMIT warning(i18n("Could not open URL"), job->errorString());
}
});
job->start();
#endif
return true;
}
void RoomManager::reset()
{
m_arg = QString();
m_currentRoom = nullptr;
m_lastCurrentRoom = nullptr;
Q_EMIT currentRoomChanged();
}
void RoomManager::leaveRoom(NeoChatRoom *room)
{
if (!room) {
return;
}
if (m_lastCurrentRoom && room->id() == m_lastCurrentRoom->id()) {
m_lastCurrentRoom = nullptr;
}
if (m_currentRoom && m_currentRoom->id() == room->id()) {
m_currentRoom = m_lastCurrentRoom;
m_lastCurrentRoom = nullptr;
Q_EMIT currentRoomChanged();
}
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();
}
#include "moc_roommanager.cpp"