Skip to content
Snippets Groups Projects
TimelineViewManager.cpp 17.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • Nicolas Werner's avatar
    Nicolas Werner committed
    #include "TimelineViewManager.h"
    
    #include <QMetaType>
    #include <QPalette>
    #include <QQmlContext>
    
    
    #include "BlurhashProvider.h"
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    #include "ChatPage.h"
    #include "ColorImageProvider.h"
    #include "DelegateChooser.h"
    
    #include "Logging.h"
    
    #include "MatrixClient.h"
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    #include "MxcImageProvider.h"
    #include "UserSettingsPage.h"
    #include "dialogs/ImageOverlay.h"
    
    #include "emoji/EmojiModel.h"
    #include "emoji/Provider.h"
    
    #include "../ui/UserProfile.h"
    
    Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
    
    
    void
    TimelineViewManager::updateEncryptedDescriptions()
    {
    
            auto decrypt = settings->decryptSidebar();
    
            QHash<QString, QSharedPointer<TimelineModel>>::iterator i;
            for (i = models.begin(); i != models.end(); ++i) {
                    auto ptr = i.value();
    
                    if (!ptr.isNull()) {
    
                            ptr->setDecryptDescription(decrypt);
                            ptr->updateLastMessage();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    TimelineViewManager::updateColorPalette()
    
            if (settings->theme() == "light") {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    view->rootContext()->setContextProperty("currentActivePalette", QPalette());
                    view->rootContext()->setContextProperty("currentInactivePalette", QPalette());
    
            } else if (settings->theme() == "dark") {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    view->rootContext()->setContextProperty("currentActivePalette", QPalette());
                    view->rootContext()->setContextProperty("currentInactivePalette", QPalette());
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            } else {
                    view->rootContext()->setContextProperty("currentActivePalette", QPalette());
                    view->rootContext()->setContextProperty("currentInactivePalette", nullptr);
    
    QColor
    TimelineViewManager::userColor(QString id, QColor background)
    {
            if (!userColors.contains(id))
                    userColors.insert(
                      id, QColor(utils::generateContrastingHexColor(id, background.name())));
            return userColors.value(id);
    }
    
    
    QString
    TimelineViewManager::userPresence(QString id) const
    {
            return QString::fromStdString(
              mtx::presence::to_string(cache::presenceState(id.toStdString())));
    }
    QString
    TimelineViewManager::userStatus(QString id) const
    {
            return QString::fromStdString(cache::statusMessage(id.toStdString()));
    }
    
    
    TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettings, QWidget *parent)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
      : imgProvider(new MxcImageProvider())
      , colorImgProvider(new ColorImageProvider())
    
      , blurhashProvider(new BlurhashProvider())
    
      , settings(userSettings)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                             1,
                                             0,
                                             "MtxEvent",
                                             "Can't instantiate enum!");
    
            qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice");
            qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser");
    
            qmlRegisterType<DeviceVerificationFlow>("im.nheko", 1, 0, "DeviceVerificationFlow");
    
            qmlRegisterType<UserProfile>("im.nheko",1,0,"UserProfileContent");
            qRegisterMetaType<DeviceInfo>();
    
            qRegisterMetaType<mtx::events::collections::TimelineEvents>();
    
            qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel");
            qmlRegisterType<emoji::EmojiProxyModel>("im.nheko.EmojiModel", 1, 0, "EmojiProxyModel");
            qmlRegisterUncreatableType<QAbstractItemModel>(
              "im.nheko.EmojiModel", 1, 0, "QAbstractItemModel", "Used by proxy models");
            qmlRegisterUncreatableType<emoji::Emoji>(
              "im.nheko.EmojiModel", 1, 0, "Emoji", "Used by emoji models");
    
            qmlRegisterUncreatableMetaObject(emoji::staticMetaObject,
    
    Joe Donofry's avatar
    Joe Donofry committed
                                             "im.nheko.EmojiModel",
                                             1,
                                             0,
                                             "EmojiCategory",
                                             "Error: Only enums");
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    
    #ifdef USE_QUICK_VIEW
            view      = new QQuickView();
            container = QWidget::createWindowContainer(view, parent);
    #else
            view      = new QQuickWidget(parent);
            container = view;
            view->setResizeMode(QQuickWidget::SizeRootObjectToView);
            container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    
    #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
    
            view->quickWindow()->setTextRenderType(QQuickWindow::NativeTextRendering);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    #endif
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    
            connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) {
                    nhlog::ui()->debug("Status changed to {}", status);
            });
    #endif
            container->setMinimumSize(200, 200);
            view->rootContext()->setContextProperty("timelineManager", this);
    
            view->rootContext()->setContextProperty("settings", settings.data());
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            updateColorPalette();
            view->engine()->addImageProvider("MxcImage", imgProvider);
            view->engine()->addImageProvider("colorimage", colorImgProvider);
    
            view->engine()->addImageProvider("blurhash", blurhashProvider);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            view->setSource(QUrl("qrc:///qml/TimelineView.qml"));
    
            connect(dynamic_cast<ChatPage *>(parent),
                    &ChatPage::themeChanged,
                    this,
                    &TimelineViewManager::updateColorPalette);
    
            connect(dynamic_cast<ChatPage *>(parent),
                    &ChatPage::decryptSidebarChanged,
                    this,
                    &TimelineViewManager::updateEncryptedDescriptions);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
    
            for (const auto &[room_id, room] : rooms.join) {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    // addRoom will only add the room, if it doesn't exist
    
                    addRoom(QString::fromStdString(room_id));
                    const auto &room_model = models.value(QString::fromStdString(room_id));
                    room_model->addEvents(room.timeline);
    
    
                    if (ChatPage::instance()->userSettings()->typingNotifications()) {
    
                            std::vector<QString> typing;
                            typing.reserve(room.ephemeral.typing.size());
                            for (const auto &user : room.ephemeral.typing) {
                                    if (user != http::client()->user_id().to_string())
                                            typing.push_back(QString::fromStdString(user));
                            }
                            room_model->updateTypingUsers(typing);
                    }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            }
    
    
            this->isInitialSync_ = false;
            emit initialSyncChanged(false);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    TimelineViewManager::addRoom(const QString &room_id)
    
            if (!models.contains(room_id)) {
                    QSharedPointer<TimelineModel> newRoom(new TimelineModel(this, room_id));
    
                    newRoom->setDecryptDescription(settings->decryptSidebar());
    
                    connect(newRoom.data(),
                            &TimelineModel::newEncryptedImage,
                            imgProvider,
                            &MxcImageProvider::addEncryptionInfo);
                    models.insert(room_id, std::move(newRoom));
            }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    TimelineViewManager::setHistoryView(const QString &room_id)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            nhlog::ui()->info("Trying to activate room {}", room_id.toStdString());
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            auto room = models.find(room_id);
            if (room != models.end()) {
                    timeline_ = room.value().data();
                    emit activeTimelineChanged(timeline_);
                    nhlog::ui()->info("Activated room {}", room_id.toStdString());
            }
    
    TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            QQuickImageResponse *imgResponse =
              imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize());
    
            connect(imgResponse, &QQuickImageResponse::finished, this, [this, eventId, imgResponse]() {
                    if (!imgResponse->errorString().isEmpty()) {
                            nhlog::ui()->error("Error when retrieving image for overlay: {}",
                                               imgResponse->errorString().toStdString());
                            return;
                    }
                    auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image());
    
                    auto imgDialog = new dialogs::ImageOverlay(pixmap);
    
                    connect(imgDialog,
                            &dialogs::ImageOverlay::saving,
                            timeline_,
                            [this, eventId, imgDialog]() {
                                    // hide the overlay while presenting the save dialog for better
                                    // cross platform support.
                                    imgDialog->hide();
    
                                    if (!timeline_->saveMedia(eventId)) {
                                            imgDialog->show();
                                    } else {
                                            imgDialog->close();
                                    }
                            });
    
    void
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    TimelineViewManager::updateReadReceipts(const QString &room_id,
                                            const std::vector<QString> &event_ids)
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            auto room = models.find(room_id);
            if (room != models.end()) {
                    room.value()->markEventsAsRead(event_ids);
    
    void
    TimelineViewManager::initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs)
    {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            for (const auto &e : msgs) {
                    addRoom(e.first);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    models.value(e.first)->addEvents(e.second);
    
    void
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    TimelineViewManager::queueTextMessage(const QString &msg)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            if (!timeline_)
                    return;
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            mtx::events::msg::Text text = {};
            text.body                   = msg.trimmed().toStdString();
    
            if (settings->markdown()) {
    
                    text.formatted_body = utils::markdownToHtml(msg).toStdString();
    
    
                    // Don't send formatted_body, when we don't need to
    
                    if (text.formatted_body.find("<") == std::string::npos)
    
                            text.formatted_body = "";
                    else
                            text.format = "org.matrix.custom.html";
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            if (!timeline_->reply().isEmpty()) {
                    auto related = timeline_->relatedInfo(timeline_->reply());
    
    
                    QString body;
                    bool firstLine = true;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    for (const auto &line : related.quoted_body.split("\n")) {
    
                            if (firstLine) {
                                    firstLine = false;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                    body = QString("> <%1> %2\n").arg(related.quoted_user).arg(line);
    
                            } else {
                                    body = QString("%1\n> %2\n").arg(body).arg(line);
                            }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    }
    
                    text.body = QString("%1\n%2").arg(body).arg(msg).toStdString();
    
                    // NOTE(Nico): rich replies always need a formatted_body!
                    text.format = "org.matrix.custom.html";
    
                    if (settings->markdown())
    
                            text.formatted_body =
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                              utils::getFormattedQuoteBody(related, utils::markdownToHtml(msg))
    
                                .toStdString();
                    else
                            text.formatted_body =
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                              utils::getFormattedQuoteBody(related, msg.toHtmlEscaped()).toStdString();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    text.relates_to.in_reply_to.event_id = related.related_event;
                    timeline_->resetReply();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            timeline_->sendMessage(text);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    TimelineViewManager::queueEmoteMessage(const QString &msg)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            auto html = utils::markdownToHtml(msg);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            mtx::events::msg::Emote emote;
            emote.body = msg.trimmed().toStdString();
    
            if (html != msg.trimmed().toHtmlEscaped() && settings->markdown()) {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    emote.formatted_body = html.toStdString();
    
                    emote.format         = "org.matrix.custom.html";
            }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            if (!timeline_->reply().isEmpty()) {
                    emote.relates_to.in_reply_to.event_id = timeline_->reply().toStdString();
                    timeline_->resetReply();
            }
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            if (timeline_)
                    timeline_->sendMessage(emote);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    TimelineViewManager::queueReactionMessage(const QString &reactedEvent, const QString &reactionKey)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            if (!timeline_)
                    return;
    
            auto reactions = timeline_->reactions(reactedEvent.toStdString());
    
            QString selfReactedEvent;
            for (const auto &reaction : reactions) {
                    if (reactionKey == reaction.key_) {
                            selfReactedEvent = reaction.selfReactedEvent_;
                            break;
                    }
            }
    
            if (selfReactedEvent.startsWith("m"))
                    return;
    
    
            // If selfReactedEvent is empty, that means we haven't previously reacted
            if (selfReactedEvent.isEmpty()) {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    mtx::events::msg::Reaction reaction;
                    reaction.relates_to.rel_type = mtx::common::RelationType::Annotation;
                    reaction.relates_to.event_id = reactedEvent.toStdString();
                    reaction.relates_to.key      = reactionKey.toStdString();
    
                    timeline_->sendMessage(reaction);
    
                    // Otherwise, we have previously reacted and the reaction should be redacted
            } else {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    timeline_->redactEvent(selfReactedEvent);
    
    void
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    TimelineViewManager::queueImageMessage(const QString &roomid,
                                           const QString &filename,
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                           const std::optional<mtx::crypto::EncryptedFile> &file,
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                           const QString &url,
                                           const QString &mime,
                                           uint64_t dsize,
    
                                           const QSize &dimensions,
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                           const QString &blurhash)
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            mtx::events::msg::Image image;
            image.info.mimetype = mime.toStdString();
            image.info.size     = dsize;
    
            image.info.blurhash = blurhash.toStdString();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            image.body          = filename.toStdString();
            image.info.h        = dimensions.height();
            image.info.w        = dimensions.width();
    
    
            if (file)
                    image.file = file;
            else
                    image.url = url.toStdString();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            auto model = models.value(roomid);
            if (!model->reply().isEmpty()) {
                    image.relates_to.in_reply_to.event_id = model->reply().toStdString();
                    model->resetReply();
            }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            model->sendMessage(image);
    
    void
    
    TimelineViewManager::queueFileMessage(
      const QString &roomid,
      const QString &filename,
    
    Nicolas Werner's avatar
    Nicolas Werner committed
      const std::optional<mtx::crypto::EncryptedFile> &encryptedFile,
    
      const QString &url,
      const QString &mime,
    
    Nicolas Werner's avatar
    Nicolas Werner committed
      uint64_t dsize)
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            mtx::events::msg::File file;
            file.info.mimetype = mime.toStdString();
            file.info.size     = dsize;
            file.body          = filename.toStdString();
    
    
            if (encryptedFile)
                    file.file = encryptedFile;
            else
                    file.url = url.toStdString();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            auto model = models.value(roomid);
            if (!model->reply().isEmpty()) {
                    file.relates_to.in_reply_to.event_id = model->reply().toStdString();
                    model->resetReply();
            }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            model->sendMessage(file);
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    void
    TimelineViewManager::queueAudioMessage(const QString &roomid,
                                           const QString &filename,
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                           const std::optional<mtx::crypto::EncryptedFile> &file,
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                           const QString &url,
                                           const QString &mime,
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                           uint64_t dsize)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            mtx::events::msg::Audio audio;
            audio.info.mimetype = mime.toStdString();
            audio.info.size     = dsize;
            audio.body          = filename.toStdString();
            audio.url           = url.toStdString();
    
    
            if (file)
                    audio.file = file;
            else
                    audio.url = url.toStdString();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            auto model = models.value(roomid);
            if (!model->reply().isEmpty()) {
                    audio.relates_to.in_reply_to.event_id = model->reply().toStdString();
                    model->resetReply();
            }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            model->sendMessage(audio);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    void
    TimelineViewManager::queueVideoMessage(const QString &roomid,
                                           const QString &filename,
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                           const std::optional<mtx::crypto::EncryptedFile> &file,
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                           const QString &url,
                                           const QString &mime,
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                           uint64_t dsize)
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            mtx::events::msg::Video video;
            video.info.mimetype = mime.toStdString();
            video.info.size     = dsize;
            video.body          = filename.toStdString();
    
    
            if (file)
                    video.file = file;
            else
                    video.url = url.toStdString();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            auto model = models.value(roomid);
            if (!model->reply().isEmpty()) {
                    video.relates_to.in_reply_to.event_id = model->reply().toStdString();
                    model->resetReply();
            }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            model->sendMessage(video);
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    }
    
    
    void
    TimelineViewManager::startDummyVerification()
    {
            emit deviceVerificationRequest(new DeviceVerificationFlow(this));
    }