summary refs log tree commit diff
path: root/src/timeline/TimelineModel.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/timeline/TimelineModel.cpp')
-rw-r--r--src/timeline/TimelineModel.cpp3135
1 files changed, 1545 insertions, 1590 deletions
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp

index 00f6d9df..720a78fe 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp
@@ -38,288 +38,285 @@ namespace std { inline uint qHash(const std::string &key, uint seed = 0) { - return qHash(QByteArray::fromRawData(key.data(), (int)key.length()), seed); + return qHash(QByteArray::fromRawData(key.data(), (int)key.length()), seed); } } namespace { struct RoomEventType { - template<class T> - qml_mtx_events::EventType operator()(const mtx::events::Event<T> &e) - { - using mtx::events::EventType; - switch (e.type) { - case EventType::RoomKeyRequest: - return qml_mtx_events::EventType::KeyRequest; - case EventType::Reaction: - return qml_mtx_events::EventType::Reaction; - case EventType::RoomAliases: - return qml_mtx_events::EventType::Aliases; - case EventType::RoomAvatar: - return qml_mtx_events::EventType::Avatar; - case EventType::RoomCanonicalAlias: - return qml_mtx_events::EventType::CanonicalAlias; - case EventType::RoomCreate: - return qml_mtx_events::EventType::RoomCreate; - case EventType::RoomEncrypted: - return qml_mtx_events::EventType::Encrypted; - case EventType::RoomEncryption: - return qml_mtx_events::EventType::Encryption; - case EventType::RoomGuestAccess: - return qml_mtx_events::EventType::RoomGuestAccess; - case EventType::RoomHistoryVisibility: - return qml_mtx_events::EventType::RoomHistoryVisibility; - case EventType::RoomJoinRules: - return qml_mtx_events::EventType::RoomJoinRules; - case EventType::RoomMember: - return qml_mtx_events::EventType::Member; - case EventType::RoomMessage: - return qml_mtx_events::EventType::UnknownMessage; - case EventType::RoomName: - return qml_mtx_events::EventType::Name; - case EventType::RoomPowerLevels: - return qml_mtx_events::EventType::PowerLevels; - case EventType::RoomTopic: - return qml_mtx_events::EventType::Topic; - case EventType::RoomTombstone: - return qml_mtx_events::EventType::Tombstone; - case EventType::RoomRedaction: - return qml_mtx_events::EventType::Redaction; - case EventType::RoomPinnedEvents: - return qml_mtx_events::EventType::PinnedEvents; - case EventType::Sticker: - return qml_mtx_events::EventType::Sticker; - case EventType::Tag: - return qml_mtx_events::EventType::Tag; - case EventType::Unsupported: - return qml_mtx_events::EventType::Unsupported; - default: - return qml_mtx_events::EventType::UnknownMessage; - } - } - qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Audio> &) - { - return qml_mtx_events::EventType::AudioMessage; - } - qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Emote> &) - { - return qml_mtx_events::EventType::EmoteMessage; - } - qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::File> &) - { - return qml_mtx_events::EventType::FileMessage; - } - qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Image> &) - { - return qml_mtx_events::EventType::ImageMessage; - } - qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Notice> &) - { - return qml_mtx_events::EventType::NoticeMessage; - } - qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Text> &) - { - return qml_mtx_events::EventType::TextMessage; - } - qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Video> &) - { - return qml_mtx_events::EventType::VideoMessage; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::KeyVerificationRequest> &) - { - return qml_mtx_events::EventType::KeyVerificationRequest; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::KeyVerificationStart> &) - { - return qml_mtx_events::EventType::KeyVerificationStart; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::KeyVerificationMac> &) - { - return qml_mtx_events::EventType::KeyVerificationMac; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::KeyVerificationAccept> &) - { - return qml_mtx_events::EventType::KeyVerificationAccept; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::KeyVerificationReady> &) - { - return qml_mtx_events::EventType::KeyVerificationReady; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::KeyVerificationCancel> &) - { - return qml_mtx_events::EventType::KeyVerificationCancel; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::KeyVerificationKey> &) - { - return qml_mtx_events::EventType::KeyVerificationKey; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::KeyVerificationDone> &) - { - return qml_mtx_events::EventType::KeyVerificationDone; - } - qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &) - { - return qml_mtx_events::EventType::Redacted; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::CallInvite> &) - { - return qml_mtx_events::EventType::CallInvite; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::CallAnswer> &) - { - return qml_mtx_events::EventType::CallAnswer; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::CallHangUp> &) - { - return qml_mtx_events::EventType::CallHangUp; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::CallCandidates> &) - { - return qml_mtx_events::EventType::CallCandidates; - } - // ::EventType::Type operator()(const Event<mtx::events::msg::Location> &e) { return - // ::EventType::LocationMessage; } + template<class T> + qml_mtx_events::EventType operator()(const mtx::events::Event<T> &e) + { + using mtx::events::EventType; + switch (e.type) { + case EventType::RoomKeyRequest: + return qml_mtx_events::EventType::KeyRequest; + case EventType::Reaction: + return qml_mtx_events::EventType::Reaction; + case EventType::RoomAliases: + return qml_mtx_events::EventType::Aliases; + case EventType::RoomAvatar: + return qml_mtx_events::EventType::Avatar; + case EventType::RoomCanonicalAlias: + return qml_mtx_events::EventType::CanonicalAlias; + case EventType::RoomCreate: + return qml_mtx_events::EventType::RoomCreate; + case EventType::RoomEncrypted: + return qml_mtx_events::EventType::Encrypted; + case EventType::RoomEncryption: + return qml_mtx_events::EventType::Encryption; + case EventType::RoomGuestAccess: + return qml_mtx_events::EventType::RoomGuestAccess; + case EventType::RoomHistoryVisibility: + return qml_mtx_events::EventType::RoomHistoryVisibility; + case EventType::RoomJoinRules: + return qml_mtx_events::EventType::RoomJoinRules; + case EventType::RoomMember: + return qml_mtx_events::EventType::Member; + case EventType::RoomMessage: + return qml_mtx_events::EventType::UnknownMessage; + case EventType::RoomName: + return qml_mtx_events::EventType::Name; + case EventType::RoomPowerLevels: + return qml_mtx_events::EventType::PowerLevels; + case EventType::RoomTopic: + return qml_mtx_events::EventType::Topic; + case EventType::RoomTombstone: + return qml_mtx_events::EventType::Tombstone; + case EventType::RoomRedaction: + return qml_mtx_events::EventType::Redaction; + case EventType::RoomPinnedEvents: + return qml_mtx_events::EventType::PinnedEvents; + case EventType::Sticker: + return qml_mtx_events::EventType::Sticker; + case EventType::Tag: + return qml_mtx_events::EventType::Tag; + case EventType::Unsupported: + return qml_mtx_events::EventType::Unsupported; + default: + return qml_mtx_events::EventType::UnknownMessage; + } + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Audio> &) + { + return qml_mtx_events::EventType::AudioMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Emote> &) + { + return qml_mtx_events::EventType::EmoteMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::File> &) + { + return qml_mtx_events::EventType::FileMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Image> &) + { + return qml_mtx_events::EventType::ImageMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Notice> &) + { + return qml_mtx_events::EventType::NoticeMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Text> &) + { + return qml_mtx_events::EventType::TextMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Video> &) + { + return qml_mtx_events::EventType::VideoMessage; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationRequest> &) + { + return qml_mtx_events::EventType::KeyVerificationRequest; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationStart> &) + { + return qml_mtx_events::EventType::KeyVerificationStart; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationMac> &) + { + return qml_mtx_events::EventType::KeyVerificationMac; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationAccept> &) + { + return qml_mtx_events::EventType::KeyVerificationAccept; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationReady> &) + { + return qml_mtx_events::EventType::KeyVerificationReady; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationCancel> &) + { + return qml_mtx_events::EventType::KeyVerificationCancel; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationKey> &) + { + return qml_mtx_events::EventType::KeyVerificationKey; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationDone> &) + { + return qml_mtx_events::EventType::KeyVerificationDone; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &) + { + return qml_mtx_events::EventType::Redacted; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::CallInvite> &) + { + return qml_mtx_events::EventType::CallInvite; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::CallAnswer> &) + { + return qml_mtx_events::EventType::CallAnswer; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::CallHangUp> &) + { + return qml_mtx_events::EventType::CallHangUp; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::CallCandidates> &) + { + return qml_mtx_events::EventType::CallCandidates; + } + // ::EventType::Type operator()(const Event<mtx::events::msg::Location> &e) { return + // ::EventType::LocationMessage; } }; } qml_mtx_events::EventType toRoomEventType(const mtx::events::collections::TimelineEvents &event) { - return std::visit(RoomEventType{}, event); + return std::visit(RoomEventType{}, event); } QString toRoomEventTypeString(const mtx::events::collections::TimelineEvents &event) { - return std::visit([](const auto &e) { return QString::fromStdString(to_string(e.type)); }, - event); + return std::visit([](const auto &e) { return QString::fromStdString(to_string(e.type)); }, + event); } mtx::events::EventType qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t) { - switch (t) { - // Unsupported event - case qml_mtx_events::Unsupported: - return mtx::events::EventType::Unsupported; - - /// m.room_key_request - case qml_mtx_events::KeyRequest: - return mtx::events::EventType::RoomKeyRequest; - /// m.reaction: - case qml_mtx_events::Reaction: - return mtx::events::EventType::Reaction; - /// m.room.aliases - case qml_mtx_events::Aliases: - return mtx::events::EventType::RoomAliases; - /// m.room.avatar - case qml_mtx_events::Avatar: - return mtx::events::EventType::RoomAvatar; - /// m.call.invite - case qml_mtx_events::CallInvite: - return mtx::events::EventType::CallInvite; - /// m.call.answer - case qml_mtx_events::CallAnswer: - return mtx::events::EventType::CallAnswer; - /// m.call.hangup - case qml_mtx_events::CallHangUp: - return mtx::events::EventType::CallHangUp; - /// m.call.candidates - case qml_mtx_events::CallCandidates: - return mtx::events::EventType::CallCandidates; - /// m.room.canonical_alias - case qml_mtx_events::CanonicalAlias: - return mtx::events::EventType::RoomCanonicalAlias; - /// m.room.create - case qml_mtx_events::RoomCreate: - return mtx::events::EventType::RoomCreate; - /// m.room.encrypted. - case qml_mtx_events::Encrypted: - return mtx::events::EventType::RoomEncrypted; - /// m.room.encryption. - case qml_mtx_events::Encryption: - return mtx::events::EventType::RoomEncryption; - /// m.room.guest_access - case qml_mtx_events::RoomGuestAccess: - return mtx::events::EventType::RoomGuestAccess; - /// m.room.history_visibility - case qml_mtx_events::RoomHistoryVisibility: - return mtx::events::EventType::RoomHistoryVisibility; - /// m.room.join_rules - case qml_mtx_events::RoomJoinRules: - return mtx::events::EventType::RoomJoinRules; - /// m.room.member - case qml_mtx_events::Member: - return mtx::events::EventType::RoomMember; - /// m.room.name - case qml_mtx_events::Name: - return mtx::events::EventType::RoomName; - /// m.room.power_levels - case qml_mtx_events::PowerLevels: - return mtx::events::EventType::RoomPowerLevels; - /// m.room.tombstone - case qml_mtx_events::Tombstone: - return mtx::events::EventType::RoomTombstone; - /// m.room.topic - case qml_mtx_events::Topic: - return mtx::events::EventType::RoomTopic; - /// m.room.redaction - case qml_mtx_events::Redaction: - return mtx::events::EventType::RoomRedaction; - /// m.room.pinned_events - case qml_mtx_events::PinnedEvents: - return mtx::events::EventType::RoomPinnedEvents; - // m.sticker - case qml_mtx_events::Sticker: - return mtx::events::EventType::Sticker; - // m.tag - case qml_mtx_events::Tag: - return mtx::events::EventType::Tag; - /// m.room.message - case qml_mtx_events::AudioMessage: - case qml_mtx_events::EmoteMessage: - case qml_mtx_events::FileMessage: - case qml_mtx_events::ImageMessage: - case qml_mtx_events::LocationMessage: - case qml_mtx_events::NoticeMessage: - case qml_mtx_events::TextMessage: - case qml_mtx_events::VideoMessage: - case qml_mtx_events::Redacted: - case qml_mtx_events::UnknownMessage: - case qml_mtx_events::KeyVerificationRequest: - case qml_mtx_events::KeyVerificationStart: - case qml_mtx_events::KeyVerificationMac: - case qml_mtx_events::KeyVerificationAccept: - case qml_mtx_events::KeyVerificationCancel: - case qml_mtx_events::KeyVerificationKey: - case qml_mtx_events::KeyVerificationDone: - case qml_mtx_events::KeyVerificationReady: - return mtx::events::EventType::RoomMessage; - //! m.image_pack, currently im.ponies.room_emotes - case qml_mtx_events::ImagePackInRoom: - return mtx::events::EventType::ImagePackInRoom; - //! m.image_pack, currently im.ponies.user_emotes - case qml_mtx_events::ImagePackInAccountData: - return mtx::events::EventType::ImagePackInAccountData; - //! m.image_pack.rooms, currently im.ponies.emote_rooms - case qml_mtx_events::ImagePackRooms: - return mtx::events::EventType::ImagePackRooms; - default: - return mtx::events::EventType::Unsupported; - }; + switch (t) { + // Unsupported event + case qml_mtx_events::Unsupported: + return mtx::events::EventType::Unsupported; + + /// m.room_key_request + case qml_mtx_events::KeyRequest: + return mtx::events::EventType::RoomKeyRequest; + /// m.reaction: + case qml_mtx_events::Reaction: + return mtx::events::EventType::Reaction; + /// m.room.aliases + case qml_mtx_events::Aliases: + return mtx::events::EventType::RoomAliases; + /// m.room.avatar + case qml_mtx_events::Avatar: + return mtx::events::EventType::RoomAvatar; + /// m.call.invite + case qml_mtx_events::CallInvite: + return mtx::events::EventType::CallInvite; + /// m.call.answer + case qml_mtx_events::CallAnswer: + return mtx::events::EventType::CallAnswer; + /// m.call.hangup + case qml_mtx_events::CallHangUp: + return mtx::events::EventType::CallHangUp; + /// m.call.candidates + case qml_mtx_events::CallCandidates: + return mtx::events::EventType::CallCandidates; + /// m.room.canonical_alias + case qml_mtx_events::CanonicalAlias: + return mtx::events::EventType::RoomCanonicalAlias; + /// m.room.create + case qml_mtx_events::RoomCreate: + return mtx::events::EventType::RoomCreate; + /// m.room.encrypted. + case qml_mtx_events::Encrypted: + return mtx::events::EventType::RoomEncrypted; + /// m.room.encryption. + case qml_mtx_events::Encryption: + return mtx::events::EventType::RoomEncryption; + /// m.room.guest_access + case qml_mtx_events::RoomGuestAccess: + return mtx::events::EventType::RoomGuestAccess; + /// m.room.history_visibility + case qml_mtx_events::RoomHistoryVisibility: + return mtx::events::EventType::RoomHistoryVisibility; + /// m.room.join_rules + case qml_mtx_events::RoomJoinRules: + return mtx::events::EventType::RoomJoinRules; + /// m.room.member + case qml_mtx_events::Member: + return mtx::events::EventType::RoomMember; + /// m.room.name + case qml_mtx_events::Name: + return mtx::events::EventType::RoomName; + /// m.room.power_levels + case qml_mtx_events::PowerLevels: + return mtx::events::EventType::RoomPowerLevels; + /// m.room.tombstone + case qml_mtx_events::Tombstone: + return mtx::events::EventType::RoomTombstone; + /// m.room.topic + case qml_mtx_events::Topic: + return mtx::events::EventType::RoomTopic; + /// m.room.redaction + case qml_mtx_events::Redaction: + return mtx::events::EventType::RoomRedaction; + /// m.room.pinned_events + case qml_mtx_events::PinnedEvents: + return mtx::events::EventType::RoomPinnedEvents; + // m.sticker + case qml_mtx_events::Sticker: + return mtx::events::EventType::Sticker; + // m.tag + case qml_mtx_events::Tag: + return mtx::events::EventType::Tag; + /// m.room.message + case qml_mtx_events::AudioMessage: + case qml_mtx_events::EmoteMessage: + case qml_mtx_events::FileMessage: + case qml_mtx_events::ImageMessage: + case qml_mtx_events::LocationMessage: + case qml_mtx_events::NoticeMessage: + case qml_mtx_events::TextMessage: + case qml_mtx_events::VideoMessage: + case qml_mtx_events::Redacted: + case qml_mtx_events::UnknownMessage: + case qml_mtx_events::KeyVerificationRequest: + case qml_mtx_events::KeyVerificationStart: + case qml_mtx_events::KeyVerificationMac: + case qml_mtx_events::KeyVerificationAccept: + case qml_mtx_events::KeyVerificationCancel: + case qml_mtx_events::KeyVerificationKey: + case qml_mtx_events::KeyVerificationDone: + case qml_mtx_events::KeyVerificationReady: + return mtx::events::EventType::RoomMessage; + //! m.image_pack, currently im.ponies.room_emotes + case qml_mtx_events::ImagePackInRoom: + return mtx::events::EventType::ImagePackInRoom; + //! m.image_pack, currently im.ponies.user_emotes + case qml_mtx_events::ImagePackInAccountData: + return mtx::events::EventType::ImagePackInAccountData; + //! m.image_pack.rooms, currently im.ponies.emote_rooms + case qml_mtx_events::ImagePackRooms: + return mtx::events::EventType::ImagePackRooms; + default: + return mtx::events::EventType::Unsupported; + }; } TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent) @@ -329,566 +326,549 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj , manager_(manager) , permissions_{room_id} { - lastMessage_.timestamp = 0; - - if (auto create = - cache::client()->getStateEvent<mtx::events::state::Create>(room_id.toStdString())) - 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, - this, - [](const QString &msg) { emit ChatPage::instance()->showNotification(msg); }, - Qt::QueuedConnection); - - connect(this, - &TimelineModel::newMessageToSend, - this, - &TimelineModel::addPendingMessage, - Qt::QueuedConnection); - connect(this, &TimelineModel::addPendingMessageToStore, &events, &EventStore::addPending); - - connect(&events, &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), - index(events.size() - from - 1, 0)); - }); - - connect(&events, &EventStore::beginInsertRows, this, [this](int from, int to) { - int first = events.size() - to; - int last = events.size() - from; - if (from >= events.size()) { - int batch_size = to - from; - first += batch_size; - last += batch_size; - } else { - first -= 1; - last -= 1; - } - nhlog::ui()->debug("begin insert from {} to {}", first, last); - beginInsertRows(QModelIndex(), first, last); - }); - connect(&events, &EventStore::endInsertRows, this, [this]() { endInsertRows(); }); - connect(&events, &EventStore::beginResetModel, this, [this]() { beginResetModel(); }); - connect(&events, &EventStore::endResetModel, this, [this]() { endResetModel(); }); - connect(&events, &EventStore::newEncryptedImage, this, &TimelineModel::newEncryptedImage); - connect( - &events, &EventStore::fetchedMore, this, [this]() { setPaginationInProgress(false); }); - connect(&events, - &EventStore::startDMVerification, - this, - [this](mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> msg) { - ChatPage::instance()->receivedRoomDeviceVerificationRequest(msg, this); - }); - connect(&events, &EventStore::updateFlowEventId, this, [this](std::string event_id) { - this->updateFlowEventId(event_id); - }); - - // When a message is sent, check if the current edit/reply relates to that message, - // and update the event_id so that it points to the sent message and not the pending one. - connect(&events, - &EventStore::messageSent, - this, - [this](std::string txn_id, std::string event_id) { - if (edit_.toStdString() == txn_id) { - edit_ = QString::fromStdString(event_id); - emit editChanged(edit_); - } - if (reply_.toStdString() == txn_id) { - reply_ = QString::fromStdString(event_id); - emit replyChanged(reply_); - } - }); - - connect(manager_, - &TimelineViewManager::initialSyncChanged, - &events, - &EventStore::enableKeyRequests); - - connect(this, &TimelineModel::encryptionChanged, this, &TimelineModel::trustlevelChanged); - connect( - this, &TimelineModel::roomMemberCountChanged, this, &TimelineModel::trustlevelChanged); - connect(cache::client(), - &Cache::verificationStatusChanged, - this, - &TimelineModel::trustlevelChanged); - - showEventTimer.callOnTimeout(this, &TimelineModel::scrollTimerEvent); + lastMessage_.timestamp = 0; + + if (auto create = + cache::client()->getStateEvent<mtx::events::state::Create>(room_id.toStdString())) + 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, + this, + [](const QString &msg) { emit ChatPage::instance()->showNotification(msg); }, + Qt::QueuedConnection); + + connect(this, + &TimelineModel::newMessageToSend, + this, + &TimelineModel::addPendingMessage, + Qt::QueuedConnection); + connect(this, &TimelineModel::addPendingMessageToStore, &events, &EventStore::addPending); + + connect(&events, &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), index(events.size() - from - 1, 0)); + }); + + connect(&events, &EventStore::beginInsertRows, this, [this](int from, int to) { + int first = events.size() - to; + int last = events.size() - from; + if (from >= events.size()) { + int batch_size = to - from; + first += batch_size; + last += batch_size; + } else { + first -= 1; + last -= 1; + } + nhlog::ui()->debug("begin insert from {} to {}", first, last); + beginInsertRows(QModelIndex(), first, last); + }); + connect(&events, &EventStore::endInsertRows, this, [this]() { endInsertRows(); }); + connect(&events, &EventStore::beginResetModel, this, [this]() { beginResetModel(); }); + connect(&events, &EventStore::endResetModel, this, [this]() { endResetModel(); }); + connect(&events, &EventStore::newEncryptedImage, this, &TimelineModel::newEncryptedImage); + connect(&events, &EventStore::fetchedMore, this, [this]() { setPaginationInProgress(false); }); + connect(&events, + &EventStore::startDMVerification, + this, + [this](mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> msg) { + ChatPage::instance()->receivedRoomDeviceVerificationRequest(msg, this); + }); + connect(&events, &EventStore::updateFlowEventId, this, [this](std::string event_id) { + this->updateFlowEventId(event_id); + }); + + // When a message is sent, check if the current edit/reply relates to that message, + // and update the event_id so that it points to the sent message and not the pending one. + connect( + &events, &EventStore::messageSent, this, [this](std::string txn_id, std::string event_id) { + if (edit_.toStdString() == txn_id) { + edit_ = QString::fromStdString(event_id); + emit editChanged(edit_); + } + if (reply_.toStdString() == txn_id) { + reply_ = QString::fromStdString(event_id); + emit replyChanged(reply_); + } + }); + + connect( + manager_, &TimelineViewManager::initialSyncChanged, &events, &EventStore::enableKeyRequests); + + connect(this, &TimelineModel::encryptionChanged, this, &TimelineModel::trustlevelChanged); + connect(this, &TimelineModel::roomMemberCountChanged, this, &TimelineModel::trustlevelChanged); + connect( + cache::client(), &Cache::verificationStatusChanged, this, &TimelineModel::trustlevelChanged); + + showEventTimer.callOnTimeout(this, &TimelineModel::scrollTimerEvent); } QHash<int, QByteArray> TimelineModel::roleNames() const { - return { - {Type, "type"}, - {TypeString, "typeString"}, - {IsOnlyEmoji, "isOnlyEmoji"}, - {Body, "body"}, - {FormattedBody, "formattedBody"}, - {PreviousMessageUserId, "previousMessageUserId"}, - {IsSender, "isSender"}, - {UserId, "userId"}, - {UserName, "userName"}, - {PreviousMessageDay, "previousMessageDay"}, - {Day, "day"}, - {Timestamp, "timestamp"}, - {Url, "url"}, - {ThumbnailUrl, "thumbnailUrl"}, - {Blurhash, "blurhash"}, - {Filename, "filename"}, - {Filesize, "filesize"}, - {MimeType, "mimetype"}, - {OriginalHeight, "originalHeight"}, - {OriginalWidth, "originalWidth"}, - {ProportionalHeight, "proportionalHeight"}, - {EventId, "eventId"}, - {State, "status"}, - {IsEdited, "isEdited"}, - {IsEditable, "isEditable"}, - {IsEncrypted, "isEncrypted"}, - {Trustlevel, "trustlevel"}, - {EncryptionError, "encryptionError"}, - {ReplyTo, "replyTo"}, - {Reactions, "reactions"}, - {RoomId, "roomId"}, - {RoomName, "roomName"}, - {RoomTopic, "roomTopic"}, - {CallType, "callType"}, - {Dump, "dump"}, - {RelatedEventCacheBuster, "relatedEventCacheBuster"}, - }; + return { + {Type, "type"}, + {TypeString, "typeString"}, + {IsOnlyEmoji, "isOnlyEmoji"}, + {Body, "body"}, + {FormattedBody, "formattedBody"}, + {PreviousMessageUserId, "previousMessageUserId"}, + {IsSender, "isSender"}, + {UserId, "userId"}, + {UserName, "userName"}, + {PreviousMessageDay, "previousMessageDay"}, + {Day, "day"}, + {Timestamp, "timestamp"}, + {Url, "url"}, + {ThumbnailUrl, "thumbnailUrl"}, + {Blurhash, "blurhash"}, + {Filename, "filename"}, + {Filesize, "filesize"}, + {MimeType, "mimetype"}, + {OriginalHeight, "originalHeight"}, + {OriginalWidth, "originalWidth"}, + {ProportionalHeight, "proportionalHeight"}, + {EventId, "eventId"}, + {State, "status"}, + {IsEdited, "isEdited"}, + {IsEditable, "isEditable"}, + {IsEncrypted, "isEncrypted"}, + {Trustlevel, "trustlevel"}, + {EncryptionError, "encryptionError"}, + {ReplyTo, "replyTo"}, + {Reactions, "reactions"}, + {RoomId, "roomId"}, + {RoomName, "roomName"}, + {RoomTopic, "roomTopic"}, + {CallType, "callType"}, + {Dump, "dump"}, + {RelatedEventCacheBuster, "relatedEventCacheBuster"}, + }; } int TimelineModel::rowCount(const QModelIndex &parent) const { - Q_UNUSED(parent); - return this->events.size(); + Q_UNUSED(parent); + return this->events.size(); } QVariantMap TimelineModel::getDump(QString eventId, QString relatedTo) const { - if (auto event = events.get(eventId.toStdString(), relatedTo.toStdString())) - return data(*event, Dump).toMap(); - return {}; + if (auto event = events.get(eventId.toStdString(), relatedTo.toStdString())) + return data(*event, Dump).toMap(); + return {}; } QVariant TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int role) const { - using namespace mtx::accessors; - namespace acc = mtx::accessors; - - switch (role) { - case IsSender: - return QVariant(acc::sender(event) == http::client()->user_id().to_string()); - case UserId: - return QVariant(QString::fromStdString(acc::sender(event))); - case UserName: - return QVariant(displayName(QString::fromStdString(acc::sender(event)))); - - case Day: { - QDateTime prevDate = origin_server_ts(event); - prevDate.setTime(QTime()); - return QVariant(prevDate.toMSecsSinceEpoch()); - } - case Timestamp: - return QVariant(origin_server_ts(event)); - case Type: - return QVariant(toRoomEventType(event)); - case TypeString: - return QVariant(toRoomEventTypeString(event)); - case IsOnlyEmoji: { - QString qBody = QString::fromStdString(body(event)); - - QVector<uint> utf32_string = qBody.toUcs4(); - int emojiCount = 0; - - for (auto &code : utf32_string) { - if (utils::codepointIsEmoji(code)) { - emojiCount++; - } else { - return QVariant(0); - } - } - - return QVariant(emojiCount); - } - case Body: - return QVariant( - utils::replaceEmoji(QString::fromStdString(body(event)).toHtmlEscaped())); - case FormattedBody: { - const static QRegularExpression replyFallback( - "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption); - - auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent(); - - bool isReply = utils::isReply(event); - - auto formattedBody_ = QString::fromStdString(formatted_body(event)); - if (formattedBody_.isEmpty()) { - auto body_ = QString::fromStdString(body(event)); - - if (isReply) { - while (body_.startsWith("> ")) - body_ = body_.right(body_.size() - body_.indexOf('\n') - 1); - if (body_.startsWith('\n')) - body_ = body_.right(body_.size() - 1); - } - formattedBody_ = body_.toHtmlEscaped().replace('\n', "<br>"); - } else { - if (isReply) - formattedBody_ = formattedBody_.remove(replyFallback); - } - - // TODO(Nico): Don't parse html with a regex - const static QRegularExpression matchImgUri( - "(<img [^>]*)src=\"mxc://([^\"]*)\"([^>]*>)"); - formattedBody_.replace(matchImgUri, "\\1 src=\"image://mxcImage/\\2\"\\3"); - // Same regex but for single quotes around the src - const static QRegularExpression matchImgUri2( - "(<img [^>]*)src=\'mxc://([^\']*)\'([^>]*>)"); - formattedBody_.replace(matchImgUri2, "\\1 src=\"image://mxcImage/\\2\"\\3"); - const static QRegularExpression matchEmoticonHeight( - "(<img data-mx-emoticon [^>]*)height=\"([^\"]*)\"([^>]*>)"); - formattedBody_.replace(matchEmoticonHeight, - QString("\\1 height=\"%1\"\\3").arg(ascent)); - - return QVariant(utils::replaceEmoji( - utils::linkifyMessage(utils::escapeBlacklistedHtml(formattedBody_)))); - } - case Url: - return QVariant(QString::fromStdString(url(event))); - case ThumbnailUrl: - return QVariant(QString::fromStdString(thumbnail_url(event))); - case Blurhash: - return QVariant(QString::fromStdString(blurhash(event))); - case Filename: - return QVariant(QString::fromStdString(filename(event))); - case Filesize: - return QVariant(utils::humanReadableFileSize(filesize(event))); - case MimeType: - return QVariant(QString::fromStdString(mimetype(event))); - case OriginalHeight: - return QVariant(qulonglong{media_height(event)}); - case OriginalWidth: - return QVariant(qulonglong{media_width(event)}); - case ProportionalHeight: { - auto w = media_width(event); - if (w == 0) - w = 1; - - double prop = media_height(event) / (double)w; - - return QVariant(prop > 0 ? prop : 1.); - } - case EventId: { - if (auto replaces = relations(event).replaces()) - return QVariant(QString::fromStdString(replaces.value())); - else - return QVariant(QString::fromStdString(event_id(event))); - } - case State: { - auto id = QString::fromStdString(event_id(event)); - auto containsOthers = [](const auto &vec) { - for (const auto &e : vec) - if (e.second != http::client()->user_id().to_string()) - return true; - return false; - }; - - // only show read receipts for messages not from us - if (acc::sender(event) != http::client()->user_id().to_string()) - return qml_mtx_events::Empty; - else if (!id.isEmpty() && id[0] == "m") - return qml_mtx_events::Sent; - else if (read.contains(id) || containsOthers(cache::readReceipts(id, room_id_))) - return qml_mtx_events::Read; - else - return qml_mtx_events::Received; - } - case IsEdited: - return QVariant(relations(event).replaces().has_value()); - case IsEditable: - return QVariant(!is_state_event(event) && mtx::accessors::sender(event) == - http::client()->user_id().to_string()); - case IsEncrypted: { - auto id = event_id(event); - auto encrypted_event = events.get(id, "", false); - return encrypted_event && - std::holds_alternative< - mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( - *encrypted_event); - } - - case Trustlevel: { - auto id = event_id(event); - auto encrypted_event = events.get(id, "", false); - if (encrypted_event) { - if (auto encrypted = - std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( - &*encrypted_event)) { - return olm::calculate_trust( - encrypted->sender, - MegolmSessionIndex(room_id_.toStdString(), encrypted->content)); - } - } - return crypto::Trust::Unverified; - } - - case EncryptionError: - return events.decryptionError(event_id(event)); + using namespace mtx::accessors; + namespace acc = mtx::accessors; + + switch (role) { + case IsSender: + return QVariant(acc::sender(event) == http::client()->user_id().to_string()); + case UserId: + return QVariant(QString::fromStdString(acc::sender(event))); + case UserName: + return QVariant(displayName(QString::fromStdString(acc::sender(event)))); + + case Day: { + QDateTime prevDate = origin_server_ts(event); + prevDate.setTime(QTime()); + return QVariant(prevDate.toMSecsSinceEpoch()); + } + case Timestamp: + return QVariant(origin_server_ts(event)); + case Type: + return QVariant(toRoomEventType(event)); + case TypeString: + return QVariant(toRoomEventTypeString(event)); + case IsOnlyEmoji: { + QString qBody = QString::fromStdString(body(event)); + + QVector<uint> utf32_string = qBody.toUcs4(); + int emojiCount = 0; + + for (auto &code : utf32_string) { + if (utils::codepointIsEmoji(code)) { + emojiCount++; + } else { + return QVariant(0); + } + } + + return QVariant(emojiCount); + } + case Body: + return QVariant(utils::replaceEmoji(QString::fromStdString(body(event)).toHtmlEscaped())); + case FormattedBody: { + const static QRegularExpression replyFallback( + "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption); + + auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent(); + + bool isReply = utils::isReply(event); + + auto formattedBody_ = QString::fromStdString(formatted_body(event)); + if (formattedBody_.isEmpty()) { + auto body_ = QString::fromStdString(body(event)); + + if (isReply) { + while (body_.startsWith("> ")) + body_ = body_.right(body_.size() - body_.indexOf('\n') - 1); + if (body_.startsWith('\n')) + body_ = body_.right(body_.size() - 1); + } + formattedBody_ = body_.toHtmlEscaped().replace('\n', "<br>"); + } else { + if (isReply) + formattedBody_ = formattedBody_.remove(replyFallback); + } + + // TODO(Nico): Don't parse html with a regex + const static QRegularExpression matchImgUri("(<img [^>]*)src=\"mxc://([^\"]*)\"([^>]*>)"); + formattedBody_.replace(matchImgUri, "\\1 src=\"image://mxcImage/\\2\"\\3"); + // Same regex but for single quotes around the src + const static QRegularExpression matchImgUri2("(<img [^>]*)src=\'mxc://([^\']*)\'([^>]*>)"); + formattedBody_.replace(matchImgUri2, "\\1 src=\"image://mxcImage/\\2\"\\3"); + const static QRegularExpression matchEmoticonHeight( + "(<img data-mx-emoticon [^>]*)height=\"([^\"]*)\"([^>]*>)"); + formattedBody_.replace(matchEmoticonHeight, QString("\\1 height=\"%1\"\\3").arg(ascent)); + + return QVariant( + utils::replaceEmoji(utils::linkifyMessage(utils::escapeBlacklistedHtml(formattedBody_)))); + } + case Url: + return QVariant(QString::fromStdString(url(event))); + case ThumbnailUrl: + return QVariant(QString::fromStdString(thumbnail_url(event))); + case Blurhash: + return QVariant(QString::fromStdString(blurhash(event))); + case Filename: + return QVariant(QString::fromStdString(filename(event))); + case Filesize: + return QVariant(utils::humanReadableFileSize(filesize(event))); + case MimeType: + return QVariant(QString::fromStdString(mimetype(event))); + case OriginalHeight: + return QVariant(qulonglong{media_height(event)}); + case OriginalWidth: + return QVariant(qulonglong{media_width(event)}); + case ProportionalHeight: { + auto w = media_width(event); + if (w == 0) + w = 1; + + double prop = media_height(event) / (double)w; + + return QVariant(prop > 0 ? prop : 1.); + } + case EventId: { + if (auto replaces = relations(event).replaces()) + return QVariant(QString::fromStdString(replaces.value())); + else + return QVariant(QString::fromStdString(event_id(event))); + } + case State: { + auto id = QString::fromStdString(event_id(event)); + auto containsOthers = [](const auto &vec) { + for (const auto &e : vec) + if (e.second != http::client()->user_id().to_string()) + return true; + return false; + }; - case ReplyTo: - return QVariant(QString::fromStdString(relations(event).reply_to().value_or(""))); - case Reactions: { - auto id = relations(event).replaces().value_or(event_id(event)); - return QVariant::fromValue(events.reactions(id)); - } - case RoomId: - return QVariant(room_id_); - case RoomName: - return QVariant( - utils::replaceEmoji(QString::fromStdString(room_name(event)).toHtmlEscaped())); - case RoomTopic: - return QVariant(utils::replaceEmoji( - utils::linkifyMessage(QString::fromStdString(room_topic(event)) - .toHtmlEscaped() - .replace("\n", "<br>")))); - case CallType: - return QVariant(QString::fromStdString(call_type(event))); - case Dump: { - QVariantMap m; - auto names = roleNames(); - - m.insert(names[Type], data(event, static_cast<int>(Type))); - m.insert(names[TypeString], data(event, static_cast<int>(TypeString))); - m.insert(names[IsOnlyEmoji], data(event, static_cast<int>(IsOnlyEmoji))); - m.insert(names[Body], data(event, static_cast<int>(Body))); - m.insert(names[FormattedBody], data(event, static_cast<int>(FormattedBody))); - m.insert(names[IsSender], data(event, static_cast<int>(IsSender))); - m.insert(names[UserId], data(event, static_cast<int>(UserId))); - m.insert(names[UserName], data(event, static_cast<int>(UserName))); - m.insert(names[Day], data(event, static_cast<int>(Day))); - m.insert(names[Timestamp], data(event, static_cast<int>(Timestamp))); - m.insert(names[Url], data(event, static_cast<int>(Url))); - m.insert(names[ThumbnailUrl], data(event, static_cast<int>(ThumbnailUrl))); - m.insert(names[Blurhash], data(event, static_cast<int>(Blurhash))); - m.insert(names[Filename], data(event, static_cast<int>(Filename))); - m.insert(names[Filesize], data(event, static_cast<int>(Filesize))); - m.insert(names[MimeType], data(event, static_cast<int>(MimeType))); - m.insert(names[OriginalHeight], data(event, static_cast<int>(OriginalHeight))); - m.insert(names[OriginalWidth], data(event, static_cast<int>(OriginalWidth))); - m.insert(names[ProportionalHeight], - data(event, static_cast<int>(ProportionalHeight))); - m.insert(names[EventId], data(event, static_cast<int>(EventId))); - m.insert(names[State], data(event, static_cast<int>(State))); - m.insert(names[IsEdited], data(event, static_cast<int>(IsEdited))); - m.insert(names[IsEditable], data(event, static_cast<int>(IsEditable))); - m.insert(names[IsEncrypted], data(event, static_cast<int>(IsEncrypted))); - m.insert(names[ReplyTo], data(event, static_cast<int>(ReplyTo))); - m.insert(names[RoomName], data(event, static_cast<int>(RoomName))); - m.insert(names[RoomTopic], data(event, static_cast<int>(RoomTopic))); - m.insert(names[CallType], data(event, static_cast<int>(CallType))); - m.insert(names[EncryptionError], data(event, static_cast<int>(EncryptionError))); - - return QVariant(m); - } - case RelatedEventCacheBuster: - return relatedEventCacheBuster; - default: - return QVariant(); - } + // only show read receipts for messages not from us + if (acc::sender(event) != http::client()->user_id().to_string()) + return qml_mtx_events::Empty; + else if (!id.isEmpty() && id[0] == "m") + return qml_mtx_events::Sent; + else if (read.contains(id) || containsOthers(cache::readReceipts(id, room_id_))) + return qml_mtx_events::Read; + else + return qml_mtx_events::Received; + } + case IsEdited: + return QVariant(relations(event).replaces().has_value()); + case IsEditable: + return QVariant(!is_state_event(event) && + mtx::accessors::sender(event) == http::client()->user_id().to_string()); + case IsEncrypted: { + auto id = event_id(event); + auto encrypted_event = events.get(id, "", false); + return encrypted_event && + std::holds_alternative<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + *encrypted_event); + } + + case Trustlevel: { + auto id = event_id(event); + auto encrypted_event = events.get(id, "", false); + if (encrypted_event) { + if (auto encrypted = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + &*encrypted_event)) { + return olm::calculate_trust( + encrypted->sender, + MegolmSessionIndex(room_id_.toStdString(), encrypted->content)); + } + } + return crypto::Trust::Unverified; + } + + case EncryptionError: + return events.decryptionError(event_id(event)); + + case ReplyTo: + return QVariant(QString::fromStdString(relations(event).reply_to().value_or(""))); + case Reactions: { + auto id = relations(event).replaces().value_or(event_id(event)); + return QVariant::fromValue(events.reactions(id)); + } + case RoomId: + return QVariant(room_id_); + case RoomName: + return QVariant( + utils::replaceEmoji(QString::fromStdString(room_name(event)).toHtmlEscaped())); + case RoomTopic: + return QVariant(utils::replaceEmoji(utils::linkifyMessage( + QString::fromStdString(room_topic(event)).toHtmlEscaped().replace("\n", "<br>")))); + case CallType: + return QVariant(QString::fromStdString(call_type(event))); + case Dump: { + QVariantMap m; + auto names = roleNames(); + + m.insert(names[Type], data(event, static_cast<int>(Type))); + m.insert(names[TypeString], data(event, static_cast<int>(TypeString))); + m.insert(names[IsOnlyEmoji], data(event, static_cast<int>(IsOnlyEmoji))); + m.insert(names[Body], data(event, static_cast<int>(Body))); + m.insert(names[FormattedBody], data(event, static_cast<int>(FormattedBody))); + m.insert(names[IsSender], data(event, static_cast<int>(IsSender))); + m.insert(names[UserId], data(event, static_cast<int>(UserId))); + m.insert(names[UserName], data(event, static_cast<int>(UserName))); + m.insert(names[Day], data(event, static_cast<int>(Day))); + m.insert(names[Timestamp], data(event, static_cast<int>(Timestamp))); + m.insert(names[Url], data(event, static_cast<int>(Url))); + m.insert(names[ThumbnailUrl], data(event, static_cast<int>(ThumbnailUrl))); + m.insert(names[Blurhash], data(event, static_cast<int>(Blurhash))); + m.insert(names[Filename], data(event, static_cast<int>(Filename))); + m.insert(names[Filesize], data(event, static_cast<int>(Filesize))); + m.insert(names[MimeType], data(event, static_cast<int>(MimeType))); + m.insert(names[OriginalHeight], data(event, static_cast<int>(OriginalHeight))); + m.insert(names[OriginalWidth], data(event, static_cast<int>(OriginalWidth))); + m.insert(names[ProportionalHeight], data(event, static_cast<int>(ProportionalHeight))); + m.insert(names[EventId], data(event, static_cast<int>(EventId))); + m.insert(names[State], data(event, static_cast<int>(State))); + m.insert(names[IsEdited], data(event, static_cast<int>(IsEdited))); + m.insert(names[IsEditable], data(event, static_cast<int>(IsEditable))); + m.insert(names[IsEncrypted], data(event, static_cast<int>(IsEncrypted))); + m.insert(names[ReplyTo], data(event, static_cast<int>(ReplyTo))); + m.insert(names[RoomName], data(event, static_cast<int>(RoomName))); + m.insert(names[RoomTopic], data(event, static_cast<int>(RoomTopic))); + m.insert(names[CallType], data(event, static_cast<int>(CallType))); + m.insert(names[EncryptionError], data(event, static_cast<int>(EncryptionError))); + + return QVariant(m); + } + case RelatedEventCacheBuster: + return relatedEventCacheBuster; + default: + return QVariant(); + } } QVariant TimelineModel::data(const QModelIndex &index, int role) const { - using namespace mtx::accessors; - namespace acc = mtx::accessors; - if (index.row() < 0 && index.row() >= rowCount()) - return QVariant(); + using namespace mtx::accessors; + namespace acc = mtx::accessors; + if (index.row() < 0 && index.row() >= rowCount()) + return QVariant(); - // HACK(Nico): fetchMore likes to break with dynamically sized delegates and reuseItems - if (index.row() + 1 == rowCount() && !m_paginationInProgress) - const_cast<TimelineModel *>(this)->fetchMore(index); + // HACK(Nico): fetchMore likes to break with dynamically sized delegates and reuseItems + if (index.row() + 1 == rowCount() && !m_paginationInProgress) + const_cast<TimelineModel *>(this)->fetchMore(index); - auto event = events.get(rowCount() - index.row() - 1); + auto event = events.get(rowCount() - index.row() - 1); - if (!event) - return ""; - - if (role == PreviousMessageDay || role == PreviousMessageUserId) { - int prevIdx = rowCount() - index.row() - 2; - if (prevIdx < 0) - return QVariant(); - auto tempEv = events.get(prevIdx); - if (!tempEv) - return QVariant(); - if (role == PreviousMessageUserId) - return data(*tempEv, UserId); - else - return data(*tempEv, Day); - } + if (!event) + return ""; - return data(*event, role); + if (role == PreviousMessageDay || role == PreviousMessageUserId) { + int prevIdx = rowCount() - index.row() - 2; + if (prevIdx < 0) + return QVariant(); + auto tempEv = events.get(prevIdx); + if (!tempEv) + return QVariant(); + if (role == PreviousMessageUserId) + return data(*tempEv, UserId); + else + return data(*tempEv, Day); + } + + 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(); + if (auto event = events.get(id.toStdString(), relatedTo.toStdString())) + return data(*event, role); + return QVariant(); } bool TimelineModel::canFetchMore(const QModelIndex &) const { - if (!events.size()) - return true; - if (auto first = events.get(0); - first && - !std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>(*first)) - return true; - else + if (!events.size()) + return true; + if (auto first = events.get(0); + first && + !std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>(*first)) + return true; + else - return false; + return false; } void TimelineModel::setPaginationInProgress(const bool paginationInProgress) { - if (m_paginationInProgress == paginationInProgress) { - return; - } + if (m_paginationInProgress == paginationInProgress) { + return; + } - m_paginationInProgress = paginationInProgress; - emit paginationInProgressChanged(m_paginationInProgress); + m_paginationInProgress = paginationInProgress; + emit paginationInProgressChanged(m_paginationInProgress); } void TimelineModel::fetchMore(const QModelIndex &) { - if (m_paginationInProgress) { - nhlog::ui()->warn("Already loading older messages"); - return; - } + if (m_paginationInProgress) { + nhlog::ui()->warn("Already loading older messages"); + return; + } - setPaginationInProgress(true); + setPaginationInProgress(true); - events.fetchMore(); + events.fetchMore(); } void TimelineModel::sync(const mtx::responses::JoinedRoom &room) { - this->syncState(room.state); - this->addEvents(room.timeline); + this->syncState(room.state); + this->addEvents(room.timeline); - if (room.unread_notifications.highlight_count != highlight_count || - room.unread_notifications.notification_count != notification_count) { - notification_count = room.unread_notifications.notification_count; - highlight_count = room.unread_notifications.highlight_count; - emit notificationsChanged(); - } + if (room.unread_notifications.highlight_count != highlight_count || + room.unread_notifications.notification_count != notification_count) { + notification_count = room.unread_notifications.notification_count; + highlight_count = room.unread_notifications.highlight_count; + emit notificationsChanged(); + } } void TimelineModel::syncState(const mtx::responses::State &s) { - using namespace mtx::events; - - for (const auto &e : s.events) { - if (std::holds_alternative<StateEvent<state::Avatar>>(e)) - emit roomAvatarUrlChanged(); - else if (std::holds_alternative<StateEvent<state::Name>>(e)) - emit roomNameChanged(); - else if (std::holds_alternative<StateEvent<state::Topic>>(e)) - emit roomTopicChanged(); - else if (std::holds_alternative<StateEvent<state::Topic>>(e)) { - permissions_.invalidate(); - emit permissionsChanged(); - } else if (std::holds_alternative<StateEvent<state::Member>>(e)) { - emit roomAvatarUrlChanged(); - emit roomNameChanged(); - emit roomMemberCountChanged(); - - if (roomMemberCount() <= 2) { - emit isDirectChanged(); - emit directChatOtherUserIdChanged(); - } - } else if (std::holds_alternative<StateEvent<state::Encryption>>(e)) { - this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString()); - emit encryptionChanged(); - } - } + using namespace mtx::events; + + for (const auto &e : s.events) { + if (std::holds_alternative<StateEvent<state::Avatar>>(e)) + emit roomAvatarUrlChanged(); + else if (std::holds_alternative<StateEvent<state::Name>>(e)) + emit roomNameChanged(); + else if (std::holds_alternative<StateEvent<state::Topic>>(e)) + emit roomTopicChanged(); + else if (std::holds_alternative<StateEvent<state::Topic>>(e)) { + permissions_.invalidate(); + emit permissionsChanged(); + } else if (std::holds_alternative<StateEvent<state::Member>>(e)) { + emit roomAvatarUrlChanged(); + emit roomNameChanged(); + emit roomMemberCountChanged(); + + if (roomMemberCount() <= 2) { + emit isDirectChanged(); + emit directChatOtherUserIdChanged(); + } + } else if (std::holds_alternative<StateEvent<state::Encryption>>(e)) { + this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString()); + emit encryptionChanged(); + } + } } void TimelineModel::addEvents(const mtx::responses::Timeline &timeline) { - if (timeline.events.empty()) - return; - - events.handleSync(timeline); - - using namespace mtx::events; - - for (auto e : timeline.events) { - if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) { - MegolmSessionIndex index(room_id_.toStdString(), encryptedEvent->content); - - auto result = olm::decryptEvent(index, *encryptedEvent); - if (result.event) - e = result.event.value(); - } + if (timeline.events.empty()) + return; - if (std::holds_alternative<RoomEvent<msg::CallCandidates>>(e) || - std::holds_alternative<RoomEvent<msg::CallInvite>>(e) || - std::holds_alternative<RoomEvent<msg::CallAnswer>>(e) || - std::holds_alternative<RoomEvent<msg::CallHangUp>>(e)) - std::visit( - [this](auto &event) { - event.room_id = room_id_.toStdString(); - if constexpr (std::is_same_v<std::decay_t<decltype(event)>, - RoomEvent<msg::CallAnswer>> || - std::is_same_v<std::decay_t<decltype(event)>, - RoomEvent<msg::CallHangUp>>) - emit newCallEvent(event); - else { - if (event.sender != http::client()->user_id().to_string()) - emit newCallEvent(event); - } - }, - e); - else if (std::holds_alternative<StateEvent<state::Avatar>>(e)) - emit roomAvatarUrlChanged(); - else if (std::holds_alternative<StateEvent<state::Name>>(e)) - emit roomNameChanged(); - else if (std::holds_alternative<StateEvent<state::Topic>>(e)) - emit roomTopicChanged(); - else if (std::holds_alternative<StateEvent<state::PowerLevels>>(e)) { - permissions_.invalidate(); - emit permissionsChanged(); - } else if (std::holds_alternative<StateEvent<state::Member>>(e)) { - emit roomAvatarUrlChanged(); - emit roomNameChanged(); - emit roomMemberCountChanged(); - } else if (std::holds_alternative<StateEvent<state::Encryption>>(e)) { - this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString()); - emit encryptionChanged(); - } - } - updateLastMessage(); + events.handleSync(timeline); + + using namespace mtx::events; + + for (auto e : timeline.events) { + if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) { + MegolmSessionIndex index(room_id_.toStdString(), encryptedEvent->content); + + auto result = olm::decryptEvent(index, *encryptedEvent); + if (result.event) + e = result.event.value(); + } + + if (std::holds_alternative<RoomEvent<msg::CallCandidates>>(e) || + std::holds_alternative<RoomEvent<msg::CallInvite>>(e) || + std::holds_alternative<RoomEvent<msg::CallAnswer>>(e) || + std::holds_alternative<RoomEvent<msg::CallHangUp>>(e)) + std::visit( + [this](auto &event) { + event.room_id = room_id_.toStdString(); + if constexpr (std::is_same_v<std::decay_t<decltype(event)>, + RoomEvent<msg::CallAnswer>> || + std::is_same_v<std::decay_t<decltype(event)>, + RoomEvent<msg::CallHangUp>>) + emit newCallEvent(event); + else { + if (event.sender != http::client()->user_id().to_string()) + emit newCallEvent(event); + } + }, + e); + else if (std::holds_alternative<StateEvent<state::Avatar>>(e)) + emit roomAvatarUrlChanged(); + else if (std::holds_alternative<StateEvent<state::Name>>(e)) + emit roomNameChanged(); + else if (std::holds_alternative<StateEvent<state::Topic>>(e)) + emit roomTopicChanged(); + else if (std::holds_alternative<StateEvent<state::PowerLevels>>(e)) { + permissions_.invalidate(); + emit permissionsChanged(); + } else if (std::holds_alternative<StateEvent<state::Member>>(e)) { + emit roomAvatarUrlChanged(); + emit roomNameChanged(); + emit roomMemberCountChanged(); + } else if (std::holds_alternative<StateEvent<state::Encryption>>(e)) { + this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString()); + emit encryptionChanged(); + } + } + updateLastMessage(); } template<typename T> @@ -896,1216 +876,1191 @@ auto isMessage(const mtx::events::RoomEvent<T> &e) -> std::enable_if_t<std::is_same<decltype(e.content.msgtype), std::string>::value, bool> { - return true; + return true; } template<typename T> auto isMessage(const mtx::events::Event<T> &) { - return false; + return false; } template<typename T> auto isMessage(const mtx::events::EncryptedEvent<T> &) { - return true; + return true; } auto isMessage(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &) { - return true; + return true; } auto isMessage(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &) { - return true; + return true; } auto isMessage(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &) { - return true; + return true; } // Workaround. We also want to see a room at the top, if we just joined it auto isYourJoin(const mtx::events::StateEvent<mtx::events::state::Member> &e) { - return e.content.membership == mtx::events::state::Membership::Join && - e.state_key == http::client()->user_id().to_string(); + return e.content.membership == mtx::events::state::Membership::Join && + e.state_key == http::client()->user_id().to_string(); } template<typename T> auto isYourJoin(const mtx::events::Event<T> &) { - return false; + return false; } void TimelineModel::updateLastMessage() { - for (auto it = events.size() - 1; it >= 0; --it) { - auto event = events.get(it, decryptDescription); - if (!event) - continue; - - if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) { - auto time = mtx::accessors::origin_server_ts(*event); - uint64_t ts = time.toMSecsSinceEpoch(); - auto description = - DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)), - QString::fromStdString(http::client()->user_id().to_string()), - tr("You joined this room."), - utils::descriptiveTime(time), - ts, - time}; - if (description != lastMessage_) { - lastMessage_ = description; - emit lastMessageChanged(); - } - return; - } - if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event)) - continue; - - auto description = utils::getMessageDescription( - *event, - QString::fromStdString(http::client()->user_id().to_string()), - cache::displayName(room_id_, - QString::fromStdString(mtx::accessors::sender(*event)))); - if (description != lastMessage_) { - lastMessage_ = description; - emit lastMessageChanged(); - } - return; + for (auto it = events.size() - 1; it >= 0; --it) { + auto event = events.get(it, decryptDescription); + if (!event) + continue; + + if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) { + auto time = mtx::accessors::origin_server_ts(*event); + uint64_t ts = time.toMSecsSinceEpoch(); + auto description = + DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)), + QString::fromStdString(http::client()->user_id().to_string()), + tr("You joined this room."), + utils::descriptiveTime(time), + ts, + time}; + if (description != lastMessage_) { + lastMessage_ = description; + emit lastMessageChanged(); + } + return; + } + if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event)) + continue; + + auto description = utils::getMessageDescription( + *event, + QString::fromStdString(http::client()->user_id().to_string()), + cache::displayName(room_id_, QString::fromStdString(mtx::accessors::sender(*event)))); + if (description != lastMessage_) { + lastMessage_ = description; + emit lastMessageChanged(); } + return; + } } void TimelineModel::setCurrentIndex(int index) { - auto oldIndex = idToIndex(currentId); - currentId = indexToId(index); - if (index != oldIndex) - emit currentIndexChanged(index); + auto oldIndex = idToIndex(currentId); + currentId = indexToId(index); + if (index != oldIndex) + emit currentIndexChanged(index); - if (!ChatPage::instance()->isActiveWindow()) - return; + if (!ChatPage::instance()->isActiveWindow()) + return; - if (!currentId.startsWith("m")) { - auto oldReadIndex = - cache::getEventIndex(roomId().toStdString(), currentReadId.toStdString()); - auto nextEventIndexAndId = - cache::lastInvisibleEventAfter(roomId().toStdString(), currentId.toStdString()); + if (!currentId.startsWith("m")) { + auto oldReadIndex = + cache::getEventIndex(roomId().toStdString(), currentReadId.toStdString()); + auto nextEventIndexAndId = + cache::lastInvisibleEventAfter(roomId().toStdString(), currentId.toStdString()); - if (nextEventIndexAndId && - (!oldReadIndex || *oldReadIndex < nextEventIndexAndId->first)) { - readEvent(nextEventIndexAndId->second); - currentReadId = QString::fromStdString(nextEventIndexAndId->second); - } + if (nextEventIndexAndId && (!oldReadIndex || *oldReadIndex < nextEventIndexAndId->first)) { + readEvent(nextEventIndexAndId->second); + currentReadId = QString::fromStdString(nextEventIndexAndId->second); } + } } void TimelineModel::readEvent(const std::string &id) { - http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to read_event ({}, {})", - room_id_.toStdString(), - currentId.toStdString()); - } - }); + http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "failed to read_event ({}, {})", room_id_.toStdString(), currentId.toStdString()); + } + }); } QString TimelineModel::displayName(QString id) const { - return cache::displayName(room_id_, id).toHtmlEscaped(); + return cache::displayName(room_id_, id).toHtmlEscaped(); } QString TimelineModel::avatarUrl(QString id) const { - return cache::avatarUrl(room_id_, id); + return cache::avatarUrl(room_id_, id); } QString TimelineModel::formatDateSeparator(QDate date) const { - auto now = QDateTime::currentDateTime(); + auto now = QDateTime::currentDateTime(); - QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); + QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); - if (now.date().year() == date.year()) { - QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); - fmt = fmt.remove(rx); - } + if (now.date().year() == date.year()) { + QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); + fmt = fmt.remove(rx); + } - return date.toString(fmt); + return date.toString(fmt); } void TimelineModel::viewRawMessage(QString id) { - auto e = events.get(id.toStdString(), "", false); - if (!e) - return; - std::string ev = mtx::accessors::serialize_event(*e).dump(4); - emit showRawMessageDialog(QString::fromStdString(ev)); + auto e = events.get(id.toStdString(), "", false); + if (!e) + return; + std::string ev = mtx::accessors::serialize_event(*e).dump(4); + emit showRawMessageDialog(QString::fromStdString(ev)); } void TimelineModel::forwardMessage(QString eventId, QString roomId) { - auto e = events.get(eventId.toStdString(), ""); - if (!e) - return; + auto e = events.get(eventId.toStdString(), ""); + if (!e) + return; - emit forwardToRoom(e, roomId); + emit forwardToRoom(e, roomId); } void TimelineModel::viewDecryptedRawMessage(QString id) { - auto e = events.get(id.toStdString(), ""); - if (!e) - return; + auto e = events.get(id.toStdString(), ""); + if (!e) + return; - std::string ev = mtx::accessors::serialize_event(*e).dump(4); - emit showRawMessageDialog(QString::fromStdString(ev)); + std::string ev = mtx::accessors::serialize_event(*e).dump(4); + emit showRawMessageDialog(QString::fromStdString(ev)); } void TimelineModel::openUserProfile(QString userid) { - UserProfile *userProfile = new UserProfile(room_id_, userid, manager_, this); - connect( - this, &TimelineModel::roomAvatarUrlChanged, userProfile, &UserProfile::updateAvatarUrl); - emit manager_->openProfile(userProfile); + UserProfile *userProfile = new UserProfile(room_id_, userid, manager_, this); + connect(this, &TimelineModel::roomAvatarUrlChanged, userProfile, &UserProfile::updateAvatarUrl); + emit manager_->openProfile(userProfile); } void TimelineModel::replyAction(QString id) { - setReply(id); + setReply(id); } void TimelineModel::editAction(QString id) { - setEdit(id); + setEdit(id); } RelatedInfo TimelineModel::relatedInfo(QString id) { - auto event = events.get(id.toStdString(), ""); - if (!event) - return {}; + auto event = events.get(id.toStdString(), ""); + if (!event) + return {}; - return utils::stripReplyFallbacks(*event, id.toStdString(), room_id_); + return utils::stripReplyFallbacks(*event, id.toStdString(), room_id_); } void TimelineModel::showReadReceipts(QString id) { - emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this}); + emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this}); } void TimelineModel::redactEvent(QString id) { - if (!id.isEmpty()) - http::client()->redact_event( - room_id_.toStdString(), - id.toStdString(), - [this, id](const mtx::responses::EventId &, mtx::http::RequestErr err) { - if (err) { - emit redactionFailed( - tr("Message redaction failed: %1") - .arg(QString::fromStdString(err->matrix_error.error))); - return; - } - - emit eventRedacted(id); - }); + if (!id.isEmpty()) + http::client()->redact_event( + room_id_.toStdString(), + id.toStdString(), + [this, id](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + emit redactionFailed(tr("Message redaction failed: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit eventRedacted(id); + }); } int TimelineModel::idToIndex(QString id) const { - if (id.isEmpty()) - return -1; + if (id.isEmpty()) + return -1; - auto idx = events.idToIndex(id.toStdString()); - if (idx) - return events.size() - *idx - 1; - else - return -1; + auto idx = events.idToIndex(id.toStdString()); + if (idx) + return events.size() - *idx - 1; + else + return -1; } QString TimelineModel::indexToId(int index) const { - auto id = events.indexToId(events.size() - index - 1); - return id ? QString::fromStdString(*id) : ""; + auto id = events.indexToId(events.size() - index - 1); + return id ? QString::fromStdString(*id) : ""; } // Note: this will only be called for our messages void TimelineModel::markEventsAsRead(const std::vector<QString> &event_ids) { - for (const auto &id : event_ids) { - read.insert(id); - int idx = idToIndex(id); - if (idx < 0) { - return; - } - emit dataChanged(index(idx, 0), index(idx, 0)); + for (const auto &id : event_ids) { + read.insert(id); + int idx = idToIndex(id); + if (idx < 0) { + return; } + emit dataChanged(index(idx, 0), index(idx, 0)); + } } template<typename T> void TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType) { - const auto room_id = room_id_.toStdString(); - - using namespace mtx::events; - using namespace mtx::identifiers; - - json doc = {{"type", mtx::events::to_string(eventType)}, - {"content", json(msg.content)}, - {"room_id", room_id}}; - - try { - mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event; - event.content = - olm::encrypt_group_message(room_id, http::client()->device_id(), doc); - event.event_id = msg.event_id; - event.room_id = room_id; - event.sender = http::client()->user_id().to_string(); - event.type = mtx::events::EventType::RoomEncrypted; - event.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); - - emit this->addPendingMessageToStore(event); - - // TODO: Let the user know about the errors. - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - emit ChatPage::instance()->showNotification( - tr("Failed to encrypt event, sending aborted!")); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - emit ChatPage::instance()->showNotification( - tr("Failed to encrypt event, sending aborted!")); - } -} - -struct SendMessageVisitor -{ - explicit SendMessageVisitor(TimelineModel *model) - : model_(model) - {} - - template<typename T, mtx::events::EventType Event> - void sendRoomEvent(mtx::events::RoomEvent<T> msg) - { - if (cache::isRoomEncrypted(model_->room_id_.toStdString())) { - auto encInfo = mtx::accessors::file(msg); - if (encInfo) - emit model_->newEncryptedImage(encInfo.value()); - - model_->sendEncryptedMessage(msg, Event); - } else { - msg.type = Event; - emit model_->addPendingMessageToStore(msg); - } - } - - // Do-nothing operator for all unhandled events - template<typename T> - void operator()(const mtx::events::Event<T> &) - {} - - // Operator for m.room.message events that contain a msgtype in their content - template<typename T, - std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0> - void operator()(mtx::events::RoomEvent<T> msg) - { - sendRoomEvent<T, mtx::events::EventType::RoomMessage>(msg); - } + const auto room_id = room_id_.toStdString(); - // Special operator for reactions, which are a type of m.room.message, but need to be - // handled distinctly for their differences from normal room messages. Specifically, - // reactions need to have the relation outside of ciphertext, or synapse / the homeserver - // cannot handle it correctly. See the MSC for more details: - // https://github.com/matrix-org/matrix-doc/blob/matthew/msc1849/proposals/1849-aggregations.md#end-to-end-encryption - void operator()(mtx::events::RoomEvent<mtx::events::msg::Reaction> msg) - { - msg.type = mtx::events::EventType::Reaction; - emit model_->addPendingMessageToStore(msg); - } - - void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &event) - { - sendRoomEvent<mtx::events::msg::CallInvite, mtx::events::EventType::CallInvite>( - event); - } - - void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &event) - { - sendRoomEvent<mtx::events::msg::CallCandidates, - mtx::events::EventType::CallCandidates>(event); - } + using namespace mtx::events; + using namespace mtx::identifiers; - void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &event) - { - sendRoomEvent<mtx::events::msg::CallAnswer, mtx::events::EventType::CallAnswer>( - event); - } + json doc = {{"type", mtx::events::to_string(eventType)}, + {"content", json(msg.content)}, + {"room_id", room_id}}; - void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &event) - { - sendRoomEvent<mtx::events::msg::CallHangUp, mtx::events::EventType::CallHangUp>( - event); - } + try { + mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event; + event.content = olm::encrypt_group_message(room_id, http::client()->device_id(), doc); + event.event_id = msg.event_id; + event.room_id = room_id; + event.sender = http::client()->user_id().to_string(); + event.type = mtx::events::EventType::RoomEncrypted; + event.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); - void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg) - { - sendRoomEvent<mtx::events::msg::KeyVerificationRequest, - mtx::events::EventType::RoomMessage>(msg); - } + emit this->addPendingMessageToStore(event); - void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg) - { - sendRoomEvent<mtx::events::msg::KeyVerificationReady, - mtx::events::EventType::KeyVerificationReady>(msg); - } - - void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg) - { - sendRoomEvent<mtx::events::msg::KeyVerificationStart, - mtx::events::EventType::KeyVerificationStart>(msg); - } - - void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg) - { - sendRoomEvent<mtx::events::msg::KeyVerificationAccept, - mtx::events::EventType::KeyVerificationAccept>(msg); - } - - void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg) - { - sendRoomEvent<mtx::events::msg::KeyVerificationMac, - mtx::events::EventType::KeyVerificationMac>(msg); - } + // TODO: Let the user know about the errors. + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to open outbound megolm session ({}): {}", room_id, e.what()); + emit ChatPage::instance()->showNotification( + tr("Failed to encrypt event, sending aborted!")); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + emit ChatPage::instance()->showNotification( + tr("Failed to encrypt event, sending aborted!")); + } +} - void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg) - { - sendRoomEvent<mtx::events::msg::KeyVerificationKey, - mtx::events::EventType::KeyVerificationKey>(msg); - } +struct SendMessageVisitor +{ + explicit SendMessageVisitor(TimelineModel *model) + : model_(model) + {} - void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg) - { - sendRoomEvent<mtx::events::msg::KeyVerificationDone, - mtx::events::EventType::KeyVerificationDone>(msg); - } + template<typename T, mtx::events::EventType Event> + void sendRoomEvent(mtx::events::RoomEvent<T> msg) + { + if (cache::isRoomEncrypted(model_->room_id_.toStdString())) { + auto encInfo = mtx::accessors::file(msg); + if (encInfo) + emit model_->newEncryptedImage(encInfo.value()); - void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg) - { - 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); - } + model_->sendEncryptedMessage(msg, Event); + } else { + msg.type = Event; + emit model_->addPendingMessageToStore(msg); + } + } + + // Do-nothing operator for all unhandled events + template<typename T> + void operator()(const mtx::events::Event<T> &) + {} + + // Operator for m.room.message events that contain a msgtype in their content + template<typename T, + std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0> + void operator()(mtx::events::RoomEvent<T> msg) + { + sendRoomEvent<T, mtx::events::EventType::RoomMessage>(msg); + } + + // Special operator for reactions, which are a type of m.room.message, but need to be + // handled distinctly for their differences from normal room messages. Specifically, + // reactions need to have the relation outside of ciphertext, or synapse / the homeserver + // cannot handle it correctly. See the MSC for more details: + // https://github.com/matrix-org/matrix-doc/blob/matthew/msc1849/proposals/1849-aggregations.md#end-to-end-encryption + void operator()(mtx::events::RoomEvent<mtx::events::msg::Reaction> msg) + { + msg.type = mtx::events::EventType::Reaction; + emit model_->addPendingMessageToStore(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &event) + { + sendRoomEvent<mtx::events::msg::CallInvite, mtx::events::EventType::CallInvite>(event); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &event) + { + sendRoomEvent<mtx::events::msg::CallCandidates, mtx::events::EventType::CallCandidates>( + event); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &event) + { + sendRoomEvent<mtx::events::msg::CallAnswer, mtx::events::EventType::CallAnswer>(event); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &event) + { + sendRoomEvent<mtx::events::msg::CallHangUp, mtx::events::EventType::CallHangUp>(event); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationRequest, + mtx::events::EventType::RoomMessage>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationReady, + mtx::events::EventType::KeyVerificationReady>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationStart, + mtx::events::EventType::KeyVerificationStart>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationAccept, + mtx::events::EventType::KeyVerificationAccept>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationMac, + mtx::events::EventType::KeyVerificationMac>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationKey, + mtx::events::EventType::KeyVerificationKey>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationDone, + mtx::events::EventType::KeyVerificationDone>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg) + { + 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_; + TimelineModel *model_; }; void 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(); - msg.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); - }, - 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(); + msg.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); + }, + event); - std::visit(SendMessageVisitor{this}, event); + std::visit(SendMessageVisitor{this}, event); } void TimelineModel::openMedia(QString eventId) { - cacheMedia(eventId, [](QString filename) { - QDesktopServices::openUrl(QUrl::fromLocalFile(filename)); - }); + cacheMedia(eventId, + [](QString filename) { QDesktopServices::openUrl(QUrl::fromLocalFile(filename)); }); } bool TimelineModel::saveMedia(QString eventId) const { - mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), ""); - if (!event) - return false; + mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), ""); + if (!event) + return false; - QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event)); - QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event)); - QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)); + QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event)); + QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event)); + QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)); - auto encryptionInfo = mtx::accessors::file(*event); + auto encryptionInfo = mtx::accessors::file(*event); - qml_mtx_events::EventType eventType = toRoomEventType(*event); + qml_mtx_events::EventType eventType = toRoomEventType(*event); - QString dialogTitle; - if (eventType == qml_mtx_events::EventType::ImageMessage) { - dialogTitle = tr("Save image"); - } else if (eventType == qml_mtx_events::EventType::VideoMessage) { - dialogTitle = tr("Save video"); - } else if (eventType == qml_mtx_events::EventType::AudioMessage) { - dialogTitle = tr("Save audio"); - } else { - dialogTitle = tr("Save file"); - } + QString dialogTitle; + if (eventType == qml_mtx_events::EventType::ImageMessage) { + dialogTitle = tr("Save image"); + } else if (eventType == qml_mtx_events::EventType::VideoMessage) { + dialogTitle = tr("Save video"); + } else if (eventType == qml_mtx_events::EventType::AudioMessage) { + dialogTitle = tr("Save audio"); + } else { + dialogTitle = tr("Save file"); + } - const QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); - const QString downloadsFolder = - QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); - const QString openLocation = downloadsFolder + "/" + originalFilename; - - const QString filename = QFileDialog::getSaveFileName( - manager_->getWidget(), dialogTitle, openLocation, filterString); - - if (filename.isEmpty()) - return false; - - const auto url = mxcUrl.toStdString(); - - http::client()->download( - url, - [filename, url, encryptionInfo](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } + const QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); + const QString downloadsFolder = + QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + const QString openLocation = downloadsFolder + "/" + originalFilename; - try { - auto temp = data; - if (encryptionInfo) - temp = mtx::crypto::to_string( - mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + const QString filename = + QFileDialog::getSaveFileName(manager_->getWidget(), dialogTitle, openLocation, filterString); - QFile file(filename); + if (filename.isEmpty()) + return false; - if (!file.open(QIODevice::WriteOnly)) - return; + const auto url = mxcUrl.toStdString(); - file.write(QByteArray(temp.data(), (int)temp.size())); - file.close(); + http::client()->download(url, + [filename, url, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } - return; - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } - }); - return true; + try { + auto temp = data; + if (encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(temp.data(), (int)temp.size())); + file.close(); + + return; + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + }); + return true; } void TimelineModel::cacheMedia(QString eventId, std::function<void(const QString)> callback) { - mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), ""); - if (!event) - return; + mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), ""); + if (!event) + return; - QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event)); - QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event)); - QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)); + QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event)); + QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event)); + QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)); - auto encryptionInfo = mtx::accessors::file(*event); + auto encryptionInfo = mtx::accessors::file(*event); - // If the message is a link to a non mxcUrl, don't download it - if (!mxcUrl.startsWith("mxc://")) { - emit mediaCached(mxcUrl, mxcUrl); - return; - } - - QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); - - const auto url = mxcUrl.toStdString(); - const auto name = QString(mxcUrl).remove("mxc://"); - QFileInfo filename(QString("%1/media_cache/%2.%3") - .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .arg(name) - .arg(suffix)); - if (QDir::cleanPath(name) != name) { - nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); - return; - } + // If the message is a link to a non mxcUrl, don't download it + if (!mxcUrl.startsWith("mxc://")) { + emit mediaCached(mxcUrl, mxcUrl); + return; + } + + QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); + + const auto url = mxcUrl.toStdString(); + const auto name = QString(mxcUrl).remove("mxc://"); + QFileInfo filename(QString("%1/media_cache/%2.%3") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .arg(name) + .arg(suffix)); + if (QDir::cleanPath(name) != name) { + nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); + return; + } - QDir().mkpath(filename.path()); + QDir().mkpath(filename.path()); - if (filename.isReadable()) { + if (filename.isReadable()) { #if defined(Q_OS_WIN) - emit mediaCached(mxcUrl, filename.filePath()); + emit mediaCached(mxcUrl, filename.filePath()); #else - emit mediaCached(mxcUrl, "file://" + filename.filePath()); + emit mediaCached(mxcUrl, "file://" + filename.filePath()); #endif - if (callback) { - callback(filename.filePath()); - } - return; + if (callback) { + callback(filename.filePath()); } - - http::client()->download( - url, - [this, callback, mxcUrl, filename, url, encryptionInfo](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - try { - auto temp = data; - if (encryptionInfo) - temp = mtx::crypto::to_string( - mtx::crypto::decrypt_file(temp, encryptionInfo.value())); - - QFile file(filename.filePath()); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(QByteArray(temp.data(), (int)temp.size())); - file.close(); - - if (callback) { - callback(filename.filePath()); - } - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } + return; + } + + http::client()->download( + url, + [this, callback, mxcUrl, filename, url, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + try { + auto temp = data; + if (encryptionInfo) + temp = + mtx::crypto::to_string(mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename.filePath()); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(temp.data(), (int)temp.size())); + file.close(); + + if (callback) { + callback(filename.filePath()); + } + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } #if defined(Q_OS_WIN) - emit mediaCached(mxcUrl, filename.filePath()); + emit mediaCached(mxcUrl, filename.filePath()); #else - emit mediaCached(mxcUrl, "file://" + filename.filePath()); + emit mediaCached(mxcUrl, "file://" + filename.filePath()); #endif - }); + }); } void TimelineModel::cacheMedia(QString eventId) { - cacheMedia(eventId, NULL); + cacheMedia(eventId, NULL); } void TimelineModel::showEvent(QString eventId) { - using namespace std::chrono_literals; - // Direct to eventId - if (eventId[0] == '$') { - int idx = idToIndex(eventId); - if (idx == -1) { - nhlog::ui()->warn("Scrolling to event id {}, failed - no known index", - eventId.toStdString()); - return; - } - eventIdToShow = eventId; - emit scrollTargetChanged(); - showEventTimer.start(50ms); - return; + using namespace std::chrono_literals; + // Direct to eventId + if (eventId[0] == '$') { + int idx = idToIndex(eventId); + if (idx == -1) { + nhlog::ui()->warn("Scrolling to event id {}, failed - no known index", + eventId.toStdString()); + return; } - // to message index - eventId = indexToId(eventId.toInt()); eventIdToShow = eventId; emit scrollTargetChanged(); showEventTimer.start(50ms); return; + } + // to message index + eventId = indexToId(eventId.toInt()); + eventIdToShow = eventId; + emit scrollTargetChanged(); + showEventTimer.start(50ms); + return; } void TimelineModel::eventShown() { - eventIdToShow.clear(); - emit scrollTargetChanged(); + eventIdToShow.clear(); + emit scrollTargetChanged(); } QString TimelineModel::scrollTarget() const { - return eventIdToShow; + return eventIdToShow; } void TimelineModel::scrollTimerEvent() { - if (eventIdToShow.isEmpty() || showEventTimerCounter > 3) { - showEventTimer.stop(); - showEventTimerCounter = 0; - } else { - emit scrollToIndex(idToIndex(eventIdToShow)); - showEventTimerCounter++; - } + if (eventIdToShow.isEmpty() || showEventTimerCounter > 3) { + showEventTimer.stop(); + showEventTimerCounter = 0; + } else { + emit scrollToIndex(idToIndex(eventIdToShow)); + showEventTimerCounter++; + } } void TimelineModel::requestKeyForEvent(QString id) { - auto encrypted_event = events.get(id.toStdString(), "", false); - if (encrypted_event) { - if (auto ev = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( - encrypted_event)) - events.requestSession(*ev, true); - } + auto encrypted_event = events.get(id.toStdString(), "", false); + if (encrypted_event) { + if (auto ev = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + encrypted_event)) + events.requestSession(*ev, true); + } } void TimelineModel::copyLinkToEvent(QString eventId) const { - QStringList vias; + QStringList vias; - auto alias = cache::client()->getRoomAliases(room_id_.toStdString()); - QString room; - if (alias) { - room = QString::fromStdString(alias->alias); - if (room.isEmpty() && !alias->alt_aliases.empty()) { - room = QString::fromStdString(alias->alt_aliases.front()); - } + auto alias = cache::client()->getRoomAliases(room_id_.toStdString()); + QString room; + if (alias) { + room = QString::fromStdString(alias->alias); + if (room.isEmpty() && !alias->alt_aliases.empty()) { + room = QString::fromStdString(alias->alt_aliases.front()); } + } - if (room.isEmpty()) - room = room_id_; + if (room.isEmpty()) + room = room_id_; - vias.push_back(QString("via=%1").arg(QString( - QUrl::toPercentEncoding(QString::fromStdString(http::client()->user_id().hostname()))))); - auto members = cache::getMembers(room_id_.toStdString(), 0, 100); - for (const auto &m : members) { - if (vias.size() >= 4) - break; + vias.push_back(QString("via=%1").arg(QString( + QUrl::toPercentEncoding(QString::fromStdString(http::client()->user_id().hostname()))))); + auto members = cache::getMembers(room_id_.toStdString(), 0, 100); + for (const auto &m : members) { + if (vias.size() >= 4) + break; - auto user_id = - mtx::identifiers::parse<mtx::identifiers::User>(m.user_id.toStdString()); - QString server = QString("via=%1").arg( - QString(QUrl::toPercentEncoding(QString::fromStdString(user_id.hostname())))); + auto user_id = mtx::identifiers::parse<mtx::identifiers::User>(m.user_id.toStdString()); + QString server = QString("via=%1").arg( + QString(QUrl::toPercentEncoding(QString::fromStdString(user_id.hostname())))); - if (!vias.contains(server)) - vias.push_back(server); - } + if (!vias.contains(server)) + vias.push_back(server); + } - auto link = QString("https://matrix.to/#/%1/%2?%3") - .arg(QString(QUrl::toPercentEncoding(room)), - QString(QUrl::toPercentEncoding(eventId)), - vias.join('&')); + auto link = QString("https://matrix.to/#/%1/%2?%3") + .arg(QString(QUrl::toPercentEncoding(room)), + QString(QUrl::toPercentEncoding(eventId)), + vias.join('&')); - QGuiApplication::clipboard()->setText(link); + QGuiApplication::clipboard()->setText(link); } QString TimelineModel::formatTypingUsers(const std::vector<QString> &users, QColor bg) { - QString temp = - tr("%1 and %2 are typing.", - "Multiple users are typing. First argument is a comma separated list of potentially " - "multiple users. Second argument is the last user of that list. (If only one user is " - "typing, %1 is empty. You should still use it in your string though to silence Qt " - "warnings.)", - (int)users.size()); + QString temp = + tr("%1 and %2 are typing.", + "Multiple users are typing. First argument is a comma separated list of potentially " + "multiple users. Second argument is the last user of that list. (If only one user is " + "typing, %1 is empty. You should still use it in your string though to silence Qt " + "warnings.)", + (int)users.size()); - if (users.empty()) { - return ""; - } + if (users.empty()) { + return ""; + } - QStringList uidWithoutLast; + QStringList uidWithoutLast; - auto formatUser = [this, bg](const QString &user_id) -> QString { - auto uncoloredUsername = utils::replaceEmoji(displayName(user_id)); - QString prefix = - QString("<font color=\"%1\">").arg(manager_->userColor(user_id, bg).name()); + auto formatUser = [this, bg](const QString &user_id) -> QString { + auto uncoloredUsername = utils::replaceEmoji(displayName(user_id)); + QString prefix = + QString("<font color=\"%1\">").arg(manager_->userColor(user_id, bg).name()); - // color only parts that don't have a font already specified - QString coloredUsername; - int index = 0; - do { - auto startIndex = uncoloredUsername.indexOf("<font", index); + // color only parts that don't have a font already specified + QString coloredUsername; + int index = 0; + do { + auto startIndex = uncoloredUsername.indexOf("<font", index); - if (startIndex - index != 0) - coloredUsername += - prefix + - uncoloredUsername.midRef( - index, startIndex > 0 ? startIndex - index : -1) + - "</font>"; + if (startIndex - index != 0) + coloredUsername += + prefix + + uncoloredUsername.midRef(index, startIndex > 0 ? startIndex - index : -1) + + "</font>"; - auto endIndex = uncoloredUsername.indexOf("</font>", startIndex); - if (endIndex > 0) - endIndex += sizeof("</font>") - 1; + auto endIndex = uncoloredUsername.indexOf("</font>", startIndex); + if (endIndex > 0) + endIndex += sizeof("</font>") - 1; - if (endIndex - startIndex != 0) - coloredUsername += - uncoloredUsername.midRef(startIndex, endIndex - startIndex); + if (endIndex - startIndex != 0) + coloredUsername += uncoloredUsername.midRef(startIndex, endIndex - startIndex); - index = endIndex; - } while (index > 0 && index < uncoloredUsername.size()); + index = endIndex; + } while (index > 0 && index < uncoloredUsername.size()); - return coloredUsername; - }; + return coloredUsername; + }; - for (size_t i = 0; i + 1 < users.size(); i++) { - uidWithoutLast.append(formatUser(users[i])); - } + for (size_t i = 0; i + 1 < users.size(); i++) { + uidWithoutLast.append(formatUser(users[i])); + } - return temp.arg(uidWithoutLast.join(", ")).arg(formatUser(users.back())); + return temp.arg(uidWithoutLast.join(", ")).arg(formatUser(users.back())); } QString TimelineModel::formatJoinRuleEvent(QString id) { - mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); - if (!e) - return ""; - - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::JoinRules>>(e); - if (!event) - return ""; - - QString user = QString::fromStdString(event->sender); - QString name = utils::replaceEmoji(displayName(user)); - - switch (event->content.join_rule) { - case mtx::events::state::JoinRule::Public: - return tr("%1 opened the room to the public.").arg(name); - case mtx::events::state::JoinRule::Invite: - return tr("%1 made this room require and invitation to join.").arg(name); - case mtx::events::state::JoinRule::Knock: - return tr("%1 allowed to join this room by knocking.").arg(name); - case mtx::events::state::JoinRule::Restricted: { - QStringList rooms; - for (const auto &r : event->content.allow) { - if (r.type == mtx::events::state::JoinAllowanceType::RoomMembership) - rooms.push_back(QString::fromStdString(r.room_id)); - } - return tr("%1 allowed members of the following rooms to automatically join this " - "room: %2") - .arg(name) - .arg(rooms.join(", ")); - } - default: - // Currently, knock and private are reserved keywords and not implemented in Matrix. - return ""; - } + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) + return ""; + + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::JoinRules>>(e); + if (!event) + return ""; + + QString user = QString::fromStdString(event->sender); + QString name = utils::replaceEmoji(displayName(user)); + + switch (event->content.join_rule) { + case mtx::events::state::JoinRule::Public: + return tr("%1 opened the room to the public.").arg(name); + case mtx::events::state::JoinRule::Invite: + return tr("%1 made this room require and invitation to join.").arg(name); + case mtx::events::state::JoinRule::Knock: + return tr("%1 allowed to join this room by knocking.").arg(name); + case mtx::events::state::JoinRule::Restricted: { + QStringList rooms; + for (const auto &r : event->content.allow) { + if (r.type == mtx::events::state::JoinAllowanceType::RoomMembership) + rooms.push_back(QString::fromStdString(r.room_id)); + } + return tr("%1 allowed members of the following rooms to automatically join this " + "room: %2") + .arg(name) + .arg(rooms.join(", ")); + } + default: + // Currently, knock and private are reserved keywords and not implemented in Matrix. + return ""; + } } QString TimelineModel::formatGuestAccessEvent(QString id) { - mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); - if (!e) - return ""; + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) + return ""; - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::GuestAccess>>(e); - if (!event) - return ""; + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::GuestAccess>>(e); + if (!event) + return ""; - QString user = QString::fromStdString(event->sender); - QString name = utils::replaceEmoji(displayName(user)); + QString user = QString::fromStdString(event->sender); + QString name = utils::replaceEmoji(displayName(user)); - switch (event->content.guest_access) { - case mtx::events::state::AccessState::CanJoin: - return tr("%1 made the room open to guests.").arg(name); - case mtx::events::state::AccessState::Forbidden: - return tr("%1 has closed the room to guest access.").arg(name); - default: - return ""; - } + switch (event->content.guest_access) { + case mtx::events::state::AccessState::CanJoin: + return tr("%1 made the room open to guests.").arg(name); + case mtx::events::state::AccessState::Forbidden: + return tr("%1 has closed the room to guest access.").arg(name); + default: + return ""; + } } QString TimelineModel::formatHistoryVisibilityEvent(QString id) { - mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); - if (!e) - return ""; + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) + return ""; - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::HistoryVisibility>>(e); + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::HistoryVisibility>>(e); - if (!event) - return ""; - - QString user = QString::fromStdString(event->sender); - QString name = utils::replaceEmoji(displayName(user)); - - switch (event->content.history_visibility) { - case mtx::events::state::Visibility::WorldReadable: - return tr("%1 made the room history world readable. Events may be now read by " - "non-joined people.") - .arg(name); - case mtx::events::state::Visibility::Shared: - return tr("%1 set the room history visible to members from this point on.") - .arg(name); - case mtx::events::state::Visibility::Invited: - return tr("%1 set the room history visible to members since they were invited.") - .arg(name); - case mtx::events::state::Visibility::Joined: - return tr("%1 set the room history visible to members since they joined the room.") - .arg(name); - default: - return ""; - } + if (!event) + return ""; + + QString user = QString::fromStdString(event->sender); + QString name = utils::replaceEmoji(displayName(user)); + + switch (event->content.history_visibility) { + case mtx::events::state::Visibility::WorldReadable: + return tr("%1 made the room history world readable. Events may be now read by " + "non-joined people.") + .arg(name); + case mtx::events::state::Visibility::Shared: + return tr("%1 set the room history visible to members from this point on.").arg(name); + case mtx::events::state::Visibility::Invited: + return tr("%1 set the room history visible to members since they were invited.").arg(name); + case mtx::events::state::Visibility::Joined: + return tr("%1 set the room history visible to members since they joined the room.") + .arg(name); + default: + return ""; + } } QString TimelineModel::formatPowerLevelEvent(QString id) { - mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); - if (!e) - return ""; + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) + return ""; - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(e); - if (!event) - return ""; + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(e); + if (!event) + return ""; - QString user = QString::fromStdString(event->sender); - QString name = utils::replaceEmoji(displayName(user)); + QString user = QString::fromStdString(event->sender); + QString name = utils::replaceEmoji(displayName(user)); - // TODO: power levels rendering is actually a bit complex. work on this later. - return tr("%1 has changed the room's permissions.").arg(name); + // TODO: power levels rendering is actually a bit complex. work on this later. + return tr("%1 has changed the room's permissions.").arg(name); } void TimelineModel::acceptKnock(QString id) { - mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); - if (!e) - return; + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) + return; - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e); - if (!event) - return; + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e); + if (!event) + return; - if (!permissions_.canInvite()) - return; + if (!permissions_.canInvite()) + return; - if (cache::isRoomMember(event->state_key, room_id_.toStdString())) - return; + if (cache::isRoomMember(event->state_key, room_id_.toStdString())) + return; - using namespace mtx::events::state; - if (event->content.membership != Membership::Knock) - return; + using namespace mtx::events::state; + if (event->content.membership != Membership::Knock) + return; - ChatPage::instance()->inviteUser(QString::fromStdString(event->state_key), ""); + ChatPage::instance()->inviteUser(QString::fromStdString(event->state_key), ""); } bool TimelineModel::showAcceptKnockButton(QString id) { - mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); - if (!e) - return false; + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) + return false; - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e); - if (!event) - return false; + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e); + if (!event) + return false; - if (!permissions_.canInvite()) - return false; + if (!permissions_.canInvite()) + return false; - if (cache::isRoomMember(event->state_key, room_id_.toStdString())) - return false; + if (cache::isRoomMember(event->state_key, room_id_.toStdString())) + return false; - using namespace mtx::events::state; - return event->content.membership == Membership::Knock; + using namespace mtx::events::state; + return event->content.membership == Membership::Knock; } QString TimelineModel::formatMemberEvent(QString id) { - mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); - if (!e) - return ""; - - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e); - if (!event) - return ""; - - mtx::events::StateEvent<mtx::events::state::Member> *prevEvent = nullptr; - if (!event->unsigned_data.replaces_state.empty()) { - auto tempPrevEvent = - events.get(event->unsigned_data.replaces_state, event->event_id); - if (tempPrevEvent) { - prevEvent = - std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>( - tempPrevEvent); - } - } + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) + return ""; + + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e); + if (!event) + return ""; + + mtx::events::StateEvent<mtx::events::state::Member> *prevEvent = nullptr; + if (!event->unsigned_data.replaces_state.empty()) { + auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id); + if (tempPrevEvent) { + prevEvent = + std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(tempPrevEvent); + } + } + + QString user = QString::fromStdString(event->state_key); + QString name = utils::replaceEmoji(displayName(user)); + QString rendered; + + // see table https://matrix.org/docs/spec/client_server/latest#m-room-member + using namespace mtx::events::state; + switch (event->content.membership) { + case Membership::Invite: + rendered = tr("%1 was invited.").arg(name); + break; + case Membership::Join: + if (prevEvent && prevEvent->content.membership == Membership::Join) { + QString oldName = utils::replaceEmoji( + QString::fromStdString(prevEvent->content.display_name).toHtmlEscaped()); + + bool displayNameChanged = + prevEvent->content.display_name != event->content.display_name; + bool avatarChanged = prevEvent->content.avatar_url != event->content.avatar_url; + + if (displayNameChanged && avatarChanged) + rendered = tr("%1 has changed their avatar and changed their " + "display name to %2.") + .arg(oldName, name); + else if (displayNameChanged) + rendered = tr("%1 has changed their display name to %2.").arg(oldName, name); + else if (avatarChanged) + rendered = tr("%1 changed their avatar.").arg(name); + else + rendered = tr("%1 changed some profile info.").arg(name); + // the case of nothing changed but join follows join shouldn't happen, so + // just show it as join + } else { + if (event->content.join_authorised_via_users_server.empty()) + rendered = tr("%1 joined.").arg(name); + else + rendered = + tr("%1 joined via authorisation from %2's server.") + .arg(name) + .arg(QString::fromStdString(event->content.join_authorised_via_users_server)); + } + break; + case Membership::Leave: + if (!prevEvent) // Should only ever happen temporarily + return ""; + + if (prevEvent->content.membership == Membership::Invite) { + if (event->state_key == event->sender) + rendered = tr("%1 rejected their invite.").arg(name); + else + rendered = tr("Revoked the invite to %1.").arg(name); + } else if (prevEvent->content.membership == Membership::Join) { + if (event->state_key == event->sender) + rendered = tr("%1 left the room.").arg(name); + else + rendered = tr("Kicked %1.").arg(name); + } else if (prevEvent->content.membership == Membership::Ban) { + rendered = tr("Unbanned %1.").arg(name); + } else if (prevEvent->content.membership == Membership::Knock) { + if (event->state_key == event->sender) + rendered = tr("%1 redacted their knock.").arg(name); + else + rendered = tr("Rejected the knock from %1.").arg(name); + } else + return tr("%1 left after having already left!", + "This is a leave event after the user already left and shouldn't " + "happen apart from state resets") + .arg(name); + break; - QString user = QString::fromStdString(event->state_key); - QString name = utils::replaceEmoji(displayName(user)); - QString rendered; - - // see table https://matrix.org/docs/spec/client_server/latest#m-room-member - using namespace mtx::events::state; - switch (event->content.membership) { - case Membership::Invite: - rendered = tr("%1 was invited.").arg(name); - break; - case Membership::Join: - if (prevEvent && prevEvent->content.membership == Membership::Join) { - QString oldName = utils::replaceEmoji( - QString::fromStdString(prevEvent->content.display_name).toHtmlEscaped()); - - bool displayNameChanged = - prevEvent->content.display_name != event->content.display_name; - bool avatarChanged = - prevEvent->content.avatar_url != event->content.avatar_url; - - if (displayNameChanged && avatarChanged) - rendered = tr("%1 has changed their avatar and changed their " - "display name to %2.") - .arg(oldName, name); - else if (displayNameChanged) - rendered = - tr("%1 has changed their display name to %2.").arg(oldName, name); - else if (avatarChanged) - rendered = tr("%1 changed their avatar.").arg(name); - else - rendered = tr("%1 changed some profile info.").arg(name); - // the case of nothing changed but join follows join shouldn't happen, so - // just show it as join - } else { - if (event->content.join_authorised_via_users_server.empty()) - rendered = tr("%1 joined.").arg(name); - else - rendered = tr("%1 joined via authorisation from %2's server.") - .arg(name) - .arg(QString::fromStdString( - event->content.join_authorised_via_users_server)); - } - break; - case Membership::Leave: - if (!prevEvent) // Should only ever happen temporarily - return ""; - - if (prevEvent->content.membership == Membership::Invite) { - if (event->state_key == event->sender) - rendered = tr("%1 rejected their invite.").arg(name); - else - rendered = tr("Revoked the invite to %1.").arg(name); - } else if (prevEvent->content.membership == Membership::Join) { - if (event->state_key == event->sender) - rendered = tr("%1 left the room.").arg(name); - else - rendered = tr("Kicked %1.").arg(name); - } else if (prevEvent->content.membership == Membership::Ban) { - rendered = tr("Unbanned %1.").arg(name); - } else if (prevEvent->content.membership == Membership::Knock) { - if (event->state_key == event->sender) - rendered = tr("%1 redacted their knock.").arg(name); - else - rendered = tr("Rejected the knock from %1.").arg(name); - } else - return tr("%1 left after having already left!", - "This is a leave event after the user already left and shouldn't " - "happen apart from state resets") - .arg(name); - break; - - case Membership::Ban: - rendered = tr("%1 was banned.").arg(name); - break; - case Membership::Knock: - rendered = tr("%1 knocked.").arg(name); - break; - } + case Membership::Ban: + rendered = tr("%1 was banned.").arg(name); + break; + case Membership::Knock: + rendered = tr("%1 knocked.").arg(name); + break; + } - if (event->content.reason != "") { - rendered += - " " + tr("Reason: %1").arg(QString::fromStdString(event->content.reason)); - } + if (event->content.reason != "") { + rendered += " " + tr("Reason: %1").arg(QString::fromStdString(event->content.reason)); + } - return rendered; + return rendered; } void TimelineModel::setEdit(QString newEdit) { - if (newEdit.isEmpty()) { - resetEdit(); - return; - } + if (newEdit.isEmpty()) { + resetEdit(); + return; + } + + if (edit_.isEmpty()) { + this->textBeforeEdit = input()->text(); + this->replyBeforeEdit = reply_; + nhlog::ui()->debug("Stored: {}", textBeforeEdit.toStdString()); + } + + if (edit_ != newEdit) { + auto ev = events.get(newEdit.toStdString(), ""); + if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) { + auto e = *ev; + setReply(QString::fromStdString(mtx::accessors::relations(e).reply_to().value_or(""))); + + auto msgType = mtx::accessors::msg_type(e); + if (msgType == mtx::events::MessageType::Text || + msgType == mtx::events::MessageType::Notice || + msgType == mtx::events::MessageType::Emote) { + auto relInfo = relatedInfo(newEdit); + auto editText = relInfo.quoted_body; + + if (!relInfo.quoted_formatted_body.isEmpty()) { + auto matches = + conf::strings::matrixToLink.globalMatch(relInfo.quoted_formatted_body); + std::map<QString, QString> reverseNameMapping; + while (matches.hasNext()) { + auto m = matches.next(); + reverseNameMapping[m.captured(2)] = m.captured(1); + } + + for (const auto &[user, link] : reverseNameMapping) { + // TODO(Nico): html unescape the user name + editText.replace(user, QStringLiteral("[%1](%2)").arg(user, link)); + } + } - if (edit_.isEmpty()) { - this->textBeforeEdit = input()->text(); - this->replyBeforeEdit = reply_; - nhlog::ui()->debug("Stored: {}", textBeforeEdit.toStdString()); - } + if (msgType == mtx::events::MessageType::Emote) + input()->setText("/me " + editText); + else + input()->setText(editText); + } else { + input()->setText(""); + } - if (edit_ != newEdit) { - auto ev = events.get(newEdit.toStdString(), ""); - if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) { - auto e = *ev; - setReply(QString::fromStdString( - mtx::accessors::relations(e).reply_to().value_or(""))); - - auto msgType = mtx::accessors::msg_type(e); - if (msgType == mtx::events::MessageType::Text || - msgType == mtx::events::MessageType::Notice || - msgType == mtx::events::MessageType::Emote) { - auto relInfo = relatedInfo(newEdit); - auto editText = relInfo.quoted_body; - - if (!relInfo.quoted_formatted_body.isEmpty()) { - auto matches = conf::strings::matrixToLink.globalMatch( - relInfo.quoted_formatted_body); - std::map<QString, QString> reverseNameMapping; - while (matches.hasNext()) { - auto m = matches.next(); - reverseNameMapping[m.captured(2)] = m.captured(1); - } - - for (const auto &[user, link] : reverseNameMapping) { - // TODO(Nico): html unescape the user name - editText.replace( - user, QStringLiteral("[%1](%2)").arg(user, link)); - } - } - - if (msgType == mtx::events::MessageType::Emote) - input()->setText("/me " + editText); - else - input()->setText(editText); - } else { - input()->setText(""); - } - - edit_ = newEdit; - } else { - resetReply(); - - input()->setText(""); - edit_ = ""; - } - emit editChanged(edit_); + edit_ = newEdit; + } else { + resetReply(); + + input()->setText(""); + edit_ = ""; } + emit editChanged(edit_); + } } void TimelineModel::resetEdit() { - if (!edit_.isEmpty()) { - edit_ = ""; - emit editChanged(edit_); - nhlog::ui()->debug("Restoring: {}", textBeforeEdit.toStdString()); - input()->setText(textBeforeEdit); - textBeforeEdit.clear(); - if (replyBeforeEdit.isEmpty()) - resetReply(); - else - setReply(replyBeforeEdit); - replyBeforeEdit.clear(); - } + if (!edit_.isEmpty()) { + edit_ = ""; + emit editChanged(edit_); + nhlog::ui()->debug("Restoring: {}", textBeforeEdit.toStdString()); + input()->setText(textBeforeEdit); + textBeforeEdit.clear(); + if (replyBeforeEdit.isEmpty()) + resetReply(); + else + setReply(replyBeforeEdit); + replyBeforeEdit.clear(); + } } QString TimelineModel::roomName() const { - auto info = cache::getRoomInfo({room_id_.toStdString()}); + auto info = cache::getRoomInfo({room_id_.toStdString()}); - if (!info.count(room_id_)) - return ""; - else - return utils::replaceEmoji( - QString::fromStdString(info[room_id_].name).toHtmlEscaped()); + if (!info.count(room_id_)) + return ""; + else + return utils::replaceEmoji(QString::fromStdString(info[room_id_].name).toHtmlEscaped()); } QString TimelineModel::plainRoomName() const { - auto info = cache::getRoomInfo({room_id_.toStdString()}); + auto info = cache::getRoomInfo({room_id_.toStdString()}); - if (!info.count(room_id_)) - return ""; - else - return QString::fromStdString(info[room_id_].name); + if (!info.count(room_id_)) + return ""; + else + return QString::fromStdString(info[room_id_].name); } QString TimelineModel::roomAvatarUrl() const { - auto info = cache::getRoomInfo({room_id_.toStdString()}); + auto info = cache::getRoomInfo({room_id_.toStdString()}); - if (!info.count(room_id_)) - return ""; - else - return QString::fromStdString(info[room_id_].avatar_url); + if (!info.count(room_id_)) + return ""; + else + return QString::fromStdString(info[room_id_].avatar_url); } QString TimelineModel::roomTopic() const { - auto info = cache::getRoomInfo({room_id_.toStdString()}); + auto info = cache::getRoomInfo({room_id_.toStdString()}); - if (!info.count(room_id_)) - return ""; - else - return utils::replaceEmoji(utils::linkifyMessage( - QString::fromStdString(info[room_id_].topic).toHtmlEscaped())); + if (!info.count(room_id_)) + return ""; + else + return utils::replaceEmoji( + utils::linkifyMessage(QString::fromStdString(info[room_id_].topic).toHtmlEscaped())); } crypto::Trust TimelineModel::trustlevel() const { - if (!isEncrypted_) - return crypto::Trust::Unverified; + if (!isEncrypted_) + return crypto::Trust::Unverified; - return cache::client()->roomVerificationStatus(room_id_.toStdString()); + return cache::client()->roomVerificationStatus(room_id_.toStdString()); } int TimelineModel::roomMemberCount() const { - return (int)cache::client()->memberCount(room_id_.toStdString()); + return (int)cache::client()->memberCount(room_id_.toStdString()); } QString TimelineModel::directChatOtherUserId() const { - if (roomMemberCount() < 3) { - QString id; - for (auto member : cache::getMembers(room_id_.toStdString())) - if (member.user_id != UserSettings::instance()->userId()) - id = member.user_id; - return id; - } else - return ""; + if (roomMemberCount() < 3) { + QString id; + for (auto member : cache::getMembers(room_id_.toStdString())) + if (member.user_id != UserSettings::instance()->userId()) + id = member.user_id; + return id; + } else + return ""; }