matrixion/src/timeline/TimelineViewManager.cpp

543 lines
19 KiB
C++
Raw Normal View History

2021-03-05 02:35:15 +03:00
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
// SPDX-FileCopyrightText: 2023 Nheko Contributors
2021-03-05 02:35:15 +03:00
//
// SPDX-License-Identifier: GPL-3.0-or-later
2019-11-09 05:06:10 +03:00
#include "TimelineViewManager.h"
#include <QApplication>
#include <QFileDialog>
#include <QStandardPaths>
2020-07-11 02:19:48 +03:00
#include <QString>
2019-11-09 05:06:10 +03:00
#include "Cache.h"
2019-11-09 05:06:10 +03:00
#include "ChatPage.h"
2021-07-21 02:03:38 +03:00
#include "CombinedImagePackModel.h"
#include "CommandCompleter.h"
#include "CompletionProxyModel.h"
#include "EventAccessors.h"
#include "ImagePackListModel.h"
2021-06-11 03:13:12 +03:00
#include "InviteesModel.h"
2018-07-17 16:37:25 +03:00
#include "Logging.h"
#include "MainWindow.h"
2020-01-17 03:25:14 +03:00
#include "MatrixClient.h"
#include "RoomsModel.h"
2019-11-09 05:06:10 +03:00
#include "UserSettingsPage.h"
#include "UsersModel.h"
2022-06-16 02:19:26 +03:00
#include "Utils.h"
#include "emoji/EmojiModel.h"
2022-06-16 02:19:26 +03:00
#include "encryption/VerificationManager.h"
#include "voip/CallManager.h"
#include "voip/WebRTCSession.h"
2017-04-06 02:06:42 +03:00
namespace msgs = mtx::events::msg;
namespace {
template<template<class...> class Op, class... Args>
using is_detected = typename nheko::detail::detector<nheko::nonesuch, void, Op, Args...>::value_t;
template<class Content>
using file_t = decltype(Content::file);
template<class Content>
using url_t = decltype(Content::url);
template<class Content>
using body_t = decltype(Content::body);
template<class Content>
using formatted_body_t = decltype(Content::formatted_body);
template<typename T>
static constexpr bool
messageWithFileAndUrl(const mtx::events::Event<T> &)
{
2021-09-18 01:22:33 +03:00
return is_detected<file_t, T>::value && is_detected<url_t, T>::value;
}
template<typename T>
static constexpr void
removeReplyFallback(mtx::events::Event<T> &e)
{
2021-09-18 01:22:33 +03:00
if constexpr (is_detected<body_t, T>::value) {
if constexpr (std::is_same_v<std::optional<std::string>,
std::remove_cv_t<decltype(e.content.body)>>) {
if (e.content.body) {
e.content.body = utils::stripReplyFromBody(e.content.body);
}
} else if constexpr (std::is_same_v<std::string,
std::remove_cv_t<decltype(e.content.body)>>) {
e.content.body = utils::stripReplyFromBody(e.content.body);
}
2021-09-18 01:22:33 +03:00
}
2021-09-18 01:22:33 +03:00
if constexpr (is_detected<formatted_body_t, T>::value) {
if (e.content.format == "org.matrix.custom.html") {
e.content.formatted_body = utils::stripReplyFromFormattedBody(e.content.formatted_body);
}
2021-09-18 01:22:33 +03:00
}
}
}
void
2019-11-09 05:06:10 +03:00
TimelineViewManager::updateColorPalette()
{
2021-09-18 01:22:33 +03:00
userColors.clear();
}
QColor
TimelineViewManager::userColor(QString id, QColor background)
{
2021-11-19 00:33:45 +03:00
QPair<QString, quint64> idx{id, background.rgba64()};
if (!userColors.contains(idx))
userColors.insert(idx, QColor(utils::generateContrastingHexColor(id, background)));
return userColors.value(idx);
}
2022-01-12 21:09:46 +03:00
TimelineViewManager::TimelineViewManager(CallManager *, ChatPage *parent)
2021-05-30 01:23:57 +03:00
: QObject(parent)
, rooms_(new RoomlistModel(this))
2021-06-10 00:52:28 +03:00
, communities_(new CommunitiesModel(this))
, verificationManager_(new VerificationManager(this))
, presenceEmitter(new PresenceEmitter(this))
{
2021-09-18 01:22:33 +03:00
static auto self = this;
qmlRegisterSingletonInstance("im.nheko", 1, 0, "TimelineManager", self);
2021-09-18 01:22:33 +03:00
qmlRegisterSingletonType<RoomlistModel>(
"im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = new FilteredRoomlistModel(self->rooms_);
connect(self->communities_,
&CommunitiesModel::currentTagIdChanged,
ptr,
&FilteredRoomlistModel::updateFilterTag);
connect(self->communities_,
&CommunitiesModel::hiddenTagsChanged,
ptr,
&FilteredRoomlistModel::updateHiddenTagsAndSpaces);
return ptr;
});
qmlRegisterSingletonInstance("im.nheko", 1, 0, "Communities", self->communities_);
qmlRegisterSingletonInstance("im.nheko", 1, 0, "VerificationManager", verificationManager_);
qmlRegisterSingletonInstance("im.nheko", 1, 0, "Presence", presenceEmitter);
2021-09-18 01:22:33 +03:00
updateColorPalette();
2022-01-12 21:09:46 +03:00
connect(UserSettings::instance().get(),
&UserSettings::themeChanged,
this,
&TimelineViewManager::updateColorPalette);
2021-12-29 08:10:08 +03:00
connect(parent,
2021-09-18 01:22:33 +03:00
&ChatPage::receivedRoomDeviceVerificationRequest,
verificationManager_,
&VerificationManager::receivedRoomDeviceVerificationRequest);
2021-12-29 08:10:08 +03:00
connect(parent,
2021-09-18 01:22:33 +03:00
&ChatPage::receivedDeviceVerificationRequest,
verificationManager_,
&VerificationManager::receivedDeviceVerificationRequest);
2021-12-29 08:10:08 +03:00
connect(parent,
2021-09-18 01:22:33 +03:00
&ChatPage::receivedDeviceVerificationStart,
verificationManager_,
&VerificationManager::receivedDeviceVerificationStart);
2021-09-18 01:22:33 +03:00
connect(parent, &ChatPage::loggedOut, this, [this]() {
isInitialSync_ = true;
emit initialSyncChanged(true);
});
2022-01-26 04:16:06 +03:00
connect(parent, &ChatPage::connectionLost, this, [this] {
isConnected_ = false;
emit isConnectedChanged(false);
});
connect(parent, &ChatPage::connectionRestored, this, [this] {
isConnected_ = true;
emit isConnectedChanged(true);
});
}
void
2021-08-14 00:58:26 +03:00
TimelineViewManager::openRoomMembers(TimelineModel *room)
{
2021-09-18 01:22:33 +03:00
if (!room)
return;
MemberList *memberList = new MemberList(room->roomId());
QQmlEngine::setObjectOwnership(memberList, QQmlEngine::JavaScriptOwnership);
2021-09-18 01:22:33 +03:00
emit openRoomMembersDialog(memberList, room);
}
void
TimelineViewManager::openRoomSettings(QString room_id)
{
RoomSettings *settings = new RoomSettings(room_id);
2021-09-18 01:22:33 +03:00
connect(rooms_->getRoomById(room_id).data(),
&TimelineModel::roomAvatarUrlChanged,
settings,
&RoomSettings::avatarChanged);
QQmlEngine::setObjectOwnership(settings, QQmlEngine::JavaScriptOwnership);
2021-09-18 01:22:33 +03:00
emit openRoomSettingsDialog(settings);
}
void
TimelineViewManager::openInviteUsers(QString roomId)
{
InviteesModel *model = new InviteesModel{};
2021-09-18 01:22:33 +03:00
connect(model, &InviteesModel::accept, this, [this, model, roomId]() {
emit inviteUsers(roomId, model->mxids());
});
QQmlEngine::setObjectOwnership(model, QQmlEngine::JavaScriptOwnership);
2021-09-18 01:22:33 +03:00
emit openInviteUsersDialog(model);
}
2021-07-22 14:55:12 +03:00
void
TimelineViewManager::openGlobalUserProfile(QString userId)
{
2021-09-18 01:22:33 +03:00
UserProfile *profile = new UserProfile{QString{}, userId, this};
QQmlEngine::setObjectOwnership(profile, QQmlEngine::JavaScriptOwnership);
2021-09-18 01:22:33 +03:00
emit openProfile(profile);
}
2022-03-27 00:28:31 +03:00
UserProfile *
TimelineViewManager::getGlobalUserProfile(QString userId)
{
UserProfile *profile = new UserProfile{QString{}, userId, this};
QQmlEngine::setObjectOwnership(profile, QQmlEngine::JavaScriptOwnership);
2022-03-27 00:28:31 +03:00
return (profile);
}
2020-10-27 20:14:06 +03:00
void
TimelineViewManager::setVideoCallItem()
{
2021-09-18 01:22:33 +03:00
WebRTCSession::instance().setVideoItem(
2022-01-12 21:09:46 +03:00
MainWindow::instance()->rootObject()->findChild<QQuickItem *>(
QStringLiteral("videoCallItem")));
}
void
TimelineViewManager::sync(const mtx::responses::Sync &sync_)
2021-05-19 20:34:10 +03:00
{
this->rooms_->sync(sync_);
this->communities_->sync(sync_);
this->presenceEmitter->sync(sync_.presence);
2021-09-18 01:22:33 +03:00
if (isInitialSync_) {
this->isInitialSync_ = false;
emit initialSyncChanged(false);
}
}
2021-04-29 20:09:16 +03:00
void
TimelineViewManager::showEvent(const QString &room_id, const QString &event_id)
{
2021-09-18 01:22:33 +03:00
if (auto room = rooms_->getRoomById(room_id)) {
auto exWin = MainWindow::instance()->windowForRoom(room_id);
if (exWin) {
exWin->requestActivate();
} else if (rooms_->currentRoom() != room) {
2021-09-18 01:22:33 +03:00
rooms_->setCurrentRoom(room_id);
2022-01-12 21:09:46 +03:00
MainWindow::instance()->requestActivate();
2021-09-18 01:22:33 +03:00
nhlog::ui()->info("Activated room {}", room_id.toStdString());
2021-04-29 20:09:16 +03:00
}
2021-09-18 01:22:33 +03:00
room->showEvent(event_id);
}
2021-04-29 20:09:16 +03:00
}
2020-09-03 20:51:50 +03:00
QString
TimelineViewManager::escapeEmoji(QString str) const
{
2021-09-18 01:22:33 +03:00
return utils::replaceEmoji(str);
2020-09-03 20:51:50 +03:00
}
2017-09-10 12:58:00 +03:00
void
2022-05-10 04:19:53 +03:00
TimelineViewManager::openImageOverlay(TimelineModel *room,
const QString &mxcUrl,
const QString &eventId,
2022-05-10 04:19:53 +03:00
double originalWidth,
double proportionalHeight)
2017-09-10 12:58:00 +03:00
{
2021-09-18 01:22:33 +03:00
if (mxcUrl.isEmpty()) {
return;
}
2022-05-10 04:19:53 +03:00
emit showImageOverlay(room, eventId, mxcUrl, originalWidth, proportionalHeight);
}
void
TimelineViewManager::openImagePackSettings(QString roomid)
{
auto room = rooms_->getRoomById(roomid).get();
auto model = new ImagePackListModel(roomid.toStdString());
QQmlEngine::setObjectOwnership(model, QQmlEngine::JavaScriptOwnership);
emit showImagePackSettings(room, model);
}
void
TimelineViewManager::saveMedia(QString mxcUrl)
{
const QString downloadsFolder =
QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
const QString openLocation = downloadsFolder + "/" + mxcUrl.splitRef(u'/').constLast();
2022-01-12 21:09:46 +03:00
const QString filename = QFileDialog::getSaveFileName(nullptr, {}, openLocation);
if (filename.isEmpty())
return;
2021-06-11 22:25:06 +03:00
const auto url = mxcUrl.toStdString();
http::client()->download(url,
[filename, url](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to retrieve image {}: {} {}",
url,
err->matrix_error.error,
static_cast<int>(err->status_code));
return;
}
try {
QFile file(filename);
if (!file.open(QIODevice::WriteOnly))
return;
file.write(QByteArray(data.data(), (int)data.size()));
file.close();
return;
} catch (const std::exception &e) {
nhlog::ui()->warn("Error while saving file to: {}", e.what());
}
});
2017-12-01 18:33:49 +03:00
}
2017-08-20 13:47:22 +03:00
void
2019-11-09 05:06:10 +03:00
TimelineViewManager::updateReadReceipts(const QString &room_id,
const std::vector<QString> &event_ids)
2017-04-06 02:06:42 +03:00
{
2021-09-18 01:22:33 +03:00
if (auto room = rooms_->getRoomById(room_id)) {
room->markEventsAsRead(event_ids);
}
2017-04-06 02:06:42 +03:00
}
2020-10-20 20:46:37 +03:00
void
TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::string &session_id)
{
2021-09-18 01:22:33 +03:00
if (auto room = rooms_->getRoomById(QString::fromStdString(room_id))) {
room->receivedSessionKey(session_id);
}
2020-10-20 20:46:37 +03:00
}
void
2021-05-24 15:04:07 +03:00
TimelineViewManager::initializeRoomlist()
{
2021-09-18 01:22:33 +03:00
rooms_->initializeRooms();
communities_->initializeSidebar();
}
void
TimelineViewManager::queueReply(const QString &roomid,
const QString &repliedToEvent,
const QString &replyBody)
{
2021-09-18 01:22:33 +03:00
if (auto room = rooms_->getRoomById(roomid)) {
room->setReply(repliedToEvent);
room->input()->message(replyBody);
}
}
2020-07-11 02:19:48 +03:00
void
TimelineViewManager::queueCallMessage(const QString &roomid,
2022-06-27 19:09:31 +03:00
const mtx::events::voip::CallInvite &callInvite)
2020-07-11 02:19:48 +03:00
{
2021-09-18 01:22:33 +03:00
if (auto room = rooms_->getRoomById(roomid))
room->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite);
2020-07-11 02:19:48 +03:00
}
void
TimelineViewManager::queueCallMessage(const QString &roomid,
2022-06-27 19:09:31 +03:00
const mtx::events::voip::CallCandidates &callCandidates)
2020-07-11 02:19:48 +03:00
{
2021-09-18 01:22:33 +03:00
if (auto room = rooms_->getRoomById(roomid))
room->sendMessageEvent(callCandidates, mtx::events::EventType::CallCandidates);
2020-07-11 02:19:48 +03:00
}
void
TimelineViewManager::queueCallMessage(const QString &roomid,
2022-06-27 19:09:31 +03:00
const mtx::events::voip::CallAnswer &callAnswer)
2020-07-11 02:19:48 +03:00
{
2021-09-18 01:22:33 +03:00
if (auto room = rooms_->getRoomById(roomid))
room->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer);
2020-07-11 02:19:48 +03:00
}
void
TimelineViewManager::queueCallMessage(const QString &roomid,
2022-06-27 19:09:31 +03:00
const mtx::events::voip::CallHangUp &callHangUp)
2020-07-11 02:19:48 +03:00
{
2021-09-18 01:22:33 +03:00
if (auto room = rooms_->getRoomById(roomid))
room->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp);
2017-10-07 20:50:32 +03:00
}
void
TimelineViewManager::queueCallMessage(const QString &roomid,
const mtx::events::voip::CallSelectAnswer &callSelectAnswer)
{
if (auto room = rooms_->getRoomById(roomid))
room->sendMessageEvent(callSelectAnswer, mtx::events::EventType::CallSelectAnswer);
}
void
TimelineViewManager::queueCallMessage(const QString &roomid,
const mtx::events::voip::CallReject &callReject)
{
if (auto room = rooms_->getRoomById(roomid))
room->sendMessageEvent(callReject, mtx::events::EventType::CallReject);
}
void
TimelineViewManager::queueCallMessage(const QString &roomid,
const mtx::events::voip::CallNegotiate &callNegotiate)
{
if (auto room = rooms_->getRoomById(roomid))
room->sendMessageEvent(callNegotiate, mtx::events::EventType::CallNegotiate);
}
void
TimelineViewManager::focusMessageInput()
{
2021-09-18 01:22:33 +03:00
emit focusInput();
2021-02-14 03:28:28 +03:00
}
QObject *
TimelineViewManager::completerFor(const QString &completerName, const QString &roomId)
{
if (completerName == QLatin1String("user")) {
2021-09-18 01:22:33 +03:00
auto userModel = new UsersModel(roomId.toStdString());
auto proxy = new CompletionProxyModel(userModel);
userModel->setParent(proxy);
return proxy;
} else if (completerName == QLatin1String("emoji")) {
2021-09-18 01:22:33 +03:00
auto emojiModel = new emoji::EmojiModel();
auto proxy = new CompletionProxyModel(emojiModel);
emojiModel->setParent(proxy);
return proxy;
} else if (completerName == QLatin1String("allemoji")) {
2021-09-18 01:22:33 +03:00
auto emojiModel = new emoji::EmojiModel();
auto proxy = new CompletionProxyModel(emojiModel, 1, static_cast<size_t>(-1) / 4);
emojiModel->setParent(proxy);
return proxy;
} else if (completerName == QLatin1String("room")) {
2021-09-18 01:22:33 +03:00
auto roomModel = new RoomsModel(false);
auto proxy = new CompletionProxyModel(roomModel, 4);
roomModel->setParent(proxy);
return proxy;
} else if (completerName == QLatin1String("roomAliases")) {
2021-09-18 01:22:33 +03:00
auto roomModel = new RoomsModel(true);
auto proxy = new CompletionProxyModel(roomModel);
roomModel->setParent(proxy);
return proxy;
} else if (completerName == QLatin1String("stickers")) {
2021-09-18 01:22:33 +03:00
auto stickerModel = new CombinedImagePackModel(roomId.toStdString(), true);
auto proxy = new CompletionProxyModel(stickerModel, 1, static_cast<size_t>(-1) / 4);
stickerModel->setParent(proxy);
return proxy;
} else if (completerName == QLatin1String("customEmoji")) {
auto stickerModel = new CombinedImagePackModel(roomId.toStdString(), false);
auto proxy = new CompletionProxyModel(stickerModel);
stickerModel->setParent(proxy);
return proxy;
} else if (completerName == QLatin1String("command")) {
2022-11-09 18:38:17 +03:00
auto commandCompleter = new CommandCompleter();
auto proxy = new CompletionProxyModel(commandCompleter);
commandCompleter->setParent(proxy);
return proxy;
2021-09-18 01:22:33 +03:00
}
return nullptr;
2021-02-22 22:16:40 +03:00
}
2021-04-11 17:31:49 +03:00
void
TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e,
QString roomId)
2021-04-11 17:31:49 +03:00
{
2021-09-18 01:22:33 +03:00
auto room = rooms_->getRoomById(roomId);
auto content = mtx::accessors::url(*e);
std::optional<mtx::crypto::EncryptedFile> encryptionInfo = mtx::accessors::file(*e);
if (encryptionInfo && !cache::isRoomEncrypted(roomId.toStdString())) {
2021-09-18 01:22:33 +03:00
http::client()->download(
content,
[this, roomId, e, encryptionInfo](const std::string &res,
const std::string &content_type,
const std::string &originalFilename,
mtx::http::RequestErr err) {
if (err)
return;
auto data =
mtx::crypto::to_string(mtx::crypto::decrypt_file(res, encryptionInfo.value()));
http::client()->upload(
data,
content_type,
originalFilename,
[this, roomId, e](const mtx::responses::ContentURI &res,
mtx::http::RequestErr err) mutable {
if (err) {
nhlog::net()->warn("failed to upload media: {} {} ({})",
err->matrix_error.error,
to_string(err->matrix_error.errcode),
static_cast<int>(err->status_code));
return;
}
std::visit(
[this, roomId, url = res.content_uri](auto ev) {
using namespace mtx::events;
if constexpr (EventType::RoomMessage ==
message_content_to_type<decltype(ev.content)> ||
EventType::Sticker ==
message_content_to_type<decltype(ev.content)>) {
if constexpr (messageWithFileAndUrl(ev)) {
ev.content.relations.relations.clear();
ev.content.file.reset();
ev.content.url = url;
}
if (auto room = rooms_->getRoomById(roomId)) {
removeReplyFallback(ev);
ev.content.relations.relations.clear();
room->sendMessageEvent(ev.content,
mtx::events::EventType::RoomMessage);
}
}
},
*e);
});
2021-09-18 01:22:33 +03:00
return;
});
2021-09-18 01:22:33 +03:00
return;
}
2021-04-11 17:31:49 +03:00
2021-09-18 01:22:33 +03:00
std::visit(
[room](auto e) {
if constexpr (mtx::events::message_content_to_type<decltype(e.content)> ==
mtx::events::EventType::RoomMessage) {
e.content.relations.relations.clear();
removeReplyFallback(e);
room->sendMessageEvent(e.content, mtx::events::EventType::RoomMessage);
}
},
*e);
}
//! WORKAROUND(Nico): for https://bugreports.qt.io/browse/QTBUG-93281
void
TimelineViewManager::fixImageRendering(QQuickTextDocument *t, QQuickItem *i)
{
2021-09-18 01:22:33 +03:00
if (t) {
QObject::connect(t->textDocument(), SIGNAL(imagesLoaded()), i, SLOT(updateWholeDocument()));
}
}