diff --git a/src/Cache.cpp b/src/Cache.cpp
index 0bcf9fbf..ee991dc2 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -125,7 +125,7 @@ template<class T>
bool
containsStateUpdates(const T &e)
{
- return std::visit([](const auto &ev) { return Cache::isStateEvent(ev); }, e);
+ return std::visit([](const auto &ev) { return Cache::isStateEvent_<decltype(ev)>; }, e);
}
bool
@@ -158,7 +158,7 @@ Cache::isHiddenEvent(lmdb::txn &txn,
index.session_id = encryptedEvent->content.session_id;
index.sender_key = encryptedEvent->content.sender_key;
- auto result = olm::decryptEvent(index, *encryptedEvent);
+ auto result = olm::decryptEvent(index, *encryptedEvent, true);
if (!result.error)
e = result.event.value();
}
@@ -288,6 +288,9 @@ Cache::setup()
outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
megolmSessionDataDb_ = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE);
+ // What rooms are encrypted
+ encryptedRooms_ = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
+
txn.commit();
databaseReady_ = true;
@@ -298,8 +301,7 @@ Cache::setEncryptedRoom(lmdb::txn &txn, const std::string &room_id)
{
nhlog::db()->info("mark room {} as encrypted", room_id);
- auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
- db.put(txn, room_id, "0");
+ encryptedRooms_.put(txn, room_id, "0");
}
bool
@@ -308,8 +310,7 @@ Cache::isRoomEncrypted(const std::string &room_id)
std::string_view unused;
auto txn = ro_txn(env_);
- auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
- auto res = db.get(txn, room_id, unused);
+ auto res = encryptedRooms_.get(txn, room_id, unused);
return res;
}
@@ -715,32 +716,29 @@ Cache::restoreOlmAccount()
}
void
-Cache::storeSecret(const std::string &name, const std::string &secret)
+Cache::storeSecret(const std::string name, const std::string secret)
{
auto settings = UserSettings::instance();
- QKeychain::WritePasswordJob job(QCoreApplication::applicationName());
- job.setAutoDelete(false);
- job.setInsecureFallback(true);
- job.setKey("matrix." +
- QString(QCryptographicHash::hash(settings->profile().toUtf8(),
- QCryptographicHash::Sha256)) +
- "." + name.c_str());
- job.setTextData(QString::fromStdString(secret));
- QEventLoop loop;
- job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
- job.start();
- loop.exec();
-
- if (job.error()) {
- nhlog::db()->warn(
- "Storing secret '{}' failed: {}", name, job.errorString().toStdString());
- } else {
- emit secretChanged(name);
- }
+ auto job = new QKeychain::WritePasswordJob(QCoreApplication::applicationName());
+ job->setInsecureFallback(true);
+ job->setKey("matrix." +
+ QString(QCryptographicHash::hash(settings->profile().toUtf8(),
+ QCryptographicHash::Sha256)) +
+ "." + name.c_str());
+ job->setTextData(QString::fromStdString(secret));
+ QObject::connect(job, &QKeychain::Job::finished, job, [name, this](QKeychain::Job *job) {
+ if (job->error()) {
+ nhlog::db()->warn(
+ "Storing secret '{}' failed: {}", name, job->errorString().toStdString());
+ } else {
+ emit secretChanged(name);
+ }
+ });
+ job->start();
}
void
-Cache::deleteSecret(const std::string &name)
+Cache::deleteSecret(const std::string name)
{
auto settings = UserSettings::instance();
QKeychain::DeletePasswordJob job(QCoreApplication::applicationName());
@@ -750,6 +748,8 @@ Cache::deleteSecret(const std::string &name)
QString(QCryptographicHash::hash(settings->profile().toUtf8(),
QCryptographicHash::Sha256)) +
"." + name.c_str());
+ // FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean
+ // time!
QEventLoop loop;
job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
@@ -759,7 +759,7 @@ Cache::deleteSecret(const std::string &name)
}
std::optional<std::string>
-Cache::secret(const std::string &name)
+Cache::secret(const std::string name)
{
auto settings = UserSettings::instance();
QKeychain::ReadPasswordJob job(QCoreApplication::applicationName());
@@ -769,6 +769,8 @@ Cache::secret(const std::string &name)
QString(QCryptographicHash::hash(settings->profile().toUtf8(),
QCryptographicHash::Sha256)) +
"." + name.c_str());
+ // FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean
+ // time!
QEventLoop loop;
job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
@@ -3383,26 +3385,30 @@ Cache::getChildRoomIds(const std::string &room_id)
}
std::vector<ImagePackInfo>
-Cache::getImagePacks(const std::string &room_id, bool stickers)
+Cache::getImagePacks(const std::string &room_id, std::optional<bool> stickers)
{
auto txn = ro_txn(env_);
std::vector<ImagePackInfo> infos;
- auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack) {
- if (!pack.pack || (stickers ? pack.pack->is_sticker() : pack.pack->is_emoji())) {
+ auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack,
+ const std::string &source_room,
+ const std::string &state_key) {
+ if (!pack.pack || !stickers.has_value() ||
+ (stickers.value() ? pack.pack->is_sticker() : pack.pack->is_emoji())) {
ImagePackInfo info;
- if (pack.pack)
- info.packname = pack.pack->display_name;
+ info.source_room = source_room;
+ info.state_key = state_key;
+ info.pack.pack = pack.pack;
for (const auto &img : pack.images) {
- if (img.second.overrides_usage() &&
+ if (stickers.has_value() && img.second.overrides_usage() &&
(stickers ? !img.second.is_sticker() : !img.second.is_emoji()))
continue;
- info.images.insert(img);
+ info.pack.images.insert(img);
}
- if (!info.images.empty())
+ if (!info.pack.images.empty())
infos.push_back(std::move(info));
}
};
@@ -3414,7 +3420,7 @@ Cache::getImagePacks(const std::string &room_id, bool stickers)
std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePack>>(
&*accountpack);
if (tmp)
- addPack(tmp->content);
+ addPack(tmp->content, "", "");
}
// packs from rooms, that were enabled globally
@@ -3433,7 +3439,7 @@ Cache::getImagePacks(const std::string &room_id, bool stickers)
if (auto pack =
getStateEvent<mtx::events::msc2545::ImagePack>(
txn, room_id2, state_id))
- addPack(pack->content);
+ addPack(pack->content, room_id2, state_id);
}
}
}
@@ -3441,17 +3447,24 @@ Cache::getImagePacks(const std::string &room_id, bool stickers)
// packs from current room
if (auto pack = getStateEvent<mtx::events::msc2545::ImagePack>(txn, room_id)) {
- addPack(pack->content);
+ addPack(pack->content, room_id, "");
}
for (const auto &pack :
getStateEventsWithType<mtx::events::msc2545::ImagePack>(txn, room_id)) {
- addPack(pack.content);
+ addPack(pack.content, room_id, pack.state_key);
}
return infos;
}
std::optional<mtx::events::collections::RoomAccountDataEvents>
+Cache::getAccountData(mtx::events::EventType type, const std::string &room_id)
+{
+ auto txn = ro_txn(env_);
+ return getAccountData(txn, type, room_id);
+}
+
+std::optional<mtx::events::collections::RoomAccountDataEvents>
Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id)
{
try {
@@ -3529,7 +3542,7 @@ Cache::roomMembers(const std::string &room_id)
}
std::map<std::string, std::optional<UserKeyCache>>
-Cache::getMembersWithKeys(const std::string &room_id)
+Cache::getMembersWithKeys(const std::string &room_id, bool verified_only)
{
std::string_view keys;
@@ -3546,10 +3559,51 @@ Cache::getMembersWithKeys(const std::string &room_id)
auto res = keysDb.get(txn, user_id, keys);
if (res) {
- members[std::string(user_id)] =
- json::parse(keys).get<UserKeyCache>();
+ auto k = json::parse(keys).get<UserKeyCache>();
+ if (verified_only) {
+ auto verif = verificationStatus(std::string(user_id));
+ if (verif.user_verified == crypto::Trust::Verified ||
+ !verif.verified_devices.empty()) {
+ auto keyCopy = k;
+ keyCopy.device_keys.clear();
+
+ std::copy_if(
+ k.device_keys.begin(),
+ k.device_keys.end(),
+ std::inserter(keyCopy.device_keys,
+ keyCopy.device_keys.end()),
+ [&verif](const auto &key) {
+ auto curve25519 = key.second.keys.find(
+ "curve25519:" + key.first);
+ if (curve25519 == key.second.keys.end())
+ return false;
+ if (auto t =
+ verif.verified_device_keys.find(
+ curve25519->second);
+ t ==
+ verif.verified_device_keys.end() ||
+ t->second != crypto::Trust::Verified)
+ return false;
+
+ return key.first ==
+ key.second.device_id &&
+ std::find(
+ verif.verified_devices.begin(),
+ verif.verified_devices.end(),
+ key.first) !=
+ verif.verified_devices.end();
+ });
+
+ if (!keyCopy.device_keys.empty())
+ members[std::string(user_id)] =
+ std::move(keyCopy);
+ }
+ } else {
+ members[std::string(user_id)] = std::move(k);
+ }
} else {
- members[std::string(user_id)] = {};
+ if (!verified_only)
+ members[std::string(user_id)] = {};
}
}
cursor.close();
@@ -4240,6 +4294,8 @@ to_json(nlohmann::json &obj, const GroupSessionData &msg)
obj["forwarding_curve25519_key_chain"] = msg.forwarding_curve25519_key_chain;
obj["currently"] = msg.currently;
+
+ obj["indices"] = msg.indices;
}
void
@@ -4253,6 +4309,8 @@ from_json(const nlohmann::json &obj, GroupSessionData &msg)
obj.value("forwarding_curve25519_key_chain", std::vector<std::string>{});
msg.currently = obj.value("currently", SharedWithUsers{});
+
+ msg.indices = obj.value("indices", std::map<uint32_t, std::string>());
}
void
diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h
index 409c9d67..69d64885 100644
--- a/src/CacheCryptoStructs.h
+++ b/src/CacheCryptoStructs.h
@@ -50,6 +50,9 @@ struct GroupSessionData
std::string sender_claimed_ed25519_key;
std::vector<std::string> forwarding_curve25519_key_chain;
+ //! map from index to event_id to check for replay attacks
+ std::map<uint32_t, std::string> indices;
+
// who has access to this session.
// Rotate, when a user leaves the room and share, when a user gets added.
SharedWithUsers currently;
diff --git a/src/CacheStructs.h b/src/CacheStructs.h
index f274d70f..4a5c5c76 100644
--- a/src/CacheStructs.h
+++ b/src/CacheStructs.h
@@ -113,6 +113,7 @@ struct RoomSearchResult
struct ImagePackInfo
{
- std::string packname;
- std::map<std::string, mtx::events::msc2545::PackImage> images;
+ mtx::events::msc2545::ImagePack pack;
+ std::string source_room;
+ std::string state_key;
};
diff --git a/src/Cache_p.h b/src/Cache_p.h
index 13fbc371..30c365a6 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -48,7 +48,8 @@ public:
// user cache stores user keys
std::optional<UserKeyCache> userKeys(const std::string &user_id);
std::map<std::string, std::optional<UserKeyCache>> getMembersWithKeys(
- const std::string &room_id);
+ const std::string &room_id,
+ bool verified_only);
void updateUserKeys(const std::string &sync_token,
const mtx::responses::QueryKeys &keyQuery);
void markUserKeysOutOfDate(lmdb::txn &txn,
@@ -97,6 +98,12 @@ public:
return getStateEvent<T>(txn, room_id, state_key);
}
+ //! retrieve a specific event from account data
+ //! pass empty room_id for global account data
+ std::optional<mtx::events::collections::RoomAccountDataEvents> getAccountData(
+ mtx::events::EventType type,
+ const std::string &room_id = "");
+
//! Retrieve member info from a room.
std::vector<RoomMember> getMembers(const std::string &room_id,
std::size_t startIndex = 0,
@@ -225,7 +232,8 @@ public:
std::vector<std::string> getParentRoomIds(const std::string &room_id);
std::vector<std::string> getChildRoomIds(const std::string &room_id);
- std::vector<ImagePackInfo> getImagePacks(const std::string &room_id, bool stickers);
+ std::vector<ImagePackInfo> getImagePacks(const std::string &room_id,
+ std::optional<bool> stickers);
//! Mark a room that uses e2e encryption.
void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id);
@@ -278,20 +286,14 @@ public:
void saveOlmAccount(const std::string &pickled);
std::string restoreOlmAccount();
- void storeSecret(const std::string &name, const std::string &secret);
- void deleteSecret(const std::string &name);
- std::optional<std::string> secret(const std::string &name);
+ void storeSecret(const std::string name, const std::string secret);
+ void deleteSecret(const std::string name);
+ std::optional<std::string> secret(const std::string name);
template<class T>
- static constexpr bool isStateEvent(const mtx::events::StateEvent<T> &)
- {
- return true;
- }
- template<class T>
- static constexpr bool isStateEvent(const mtx::events::Event<T> &)
- {
- return false;
- }
+ constexpr static bool isStateEvent_ =
+ std::is_same_v<std::remove_cv_t<std::remove_reference_t<T>>,
+ mtx::events::StateEvent<decltype(std::declval<T>().content)>>;
static int compare_state_key(const MDB_val *a, const MDB_val *b)
{
@@ -408,11 +410,27 @@ private:
}
std::visit(
- [&txn, &statesdb, &stateskeydb, &eventsDb](auto e) {
- if constexpr (isStateEvent(e)) {
+ [&txn, &statesdb, &stateskeydb, &eventsDb, &membersdb](const auto &e) {
+ if constexpr (isStateEvent_<decltype(e)>) {
eventsDb.put(txn, e.event_id, json(e).dump());
- if (e.type != EventType::Unsupported) {
+ if (std::is_same_v<
+ std::remove_cv_t<std::remove_reference_t<decltype(e)>>,
+ StateEvent<mtx::events::msg::Redacted>>) {
+ if (e.type == EventType::RoomMember)
+ membersdb.del(txn, e.state_key, "");
+ else if (e.state_key.empty())
+ statesdb.del(txn, to_string(e.type));
+ else
+ stateskeydb.del(
+ txn,
+ to_string(e.type),
+ json::object({
+ {"key", e.state_key},
+ {"id", e.event_id},
+ })
+ .dump());
+ } else if (e.type != EventType::Unsupported) {
if (e.state_key.empty())
statesdb.put(
txn, to_string(e.type), json(e).dump());
@@ -682,6 +700,8 @@ private:
lmdb::dbi outboundMegolmSessionDb_;
lmdb::dbi megolmSessionDataDb_;
+ lmdb::dbi encryptedRooms_;
+
QString localUserId_;
QString cacheDirectory_;
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 10a91557..42e3bc7b 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -31,7 +31,6 @@
#include "notifications/Manager.h"
-#include "dialogs/ReadReceipts.h"
#include "timeline/TimelineViewManager.h"
#include "blurhash.hpp"
@@ -116,29 +115,31 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
connect(this, &ChatPage::loggedOut, this, &ChatPage::logout);
- connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList users) {
- const auto room_id = currentRoom().toStdString();
-
- for (int ii = 0; ii < users.size(); ++ii) {
- QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() {
- const auto user = users.at(ii);
+ connect(
+ view_manager_,
+ &TimelineViewManager::inviteUsers,
+ this,
+ [this](QString roomId, QStringList users) {
+ for (int ii = 0; ii < users.size(); ++ii) {
+ QTimer::singleShot(ii * 500, this, [this, roomId, ii, users]() {
+ const auto user = users.at(ii);
- http::client()->invite_user(
- room_id,
- user.toStdString(),
- [this, user](const mtx::responses::RoomInvite &,
- mtx::http::RequestErr err) {
- if (err) {
- emit showNotification(
- tr("Failed to invite user: %1").arg(user));
- return;
- }
+ http::client()->invite_user(
+ roomId.toStdString(),
+ user.toStdString(),
+ [this, user](const mtx::responses::RoomInvite &,
+ mtx::http::RequestErr err) {
+ if (err) {
+ emit showNotification(
+ tr("Failed to invite user: %1").arg(user));
+ return;
+ }
- emit showNotification(tr("Invited user: %1").arg(user));
- });
- });
- }
- });
+ emit showNotification(tr("Invited user: %1").arg(user));
+ });
+ });
+ }
+ });
connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom);
connect(this, &ChatPage::newRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection);
@@ -927,31 +928,33 @@ ChatPage::currentPresence() const
void
ChatPage::ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts)
{
- for (const auto &entry : counts) {
- if (entry.second < MAX_ONETIME_KEYS) {
- const int nkeys = MAX_ONETIME_KEYS - entry.second;
+ uint16_t count = 0;
+ if (auto c = counts.find(mtx::crypto::SIGNED_CURVE25519); c != counts.end())
+ count = c->second;
- nhlog::crypto()->info("uploading {} {} keys", nkeys, entry.first);
- olm::client()->generate_one_time_keys(nkeys);
+ if (count < MAX_ONETIME_KEYS) {
+ const int nkeys = MAX_ONETIME_KEYS - count;
- http::client()->upload_keys(
- olm::client()->create_upload_keys_request(),
- [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) {
- if (err) {
- nhlog::crypto()->warn(
- "failed to update one-time keys: {} {} {}",
- err->matrix_error.error,
- static_cast<int>(err->status_code),
- static_cast<int>(err->error_code));
+ nhlog::crypto()->info(
+ "uploading {} {} keys", nkeys, mtx::crypto::SIGNED_CURVE25519);
+ olm::client()->generate_one_time_keys(nkeys);
- if (err->status_code < 400 || err->status_code >= 500)
- return;
- }
+ http::client()->upload_keys(
+ olm::client()->create_upload_keys_request(),
+ [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::crypto()->warn("failed to update one-time keys: {} {} {}",
+ err->matrix_error.error,
+ static_cast<int>(err->status_code),
+ static_cast<int>(err->error_code));
- // mark as published anyway, otherwise we may end up in a loop.
- olm::mark_keys_as_published();
- });
- }
+ if (err->status_code < 400 || err->status_code >= 500)
+ return;
+ }
+
+ // mark as published anyway, otherwise we may end up in a loop.
+ olm::mark_keys_as_published();
+ });
}
}
@@ -1024,8 +1027,15 @@ ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescriptio
auto decryptionKey = mtx::crypto::key_from_recoverykey(text.toStdString(), keyDesc);
- if (!decryptionKey)
- decryptionKey = mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc);
+ if (!decryptionKey && keyDesc.passphrase) {
+ try {
+ decryptionKey =
+ mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc);
+ } catch (std::exception &e) {
+ nhlog::crypto()->error("Failed to derive secret key from passphrase: {}",
+ e.what());
+ }
+ }
if (!decryptionKey) {
QMessageBox::information(
diff --git a/src/ImagePackModel.cpp b/src/CombinedImagePackModel.cpp
index 9b0dca8d..341a34ec 100644
--- a/src/ImagePackModel.cpp
+++ b/src/CombinedImagePackModel.cpp
@@ -2,21 +2,24 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
-#include "ImagePackModel.h"
+#include "CombinedImagePackModel.h"
#include "Cache_p.h"
#include "CompletionModelRoles.h"
-ImagePackModel::ImagePackModel(const std::string &roomId, bool stickers, QObject *parent)
+CombinedImagePackModel::CombinedImagePackModel(const std::string &roomId,
+ bool stickers,
+ QObject *parent)
: QAbstractListModel(parent)
, room_id(roomId)
{
auto packs = cache::client()->getImagePacks(room_id, stickers);
for (const auto &pack : packs) {
- QString packname = QString::fromStdString(pack.packname);
+ QString packname =
+ pack.pack.pack ? QString::fromStdString(pack.pack.pack->display_name) : "";
- for (const auto &img : pack.images) {
+ for (const auto &img : pack.pack.images) {
ImageDesc i{};
i.shortcode = QString::fromStdString(img.first);
i.packname = packname;
@@ -27,13 +30,13 @@ ImagePackModel::ImagePackModel(const std::string &roomId, bool stickers, QObject
}
int
-ImagePackModel::rowCount(const QModelIndex &) const
+CombinedImagePackModel::rowCount(const QModelIndex &) const
{
return (int)images.size();
}
QHash<int, QByteArray>
-ImagePackModel::roleNames() const
+CombinedImagePackModel::roleNames() const
{
return {
{CompletionModel::CompletionRole, "completionRole"},
@@ -48,7 +51,7 @@ ImagePackModel::roleNames() const
}
QVariant
-ImagePackModel::data(const QModelIndex &index, int role) const
+CombinedImagePackModel::data(const QModelIndex &index, int role) const
{
if (hasIndex(index.row(), index.column(), index.parent())) {
switch (role) {
diff --git a/src/ImagePackModel.h b/src/CombinedImagePackModel.h
index 937014ec..f0f69799 100644
--- a/src/ImagePackModel.h
+++ b/src/CombinedImagePackModel.h
@@ -8,7 +8,7 @@
#include <mtx/events/mscs/image_packs.hpp>
-class ImagePackModel : public QAbstractListModel
+class CombinedImagePackModel : public QAbstractListModel
{
Q_OBJECT
public:
@@ -21,7 +21,7 @@ public:
OriginalRow,
};
- ImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr);
+ CombinedImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
diff --git a/src/ImagePackListModel.cpp b/src/ImagePackListModel.cpp
new file mode 100644
index 00000000..6392de22
--- /dev/null
+++ b/src/ImagePackListModel.cpp
@@ -0,0 +1,94 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "ImagePackListModel.h"
+
+#include <QQmlEngine>
+
+#include "Cache_p.h"
+#include "SingleImagePackModel.h"
+
+ImagePackListModel::ImagePackListModel(const std::string &roomId, QObject *parent)
+ : QAbstractListModel(parent)
+ , room_id(roomId)
+{
+ auto packs_ = cache::client()->getImagePacks(room_id, std::nullopt);
+
+ for (const auto &pack : packs_) {
+ packs.push_back(
+ QSharedPointer<SingleImagePackModel>(new SingleImagePackModel(pack)));
+ }
+}
+
+int
+ImagePackListModel::rowCount(const QModelIndex &) const
+{
+ return (int)packs.size();
+}
+
+QHash<int, QByteArray>
+ImagePackListModel::roleNames() const
+{
+ return {
+ {Roles::DisplayName, "displayName"},
+ {Roles::AvatarUrl, "avatarUrl"},
+ {Roles::FromAccountData, "fromAccountData"},
+ {Roles::FromCurrentRoom, "fromCurrentRoom"},
+ {Roles::StateKey, "statekey"},
+ {Roles::RoomId, "roomid"},
+ };
+}
+
+QVariant
+ImagePackListModel::data(const QModelIndex &index, int role) const
+{
+ if (hasIndex(index.row(), index.column(), index.parent())) {
+ const auto &pack = packs.at(index.row());
+ switch (role) {
+ case Roles::DisplayName:
+ return pack->packname();
+ case Roles::AvatarUrl:
+ return pack->avatarUrl();
+ case Roles::FromAccountData:
+ return pack->roomid().isEmpty();
+ case Roles::FromCurrentRoom:
+ return pack->roomid().toStdString() == this->room_id;
+ case Roles::StateKey:
+ return pack->statekey();
+ case Roles::RoomId:
+ return pack->roomid();
+ default:
+ return {};
+ }
+ }
+ return {};
+}
+
+SingleImagePackModel *
+ImagePackListModel::packAt(int row)
+{
+ if (row < 0 || static_cast<size_t>(row) >= packs.size())
+ return {};
+ auto e = packs.at(row).get();
+ QQmlEngine::setObjectOwnership(e, QQmlEngine::CppOwnership);
+ return e;
+}
+
+SingleImagePackModel *
+ImagePackListModel::newPack(bool inRoom)
+{
+ ImagePackInfo info{};
+ if (inRoom)
+ info.source_room = room_id;
+ return new SingleImagePackModel(info);
+}
+
+bool
+ImagePackListModel::containsAccountPack() const
+{
+ for (const auto &p : packs)
+ if (p->roomid().isEmpty())
+ return true;
+ return false;
+}
diff --git a/src/ImagePackListModel.h b/src/ImagePackListModel.h
new file mode 100644
index 00000000..2aa5abb2
--- /dev/null
+++ b/src/ImagePackListModel.h
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QAbstractListModel>
+#include <QQmlEngine>
+#include <QSharedPointer>
+
+class SingleImagePackModel;
+class ImagePackListModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(bool containsAccountPack READ containsAccountPack CONSTANT)
+public:
+ enum Roles
+ {
+ DisplayName = Qt::UserRole,
+ AvatarUrl,
+ FromAccountData,
+ FromCurrentRoom,
+ StateKey,
+ RoomId,
+ };
+
+ ImagePackListModel(const std::string &roomId, QObject *parent = nullptr);
+ QHash<int, QByteArray> roleNames() const override;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+
+ Q_INVOKABLE SingleImagePackModel *packAt(int row);
+ Q_INVOKABLE SingleImagePackModel *newPack(bool inRoom);
+
+ bool containsAccountPack() const;
+
+private:
+ std::string room_id;
+
+ std::vector<QSharedPointer<SingleImagePackModel>> packs;
+};
diff --git a/src/InviteeItem.cpp b/src/InviteeItem.cpp
deleted file mode 100644
index 27f02560..00000000
--- a/src/InviteeItem.cpp
+++ /dev/null
@@ -1,28 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QHBoxLayout>
-#include <QLabel>
-#include <QPushButton>
-
-#include "InviteeItem.h"
-
-constexpr int SidePadding = 10;
-
-InviteeItem::InviteeItem(mtx::identifiers::User user, QWidget *parent)
- : QWidget{parent}
- , user_{QString::fromStdString(user.to_string())}
-{
- auto topLayout_ = new QHBoxLayout(this);
- topLayout_->setSpacing(0);
- topLayout_->setContentsMargins(SidePadding, 0, 3 * SidePadding, 0);
-
- name_ = new QLabel(user_, this);
- removeUserBtn_ = new QPushButton(tr("Remove"), this);
-
- topLayout_->addWidget(name_);
- topLayout_->addWidget(removeUserBtn_, 0, Qt::AlignRight);
-
- connect(removeUserBtn_, &QPushButton::clicked, this, &InviteeItem::removeItem);
-}
diff --git a/src/InviteeItem.h b/src/InviteeItem.h
deleted file mode 100644
index 014541ea..00000000
--- a/src/InviteeItem.h
+++ /dev/null
@@ -1,31 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QWidget>
-
-#include <mtx/identifiers.hpp>
-
-class QPushButton;
-class QLabel;
-
-class InviteeItem : public QWidget
-{
- Q_OBJECT
-
-public:
- InviteeItem(mtx::identifiers::User user, QWidget *parent = nullptr);
-
- QString userID() { return user_; }
-
-signals:
- void removeItem();
-
-private:
- QString user_;
-
- QLabel *name_;
- QPushButton *removeUserBtn_;
-};
diff --git a/src/InviteesModel.cpp b/src/InviteesModel.cpp
new file mode 100644
index 00000000..27b2116f
--- /dev/null
+++ b/src/InviteesModel.cpp
@@ -0,0 +1,84 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "InviteesModel.h"
+
+#include "Cache.h"
+#include "Logging.h"
+#include "MatrixClient.h"
+#include "mtx/responses/profile.hpp"
+
+InviteesModel::InviteesModel(QObject *parent)
+ : QAbstractListModel{parent}
+{}
+
+void
+InviteesModel::addUser(QString mxid)
+{
+ beginInsertRows(QModelIndex(), invitees_.count(), invitees_.count());
+
+ auto invitee = new Invitee{mxid, this};
+ auto indexOfInvitee = invitees_.count();
+ connect(invitee, &Invitee::userInfoLoaded, this, [this, indexOfInvitee]() {
+ emit dataChanged(index(indexOfInvitee), index(indexOfInvitee));
+ });
+
+ invitees_.push_back(invitee);
+
+ endInsertRows();
+ emit countChanged();
+}
+
+QHash<int, QByteArray>
+InviteesModel::roleNames() const
+{
+ return {{Mxid, "mxid"}, {DisplayName, "displayName"}, {AvatarUrl, "avatarUrl"}};
+}
+
+QVariant
+InviteesModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || index.row() >= (int)invitees_.size() || index.row() < 0)
+ return {};
+
+ switch (role) {
+ case Mxid:
+ return invitees_[index.row()]->mxid_;
+ case DisplayName:
+ return invitees_[index.row()]->displayName_;
+ case AvatarUrl:
+ return invitees_[index.row()]->avatarUrl_;
+ default:
+ return {};
+ }
+}
+
+QStringList
+InviteesModel::mxids()
+{
+ QStringList mxidList;
+ for (int i = 0; i < invitees_.length(); ++i)
+ mxidList.push_back(invitees_[i]->mxid_);
+ return mxidList;
+}
+
+Invitee::Invitee(const QString &mxid, QObject *parent)
+ : QObject{parent}
+ , mxid_{mxid}
+{
+ http::client()->get_profile(
+ mxid_.toStdString(),
+ [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn("failed to retrieve profile info");
+ emit userInfoLoaded();
+ return;
+ }
+
+ displayName_ = QString::fromStdString(res.display_name);
+ avatarUrl_ = QString::fromStdString(res.avatar_url);
+
+ emit userInfoLoaded();
+ });
+}
diff --git a/src/InviteesModel.h b/src/InviteesModel.h
new file mode 100644
index 00000000..a4e19ebb
--- /dev/null
+++ b/src/InviteesModel.h
@@ -0,0 +1,63 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef INVITEESMODEL_H
+#define INVITEESMODEL_H
+
+#include <QAbstractListModel>
+#include <QVector>
+
+class Invitee : public QObject
+{
+ Q_OBJECT
+
+public:
+ Invitee(const QString &mxid, QObject *parent = nullptr);
+
+signals:
+ void userInfoLoaded();
+
+private:
+ const QString mxid_;
+ QString displayName_;
+ QString avatarUrl_;
+
+ friend class InviteesModel;
+};
+
+class InviteesModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+ Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
+
+public:
+ enum Roles
+ {
+ Mxid,
+ DisplayName,
+ AvatarUrl,
+ };
+
+ InviteesModel(QObject *parent = nullptr);
+
+ Q_INVOKABLE void addUser(QString mxid);
+
+ QHash<int, QByteArray> roleNames() const override;
+ int rowCount(const QModelIndex & = QModelIndex()) const override
+ {
+ return (int)invitees_.size();
+ }
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ QStringList mxids();
+
+signals:
+ void accept();
+ void countChanged();
+
+private:
+ QVector<Invitee *> invitees_;
+};
+
+#endif // INVITEESMODEL_H
diff --git a/src/Logging.cpp b/src/Logging.cpp
index 642e8957..67bcaf7a 100644
--- a/src/Logging.cpp
+++ b/src/Logging.cpp
@@ -30,19 +30,11 @@ qmlMessageHandler(QtMsgType type, const QMessageLogContext &context, const QStri
const char *function = context.function ? context.function : "";
if (
- // Surpress binding wrning for now, as we can't set restore mode to keep compat with
- // qt 5.10
- msg.contains(QStringLiteral(
- "QML Binding: Not restoring previous value because restoreMode has not been set.")) ||
// The default style has the point size set. If you use pixel size anywhere, you get
// that warning, which is useless, since sometimes you need the pixel size to match the
// text to the size of the outer element for example. This is done in the avatar and
// without that you get one warning for every Avatar displayed, which is stupid!
- msg.endsWith(QStringLiteral("Both point size and pixel size set. Using pixel size.")) ||
- // The new syntax breaks rebinding on Qt < 5.15. Until we can drop that, we still need it.
- msg.endsWith(QStringLiteral(
- "QML Connections: Implicitly defined onFoo properties in Connections are "
- "deprecated. Use this syntax instead: function onFoo(<arguments>) { ... }")))
+ msg.endsWith(QStringLiteral("Both point size and pixel size set. Using pixel size.")))
return;
switch (type) {
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index ed337ca4..8bc90f29 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -21,6 +21,7 @@
#include "LoginPage.h"
#include "MainWindow.h"
#include "MatrixClient.h"
+#include "MemberList.h"
#include "RegisterPage.h"
#include "TrayIcon.h"
#include "UserSettingsPage.h"
@@ -32,12 +33,9 @@
#include "ui/SnackBar.h"
#include "dialogs/CreateRoom.h"
-#include "dialogs/InviteUsers.h"
#include "dialogs/JoinRoom.h"
#include "dialogs/LeaveRoom.h"
#include "dialogs/Logout.h"
-#include "dialogs/MemberList.h"
-#include "dialogs/ReadReceipts.h"
MainWindow *MainWindow::instance_ = nullptr;
@@ -311,14 +309,6 @@ MainWindow::hasActiveUser()
}
void
-MainWindow::openMemberListDialog(const QString &room_id)
-{
- auto dialog = new dialogs::MemberList(room_id, this);
-
- showDialog(dialog);
-}
-
-void
MainWindow::openLeaveRoomDialog(const QString &room_id)
{
auto dialog = new dialogs::LeaveRoom(this);
@@ -342,18 +332,6 @@ MainWindow::showOverlayProgressBar()
}
void
-MainWindow::openInviteUsersDialog(std::function<void(const QStringList &invitees)> callback)
-{
- auto dialog = new dialogs::InviteUsers(this);
- connect(dialog, &dialogs::InviteUsers::sendInvites, this, [callback](QStringList invitees) {
- if (!invitees.isEmpty())
- callback(invitees);
- });
-
- showDialog(dialog);
-}
-
-void
MainWindow::openJoinRoomDialog(std::function<void(const QString &room_id)> callback)
{
auto dialog = new dialogs::JoinRoom(this);
@@ -419,27 +397,6 @@ MainWindow::openLogoutDialog()
showDialog(dialog);
}
-void
-MainWindow::openReadReceiptsDialog(const QString &event_id)
-{
- auto dialog = new dialogs::ReadReceipts(this);
-
- const auto room_id = chat_page_->currentRoom();
-
- try {
- dialog->addUsers(cache::readReceipts(event_id, room_id));
- } catch (const lmdb::error &) {
- nhlog::db()->warn("failed to retrieve read receipts for {} {}",
- event_id.toStdString(),
- chat_page_->currentRoom().toStdString());
- dialog->deleteLater();
-
- return;
- }
-
- showDialog(dialog);
-}
-
bool
MainWindow::hasActiveDialogs() const
{
diff --git a/src/MainWindow.h b/src/MainWindow.h
index 3571f079..d423af9f 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -65,8 +65,6 @@ public:
std::function<void(const mtx::requests::CreateRoom &request)> callback);
void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
void openLogoutDialog();
- void openMemberListDialog(const QString &room_id);
- void openReadReceiptsDialog(const QString &event_id);
void hideOverlay();
void showSolidOverlayModal(QWidget *content,
diff --git a/src/MemberList.cpp b/src/MemberList.cpp
new file mode 100644
index 00000000..196647fe
--- /dev/null
+++ b/src/MemberList.cpp
@@ -0,0 +1,100 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "MemberList.h"
+
+#include "Cache.h"
+#include "ChatPage.h"
+#include "Config.h"
+#include "Logging.h"
+#include "Utils.h"
+#include "timeline/TimelineViewManager.h"
+
+MemberList::MemberList(const QString &room_id, QObject *parent)
+ : QAbstractListModel{parent}
+ , room_id_{room_id}
+{
+ try {
+ info_ = cache::singleRoomInfo(room_id_.toStdString());
+ } catch (const lmdb::error &) {
+ nhlog::db()->warn("failed to retrieve room info from cache: {}",
+ room_id_.toStdString());
+ }
+
+ try {
+ auto members = cache::getMembers(room_id_.toStdString());
+ addUsers(members);
+ numUsersLoaded_ = members.size();
+ } catch (const lmdb::error &e) {
+ nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what());
+ }
+}
+
+void
+MemberList::addUsers(const std::vector<RoomMember> &members)
+{
+ beginInsertRows(
+ QModelIndex{}, m_memberList.count(), m_memberList.count() + members.size() - 1);
+
+ for (const auto &member : members)
+ m_memberList.push_back(
+ {member,
+ ChatPage::instance()->timelineManager()->rooms()->currentRoom()->avatarUrl(
+ member.user_id)});
+
+ endInsertRows();
+}
+
+QHash<int, QByteArray>
+MemberList::roleNames() const
+{
+ return {
+ {Mxid, "mxid"},
+ {DisplayName, "displayName"},
+ {AvatarUrl, "avatarUrl"},
+ };
+}
+
+QVariant
+MemberList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || index.row() >= (int)m_memberList.size() || index.row() < 0)
+ return {};
+
+ switch (role) {
+ case Mxid:
+ return m_memberList[index.row()].first.user_id;
+ case DisplayName:
+ return m_memberList[index.row()].first.display_name;
+ case AvatarUrl:
+ return m_memberList[index.row()].second;
+ default:
+ return {};
+ }
+}
+
+bool
+MemberList::canFetchMore(const QModelIndex &) const
+{
+ const size_t numMembers = rowCount();
+ if (numMembers > 1 && numMembers < info_.member_count)
+ return true;
+ else
+ return false;
+}
+
+void
+MemberList::fetchMore(const QModelIndex &)
+{
+ loadingMoreMembers_ = true;
+ emit loadingMoreMembersChanged();
+
+ auto members = cache::getMembers(room_id_.toStdString(), rowCount());
+ addUsers(members);
+ numUsersLoaded_ += members.size();
+ emit numUsersLoadedChanged();
+
+ loadingMoreMembers_ = false;
+ emit loadingMoreMembersChanged();
+}
diff --git a/src/MemberList.h b/src/MemberList.h
new file mode 100644
index 00000000..e6522694
--- /dev/null
+++ b/src/MemberList.h
@@ -0,0 +1,67 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QAbstractListModel>
+
+#include "CacheStructs.h"
+
+class MemberList : public QAbstractListModel
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
+ Q_PROPERTY(int memberCount READ memberCount NOTIFY memberCountChanged)
+ Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged)
+ Q_PROPERTY(QString roomId READ roomId NOTIFY roomIdChanged)
+ Q_PROPERTY(int numUsersLoaded READ numUsersLoaded NOTIFY numUsersLoadedChanged)
+ Q_PROPERTY(bool loadingMoreMembers READ loadingMoreMembers NOTIFY loadingMoreMembersChanged)
+
+public:
+ enum Roles
+ {
+ Mxid,
+ DisplayName,
+ AvatarUrl,
+ };
+ MemberList(const QString &room_id, QObject *parent = nullptr);
+
+ QHash<int, QByteArray> roleNames() const override;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override
+ {
+ Q_UNUSED(parent)
+ return static_cast<int>(m_memberList.size());
+ }
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+
+ QString roomName() const { return QString::fromStdString(info_.name); }
+ int memberCount() const { return info_.member_count; }
+ QString avatarUrl() const { return QString::fromStdString(info_.avatar_url); }
+ QString roomId() const { return room_id_; }
+ int numUsersLoaded() const { return numUsersLoaded_; }
+ bool loadingMoreMembers() const { return loadingMoreMembers_; }
+
+signals:
+ void roomNameChanged();
+ void memberCountChanged();
+ void avatarUrlChanged();
+ void roomIdChanged();
+ void numUsersLoadedChanged();
+ void loadingMoreMembersChanged();
+
+public slots:
+ void addUsers(const std::vector<RoomMember> &users);
+
+protected:
+ bool canFetchMore(const QModelIndex &) const override;
+ void fetchMore(const QModelIndex &) override;
+
+private:
+ QVector<QPair<RoomMember, QString>> m_memberList;
+ QString room_id_;
+ RoomInfo info_;
+ int numUsersLoaded_{0};
+ bool loadingMoreMembers_{false};
+};
diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp
index ab6540a4..b8648269 100644
--- a/src/MxcImageProvider.cpp
+++ b/src/MxcImageProvider.cpp
@@ -22,7 +22,14 @@ QHash<QString, mtx::crypto::EncryptedFile> infos;
QQuickImageResponse *
MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{
- MxcImageResponse *response = new MxcImageResponse(id, requestedSize);
+ auto id_ = id;
+ bool crop = true;
+ if (id.endsWith("?scale")) {
+ crop = false;
+ id_.remove("?scale");
+ }
+
+ MxcImageResponse *response = new MxcImageResponse(id_, crop, requestedSize);
pool.start(response);
return response;
}
@@ -36,20 +43,24 @@ void
MxcImageResponse::run()
{
MxcImageProvider::download(
- m_id, m_requestedSize, [this](QString, QSize, QImage image, QString) {
+ m_id,
+ m_requestedSize,
+ [this](QString, QSize, QImage image, QString) {
if (image.isNull()) {
m_error = "Failed to download image.";
} else {
m_image = image;
}
emit finished();
- });
+ },
+ m_crop);
}
void
MxcImageProvider::download(const QString &id,
const QSize &requestedSize,
- std::function<void(QString, QSize, QImage, QString)> then)
+ std::function<void(QString, QSize, QImage, QString)> then,
+ bool crop)
{
std::optional<mtx::crypto::EncryptedFile> encryptionInfo;
auto temp = infos.find("mxc://" + id);
@@ -58,11 +69,12 @@ MxcImageProvider::download(const QString &id,
if (requestedSize.isValid() && !encryptionInfo) {
QString fileName =
- QString("%1_%2x%3_crop")
+ QString("%1_%2x%3_%4")
.arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding |
QByteArray::OmitTrailingEquals)))
.arg(requestedSize.width())
- .arg(requestedSize.height());
+ .arg(requestedSize.height())
+ .arg(crop ? "crop" : "scale");
QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/media_cache",
fileName);
@@ -85,7 +97,7 @@ MxcImageProvider::download(const QString &id,
opts.mxc_url = "mxc://" + id.toStdString();
opts.width = requestedSize.width() > 0 ? requestedSize.width() : -1;
opts.height = requestedSize.height() > 0 ? requestedSize.height() : -1;
- opts.method = "crop";
+ opts.method = crop ? "crop" : "scale";
http::client()->get_thumbnail(
opts,
[fileInfo, requestedSize, then, id](const std::string &res,
@@ -196,7 +208,6 @@ MxcImageProvider::download(const QString &id,
image.setText("original filename",
QString::fromStdString(originalFilename));
image.setText("mxc url", "mxc://" + id);
- image.save(fileInfo.absoluteFilePath());
then(id, requestedSize, image, fileInfo.absoluteFilePath());
});
} catch (std::exception &e) {
diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h
index 7b960836..61d82852 100644
--- a/src/MxcImageProvider.h
+++ b/src/MxcImageProvider.h
@@ -19,9 +19,10 @@ class MxcImageResponse
, public QRunnable
{
public:
- MxcImageResponse(const QString &id, const QSize &requestedSize)
+ MxcImageResponse(const QString &id, bool crop, const QSize &requestedSize)
: m_id(id)
, m_requestedSize(requestedSize)
+ , m_crop(crop)
{
setAutoDelete(false);
}
@@ -37,6 +38,7 @@ public:
QString m_id, m_error;
QSize m_requestedSize;
QImage m_image;
+ bool m_crop;
};
class MxcImageProvider
@@ -51,7 +53,8 @@ public slots:
static void addEncryptionInfo(mtx::crypto::EncryptedFile info);
static void download(const QString &id,
const QSize &requestedSize,
- std::function<void(QString, QSize, QImage, QString)> then);
+ std::function<void(QString, QSize, QImage, QString)> then,
+ bool crop = true);
private:
QThreadPool pool;
diff --git a/src/Olm.cpp b/src/Olm.cpp
index 18e2ddcf..e4ab0aa1 100644
--- a/src/Olm.cpp
+++ b/src/Olm.cpp
@@ -212,14 +212,21 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey
nhlog::crypto()->info("sender : {}", msg.sender);
nhlog::crypto()->info("sender_key: {}", msg.sender_key);
+ if (msg.sender_key == olm::client()->identity_keys().ed25519) {
+ nhlog::crypto()->warn("Ignoring olm message from ourselves!");
+ return;
+ }
+
const auto my_key = olm::client()->identity_keys().curve25519;
+ bool failed_decryption = false;
+
for (const auto &cipher : msg.ciphertext) {
// We skip messages not meant for the current device.
if (cipher.first != my_key) {
nhlog::crypto()->debug(
"Skipping message for {} since we are {}.", cipher.first, my_key);
- return;
+ continue;
}
const auto type = cipher.second.type;
@@ -234,6 +241,7 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey
msg.sender, msg.sender_key, cipher.second);
} else {
nhlog::crypto()->error("Undecryptable olm message!");
+ failed_decryption = true;
continue;
}
}
@@ -278,11 +286,17 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey
bool from_their_device = false;
for (auto [device_id, key] : otherUserDeviceKeys.device_keys) {
- if (key.keys.at("curve25519:" + device_id) == msg.sender_key) {
- if (key.keys.at("ed25519:" + device_id) == sender_ed25519) {
- from_their_device = true;
- break;
- }
+ auto c_key = key.keys.find("curve25519:" + device_id);
+ auto e_key = key.keys.find("ed25519:" + device_id);
+
+ if (c_key == key.keys.end() || e_key == key.keys.end()) {
+ nhlog::crypto()->warn(
+ "Skipping device {} as we have no keys for it.",
+ device_id);
+ } else if (c_key->second == msg.sender_key &&
+ e_key->second == sender_ed25519) {
+ from_their_device = true;
+ break;
}
}
if (!from_their_device) {
@@ -423,22 +437,28 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey
}
return;
+ } else {
+ failed_decryption = true;
}
}
- try {
- std::map<std::string, std::vector<std::string>> targets;
- for (auto [device_id, key] : otherUserDeviceKeys.device_keys) {
- if (key.keys.at("curve25519:" + device_id) == msg.sender_key)
- targets[msg.sender].push_back(device_id);
- }
+ if (failed_decryption) {
+ try {
+ std::map<std::string, std::vector<std::string>> targets;
+ for (auto [device_id, key] : otherUserDeviceKeys.device_keys) {
+ if (key.keys.at("curve25519:" + device_id) == msg.sender_key)
+ targets[msg.sender].push_back(device_id);
+ }
- send_encrypted_to_device_messages(
- targets, mtx::events::DeviceEvent<mtx::events::msg::Dummy>{}, true);
- nhlog::crypto()->info(
- "Recovering from broken olm channel with {}:{}", msg.sender, msg.sender_key);
- } catch (std::exception &e) {
- nhlog::crypto()->error("Failed to recover from broken olm sessions: {}", e.what());
+ send_encrypted_to_device_messages(
+ targets, mtx::events::DeviceEvent<mtx::events::msg::Dummy>{}, true);
+ nhlog::crypto()->info("Recovering from broken olm channel with {}:{}",
+ msg.sender,
+ msg.sender_key);
+ } catch (std::exception &e) {
+ nhlog::crypto()->error("Failed to recover from broken olm sessions: {}",
+ e.what());
+ }
}
}
@@ -504,7 +524,8 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
auto own_user_id = http::client()->user_id().to_string();
- auto members = cache::client()->getMembersWithKeys(room_id);
+ auto members = cache::client()->getMembersWithKeys(
+ room_id, UserSettings::instance()->onlyShareKeysWithVerifiedUsers());
std::map<std::string, std::vector<std::string>> sendSessionTo;
mtx::crypto::OutboundGroupSessionPtr session = nullptr;
@@ -955,13 +976,12 @@ handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyR
}
}
- if (!verifiedDevice && !shouldSeeKeys &&
- !utils::respondsToKeyRequests(req.content.room_id)) {
+ if (!verifiedDevice && !shouldSeeKeys) {
nhlog::crypto()->debug("ignoring key request for room {}", req.content.room_id);
return;
}
- if (verifiedDevice || utils::respondsToKeyRequests(req.content.room_id)) {
+ if (verifiedDevice) {
// share the minimum index we have
minimumIndex = -1;
}
@@ -1008,7 +1028,8 @@ send_megolm_key_to_device(const std::string &user_id,
DecryptionResult
decryptEvent(const MegolmSessionIndex &index,
- const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &event)
+ const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &event,
+ bool dont_write_db)
{
try {
if (!cache::client()->inboundMegolmSessionExists(index)) {
@@ -1023,10 +1044,26 @@ decryptEvent(const MegolmSessionIndex &index,
std::string msg_str;
try {
auto session = cache::client()->getInboundMegolmSession(index);
+ auto sessionData =
+ cache::client()->getMegolmSessionData(index).value_or(GroupSessionData{});
auto res =
olm::client()->decrypt_group_message(session.get(), event.content.ciphertext);
msg_str = std::string((char *)res.data.data(), res.data.size());
+
+ if (!event.event_id.empty() && event.event_id[0] == '$') {
+ auto oldIdx = sessionData.indices.find(res.message_index);
+ if (oldIdx != sessionData.indices.end()) {
+ if (oldIdx->second != event.event_id)
+ return {DecryptionErrorCode::ReplayAttack,
+ std::nullopt,
+ std::nullopt};
+ } else if (!dont_write_db) {
+ sessionData.indices[res.message_index] = event.event_id;
+ cache::client()->saveInboundMegolmSession(
+ index, std::move(session), sessionData);
+ }
+ }
} catch (const lmdb::error &e) {
return {DecryptionErrorCode::DbError, e.what(), std::nullopt};
} catch (const mtx::crypto::olm_exception &e) {
@@ -1035,24 +1072,24 @@ decryptEvent(const MegolmSessionIndex &index,
return {DecryptionErrorCode::DecryptionFailed, e.what(), std::nullopt};
}
- // Add missing fields for the event.
- json body = json::parse(msg_str);
- body["event_id"] = event.event_id;
- body["sender"] = event.sender;
- body["origin_server_ts"] = event.origin_server_ts;
- body["unsigned"] = event.unsigned_data;
+ try {
+ // Add missing fields for the event.
+ json body = json::parse(msg_str);
+ body["event_id"] = event.event_id;
+ body["sender"] = event.sender;
+ body["origin_server_ts"] = event.origin_server_ts;
+ body["unsigned"] = event.unsigned_data;
- // relations are unencrypted in content...
- mtx::common::add_relations(body["content"], event.content.relations);
+ // relations are unencrypted in content...
+ mtx::common::add_relations(body["content"], event.content.relations);
- mtx::events::collections::TimelineEvent te;
- try {
+ mtx::events::collections::TimelineEvent te;
mtx::events::collections::from_json(body, te);
+
+ return {DecryptionErrorCode::NoError, std::nullopt, std::move(te.data)};
} catch (std::exception &e) {
return {DecryptionErrorCode::ParsingFailed, e.what(), std::nullopt};
}
-
- return {std::nullopt, std::nullopt, std::move(te.data)};
}
crypto::Trust
@@ -1081,6 +1118,8 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
messages;
std::map<std::string, std::map<std::string, DevicePublicKeys>> pks;
+ auto our_curve = olm::client()->identity_keys().curve25519;
+
for (const auto &[user, devices] : targets) {
auto deviceKeys = cache::client()->userKeys(user);
@@ -1114,12 +1153,32 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
continue;
}
- auto session =
- cache::getLatestOlmSession(d.keys.at("curve25519:" + device));
+ auto device_curve = d.keys.at("curve25519:" + device);
+ if (device_curve == our_curve) {
+ nhlog::crypto()->warn("Skipping our own device, since sending "
+ "ourselves olm messages makes no sense.");
+ continue;
+ }
+
+ auto session = cache::getLatestOlmSession(device_curve);
if (!session || force_new_session) {
- claims.one_time_keys[user][device] = mtx::crypto::SIGNED_CURVE25519;
- pks[user][device].ed25519 = d.keys.at("ed25519:" + device);
- pks[user][device].curve25519 = d.keys.at("curve25519:" + device);
+ static QMap<QPair<std::string, std::string>, qint64> rateLimit;
+ auto currentTime = QDateTime::currentSecsSinceEpoch();
+ if (rateLimit.value(QPair(user, device)) + 60 * 60 * 10 <
+ currentTime) {
+ claims.one_time_keys[user][device] =
+ mtx::crypto::SIGNED_CURVE25519;
+ pks[user][device].ed25519 = d.keys.at("ed25519:" + device);
+ pks[user][device].curve25519 =
+ d.keys.at("curve25519:" + device);
+
+ rateLimit.insert(QPair(user, device), currentTime);
+ } else {
+ nhlog::crypto()->warn("Not creating new session with {}:{} "
+ "because of rate limit",
+ user,
+ device);
+ }
continue;
}
@@ -1129,7 +1188,7 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
ev_json,
UserId(user),
d.keys.at("ed25519:" + device),
- d.keys.at("curve25519:" + device))
+ device_curve)
.get<mtx::events::msg::OlmEncrypted>();
try {
@@ -1187,22 +1246,40 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
continue;
}
- // TODO: Verify signatures
auto otk = rd.second.begin()->at("key");
- auto id_key = pks.at(user_id).at(device_id).curve25519;
+ auto sign_key = pks.at(user_id).at(device_id).ed25519;
+ auto id_key = pks.at(user_id).at(device_id).curve25519;
+
+ // Verify signature
+ {
+ auto signedKey = *rd.second.begin();
+ std::string signature =
+ signedKey["signatures"][user_id].value(
+ "ed25519:" + device_id, "");
+
+ if (signature.empty() ||
+ !mtx::crypto::ed25519_verify_signature(
+ sign_key, signedKey, signature)) {
+ nhlog::net()->warn(
+ "Skipping device {} as its one time key "
+ "has an invalid signature.",
+ device_id);
+ continue;
+ }
+ }
+
auto session =
olm::client()->create_outbound_session(id_key, otk);
messages[mtx::identifiers::parse<mtx::identifiers::User>(
user_id)][device_id] =
olm::client()
- ->create_olm_encrypted_content(
- session.get(),
- ev_json,
- UserId(user_id),
- pks.at(user_id).at(device_id).ed25519,
- id_key)
+ ->create_olm_encrypted_content(session.get(),
+ ev_json,
+ UserId(user_id),
+ sign_key,
+ id_key)
.get<mtx::events::msg::OlmEncrypted>();
try {
@@ -1248,8 +1325,8 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
req.device_keys = keysToQuery;
http::client()->query_keys(
req,
- [ev_json, BindPks](const mtx::responses::QueryKeys &res,
- mtx::http::RequestErr err) {
+ [ev_json, BindPks, our_curve](const mtx::responses::QueryKeys &res,
+ mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to query device keys: {} {}",
err->matrix_error.error,
@@ -1291,6 +1368,13 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
pks.ed25519 = device_keys.at(edKey);
pks.curve25519 = device_keys.at(curveKey);
+ if (pks.curve25519 == our_curve) {
+ nhlog::crypto()->warn(
+ "Skipping our own device, since sending "
+ "ourselves olm messages makes no sense.");
+ continue;
+ }
+
try {
if (!mtx::crypto::verify_identity_signature(
dev.second, device_id, user_id)) {
@@ -1360,9 +1444,12 @@ request_cross_signing_keys()
body,
[request_id = secretRequest.request_id, secretName](mtx::http::RequestErr err) {
if (err) {
- request_id_to_secret_name.erase(request_id);
nhlog::net()->error("Failed to send request for secrect '{}'",
secretName);
+ // Cancel request on UI thread
+ QTimer::singleShot(1, cache::client(), [request_id]() {
+ request_id_to_secret_name.erase(request_id);
+ });
return;
}
});
diff --git a/src/Olm.h b/src/Olm.h
index a18cbbfb..ab86ca00 100644
--- a/src/Olm.h
+++ b/src/Olm.h
@@ -14,9 +14,11 @@
constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2";
namespace olm {
+Q_NAMESPACE
-enum class DecryptionErrorCode
+enum DecryptionErrorCode
{
+ NoError,
MissingSession, // Session was not found, retrieve from backup or request from other devices
// and try again
MissingSessionIndex, // Session was found, but it does not reach back enough to this index,
@@ -25,14 +27,13 @@ enum class DecryptionErrorCode
DecryptionFailed, // libolm error
ParsingFailed, // Failed to parse the actual event
ReplayAttack, // Megolm index reused
- UnknownFingerprint, // Unknown device Fingerprint
};
+Q_ENUM_NS(DecryptionErrorCode)
struct DecryptionResult
{
- std::optional<DecryptionErrorCode> error;
+ DecryptionErrorCode error;
std::optional<std::string> error_message;
-
std::optional<mtx::events::collections::TimelineEvents> event;
};
@@ -80,9 +81,11 @@ encrypt_group_message(const std::string &room_id,
const std::string &device_id,
nlohmann::json body);
+//! Decrypt an event. Use dont_write_db to prevent db writes when already in a write transaction.
DecryptionResult
decryptEvent(const MegolmSessionIndex &index,
- const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &event);
+ const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &event,
+ bool dont_write_db = false);
crypto::Trust
calculate_trust(const std::string &user_id, const std::string &curve25519);
diff --git a/src/ReadReceiptsModel.cpp b/src/ReadReceiptsModel.cpp
new file mode 100644
index 00000000..25262c59
--- /dev/null
+++ b/src/ReadReceiptsModel.cpp
@@ -0,0 +1,131 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "ReadReceiptsModel.h"
+
+#include <QLocale>
+
+#include "Cache.h"
+#include "Cache_p.h"
+#include "Logging.h"
+#include "Utils.h"
+
+ReadReceiptsModel::ReadReceiptsModel(QString event_id, QString room_id, QObject *parent)
+ : QAbstractListModel{parent}
+ , event_id_{event_id}
+ , room_id_{room_id}
+{
+ try {
+ addUsers(cache::readReceipts(event_id_, room_id_));
+ } catch (const lmdb::error &) {
+ nhlog::db()->warn("failed to retrieve read receipts for {} {}",
+ event_id_.toStdString(),
+ room_id_.toStdString());
+
+ return;
+ }
+
+ connect(cache::client(), &Cache::newReadReceipts, this, &ReadReceiptsModel::update);
+}
+
+void
+ReadReceiptsModel::update()
+{
+ try {
+ addUsers(cache::readReceipts(event_id_, room_id_));
+ } catch (const lmdb::error &) {
+ nhlog::db()->warn("failed to retrieve read receipts for {} {}",
+ event_id_.toStdString(),
+ room_id_.toStdString());
+
+ return;
+ }
+}
+
+QHash<int, QByteArray>
+ReadReceiptsModel::roleNames() const
+{
+ // Note: RawTimestamp is purposely not included here
+ return {
+ {Mxid, "mxid"},
+ {DisplayName, "displayName"},
+ {AvatarUrl, "avatarUrl"},
+ {Timestamp, "timestamp"},
+ };
+}
+
+QVariant
+ReadReceiptsModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || index.row() >= (int)readReceipts_.size() || index.row() < 0)
+ return {};
+
+ switch (role) {
+ case Mxid:
+ return readReceipts_[index.row()].first;
+ case DisplayName:
+ return cache::displayName(room_id_, readReceipts_[index.row()].first);
+ case AvatarUrl:
+ return cache::avatarUrl(room_id_, readReceipts_[index.row()].first);
+ case Timestamp:
+ return dateFormat(readReceipts_[index.row()].second);
+ case RawTimestamp:
+ return readReceipts_[index.row()].second;
+ default:
+ return {};
+ }
+}
+
+void
+ReadReceiptsModel::addUsers(
+ const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users)
+{
+ auto newReceipts = users.size() - readReceipts_.size();
+
+ if (newReceipts > 0) {
+ beginInsertRows(
+ QModelIndex{}, readReceipts_.size(), readReceipts_.size() + newReceipts - 1);
+
+ for (const auto &user : users) {
+ QPair<QString, QDateTime> item = {
+ QString::fromStdString(user.second),
+ QDateTime::fromMSecsSinceEpoch(user.first)};
+ if (!readReceipts_.contains(item))
+ readReceipts_.push_back(item);
+ }
+
+ endInsertRows();
+ }
+}
+
+QString
+ReadReceiptsModel::dateFormat(const QDateTime &then) const
+{
+ auto now = QDateTime::currentDateTime();
+ auto days = then.daysTo(now);
+
+ if (days == 0)
+ return QLocale::system().toString(then.time(), QLocale::ShortFormat);
+ else if (days < 2)
+ return tr("Yesterday, %1")
+ .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
+ else if (days < 7)
+ //: %1 is the name of the current day, %2 is the time the read receipt was read. The
+ //: result may look like this: Monday, 7:15
+ return QString("%1, %2")
+ .arg(then.toString("dddd"))
+ .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
+
+ return QLocale::system().toString(then.time(), QLocale::ShortFormat);
+}
+
+ReadReceiptsProxy::ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent)
+ : QSortFilterProxyModel{parent}
+ , model_{event_id, room_id, this}
+{
+ setSourceModel(&model_);
+ setSortRole(ReadReceiptsModel::RawTimestamp);
+ sort(0, Qt::DescendingOrder);
+ setDynamicSortFilter(true);
+}
diff --git a/src/ReadReceiptsModel.h b/src/ReadReceiptsModel.h
new file mode 100644
index 00000000..3b45716c
--- /dev/null
+++ b/src/ReadReceiptsModel.h
@@ -0,0 +1,73 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef READRECEIPTSMODEL_H
+#define READRECEIPTSMODEL_H
+
+#include <QAbstractListModel>
+#include <QDateTime>
+#include <QObject>
+#include <QSortFilterProxyModel>
+#include <QString>
+
+class ReadReceiptsModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ enum Roles
+ {
+ Mxid,
+ DisplayName,
+ AvatarUrl,
+ Timestamp,
+ RawTimestamp,
+ };
+
+ explicit ReadReceiptsModel(QString event_id, QString room_id, QObject *parent = nullptr);
+
+ QString eventId() const { return event_id_; }
+ QString roomId() const { return room_id_; }
+
+ QHash<int, QByteArray> roleNames() const override;
+ int rowCount(const QModelIndex &parent) const override
+ {
+ Q_UNUSED(parent)
+ return readReceipts_.size();
+ }
+ QVariant data(const QModelIndex &index, int role) const override;
+
+public slots:
+ void addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users);
+ void update();
+
+private:
+ QString dateFormat(const QDateTime &then) const;
+
+ QString event_id_;
+ QString room_id_;
+ QVector<QPair<QString, QDateTime>> readReceipts_;
+};
+
+class ReadReceiptsProxy : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QString eventId READ eventId CONSTANT)
+ Q_PROPERTY(QString roomId READ roomId CONSTANT)
+
+public:
+ explicit ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent = nullptr);
+
+ QString eventId() const { return event_id_; }
+ QString roomId() const { return room_id_; }
+
+private:
+ QString event_id_;
+ QString room_id_;
+
+ ReadReceiptsModel model_;
+};
+
+#endif // READRECEIPTSMODEL_H
diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp
index 1588d07d..bae24df0 100644
--- a/src/RegisterPage.cpp
+++ b/src/RegisterPage.cpp
@@ -12,6 +12,7 @@
#include <mtx/responses/register.hpp>
#include <mtx/responses/well-known.hpp>
+#include <mtxclient/http/client.hpp>
#include "Config.h"
#include "Logging.h"
@@ -93,6 +94,7 @@ RegisterPage::RegisterPage(QWidget *parent)
server_input_ = new TextField();
server_input_->setLabel(tr("Homeserver"));
+ server_input_->setRegexp(QRegularExpression(".+"));
server_input_->setToolTip(
tr("A server that allows registration. Since matrix is decentralized, you need to first "
"find a server you can register on or host your own."));
@@ -145,178 +147,39 @@ RegisterPage::RegisterPage(QWidget *parent)
top_layout_->addLayout(button_layout_);
top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter);
top_layout_->addStretch(1);
-
- connect(
- this,
- &RegisterPage::versionErrorCb,
- this,
- [this](const QString &msg) {
- error_server_label_->show();
- server_input_->setValid(false);
- showError(error_server_label_, msg);
- },
- Qt::QueuedConnection);
+ setLayout(top_layout_);
connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked()));
connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
- connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkFields);
+ connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkUsername);
connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
- connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkFields);
+ connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkPassword);
connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
- connect(
- password_confirmation_, &TextField::editingFinished, this, &RegisterPage::checkFields);
+ connect(password_confirmation_,
+ &TextField::editingFinished,
+ this,
+ &RegisterPage::checkPasswordConfirmation);
connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
- connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkFields);
- connect(this, &RegisterPage::registerErrorCb, this, [this](const QString &msg) {
- showError(msg);
- });
- connect(
- this,
- &RegisterPage::registrationFlow,
- this,
- [this](const std::string &user,
- const std::string &pass,
- const mtx::user_interactive::Unauthorized &unauthorized) {
- auto completed_stages = unauthorized.completed;
- auto flows = unauthorized.flows;
- auto session = unauthorized.session.empty() ? http::client()->generate_txn_id()
- : unauthorized.session;
-
- nhlog::ui()->info("Completed stages: {}", completed_stages.size());
-
- if (!completed_stages.empty())
- flows.erase(std::remove_if(
- flows.begin(),
- flows.end(),
- [completed_stages](auto flow) {
- if (completed_stages.size() > flow.stages.size())
- return true;
- for (size_t f = 0; f < completed_stages.size(); f++)
- if (completed_stages[f] != flow.stages[f])
- return true;
- return false;
- }),
- flows.end());
-
- if (flows.empty()) {
- nhlog::net()->error("No available registration flows!");
- emit registerErrorCb(tr("No supported registration flows!"));
- return;
- }
-
- auto current_stage = flows.front().stages.at(completed_stages.size());
-
- if (current_stage == mtx::user_interactive::auth_types::recaptcha) {
- auto captchaDialog =
- new dialogs::ReCaptcha(QString::fromStdString(session), this);
-
- connect(captchaDialog,
- &dialogs::ReCaptcha::confirmation,
- this,
- [this, user, pass, session, captchaDialog]() {
- captchaDialog->close();
- captchaDialog->deleteLater();
-
- emit registerAuth(
- user,
- pass,
- mtx::user_interactive::Auth{
- session, mtx::user_interactive::auth::Fallback{}});
- });
- connect(captchaDialog,
- &dialogs::ReCaptcha::cancel,
- this,
- &RegisterPage::errorOccurred);
-
- QTimer::singleShot(
- 1000, this, [captchaDialog]() { captchaDialog->show(); });
- } else if (current_stage == mtx::user_interactive::auth_types::dummy) {
- emit registerAuth(user,
- pass,
- mtx::user_interactive::Auth{
- session, mtx::user_interactive::auth::Dummy{}});
- } else {
- // use fallback
- auto dialog =
- new dialogs::FallbackAuth(QString::fromStdString(current_stage),
- QString::fromStdString(session),
- this);
-
- connect(dialog,
- &dialogs::FallbackAuth::confirmation,
- this,
- [this, user, pass, session, dialog]() {
- dialog->close();
- dialog->deleteLater();
-
- emit registerAuth(
- user,
- pass,
- mtx::user_interactive::Auth{
- session, mtx::user_interactive::auth::Fallback{}});
- });
- connect(dialog,
- &dialogs::FallbackAuth::cancel,
- this,
- &RegisterPage::errorOccurred);
-
- dialog->show();
- }
- });
+ connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkServer);
connect(
this,
- &RegisterPage::registerAuth,
+ &RegisterPage::serverError,
this,
- [this](const std::string &user,
- const std::string &pass,
- const mtx::user_interactive::Auth &auth) {
- http::client()->registration(
- user,
- pass,
- auth,
- [this, user, pass](const mtx::responses::Register &res,
- mtx::http::RequestErr err) {
- if (!err) {
- http::client()->set_user(res.user_id);
- http::client()->set_access_token(res.access_token);
- http::client()->set_device_id(res.device_id);
-
- emit registerOk();
- return;
- }
-
- // The server requires registration flows.
- if (err->status_code == 401) {
- if (err->matrix_error.unauthorized.flows.empty()) {
- nhlog::net()->warn(
- "failed to retrieve registration flows: ({}) "
- "{}",
- static_cast<int>(err->status_code),
- err->matrix_error.error);
- emit registerErrorCb(
- QString::fromStdString(err->matrix_error.error));
- return;
- }
-
- emit registrationFlow(
- user, pass, err->matrix_error.unauthorized);
- return;
- }
-
- nhlog::net()->warn("failed to register: status_code ({}), "
- "matrix_error: ({}), parser error ({})",
- static_cast<int>(err->status_code),
- err->matrix_error.error,
- err->parse_error);
-
- emit registerErrorCb(QString::fromStdString(err->matrix_error.error));
- });
- });
+ [this](const QString &msg) {
+ server_input_->setValid(false);
+ showError(error_server_label_, msg);
+ },
+ Qt::QueuedConnection);
- setLayout(top_layout_);
+ connect(this, &RegisterPage::wellKnownLookup, this, &RegisterPage::doWellKnownLookup);
+ connect(this, &RegisterPage::versionsCheck, this, &RegisterPage::doVersionsCheck);
+ connect(this, &RegisterPage::registration, this, &RegisterPage::doRegistration);
+ connect(this, &RegisterPage::UIA, this, &RegisterPage::doUIA);
+ connect(
+ this, &RegisterPage::registrationWithAuth, this, &RegisterPage::doRegistrationWithAuth);
}
void
@@ -345,189 +208,296 @@ RegisterPage::showError(QLabel *label, const QString &msg)
int height = rect.height();
label->setFixedHeight((int)qCeil(width / 200.0) * height);
label->setText(msg);
+ label->show();
}
bool
RegisterPage::checkOneField(QLabel *label, const TextField *t_field, const QString &msg)
{
if (t_field->isValid()) {
- label->setText("");
label->hide();
return true;
} else {
- label->show();
showError(label, msg);
return false;
}
}
bool
-RegisterPage::checkFields()
+RegisterPage::checkUsername()
{
- error_label_->setText("");
- error_username_label_->setText("");
- error_password_label_->setText("");
- error_password_confirmation_label_->setText("");
- error_server_label_->setText("");
-
- error_username_label_->hide();
- error_password_label_->hide();
- error_password_confirmation_label_->hide();
- error_server_label_->hide();
+ return checkOneField(error_username_label_,
+ username_input_,
+ tr("The username must not be empty, and must contain only the "
+ "characters a-z, 0-9, ., _, =, -, and /."));
+}
- password_confirmation_->setValid(true);
- server_input_->setValid(true);
+bool
+RegisterPage::checkPassword()
+{
+ return checkOneField(
+ error_password_label_, password_input_, tr("Password is not long enough (min 8 chars)"));
+}
- bool all_fields_good = true;
- if (username_input_->isModified() &&
- !checkOneField(error_username_label_,
- username_input_,
- tr("The username must not be empty, and must contain only the "
- "characters a-z, 0-9, ., _, =, -, and /."))) {
- all_fields_good = false;
- } else if (password_input_->isModified() &&
- !checkOneField(error_password_label_,
- password_input_,
- tr("Password is not long enough (min 8 chars)"))) {
- all_fields_good = false;
- } else if (password_confirmation_->isModified() &&
- password_input_->text() != password_confirmation_->text()) {
- error_password_confirmation_label_->show();
+bool
+RegisterPage::checkPasswordConfirmation()
+{
+ if (password_input_->text() == password_confirmation_->text()) {
+ error_password_confirmation_label_->hide();
+ password_confirmation_->setValid(true);
+ return true;
+ } else {
showError(error_password_confirmation_label_, tr("Passwords don't match"));
password_confirmation_->setValid(false);
- all_fields_good = false;
- } else if (server_input_->isModified() &&
- (!server_input_->hasAcceptableInput() || server_input_->text().isEmpty())) {
- error_server_label_->show();
- showError(error_server_label_, tr("Invalid server name"));
- server_input_->setValid(false);
- all_fields_good = false;
- }
- if (!username_input_->isModified() || !password_input_->isModified() ||
- !password_confirmation_->isModified() || !server_input_->isModified()) {
- all_fields_good = false;
+ return false;
}
- return all_fields_good;
+}
+
+bool
+RegisterPage::checkServer()
+{
+ // This doesn't check that the server is reachable,
+ // just that the input is not obviously wrong.
+ return checkOneField(error_server_label_, server_input_, tr("Invalid server name"));
}
void
RegisterPage::onRegisterButtonClicked()
{
- if (!checkFields()) {
- showError(error_label_,
- tr("One or more fields have invalid inputs. Please correct those issues "
- "and try again."));
- return;
- } else {
- auto username = username_input_->text().toStdString();
- auto password = password_input_->text().toStdString();
- auto server = server_input_->text().toStdString();
+ if (checkUsername() && checkPassword() && checkPasswordConfirmation() && checkServer()) {
+ auto server = server_input_->text().toStdString();
http::client()->set_server(server);
http::client()->verify_certificates(
!UserSettings::instance()->disableCertificateValidation());
- http::client()->well_known(
- [this, username, password](const mtx::responses::WellKnown &res,
- mtx::http::RequestErr err) {
- if (err) {
- if (err->status_code == 404) {
- nhlog::net()->info("Autodiscovery: No .well-known.");
- checkVersionAndRegister(username, password);
- return;
- }
+ // This starts a chain of `emit`s which ends up doing the
+ // registration. Signals are used rather than normal function
+ // calls so that the dialogs used in UIA work correctly.
+ //
+ // The sequence of events looks something like this:
+ //
+ // dowellKnownLookup
+ // v
+ // doVersionsCheck
+ // v
+ // doRegistration
+ // v
+ // doUIA <-----------------+
+ // v | More auth required
+ // doRegistrationWithAuth -+
+ // | Success
+ // v
+ // registering
+
+ emit wellKnownLookup();
- if (!err->parse_error.empty()) {
- emit versionErrorCb(tr(
- "Autodiscovery failed. Received malformed response."));
- nhlog::net()->error(
- "Autodiscovery failed. Received malformed response.");
- return;
- }
+ emit registering();
+ }
+}
- emit versionErrorCb(tr("Autodiscovery failed. Unknown error when "
- "requesting .well-known."));
- nhlog::net()->error("Autodiscovery failed. Unknown error when "
- "requesting .well-known. {} {}",
- err->status_code,
- err->error_code);
+void
+RegisterPage::doWellKnownLookup()
+{
+ http::client()->well_known(
+ [this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) {
+ if (err) {
+ if (err->status_code == 404) {
+ nhlog::net()->info("Autodiscovery: No .well-known.");
+ // Check that the homeserver can be reached
+ emit versionsCheck();
return;
}
- nhlog::net()->info("Autodiscovery: Discovered '" +
- res.homeserver.base_url + "'");
- http::client()->set_server(res.homeserver.base_url);
- checkVersionAndRegister(username, password);
- });
+ if (!err->parse_error.empty()) {
+ emit serverError(
+ tr("Autodiscovery failed. Received malformed response."));
+ nhlog::net()->error(
+ "Autodiscovery failed. Received malformed response.");
+ return;
+ }
- emit registering();
- }
+ emit serverError(tr("Autodiscovery failed. Unknown error when "
+ "requesting .well-known."));
+ nhlog::net()->error("Autodiscovery failed. Unknown error when "
+ "requesting .well-known. {} {}",
+ err->status_code,
+ err->error_code);
+ return;
+ }
+
+ nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'");
+ http::client()->set_server(res.homeserver.base_url);
+ // Check that the homeserver can be reached
+ emit versionsCheck();
+ });
}
void
-RegisterPage::checkVersionAndRegister(const std::string &username, const std::string &password)
+RegisterPage::doVersionsCheck()
{
+ // Make a request to /_matrix/client/versions to check the address
+ // given is a Matrix homeserver.
http::client()->versions(
- [this, username, password](const mtx::responses::Versions &, mtx::http::RequestErr err) {
+ [this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
if (err) {
if (err->status_code == 404) {
- emit versionErrorCb(tr("The required endpoints were not found. "
- "Possibly not a Matrix server."));
+ emit serverError(
+ tr("The required endpoints were not found. Possibly "
+ "not a Matrix server."));
return;
}
if (!err->parse_error.empty()) {
- emit versionErrorCb(tr("Received malformed response. Make sure "
- "the homeserver domain is valid."));
+ emit serverError(
+ tr("Received malformed response. Make sure the homeserver "
+ "domain is valid."));
return;
}
- emit versionErrorCb(tr(
- "An unknown error occured. Make sure the homeserver domain is valid."));
+ emit serverError(tr("An unknown error occured. Make sure the "
+ "homeserver domain is valid."));
return;
}
- http::client()->registration(
- username,
- password,
- [this, username, password](const mtx::responses::Register &res,
- mtx::http::RequestErr err) {
- if (!err) {
- http::client()->set_user(res.user_id);
- http::client()->set_access_token(res.access_token);
+ // Attempt registration without an `auth` dict
+ emit registration();
+ });
+}
+
+void
+RegisterPage::doRegistration()
+{
+ // These inputs should still be alright, but check just in case
+ if (checkUsername() && checkPassword() && checkPasswordConfirmation()) {
+ auto username = username_input_->text().toStdString();
+ auto password = password_input_->text().toStdString();
+ http::client()->registration(username, password, registrationCb());
+ }
+}
+
+void
+RegisterPage::doRegistrationWithAuth(const mtx::user_interactive::Auth &auth)
+{
+ // These inputs should still be alright, but check just in case
+ if (checkUsername() && checkPassword() && checkPasswordConfirmation()) {
+ auto username = username_input_->text().toStdString();
+ auto password = password_input_->text().toStdString();
+ http::client()->registration(username, password, auth, registrationCb());
+ }
+}
- emit registerOk();
- return;
- }
+mtx::http::Callback<mtx::responses::Register>
+RegisterPage::registrationCb()
+{
+ // Return a function to be used as the callback when an attempt at
+ // registration is made.
+ return [this](const mtx::responses::Register &res, mtx::http::RequestErr err) {
+ if (!err) {
+ http::client()->set_user(res.user_id);
+ http::client()->set_access_token(res.access_token);
+ emit registerOk();
+ return;
+ }
- // The server requires registration flows.
- if (err->status_code == 401) {
- if (err->matrix_error.unauthorized.flows.empty()) {
- nhlog::net()->warn(
- "failed to retrieve registration flows1: ({}) "
- "{}",
- static_cast<int>(err->status_code),
- err->matrix_error.error);
- emit errorOccurred();
- emit registerErrorCb(
- QString::fromStdString(err->matrix_error.error));
- return;
- }
+ // The server requires registration flows.
+ if (err->status_code == 401) {
+ if (err->matrix_error.unauthorized.flows.empty()) {
+ nhlog::net()->warn("failed to retrieve registration flows: "
+ "status_code({}), matrix_error({}) ",
+ static_cast<int>(err->status_code),
+ err->matrix_error.error);
+ showError(QString::fromStdString(err->matrix_error.error));
+ return;
+ }
- emit registrationFlow(
- username, password, err->matrix_error.unauthorized);
- return;
- }
+ // Attempt to complete a UIA stage
+ emit UIA(err->matrix_error.unauthorized);
+ return;
+ }
- nhlog::net()->error(
- "failed to register: status_code ({}), matrix_error({})",
- static_cast<int>(err->status_code),
- err->matrix_error.error);
+ nhlog::net()->error("failed to register: status_code ({}), matrix_error({})",
+ static_cast<int>(err->status_code),
+ err->matrix_error.error);
- emit registerErrorCb(QString::fromStdString(err->matrix_error.error));
- emit errorOccurred();
- });
- });
+ showError(QString::fromStdString(err->matrix_error.error));
+ };
+}
+
+void
+RegisterPage::doUIA(const mtx::user_interactive::Unauthorized &unauthorized)
+{
+ auto completed_stages = unauthorized.completed;
+ auto flows = unauthorized.flows;
+ auto session =
+ unauthorized.session.empty() ? http::client()->generate_txn_id() : unauthorized.session;
+
+ nhlog::ui()->info("Completed stages: {}", completed_stages.size());
+
+ if (!completed_stages.empty()) {
+ // Get rid of all flows which don't start with the sequence of
+ // stages that have already been completed.
+ flows.erase(
+ std::remove_if(flows.begin(),
+ flows.end(),
+ [completed_stages](auto flow) {
+ if (completed_stages.size() > flow.stages.size())
+ return true;
+ for (size_t f = 0; f < completed_stages.size(); f++)
+ if (completed_stages[f] != flow.stages[f])
+ return true;
+ return false;
+ }),
+ flows.end());
+ }
+
+ if (flows.empty()) {
+ nhlog::ui()->error("No available registration flows!");
+ showError(tr("No supported registration flows!"));
+ return;
+ }
+
+ auto current_stage = flows.front().stages.at(completed_stages.size());
+
+ if (current_stage == mtx::user_interactive::auth_types::recaptcha) {
+ auto captchaDialog = new dialogs::ReCaptcha(QString::fromStdString(session), this);
+
+ connect(captchaDialog,
+ &dialogs::ReCaptcha::confirmation,
+ this,
+ [this, session, captchaDialog]() {
+ captchaDialog->close();
+ captchaDialog->deleteLater();
+ doRegistrationWithAuth(mtx::user_interactive::Auth{
+ session, mtx::user_interactive::auth::Fallback{}});
+ });
+
+ connect(
+ captchaDialog, &dialogs::ReCaptcha::cancel, this, &RegisterPage::errorOccurred);
+
+ QTimer::singleShot(1000, this, [captchaDialog]() { captchaDialog->show(); });
+
+ } else if (current_stage == mtx::user_interactive::auth_types::dummy) {
+ doRegistrationWithAuth(
+ mtx::user_interactive::Auth{session, mtx::user_interactive::auth::Dummy{}});
+
+ } else {
+ // use fallback
+ auto dialog = new dialogs::FallbackAuth(
+ QString::fromStdString(current_stage), QString::fromStdString(session), this);
+
+ connect(
+ dialog, &dialogs::FallbackAuth::confirmation, this, [this, session, dialog]() {
+ dialog->close();
+ dialog->deleteLater();
+ emit registrationWithAuth(mtx::user_interactive::Auth{
+ session, mtx::user_interactive::auth::Fallback{}});
+ });
+
+ connect(dialog, &dialogs::FallbackAuth::cancel, this, &RegisterPage::errorOccurred);
+
+ dialog->show();
+ }
}
void
diff --git a/src/RegisterPage.h b/src/RegisterPage.h
index 0e4a45d0..42ea00cb 100644
--- a/src/RegisterPage.h
+++ b/src/RegisterPage.h
@@ -10,6 +10,7 @@
#include <memory>
#include <mtx/user_interactive.hpp>
+#include <mtxclient/http/client.hpp>
class FlatButton;
class RaisedButton;
@@ -33,17 +34,16 @@ signals:
void errorOccurred();
//! Used to trigger the corresponding slot outside of the main thread.
- void versionErrorCb(const QString &err);
+ void serverError(const QString &err);
+
+ void wellKnownLookup();
+ void versionsCheck();
+ void registration();
+ void UIA(const mtx::user_interactive::Unauthorized &unauthorized);
+ void registrationWithAuth(const mtx::user_interactive::Auth &auth);
void registering();
void registerOk();
- void registerErrorCb(const QString &msg);
- void registrationFlow(const std::string &user,
- const std::string &pass,
- const mtx::user_interactive::Unauthorized &unauthorized);
- void registerAuth(const std::string &user,
- const std::string &pass,
- const mtx::user_interactive::Auth &auth);
private slots:
void onBackButtonClicked();
@@ -51,12 +51,22 @@ private slots:
// function for showing different errors
void showError(const QString &msg);
+ void showError(QLabel *label, const QString &msg);
-private:
bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg);
- bool checkFields();
- void showError(QLabel *label, const QString &msg);
- void checkVersionAndRegister(const std::string &username, const std::string &password);
+ bool checkUsername();
+ bool checkPassword();
+ bool checkPasswordConfirmation();
+ bool checkServer();
+
+ void doWellKnownLookup();
+ void doVersionsCheck();
+ void doRegistration();
+ void doUIA(const mtx::user_interactive::Unauthorized &unauthorized);
+ void doRegistrationWithAuth(const mtx::user_interactive::Auth &auth);
+ mtx::http::Callback<mtx::responses::Register> registrationCb();
+
+private:
QVBoxLayout *top_layout_;
QHBoxLayout *back_layout_;
@@ -69,6 +79,7 @@ private:
QLabel *error_password_label_;
QLabel *error_password_confirmation_label_;
QLabel *error_server_label_;
+ QLabel *error_registration_token_label_;
FlatButton *back_button_;
RaisedButton *register_button_;
@@ -81,4 +92,5 @@ private:
TextField *password_input_;
TextField *password_confirmation_;
TextField *server_input_;
+ TextField *registration_token_input_;
};
diff --git a/src/SingleImagePackModel.cpp b/src/SingleImagePackModel.cpp
new file mode 100644
index 00000000..7bf55617
--- /dev/null
+++ b/src/SingleImagePackModel.cpp
@@ -0,0 +1,350 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "SingleImagePackModel.h"
+
+#include <QFile>
+#include <QMimeDatabase>
+
+#include "Cache_p.h"
+#include "ChatPage.h"
+#include "Logging.h"
+#include "MatrixClient.h"
+#include "Utils.h"
+#include "timeline/Permissions.h"
+#include "timeline/TimelineModel.h"
+
+Q_DECLARE_METATYPE(mtx::common::ImageInfo)
+
+SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent)
+ : QAbstractListModel(parent)
+ , roomid_(std::move(pack_.source_room))
+ , statekey_(std::move(pack_.state_key))
+ , old_statekey_(statekey_)
+ , pack(std::move(pack_.pack))
+{
+ [[maybe_unused]] static auto imageInfoType = qRegisterMetaType<mtx::common::ImageInfo>();
+
+ if (!pack.pack)
+ pack.pack = mtx::events::msc2545::ImagePack::PackDescription{};
+
+ for (const auto &e : pack.images)
+ shortcodes.push_back(e.first);
+
+ connect(this, &SingleImagePackModel::addImage, this, &SingleImagePackModel::addImageCb);
+}
+
+int
+SingleImagePackModel::rowCount(const QModelIndex &) const
+{
+ return (int)shortcodes.size();
+}
+
+QHash<int, QByteArray>
+SingleImagePackModel::roleNames() const
+{
+ return {
+ {Roles::Url, "url"},
+ {Roles::ShortCode, "shortCode"},
+ {Roles::Body, "body"},
+ {Roles::IsEmote, "isEmote"},
+ {Roles::IsSticker, "isSticker"},
+ };
+}
+
+QVariant
+SingleImagePackModel::data(const QModelIndex &index, int role) const
+{
+ if (hasIndex(index.row(), index.column(), index.parent())) {
+ const auto &img = pack.images.at(shortcodes.at(index.row()));
+ switch (role) {
+ case Url:
+ return QString::fromStdString(img.url);
+ case ShortCode:
+ return QString::fromStdString(shortcodes.at(index.row()));
+ case Body:
+ return QString::fromStdString(img.body);
+ case IsEmote:
+ return img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji();
+ case IsSticker:
+ return img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker();
+ default:
+ return {};
+ }
+ }
+ return {};
+}
+
+bool
+SingleImagePackModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ using mtx::events::msc2545::PackUsage;
+
+ if (hasIndex(index.row(), index.column(), index.parent())) {
+ auto &img = pack.images.at(shortcodes.at(index.row()));
+ switch (role) {
+ case ShortCode: {
+ auto newCode = value.toString().toStdString();
+
+ // otherwise we delete this by accident
+ if (pack.images.count(newCode))
+ return false;
+
+ auto tmp = img;
+ auto oldCode = shortcodes.at(index.row());
+ pack.images.erase(oldCode);
+ shortcodes[index.row()] = newCode;
+ pack.images.insert({newCode, tmp});
+
+ emit dataChanged(
+ this->index(index.row()), this->index(index.row()), {Roles::ShortCode});
+ return true;
+ }
+ case Body:
+ img.body = value.toString().toStdString();
+ emit dataChanged(
+ this->index(index.row()), this->index(index.row()), {Roles::Body});
+ return true;
+ case IsEmote: {
+ bool isEmote = value.toBool();
+ bool isSticker =
+ img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker();
+
+ img.usage.set(PackUsage::Emoji, isEmote);
+ img.usage.set(PackUsage::Sticker, isSticker);
+
+ if (img.usage == pack.pack->usage)
+ img.usage.reset();
+
+ emit dataChanged(
+ this->index(index.row()), this->index(index.row()), {Roles::IsEmote});
+
+ return true;
+ }
+ case IsSticker: {
+ bool isEmote =
+ img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji();
+ bool isSticker = value.toBool();
+
+ img.usage.set(PackUsage::Emoji, isEmote);
+ img.usage.set(PackUsage::Sticker, isSticker);
+
+ if (img.usage == pack.pack->usage)
+ img.usage.reset();
+
+ emit dataChanged(
+ this->index(index.row()), this->index(index.row()), {Roles::IsSticker});
+
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+bool
+SingleImagePackModel::isGloballyEnabled() const
+{
+ if (auto roomPacks =
+ cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) {
+ if (auto tmp = std::get_if<
+ mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>(
+ &*roomPacks)) {
+ if (tmp->content.rooms.count(roomid_) &&
+ tmp->content.rooms.at(roomid_).count(statekey_))
+ return true;
+ }
+ }
+ return false;
+}
+void
+SingleImagePackModel::setGloballyEnabled(bool enabled)
+{
+ mtx::events::msc2545::ImagePackRooms content{};
+ if (auto roomPacks =
+ cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) {
+ if (auto tmp = std::get_if<
+ mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>(
+ &*roomPacks)) {
+ content = tmp->content;
+ }
+ }
+
+ if (enabled)
+ content.rooms[roomid_][statekey_] = {};
+ else
+ content.rooms[roomid_].erase(statekey_);
+
+ http::client()->put_account_data(content, [](mtx::http::RequestErr) {
+ // emit this->globallyEnabledChanged();
+ });
+}
+
+bool
+SingleImagePackModel::canEdit() const
+{
+ if (roomid_.empty())
+ return true;
+ else
+ return Permissions(QString::fromStdString(roomid_))
+ .canChange(qml_mtx_events::ImagePackInRoom);
+}
+
+void
+SingleImagePackModel::setPackname(QString val)
+{
+ auto val_ = val.toStdString();
+ if (val_ != this->pack.pack->display_name) {
+ this->pack.pack->display_name = val_;
+ emit packnameChanged();
+ }
+}
+
+void
+SingleImagePackModel::setAttribution(QString val)
+{
+ auto val_ = val.toStdString();
+ if (val_ != this->pack.pack->attribution) {
+ this->pack.pack->attribution = val_;
+ emit attributionChanged();
+ }
+}
+
+void
+SingleImagePackModel::setAvatarUrl(QString val)
+{
+ auto val_ = val.toStdString();
+ if (val_ != this->pack.pack->avatar_url) {
+ this->pack.pack->avatar_url = val_;
+ emit avatarUrlChanged();
+ }
+}
+
+void
+SingleImagePackModel::setStatekey(QString val)
+{
+ auto val_ = val.toStdString();
+ if (val_ != statekey_) {
+ statekey_ = val_;
+ emit statekeyChanged();
+ }
+}
+
+void
+SingleImagePackModel::setIsStickerPack(bool val)
+{
+ using mtx::events::msc2545::PackUsage;
+ if (val != pack.pack->is_sticker()) {
+ pack.pack->usage.set(PackUsage::Sticker, val);
+ emit isStickerPackChanged();
+ }
+}
+
+void
+SingleImagePackModel::setIsEmotePack(bool val)
+{
+ using mtx::events::msc2545::PackUsage;
+ if (val != pack.pack->is_emoji()) {
+ pack.pack->usage.set(PackUsage::Emoji, val);
+ emit isEmotePackChanged();
+ }
+}
+
+void
+SingleImagePackModel::save()
+{
+ if (roomid_.empty()) {
+ http::client()->put_account_data(pack, [](mtx::http::RequestErr e) {
+ if (e)
+ ChatPage::instance()->showNotification(
+ tr("Failed to update image pack: {}")
+ .arg(QString::fromStdString(e->matrix_error.error)));
+ });
+ } else {
+ if (old_statekey_ != statekey_) {
+ http::client()->send_state_event(
+ roomid_,
+ to_string(mtx::events::EventType::ImagePackInRoom),
+ old_statekey_,
+ nlohmann::json::object(),
+ [](const mtx::responses::EventId &, mtx::http::RequestErr e) {
+ if (e)
+ ChatPage::instance()->showNotification(
+ tr("Failed to delete old image pack: {}")
+ .arg(QString::fromStdString(e->matrix_error.error)));
+ });
+ }
+
+ http::client()->send_state_event(
+ roomid_,
+ statekey_,
+ pack,
+ [this](const mtx::responses::EventId &, mtx::http::RequestErr e) {
+ if (e)
+ ChatPage::instance()->showNotification(
+ tr("Failed to update image pack: {}")
+ .arg(QString::fromStdString(e->matrix_error.error)));
+
+ nhlog::net()->info("Uploaded image pack: {}", statekey_);
+ });
+ }
+}
+
+void
+SingleImagePackModel::addStickers(QList<QUrl> files)
+{
+ for (const auto &f : files) {
+ auto file = QFile(f.toLocalFile());
+ if (!file.open(QFile::ReadOnly)) {
+ ChatPage::instance()->showNotification(
+ tr("Failed to open image: {}").arg(f.toLocalFile()));
+ return;
+ }
+
+ auto bytes = file.readAll();
+ auto img = utils::readImage(bytes);
+
+ mtx::common::ImageInfo info{};
+
+ auto sz = img.size() / 2;
+ if (sz.width() > 512 || sz.height() > 512) {
+ sz.scale(512, 512, Qt::AspectRatioMode::KeepAspectRatio);
+ }
+
+ info.h = sz.height();
+ info.w = sz.width();
+ info.size = bytes.size();
+
+ auto filename = f.fileName().toStdString();
+ http::client()->upload(
+ bytes.toStdString(),
+ QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(),
+ filename,
+ [this, filename, info](const mtx::responses::ContentURI &uri,
+ mtx::http::RequestErr e) {
+ if (e) {
+ ChatPage::instance()->showNotification(
+ tr("Failed to upload image: {}")
+ .arg(QString::fromStdString(e->matrix_error.error)));
+ return;
+ }
+
+ emit addImage(uri.content_uri, filename, info);
+ });
+ }
+}
+void
+SingleImagePackModel::addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info)
+{
+ mtx::events::msc2545::PackImage img{};
+ img.url = uri;
+ img.info = info;
+ beginInsertRows(
+ QModelIndex(), static_cast<int>(shortcodes.size()), static_cast<int>(shortcodes.size()));
+
+ pack.images[filename] = img;
+ shortcodes.push_back(filename);
+
+ endInsertRows();
+}
diff --git a/src/SingleImagePackModel.h b/src/SingleImagePackModel.h
new file mode 100644
index 00000000..cd38b3b6
--- /dev/null
+++ b/src/SingleImagePackModel.h
@@ -0,0 +1,93 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QAbstractListModel>
+#include <QList>
+#include <QUrl>
+
+#include <mtx/events/mscs/image_packs.hpp>
+
+#include "CacheStructs.h"
+
+class SingleImagePackModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QString roomid READ roomid CONSTANT)
+ Q_PROPERTY(QString statekey READ statekey WRITE setStatekey NOTIFY statekeyChanged)
+ Q_PROPERTY(
+ QString attribution READ attribution WRITE setAttribution NOTIFY attributionChanged)
+ Q_PROPERTY(QString packname READ packname WRITE setPackname NOTIFY packnameChanged)
+ Q_PROPERTY(QString avatarUrl READ avatarUrl WRITE setAvatarUrl NOTIFY avatarUrlChanged)
+ Q_PROPERTY(
+ bool isStickerPack READ isStickerPack WRITE setIsStickerPack NOTIFY isStickerPackChanged)
+ Q_PROPERTY(bool isEmotePack READ isEmotePack WRITE setIsEmotePack NOTIFY isEmotePackChanged)
+ Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY
+ globallyEnabledChanged)
+ Q_PROPERTY(bool canEdit READ canEdit CONSTANT)
+
+public:
+ enum Roles
+ {
+ Url = Qt::UserRole,
+ ShortCode,
+ Body,
+ IsEmote,
+ IsSticker,
+ };
+ Q_ENUM(Roles);
+
+ SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr);
+ QHash<int, QByteArray> roleNames() const override;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ bool setData(const QModelIndex &index,
+ const QVariant &value,
+ int role = Qt::EditRole) override;
+
+ QString roomid() const { return QString::fromStdString(roomid_); }
+ QString statekey() const { return QString::fromStdString(statekey_); }
+ QString packname() const { return QString::fromStdString(pack.pack->display_name); }
+ QString attribution() const { return QString::fromStdString(pack.pack->attribution); }
+ QString avatarUrl() const { return QString::fromStdString(pack.pack->avatar_url); }
+ bool isStickerPack() const { return pack.pack->is_sticker(); }
+ bool isEmotePack() const { return pack.pack->is_emoji(); }
+
+ bool isGloballyEnabled() const;
+ bool canEdit() const;
+ void setGloballyEnabled(bool enabled);
+
+ void setPackname(QString val);
+ void setAttribution(QString val);
+ void setAvatarUrl(QString val);
+ void setStatekey(QString val);
+ void setIsStickerPack(bool val);
+ void setIsEmotePack(bool val);
+
+ Q_INVOKABLE void save();
+ Q_INVOKABLE void addStickers(QList<QUrl> files);
+
+signals:
+ void globallyEnabledChanged();
+ void statekeyChanged();
+ void attributionChanged();
+ void packnameChanged();
+ void avatarUrlChanged();
+ void isEmotePackChanged();
+ void isStickerPackChanged();
+
+ void addImage(std::string uri, std::string filename, mtx::common::ImageInfo info);
+
+private slots:
+ void addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info);
+
+private:
+ std::string roomid_;
+ std::string statekey_, old_statekey_;
+
+ mtx::events::msc2545::ImagePack pack;
+ std::vector<std::string> shortcodes;
+};
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index ffaebe61..ab6ac492 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -90,13 +90,11 @@ UserSettings::load(std::optional<QString> profile)
decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool();
privacyScreen_ = settings.value("user/privacy_screen", false).toBool();
privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt();
- shareKeysWithTrustedUsers_ =
- settings.value("user/automatically_share_keys_with_trusted_users", false).toBool();
- mobileMode_ = settings.value("user/mobile_mode", false).toBool();
- emojiFont_ = settings.value("user/emoji_font_family", "default").toString();
- baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble();
- auto tempPresence = settings.value("user/presence", "").toString().toStdString();
- auto presenceValue = QMetaEnum::fromType<Presence>().keyToValue(tempPresence.c_str());
+ mobileMode_ = settings.value("user/mobile_mode", false).toBool();
+ emojiFont_ = settings.value("user/emoji_font_family", "default").toString();
+ baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble();
+ auto tempPresence = settings.value("user/presence", "").toString().toStdString();
+ auto presenceValue = QMetaEnum::fromType<Presence>().keyToValue(tempPresence.c_str());
if (presenceValue < 0)
presenceValue = 0;
presence_ = static_cast<Presence>(presenceValue);
@@ -123,6 +121,12 @@ UserSettings::load(std::optional<QString> profile)
userId_ = settings.value(prefix + "auth/user_id", "").toString();
deviceId_ = settings.value(prefix + "auth/device_id", "").toString();
+ shareKeysWithTrustedUsers_ =
+ settings.value(prefix + "user/automatically_share_keys_with_trusted_users", false)
+ .toBool();
+ onlyShareKeysWithVerifiedUsers_ =
+ settings.value(prefix + "user/only_share_keys_with_verified_users", false).toBool();
+
disableCertificateValidation_ =
settings.value("disable_certificate_validation", false).toBool();
@@ -402,6 +406,17 @@ UserSettings::setUseStunServer(bool useStunServer)
}
void
+UserSettings::setOnlyShareKeysWithVerifiedUsers(bool shareKeys)
+{
+ if (shareKeys == onlyShareKeysWithVerifiedUsers_)
+ return;
+
+ onlyShareKeysWithVerifiedUsers_ = shareKeys;
+ emit onlyShareKeysWithVerifiedUsersChanged(shareKeys);
+ save();
+}
+
+void
UserSettings::setShareKeysWithTrustedUsers(bool shareKeys)
{
if (shareKeys == shareKeysWithTrustedUsers_)
@@ -610,8 +625,6 @@ UserSettings::save()
settings.setValue("decrypt_sidebar", decryptSidebar_);
settings.setValue("privacy_screen", privacyScreen_);
settings.setValue("privacy_screen_timeout", privacyScreenTimeout_);
- settings.setValue("automatically_share_keys_with_trusted_users",
- shareKeysWithTrustedUsers_);
settings.setValue("mobile_mode", mobileMode_);
settings.setValue("font_size", baseFontSize_);
settings.setValue("typing_notifications", typingNotifications_);
@@ -650,6 +663,11 @@ UserSettings::save()
settings.setValue(prefix + "auth/user_id", userId_);
settings.setValue(prefix + "auth/device_id", deviceId_);
+ settings.setValue(prefix + "user/automatically_share_keys_with_trusted_users",
+ shareKeysWithTrustedUsers_);
+ settings.setValue(prefix + "user/only_share_keys_with_verified_users",
+ onlyShareKeysWithVerifiedUsers_);
+
settings.setValue("disable_certificate_validation", disableCertificateValidation_);
settings.sync();
@@ -703,41 +721,43 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
general_->setFont(font);
- trayToggle_ = new Toggle{this};
- startInTrayToggle_ = new Toggle{this};
- avatarCircles_ = new Toggle{this};
- decryptSidebar_ = new Toggle(this);
- privacyScreen_ = new Toggle{this};
- shareKeysWithTrustedUsers_ = new Toggle(this);
- groupViewToggle_ = new Toggle{this};
- timelineButtonsToggle_ = new Toggle{this};
- typingNotifications_ = new Toggle{this};
- messageHoverHighlight_ = new Toggle{this};
- enlargeEmojiOnlyMessages_ = new Toggle{this};
- sortByImportance_ = new Toggle{this};
- readReceipts_ = new Toggle{this};
- markdown_ = new Toggle{this};
- desktopNotifications_ = new Toggle{this};
- alertOnNotification_ = new Toggle{this};
- useStunServer_ = new Toggle{this};
- mobileMode_ = new Toggle{this};
- scaleFactorCombo_ = new QComboBox{this};
- fontSizeCombo_ = new QComboBox{this};
- fontSelectionCombo_ = new QFontComboBox{this};
- emojiFontSelectionCombo_ = new QComboBox{this};
- ringtoneCombo_ = new QComboBox{this};
- microphoneCombo_ = new QComboBox{this};
- cameraCombo_ = new QComboBox{this};
- cameraResolutionCombo_ = new QComboBox{this};
- cameraFrameRateCombo_ = new QComboBox{this};
- timelineMaxWidthSpin_ = new QSpinBox{this};
- privacyScreenTimeout_ = new QSpinBox{this};
+ trayToggle_ = new Toggle{this};
+ startInTrayToggle_ = new Toggle{this};
+ avatarCircles_ = new Toggle{this};
+ decryptSidebar_ = new Toggle(this);
+ privacyScreen_ = new Toggle{this};
+ onlyShareKeysWithVerifiedUsers_ = new Toggle(this);
+ shareKeysWithTrustedUsers_ = new Toggle(this);
+ groupViewToggle_ = new Toggle{this};
+ timelineButtonsToggle_ = new Toggle{this};
+ typingNotifications_ = new Toggle{this};
+ messageHoverHighlight_ = new Toggle{this};
+ enlargeEmojiOnlyMessages_ = new Toggle{this};
+ sortByImportance_ = new Toggle{this};
+ readReceipts_ = new Toggle{this};
+ markdown_ = new Toggle{this};
+ desktopNotifications_ = new Toggle{this};
+ alertOnNotification_ = new Toggle{this};
+ useStunServer_ = new Toggle{this};
+ mobileMode_ = new Toggle{this};
+ scaleFactorCombo_ = new QComboBox{this};
+ fontSizeCombo_ = new QComboBox{this};
+ fontSelectionCombo_ = new QFontComboBox{this};
+ emojiFontSelectionCombo_ = new QComboBox{this};
+ ringtoneCombo_ = new QComboBox{this};
+ microphoneCombo_ = new QComboBox{this};
+ cameraCombo_ = new QComboBox{this};
+ cameraResolutionCombo_ = new QComboBox{this};
+ cameraFrameRateCombo_ = new QComboBox{this};
+ timelineMaxWidthSpin_ = new QSpinBox{this};
+ privacyScreenTimeout_ = new QSpinBox{this};
trayToggle_->setChecked(settings_->tray());
startInTrayToggle_->setChecked(settings_->startInTray());
avatarCircles_->setChecked(settings_->avatarCircles());
decryptSidebar_->setChecked(settings_->decryptSidebar());
privacyScreen_->setChecked(settings_->privacyScreen());
+ onlyShareKeysWithVerifiedUsers_->setChecked(settings_->onlyShareKeysWithVerifiedUsers());
shareKeysWithTrustedUsers_->setChecked(settings_->shareKeysWithTrustedUsers());
groupViewToggle_->setChecked(settings_->groupView());
timelineButtonsToggle_->setChecked(settings_->buttonsInTimeline());
@@ -1008,10 +1028,14 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
formLayout_->addRow(new HorizontalLine{this});
boxWrap(tr("Device ID"), deviceIdValue_);
boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_);
- boxWrap(
- tr("Share keys with verified users and devices"),
- shareKeysWithTrustedUsers_,
- tr("Automatically replies to key requests from other users, if they are verified."));
+ boxWrap(tr("Send encrypted messages to verified users only"),
+ onlyShareKeysWithVerifiedUsers_,
+ tr("Requires a user to be verified to send encrypted messages to them. This "
+ "improves safety but makes E2EE more tedious."));
+ boxWrap(tr("Share keys with verified users and devices"),
+ shareKeysWithTrustedUsers_,
+ tr("Automatically replies to key requests from other users, if they are verified, "
+ "even if that device shouldn't have access to those keys otherwise."));
formLayout_->addRow(new HorizontalLine{this});
formLayout_->addRow(sessionKeysLabel, sessionKeysLayout);
formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout);
@@ -1179,6 +1203,10 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
}
});
+ connect(onlyShareKeysWithVerifiedUsers_, &Toggle::toggled, this, [this](bool enabled) {
+ settings_->setOnlyShareKeysWithVerifiedUsers(enabled);
+ });
+
connect(shareKeysWithTrustedUsers_, &Toggle::toggled, this, [this](bool enabled) {
settings_->setShareKeysWithTrustedUsers(enabled);
});
@@ -1271,6 +1299,7 @@ UserSettingsPage::showEvent(QShowEvent *)
groupViewToggle_->setState(settings_->groupView());
decryptSidebar_->setState(settings_->decryptSidebar());
privacyScreen_->setState(settings_->privacyScreen());
+ onlyShareKeysWithVerifiedUsers_->setState(settings_->onlyShareKeysWithVerifiedUsers());
shareKeysWithTrustedUsers_->setState(settings_->shareKeysWithTrustedUsers());
avatarCircles_->setState(settings_->avatarCircles());
typingNotifications_->setState(settings_->typingNotifications());
@@ -1399,7 +1428,7 @@ UserSettingsPage::exportSessionKeys()
QString suffix("-----END MEGOLM SESSION DATA-----");
QString newline("\n");
QTextStream out(&file);
- out << prefix << newline << b64 << newline << suffix;
+ out << prefix << newline << b64 << newline << suffix << newline;
file.close();
} catch (const std::exception &e) {
QMessageBox::warning(this, tr("Error"), e.what());
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index acb08569..096aab81 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -88,6 +88,8 @@ class UserSettings : public QObject
setScreenShareHideCursor NOTIFY screenShareHideCursorChanged)
Q_PROPERTY(
bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged)
+ Q_PROPERTY(bool onlyShareKeysWithVerifiedUsers READ onlyShareKeysWithVerifiedUsers WRITE
+ setOnlyShareKeysWithVerifiedUsers NOTIFY onlyShareKeysWithVerifiedUsersChanged)
Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE
setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged)
Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged)
@@ -152,6 +154,7 @@ public:
void setScreenShareRemoteVideo(bool state);
void setScreenShareHideCursor(bool state);
void setUseStunServer(bool state);
+ void setOnlyShareKeysWithVerifiedUsers(bool state);
void setShareKeysWithTrustedUsers(bool state);
void setProfile(QString profile);
void setUserId(QString userId);
@@ -208,6 +211,7 @@ public:
bool screenShareHideCursor() const { return screenShareHideCursor_; }
bool useStunServer() const { return useStunServer_; }
bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; }
+ bool onlyShareKeysWithVerifiedUsers() const { return onlyShareKeysWithVerifiedUsers_; }
QString profile() const { return profile_; }
QString userId() const { return userId_; }
QString accessToken() const { return accessToken_; }
@@ -252,6 +256,7 @@ signals:
void screenShareRemoteVideoChanged(bool state);
void screenShareHideCursorChanged(bool state);
void useStunServerChanged(bool state);
+ void onlyShareKeysWithVerifiedUsersChanged(bool state);
void shareKeysWithTrustedUsersChanged(bool state);
void profileChanged(QString profile);
void userIdChanged(QString userId);
@@ -284,6 +289,7 @@ private:
bool privacyScreen_;
int privacyScreenTimeout_;
bool shareKeysWithTrustedUsers_;
+ bool onlyShareKeysWithVerifiedUsers_;
bool mobileMode_;
int timelineMaxWidth_;
int roomListWidth_;
@@ -372,6 +378,7 @@ private:
Toggle *privacyScreen_;
QSpinBox *privacyScreenTimeout_;
Toggle *shareKeysWithTrustedUsers_;
+ Toggle *onlyShareKeysWithVerifiedUsers_;
Toggle *mobileMode_;
QLabel *deviceFingerprintValue_;
QLabel *deviceIdValue_;
diff --git a/src/Utils.cpp b/src/Utils.cpp
index 8d5ae4a9..41013e39 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -172,32 +172,6 @@ utils::scaleFactor()
return settings.value("settings/scale_factor", -1).toFloat();
}
-bool
-utils::respondsToKeyRequests(const std::string &roomId)
-{
- return respondsToKeyRequests(QString::fromStdString(roomId));
-}
-
-bool
-utils::respondsToKeyRequests(const QString &roomId)
-{
- if (roomId.isEmpty())
- return false;
-
- QSettings settings;
- return settings.value("rooms/respond_to_key_requests/" + roomId, false).toBool();
-}
-
-void
-utils::setKeyRequestsPreference(QString roomId, bool value)
-{
- if (roomId.isEmpty())
- return;
-
- QSettings settings;
- settings.setValue("rooms/respond_to_key_requests/" + roomId, value);
-}
-
QString
utils::descriptiveTime(const QDateTime &then)
{
@@ -556,7 +530,7 @@ utils::markdownToHtml(const QString &text, bool rainbowify)
// Use colors as described here:
// https://shark.comfsm.fm/~dleeling/cis/hsl_rainbow.html
auto color =
- QColor::fromHslF((charIdx - 1.0) / textLen * (5. / 6.), 1.0, 0.5);
+ QColor::fromHslF((charIdx - 1.0) / textLen * (5. / 6.), 0.9, 0.5);
// format color for HTML
auto colorString = color.name(QColor::NameFormat::HexRgb);
// create HTML element for current char
diff --git a/src/Utils.h b/src/Utils.h
index 1d48e2c7..8f37a574 100644
--- a/src/Utils.h
+++ b/src/Utils.h
@@ -67,15 +67,6 @@ scaleFactor();
void
setScaleFactor(float factor);
-//! Whether or not we should respond to key requests for the given room.
-bool
-respondsToKeyRequests(const QString &roomId);
-bool
-respondsToKeyRequests(const std::string &roomId);
-
-void
-setKeyRequestsPreference(QString roomId, bool value);
-
//! Human friendly timestamp representation.
QString
descriptiveTime(const QDateTime &then);
diff --git a/src/dialogs/InviteUsers.cpp b/src/dialogs/InviteUsers.cpp
deleted file mode 100644
index 9dd6085f..00000000
--- a/src/dialogs/InviteUsers.cpp
+++ /dev/null
@@ -1,158 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QDebug>
-#include <QIcon>
-#include <QLabel>
-#include <QListWidget>
-#include <QListWidgetItem>
-#include <QPushButton>
-#include <QStyleOption>
-#include <QTimer>
-#include <QVBoxLayout>
-
-#include "dialogs/InviteUsers.h"
-
-#include "Config.h"
-#include "InviteeItem.h"
-#include "ui/TextField.h"
-
-#include <mtx/identifiers.hpp>
-
-using namespace dialogs;
-
-InviteUsers::InviteUsers(QWidget *parent)
- : QFrame(parent)
-{
- setAutoFillBackground(true);
- setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
- setWindowModality(Qt::WindowModal);
- setAttribute(Qt::WA_DeleteOnClose, true);
-
- setMinimumWidth(conf::window::minModalWidth);
- setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
-
- auto layout = new QVBoxLayout(this);
- layout->setSpacing(conf::modals::WIDGET_SPACING);
- layout->setMargin(conf::modals::WIDGET_MARGIN);
-
- auto buttonLayout = new QHBoxLayout();
- buttonLayout->setSpacing(0);
- buttonLayout->setMargin(0);
-
- confirmBtn_ = new QPushButton("Invite", this);
- confirmBtn_->setDefault(true);
- cancelBtn_ = new QPushButton(tr("Cancel"), this);
-
- buttonLayout->addStretch(1);
- buttonLayout->setSpacing(15);
- buttonLayout->addWidget(cancelBtn_);
- buttonLayout->addWidget(confirmBtn_);
-
- inviteeInput_ = new TextField(this);
- inviteeInput_->setLabel(tr("User ID to invite"));
-
- inviteeList_ = new QListWidget;
- inviteeList_->setFrameStyle(QFrame::NoFrame);
- inviteeList_->setSelectionMode(QAbstractItemView::NoSelection);
- inviteeList_->setAttribute(Qt::WA_MacShowFocusRect, 0);
- inviteeList_->setSpacing(5);
-
- errorLabel_ = new QLabel(this);
- errorLabel_->setAlignment(Qt::AlignCenter);
-
- layout->addWidget(inviteeInput_);
- layout->addWidget(errorLabel_);
- layout->addWidget(inviteeList_);
- layout->addLayout(buttonLayout);
-
- connect(inviteeInput_, &TextField::returnPressed, this, &InviteUsers::addUser);
- connect(confirmBtn_, &QPushButton::clicked, [this]() {
- if (!inviteeInput_->text().trimmed().isEmpty()) {
- addUser();
- }
-
- emit sendInvites(invitedUsers());
-
- inviteeInput_->clear();
- inviteeList_->clear();
- errorLabel_->hide();
-
- emit close();
- });
-
- connect(cancelBtn_, &QPushButton::clicked, [this]() {
- inviteeInput_->clear();
- inviteeList_->clear();
- errorLabel_->hide();
-
- emit close();
- });
-}
-
-void
-InviteUsers::addUser()
-{
- auto user_id = inviteeInput_->text();
-
- try {
- namespace ids = mtx::identifiers;
- auto user = ids::parse<ids::User>(user_id.toStdString());
-
- auto item = new QListWidgetItem(inviteeList_);
- auto invitee = new InviteeItem(user, this);
-
- item->setSizeHint(invitee->minimumSizeHint());
- item->setFlags(Qt::NoItemFlags);
- item->setTextAlignment(Qt::AlignCenter);
-
- inviteeList_->setItemWidget(item, invitee);
-
- connect(invitee, &InviteeItem::removeItem, this, [this, item]() {
- emit removeInvitee(item);
- });
-
- errorLabel_->hide();
- inviteeInput_->clear();
- } catch (std::exception &e) {
- errorLabel_->setText(e.what());
- errorLabel_->show();
- }
-}
-
-void
-InviteUsers::removeInvitee(QListWidgetItem *item)
-{
- int row = inviteeList_->row(item);
- auto widget = inviteeList_->takeItem(row);
-
- inviteeList_->removeItemWidget(widget);
-}
-
-QStringList
-InviteUsers::invitedUsers() const
-{
- QStringList users;
-
- for (int ii = 0; ii < inviteeList_->count(); ++ii) {
- auto item = inviteeList_->item(ii);
- auto widget = inviteeList_->itemWidget(item);
- auto invitee = qobject_cast<InviteeItem *>(widget);
-
- if (invitee)
- users << invitee->userID();
- else
- qDebug() << "Cast InviteeItem failed";
- }
-
- return users;
-}
-
-void
-InviteUsers::showEvent(QShowEvent *event)
-{
- inviteeInput_->setFocus();
-
- QFrame::showEvent(event);
-}
diff --git a/src/dialogs/InviteUsers.h b/src/dialogs/InviteUsers.h
deleted file mode 100644
index e40183c1..00000000
--- a/src/dialogs/InviteUsers.h
+++ /dev/null
@@ -1,45 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QFrame>
-#include <QStringList>
-
-class QPushButton;
-class QLabel;
-class TextField;
-class QListWidget;
-class QListWidgetItem;
-
-namespace dialogs {
-
-class InviteUsers : public QFrame
-{
- Q_OBJECT
-public:
- explicit InviteUsers(QWidget *parent = nullptr);
-
-protected:
- void showEvent(QShowEvent *event) override;
-
-signals:
- void sendInvites(QStringList invitees);
-
-private slots:
- void removeInvitee(QListWidgetItem *item);
-
-private:
- void addUser();
- QStringList invitedUsers() const;
-
- QPushButton *confirmBtn_;
- QPushButton *cancelBtn_;
-
- TextField *inviteeInput_;
- QLabel *errorLabel_;
-
- QListWidget *inviteeList_;
-};
-} // dialogs
diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp
deleted file mode 100644
index 21eb72b0..00000000
--- a/src/dialogs/MemberList.cpp
+++ /dev/null
@@ -1,146 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QAbstractSlider>
-#include <QLabel>
-#include <QListWidgetItem>
-#include <QPainter>
-#include <QPushButton>
-#include <QScrollBar>
-#include <QShortcut>
-#include <QStyleOption>
-#include <QVBoxLayout>
-
-#include "dialogs/MemberList.h"
-
-#include "Cache.h"
-#include "ChatPage.h"
-#include "Config.h"
-#include "Logging.h"
-#include "Utils.h"
-#include "ui/Avatar.h"
-
-using namespace dialogs;
-
-MemberItem::MemberItem(const RoomMember &member, QWidget *parent)
- : QWidget(parent)
-{
- topLayout_ = new QHBoxLayout(this);
- topLayout_->setMargin(0);
-
- textLayout_ = new QVBoxLayout;
- textLayout_->setMargin(0);
- textLayout_->setSpacing(0);
-
- avatar_ = new Avatar(this, 44);
- avatar_->setLetter(utils::firstChar(member.display_name));
-
- avatar_->setImage(ChatPage::instance()->currentRoom(), member.user_id);
-
- QFont nameFont;
- nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1);
-
- userId_ = new QLabel(member.user_id, this);
- userName_ = new QLabel(member.display_name, this);
- userName_->setFont(nameFont);
-
- textLayout_->addWidget(userName_);
- textLayout_->addWidget(userId_);
-
- topLayout_->addWidget(avatar_);
- topLayout_->addLayout(textLayout_, 1);
-}
-
-void
-MemberItem::paintEvent(QPaintEvent *)
-{
- QStyleOption opt;
- opt.init(this);
- QPainter p(this);
- style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
-
-MemberList::MemberList(const QString &room_id, QWidget *parent)
- : QFrame(parent)
- , room_id_{room_id}
-{
- setAutoFillBackground(true);
- setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
- setWindowModality(Qt::WindowModal);
- setAttribute(Qt::WA_DeleteOnClose, true);
-
- auto layout = new QVBoxLayout(this);
- layout->setSpacing(conf::modals::WIDGET_SPACING);
- layout->setMargin(conf::modals::WIDGET_MARGIN);
-
- list_ = new QListWidget;
- list_->setFrameStyle(QFrame::NoFrame);
- list_->setSelectionMode(QAbstractItemView::NoSelection);
- list_->setSpacing(5);
-
- QFont largeFont;
- largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5);
-
- setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
- setMinimumHeight(list_->sizeHint().height() * 2);
- setMinimumWidth(std::max(list_->sizeHint().width() + 4 * conf::modals::WIDGET_MARGIN,
- QFontMetrics(largeFont).averageCharWidth() * 30 -
- 2 * conf::modals::WIDGET_MARGIN));
-
- QFont font;
- font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO);
-
- topLabel_ = new QLabel(tr("Room members"), this);
- topLabel_->setAlignment(Qt::AlignCenter);
- topLabel_->setFont(font);
-
- auto okBtn = new QPushButton(tr("OK"), this);
-
- auto buttonLayout = new QHBoxLayout();
- buttonLayout->setSpacing(15);
- buttonLayout->addStretch(1);
- buttonLayout->addWidget(okBtn);
-
- layout->addWidget(topLabel_);
- layout->addWidget(list_);
- layout->addLayout(buttonLayout);
-
- list_->clear();
-
- connect(list_->verticalScrollBar(), &QAbstractSlider::valueChanged, this, [this](int pos) {
- if (pos != list_->verticalScrollBar()->maximum())
- return;
-
- const size_t numMembers = list_->count() - 1;
-
- if (numMembers > 0)
- addUsers(cache::getMembers(room_id_.toStdString(), numMembers));
- });
-
- try {
- addUsers(cache::getMembers(room_id_.toStdString()));
- } catch (const lmdb::error &e) {
- nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what());
- }
-
- auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this);
- connect(closeShortcut, &QShortcut::activated, this, &MemberList::close);
- connect(okBtn, &QPushButton::clicked, this, &MemberList::close);
-}
-
-void
-MemberList::addUsers(const std::vector<RoomMember> &members)
-{
- for (const auto &member : members) {
- auto user = new MemberItem(member, this);
- auto item = new QListWidgetItem;
-
- item->setSizeHint(user->minimumSizeHint());
- item->setFlags(Qt::NoItemFlags);
- item->setTextAlignment(Qt::AlignCenter);
-
- list_->insertItem(list_->count() - 1, item);
- list_->setItemWidget(item, user);
- }
-}
diff --git a/src/dialogs/MemberList.h b/src/dialogs/MemberList.h
deleted file mode 100644
index b822eec8..00000000
--- a/src/dialogs/MemberList.h
+++ /dev/null
@@ -1,57 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QFrame>
-#include <QListWidget>
-
-class Avatar;
-class QPushButton;
-class QHBoxLayout;
-class QLabel;
-class QVBoxLayout;
-
-struct RoomMember;
-
-template<class T>
-class QSharedPointer;
-
-namespace dialogs {
-
-class MemberItem : public QWidget
-{
- Q_OBJECT
-
-public:
- MemberItem(const RoomMember &member, QWidget *parent);
-
-protected:
- void paintEvent(QPaintEvent *) override;
-
-private:
- QHBoxLayout *topLayout_;
- QVBoxLayout *textLayout_;
-
- Avatar *avatar_;
-
- QLabel *userName_;
- QLabel *userId_;
-};
-
-class MemberList : public QFrame
-{
- Q_OBJECT
-public:
- MemberList(const QString &room_id, QWidget *parent = nullptr);
-
-public slots:
- void addUsers(const std::vector<RoomMember> &users);
-
-private:
- QString room_id_;
- QLabel *topLabel_;
- QListWidget *list_;
-};
-} // dialogs
diff --git a/src/dialogs/RawMessage.h b/src/dialogs/RawMessage.h
deleted file mode 100644
index e95f675c..00000000
--- a/src/dialogs/RawMessage.h
+++ /dev/null
@@ -1,60 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QFont>
-#include <QFontDatabase>
-#include <QTextBrowser>
-#include <QVBoxLayout>
-#include <QWidget>
-
-#include "nlohmann/json.hpp"
-
-#include "Logging.h"
-#include "MainWindow.h"
-#include "ui/FlatButton.h"
-
-namespace dialogs {
-
-class RawMessage : public QWidget
-{
- Q_OBJECT
-public:
- RawMessage(QString msg, QWidget *parent = nullptr)
- : QWidget{parent}
- {
- QFont monospaceFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
-
- auto layout = new QVBoxLayout{this};
- auto viewer = new QTextBrowser{this};
- viewer->setFont(monospaceFont);
- viewer->setText(msg);
-
- layout->setSpacing(0);
- layout->setMargin(0);
- layout->addWidget(viewer);
-
- setAutoFillBackground(true);
- setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
- setAttribute(Qt::WA_DeleteOnClose, true);
-
- QSize winsize;
- QPoint center;
-
- auto window = MainWindow::instance();
- if (window) {
- winsize = window->frameGeometry().size();
- center = window->frameGeometry().center();
-
- move(center.x() - (width() * 0.5), center.y() - (height() * 0.5));
- } else {
- nhlog::ui()->warn("unable to retrieve MainWindow's size");
- }
-
- raise();
- show();
- }
-};
-} // namespace dialogs
diff --git a/src/dialogs/ReadReceipts.cpp b/src/dialogs/ReadReceipts.cpp
deleted file mode 100644
index fa7132fd..00000000
--- a/src/dialogs/ReadReceipts.cpp
+++ /dev/null
@@ -1,179 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QDebug>
-#include <QIcon>
-#include <QLabel>
-#include <QListWidgetItem>
-#include <QPainter>
-#include <QPushButton>
-#include <QShortcut>
-#include <QStyleOption>
-#include <QTimer>
-#include <QVBoxLayout>
-
-#include "dialogs/ReadReceipts.h"
-
-#include "AvatarProvider.h"
-#include "Cache.h"
-#include "ChatPage.h"
-#include "Config.h"
-#include "Utils.h"
-#include "ui/Avatar.h"
-
-using namespace dialogs;
-
-ReceiptItem::ReceiptItem(QWidget *parent,
- const QString &user_id,
- uint64_t timestamp,
- const QString &room_id)
- : QWidget(parent)
-{
- topLayout_ = new QHBoxLayout(this);
- topLayout_->setMargin(0);
-
- textLayout_ = new QVBoxLayout;
- textLayout_->setMargin(0);
- textLayout_->setSpacing(conf::modals::TEXT_SPACING);
-
- QFont nameFont;
- nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1);
-
- auto displayName = cache::displayName(room_id, user_id);
-
- avatar_ = new Avatar(this, 44);
- avatar_->setLetter(utils::firstChar(displayName));
-
- // If it's a matrix id we use the second letter.
- if (displayName.size() > 1 && displayName.at(0) == '@')
- avatar_->setLetter(QChar(displayName.at(1)));
-
- userName_ = new QLabel(displayName, this);
- userName_->setFont(nameFont);
-
- timestamp_ = new QLabel(dateFormat(QDateTime::fromMSecsSinceEpoch(timestamp)), this);
-
- textLayout_->addWidget(userName_);
- textLayout_->addWidget(timestamp_);
-
- topLayout_->addWidget(avatar_);
- topLayout_->addLayout(textLayout_, 1);
-
- avatar_->setImage(ChatPage::instance()->currentRoom(), user_id);
-}
-
-void
-ReceiptItem::paintEvent(QPaintEvent *)
-{
- QStyleOption opt;
- opt.init(this);
- QPainter p(this);
- style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
-
-QString
-ReceiptItem::dateFormat(const QDateTime &then) const
-{
- auto now = QDateTime::currentDateTime();
- auto days = then.daysTo(now);
-
- if (days == 0)
- return tr("Today %1")
- .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
- else if (days < 2)
- return tr("Yesterday %1")
- .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
- else if (days < 7)
- return QString("%1 %2")
- .arg(then.toString("dddd"))
- .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
-
- return QLocale::system().toString(then.time(), QLocale::ShortFormat);
-}
-
-ReadReceipts::ReadReceipts(QWidget *parent)
- : QFrame(parent)
-{
- setAutoFillBackground(true);
- setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
- setWindowModality(Qt::WindowModal);
- setAttribute(Qt::WA_DeleteOnClose, true);
-
- auto layout = new QVBoxLayout(this);
- layout->setSpacing(conf::modals::WIDGET_SPACING);
- layout->setMargin(conf::modals::WIDGET_MARGIN);
-
- userList_ = new QListWidget;
- userList_->setFrameStyle(QFrame::NoFrame);
- userList_->setSelectionMode(QAbstractItemView::NoSelection);
- userList_->setSpacing(conf::modals::TEXT_SPACING);
-
- QFont largeFont;
- largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5);
-
- setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
- setMinimumHeight(userList_->sizeHint().height() * 2);
- setMinimumWidth(std::max(userList_->sizeHint().width() + 4 * conf::modals::WIDGET_MARGIN,
- QFontMetrics(largeFont).averageCharWidth() * 30 -
- 2 * conf::modals::WIDGET_MARGIN));
-
- QFont font;
- font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO);
-
- topLabel_ = new QLabel(tr("Read receipts"), this);
- topLabel_->setAlignment(Qt::AlignCenter);
- topLabel_->setFont(font);
-
- auto okBtn = new QPushButton(tr("Close"), this);
-
- auto buttonLayout = new QHBoxLayout();
- buttonLayout->setSpacing(15);
- buttonLayout->addStretch(1);
- buttonLayout->addWidget(okBtn);
-
- layout->addWidget(topLabel_);
- layout->addWidget(userList_);
- layout->addLayout(buttonLayout);
-
- auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this);
- connect(closeShortcut, &QShortcut::activated, this, &ReadReceipts::close);
- connect(okBtn, &QPushButton::clicked, this, &ReadReceipts::close);
-}
-
-void
-ReadReceipts::addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &receipts)
-{
- // We want to remove any previous items that have been set.
- userList_->clear();
-
- for (const auto &receipt : receipts) {
- auto user = new ReceiptItem(this,
- QString::fromStdString(receipt.second),
- receipt.first,
- ChatPage::instance()->currentRoom());
- auto item = new QListWidgetItem(userList_);
-
- item->setSizeHint(user->minimumSizeHint());
- item->setFlags(Qt::NoItemFlags);
- item->setTextAlignment(Qt::AlignCenter);
-
- userList_->setItemWidget(item, user);
- }
-}
-
-void
-ReadReceipts::paintEvent(QPaintEvent *)
-{
- QStyleOption opt;
- opt.init(this);
- QPainter p(this);
- style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
-
-void
-ReadReceipts::hideEvent(QHideEvent *event)
-{
- userList_->clear();
- QFrame::hideEvent(event);
-}
diff --git a/src/dialogs/ReadReceipts.h b/src/dialogs/ReadReceipts.h
deleted file mode 100644
index 5c6c5d2b..00000000
--- a/src/dialogs/ReadReceipts.h
+++ /dev/null
@@ -1,61 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QDateTime>
-#include <QFrame>
-
-class Avatar;
-class QLabel;
-class QListWidget;
-class QHBoxLayout;
-class QVBoxLayout;
-
-namespace dialogs {
-
-class ReceiptItem : public QWidget
-{
- Q_OBJECT
-
-public:
- ReceiptItem(QWidget *parent,
- const QString &user_id,
- uint64_t timestamp,
- const QString &room_id);
-
-protected:
- void paintEvent(QPaintEvent *) override;
-
-private:
- QString dateFormat(const QDateTime &then) const;
-
- QHBoxLayout *topLayout_;
- QVBoxLayout *textLayout_;
-
- Avatar *avatar_;
-
- QLabel *userName_;
- QLabel *timestamp_;
-};
-
-class ReadReceipts : public QFrame
-{
- Q_OBJECT
-public:
- explicit ReadReceipts(QWidget *parent = nullptr);
-
-public slots:
- void addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users);
-
-protected:
- void paintEvent(QPaintEvent *event) override;
- void hideEvent(QHideEvent *event) override;
-
-private:
- QLabel *topLabel_;
-
- QListWidget *userList_;
-};
-} // dialogs
diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp
index 598b2bd0..2809de87 100644
--- a/src/notifications/ManagerLinux.cpp
+++ b/src/notifications/ManagerLinux.cpp
@@ -295,12 +295,9 @@ operator<<(QDBusArgument &arg, const QImage &image)
int channels = i.isGrayscale() ? 1 : (i.hasAlphaChannel() ? 4 : 3);
arg << i.depth() / channels;
arg << channels;
-#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
- arg << QByteArray(reinterpret_cast<const char *>(i.bits()), i.byteCount());
-#else
arg << QByteArray(reinterpret_cast<const char *>(i.bits()), i.sizeInBytes());
-#endif
arg.endStructure();
+
return arg;
}
diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 9a91ff79..742f8dbb 100644
--- a/src/timeline/EventStore.cpp
+++ b/src/timeline/EventStore.cpp
@@ -20,8 +20,7 @@
Q_DECLARE_METATYPE(Reaction)
-QCache<EventStore::IdIndex, mtx::events::collections::TimelineEvents> EventStore::decryptedEvents_{
- 1000};
+QCache<EventStore::IdIndex, olm::DecryptionResult> EventStore::decryptedEvents_{1000};
QCache<EventStore::IdIndex, mtx::events::collections::TimelineEvents> EventStore::events_by_id_{
1000};
QCache<EventStore::Index, mtx::events::collections::TimelineEvents> EventStore::events_{1000};
@@ -144,12 +143,16 @@ EventStore::EventStore(std::string room_id, QObject *)
mtx::events::msg::Encrypted>) {
auto event =
decryptEvent({room_id_, e.event_id}, e);
- if (auto dec =
- std::get_if<mtx::events::RoomEvent<
- mtx::events::msg::
- KeyVerificationRequest>>(event)) {
- emit updateFlowEventId(
- event_id.event_id.to_string());
+ if (event->event) {
+ if (auto dec = std::get_if<
+ mtx::events::RoomEvent<
+ mtx::events::msg::
+ KeyVerificationRequest>>(
+ &event->event.value())) {
+ emit updateFlowEventId(
+ event_id.event_id
+ .to_string());
+ }
}
}
});
@@ -393,12 +396,12 @@ EventStore::handleSync(const mtx::responses::Timeline &events)
if (auto encrypted =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
&event)) {
- mtx::events::collections::TimelineEvents *d_event =
- decryptEvent({room_id_, encrypted->event_id}, *encrypted);
- if (std::visit(
+ auto d_event = decryptEvent({room_id_, encrypted->event_id}, *encrypted);
+ if (d_event->event &&
+ std::visit(
[](auto e) { return (e.sender != utils::localUser().toStdString()); },
- *d_event)) {
- handle_room_verification(*d_event);
+ *d_event->event)) {
+ handle_room_verification(*d_event->event);
}
}
}
@@ -599,11 +602,15 @@ EventStore::get(int idx, bool decrypt)
events_.insert(index, event_ptr);
}
- if (decrypt)
+ if (decrypt) {
if (auto encrypted =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
- event_ptr))
- return decryptEvent({room_id_, encrypted->event_id}, *encrypted);
+ event_ptr)) {
+ auto decrypted = decryptEvent({room_id_, encrypted->event_id}, *encrypted);
+ if (decrypted->event)
+ return &*decrypted->event;
+ }
+ }
return event_ptr;
}
@@ -629,7 +636,7 @@ EventStore::indexToId(int idx) const
return cache::client()->getTimelineEventId(room_id_, toInternalIdx(idx));
}
-mtx::events::collections::TimelineEvents *
+olm::DecryptionResult *
EventStore::decryptEvent(const IdIndex &idx,
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e)
{
@@ -641,57 +648,24 @@ EventStore::decryptEvent(const IdIndex &idx,
index.session_id = e.content.session_id;
index.sender_key = e.content.sender_key;
- auto asCacheEntry = [&idx](mtx::events::collections::TimelineEvents &&event) {
- auto event_ptr = new mtx::events::collections::TimelineEvents(std::move(event));
+ auto asCacheEntry = [&idx](olm::DecryptionResult &&event) {
+ auto event_ptr = new olm::DecryptionResult(std::move(event));
decryptedEvents_.insert(idx, event_ptr);
return event_ptr;
};
auto decryptionResult = olm::decryptEvent(index, e);
- mtx::events::RoomEvent<mtx::events::msg::Notice> dummy;
- dummy.origin_server_ts = e.origin_server_ts;
- dummy.event_id = e.event_id;
- dummy.sender = e.sender;
-
if (decryptionResult.error) {
- switch (*decryptionResult.error) {
+ switch (decryptionResult.error) {
case olm::DecryptionErrorCode::MissingSession:
case olm::DecryptionErrorCode::MissingSessionIndex: {
- if (decryptionResult.error == olm::DecryptionErrorCode::MissingSession)
- dummy.content.body =
- tr("-- Encrypted Event (No keys found for decryption) --",
- "Placeholder, when the message was not decrypted yet or can't "
- "be "
- "decrypted.")
- .toStdString();
- else
- dummy.content.body =
- tr("-- Encrypted Event (Key not valid for this index) --",
- "Placeholder, when the message can't be decrypted with this "
- "key since it is not valid for this index ")
- .toStdString();
nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
index.room_id,
index.session_id,
e.sender);
- // we may not want to request keys during initial sync and such
- if (suppressKeyRequests)
- break;
- // TODO: Check if this actually works and look in key backup
- auto copy = e;
- copy.room_id = room_id_;
- if (pending_key_requests.count(e.content.session_id)) {
- pending_key_requests.at(e.content.session_id)
- .events.push_back(copy);
- } else {
- PendingKeyRequests request;
- request.request_id =
- "key_request." + http::client()->generate_txn_id();
- request.events.push_back(copy);
- olm::send_key_request_for(copy, request.request_id);
- pending_key_requests[e.content.session_id] = request;
- }
+
+ requestSession(e, false);
break;
}
case olm::DecryptionErrorCode::DbError:
@@ -701,12 +675,6 @@ EventStore::decryptEvent(const IdIndex &idx,
index.session_id,
index.sender_key,
decryptionResult.error_message.value_or(""));
- dummy.content.body =
- tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
- "Placeholder, when the message can't be decrypted, because the DB "
- "access "
- "failed.")
- .toStdString();
break;
case olm::DecryptionErrorCode::DecryptionFailed:
nhlog::crypto()->critical(
@@ -715,22 +683,8 @@ EventStore::decryptEvent(const IdIndex &idx,
index.session_id,
index.sender_key,
decryptionResult.error_message.value_or(""));
- dummy.content.body =
- tr("-- Decryption Error (%1) --",
- "Placeholder, when the message can't be decrypted. In this case, the "
- "Olm "
- "decrytion returned an error, which is passed as %1.")
- .arg(
- QString::fromStdString(decryptionResult.error_message.value_or("")))
- .toStdString();
break;
case olm::DecryptionErrorCode::ParsingFailed:
- dummy.content.body =
- tr("-- Encrypted Event (Unknown event type) --",
- "Placeholder, when the message was decrypted, but we couldn't parse "
- "it, because "
- "Nheko/mtxclient don't support that event type yet.")
- .toStdString();
break;
case olm::DecryptionErrorCode::ReplayAttack:
nhlog::crypto()->critical(
@@ -738,85 +692,50 @@ EventStore::decryptEvent(const IdIndex &idx,
e.event_id,
room_id_,
index.sender_key);
- dummy.content.body =
- tr("-- Replay attack! This message index was reused! --").toStdString();
break;
- case olm::DecryptionErrorCode::UnknownFingerprint:
- // TODO: don't fail, just show in UI.
- nhlog::crypto()->critical("Message by unverified fingerprint {}",
- index.sender_key);
- dummy.content.body =
- tr("-- Message by unverified device! --").toStdString();
+ case olm::DecryptionErrorCode::NoError:
+ // unreachable
break;
}
- return asCacheEntry(std::move(dummy));
- }
-
- std::string msg_str;
- try {
- auto session = cache::client()->getInboundMegolmSession(index);
- auto res =
- olm::client()->decrypt_group_message(session.get(), e.content.ciphertext);
- msg_str = std::string((char *)res.data.data(), res.data.size());
- } catch (const lmdb::error &e) {
- nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})",
- index.room_id,
- index.session_id,
- index.sender_key,
- e.what());
- dummy.content.body =
- tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
- "Placeholder, when the message can't be decrypted, because the DB "
- "access "
- "failed.")
- .toStdString();
- return asCacheEntry(std::move(dummy));
- } catch (const mtx::crypto::olm_exception &e) {
- nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}",
- index.room_id,
- index.session_id,
- index.sender_key,
- e.what());
- dummy.content.body =
- tr("-- Decryption Error (%1) --",
- "Placeholder, when the message can't be decrypted. In this case, the "
- "Olm "
- "decrytion returned an error, which is passed as %1.")
- .arg(e.what())
- .toStdString();
- return asCacheEntry(std::move(dummy));
+ return asCacheEntry(std::move(decryptionResult));
}
- // Add missing fields for the event.
- json body = json::parse(msg_str);
- body["event_id"] = e.event_id;
- body["sender"] = e.sender;
- body["origin_server_ts"] = e.origin_server_ts;
- body["unsigned"] = e.unsigned_data;
-
- // relations are unencrypted in content...
- mtx::common::add_relations(body["content"], e.content.relations);
-
- json event_array = json::array();
- event_array.push_back(body);
+ auto encInfo = mtx::accessors::file(decryptionResult.event.value());
+ if (encInfo)
+ emit newEncryptedImage(encInfo.value());
- std::vector<mtx::events::collections::TimelineEvents> temp_events;
- mtx::responses::utils::parse_timeline_events(event_array, temp_events);
+ return asCacheEntry(std::move(decryptionResult));
+}
- if (temp_events.size() == 1) {
- auto encInfo = mtx::accessors::file(temp_events[0]);
+void
+EventStore::requestSession(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &ev,
+ bool manual)
+{
+ // we may not want to request keys during initial sync and such
+ if (suppressKeyRequests)
+ return;
- if (encInfo)
- emit newEncryptedImage(encInfo.value());
+ // TODO: Look in key backup
+ auto copy = ev;
+ copy.room_id = room_id_;
+ if (pending_key_requests.count(ev.content.session_id)) {
+ auto &r = pending_key_requests.at(ev.content.session_id);
+ r.events.push_back(copy);
- return asCacheEntry(std::move(temp_events[0]));
+ // automatically request once every 10 min, manually every 1 min
+ qint64 delay = manual ? 60 : (60 * 10);
+ if (r.requested_at + delay < QDateTime::currentSecsSinceEpoch()) {
+ r.requested_at = QDateTime::currentSecsSinceEpoch();
+ olm::send_key_request_for(copy, r.request_id);
+ }
+ } else {
+ PendingKeyRequests request;
+ request.request_id = "key_request." + http::client()->generate_txn_id();
+ request.requested_at = QDateTime::currentSecsSinceEpoch();
+ request.events.push_back(copy);
+ olm::send_key_request_for(copy, request.request_id);
+ pending_key_requests[ev.content.session_id] = request;
}
-
- auto encInfo = mtx::accessors::file(decryptionResult.event.value());
- if (encInfo)
- emit newEncryptedImage(encInfo.value());
-
- return asCacheEntry(std::move(decryptionResult.event.value()));
}
void
@@ -877,15 +796,56 @@ EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool
events_by_id_.insert(index, event_ptr);
}
- if (decrypt)
+ if (decrypt) {
if (auto encrypted =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
- event_ptr))
- return decryptEvent(index, *encrypted);
+ event_ptr)) {
+ auto decrypted = decryptEvent(index, *encrypted);
+ if (decrypted->event)
+ return &*decrypted->event;
+ }
+ }
return event_ptr;
}
+olm::DecryptionErrorCode
+EventStore::decryptionError(std::string id)
+{
+ if (this->thread() != QThread::currentThread())
+ nhlog::db()->warn("{} called from a different thread!", __func__);
+
+ if (id.empty())
+ return olm::DecryptionErrorCode::NoError;
+
+ IdIndex index{room_id_, std::move(id)};
+ auto edits_ = edits(index.id);
+ if (!edits_.empty()) {
+ index.id = mtx::accessors::event_id(edits_.back());
+ auto event_ptr =
+ new mtx::events::collections::TimelineEvents(std::move(edits_.back()));
+ events_by_id_.insert(index, event_ptr);
+ }
+
+ auto event_ptr = events_by_id_.object(index);
+ if (!event_ptr) {
+ auto event = cache::client()->getEvent(room_id_, index.id);
+ if (!event) {
+ return olm::DecryptionErrorCode::NoError;
+ }
+ event_ptr = new mtx::events::collections::TimelineEvents(std::move(event->data));
+ events_by_id_.insert(index, event_ptr);
+ }
+
+ if (auto encrypted =
+ std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(event_ptr)) {
+ auto decrypted = decryptEvent(index, *encrypted);
+ return decrypted->error;
+ }
+
+ return olm::DecryptionErrorCode::NoError;
+}
+
void
EventStore::fetchMore()
{
diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h
index 7c404102..59c1c7c0 100644
--- a/src/timeline/EventStore.h
+++ b/src/timeline/EventStore.h
@@ -15,6 +15,7 @@
#include <mtx/responses/messages.hpp>
#include <mtx/responses/sync.hpp>
+#include "Olm.h"
#include "Reaction.h"
class EventStore : public QObject
@@ -78,6 +79,9 @@ public:
mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true);
QVariantList reactions(const std::string &event_id);
+ olm::DecryptionErrorCode decryptionError(std::string id);
+ void requestSession(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &ev,
+ bool manual);
int size() const
{
@@ -119,7 +123,7 @@ public slots:
private:
std::vector<mtx::events::collections::TimelineEvents> edits(const std::string &event_id);
- mtx::events::collections::TimelineEvents *decryptEvent(
+ olm::DecryptionResult *decryptEvent(
const IdIndex &idx,
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
void handle_room_verification(mtx::events::collections::TimelineEvents event);
@@ -129,7 +133,7 @@ private:
uint64_t first = std::numeric_limits<uint64_t>::max(),
last = std::numeric_limits<uint64_t>::max();
- static QCache<IdIndex, mtx::events::collections::TimelineEvents> decryptedEvents_;
+ static QCache<IdIndex, olm::DecryptionResult> decryptedEvents_;
static QCache<Index, mtx::events::collections::TimelineEvents> events_;
static QCache<IdIndex, mtx::events::collections::TimelineEvents> events_by_id_;
@@ -137,6 +141,7 @@ private:
{
std::string request_id;
std::vector<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>> events;
+ qint64 requested_at;
};
std::map<std::string, PendingKeyRequests> pending_key_requests;
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index 56d0d1ce..f17081e5 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -19,9 +19,9 @@
#include "Cache.h"
#include "ChatPage.h"
+#include "CombinedImagePackModel.h"
#include "CompletionProxyModel.h"
#include "Config.h"
-#include "ImagePackModel.h"
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
@@ -503,7 +503,7 @@ InputBar::video(const QString &filename,
}
void
-InputBar::sticker(ImagePackModel *model, int row)
+InputBar::sticker(CombinedImagePackModel *model, int row)
{
if (!model || row < 0)
return;
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index acedceb7..2e6fb5c0 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -12,7 +12,7 @@
#include <mtx/responses/messages.hpp>
class TimelineModel;
-class ImagePackModel;
+class CombinedImagePackModel;
class QMimeData;
class QDropEvent;
class QStringList;
@@ -58,7 +58,7 @@ public slots:
MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED,
bool rainbowify = false);
void reaction(const QString &reactedEvent, const QString &reactionKey);
- void sticker(ImagePackModel *model, int row);
+ void sticker(CombinedImagePackModel *model, int row);
private slots:
void startTyping();
diff --git a/src/timeline/Permissions.cpp b/src/timeline/Permissions.cpp
index 1eaab468..e4957045 100644
--- a/src/timeline/Permissions.cpp
+++ b/src/timeline/Permissions.cpp
@@ -8,9 +8,9 @@
#include "MatrixClient.h"
#include "TimelineModel.h"
-Permissions::Permissions(TimelineModel *parent)
+Permissions::Permissions(QString roomId, QObject *parent)
: QObject(parent)
- , room(parent)
+ , roomId_(roomId)
{
invalidate();
}
@@ -19,7 +19,7 @@ void
Permissions::invalidate()
{
pl = cache::client()
- ->getStateEvent<mtx::events::state::PowerLevels>(room->roomId().toStdString())
+ ->getStateEvent<mtx::events::state::PowerLevels>(roomId_.toStdString())
.value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
.content;
}
diff --git a/src/timeline/Permissions.h b/src/timeline/Permissions.h
index f7e6f389..7aab1ddb 100644
--- a/src/timeline/Permissions.h
+++ b/src/timeline/Permissions.h
@@ -15,7 +15,7 @@ class Permissions : public QObject
Q_OBJECT
public:
- Permissions(TimelineModel *parent);
+ Permissions(QString roomId, QObject *parent = nullptr);
Q_INVOKABLE bool canInvite();
Q_INVOKABLE bool canBan();
@@ -28,6 +28,6 @@ public:
void invalidate();
private:
- TimelineModel *room;
+ QString roomId_;
mtx::events::state::PowerLevels pl;
};
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index f7f377fb..f4c927ac 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -533,6 +533,8 @@ RoomlistModel::initializeRooms()
for (const auto &id : cache::client()->roomIds())
addRoom(id, true);
+ nhlog::db()->info("Restored {} rooms from cache", rowCount());
+
endResetModel();
}
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index abfe28a9..99e00a67 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -25,11 +25,12 @@
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
+#include "MemberList.h"
#include "MxcImageProvider.h"
#include "Olm.h"
+#include "ReadReceiptsModel.h"
#include "TimelineViewManager.h"
#include "Utils.h"
-#include "dialogs/RawMessage.h"
Q_DECLARE_METATYPE(QModelIndex)
@@ -307,6 +308,15 @@ qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t)
case qml_mtx_events::KeyVerificationDone:
case qml_mtx_events::KeyVerificationReady:
return mtx::events::EventType::RoomMessage;
+ //! m.image_pack, currently im.ponies.room_emotes
+ case qml_mtx_events::ImagePackInRoom:
+ return mtx::events::EventType::ImagePackRooms;
+ //! m.image_pack, currently im.ponies.user_emotes
+ case qml_mtx_events::ImagePackInAccountData:
+ return mtx::events::EventType::ImagePackInAccountData;
+ //! m.image_pack.rooms, currently im.ponies.emote_rooms
+ case qml_mtx_events::ImagePackRooms:
+ return mtx::events::EventType::ImagePackRooms;
default:
return mtx::events::EventType::Unsupported;
};
@@ -317,6 +327,7 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
, events(room_id.toStdString(), this)
, room_id_(room_id)
, manager_(manager)
+ , permissions_{room_id}
{
lastMessage_.timestamp = 0;
@@ -325,6 +336,10 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
this->isSpace_ = create->content.type == mtx::events::state::room_type::space;
this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString());
+ // this connection will simplify adding the plainRoomNameChanged() signal everywhere that it
+ // needs to be
+ connect(this, &TimelineModel::roomNameChanged, this, &TimelineModel::plainRoomNameChanged);
+
connect(
this,
&TimelineModel::redactionFailed,
@@ -344,6 +359,7 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
&EventStore::dataChanged,
this,
[this](int from, int to) {
+ relatedEventCacheBuster++;
nhlog::ui()->debug(
"data changed {} to {}", events.size() - to - 1, events.size() - from - 1);
emit dataChanged(index(events.size() - to - 1, 0),
@@ -436,6 +452,7 @@ TimelineModel::roleNames() const
{IsEditable, "isEditable"},
{IsEncrypted, "isEncrypted"},
{Trustlevel, "trustlevel"},
+ {EncryptionError, "encryptionError"},
{ReplyTo, "replyTo"},
{Reactions, "reactions"},
{RoomId, "roomId"},
@@ -443,6 +460,7 @@ TimelineModel::roleNames() const
{RoomTopic, "roomTopic"},
{CallType, "callType"},
{Dump, "dump"},
+ {RelatedEventCacheBuster, "relatedEventCacheBuster"},
};
}
int
@@ -622,6 +640,9 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
return crypto::Trust::Unverified;
}
+ case EncryptionError:
+ return events.decryptionError(event_id(event));
+
case ReplyTo:
return QVariant(QString::fromStdString(relations(event).reply_to().value_or("")));
case Reactions: {
@@ -673,9 +694,12 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
m.insert(names[RoomName], data(event, static_cast<int>(RoomName)));
m.insert(names[RoomTopic], data(event, static_cast<int>(RoomTopic)));
m.insert(names[CallType], data(event, static_cast<int>(CallType)));
+ m.insert(names[EncryptionError], data(event, static_cast<int>(EncryptionError)));
return QVariant(m);
}
+ case RelatedEventCacheBuster:
+ return relatedEventCacheBuster;
default:
return QVariant();
}
@@ -1015,14 +1039,13 @@ TimelineModel::formatDateSeparator(QDate date) const
}
void
-TimelineModel::viewRawMessage(QString id) const
+TimelineModel::viewRawMessage(QString id)
{
auto e = events.get(id.toStdString(), "", false);
if (!e)
return;
std::string ev = mtx::accessors::serialize_event(*e).dump(4);
- auto dialog = new dialogs::RawMessage(QString::fromStdString(ev));
- Q_UNUSED(dialog);
+ emit showRawMessageDialog(QString::fromStdString(ev));
}
void
@@ -1036,15 +1059,14 @@ TimelineModel::forwardMessage(QString eventId, QString roomId)
}
void
-TimelineModel::viewDecryptedRawMessage(QString id) const
+TimelineModel::viewDecryptedRawMessage(QString id)
{
auto e = events.get(id.toStdString(), "");
if (!e)
return;
std::string ev = mtx::accessors::serialize_event(*e).dump(4);
- auto dialog = new dialogs::RawMessage(QString::fromStdString(ev));
- Q_UNUSED(dialog);
+ emit showRawMessageDialog(QString::fromStdString(ev));
}
void
@@ -1057,14 +1079,6 @@ TimelineModel::openUserProfile(QString userid)
}
void
-TimelineModel::openRoomSettings()
-{
- RoomSettings *settings = new RoomSettings(roomId(), this);
- connect(this, &TimelineModel::roomAvatarUrlChanged, settings, &RoomSettings::avatarChanged);
- openRoomSettingsDialog(settings);
-}
-
-void
TimelineModel::replyAction(QString id)
{
setReply(id);
@@ -1087,9 +1101,9 @@ TimelineModel::relatedInfo(QString id)
}
void
-TimelineModel::readReceiptsAction(QString id) const
+TimelineModel::showReadReceipts(QString id)
{
- MainWindow::instance()->openReadReceiptsDialog(id);
+ emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this});
}
void
@@ -1543,6 +1557,17 @@ TimelineModel::scrollTimerEvent()
}
void
+TimelineModel::requestKeyForEvent(QString id)
+{
+ auto encrypted_event = events.get(id.toStdString(), "", false);
+ if (encrypted_event) {
+ if (auto ev = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+ encrypted_event))
+ events.requestSession(*ev, true);
+ }
+}
+
+void
TimelineModel::copyLinkToEvent(QString eventId) const
{
QStringList vias;
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 0e2895d4..ad7cfbbb 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -17,7 +17,10 @@
#include "CacheStructs.h"
#include "EventStore.h"
#include "InputBar.h"
+#include "InviteesModel.h"
+#include "MemberList.h"
#include "Permissions.h"
+#include "ReadReceiptsModel.h"
#include "ui/RoomSettings.h"
#include "ui/UserProfile.h"
@@ -104,7 +107,13 @@ enum EventType
KeyVerificationCancel,
KeyVerificationKey,
KeyVerificationDone,
- KeyVerificationReady
+ KeyVerificationReady,
+ //! m.image_pack, currently im.ponies.room_emotes
+ ImagePackInRoom,
+ //! m.image_pack, currently im.ponies.user_emotes
+ ImagePackInAccountData,
+ //! m.image_pack.rooms, currently im.ponies.emote_rooms
+ ImagePackRooms,
};
Q_ENUM_NS(EventType)
mtx::events::EventType fromRoomEventType(qml_mtx_events::EventType);
@@ -158,7 +167,9 @@ class TimelineModel : public QAbstractListModel
Q_PROPERTY(QString edit READ edit WRITE setEdit NOTIFY editChanged RESET resetEdit)
Q_PROPERTY(
bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged)
+ Q_PROPERTY(QString roomId READ roomId CONSTANT)
Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
+ Q_PROPERTY(QString plainRoomName READ plainRoomName NOTIFY plainRoomNameChanged)
Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged)
Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged)
Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged)
@@ -201,6 +212,7 @@ public:
IsEditable,
IsEncrypted,
Trustlevel,
+ EncryptionError,
ReplyTo,
Reactions,
RoomId,
@@ -208,6 +220,7 @@ public:
RoomTopic,
CallType,
Dump,
+ RelatedEventCacheBuster,
};
Q_ENUM(Roles);
@@ -230,14 +243,13 @@ public:
Q_INVOKABLE QString formatGuestAccessEvent(QString id);
Q_INVOKABLE QString formatPowerLevelEvent(QString id);
- Q_INVOKABLE void viewRawMessage(QString id) const;
+ Q_INVOKABLE void viewRawMessage(QString id);
Q_INVOKABLE void forwardMessage(QString eventId, QString roomId);
- Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
+ Q_INVOKABLE void viewDecryptedRawMessage(QString id);
Q_INVOKABLE void openUserProfile(QString userid);
- Q_INVOKABLE void openRoomSettings();
Q_INVOKABLE void editAction(QString id);
Q_INVOKABLE void replyAction(QString id);
- Q_INVOKABLE void readReceiptsAction(QString id) const;
+ Q_INVOKABLE void showReadReceipts(QString id);
Q_INVOKABLE void redactEvent(QString id);
Q_INVOKABLE int idToIndex(QString id) const;
Q_INVOKABLE QString indexToId(int index) const;
@@ -253,6 +265,8 @@ public:
endResetModel();
}
+ Q_INVOKABLE void requestKeyForEvent(QString id);
+
std::vector<::Reaction> reactions(const std::string &event_id)
{
auto list = events.reactions(event_id);
@@ -344,6 +358,8 @@ signals:
void typingUsersChanged(std::vector<QString> users);
void replyChanged(QString reply);
void editChanged(QString reply);
+ void openReadReceiptsDialog(ReadReceiptsProxy *rr);
+ void showRawMessageDialog(QString rawMessage);
void paginationInProgressChanged(const bool);
void newCallEvent(const mtx::events::collections::TimelineEvents &event);
void scrollToIndex(int index);
@@ -351,14 +367,13 @@ signals:
void lastMessageChanged();
void notificationsChanged();
- void openRoomSettingsDialog(RoomSettings *settings);
-
void newMessageToSend(mtx::events::collections::TimelineEvents event);
void addPendingMessageToStore(mtx::events::collections::TimelineEvents event);
void updateFlowEventId(std::string event_id);
void encryptionChanged();
void roomNameChanged();
+ void plainRoomNameChanged();
void roomTopicChanged();
void roomAvatarUrlChanged();
void roomMemberCountChanged();
@@ -388,7 +403,7 @@ private:
TimelineViewManager *manager_;
InputBar input_{this};
- Permissions permissions_{this};
+ Permissions permissions_;
QTimer showEventTimer{this};
QString eventIdToShow;
@@ -400,6 +415,8 @@ private:
int notification_count = 0, highlight_count = 0;
+ unsigned int relatedEventCacheBuster = 0;
+
bool decryptDescription = true;
bool m_paginationInProgress = false;
bool isSpace_ = false;
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 3e69f92b..b23ed278 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -15,16 +15,20 @@
#include "ChatPage.h"
#include "Clipboard.h"
#include "ColorImageProvider.h"
+#include "CombinedImagePackModel.h"
#include "CompletionProxyModel.h"
#include "DelegateChooser.h"
#include "DeviceVerificationFlow.h"
#include "EventAccessors.h"
-#include "ImagePackModel.h"
+#include "ImagePackListModel.h"
+#include "InviteesModel.h"
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
#include "MxcImageProvider.h"
+#include "ReadReceiptsModel.h"
#include "RoomsModel.h"
+#include "SingleImagePackModel.h"
#include "UserSettingsPage.h"
#include "UsersModel.h"
#include "dialogs/ImageOverlay.h"
@@ -145,7 +149,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
qRegisterMetaType<mtx::events::msg::KeyVerificationReady>();
qRegisterMetaType<mtx::events::msg::KeyVerificationRequest>();
qRegisterMetaType<mtx::events::msg::KeyVerificationStart>();
- qRegisterMetaType<ImagePackModel *>();
+ qRegisterMetaType<CombinedImagePackModel *>();
qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
"im.nheko",
@@ -154,6 +158,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
"MtxEvent",
"Can't instantiate enum!");
qmlRegisterUncreatableMetaObject(
+ olm::staticMetaObject, "im.nheko", 1, 0, "Olm", "Can't instantiate enum!");
+ qmlRegisterUncreatableMetaObject(
crypto::staticMetaObject, "im.nheko", 1, 0, "Crypto", "Can't instantiate enum!");
qmlRegisterUncreatableMetaObject(verification::staticMetaObject,
"im.nheko",
@@ -174,6 +180,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
0,
"UserProfileModel",
"UserProfile needs to be instantiated on the C++ side");
+ qmlRegisterUncreatableType<MemberList>(
+ "im.nheko", 1, 0, "MemberList", "MemberList needs to be instantiated on the C++ side");
qmlRegisterUncreatableType<RoomSettings>(
"im.nheko",
1,
@@ -182,6 +190,30 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
"Room Settings needs to be instantiated on the C++ side");
qmlRegisterUncreatableType<TimelineModel>(
"im.nheko", 1, 0, "Room", "Room needs to be instantiated on the C++ side");
+ qmlRegisterUncreatableType<ImagePackListModel>(
+ "im.nheko",
+ 1,
+ 0,
+ "ImagePackListModel",
+ "ImagePackListModel needs to be instantiated on the C++ side");
+ qmlRegisterUncreatableType<SingleImagePackModel>(
+ "im.nheko",
+ 1,
+ 0,
+ "SingleImagePackModel",
+ "SingleImagePackModel needs to be instantiated on the C++ side");
+ qmlRegisterUncreatableType<InviteesModel>(
+ "im.nheko",
+ 1,
+ 0,
+ "InviteesModel",
+ "InviteesModel needs to be instantiated on the C++ side");
+ qmlRegisterUncreatableType<ReadReceiptsProxy>(
+ "im.nheko",
+ 1,
+ 0,
+ "ReadReceiptsProxy",
+ "ReadReceiptsProxy needs to be instantiated on the C++ side");
static auto self = this;
qmlRegisterSingletonType<MainWindow>(
@@ -343,6 +375,41 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
}
void
+TimelineViewManager::openRoomMembers(QString room_id)
+{
+ MemberList *memberList = new MemberList(room_id, this);
+ emit openRoomMembersDialog(memberList);
+}
+
+void
+TimelineViewManager::openRoomSettings(QString room_id)
+{
+ RoomSettings *settings = new RoomSettings(room_id, this);
+ connect(rooms_->getRoomById(room_id).data(),
+ &TimelineModel::roomAvatarUrlChanged,
+ settings,
+ &RoomSettings::avatarChanged);
+ emit openRoomSettingsDialog(settings);
+}
+
+void
+TimelineViewManager::openInviteUsers(QString roomId)
+{
+ InviteesModel *model = new InviteesModel{this};
+ connect(model, &InviteesModel::accept, this, [this, model, roomId]() {
+ emit inviteUsers(roomId, model->mxids());
+ });
+ emit openInviteUsersDialog(model);
+}
+
+void
+TimelineViewManager::openGlobalUserProfile(QString userId)
+{
+ UserProfile *profile = new UserProfile{QString{}, userId, this};
+ emit openProfile(profile);
+}
+
+void
TimelineViewManager::setVideoCallItem()
{
WebRTCSession::instance().setVideoItem(
@@ -400,6 +467,12 @@ TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId)
}
void
+TimelineViewManager::openImagePackSettings(QString roomid)
+{
+ emit showImagePackSettings(new ImagePackListModel(roomid.toStdString(), this));
+}
+
+void
TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img)
{
auto pixmap = QPixmap::fromImage(img);
@@ -422,17 +495,6 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img)
}
void
-TimelineViewManager::openInviteUsersDialog()
-{
- MainWindow::instance()->openInviteUsersDialog(
- [this](const QStringList &invitees) { emit inviteUsers(invitees); });
-}
-void
-TimelineViewManager::openMemberListDialog(QString roomid) const
-{
- MainWindow::instance()->openMemberListDialog(roomid);
-}
-void
TimelineViewManager::openLeaveRoomDialog(QString roomid) const
{
MainWindow::instance()->openLeaveRoomDialog(roomid);
@@ -596,7 +658,7 @@ TimelineViewManager::completerFor(QString completerName, QString roomId)
roomModel->setParent(proxy);
return proxy;
} else if (completerName == "stickers") {
- auto stickerModel = new ImagePackModel(roomId.toStdString(), true);
+ auto stickerModel = new CombinedImagePackModel(roomId.toStdString(), true);
auto proxy = new CompletionProxyModel(stickerModel, 1, static_cast<size_t>(-1) / 4);
stickerModel->setParent(proxy);
return proxy;
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 15b4f523..54e3a935 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -33,6 +33,7 @@ class ColorImageProvider;
class UserSettings;
class ChatPage;
class DeviceVerificationFlow;
+class ImagePackListModel;
class TimelineViewManager : public QObject
{
@@ -57,6 +58,7 @@ public:
Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
bool isWindowFocused() const { return isWindowFocused_; }
Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId);
+ Q_INVOKABLE void openImagePackSettings(QString roomid);
Q_INVOKABLE QColor userColor(QString id, QColor background);
Q_INVOKABLE QString escapeEmoji(QString str) const;
Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); }
@@ -64,9 +66,12 @@ public:
Q_INVOKABLE QString userPresence(QString id) const;
Q_INVOKABLE QString userStatus(QString id) const;
+ Q_INVOKABLE void openRoomMembers(QString room_id);
+ Q_INVOKABLE void openRoomSettings(QString room_id);
+ Q_INVOKABLE void openInviteUsers(QString roomId);
+ Q_INVOKABLE void openGlobalUserProfile(QString userId);
+
Q_INVOKABLE void focusMessageInput();
- Q_INVOKABLE void openInviteUsersDialog();
- Q_INVOKABLE void openMemberListDialog(QString roomid) const;
Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const;
Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
@@ -81,11 +86,17 @@ signals:
void replyingEventChanged(QString replyingEvent);
void replyClosed();
void newDeviceVerificationRequest(DeviceVerificationFlow *flow);
- void inviteUsers(QStringList users);
+ void inviteUsers(QString roomId, QStringList users);
+ void showRoomList();
+ void narrowViewChanged();
void focusChanged();
void focusInput();
void openImageOverlayInternalCb(QString eventId, QImage img);
+ void openRoomMembersDialog(MemberList *members);
+ void openRoomSettingsDialog(RoomSettings *settings);
+ void openInviteUsersDialog(InviteesModel *invitees);
void openProfile(UserProfile *profile);
+ void showImagePackSettings(ImagePackListModel *packlist);
public slots:
void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
diff --git a/src/ui/Avatar.cpp b/src/ui/Avatar.cpp
deleted file mode 100644
index 154a0e2c..00000000
--- a/src/ui/Avatar.cpp
+++ /dev/null
@@ -1,168 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#include <QPainter>
-#include <QPainterPath>
-#include <QSettings>
-
-#include "AvatarProvider.h"
-#include "Utils.h"
-#include "ui/Avatar.h"
-
-Avatar::Avatar(QWidget *parent, int size)
- : QWidget(parent)
- , size_(size)
-{
- type_ = ui::AvatarType::Letter;
- letter_ = "A";
-
- QFont _font(font());
- _font.setPointSizeF(ui::FontSize);
- setFont(_font);
-
- QSizePolicy policy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
- setSizePolicy(policy);
-}
-
-QColor
-Avatar::textColor() const
-{
- if (!text_color_.isValid())
- return QColor("black");
-
- return text_color_;
-}
-
-QColor
-Avatar::backgroundColor() const
-{
- if (!text_color_.isValid())
- return QColor("white");
-
- return background_color_;
-}
-
-QSize
-Avatar::sizeHint() const
-{
- return QSize(size_ + 2, size_ + 2);
-}
-
-void
-Avatar::setTextColor(const QColor &color)
-{
- text_color_ = color;
-}
-
-void
-Avatar::setBackgroundColor(const QColor &color)
-{
- background_color_ = color;
-}
-
-void
-Avatar::setLetter(const QString &letter)
-{
- letter_ = letter;
- type_ = ui::AvatarType::Letter;
- update();
-}
-
-void
-Avatar::setImage(const QString &avatar_url)
-{
- avatar_url_ = avatar_url;
- AvatarProvider::resolve(avatar_url,
- static_cast<int>(size_ * pixmap_.devicePixelRatio()),
- this,
- [this, requestedRatio = pixmap_.devicePixelRatio()](QPixmap pm) {
- if (pm.isNull())
- return;
- type_ = ui::AvatarType::Image;
- pixmap_ = pm;
- pixmap_.setDevicePixelRatio(requestedRatio);
- update();
- });
-}
-
-void
-Avatar::setImage(const QString &room, const QString &user)
-{
- room_ = room;
- user_ = user;
- AvatarProvider::resolve(room,
- user,
- static_cast<int>(size_ * pixmap_.devicePixelRatio()),
- this,
- [this, requestedRatio = pixmap_.devicePixelRatio()](QPixmap pm) {
- if (pm.isNull())
- return;
- type_ = ui::AvatarType::Image;
- pixmap_ = pm;
- pixmap_.setDevicePixelRatio(requestedRatio);
- update();
- });
-}
-
-void
-Avatar::setDevicePixelRatio(double ratio)
-{
- if (type_ == ui::AvatarType::Image && abs(pixmap_.devicePixelRatio() - ratio) > 0.01) {
- pixmap_ = pixmap_.scaled(QSize(size_, size_) * ratio);
- pixmap_.setDevicePixelRatio(ratio);
-
- if (!avatar_url_.isEmpty())
- setImage(avatar_url_);
- else
- setImage(room_, user_);
- }
-}
-
-void
-Avatar::paintEvent(QPaintEvent *)
-{
- bool rounded = QSettings().value(QStringLiteral("user/avatar_circles"), true).toBool();
-
- QPainter painter(this);
-
- painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform |
- QPainter::TextAntialiasing);
-
- QRectF r = rect();
- const int hs = size_ / 2;
-
- if (type_ != ui::AvatarType::Image) {
- QBrush brush;
- brush.setStyle(Qt::SolidPattern);
- brush.setColor(backgroundColor());
-
- painter.setPen(Qt::NoPen);
- painter.setBrush(brush);
- rounded ? painter.drawEllipse(r) : painter.drawRoundedRect(r, 3, 3);
- } else if (painter.isActive()) {
- setDevicePixelRatio(painter.device()->devicePixelRatioF());
- }
-
- switch (type_) {
- case ui::AvatarType::Image: {
- QPainterPath ppath;
-
- rounded ? ppath.addEllipse(width() / 2 - hs, height() / 2 - hs, size_, size_)
- : ppath.addRoundedRect(r, 3, 3);
-
- painter.setClipPath(ppath);
- painter.drawPixmap(QRect(width() / 2 - hs, height() / 2 - hs, size_, size_),
- pixmap_);
- break;
- }
- case ui::AvatarType::Letter: {
- painter.setPen(textColor());
- painter.setBrush(Qt::NoBrush);
- painter.drawText(r.translated(0, -1), Qt::AlignCenter, letter_);
- break;
- }
- default:
- break;
- }
-}
diff --git a/src/ui/Avatar.h b/src/ui/Avatar.h
deleted file mode 100644
index bbf05be3..00000000
--- a/src/ui/Avatar.h
+++ /dev/null
@@ -1,48 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-#pragma once
-
-#include <QImage>
-#include <QPixmap>
-#include <QWidget>
-
-#include "Theme.h"
-
-class Avatar : public QWidget
-{
- Q_OBJECT
-
- Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor)
- Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
-
-public:
- explicit Avatar(QWidget *parent = nullptr, int size = ui::AvatarSize);
-
- void setBackgroundColor(const QColor &color);
- void setImage(const QString &avatar_url);
- void setImage(const QString &room, const QString &user);
- void setLetter(const QString &letter);
- void setTextColor(const QColor &color);
- void setDevicePixelRatio(double ratio);
-
- QColor backgroundColor() const;
- QColor textColor() const;
-
- QSize sizeHint() const override;
-
-protected:
- void paintEvent(QPaintEvent *event) override;
-
-private:
- void init();
-
- ui::AvatarType type_;
- QString letter_;
- QString avatar_url_, room_, user_;
- QColor background_color_;
- QColor text_color_;
- QPixmap pixmap_;
- int size_;
-};
diff --git a/src/ui/InfoMessage.cpp b/src/ui/InfoMessage.cpp
index fb3b306a..ebe0e63f 100644
--- a/src/ui/InfoMessage.cpp
+++ b/src/ui/InfoMessage.cpp
@@ -29,13 +29,7 @@ InfoMessage::InfoMessage(QString msg, QWidget *parent)
initFont();
QFontMetrics fm{font()};
-#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
- // width deprecated in 5.13
- width_ = fm.width(msg_) + HPadding * 2;
-#else
- width_ = fm.horizontalAdvance(msg_) + HPadding * 2;
-#endif
-
+ width_ = fm.horizontalAdvance(msg_) + HPadding * 2;
height_ = fm.ascent() + 2 * VPadding;
setFixedHeight(height_ + 2 * HMargin);
@@ -77,12 +71,7 @@ DateSeparator::DateSeparator(QDateTime datetime, QWidget *parent)
msg_ = datetime.date().toString(fmt);
QFontMetrics fm{font()};
-#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
- // width deprecated in 5.13
- width_ = fm.width(msg_) + HPadding * 2;
-#else
- width_ = fm.horizontalAdvance(msg_) + HPadding * 2;
-#endif
+ width_ = fm.horizontalAdvance(msg_) + HPadding * 2;
height_ = fm.ascent() + 2 * VPadding;
setFixedHeight(height_ + 2 * HMargin);
diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp
index fea10839..9e0d706b 100644
--- a/src/ui/NhekoGlobalObject.cpp
+++ b/src/ui/NhekoGlobalObject.cpp
@@ -6,6 +6,7 @@
#include <QDesktopServices>
#include <QUrl>
+#include <QWindow>
#include "Cache_p.h"
#include "ChatPage.h"
@@ -140,3 +141,9 @@ Nheko::openJoinRoomDialog() const
MainWindow::instance()->openJoinRoomDialog(
[](const QString &room_id) { ChatPage::instance()->joinRoom(room_id); });
}
+
+void
+Nheko::reparent(QWindow *win) const
+{
+ win->setTransientParent(MainWindow::instance()->windowHandle());
+}
diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h
index 14135fd1..d4d119dc 100644
--- a/src/ui/NhekoGlobalObject.h
+++ b/src/ui/NhekoGlobalObject.h
@@ -4,12 +4,15 @@
#pragma once
+#include <QFontDatabase>
#include <QObject>
#include <QPalette>
#include "Theme.h"
#include "UserProfile.h"
+class QWindow;
+
class Nheko : public QObject
{
Q_OBJECT
@@ -38,12 +41,17 @@ public:
int paddingLarge() const { return 20; }
UserProfile *currentUser() const;
+ Q_INVOKABLE QFont monospaceFont() const
+ {
+ return QFontDatabase::systemFont(QFontDatabase::FixedFont);
+ }
Q_INVOKABLE void openLink(QString link) const;
Q_INVOKABLE void setStatusMessage(QString msg) const;
Q_INVOKABLE void showUserSettingsPage() const;
Q_INVOKABLE void openLogoutDialog() const;
Q_INVOKABLE void openCreateRoomDialog() const;
Q_INVOKABLE void openJoinRoomDialog() const;
+ Q_INVOKABLE void reparent(QWindow *win) const;
public slots:
void updateUserProfile();
diff --git a/src/ui/Painter.h b/src/ui/Painter.h
index 3353f0c7..9f974116 100644
--- a/src/ui/Painter.h
+++ b/src/ui/Painter.h
@@ -27,12 +27,7 @@ public:
{
QFontMetrics m(fontMetrics());
if (textWidth < 0) {
-#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
- // deprecated in 5.13:
- textWidth = m.width(text);
-#else
textWidth = m.horizontalAdvance(text);
-#endif
}
drawText((outerw - x - textWidth), y + m.ascent(), text);
}
diff --git a/src/ui/RoomSettings.cpp b/src/ui/RoomSettings.cpp
index f78ef09b..fcba8205 100644
--- a/src/ui/RoomSettings.cpp
+++ b/src/ui/RoomSettings.cpp
@@ -291,19 +291,6 @@ RoomSettings::accessJoinRules()
return accessRules_;
}
-bool
-RoomSettings::respondsToKeyRequests()
-{
- return usesEncryption_ && utils::respondsToKeyRequests(roomid_);
-}
-
-void
-RoomSettings::changeKeyRequestsPreference(bool isOn)
-{
- utils::setKeyRequestsPreference(roomid_, isOn);
- emit keyRequestsChanged();
-}
-
void
RoomSettings::enableEncryption()
{
diff --git a/src/ui/RoomSettings.h b/src/ui/RoomSettings.h
index 367f3111..1c8b47d6 100644
--- a/src/ui/RoomSettings.h
+++ b/src/ui/RoomSettings.h
@@ -78,7 +78,6 @@ class RoomSettings : public QObject
Q_PROPERTY(bool canChangeJoinRules READ canChangeJoinRules CONSTANT)
Q_PROPERTY(bool canChangeNameAndTopic READ canChangeNameAndTopic CONSTANT)
Q_PROPERTY(bool isEncryptionEnabled READ isEncryptionEnabled NOTIFY encryptionChanged)
- Q_PROPERTY(bool respondsToKeyRequests READ respondsToKeyRequests NOTIFY keyRequestsChanged)
public:
RoomSettings(QString roomid, QObject *parent = nullptr);
@@ -91,7 +90,6 @@ public:
int memberCount() const;
int notifications();
int accessJoinRules();
- bool respondsToKeyRequests();
bool isLoading() const;
//! Whether the user has enough power level to send m.room.join_rules events.
bool canChangeJoinRules() const;
@@ -106,7 +104,6 @@ public:
Q_INVOKABLE void openEditModal();
Q_INVOKABLE void changeAccessRules(int index);
Q_INVOKABLE void changeNotifications(int currentIndex);
- Q_INVOKABLE void changeKeyRequestsPreference(bool isOn);
signals:
void loadingChanged();
@@ -114,7 +111,6 @@ signals:
void roomTopicChanged();
void avatarUrlChanged();
void encryptionChanged();
- void keyRequestsChanged();
void notificationsChanged();
void accessJoinRulesChanged();
void displayError(const QString &errorMessage);
@@ -136,4 +132,4 @@ private:
RoomInfo info_;
int notifications_ = 0;
int accessRules_ = 0;
-};
\ No newline at end of file
+};
|