Files
neochat/src/app/roommanager.h
James Graham 275d221f75 Improve time handling in NeoChat
This introduces a new NeoChatDateTime object that wraps a QDateTime. The intent is that it can be passed to QML and has a series of functions that format the QDateTime into the various string representations we need.

This means we only have to send the single object to QML and then the correct string can be grabbed from there, simplifying the backend. It is also easy to add a new representation if needed as a function with a QString output and Q_PROPERTY can be added and then it will be available.
2026-01-28 16:16:40 +00:00

470 lines
16 KiB
C++

// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <KConfigGroup>
#include <KSharedConfig>
#include <QObject>
#include <QQmlEngine>
#include <Quotient/room.h>
#include <Quotient/roommember.h>
#include <Quotient/uriresolver.h>
#include "enums/messagecomponenttype.h"
#include "enums/messagetype.h"
#include "models/mediamessagefiltermodel.h"
#include "models/messagefiltermodel.h"
#include "models/roomlistmodel.h"
#include "models/roomtreemodel.h"
#include "models/sortfilterroomlistmodel.h"
#include "models/sortfilterroomtreemodel.h"
#include "models/sortfilterspacelistmodel.h"
#include "models/timelinemodel.h"
#include "models/userlistmodel.h"
#include "models/widgetmodel.h"
#include "neochatdatetime.h"
#include "neochatroommember.h"
class NeoChatRoom;
class NeoChatConnection;
using namespace Quotient;
/**
* @class RoomManager
*
* A singleton class to help manage which room is open in NeoChat.
*
* This class also inherits UriResolverBase and overrides the relevant functions to
* resolve various URIs. The base functions visitUser(), visitRoom(), etc are held
* private intentionally and instead resolveResource() should be called with either
* an appropriate URI or a Matrix ID and action.
*/
class RoomManager : public QObject, public UriResolverBase
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
/**
* @brief The current open room in NeoChat, if any.
*
* @sa hasOpenRoom
*/
Q_PROPERTY(NeoChatRoom *currentRoom READ currentRoom NOTIFY currentRoomChanged)
/**
* @brief The id of the space currently opened in the space drawer.
*
* If this is an empty string, the uncategorized rooms are shown.
* If it is the string "DM", the DMs are shown.
*/
Q_PROPERTY(QString currentSpace READ currentSpace WRITE setCurrentSpace NOTIFY currentSpaceChanged)
/**
* @brief The RoomListModel that should be used for linear room visualisation.
*
* The connection the model uses to get the data will be updated by this class
* so there is no need to do this manually or replace the model when the connection
* changes.
*/
Q_PROPERTY(RoomListModel *roomListModel READ roomListModel CONSTANT)
/**
* @brief The SortFilterRoomListModel that should be used for room visualisation.
*/
Q_PROPERTY(SortFilterRoomListModel *sortFilterRoomListModel READ sortFilterRoomListModel CONSTANT)
/**
* @brief The SortFilterSpaceListModel that should be used for space visualisation.
*/
Q_PROPERTY(SortFilterSpaceListModel *sortFilterSpaceListModel READ sortFilterSpaceListModel CONSTANT)
/**
* @brief The RoomTreeModel that should be used for room visualisation.
*
* The connection the model uses to get the data will be updated by this class
* so there is no need to do this manually or replace the model when the connection
* changes.
*/
Q_PROPERTY(RoomTreeModel *roomTreeModel READ roomTreeModel CONSTANT)
/**
* @brief The SortFilterRoomTreeModel that should be used for room visualisation.
*/
Q_PROPERTY(SortFilterRoomTreeModel *sortFilterRoomTreeModel READ sortFilterRoomTreeModel CONSTANT)
/**
* @brief The TimelineModel that should be used for room message visualisation.
*
* The room object the model uses to get the data will be updated by this class
* so there is no need to do this manually or replace the model when a room
* changes.
*
* @note Available here so that the room page and drawer both have access to the
* same model.
*/
Q_PROPERTY(TimelineModel *timelineModel READ timelineModel CONSTANT)
/**
* @brief The MessageFilterModel that should be used for room message visualisation.
*
* @note Available here so that the room page and drawer both have access to the
* same model.
*/
Q_PROPERTY(MessageFilterModel *messageFilterModel READ messageFilterModel CONSTANT)
/**
* @brief The MediaMessageFilterModel that should be used for room media message visualisation.
*
* @note Available here so that the room page and drawer both have access to the
* same model.
*/
Q_PROPERTY(MediaMessageFilterModel *mediaMessageFilterModel READ mediaMessageFilterModel CONSTANT)
/**
* @brief The UserListModel that should be used for room member visualisation.
*
* @note Available here so that the room page and drawer both have access to the
* same model.
*/
Q_PROPERTY(UserListModel *userListModel READ userListModel CONSTANT)
/**
* @brief The WidgetModel that should be used for room widget visualisation.
*
* @note Available here so that the room page and drawer both have access to the
* same model.
*/
Q_PROPERTY(WidgetModel *widgetModel READ widgetModel CONSTANT)
/**
* @brief Whether a room is currently open in NeoChat.
*
* @sa room
*/
Q_PROPERTY(bool hasOpenRoom READ hasOpenRoom NOTIFY currentRoomChanged)
public:
virtual ~RoomManager();
static RoomManager &instance();
static RoomManager *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
NeoChatRoom *currentRoom() const;
RoomListModel *roomListModel() const;
SortFilterRoomListModel *sortFilterRoomListModel() const;
SortFilterSpaceListModel *sortFilterSpaceListModel() const;
RoomTreeModel *roomTreeModel() const;
SortFilterRoomTreeModel *sortFilterRoomTreeModel() const;
TimelineModel *timelineModel() const;
MessageFilterModel *messageFilterModel() const;
MediaMessageFilterModel *mediaMessageFilterModel() const;
UserListModel *userListModel() const;
Q_INVOKABLE void activateUserModel();
WidgetModel *widgetModel() const;
/**
* @brief Resolve the given resource.
*
* @note It's actually Quotient::UriResolverBase::visitResource() but with Q_INVOKABLE
* and the connection grabbed from RoomManager.
*
* @sa Quotient::UriResolverBase::visitResource()
*/
Q_INVOKABLE void resolveResource(const QString &idOrUri, const QString &action = {});
/**
* @brief Resolve the given resource URI.
*
* @note It's actually Quotient::UriResolverBase::visitResource() but with Q_INVOKABLE
* and the connection grabbed from RoomManager.
*
* @sa Quotient::UriResolverBase::visitResource()
*/
Q_INVOKABLE void resolveResource(Uri uri, const QString &action = {});
bool hasOpenRoom() const;
/**
* @brief Load the last opened room or the welcome page.
*/
Q_INVOKABLE void loadInitialRoom();
/**
* @brief Knock a room.
*
* See https://spec.matrix.org/latest/client-server-api/#knocking-on-rooms for
* knocking on rooms.
*/
void knockRoom(NeoChatConnection *account, const QString &roomAliasOrId, const QString &reason, const QStringList &viaServers);
/**
* @brief Cleanup after the given room is left.
*
* This ensures that the current room and space are not set to the left room.
*/
void roomLeft(const QString &id);
/**
* @brief Show a media item maximized.
*/
Q_INVOKABLE void maximizeMedia(const QString &eventId);
Q_INVOKABLE void maximizeCode(NeochatRoomMember *author, const NeoChatDateTime &time, const QString &codeText, const QString &language);
/**
* @brief Request that any full screen overlay currently open closes.
*/
Q_INVOKABLE void requestFullScreenClose();
/**
* @brief Show the JSON source for the given event Matrix ID
*/
Q_INVOKABLE void viewEventSource(const QString &eventId);
/**
* @brief Show a context menu for the given event.
*/
Q_INVOKABLE void
viewEventMenu(QObject *parent, const RoomEvent *event, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
/**
* @brief Set a URL to be loaded as the initial room.
*/
void setUrlArgument(const QString &arg);
QString currentSpace() const;
bool directChatsActive() const;
void setDirectChatsActive(bool directChatsActive);
/**
* @brief Set the current connection
*/
void setConnection(NeoChatConnection *connection);
/**
* @brief Clear the current room.
*/
Q_INVOKABLE void clearCurrentRoom();
/**
* Closes the current room and space; for situations, where it is unclear which room should be opened.
*/
void resetState();
Q_SIGNALS:
/** Ask the user whether the room should be joined. */
void askJoinRoom(const QString &nameOrId);
void currentRoomChanged();
/**
* @brief Go to the specified event in the current room.
*/
void goToEvent(const QString &event);
/**
* @brief Show details for the given user.
*
* Ask current room to open the user's details for the give user.
* This assumes the user is loaded.
*/
void showUserDetail(const Quotient::User *user, const NeoChatRoom *room);
/**
* @brief Request a media item is shown maximized.
*
* @param index the index to open the maximize delegate model at. This is the
* index in the MediaMessageFilterModel owned by this RoomManager. A value
* of -1 opens a the default item.
*/
void showMaximizedMedia(int index);
/**
* @brief Request a block of code is shown maximized.
*/
void showMaximizedCode(NeochatRoomMember *author, const NeoChatDateTime &dateTime, const QString &codeText, const QString &language);
/**
* @brief Request that any full screen overlay closes.
*/
void closeFullScreen();
/**
* @brief Request the JSON source for the given event ID is shown.
*/
void showEventSource(const QString &eventId);
/**
* @brief Request to show a menu for the given event.
*/
void showDelegateMenu(QObject *parent,
NeoChatRoom *room,
const QString &eventId,
const NeochatRoomMember *author,
MessageComponentType::Type messageComponentType,
const QString &plainText,
const QString &richtText,
const QString &mimeType,
const FileTransferInfo &progressInfo,
const QString &selectedText,
const QString &hoveredLink);
/**
* @brief Show the direct chat confirmation dialog.
*
* Ask current room to show confirmation dialog to open direct chat.
* This assumes the user is loaded.
*/
void askDirectChatConfirmation(const Quotient::User *user);
/**
* @brief Request a message be shown to the user of the given type.
*/
void showMessage(MessageType::Type messageType, const QString &message);
void connectionChanged();
void directChatsActiveChanged();
void externalUrl(const QUrl &url);
void currentSpaceChanged();
protected:
bool m_dontUpdateLastRoom = false; // Don't set directly, use LastRoomBlocker.
friend class LastRoomBlocker;
private:
bool m_isMobile = false;
void openRoomForActiveConnection();
/** The room currently being shown in the main view (RoomPage.qml). This can be null, if there is no room.
* If this is a space, the space home page is shown.
*/
QPointer<NeoChatRoom> m_currentRoom;
/** The id of the space currently opened in the space drawer. If this is empty, the uncategorized rooms are shown.
* If it is "DM", the direct messages are shown. Otherwise it's the id of a toplevel space.
*/
QString m_currentSpaceId;
QString m_arg;
KSharedConfig::Ptr m_config;
KConfigGroup m_lastRoomConfig;
RoomListModel *m_roomListModel;
SortFilterRoomListModel *m_sortFilterRoomListModel;
SortFilterSpaceListModel *m_sortFilterSpaceListModel;
RoomTreeModel *m_roomTreeModel;
SortFilterRoomTreeModel *m_sortFilterRoomTreeModel;
TimelineModel *m_timelineModel;
MessageFilterModel *m_messageFilterModel;
MediaMessageFilterModel *m_mediaMessageFilterModel;
UserListModel *m_userListModel;
WidgetModel *m_widgetModel;
QPointer<NeoChatConnection> m_connection;
void setCurrentRoom(const QString &roomId);
/**
* @brief Find the most appropriate space for the currently selected room
*
* Should be used to figure out what space to switch to after a room change.
*
* @return The Space ID that the currently set room should be displayed as part of. (or "DM" for DM and "" for Home)
*/
QString findSpaceIdForCurrentRoom() const;
/**
* @brief Sets the current space.
*
* @param spaceId The ID of the space, "DM" for direct messages or an empty string for Home.
* @param goToLastUsedRoom If true, we will navigate to the last opened room in this space.
*/
void setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom = true);
/**
* @brief Resolve a user URI.
*
* This overloads Quotient::UriResolverBase::visitUser().
*
* Called by Quotient::UriResolverBase::visitResource() when the passed URI
* identifies a Matrix user.
*
* @note This is private as resolveResource() should always be called, which
* will in turn call Quotient::UriResolverBase::visitResource() and this
* function if appropriate for the URI.
*
* @sa resolveResource(), Quotient::UriResolverBase::visitUser(), Quotient::UriResolverBase::visitResource()
*/
UriResolveResult visitUser(User *user, const QString &action) override;
/**
* @brief Visit a room.
*
* This overloads Quotient::UriResolverBase::visitRoom().
*
* Called by Quotient::UriResolverBase::visitResource() when the passed URI
* identifies a room or an event in a room.
*
* @note This is private as resolveResource() should always be called, which
* will in turn call Quotient::UriResolverBase::visitResource() and this
* function if appropriate for the URI.
*
* @sa resolveResource(), Quotient::UriResolverBase::visitUser(), Quotient::UriResolverBase::visitResource()
*/
Q_INVOKABLE void visitRoom(Quotient::Room *room, const QString &eventId) override;
/**
* @brief Join a room.
*
* This overloads Quotient::UriResolverBase::joinRoom().
*
* Called by Quotient::UriResolverBase::visitResource() when the passed URI has
* `action() == "join"` and identifies a room that the user defined by the
* Connection argument is not a member of.
*
* @note This is private as resolveResource() should always be called, which
* will in turn call Quotient::UriResolverBase::visitResource() and this
* function if appropriate for the URI.
*
* @sa resolveResource(), Quotient::UriResolverBase::visitUser(), Quotient::UriResolverBase::visitResource()
*/
void joinRoom(Quotient::Connection *account, const QString &roomAliasOrId, const QStringList &viaServers) override;
/**
* @brief Visit a non-matrix resource.
*
* This overloads Quotient::UriResolverBase::visitNonMatrix().
*
* Called by Quotient::UriResolverBase::visitResource() when the passed URI
* has `type() == NonMatrix`
*
* @note This is private as resolveResource() should always be called, which
* will in turn call Quotient::UriResolverBase::visitResource() and this
* function if appropriate for the URI.
*
* @sa resolveResource(), Quotient::UriResolverBase::visitUser(), Quotient::UriResolverBase::visitResource()
*/
Q_INVOKABLE bool visitNonMatrix(const QUrl &url) override;
private:
explicit RoomManager(QObject *parent = nullptr);
};