Skip to content
Snippets Groups Projects
TimelineModel.cpp 71.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • #include "TimelineModel.h"
    
    
    #include <algorithm>
    
    #include <thread>
    
    #include <type_traits>
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    #include <QCache>
    
    #include <QFileDialog>
    #include <QMimeDatabase>
    
    #include <QRegularExpression>
    
    #include <QStandardPaths>
    
    #include "ChatPage.h"
    
    #include "EventAccessors.h"
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    #include "Logging.h"
    
    #include "MxcImageProvider.h"
    
    #include "Olm.h"
    
    #include "TimelineViewManager.h"
    
    #include "Utils.h"
    
    #include "dialogs/RawMessage.h"
    
    Q_DECLARE_METATYPE(QModelIndex)
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    namespace std {
    inline uint
    qHash(const std::string &key, uint seed = 0)
    {
            return qHash(QByteArray::fromRawData(key.data(), key.length()), seed);
    }
    }
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    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::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;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            }
    
            qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &)
            {
                    return qml_mtx_events::EventType::Redacted;
    
            // ::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);
    
    QString
    toRoomEventTypeString(const mtx::events::collections::TimelineEvents &event)
    {
            return std::visit([](const auto &e) { return QString::fromStdString(to_string(e.type)); },
                              event);
    }
    
    
    TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent)
    
      : QAbstractListModel(parent)
      , room_id_(room_id)
    
      , manager_(manager)
    
    {
            connect(
              this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents);
    
            connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) {
    
                    nhlog::ui()->error("Failed to send {}, retrying", txn_id.toStdString());
    
                    QTimer::singleShot(5000, this, [this]() { emit nextPendingMessage(); });
    
            });
            connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) {
    
                    pending.removeOne(txn_id);
    
                    int idx = idToIndex(txn_id);
                    if (idx < 0) {
    
                            // transaction already received via sync
    
                            return;
                    }
                    eventOrder[idx] = event_id;
                    auto ev         = events.value(txn_id);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    ev              = std::visit(
    
                      [event_id](const auto &e) -> mtx::events::collections::TimelineEvents {
                              auto eventCopy     = e;
                              eventCopy.event_id = event_id.toStdString();
                              return eventCopy;
                      },
                      ev);
    
                    events.remove(txn_id);
                    events.insert(event_id, ev);
    
                    // mark our messages as read
                    readEvent(event_id.toStdString());
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    // ask to be notified for read receipts
    
                    cache::addPendingReceipt(room_id_, event_id);
    
                    emit dataChanged(index(idx, 0), index(idx, 0));
    
    
                    if (pending.size() > 0)
                            emit nextPendingMessage();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) {
                    emit ChatPage::instance()->showNotification(msg);
            });
    
    
            connect(
              this, &TimelineModel::nextPendingMessage, this, &TimelineModel::processOnePendingMessage);
            connect(this, &TimelineModel::newMessageToSend, this, &TimelineModel::addPendingMessage);
    
                    &TimelineModel::eventFetched,
    
                    this,
                    [this](QString requestingEvent, mtx::events::collections::TimelineEvents event) {
                            events.insert(QString::fromStdString(mtx::accessors::event_id(event)),
                                          event);
                            auto idx = idToIndex(requestingEvent);
                            if (idx >= 0)
                                    emit dataChanged(index(idx, 0), index(idx, 0));
                    });
    
    QHash<int, QByteArray>
    TimelineModel::roleNames() const
    {
            return {
    
              {Type, "type"},
    
              {TypeString, "typeString"},
    
              {Body, "body"},
              {FormattedBody, "formattedBody"},
              {UserId, "userId"},
              {UserName, "userName"},
              {Timestamp, "timestamp"},
    
              {ThumbnailUrl, "thumbnailUrl"},
    
              {Blurhash, "blurhash"},
    
    Nicolas Werner's avatar
    Nicolas Werner committed
              {Filename, "filename"},
    
    Nicolas Werner's avatar
    Nicolas Werner committed
              {Filesize, "filesize"},
    
    Nicolas Werner's avatar
    Nicolas Werner committed
              {MimeType, "mimetype"},
    
              {Height, "height"},
              {Width, "width"},
              {ProportionalHeight, "proportionalHeight"},
    
              {Id, "id"},
    
              {State, "state"},
    
              {IsEncrypted, "isEncrypted"},
    
              {ReplyTo, "replyTo"},
    
              {RoomId, "roomId"},
    
              {RoomName, "roomName"},
              {RoomTopic, "roomTopic"},
    
    Nicolas Werner's avatar
    Nicolas Werner committed
              {Dump, "dump"},
    
            };
    }
    int
    TimelineModel::rowCount(const QModelIndex &parent) const
    {
            Q_UNUSED(parent);
            return (int)this->eventOrder.size();
    }
    
    
    QVariantMap
    TimelineModel::getDump(QString eventId) const
    {
            if (events.contains(eventId))
    
                    return data(eventId, Dump).toMap();
    
    QVariant
    
    TimelineModel::data(const QString &id, int role) const
    
            using namespace mtx::accessors;
    
            namespace acc                                  = mtx::accessors;
    
            mtx::events::collections::TimelineEvents event = events.value(id);
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            if (auto e =
                  std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
    
                    event = decryptEvent(*e).event;
            }
    
    
            switch (role) {
            case UserId:
    
                    return QVariant(QString::fromStdString(acc::sender(event)));
    
                    return QVariant(displayName(QString::fromStdString(acc::sender(event))));
    
                    return QVariant(origin_server_ts(event));
    
            case Type:
    
                    return QVariant(toRoomEventType(event));
    
            case TypeString:
                    return QVariant(toRoomEventTypeString(event));
    
            case Body:
    
                    return QVariant(utils::replaceEmoji(QString::fromStdString(body(event))));
    
            case FormattedBody: {
                    const static QRegularExpression replyFallback(
                      "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption);
    
    
                    bool isReply = !in_reply_to_event(event).empty();
    
                    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);
                    }
                    return QVariant(utils::replaceEmoji(
                      utils::linkifyMessage(utils::escapeBlacklistedHtml(formattedBody_))));
    
                    return QVariant(QString::fromStdString(url(event)));
    
            case ThumbnailUrl:
    
                    return QVariant(QString::fromStdString(thumbnail_url(event)));
    
            case Blurhash:
                    return QVariant(QString::fromStdString(blurhash(event)));
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            case Filename:
    
                    return QVariant(QString::fromStdString(filename(event)));
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            case Filesize:
    
                    return QVariant(utils::humanReadableFileSize(filesize(event)));
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            case MimeType:
    
                    return QVariant(QString::fromStdString(mimetype(event)));
    
                    return QVariant(qulonglong{media_height(event)});
    
                    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 Id:
                    return id;
    
            case State:
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    // only show read receipts for messages not from us
    
                    if (acc::sender(event) != http::client()->user_id().to_string())
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            return qml_mtx_events::Empty;
    
                    else if (pending.contains(id))
                            return qml_mtx_events::Sent;
    
                    else if (read.contains(id) || cache::readReceipts(id, room_id_).size() > 1)
    
                    else
                            return qml_mtx_events::Received;
    
            case IsEncrypted: {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    return std::holds_alternative<
    
                      mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(events[id]);
    
            case ReplyTo:
                    return QVariant(QString::fromStdString(in_reply_to_event(event)));
    
            case RoomId:
                    return QVariant(QString::fromStdString(room_id(event)));
    
            case RoomName:
    
                    return QVariant(QString::fromStdString(room_name(event)));
    
            case RoomTopic:
    
                    return QVariant(QString::fromStdString(room_topic(event)));
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            case Dump: {
                    QVariantMap m;
                    auto names = roleNames();
    
    
                    // m.insert(names[Section], data(id, static_cast<int>(Section)));
                    m.insert(names[Type], data(id, static_cast<int>(Type)));
    
                    m.insert(names[TypeString], data(id, static_cast<int>(TypeString)));
    
                    m.insert(names[Body], data(id, static_cast<int>(Body)));
                    m.insert(names[FormattedBody], data(id, static_cast<int>(FormattedBody)));
                    m.insert(names[UserId], data(id, static_cast<int>(UserId)));
                    m.insert(names[UserName], data(id, static_cast<int>(UserName)));
                    m.insert(names[Timestamp], data(id, static_cast<int>(Timestamp)));
                    m.insert(names[Url], data(id, static_cast<int>(Url)));
                    m.insert(names[ThumbnailUrl], data(id, static_cast<int>(ThumbnailUrl)));
    
                    m.insert(names[Blurhash], data(id, static_cast<int>(Blurhash)));
    
                    m.insert(names[Filename], data(id, static_cast<int>(Filename)));
                    m.insert(names[Filesize], data(id, static_cast<int>(Filesize)));
                    m.insert(names[MimeType], data(id, static_cast<int>(MimeType)));
                    m.insert(names[Height], data(id, static_cast<int>(Height)));
                    m.insert(names[Width], data(id, static_cast<int>(Width)));
                    m.insert(names[ProportionalHeight], data(id, static_cast<int>(ProportionalHeight)));
                    m.insert(names[Id], data(id, static_cast<int>(Id)));
                    m.insert(names[State], data(id, static_cast<int>(State)));
                    m.insert(names[IsEncrypted], data(id, static_cast<int>(IsEncrypted)));
                    m.insert(names[ReplyTo], data(id, static_cast<int>(ReplyTo)));
                    m.insert(names[RoomName], data(id, static_cast<int>(RoomName)));
                    m.insert(names[RoomTopic], data(id, static_cast<int>(RoomTopic)));
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    
                    return QVariant(m);
            }
    
            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() >= (int)eventOrder.size())
                    return QVariant();
    
            QString id = eventOrder[index.row()];
    
            mtx::events::collections::TimelineEvents event = events.value(id);
    
            if (role == Section) {
                    QDateTime date = origin_server_ts(event);
                    date.setTime(QTime());
    
                    std::string userId = acc::sender(event);
    
                    for (size_t r = index.row() + 1; r < eventOrder.size(); r++) {
                            auto tempEv        = events.value(eventOrder[r]);
                            QDateTime prevDate = origin_server_ts(tempEv);
                            prevDate.setTime(QTime());
                            if (prevDate != date)
                                    return QString("%2 %1")
                                      .arg(date.toMSecsSinceEpoch())
                                      .arg(QString::fromStdString(userId));
    
                            std::string prevUserId = acc::sender(tempEv);
                            if (userId != prevUserId)
                                    break;
                    }
    
                    return QString("%1").arg(QString::fromStdString(userId));
            }
    
            return data(id, role);
    }
    
    
    bool
    TimelineModel::canFetchMore(const QModelIndex &) const
    {
            if (eventOrder.empty())
                    return true;
            if (!std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>(
                  events[eventOrder.back()]))
                    return true;
            else
    
                    return false;
    }
    
    void
    TimelineModel::fetchMore(const QModelIndex &)
    {
            if (paginationInProgress) {
                    nhlog::ui()->warn("Already loading older messages");
                    return;
            }
    
            paginationInProgress = true;
            mtx::http::MessagesOpts opts;
            opts.room_id = room_id_.toStdString();
            opts.from    = prev_batch_token_.toStdString();
    
    
            nhlog::ui()->debug("Paginating room {}", opts.room_id);
    
    
            http::client()->messages(
              opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) {
                      if (err) {
    
                              nhlog::net()->error("failed to call /messages ({}): {} - {} - {}",
    
                                                  opts.room_id,
                                                  mtx::errors::to_string(err->matrix_error.errcode),
    
                              paginationInProgress = false;
                              return;
                      }
    
                      emit oldMessagesRetrieved(std::move(res));
                      paginationInProgress = false;
              });
    }
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    void
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    prev_batch_token_ = QString::fromStdString(timeline.prev_batch);
    
            if (timeline.events.empty())
                    return;
    
    
            std::vector<QString> ids = internalAddEvents(timeline.events);
    
            if (ids.empty())
                    return;
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            beginInsertRows(QModelIndex(), 0, static_cast<int>(ids.size() - 1));
            this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend());
    
            endInsertRows();
    
    template<typename T>
    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;
    }
    
    template<typename T>
    auto
    isMessage(const mtx::events::Event<T> &)
    {
            return false;
    }
    
    
    void
    TimelineModel::updateLastMessage()
    {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            for (auto it = eventOrder.begin(); it != eventOrder.end(); ++it) {
    
                    auto event = events.value(*it);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    if (auto e = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
    
                          &event)) {
                            event = decryptEvent(*e).event;
                    }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    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()), room_id_);
                    emit manager_->updateRoomsLastMessage(room_id_, description);
                    return;
            }
    
    }
    
    std::vector<QString>
    TimelineModel::internalAddEvents(
      const std::vector<mtx::events::collections::TimelineEvents> &timeline)
    {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            std::vector<QString> ids;
    
            for (auto e : timeline) {
    
                    QString id = QString::fromStdString(mtx::accessors::event_id(e));
    
                    if (this->events.contains(id)) {
                            this->events.insert(id, e);
                            int idx = idToIndex(id);
                            emit dataChanged(index(idx, 0), index(idx, 0));
    
                    QString txid = QString::fromStdString(mtx::accessors::transaction_id(e));
                    if (this->pending.removeOne(txid)) {
                            this->events.insert(id, e);
                            this->events.remove(txid);
                            int idx = idToIndex(txid);
                            if (idx < 0) {
                                    nhlog::ui()->warn("Received index out of range");
                                    continue;
                            }
                            eventOrder[idx] = id;
                            emit dataChanged(index(idx, 0), index(idx, 0));
                            continue;
                    }
    
    
                    if (auto redaction =
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                          std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(&e)) {
    
                            QString redacts = QString::fromStdString(redaction->redacts);
                            auto redacted   = std::find(eventOrder.begin(), eventOrder.end(), redacts);
    
                            if (redacted != eventOrder.end()) {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                    auto redactedEvent = std::visit(
    
                                      [](const auto &ev)
                                        -> mtx::events::RoomEvent<mtx::events::msg::Redacted> {
                                              mtx::events::RoomEvent<mtx::events::msg::Redacted>
                                                replacement                = {};
                                              replacement.event_id         = ev.event_id;
                                              replacement.room_id          = ev.room_id;
                                              replacement.sender           = ev.sender;
                                              replacement.origin_server_ts = ev.origin_server_ts;
                                              replacement.type             = ev.type;
                                              return replacement;
                                      },
                                      e);
                                    events.insert(redacts, redactedEvent);
    
                                    int row = (int)std::distance(eventOrder.begin(), redacted);
                                    emit dataChanged(index(row, 0), index(row, 0));
                            }
    
                            continue; // don't insert redaction into timeline
                    }
    
    
                    if (auto event =
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                          std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&e)) {
    
                            auto e_      = decryptEvent(*event).event;
                            auto encInfo = mtx::accessors::file(e_);
    
                            if (encInfo)
                                    emit newEncryptedImage(encInfo.value());
                    }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    this->events.insert(id, e);
                    ids.push_back(id);
    
    
                    auto replyTo  = mtx::accessors::in_reply_to_event(e);
                    auto qReplyTo = QString::fromStdString(replyTo);
                    if (!replyTo.empty() && !events.contains(qReplyTo)) {
                            http::client()->get_event(
                              this->room_id_.toStdString(),
                              replyTo,
                              [this, id, replyTo](
                                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 {}",
                                                replyTo,
                                                id.toStdString());
                                              return;
                                      }
    
                                      emit eventFetched(id, timeline);
    
            return ids;
    
    void
    TimelineModel::setCurrentIndex(int index)
    {
            auto oldIndex = idToIndex(currentId);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            currentId     = indexToId(index);
    
            emit currentIndexChanged(index);
    
    
            if ((oldIndex > index || oldIndex == -1) && !pending.contains(currentId) &&
    
                ChatPage::instance()->isActiveWindow()) {
    
                    readEvent(currentId.toStdString());
    
    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());
                    }
            });
    }
    
    
    void
    TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs)
    {
    
            std::vector<QString> ids = internalAddEvents(msgs.chunk);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    beginInsertRows(QModelIndex(),
                                    static_cast<int>(this->eventOrder.size()),
                                    static_cast<int>(this->eventOrder.size() + ids.size() - 1));
                    this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end());
    
    
            prev_batch_token_ = QString::fromStdString(msgs.end);
    }
    
    
    QString
    TimelineModel::displayName(QString id) const
    {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            return cache::displayName(room_id_, id).toHtmlEscaped();
    
    QString
    TimelineModel::avatarUrl(QString id) const
    {
    
            return cache::avatarUrl(room_id_, id);
    
    QString
    TimelineModel::formatDateSeparator(QDate date) const
    {
            auto now = QDateTime::currentDateTime();
    
            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);
            }
    
            return date.toString(fmt);
    }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    
    QString
    TimelineModel::escapeEmoji(QString str) const
    {
            return utils::replaceEmoji(str);
    }
    
    
    void
    TimelineModel::viewRawMessage(QString id) const
    {
            std::string ev = utils::serialize_event(events.value(id)).dump(4);
            auto dialog    = new dialogs::RawMessage(QString::fromStdString(ev));
            Q_UNUSED(dialog);
    }
    
    void
    
    TimelineModel::openUserProfile(QString userid) const
    {
            MainWindow::instance()->openUserProfile(userid, room_id_);
    }
    
    
    DecryptionResult
    TimelineModel::decryptEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) const
    {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            static QCache<std::string, DecryptionResult> decryptedEvents{300};
    
            if (auto cachedEvent = decryptedEvents.object(e.event_id))
                    return *cachedEvent;
    
    
            MegolmSessionIndex index;
            index.room_id    = room_id_.toStdString();
            index.session_id = e.content.session_id;
            index.sender_key = e.content.sender_key;
    
            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;
            dummy.content.body =
              tr("-- Encrypted Event (No keys found for decryption) --",
    
                 "Placeholder, when the message was not decrypted yet or can't be decrypted.")
    
                    if (!cache::inboundMegolmSessionExists(index)) {
    
                            nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
                                                  index.room_id,
                                                  index.session_id,
                                                  e.sender);
                            // TODO: request megolm session_id & session_key from the sender.
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            decryptedEvents.insert(
                              dummy.event_id, new DecryptionResult{dummy, false}, 1);
    
                            return {dummy, false};
                    }
            } catch (const lmdb::error &e) {
                    nhlog::db()->critical("failed to check megolm session's existence: {}", e.what());
                    dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --",
                                            "Placeholder, when the message can't be decrypted, because "
                                            "the DB access failed when trying to lookup the session.")
                                           .toStdString();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    decryptedEvents.insert(dummy.event_id, new DecryptionResult{dummy, false}, 1);
    
                    return {dummy, false};
            }
    
            std::string msg_str;
            try {
    
                    auto session = cache::getInboundMegolmSession(index);
    
                    auto res     = olm::client()->decrypt_group_message(session, e.content.ciphertext);
                    msg_str      = std::string((char *)res.data.data(), res.data.size());
            } catch (const lmdb::error &e) {
                    nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})",
                                          index.room_id,
                                          index.session_id,
                                          index.sender_key,
                                          e.what());
                    dummy.content.body =
                      tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
                         "Placeholder, when the message can't be decrypted, because the DB access "
                         "failed.")
                        .toStdString();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    decryptedEvents.insert(dummy.event_id, new DecryptionResult{dummy, false}, 1);
    
                    return {dummy, false};
            } catch (const mtx::crypto::olm_exception &e) {
                    nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}",
                                              index.room_id,
                                              index.session_id,
                                              index.sender_key,
                                              e.what());
                    dummy.content.body =
                      tr("-- Decryption Error (%1) --",
                         "Placeholder, when the message can't be decrypted. In this case, the Olm "
    
                         "decrytion returned an error, which is passed ad %1.")
    
                        .arg(e.what())
                        .toStdString();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    decryptedEvents.insert(dummy.event_id, new DecryptionResult{dummy, false}, 1);
    
                    return {dummy, false};
            }
    
            // Add missing fields for the event.
            json body                = json::parse(msg_str);
            body["event_id"]         = e.event_id;
            body["sender"]           = e.sender;
            body["origin_server_ts"] = e.origin_server_ts;
            body["unsigned"]         = e.unsigned_data;
    
    
            // relations are unencrypted in content...
            if (json old_ev = e; old_ev["content"].count("m.relates_to") != 0)
                    body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"];
    
    
            json event_array = json::array();
            event_array.push_back(body);
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            std::vector<mtx::events::collections::TimelineEvents> temp_events;
            mtx::responses::utils::parse_timeline_events(event_array, temp_events);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            if (temp_events.size() == 1) {
                    decryptedEvents.insert(e.event_id, new DecryptionResult{temp_events[0], true}, 1);
                    return {temp_events[0], true};
            }
    
    
            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();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            decryptedEvents.insert(dummy.event_id, new DecryptionResult{dummy, false}, 1);
    
            return {dummy, false};
    }
    
    
    void
    TimelineModel::replyAction(QString id)
    {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            setReply(id);
            ChatPage::instance()->focusMessageInput();
    }
    
    RelatedInfo
    TimelineModel::relatedInfo(QString id)
    {
            if (!events.contains(id))
                    return {};
    
    
            auto event = events.value(id);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            if (auto e =
                  std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
    
                    event = decryptEvent(*e).event;
            }
    
    
            RelatedInfo related           = {};
            related.quoted_user           = QString::fromStdString(mtx::accessors::sender(event));
            related.related_event         = mtx::accessors::event_id(event);
            related.type                  = mtx::accessors::msg_type(event);
            related.quoted_body           = QString::fromStdString(mtx::accessors::body(event));
            related.quoted_body           = utils::getQuoteBody(related);
            related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event);
            related.quoted_formatted_body.remove(QRegularExpression(
    
              "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption));
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            related.room = room_id_;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            return related;
    
    }
    
    void
    TimelineModel::readReceiptsAction(QString id) const
    {
            MainWindow::instance()->openReadReceiptsDialog(id);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    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);
                      });
    }
    
    
    int
    TimelineModel::idToIndex(QString id) const
    {
            if (id.isEmpty())
                    return -1;
            for (int i = 0; i < (int)eventOrder.size(); i++)
                    if (id == eventOrder[i])
                            return i;
            return -1;
    }
    
    QString
    TimelineModel::indexToId(int index) const
    {
            if (index < 0 || index >= (int)eventOrder.size())
                    return "";
            return eventOrder[index];
    }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    // 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) {
                            nhlog::ui()->warn("Read index out of range");
                            return;
                    }
                    emit dataChanged(index(idx, 0), index(idx, 0));
            }
    }
    
    
    void
    TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json content)
    {
            const auto room_id = room_id_.toStdString();
    
            using namespace mtx::events;
            using namespace mtx::identifiers;
    
    
            json doc = {{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}};
    
    
            try {
                    // Check if we have already an outbound megolm session then we can use.
    
                    if (cache::outboundMegolmSessionExists(room_id)) {
    
                            auto data =
                              olm::encrypt_group_message(room_id, http::client()->device_id(), doc);
    
    
                            http::client()->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
                              room_id,
                              txn_id,
                              data,
                              [this, txn_id](const mtx::responses::EventId &res,
                                             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(QString::fromStdString(txn_id));
                                      }
                                      emit messageSent(
                                        QString::fromStdString(txn_id),
                                        QString::fromStdString(res.event_id.to_string()));
                              });
                            return;
                    }
    
                    nhlog::ui()->debug("creating new outbound megolm session");
    
                    // Create a new outbound megolm session.
                    auto outbound_session  = olm::client()->init_outbound_group_session();
                    const auto session_id  = mtx::crypto::session_id(outbound_session.get());
                    const auto session_key = mtx::crypto::session_key(outbound_session.get());
    
                    // TODO: needs to be moved in the lib.
                    auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"},
                                               {"room_id", room_id},
                                               {"session_id", session_id},
                                               {"session_key", session_key}};
    
                    // Saving the new megolm session.
                    // TODO: Maybe it's too early to save.
                    OutboundGroupSessionData session_data;
                    session_data.session_id    = session_id;
                    session_data.session_key   = session_key;
                    session_data.message_index = 0; // TODO Update me
    
                    cache::saveOutboundMegolmSession(
    
                      room_id, session_data, std::move(outbound_session));
    
    
                    const auto members = cache::roomMembers(room_id);
    
                    nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id);
    
                    auto keeper =
                      std::make_shared<StateKeeper>([megolm_payload, room_id, doc, txn_id, this]() {
                              try {
                                      auto data = olm::encrypt_group_message(
    
                                        room_id, http::client()->device_id(), doc);
    
    
                                      http::client()
                                        ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
                                          room_id,
                                          txn_id,
                                          data,
                                          [this, txn_id](const mtx::responses::EventId &res,
                                                         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(
                                                            QString::fromStdString(txn_id));
                                                  }
                                                  emit messageSent(
                                                    QString::fromStdString(txn_id),
                                                    QString::fromStdString(res.event_id.to_string()));
                                          });
                              } catch (const lmdb::error &e) {
                                      nhlog::db()->critical(
                                        "failed to save megolm outbound session: {}", e.what());
    
                                      emit messageFailed(QString::fromStdString(txn_id));