mirror of https://github.com/Nheko-Reborn/nheko
commit
b5669310e5
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,20 @@ |
||||
#pragma once |
||||
|
||||
// Class for showing a limited amount of completions at a time
|
||||
|
||||
#include <QSortFilterProxyModel> |
||||
|
||||
class CompletionModel : public QSortFilterProxyModel |
||||
{ |
||||
public: |
||||
CompletionModel(QAbstractItemModel *model, QObject *parent = nullptr) |
||||
: QSortFilterProxyModel(parent) |
||||
{ |
||||
setSourceModel(model); |
||||
} |
||||
int rowCount(const QModelIndex &parent) const override |
||||
{ |
||||
auto row_count = QSortFilterProxyModel::rowCount(parent); |
||||
return (row_count < 7) ? row_count : 7; |
||||
} |
||||
}; |
@ -0,0 +1,37 @@ |
||||
#pragma once |
||||
|
||||
#include "EmojiModel.h" |
||||
|
||||
#include <QDebug> |
||||
#include <QEvent> |
||||
#include <QSortFilterProxyModel> |
||||
|
||||
namespace emoji { |
||||
|
||||
// Map emoji data to searchable data
|
||||
class EmojiSearchModel : public QSortFilterProxyModel |
||||
{ |
||||
public: |
||||
EmojiSearchModel(QObject *parent = nullptr) |
||||
: QSortFilterProxyModel(parent) |
||||
{ |
||||
setSourceModel(new EmojiModel(this)); |
||||
} |
||||
QVariant data(const QModelIndex &index, int role = Qt::UserRole + 1) const override |
||||
{ |
||||
if (role == Qt::DisplayRole) { |
||||
auto emoji = QSortFilterProxyModel::data(index, role).toString(); |
||||
return emoji + " :" + |
||||
toShortcode(data(index, EmojiModel::ShortName).toString()) + ":"; |
||||
} |
||||
return QSortFilterProxyModel::data(index, role); |
||||
} |
||||
|
||||
private: |
||||
QString toShortcode(QString shortname) const |
||||
{ |
||||
return shortname.replace(" ", "-").replace(":", "-").replace("--", "-").toLower(); |
||||
} |
||||
}; |
||||
|
||||
} |
@ -0,0 +1,570 @@ |
||||
#include "EventStore.h" |
||||
|
||||
#include <QThread> |
||||
#include <QTimer> |
||||
|
||||
#include "Cache.h" |
||||
#include "Cache_p.h" |
||||
#include "EventAccessors.h" |
||||
#include "Logging.h" |
||||
#include "MatrixClient.h" |
||||
#include "Olm.h" |
||||
|
||||
Q_DECLARE_METATYPE(Reaction) |
||||
|
||||
QCache<EventStore::IdIndex, mtx::events::collections::TimelineEvents> EventStore::decryptedEvents_{ |
||||
1000}; |
||||
QCache<EventStore::IdIndex, mtx::events::collections::TimelineEvents> EventStore::events_by_id_{ |
||||
1000}; |
||||
QCache<EventStore::Index, mtx::events::collections::TimelineEvents> EventStore::events_{1000}; |
||||
|
||||
EventStore::EventStore(std::string room_id, QObject *) |
||||
: room_id_(std::move(room_id)) |
||||
{ |
||||
static auto reactionType = qRegisterMetaType<Reaction>(); |
||||
(void)reactionType; |
||||
|
||||
auto range = cache::client()->getTimelineRange(room_id_); |
||||
|
||||
if (range) { |
||||
this->first = range->first; |
||||
this->last = range->last; |
||||
} |
||||
|
||||
connect( |
||||
this, |
||||
&EventStore::eventFetched, |
||||
this, |
||||
[this](std::string id, |
||||
std::string relatedTo, |
||||
mtx::events::collections::TimelineEvents timeline) { |
||||
cache::client()->storeEvent(room_id_, id, {timeline}); |
||||
|
||||
if (!relatedTo.empty()) { |
||||
auto idx = idToIndex(relatedTo); |
||||
if (idx) |
||||
emit dataChanged(*idx, *idx); |
||||
} |
||||
}, |
||||
Qt::QueuedConnection); |
||||
|
||||
connect( |
||||
this, |
||||
&EventStore::oldMessagesRetrieved, |
||||
this, |
||||
[this](const mtx::responses::Messages &res) { |
||||
//
|
||||
uint64_t newFirst = cache::client()->saveOldMessages(room_id_, res); |
||||
if (newFirst == first && !res.chunk.empty()) |
||||
fetchMore(); |
||||
else { |
||||
emit beginInsertRows(toExternalIdx(newFirst), |
||||
toExternalIdx(this->first - 1)); |
||||
this->first = newFirst; |
||||
emit endInsertRows(); |
||||
emit fetchedMore(); |
||||
} |
||||
}, |
||||
Qt::QueuedConnection); |
||||
|
||||
connect(this, &EventStore::processPending, this, [this]() { |
||||
if (!current_txn.empty()) { |
||||
nhlog::ui()->debug("Already processing {}", current_txn); |
||||
return; |
||||
} |
||||
|
||||
auto event = cache::client()->firstPendingMessage(room_id_); |
||||
|
||||
if (!event) { |
||||
nhlog::ui()->debug("No event to send"); |
||||
return; |
||||
} |
||||
|
||||
std::visit( |
||||
[this](auto e) { |
||||
auto txn_id = e.event_id; |
||||
this->current_txn = txn_id; |
||||
|
||||
if (txn_id.empty() || txn_id[0] != 'm') { |
||||
nhlog::ui()->debug("Invalid txn id '{}'", txn_id); |
||||
cache::client()->removePendingStatus(room_id_, txn_id); |
||||
return; |
||||
} |
||||
|
||||
if constexpr (mtx::events::message_content_to_type<decltype(e.content)> != |
||||
mtx::events::EventType::Unsupported) |
||||
http::client()->send_room_message( |
||||
room_id_, |
||||
txn_id, |
||||
e.content, |
||||
[this, txn_id](const mtx::responses::EventId &event_id, |
||||
mtx::http::RequestErr err) { |
||||
if (err) { |
||||
const int status_code = |
||||
static_cast<int>(err->status_code); |
||||
nhlog::net()->warn( |
||||
"[{}] failed to send message: {} {}", |
||||
txn_id, |
||||
err->matrix_error.error, |
||||
status_code); |
||||
emit messageFailed(txn_id); |
||||
return; |
||||
} |
||||
emit messageSent(txn_id, event_id.event_id.to_string()); |
||||
}); |
||||
}, |
||||
event->data); |
||||
}); |
||||
|
||||
connect( |
||||
this, |
||||
&EventStore::messageFailed, |
||||
this, |
||||
[this](std::string txn_id) { |
||||
if (current_txn == txn_id) { |
||||
current_txn_error_count++; |
||||
if (current_txn_error_count > 10) { |
||||
nhlog::ui()->debug("failing txn id '{}'", txn_id); |
||||
cache::client()->removePendingStatus(room_id_, txn_id); |
||||
current_txn_error_count = 0; |
||||
} |
||||
} |
||||
QTimer::singleShot(1000, this, [this]() { |
||||
nhlog::ui()->debug("timeout"); |
||||
this->current_txn = ""; |
||||
emit processPending(); |
||||
}); |
||||
}, |
||||
Qt::QueuedConnection); |
||||
|
||||
connect( |
||||
this, |
||||
&EventStore::messageSent, |
||||
this, |
||||
[this](std::string txn_id, std::string event_id) { |
||||
nhlog::ui()->debug("sent {}", txn_id); |
||||
|
||||
http::client()->read_event( |
||||
room_id_, event_id, [this, event_id](mtx::http::RequestErr err) { |
||||
if (err) { |
||||
nhlog::net()->warn( |
||||
"failed to read_event ({}, {})", room_id_, event_id); |
||||
} |
||||
}); |
||||
|
||||
cache::client()->removePendingStatus(room_id_, txn_id); |
||||
this->current_txn = ""; |
||||
this->current_txn_error_count = 0; |
||||
emit processPending(); |
||||
}, |
||||
Qt::QueuedConnection); |
||||
} |
||||
|
||||
void |
||||
EventStore::addPending(mtx::events::collections::TimelineEvents event) |
||||
{ |
||||
if (this->thread() != QThread::currentThread()) |
||||
nhlog::db()->warn("{} called from a different thread!", __func__); |
||||
|
||||
cache::client()->savePendingMessage(this->room_id_, {event}); |
||||
mtx::responses::Timeline events; |
||||
events.limited = false; |
||||
events.events.emplace_back(event); |
||||
handleSync(events); |
||||
|
||||
emit processPending(); |
||||
} |
||||
|
||||
void |
||||
EventStore::clearTimeline() |
||||
{ |
||||
emit beginResetModel(); |
||||
|
||||
cache::client()->clearTimeline(room_id_); |
||||
auto range = cache::client()->getTimelineRange(room_id_); |
||||
if (range) { |
||||
nhlog::db()->info("Range {} {}", range->last, range->first); |
||||
this->last = range->last; |
||||
this->first = range->first; |
||||
} else { |
||||
this->first = std::numeric_limits<uint64_t>::max(); |
||||
this->last = std::numeric_limits<uint64_t>::max(); |
||||
} |
||||
nhlog::ui()->info("Range {} {}", this->last, this->first); |
||||
|
||||
emit endResetModel(); |
||||
} |
||||
|
||||
void |
||||
EventStore::handleSync(const mtx::responses::Timeline &events) |
||||
{ |
||||
if (this->thread() != QThread::currentThread()) |
||||
nhlog::db()->warn("{} called from a different thread!", __func__); |
||||
|
||||
auto range = cache::client()->getTimelineRange(room_id_); |
||||
if (!range) |
||||
return; |
||||
|
||||
if (events.limited) { |
||||
emit beginResetModel(); |
||||
this->last = range->last; |
||||
this->first = range->first; |
||||
emit endResetModel(); |
||||
|
||||
} else if (range->last > this->last) { |
||||
emit beginInsertRows(toExternalIdx(this->last + 1), toExternalIdx(range->last)); |
||||
this->last = range->last; |
||||
emit endInsertRows(); |
||||
} |
||||
|
||||
for (const auto &event : events.events) { |
||||
std::string relates_to; |
||||
if (auto redaction = |
||||
std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>( |
||||
&event)) { |
||||
// fixup reactions
|
||||
auto redacted = events_by_id_.object({room_id_, redaction->redacts}); |
||||
if (redacted) { |
||||
auto id = mtx::accessors::relates_to_event_id(*redacted); |
||||
if (!id.empty()) { |
||||
auto idx = idToIndex(id); |
||||
if (idx) { |
||||
events_by_id_.remove( |
||||
{room_id_, redaction->redacts}); |
||||
events_.remove({room_id_, toInternalIdx(*idx)}); |
||||
emit dataChanged(*idx, *idx); |
||||
} |
||||
} |
||||
} |
||||
|
||||
relates_to = redaction->redacts; |
||||
} else if (auto reaction = |
||||
std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>( |
||||
&event)) { |
||||
relates_to = reaction->content.relates_to.event_id; |
||||
} else { |
||||
relates_to = mtx::accessors::in_reply_to_event(event); |
||||
} |
||||
|
||||
if (!relates_to.empty()) { |
||||
auto idx = cache::client()->getTimelineIndex(room_id_, relates_to); |
||||
if (idx) { |
||||
events_by_id_.remove({room_id_, relates_to}); |
||||
decryptedEvents_.remove({room_id_, relates_to}); |
||||
events_.remove({room_id_, *idx}); |
||||
emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx)); |
||||
} |
||||
} |
||||
|
||||
if (auto txn_id = mtx::accessors::transaction_id(event); !txn_id.empty()) { |
||||
auto idx = cache::client()->getTimelineIndex( |
||||
room_id_, mtx::accessors::event_id(event)); |
||||
if (idx) { |
||||
Index index{room_id_, *idx}; |
||||
events_.remove(index); |
||||
emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx)); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
QVariantList |
||||
EventStore::reactions(const std::string &event_id) |
||||
{ |
||||
auto event_ids = cache::client()->relatedEvents(room_id_, event_id); |
||||
|
||||
struct TempReaction |
||||
{ |
||||
int count = 0; |
||||
std::vector<std::string> users; |
||||
std::string reactedBySelf; |
||||
}; |
||||
std::map<std::string, TempReaction> aggregation; |
||||
std::vector<Reaction> reactions; |
||||
|
||||
auto self = http::client()->user_id().to_string(); |
||||
for (const auto &id : event_ids) { |
||||
auto related_event = get(id, event_id); |
||||
if (!related_event) |
||||
continue; |
||||
|
||||
if (auto reaction = std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>( |
||||
related_event)) { |
||||
auto &agg = aggregation[reaction->content.relates_to.key]; |
||||
|
||||
if (agg.count == 0) { |
||||
Reaction temp{}; |
||||
temp.key_ = |
||||
QString::fromStdString(reaction->content.relates_to.key); |
||||
reactions.push_back(temp); |
||||
} |
||||
|
||||
agg.count++; |
||||
agg.users.push_back(cache::displayName(room_id_, reaction->sender)); |
||||
if (reaction->sender == self) |
||||
agg.reactedBySelf = reaction->event_id; |
||||
} |
||||
} |
||||
|
||||
QVariantList temp; |
||||
for (auto &reaction : reactions) { |
||||
const auto &agg = aggregation[reaction.key_.toStdString()]; |
||||
reaction.count_ = agg.count; |
||||
reaction.selfReactedEvent_ = QString::fromStdString(agg.reactedBySelf); |
||||
|
||||
bool firstReaction = true; |
||||
for (const auto &user : agg.users) { |
||||
if (firstReaction) |
||||
firstReaction = false; |
||||
else |
||||
reaction.users_ += ", "; |
||||
|
||||
reaction.users_ += QString::fromStdString(user); |
||||
} |
||||
|
||||
nhlog::db()->debug("key: {}, count: {}, users: {}", |
||||
reaction.key_.toStdString(), |
||||
reaction.count_, |
||||
reaction.users_.toStdString()); |
||||
temp.append(QVariant::fromValue(reaction)); |
||||
} |
||||
|
||||
return temp; |
||||
} |
||||
|
||||
mtx::events::collections::TimelineEvents * |
||||
EventStore::get(int idx, bool decrypt) |
||||
{ |
||||
if (this->thread() != QThread::currentThread()) |
||||
nhlog::db()->warn("{} called from a different thread!", __func__); |
||||
|
||||
Index index{room_id_, toInternalIdx(idx)}; |
||||
if (index.idx > last || index.idx < first) |
||||
return nullptr; |
||||
|
||||
auto event_ptr = events_.object(index); |
||||
if (!event_ptr) { |
||||
auto event_id = cache::client()->getTimelineEventId(room_id_, index.idx); |
||||
if (!event_id) |
||||
return nullptr; |
||||
|
||||
auto event = cache::client()->getEvent(room_id_, *event_id); |
||||
if (!event) |
||||
return nullptr; |
||||
else |
||||
event_ptr = |
||||
new mtx::events::collections::TimelineEvents(std::move(event->data)); |
||||
events_.insert(index, event_ptr); |
||||
} |
||||
|
||||
if (decrypt) |
||||
if (auto encrypted = |
||||
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( |
||||
event_ptr)) |
||||
return decryptEvent({room_id_, encrypted->event_id}, *encrypted); |
||||
|
||||
return event_ptr; |
||||
} |
||||
|
||||
std::optional<int> |
||||
EventStore::idToIndex(std::string_view id) const |
||||
{ |
||||
if (this->thread() != QThread::currentThread()) |
||||
nhlog::db()->warn("{} called from a different thread!", __func__); |
||||
|
||||
auto idx = cache::client()->getTimelineIndex(room_id_, id); |
||||
if (idx) |
||||
return toExternalIdx(*idx); |
||||
else |
||||
return std::nullopt; |
||||
} |
||||
std::optional<std::string> |
||||
EventStore::indexToId(int idx) const |
||||
{ |
||||
if (this->thread() != QThread::currentThread()) |
||||
nhlog::db()->warn("{} called from a different thread!", __func__); |
||||
|
||||
return cache::client()->getTimelineEventId(room_id_, toInternalIdx(idx)); |
||||
} |
||||
|
||||
mtx::events::collections::TimelineEvents * |
||||
EventStore::decryptEvent(const IdIndex &idx, |
||||
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) |
||||
{ |
||||
if (auto cachedEvent = decryptedEvents_.object(idx)) |
||||
return cachedEvent; |
||||
|
||||
MegolmSessionIndex index; |
||||
index.room_id = room_id_; |
||||
index.session_id = e.content.session_id; |
||||
index.sender_key = e.content.sender_key; |
||||
|
||||
auto asCacheEntry = [&idx](mtx::events::collections::TimelineEvents &&event) { |
||||
auto event_ptr = new mtx::events::collections::TimelineEvents(std::move(event)); |
||||
decryptedEvents_.insert(idx, event_ptr); |
||||
return event_ptr; |
||||
}; |
||||
|
||||
auto decryptionResult = olm::decryptEvent(index, e); |
||||
|
||||
if (decryptionResult.error) { |
||||
mtx::events::RoomEvent<mtx::events::msg::Notice> dummy; |
||||
dummy.origin_server_ts = e.origin_server_ts; |
||||
dummy.event_id = e.event_id; |
||||
dummy.sender = e.sender; |
||||
switch (*decryptionResult.error) { |
||||
case olm::DecryptionErrorCode::MissingSession: |
||||
dummy.content.body = |
||||
tr("-- Encrypted Event (No keys found for decryption) --", |
||||
"Placeholder, when the message was not decrypted yet or can't be " |
||||
"decrypted.") |
||||
.toStdString(); |
||||
nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", |
||||
index.room_id, |
||||
index.session_id, |
||||
e.sender); |
||||
// TODO: Check if this actually works and look in key backup
|
||||
olm::send_key_request_for(room_id_, e); |
||||
break; |
||||
case olm::DecryptionErrorCode::DbError: |
||||
nhlog::db()->critical( |
||||
"failed to retrieve megolm session with index ({}, {}, {})", |
||||
index.room_id, |
||||
index.session_id, |
||||
index.sender_key, |
||||
decryptionResult.error_message.value_or("")); |
||||
dummy.content.body = |
||||
tr("-- Decryption Error (failed to retrieve megolm keys from db) --", |
||||
"Placeholder, when the message can't be decrypted, because the DB " |
||||
"access " |
||||
"failed.") |
||||
.toStdString(); |
||||
break; |
||||
case olm::DecryptionErrorCode::DecryptionFailed: |
||||
nhlog::crypto()->critical( |
||||
"failed to decrypt message with index ({}, {}, {}): {}", |
||||
index.room_id, |
||||
index.session_id, |
||||
index.sender_key, |
||||
decryptionResult.error_message.value_or("")); |
||||
dummy.content.body = |
||||
tr("-- Decryption Error (%1) --", |
||||
"Placeholder, when the message can't be decrypted. In this case, the " |
||||
"Olm " |
||||
"decrytion returned an error, which is passed as %1.") |
||||
.arg( |
||||
QString::fromStdString(decryptionResult.error_message.value_or(""))) |
||||
.toStdString(); |
||||
break; |
||||
case olm::DecryptionErrorCode::ParsingFailed: |
||||
dummy.content.body = |
||||
tr("-- Encrypted Event (Unknown event type) --", |
||||
"Placeholder, when the message was decrypted, but we couldn't parse " |
||||
"it, because " |
||||
"Nheko/mtxclient don't support that event type yet.") |
||||
.toStdString(); |
||||
break; |
||||
case olm::DecryptionErrorCode::ReplayAttack: |
||||
nhlog::crypto()->critical( |
||||
"Reply attack while decryptiong event {} in room {} from {}!", |
||||
e.event_id, |
||||
room_id_, |
||||
index.sender_key); |
||||
dummy.content.body = |
||||
tr("-- Reply attack! This message index was reused! --").toStdString(); |
||||
break; |
||||
case olm::DecryptionErrorCode::UnknownFingerprint: |
||||
// TODO: don't fail, just show in UI.
|
||||
nhlog::crypto()->critical("Message by unverified fingerprint {}", |
||||
index.sender_key); |
||||
dummy.content.body = |
||||
tr("-- Message by unverified device! --").toStdString(); |
||||
break; |
||||
} |
||||
return asCacheEntry(std::move(dummy)); |
||||
} |
||||
|
||||
auto encInfo = mtx::accessors::file(decryptionResult.event.value()); |
||||
if (encInfo) |
||||
emit newEncryptedImage(encInfo.value()); |
||||
|
||||
return asCacheEntry(std::move(decryptionResult.event.value())); |
||||
} |
||||
|
||||
mtx::events::collections::TimelineEvents * |
||||
EventStore::get(std::string_view id, std::string_view related_to, bool decrypt) |
||||
{ |
||||
if (this->thread() != QThread::currentThread()) |
||||
nhlog::db()->warn("{} called from a different thread!", __func__); |
||||
|
||||
if (id.empty()) |
||||
return nullptr; |
||||
|
||||
IdIndex index{room_id_, std::string(id.data(), id.size())}; |
||||
|
||||
auto event_ptr = events_by_id_.object(index); |
||||
if (!event_ptr) { |
||||
auto event = cache::client()->getEvent(room_id_, index.id); |
||||
if (!event) { |
||||
http::client()->get_event( |
||||
room_id_, |
||||
index.id, |
||||
[this, |
||||
relatedTo = std::string(related_to.data(), related_to.size()), |
||||
id = index.id](const mtx::events::collections::TimelineEvents &timeline, |
||||
mtx::http::RequestErr err) { |
||||
if (err) { |
||||
nhlog::net()->error( |
||||
"Failed to retrieve event with id {}, which was " |
||||
"requested to show the replyTo for event {}", |
||||
relatedTo, |
||||
id); |
||||
return; |
||||
} |
||||
emit eventFetched(id, relatedTo, timeline); |
||||
}); |
||||
return nullptr; |
||||
} |
||||
event_ptr = new mtx::events::collections::TimelineEvents(std::move(event->data)); |
||||
events_by_id_.insert(index, event_ptr); |
||||
} |
||||
|
||||
if (decrypt) |
||||
if (auto encrypted = |
||||
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( |
||||
event_ptr)) |
||||
return decryptEvent(index, *encrypted); |
||||
|
||||
return event_ptr; |
||||
} |
||||
|
||||
void |
||||
EventStore::fetchMore() |
||||
{ |
||||
mtx::http::MessagesOpts opts; |
||||
opts.room_id = room_id_; |
||||
opts.from = cache::client()->previousBatchToken(room_id_); |
||||
|
||||
nhlog::ui()->debug("Paginating room {}, token {}", opts.room_id, opts.from); |
||||
|
||||
http::client()->messages( |
||||
opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { |
||||
if (cache::client()->previousBatchToken(room_id_) != opts.from) { |
||||
nhlog::net()->warn("Cache cleared while fetching more messages, dropping " |
||||
"/messages response"); |
||||
emit fetchedMore(); |
||||
return; |
||||
} |
||||
if (err) { |
||||
nhlog::net()->error("failed to call /messages ({}): {} - {} - {}", |
||||
opts.room_id, |
||||
mtx::errors::to_string(err->matrix_error.errcode), |
||||
err->matrix_error.error, |
||||
err->parse_error); |
||||
emit fetchedMore(); |
||||
return; |
||||
} |
||||
|
||||
emit oldMessagesRetrieved(std::move(res)); |
||||
}); |
||||
} |
@ -0,0 +1,122 @@ |
||||
#pragma once |
||||
|
||||
#include <limits> |
||||
#include <string> |
||||
|
||||
#include <QCache> |
||||
#include <QObject> |
||||
#include <QVariant> |
||||
#include <qhashfunctions.h> |
||||
|
||||
#include <mtx/events/collections.hpp> |
||||
#include <mtx/responses/messages.hpp> |
||||
#include <mtx/responses/sync.hpp> |
||||
|
||||
#include "Reaction.h" |
||||
|
||||
class EventStore : public QObject |
||||
{ |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
EventStore(std::string room_id, QObject *parent); |
||||
|
||||
struct Index |
||||
{ |
||||
std::string room; |
||||
uint64_t idx; |
||||
|
||||
friend uint qHash(const Index &i, uint seed = 0) noexcept |
||||
{ |
||||
QtPrivate::QHashCombine hash; |
||||
seed = hash(seed, QByteArray::fromRawData(i.room.data(), i.room.size())); |
||||
seed = hash(seed, i.idx); |
||||
return seed; |
||||
} |
||||
|
||||
friend bool operator==(const Index &a, const Index &b) noexcept |
||||
{ |
||||
return a.idx == b.idx && a.room == b.room; |
||||
} |
||||
}; |
||||
struct IdIndex |
||||
{ |
||||
std::string room, id; |
||||
|
||||
friend uint qHash(const IdIndex &i, uint seed = 0) noexcept |
||||
{ |
||||
QtPrivate::QHashCombine hash; |
||||
seed = hash(seed, QByteArray::fromRawData(i.room.data(), i.room.size())); |
||||
seed = hash(seed, QByteArray::fromRawData(i.id.data(), i.id.size())); |
||||
return seed; |
||||
} |
||||
|
||||
friend bool operator==(const IdIndex &a, const IdIndex &b) noexcept |
||||
{ |
||||
return a.id == b.id && a.room == b.room; |
||||
} |
||||
}; |
||||
|
||||
void fetchMore(); |
||||
void handleSync(const mtx::responses::Timeline &events); |
||||
|
||||
// optionally returns the event or nullptr and fetches it, after which it emits a
|
||||
// relatedFetched event
|
||||
mtx::events::collections::TimelineEvents *get(std::string_view id, |
||||
std::string_view related_to, |
||||
bool decrypt = true); |
||||
// always returns a proper event as long as the idx is valid
|
||||
mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true); |
||||
|
||||
QVariantList reactions(const std::string &event_id); |
||||
|
||||
int size() const |
||||
{ |
||||
return last != std::numeric_limits<uint64_t>::max() |
||||
? static_cast<int>(last - first) + 1 |
||||
: 0; |
||||
} |
||||
int toExternalIdx(uint64_t idx) const { return static_cast<int>(idx - first); } |
||||
uint64_t toInternalIdx(int idx) const { return first + idx; } |
||||
|
||||
std::optional<int> idToIndex(std::string_view id) const; |
||||
std::optional<std::string> indexToId(int idx) const; |
||||
|
||||
signals: |
||||
void beginInsertRows(int from, int to); |
||||
void endInsertRows(); |
||||
void beginResetModel(); |
||||
void endResetModel(); |
||||
void dataChanged(int from, int to); |
||||
void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); |
||||
void eventFetched(std::string id, |
||||
std::string relatedTo, |
||||
mtx::events::collections::TimelineEvents timeline); |
||||
void oldMessagesRetrieved(const mtx::responses::Messages &); |
||||
void fetchedMore(); |
||||
|
||||
void processPending(); |
||||
void messageSent(std::string txn_id, std::string event_id); |
||||
void messageFailed(std::string txn_id); |
||||
|
||||
public slots: |
||||
void addPending(mtx::events::collections::TimelineEvents event); |
||||
void clearTimeline(); |
||||
|
||||
private: |
||||
mtx::events::collections::TimelineEvents *decryptEvent( |
||||
const IdIndex &idx, |
||||
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e); |
||||
|
||||
std::string room_id_; |
||||
|
||||
uint64_t first = std::numeric_limits<uint64_t>::max(), |
||||
last = std::numeric_limits<uint64_t>::max(); |
||||
|
||||
static QCache<IdIndex, mtx::events::collections::TimelineEvents> decryptedEvents_; |
||||
static QCache<Index, mtx::events::collections::TimelineEvents> events_; |
||||
static QCache<IdIndex, mtx::events::collections::TimelineEvents> events_by_id_; |
||||
|
||||
std::string current_txn; |
||||
int current_txn_error_count = 0; |
||||
}; |
@ -0,0 +1 @@ |
||||
#include "Reaction.h" |
@ -0,0 +1,24 @@ |
||||
#pragma once |
||||
|
||||
#include <QObject> |
||||
#include <QString> |
||||
|
||||
struct Reaction |
||||
{ |
||||
Q_GADGET |
||||
Q_PROPERTY(QString key READ key) |
||||
Q_PROPERTY(QString users READ users) |
||||
Q_PROPERTY(QString selfReactedEvent READ selfReactedEvent) |
||||
Q_PROPERTY(int count READ count) |
||||
|
||||
public: |
||||
QString key() const { return key_; } |
||||
QString users() const { return users_; } |
||||
QString selfReactedEvent() const { return selfReactedEvent_; } |
||||
int count() const { return count_; } |
||||
|
||||
QString key_; |
||||
QString users_; |
||||
QString selfReactedEvent_; |
||||
int count_; |
||||
}; |
@ -1,98 +0,0 @@ |
||||
#include "ReactionsModel.h" |
||||
|
||||
#include <Cache.h> |
||||
#include <MatrixClient.h> |
||||
|
||||
QHash<int, QByteArray> |
||||
ReactionsModel::roleNames() const |
||||
{ |
||||
return { |
||||
{Key, "key"}, |
||||
{Count, "counter"}, |
||||
{Users, "users"}, |
||||
{SelfReactedEvent, "selfReactedEvent"}, |
||||
}; |
||||
} |
||||
|
||||
int |
||||
ReactionsModel::rowCount(const QModelIndex &) const |
||||
{ |
||||
return static_cast<int>(reactions.size()); |
||||
} |
||||
|
||||
QVariant |
||||
ReactionsModel::data(const QModelIndex &index, int role) const |
||||
{ |
||||
const int i = index.row(); |
||||
if (i < 0 || i >= static_cast<int>(reactions.size())) |
||||
return {}; |
||||
|
||||
switch (role) { |
||||
case Key: |
||||
return QString::fromStdString(reactions[i].key); |
||||
case Count: |
||||
return static_cast<int>(reactions[i].reactions.size()); |
||||
case Users: { |
||||
QString users; |
||||
bool first = true; |
||||
for (const auto &reaction : reactions[i].reactions) { |
||||
if (!first) |
||||
users += ", "; |
||||
else |
||||
first = false; |
||||
users += QString::fromStdString( |
||||
cache::displayName(room_id_, reaction.second.sender)); |
||||
} |
||||
return users; |
||||
} |
||||
case SelfReactedEvent: |
||||
for (const auto &reaction : reactions[i].reactions) |
||||
if (reaction.second.sender == http::client()->user_id().to_string()) |
||||
return QString::fromStdString(reaction.second.event_id); |
||||
return QStringLiteral(""); |
||||
default: |
||||
return {}; |
||||
} |
||||
} |
||||
|
||||
void |
||||
ReactionsModel::addReaction(const std::string &room_id, |
||||
const mtx::events::RoomEvent<mtx::events::msg::Reaction> &reaction) |
||||
{ |
||||
room_id_ = room_id; |
||||
|
||||
int idx = 0; |
||||
for (auto &storedReactions : reactions) { |
||||
if (storedReactions.key == reaction.content.relates_to.key) { |
||||
storedReactions.reactions[reaction.event_id] = reaction; |
||||
emit dataChanged(index(idx, 0), index(idx, 0)); |
||||
return; |
||||
} |
||||
idx++; |
||||
} |
||||
|
||||
beginInsertRows(QModelIndex(), idx, idx); |
||||
reactions.push_back( |
||||
KeyReaction{reaction.content.relates_to.key, {{reaction.event_id, reaction}}}); |
||||
endInsertRows(); |
||||
} |
||||
|
||||
void |
||||
ReactionsModel::removeReaction(const mtx::events::RoomEvent<mtx::events::msg::Reaction> &reaction) |
||||
{ |
||||
int idx = 0; |
||||
for (auto &storedReactions : reactions) { |
||||
if (storedReactions.key == reaction.content.relates_to.key) { |
||||
storedReactions.reactions.erase(reaction.event_id); |
||||
|
||||
if (storedReactions.reactions.size() == 0) { |
||||
beginRemoveRows(QModelIndex(), idx, idx); |
||||
reactions.erase(reactions.begin() + idx); |
||||
endRemoveRows(); |
||||
} else |
||||
emit dataChanged(index(idx, 0), index(idx, 0)); |
||||
return; |
||||
} |
||||
idx++; |
||||
} |
||||
} |
@ -1,41 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QAbstractListModel> |
||||
#include <QHash> |
||||
|
||||
#include <utility> |
||||
#include <vector> |
||||
|
||||
#include <mtx/events/collections.hpp> |
||||
|
||||
class ReactionsModel : public QAbstractListModel |
||||
{ |
||||
Q_OBJECT |
||||
public: |
||||
explicit ReactionsModel(QObject *parent = nullptr) { Q_UNUSED(parent); } |
||||
enum Roles |
||||
{ |
||||
Key, |
||||
Count, |
||||
Users, |
||||
SelfReactedEvent, |
||||
}; |
||||
|
||||
QHash<int, QByteArray> roleNames() const override; |
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override; |
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; |
||||
|
||||
public slots: |
||||
void addReaction(const std::string &room_id, |
||||
const mtx::events::RoomEvent<mtx::events::msg::Reaction> &reaction); |
||||
void removeReaction(const mtx::events::RoomEvent<mtx::events::msg::Reaction> &reaction); |
||||
|
||||
private: |
||||
struct KeyReaction |
||||
{ |
||||
std::string key; |
||||
std::map<std::string, mtx::events::RoomEvent<mtx::events::msg::Reaction>> reactions; |
||||
}; |
||||
std::string room_id_; |
||||
std::vector<KeyReaction> reactions; |
||||
}; |
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue