diff --git a/src/Cache.cpp b/src/Cache.cpp
index 9304db0e..d651b182 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -78,6 +78,8 @@ constexpr auto ENCRYPTED_ROOMS_DB("encrypted_rooms");
constexpr auto INBOUND_MEGOLM_SESSIONS_DB("inbound_megolm_sessions");
//! MegolmSessionIndex -> pickled OlmOutboundGroupSession
constexpr auto OUTBOUND_MEGOLM_SESSIONS_DB("outbound_megolm_sessions");
+//! MegolmSessionIndex -> session data about which devices have access to this
+constexpr auto MEGOLM_SESSIONS_DATA_DB("megolm_sessions_data_db");
using CachedReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
using Receipts = std::map<std::string, std::map<std::string, uint64_t>>;
@@ -284,6 +286,7 @@ Cache::setup()
// Session management
inboundMegolmSessionDb_ = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
+ megolmSessionDataDb_ = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE);
txn.commit();
@@ -387,9 +390,14 @@ Cache::importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys)
index.session_id = s.session_id;
index.sender_key = s.sender_key;
+ GroupSessionData data{};
+ data.forwarding_curve25519_key_chain = s.forwarding_curve25519_key_chain;
+ if (s.sender_claimed_keys.count("ed25519"))
+ data.sender_claimed_ed25519_key = s.sender_claimed_keys.at("ed25519");
+
auto exported_session = mtx::crypto::import_session(s.session_key);
- saveInboundMegolmSession(index, std::move(exported_session));
+ saveInboundMegolmSession(index, std::move(exported_session), data);
ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id);
}
}
@@ -400,7 +408,8 @@ Cache::importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys)
void
Cache::saveInboundMegolmSession(const MegolmSessionIndex &index,
- mtx::crypto::InboundGroupSessionPtr session)
+ mtx::crypto::InboundGroupSessionPtr session,
+ const GroupSessionData &data)
{
using namespace mtx::crypto;
const auto key = json(index).dump();
@@ -420,6 +429,7 @@ Cache::saveInboundMegolmSession(const MegolmSessionIndex &index,
}
inboundMegolmSessionDb_.put(txn, key, pickled);
+ megolmSessionDataDb_.put(txn, key, json(data).dump());
txn.commit();
}
@@ -464,7 +474,7 @@ Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index)
void
Cache::updateOutboundMegolmSession(const std::string &room_id,
- const OutboundGroupSessionData &data_,
+ const GroupSessionData &data_,
mtx::crypto::OutboundGroupSessionPtr &ptr)
{
using namespace mtx::crypto;
@@ -472,18 +482,20 @@ Cache::updateOutboundMegolmSession(const std::string &room_id,
if (!outboundMegolmSessionExists(room_id))
return;
- OutboundGroupSessionData data = data_;
- data.message_index = olm_outbound_group_session_message_index(ptr.get());
- data.session_id = mtx::crypto::session_id(ptr.get());
- data.session_key = mtx::crypto::session_key(ptr.get());
+ GroupSessionData data = data_;
+ data.message_index = olm_outbound_group_session_message_index(ptr.get());
+ MegolmSessionIndex index;
+ index.room_id = room_id;
+ index.sender_key = olm::client()->identity_keys().ed25519;
+ index.session_id = mtx::crypto::session_id(ptr.get());
// Save the updated pickled data for the session.
json j;
- j["data"] = data;
j["session"] = pickle<OutboundSessionObject>(ptr.get(), SECRET);
auto txn = lmdb::txn::begin(env_);
outboundMegolmSessionDb_.put(txn, room_id, j.dump());
+ megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump());
txn.commit();
}
@@ -498,24 +510,32 @@ Cache::dropOutboundMegolmSession(const std::string &room_id)
{
auto txn = lmdb::txn::begin(env_);
outboundMegolmSessionDb_.del(txn, room_id);
+ // don't delete session data, so that we can still share the session.
txn.commit();
}
}
void
Cache::saveOutboundMegolmSession(const std::string &room_id,
- const OutboundGroupSessionData &data,
+ const GroupSessionData &data_,
mtx::crypto::OutboundGroupSessionPtr &session)
{
using namespace mtx::crypto;
const auto pickled = pickle<OutboundSessionObject>(session.get(), SECRET);
+ GroupSessionData data = data_;
+ data.message_index = olm_outbound_group_session_message_index(session.get());
+ MegolmSessionIndex index;
+ index.room_id = room_id;
+ index.sender_key = olm::client()->identity_keys().ed25519;
+ index.session_id = mtx::crypto::session_id(session.get());
+
json j;
- j["data"] = data;
j["session"] = pickled;
auto txn = lmdb::txn::begin(env_);
outboundMegolmSessionDb_.put(txn, room_id, j.dump());
+ megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump());
txn.commit();
}
@@ -544,8 +564,17 @@ Cache::getOutboundMegolmSession(const std::string &room_id)
auto obj = json::parse(value);
OutboundGroupSessionDataRef ref{};
- ref.data = obj.at("data").get<OutboundGroupSessionData>();
ref.session = unpickle<OutboundSessionObject>(obj.at("session"), SECRET);
+
+ MegolmSessionIndex index;
+ index.room_id = room_id;
+ index.sender_key = olm::client()->identity_keys().ed25519;
+ index.session_id = mtx::crypto::session_id(ref.session.get());
+
+ if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) {
+ ref.data = nlohmann::json::parse(value).get<GroupSessionData>();
+ }
+
return ref;
} catch (std::exception &e) {
nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what());
@@ -553,6 +582,25 @@ Cache::getOutboundMegolmSession(const std::string &room_id)
}
}
+std::optional<GroupSessionData>
+Cache::getMegolmSessionData(const MegolmSessionIndex &index)
+{
+ try {
+ using namespace mtx::crypto;
+
+ auto txn = ro_txn(env_);
+
+ std::string_view value;
+ if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) {
+ return nlohmann::json::parse(value).get<GroupSessionData>();
+ }
+
+ return std::nullopt;
+ } catch (std::exception &e) {
+ nhlog::db()->error("Failed to retrieve Megolm Session Data: {}", e.what());
+ return std::nullopt;
+ }
+}
//
// OLM sessions.
//
@@ -829,6 +877,7 @@ Cache::deleteData()
lmdb::dbi_close(env_, inboundMegolmSessionDb_);
lmdb::dbi_close(env_, outboundMegolmSessionDb_);
+ lmdb::dbi_close(env_, megolmSessionDataDb_);
env_.close();
@@ -3333,6 +3382,86 @@ Cache::getChildRoomIds(const std::string &room_id)
return roomids;
}
+std::vector<ImagePackInfo>
+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,
+ 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;
+ 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() &&
+ (stickers ? !img.second.is_sticker() : !img.second.is_emoji()))
+ continue;
+
+ info.pack.images.insert(img);
+ }
+
+ if (!info.pack.images.empty())
+ infos.push_back(std::move(info));
+ }
+ };
+
+ // packs from account data
+ if (auto accountpack =
+ getAccountData(txn, mtx::events::EventType::ImagePackInAccountData, "")) {
+ auto tmp =
+ std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePack>>(
+ &*accountpack);
+ if (tmp)
+ addPack(tmp->content, "", "");
+ }
+
+ // packs from rooms, that were enabled globally
+ if (auto roomPacks = getAccountData(txn, mtx::events::EventType::ImagePackRooms, "")) {
+ auto tmp =
+ std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>(
+ &*roomPacks);
+ if (tmp) {
+ for (const auto &[room_id2, state_to_d] : tmp->content.rooms) {
+ // don't add stickers from this room twice
+ if (room_id2 == room_id)
+ continue;
+
+ for (const auto &[state_id, d] : state_to_d) {
+ (void)d;
+ if (auto pack =
+ getStateEvent<mtx::events::msc2545::ImagePack>(
+ txn, room_id2, state_id))
+ addPack(pack->content, room_id2, state_id);
+ }
+ }
+ }
+ }
+
+ // packs from current room
+ if (auto pack = getStateEvent<mtx::events::msc2545::ImagePack>(txn, room_id)) {
+ addPack(pack->content, room_id, "");
+ }
+ for (const auto &pack :
+ getStateEventsWithType<mtx::events::msc2545::ImagePack>(txn, room_id)) {
+ 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)
{
@@ -3525,6 +3654,7 @@ to_json(json &j, const UserKeyCache &info)
{
j["device_keys"] = info.device_keys;
j["seen_device_keys"] = info.seen_device_keys;
+ j["seen_device_ids"] = info.seen_device_ids;
j["master_keys"] = info.master_keys;
j["master_key_changed"] = info.master_key_changed;
j["user_signing_keys"] = info.user_signing_keys;
@@ -3538,6 +3668,7 @@ from_json(const json &j, UserKeyCache &info)
{
info.device_keys = j.value("device_keys", std::map<std::string, mtx::crypto::DeviceKeys>{});
info.seen_device_keys = j.value("seen_device_keys", std::set<std::string>{});
+ info.seen_device_ids = j.value("seen_device_ids", std::set<std::string>{});
info.master_keys = j.value("master_keys", mtx::crypto::CrossSigningKeys{});
info.master_key_changed = j.value("master_key_changed", false);
info.user_signing_keys = j.value("user_signing_keys", mtx::crypto::CrossSigningKeys{});
@@ -3634,6 +3765,15 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
keyReused = true;
break;
}
+ if (updateToWrite.seen_device_ids.count(
+ device_id)) {
+ nhlog::crypto()->warn(
+ "device_id '{}' reused by ({})",
+ device_id,
+ user);
+ keyReused = true;
+ break;
+ }
}
if (!keyReused && !oldDeviceKeys.count(device_id))
@@ -3644,6 +3784,7 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
(void)key_id;
updateToWrite.seen_device_keys.insert(key);
}
+ updateToWrite.seen_device_ids.insert(device_id);
}
}
db.put(txn, user, json(updateToWrite).dump());
@@ -4077,17 +4218,15 @@ from_json(const json &j, MemberInfo &info)
}
void
-to_json(nlohmann::json &obj, const DeviceAndMasterKeys &msg)
+to_json(nlohmann::json &obj, const DeviceKeysToMsgIndex &msg)
{
- obj["devices"] = msg.devices;
- obj["master_keys"] = msg.master_keys;
+ obj["deviceids"] = msg.deviceids;
}
void
-from_json(const nlohmann::json &obj, DeviceAndMasterKeys &msg)
+from_json(const nlohmann::json &obj, DeviceKeysToMsgIndex &msg)
{
- msg.devices = obj.at("devices").get<decltype(msg.devices)>();
- msg.master_keys = obj.at("master_keys").get<decltype(msg.master_keys)>();
+ msg.deviceids = obj.at("deviceids").get<decltype(msg.deviceids)>();
}
void
@@ -4099,30 +4238,31 @@ to_json(nlohmann::json &obj, const SharedWithUsers &msg)
void
from_json(const nlohmann::json &obj, SharedWithUsers &msg)
{
- msg.keys = obj.at("keys").get<std::map<std::string, DeviceAndMasterKeys>>();
+ msg.keys = obj.at("keys").get<std::map<std::string, DeviceKeysToMsgIndex>>();
}
void
-to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg)
+to_json(nlohmann::json &obj, const GroupSessionData &msg)
{
- obj["session_id"] = msg.session_id;
- obj["session_key"] = msg.session_key;
obj["message_index"] = msg.message_index;
obj["ts"] = msg.timestamp;
- obj["initially"] = msg.initially;
+ obj["sender_claimed_ed25519_key"] = msg.sender_claimed_ed25519_key;
+ obj["forwarding_curve25519_key_chain"] = msg.forwarding_curve25519_key_chain;
+
obj["currently"] = msg.currently;
}
void
-from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg)
+from_json(const nlohmann::json &obj, GroupSessionData &msg)
{
- msg.session_id = obj.at("session_id");
- msg.session_key = obj.at("session_key");
msg.message_index = obj.at("message_index");
msg.timestamp = obj.value("ts", 0ULL);
- msg.initially = obj.value("initially", SharedWithUsers{});
+ msg.sender_claimed_ed25519_key = obj.value("sender_claimed_ed25519_key", "");
+ msg.forwarding_curve25519_key_chain =
+ obj.value("forwarding_curve25519_key_chain", std::vector<std::string>{});
+
msg.currently = obj.value("currently", SharedWithUsers{});
}
@@ -4522,7 +4662,7 @@ isRoomMember(const std::string &user_id, const std::string &room_id)
//
void
saveOutboundMegolmSession(const std::string &room_id,
- const OutboundGroupSessionData &data,
+ const GroupSessionData &data,
mtx::crypto::OutboundGroupSessionPtr &session)
{
instance_->saveOutboundMegolmSession(room_id, data, session);
@@ -4539,7 +4679,7 @@ outboundMegolmSessionExists(const std::string &room_id) noexcept
}
void
updateOutboundMegolmSession(const std::string &room_id,
- const OutboundGroupSessionData &data,
+ const GroupSessionData &data,
mtx::crypto::OutboundGroupSessionPtr &session)
{
instance_->updateOutboundMegolmSession(room_id, data, session);
@@ -4566,9 +4706,10 @@ exportSessionKeys()
//
void
saveInboundMegolmSession(const MegolmSessionIndex &index,
- mtx::crypto::InboundGroupSessionPtr session)
+ mtx::crypto::InboundGroupSessionPtr session,
+ const GroupSessionData &data)
{
- instance_->saveInboundMegolmSession(index, std::move(session));
+ instance_->saveInboundMegolmSession(index, std::move(session), data);
}
mtx::crypto::InboundGroupSessionPtr
getInboundMegolmSession(const MegolmSessionIndex &index)
@@ -4580,6 +4721,11 @@ inboundMegolmSessionExists(const MegolmSessionIndex &index)
{
return instance_->inboundMegolmSessionExists(index);
}
+std::optional<GroupSessionData>
+getMegolmSessionData(const MegolmSessionIndex &index)
+{
+ return instance_->getMegolmSessionData(index);
+}
//
// Olm Sessions
diff --git a/src/Cache.h b/src/Cache.h
index b0520f6b..57a36d73 100644
--- a/src/Cache.h
+++ b/src/Cache.h
@@ -200,7 +200,7 @@ isRoomMember(const std::string &user_id, const std::string &room_id);
//
void
saveOutboundMegolmSession(const std::string &room_id,
- const OutboundGroupSessionData &data,
+ const GroupSessionData &data,
mtx::crypto::OutboundGroupSessionPtr &session);
OutboundGroupSessionDataRef
getOutboundMegolmSession(const std::string &room_id);
@@ -208,7 +208,7 @@ bool
outboundMegolmSessionExists(const std::string &room_id) noexcept;
void
updateOutboundMegolmSession(const std::string &room_id,
- const OutboundGroupSessionData &data,
+ const GroupSessionData &data,
mtx::crypto::OutboundGroupSessionPtr &session);
void
dropOutboundMegolmSession(const std::string &room_id);
@@ -223,11 +223,14 @@ exportSessionKeys();
//
void
saveInboundMegolmSession(const MegolmSessionIndex &index,
- mtx::crypto::InboundGroupSessionPtr session);
+ mtx::crypto::InboundGroupSessionPtr session,
+ const GroupSessionData &data);
mtx::crypto::InboundGroupSessionPtr
getInboundMegolmSession(const MegolmSessionIndex &index);
bool
inboundMegolmSessionExists(const MegolmSessionIndex &index);
+std::optional<GroupSessionData>
+getMegolmSessionData(const MegolmSessionIndex &index);
//
// Olm Sessions
diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h
index 07ca274e..409c9d67 100644
--- a/src/CacheCryptoStructs.h
+++ b/src/CacheCryptoStructs.h
@@ -27,40 +27,43 @@ enum Trust
Q_ENUM_NS(Trust)
}
-struct DeviceAndMasterKeys
+struct DeviceKeysToMsgIndex
{
- // map from device id or master key id to message_index
- std::map<std::string, uint64_t> devices, master_keys;
+ // map from device key to message_index
+ // Using the device id is safe because we check for reuse on device list updates
+ // Using the device id makes our logic much easier to read.
+ std::map<std::string, uint64_t> deviceids;
};
struct SharedWithUsers
{
// userid to keys
- std::map<std::string, DeviceAndMasterKeys> keys;
+ std::map<std::string, DeviceKeysToMsgIndex> keys;
};
// Extra information associated with an outbound megolm session.
-struct OutboundGroupSessionData
+struct GroupSessionData
{
- std::string session_id;
- std::string session_key;
uint64_t message_index = 0;
uint64_t timestamp = 0;
+ std::string sender_claimed_ed25519_key;
+ std::vector<std::string> forwarding_curve25519_key_chain;
+
// who has access to this session.
// Rotate, when a user leaves the room and share, when a user gets added.
- SharedWithUsers initially, currently;
+ SharedWithUsers currently;
};
void
-to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg);
+to_json(nlohmann::json &obj, const GroupSessionData &msg);
void
-from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg);
+from_json(const nlohmann::json &obj, GroupSessionData &msg);
struct OutboundGroupSessionDataRef
{
mtx::crypto::OutboundGroupSessionPtr session;
- OutboundGroupSessionData data;
+ GroupSessionData data;
};
struct DevicePublicKeys
@@ -134,6 +137,8 @@ struct UserKeyCache
bool master_key_changed = false;
//! Device keys that were already used at least once
std::set<std::string> seen_device_keys;
+ //! Device ids that were already used at least once
+ std::set<std::string> seen_device_ids;
};
void
diff --git a/src/CacheStructs.h b/src/CacheStructs.h
index 28c70055..4a5c5c76 100644
--- a/src/CacheStructs.h
+++ b/src/CacheStructs.h
@@ -11,6 +11,7 @@
#include <string>
#include <mtx/events/join_rules.hpp>
+#include <mtx/events/mscs/image_packs.hpp>
namespace cache {
enum class CacheVersion : int
@@ -109,3 +110,10 @@ struct RoomSearchResult
std::string room_id;
RoomInfo info;
};
+
+struct ImagePackInfo
+{
+ 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 c76cc717..c9d42202 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -97,6 +97,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,6 +231,9 @@ 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,
+ std::optional<bool> stickers);
+
//! Mark a room that uses e2e encryption.
void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id);
bool isRoomEncrypted(const std::string &room_id);
@@ -238,12 +247,12 @@ public:
// Outbound Megolm Sessions
//
void saveOutboundMegolmSession(const std::string &room_id,
- const OutboundGroupSessionData &data,
+ const GroupSessionData &data,
mtx::crypto::OutboundGroupSessionPtr &session);
OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id);
bool outboundMegolmSessionExists(const std::string &room_id) noexcept;
void updateOutboundMegolmSession(const std::string &room_id,
- const OutboundGroupSessionData &data,
+ const GroupSessionData &data,
mtx::crypto::OutboundGroupSessionPtr &session);
void dropOutboundMegolmSession(const std::string &room_id);
@@ -254,10 +263,12 @@ public:
// Inbound Megolm Sessions
//
void saveInboundMegolmSession(const MegolmSessionIndex &index,
- mtx::crypto::InboundGroupSessionPtr session);
+ mtx::crypto::InboundGroupSessionPtr session,
+ const GroupSessionData &data);
mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(
const MegolmSessionIndex &index);
bool inboundMegolmSessionExists(const MegolmSessionIndex &index);
+ std::optional<GroupSessionData> getMegolmSessionData(const MegolmSessionIndex &index);
//
// Olm Sessions
@@ -676,6 +687,7 @@ private:
lmdb::dbi inboundMegolmSessionDb_;
lmdb::dbi outboundMegolmSessionDb_;
+ lmdb::dbi megolmSessionDataDb_;
QString localUserId_;
QString cacheDirectory_;
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 6003eb85..615a15b3 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -116,29 +116,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,27 +929,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));
+ nhlog::crypto()->info(
+ "uploading {} {} keys", nkeys, mtx::crypto::SIGNED_CURVE25519);
+ olm::client()->generate_one_time_keys(nkeys);
+
+ 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));
+
+ if (err->status_code < 400 || err->status_code >= 500)
return;
- }
+ }
- olm::mark_keys_as_published();
- });
- }
+ // mark as published anyway, otherwise we may end up in a loop.
+ olm::mark_keys_as_published();
+ });
}
}
diff --git a/src/CombinedImagePackModel.cpp b/src/CombinedImagePackModel.cpp
new file mode 100644
index 00000000..341a34ec
--- /dev/null
+++ b/src/CombinedImagePackModel.cpp
@@ -0,0 +1,77 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "CombinedImagePackModel.h"
+
+#include "Cache_p.h"
+#include "CompletionModelRoles.h"
+
+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 =
+ pack.pack.pack ? QString::fromStdString(pack.pack.pack->display_name) : "";
+
+ for (const auto &img : pack.pack.images) {
+ ImageDesc i{};
+ i.shortcode = QString::fromStdString(img.first);
+ i.packname = packname;
+ i.image = img.second;
+ images.push_back(std::move(i));
+ }
+ }
+}
+
+int
+CombinedImagePackModel::rowCount(const QModelIndex &) const
+{
+ return (int)images.size();
+}
+
+QHash<int, QByteArray>
+CombinedImagePackModel::roleNames() const
+{
+ return {
+ {CompletionModel::CompletionRole, "completionRole"},
+ {CompletionModel::SearchRole, "searchRole"},
+ {CompletionModel::SearchRole2, "searchRole2"},
+ {Roles::Url, "url"},
+ {Roles::ShortCode, "shortcode"},
+ {Roles::Body, "body"},
+ {Roles::PackName, "packname"},
+ {Roles::OriginalRow, "originalRow"},
+ };
+}
+
+QVariant
+CombinedImagePackModel::data(const QModelIndex &index, int role) const
+{
+ if (hasIndex(index.row(), index.column(), index.parent())) {
+ switch (role) {
+ case CompletionModel::CompletionRole:
+ return QString::fromStdString(images[index.row()].image.url);
+ case Roles::Url:
+ return QString::fromStdString(images[index.row()].image.url);
+ case CompletionModel::SearchRole:
+ case Roles::ShortCode:
+ return images[index.row()].shortcode;
+ case CompletionModel::SearchRole2:
+ case Roles::Body:
+ return QString::fromStdString(images[index.row()].image.body);
+ case Roles::PackName:
+ return images[index.row()].packname;
+ case Roles::OriginalRow:
+ return index.row();
+ default:
+ return {};
+ }
+ }
+ return {};
+}
diff --git a/src/CombinedImagePackModel.h b/src/CombinedImagePackModel.h
new file mode 100644
index 00000000..f0f69799
--- /dev/null
+++ b/src/CombinedImagePackModel.h
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QAbstractListModel>
+
+#include <mtx/events/mscs/image_packs.hpp>
+
+class CombinedImagePackModel : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ enum Roles
+ {
+ Url = Qt::UserRole,
+ ShortCode,
+ Body,
+ PackName,
+ OriginalRow,
+ };
+
+ 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;
+
+ mtx::events::msc2545::PackImage imageAt(int row)
+ {
+ if (row < 0 || static_cast<size_t>(row) >= images.size())
+ return {};
+ return images.at(static_cast<size_t>(row)).image;
+ }
+
+private:
+ std::string room_id;
+
+ struct ImageDesc
+ {
+ QString shortcode;
+ QString packname;
+
+ mtx::events::msc2545::PackImage image;
+ };
+
+ std::vector<ImageDesc> images;
+};
diff --git a/src/ImagePackListModel.cpp b/src/ImagePackListModel.cpp
new file mode 100644
index 00000000..89f1f68e
--- /dev/null
+++ b/src/ImagePackListModel.cpp
@@ -0,0 +1,76 @@
+// 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;
+}
diff --git a/src/ImagePackListModel.h b/src/ImagePackListModel.h
new file mode 100644
index 00000000..0a044690
--- /dev/null
+++ b/src/ImagePackListModel.h
@@ -0,0 +1,37 @@
+// 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
+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);
+
+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/MainWindow.cpp b/src/MainWindow.cpp
index ed337ca4..c0486d01 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,11 +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 +310,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 +333,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);
diff --git a/src/MainWindow.h b/src/MainWindow.h
index 3571f079..6d62545c 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -65,7 +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();
diff --git a/src/MemberList.cpp b/src/MemberList.cpp
new file mode 100644
index 00000000..0ef3b696
--- /dev/null
+++ b/src/MemberList.cpp
@@ -0,0 +1,111 @@
+// 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 "MemberList.h"
+
+#include "Cache.h"
+#include "ChatPage.h"
+#include "Config.h"
+#include "Logging.h"
+#include "Utils.h"
+#include "timeline/TimelineViewManager.h"
+#include "ui/Avatar.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..9932f6a4
--- /dev/null
+++ b/src/MemberList.h
@@ -0,0 +1,66 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include "CacheStructs.h"
+#include <QAbstractListModel>
+
+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..ab0f8152 100644
--- a/src/MxcImageProvider.cpp
+++ b/src/MxcImageProvider.cpp
@@ -196,7 +196,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/Olm.cpp b/src/Olm.cpp
index 4a403cd0..69503e6e 100644
--- a/src/Olm.cpp
+++ b/src/Olm.cpp
@@ -123,7 +123,17 @@ handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEven
if (msg_type == to_string(mtx::events::EventType::RoomEncrypted)) {
try {
olm::OlmMessage olm_msg = j_msg;
- handle_olm_message(std::move(olm_msg));
+ cache::client()->query_keys(
+ olm_msg.sender,
+ [olm_msg](const UserKeyCache &userKeys, mtx::http::RequestErr e) {
+ if (e) {
+ nhlog::crypto()->error(
+ "Failed to query user keys, dropping olm "
+ "message");
+ return;
+ }
+ handle_olm_message(std::move(olm_msg), userKeys);
+ });
} catch (const nlohmann::json::exception &e) {
nhlog::crypto()->warn(
"parsing error for olm message: {} {}", e.what(), j_msg.dump(2));
@@ -197,13 +207,15 @@ handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEven
}
void
-handle_olm_message(const OlmMessage &msg)
+handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKeys)
{
nhlog::crypto()->info("sender : {}", msg.sender);
nhlog::crypto()->info("sender_key: {}", msg.sender_key);
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) {
@@ -224,6 +236,7 @@ handle_olm_message(const OlmMessage &msg)
msg.sender, msg.sender_key, cipher.second);
} else {
nhlog::crypto()->error("Undecryptable olm message!");
+ failed_decryption = true;
continue;
}
}
@@ -231,6 +244,57 @@ handle_olm_message(const OlmMessage &msg)
if (!payload.is_null()) {
mtx::events::collections::DeviceEvents device_event;
+ // Other properties are included in order to prevent an attacker from
+ // publishing someone else's curve25519 keys as their own and subsequently
+ // claiming to have sent messages which they didn't. sender must correspond
+ // to the user who sent the event, recipient to the local user, and
+ // recipient_keys to the local ed25519 key.
+ std::string receiver_ed25519 = payload["recipient_keys"]["ed25519"];
+ if (receiver_ed25519.empty() ||
+ receiver_ed25519 != olm::client()->identity_keys().ed25519) {
+ nhlog::crypto()->warn(
+ "Decrypted event doesn't include our ed25519: {}",
+ payload.dump());
+ return;
+ }
+ std::string receiver = payload["recipient"];
+ if (receiver.empty() || receiver != http::client()->user_id().to_string()) {
+ nhlog::crypto()->warn(
+ "Decrypted event doesn't include our user_id: {}",
+ payload.dump());
+ return;
+ }
+
+ // Clients must confirm that the sender_key and the ed25519 field value
+ // under the keys property match the keys returned by /keys/query for the
+ // given user, and must also verify the signature of the payload. Without
+ // this check, a client cannot be sure that the sender device owns the
+ // private part of the ed25519 key it claims to have in the Olm payload.
+ // This is crucial when the ed25519 key corresponds to a verified device.
+ std::string sender_ed25519 = payload["keys"]["ed25519"];
+ if (sender_ed25519.empty()) {
+ nhlog::crypto()->warn(
+ "Decrypted event doesn't include sender ed25519: {}",
+ payload.dump());
+ return;
+ }
+
+ 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;
+ }
+ }
+ }
+ if (!from_their_device) {
+ nhlog::crypto()->warn("Decrypted event isn't sent from a device "
+ "listed by that user! {}",
+ payload.dump());
+ return;
+ }
+
{
std::string msg_type = payload["type"];
json event_array = json::array();
@@ -242,7 +306,7 @@ handle_olm_message(const OlmMessage &msg)
if (temp_events.empty()) {
nhlog::crypto()->warn("Decrypted unknown event: {}",
payload.dump());
- continue;
+ return;
}
device_event = temp_events.at(0);
}
@@ -276,17 +340,20 @@ handle_olm_message(const OlmMessage &msg)
ChatPage::instance()->receivedDeviceVerificationDone(e8->content);
} else if (auto roomKey =
std::get_if<DeviceEvent<msg::RoomKey>>(&device_event)) {
- create_inbound_megolm_session(*roomKey, msg.sender_key);
+ create_inbound_megolm_session(
+ *roomKey, msg.sender_key, sender_ed25519);
} else if (auto forwardedRoomKey =
std::get_if<DeviceEvent<msg::ForwardedRoomKey>>(
&device_event)) {
+ forwardedRoomKey->content.forwarding_curve25519_key_chain.push_back(
+ msg.sender_key);
import_inbound_megolm_session(*forwardedRoomKey);
} else if (auto e =
std::get_if<DeviceEvent<msg::SecretSend>>(&device_event)) {
auto local_user = http::client()->user_id();
if (msg.sender != local_user.to_string())
- continue;
+ return;
auto secret_name =
request_id_to_secret_name.find(e->content.request_id);
@@ -306,7 +373,7 @@ handle_olm_message(const OlmMessage &msg)
cache::verificationStatus(local_user.to_string());
if (!verificationStatus)
- continue;
+ return;
auto deviceKeys = cache::userKeys(local_user.to_string());
std::string sender_device_id;
@@ -344,7 +411,6 @@ handle_olm_message(const OlmMessage &msg)
"for secrect "
"'{}'",
name);
- return;
}
});
@@ -360,27 +426,28 @@ handle_olm_message(const OlmMessage &msg)
}
return;
+ } else {
+ failed_decryption = true;
}
}
- try {
- auto otherUserDeviceKeys = cache::userKeys(msg.sender);
-
- if (!otherUserDeviceKeys)
- return;
+ 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);
+ }
- 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());
}
}
@@ -450,7 +517,7 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
std::map<std::string, std::vector<std::string>> sendSessionTo;
mtx::crypto::OutboundGroupSessionPtr session = nullptr;
- OutboundGroupSessionData group_session_data;
+ GroupSessionData group_session_data;
if (cache::outboundMegolmSessionExists(room_id)) {
auto res = cache::getOutboundMegolmSession(room_id);
@@ -519,7 +586,8 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
} else {
// compare devices
bool device_removed = false;
- for (const auto &dev : session_member_it->second.devices) {
+ for (const auto &dev :
+ session_member_it->second.deviceids) {
if (!member_it->second ||
!member_it->second->device_keys.count(
dev.first)) {
@@ -541,7 +609,7 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
if (member_it->second)
for (const auto &dev :
member_it->second->device_keys)
- if (!session_member_it->second.devices
+ if (!session_member_it->second.deviceids
.count(dev.first) &&
(member_it->first != own_user_id ||
dev.first != device_id))
@@ -571,32 +639,28 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
const auto session_key = mtx::crypto::session_key(session.get());
// Saving the new megolm session.
- OutboundGroupSessionData session_data{};
- session_data.session_id = mtx::crypto::session_id(session.get());
- session_data.session_key = mtx::crypto::session_key(session.get());
- session_data.message_index = 0;
- session_data.timestamp = QDateTime::currentMSecsSinceEpoch();
+ GroupSessionData session_data{};
+ session_data.message_index = 0;
+ session_data.timestamp = QDateTime::currentMSecsSinceEpoch();
+ session_data.sender_claimed_ed25519_key = olm::client()->identity_keys().ed25519;
sendSessionTo.clear();
for (const auto &[user, devices] : members) {
sendSessionTo[user] = {};
- session_data.initially.keys[user] = {};
+ session_data.currently.keys[user] = {};
if (devices) {
for (const auto &[device_id_, key] : devices->device_keys) {
(void)key;
if (device_id != device_id_ || user != own_user_id) {
sendSessionTo[user].push_back(device_id_);
- session_data.initially.keys[user]
- .devices[device_id_] = 0;
+ session_data.currently.keys[user]
+ .deviceids[device_id_] = 0;
}
}
}
}
- cache::saveOutboundMegolmSession(room_id, session_data, session);
- group_session_data = std::move(session_data);
-
{
MegolmSessionIndex index;
index.room_id = room_id;
@@ -604,8 +668,12 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
index.sender_key = olm::client()->identity_keys().curve25519;
auto megolm_session =
olm::client()->init_inbound_group_session(session_key);
- cache::saveInboundMegolmSession(index, std::move(megolm_session));
+ cache::saveInboundMegolmSession(
+ index, std::move(megolm_session), session_data);
}
+
+ cache::saveOutboundMegolmSession(room_id, session_data, session);
+ group_session_data = std::move(session_data);
}
mtx::events::DeviceEvent<mtx::events::msg::RoomKey> megolm_payload{};
@@ -641,8 +709,8 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
group_session_data.currently.keys[user] = {};
for (const auto &device_id_ : devices) {
- if (!group_session_data.currently.keys[user].devices.count(device_id_))
- group_session_data.currently.keys[user].devices[device_id_] =
+ if (!group_session_data.currently.keys[user].deviceids.count(device_id_))
+ group_session_data.currently.keys[user].deviceids[device_id_] =
group_session_data.message_index;
}
}
@@ -704,7 +772,8 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip
void
create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::RoomKey> &roomKey,
- const std::string &sender_key)
+ const std::string &sender_key,
+ const std::string &sender_ed25519)
{
MegolmSessionIndex index;
index.room_id = roomKey.content.room_id;
@@ -712,9 +781,13 @@ create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::R
index.sender_key = sender_key;
try {
+ GroupSessionData data{};
+ data.forwarding_curve25519_key_chain = {sender_key};
+ data.sender_claimed_ed25519_key = sender_ed25519;
+
auto megolm_session =
olm::client()->init_inbound_group_session(roomKey.content.session_key);
- cache::saveInboundMegolmSession(index, std::move(megolm_session));
+ cache::saveInboundMegolmSession(index, std::move(megolm_session), data);
} catch (const lmdb::error &e) {
nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what());
return;
@@ -741,7 +814,13 @@ import_inbound_megolm_session(
try {
auto megolm_session =
olm::client()->import_inbound_group_session(roomKey.content.session_key);
- cache::saveInboundMegolmSession(index, std::move(megolm_session));
+
+ GroupSessionData data{};
+ data.forwarding_curve25519_key_chain =
+ roomKey.content.forwarding_curve25519_key_chain;
+ data.sender_claimed_ed25519_key = roomKey.content.sender_claimed_ed25519_key;
+
+ cache::saveInboundMegolmSession(index, std::move(megolm_session), data);
} catch (const lmdb::error &e) {
nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what());
return;
@@ -817,30 +896,33 @@ handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyR
return;
}
- // Check if we were the sender of the session being requested.
- if (req.content.sender_key != olm::client()->identity_keys().curve25519) {
- nhlog::crypto()->debug("ignoring key request {} because we were not the sender: "
- "\nrequested({}) ours({})",
- req.content.request_id,
- req.content.sender_key,
- olm::client()->identity_keys().curve25519);
+ // Check if we were the sender of the session being requested (unless it is actually us
+ // requesting the session).
+ if (req.sender != http::client()->user_id().to_string() &&
+ req.content.sender_key != olm::client()->identity_keys().curve25519) {
+ nhlog::crypto()->debug(
+ "ignoring key request {} because we did not create the requested session: "
+ "\nrequested({}) ours({})",
+ req.content.request_id,
+ req.content.sender_key,
+ olm::client()->identity_keys().curve25519);
return;
}
+ // Check that the requested session_id and the one we have saved match.
+ MegolmSessionIndex index{};
+ index.room_id = req.content.room_id;
+ index.session_id = req.content.session_id;
+ index.sender_key = req.content.sender_key;
+
// Check if we have the keys for the requested session.
- auto outboundSession = cache::getOutboundMegolmSession(req.content.room_id);
- if (!outboundSession.session) {
+ auto sessionData = cache::getMegolmSessionData(index);
+ if (!sessionData) {
nhlog::crypto()->warn("requested session not found in room: {}",
req.content.room_id);
return;
}
- // Check that the requested session_id and the one we have saved match.
- MegolmSessionIndex index{};
- index.room_id = req.content.room_id;
- index.session_id = req.content.session_id;
- index.sender_key = olm::client()->identity_keys().curve25519;
-
const auto session = cache::getInboundMegolmSession(index);
if (!session) {
nhlog::crypto()->warn("No session with id {} in db", req.content.session_id);
@@ -873,12 +955,12 @@ handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyR
bool shouldSeeKeys = false;
uint64_t minimumIndex = -1;
- if (outboundSession.data.currently.keys.count(req.sender)) {
- if (outboundSession.data.currently.keys.at(req.sender)
- .devices.count(req.content.requesting_device_id)) {
+ if (sessionData->currently.keys.count(req.sender)) {
+ if (sessionData->currently.keys.at(req.sender)
+ .deviceids.count(req.content.requesting_device_id)) {
shouldSeeKeys = true;
- minimumIndex = outboundSession.data.currently.keys.at(req.sender)
- .devices.at(req.content.requesting_device_id);
+ minimumIndex = sessionData->currently.keys.at(req.sender)
+ .deviceids.at(req.content.requesting_device_id);
}
}
@@ -906,8 +988,9 @@ handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyR
forward_key.sender_key = index.sender_key;
// TODO(Nico): Figure out if this is correct
- forward_key.sender_claimed_ed25519_key = olm::client()->identity_keys().ed25519;
- forward_key.forwarding_curve25519_key_chain = {};
+ forward_key.sender_claimed_ed25519_key = sessionData->sender_claimed_ed25519_key;
+ forward_key.forwarding_curve25519_key_chain =
+ sessionData->forwarding_curve25519_key_chain;
send_megolm_key_to_device(
req.sender, req.content.requesting_device_id, forward_key);
@@ -928,6 +1011,7 @@ send_megolm_key_to_device(const std::string &user_id,
std::map<std::string, std::vector<std::string>> targets;
targets[user_id] = {device_id};
send_encrypted_to_device_messages(targets, room_key);
+ nhlog::crypto()->debug("Forwarded key to {}:{}", user_id, device_id);
}
DecryptionResult
diff --git a/src/Olm.h b/src/Olm.h
index 8479f4f2..a18cbbfb 100644
--- a/src/Olm.h
+++ b/src/Olm.h
@@ -59,12 +59,13 @@ try_olm_decryption(const std::string &sender_key,
const mtx::events::msg::OlmCipherContent &content);
void
-handle_olm_message(const OlmMessage &msg);
+handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKeys);
//! Establish a new inbound megolm session with the decrypted payload from olm.
void
create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::RoomKey> &roomKey,
- const std::string &sender_key);
+ const std::string &sender_key,
+ const std::string &sender_ed25519);
void
import_inbound_megolm_session(
const mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> &roomKey);
diff --git a/src/SingleImagePackModel.cpp b/src/SingleImagePackModel.cpp
new file mode 100644
index 00000000..6c508da0
--- /dev/null
+++ b/src/SingleImagePackModel.cpp
@@ -0,0 +1,100 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "SingleImagePackModel.h"
+
+#include "Cache_p.h"
+#include "MatrixClient.h"
+
+SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent)
+ : QAbstractListModel(parent)
+ , roomid_(std::move(pack_.source_room))
+ , statekey_(std::move(pack_.state_key))
+ , pack(std::move(pack_.pack))
+{
+ if (!pack.pack)
+ pack.pack = mtx::events::msc2545::ImagePack::PackDescription{};
+
+ for (const auto &e : pack.images)
+ shortcodes.push_back(e.first);
+}
+
+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::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();
+ });
+}
diff --git a/src/SingleImagePackModel.h b/src/SingleImagePackModel.h
new file mode 100644
index 00000000..e0c791ba
--- /dev/null
+++ b/src/SingleImagePackModel.h
@@ -0,0 +1,61 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QAbstractListModel>
+
+#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 CONSTANT)
+ Q_PROPERTY(QString attribution READ statekey CONSTANT)
+ Q_PROPERTY(QString packname READ packname CONSTANT)
+ Q_PROPERTY(QString avatarUrl READ avatarUrl CONSTANT)
+ Q_PROPERTY(bool isStickerPack READ isStickerPack CONSTANT)
+ Q_PROPERTY(bool isEmotePack READ isEmotePack CONSTANT)
+ Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY
+ globallyEnabledChanged)
+public:
+ enum Roles
+ {
+ Url = Qt::UserRole,
+ ShortCode,
+ Body,
+ IsEmote,
+ IsSticker,
+ };
+
+ 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;
+
+ 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;
+ void setGloballyEnabled(bool enabled);
+
+signals:
+ void globallyEnabledChanged();
+
+private:
+ std::string roomid_;
+ std::string statekey_;
+
+ mtx::events::msc2545::ImagePack pack;
+ std::vector<std::string> shortcodes;
+};
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 740b8979..a062780a 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -91,7 +91,7 @@ UserSettings::load(std::optional<QString> profile)
privacyScreen_ = settings.value("user/privacy_screen", false).toBool();
privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt();
shareKeysWithTrustedUsers_ =
- settings.value("user/share_keys_with_trusted_users", true).toBool();
+ 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();
@@ -610,7 +610,8 @@ UserSettings::save()
settings.setValue("decrypt_sidebar", decryptSidebar_);
settings.setValue("privacy_screen", privacyScreen_);
settings.setValue("privacy_screen_timeout", privacyScreenTimeout_);
- settings.setValue("share_keys_with_trusted_users", shareKeysWithTrustedUsers_);
+ settings.setValue("automatically_share_keys_with_trusted_users",
+ shareKeysWithTrustedUsers_);
settings.setValue("mobile_mode", mobileMode_);
settings.setValue("font_size", baseFontSize_);
settings.setValue("typing_notifications", typingNotifications_);
@@ -1398,7 +1399,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/Utils.cpp b/src/Utils.cpp
index eabf50d9..a99831c4 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -540,7 +540,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/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/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index b0747a7c..f17081e5 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -19,6 +19,7 @@
#include "Cache.h"
#include "ChatPage.h"
+#include "CombinedImagePackModel.h"
#include "CompletionProxyModel.h"
#include "Config.h"
#include "Logging.h"
@@ -502,6 +503,31 @@ InputBar::video(const QString &filename,
}
void
+InputBar::sticker(CombinedImagePackModel *model, int row)
+{
+ if (!model || row < 0)
+ return;
+
+ auto img = model->imageAt(row);
+
+ mtx::events::msg::StickerImage sticker{};
+ sticker.info = img.info.value_or(mtx::common::ImageInfo{});
+ sticker.url = img.url;
+ sticker.body = img.body;
+
+ if (!room->reply().isEmpty()) {
+ sticker.relations.relations.push_back(
+ {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
+ }
+ if (!room->edit().isEmpty()) {
+ sticker.relations.relations.push_back(
+ {mtx::common::RelationType::Replace, room->edit().toStdString()});
+ }
+
+ room->sendMessageEvent(sticker, mtx::events::EventType::Sticker);
+}
+
+void
InputBar::command(QString command, QString args)
{
if (command == "me") {
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index c9728379..2e6fb5c0 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -12,6 +12,7 @@
#include <mtx/responses/messages.hpp>
class TimelineModel;
+class CombinedImagePackModel;
class QMimeData;
class QDropEvent;
class QStringList;
@@ -57,6 +58,7 @@ public slots:
MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED,
bool rainbowify = false);
void reaction(const QString &reactedEvent, const QString &reactionKey);
+ 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/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index ab11f99b..ee5564a5 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -25,6 +25,7 @@
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
+#include "MemberList.h"
#include "MxcImageProvider.h"
#include "Olm.h"
#include "TimelineViewManager.h"
@@ -317,6 +318,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 +327,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 +350,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),
@@ -443,6 +450,7 @@ TimelineModel::roleNames() const
{RoomTopic, "roomTopic"},
{CallType, "callType"},
{Dump, "dump"},
+ {RelatedEventCacheBuster, "relatedEventCacheBuster"},
};
}
int
@@ -676,6 +684,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
return QVariant(m);
}
+ case RelatedEventCacheBuster:
+ return relatedEventCacheBuster;
default:
return QVariant();
}
@@ -710,6 +720,14 @@ TimelineModel::data(const QModelIndex &index, int role) const
return data(*event, role);
}
+QVariant
+TimelineModel::dataById(QString id, int role, QString relatedTo)
+{
+ if (auto event = events.get(id.toStdString(), relatedTo.toStdString()))
+ return data(*event, role);
+ return QVariant();
+}
+
bool
TimelineModel::canFetchMore(const QModelIndex &) const
{
@@ -1049,14 +1067,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);
@@ -1292,6 +1302,14 @@ struct SendMessageVisitor
sendRoomEvent<mtx::events::msg::KeyVerificationCancel,
mtx::events::EventType::KeyVerificationCancel>(msg);
}
+ void operator()(mtx::events::Sticker msg)
+ {
+ msg.type = mtx::events::EventType::Sticker;
+ if (cache::isRoomEncrypted(model_->room_id_.toStdString())) {
+ model_->sendEncryptedMessage(msg, mtx::events::EventType::Sticker);
+ } else
+ emit model_->addPendingMessageToStore(msg);
+ }
TimelineModel *model_;
};
@@ -1301,6 +1319,7 @@ TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event)
{
std::visit(
[](auto &msg) {
+ // gets overwritten for reactions and stickers in SendMessageVisitor
msg.type = mtx::events::EventType::RoomMessage;
msg.event_id = "m" + http::client()->generate_txn_id();
msg.sender = http::client()->user_id().to_string();
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index a3c973d6..0e2ce153 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -17,6 +17,8 @@
#include "CacheStructs.h"
#include "EventStore.h"
#include "InputBar.h"
+#include "InviteesModel.h"
+#include "MemberList.h"
#include "Permissions.h"
#include "ui/RoomSettings.h"
#include "ui/UserProfile.h"
@@ -158,7 +160,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)
@@ -208,6 +212,7 @@ public:
RoomTopic,
CallType,
Dump,
+ RelatedEventCacheBuster,
};
Q_ENUM(Roles);
@@ -215,10 +220,7 @@ public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant data(const mtx::events::collections::TimelineEvents &event, int role) const;
- Q_INVOKABLE QVariant dataById(QString id, int role)
- {
- return data(index(idToIndex(id)), role);
- }
+ Q_INVOKABLE QVariant dataById(QString id, int role, QString relatedTo);
bool canFetchMore(const QModelIndex &) const override;
void fetchMore(const QModelIndex &) override;
@@ -237,7 +239,6 @@ public:
Q_INVOKABLE void forwardMessage(QString eventId, QString roomId);
Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
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;
@@ -354,14 +355,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();
@@ -391,7 +391,7 @@ private:
TimelineViewManager *manager_;
InputBar input_{this};
- Permissions permissions_{this};
+ Permissions permissions_;
QTimer showEventTimer{this};
QString eventIdToShow;
@@ -403,6 +403,8 @@ private:
int notification_count = 0, highlight_count = 0;
+ unsigned int relatedEventCacheBuster = 0;
+
bool decryptDescription = true;
bool m_paginationInProgress = false;
bool isSpace_ = false;
@@ -413,10 +415,17 @@ template<class T>
void
TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventType)
{
- mtx::events::RoomEvent<T> msgCopy = {};
- msgCopy.content = content;
- msgCopy.type = eventType;
- emit newMessageToSend(msgCopy);
+ if constexpr (std::is_same_v<T, mtx::events::msg::StickerImage>) {
+ mtx::events::Sticker msgCopy = {};
+ msgCopy.content = content;
+ msgCopy.type = eventType;
+ emit newMessageToSend(msgCopy);
+ } else {
+ mtx::events::RoomEvent<T> msgCopy = {};
+ msgCopy.content = content;
+ msgCopy.type = eventType;
+ emit newMessageToSend(msgCopy);
+ }
resetReply();
resetEdit();
}
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index b39ef615..a6922be7 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -15,15 +15,19 @@
#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 "ImagePackListModel.h"
+#include "InviteesModel.h"
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
#include "MxcImageProvider.h"
#include "RoomsModel.h"
+#include "SingleImagePackModel.h"
#include "UserSettingsPage.h"
#include "UsersModel.h"
#include "dialogs/ImageOverlay.h"
@@ -144,6 +148,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
qRegisterMetaType<mtx::events::msg::KeyVerificationReady>();
qRegisterMetaType<mtx::events::msg::KeyVerificationRequest>();
qRegisterMetaType<mtx::events::msg::KeyVerificationStart>();
+ qRegisterMetaType<CombinedImagePackModel *>();
qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
"im.nheko",
@@ -172,6 +177,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,
@@ -180,6 +187,24 @@ 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");
static auto self = this;
qmlRegisterSingletonType<MainWindow>(
@@ -341,6 +366,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(
@@ -398,6 +458,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);
@@ -420,17 +486,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);
@@ -593,6 +648,11 @@ TimelineViewManager::completerFor(QString completerName, QString roomId)
auto proxy = new CompletionProxyModel(roomModel);
roomModel->setParent(proxy);
return proxy;
+ } else if (completerName == "stickers") {
+ auto stickerModel = new CombinedImagePackModel(roomId.toStdString(), true);
+ auto proxy = new CompletionProxyModel(stickerModel, 1, static_cast<size_t>(-1) / 4);
+ stickerModel->setParent(proxy);
+ return proxy;
}
return nullptr;
}
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/RoomSettings.h b/src/ui/RoomSettings.h
index 2a68a182..2deaa5e3 100644
--- a/src/ui/RoomSettings.h
+++ b/src/ui/RoomSettings.h
@@ -134,4 +134,4 @@ private:
RoomInfo info_;
int notifications_ = 0;
int accessRules_ = 0;
-};
\ No newline at end of file
+};
|