Skip to content
Snippets Groups Projects
TimelineModel.cpp 57.8 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(), (int)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::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;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            }
    
            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;
    
    trilene's avatar
    trilene committed
            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);
    
    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)
    
      , events(room_id.toStdString(), this)
    
      , manager_(manager)
    
            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) {
                      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));
              },
              Qt::QueuedConnection);
    
    
            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);
            });
    
    QHash<int, QByteArray>
    TimelineModel::roleNames() const
    {
            return {
              {Type, "type"},
    
              {TypeString, "typeString"},
    
              {Body, "body"},
              {FormattedBody, "formattedBody"},
    
              {PreviousMessageUserId, "previousMessageUserId"},
    
              {UserId, "userId"},
              {UserName, "userName"},
    
              {PreviousMessageDay, "previousMessageDay"},
              {Day, "day"},
    
              {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"},
    
              {IsRoomEncrypted, "isRoomEncrypted"},
    
              {ReplyTo, "replyTo"},
    
              {Reactions, "reactions"},
    
              {RoomId, "roomId"},
    
              {RoomName, "roomName"},
              {RoomTopic, "roomTopic"},
    
    trilene's avatar
    trilene committed
              {CallType, "callType"},
    
    Nicolas Werner's avatar
    Nicolas Werner committed
              {Dump, "dump"},
    
            };
    }
    int
    TimelineModel::rowCount(const QModelIndex &parent) const
    {
            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();
    
    QVariant
    
    TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int role) const
    
            using namespace mtx::accessors;
    
            namespace acc = mtx::accessors;
    
            switch (role) {
            case UserId:
    
                    return QVariant(QString::fromStdString(acc::sender(event)));
    
                    return QVariant(displayName(QString::fromStdString(acc::sender(event))));
    
            case Day: {
                    QDateTime prevDate = origin_server_ts(event);
                    prevDate.setTime(QTime());
                    return QVariant(prevDate.toMSecsSinceEpoch());
            }
    
                    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);
    
    
                    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);
                    }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    
                    formattedBody_.replace("<img src=\"mxc:&#47;&#47;", "<img src=\"image://mxcImage/");
                    formattedBody_.replace("<img src=\"mxc://", "<img src=\"image://mxcImage/");
    
    
                    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 QVariant(QString::fromStdString(event_id(event)));
    
                    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;
                    };
    
    
    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 (!id.isEmpty() && id[0] == "m")
    
                            return qml_mtx_events::Sent;
    
                    else if (read.contains(id) || containsOthers(cache::readReceipts(id, room_id_)))
    
                    else
                            return qml_mtx_events::Received;
    
            case IsEncrypted: {
    
                    auto id              = event_id(event);
    
                    auto encrypted_event = events.get(id, id, false);
    
                    return encrypted_event &&
                           std::holds_alternative<
                             mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
                             *encrypted_event);
    
            case IsRoomEncrypted: {
                    return cache::isRoomEncrypted(room_id_.toStdString());
            }
    
            case ReplyTo:
                    return QVariant(QString::fromStdString(in_reply_to_event(event)));
    
            case Reactions: {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    auto id = event_id(event);
                    return QVariant::fromValue(events.reactions(id));
    
                    return QVariant(room_id_);
    
            case RoomName:
    
                    return QVariant(QString::fromStdString(room_name(event)));
    
            case RoomTopic:
    
                    return QVariant(QString::fromStdString(room_topic(event)));
    
    trilene's avatar
    trilene committed
            case CallType:
                    return QVariant(QString::fromStdString(call_type(event)));
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            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[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[Height], data(event, static_cast<int>(Height)));
                    m.insert(names[Width], data(event, static_cast<int>(Width)));
                    m.insert(names[ProportionalHeight],
                             data(event, static_cast<int>(ProportionalHeight)));
                    m.insert(names[Id], data(event, static_cast<int>(Id)));
                    m.insert(names[State], data(event, static_cast<int>(State)));
                    m.insert(names[IsEncrypted], data(event, static_cast<int>(IsEncrypted)));
                    m.insert(names[IsRoomEncrypted], data(event, static_cast<int>(IsRoomEncrypted)));
                    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)));
    
    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() >= rowCount())
    
                    return QVariant();
    
    
            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);
    
            return data(*event, role);
    
    bool
    TimelineModel::canFetchMore(const QModelIndex &) const
    {
    
            if (!events.size())
    
                first &&
                !std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>(*first))
    
    void
    TimelineModel::setPaginationInProgress(const bool paginationInProgress)
    {
            if (m_paginationInProgress == paginationInProgress) {
                    return;
            }
    
            m_paginationInProgress = paginationInProgress;
            emit paginationInProgressChanged(m_paginationInProgress);
    }
    
    
    void
    TimelineModel::fetchMore(const QModelIndex &)
    {
    
            if (m_paginationInProgress) {
    
                    nhlog::ui()->warn("Already loading older messages");
                    return;
            }
    
    
            setPaginationInProgress(true);
    
    
            events.fetchMore();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    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::Member>>(e)) {
                            emit roomAvatarUrlChanged();
                            emit roomNameChanged();
                    }
            }
    }
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    void
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
    
            if (timeline.events.empty())
                    return;
    
    
            events.handleSync(timeline);
    
            for (auto e : timeline.events) {
                    if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) {
                            MegolmSessionIndex index;
                            index.room_id    = room_id_.toStdString();
                            index.session_id = encryptedEvent->content.session_id;
                            index.sender_key = encryptedEvent->content.sender_key;
    
                            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>>)
    
                                      else {
                                              if (event.sender != http::client()->user_id().to_string())
                                                      emit newCallEvent(event);
                                      }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    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::Member>>(e)) {
                            emit roomAvatarUrlChanged();
                            emit roomNameChanged();
                    }
    
    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;
    }
    
    
    template<typename T>
    auto
    isMessage(const mtx::events::EncryptedEvent<T> &)
    {
            return true;
    }
    
    
    trilene's avatar
    trilene committed
    auto
    isMessage(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &)
    {
            return true;
    }
    
    auto
    isMessage(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &)
    {
            return true;
    }
    auto
    isMessage(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &)
    {
            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();
    }
    template<typename T>
    auto
    isYourJoin(const mtx::events::Event<T> &)
    {
            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();
                            emit manager_->updateRoomsLastMessage(
                              room_id_,
    
                              DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)),
    
                                       QString::fromStdString(http::client()->user_id().to_string()),
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                       tr("You joined this room."),
    
                                       utils::descriptiveTime(time),
                                       ts,
                                       time});
                            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))));
    
                    emit manager_->updateRoomsLastMessage(room_id_, description);
                    return;
            }
    
    void
    TimelineModel::setCurrentIndex(int index)
    {
    
            if (!ChatPage::instance()->isActiveWindow())
                    return;
    
    
            auto oldIndex = idToIndex(currentId);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            currentId     = indexToId(index);
    
            emit currentIndexChanged(index);
    
    
            if ((oldIndex > index || oldIndex == -1) && !currentId.startsWith("m")) {
    
                    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());
                    }
            });
    }
    
    
    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
    
    
    void
    TimelineModel::viewRawMessage(QString id) const
    {
    
            auto e = events.get(id.toStdString(), "", false);
    
            if (!e)
                    return;
            std::string ev = mtx::accessors::serialize_event(*e).dump(4);
    
            auto dialog    = new dialogs::RawMessage(QString::fromStdString(ev));
            Q_UNUSED(dialog);
    }
    
    TimelineModel::viewDecryptedRawMessage(QString id) const
    {
    
            auto e = events.get(id.toStdString(), "");
    
            if (!e)
                    return;
    
            std::string ev = mtx::accessors::serialize_event(*e).dump(4);
    
            auto dialog    = new dialogs::RawMessage(QString::fromStdString(ev));
            Q_UNUSED(dialog);
    }
    
    void
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    TimelineModel::openUserProfile(QString userid)
    
            emit openProfile(new UserProfile(room_id_, userid, manager_, this));
    
    void
    TimelineModel::replyAction(QString id)
    {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            setReply(id);
    }
    
    RelatedInfo
    TimelineModel::relatedInfo(QString id)
    {
    
            auto event = events.get(id.toStdString(), "");
    
            if (!event)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    return {};
    
    
            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);
    
    
            // get body, strip reply fallback, then transform the event to text, if it is a media event
            // etc
    
            related.quoted_body = QString::fromStdString(mtx::accessors::body(*event));
    
            QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption);
            while (related.quoted_body.startsWith(">"))
                    related.quoted_body.remove(plainQuote);
            if (related.quoted_body.startsWith("\n"))
                    related.quoted_body.remove(0, 1);
            related.quoted_body = utils::getQuoteBody(related);
    
            // get quoted body and strip reply fallback
    
            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;
    
    
            auto idx = events.idToIndex(id.toStdString());
            if (idx)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    return events.size() - *idx - 1;
    
            else
                    return -1;
    
    }
    
    QString
    TimelineModel::indexToId(int index) const
    {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            auto id = events.indexToId(events.size() - index - 1);
    
            return id ? QString::fromStdString(*id) : "";
    
    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) {
                            return;
                    }
                    emit dataChanged(index(idx, 0), index(idx, 0));
            }
    }
    
    template<typename T>
    
    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)},
    
                    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)
    
    trilene's avatar
    trilene committed
            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);
    
                            emit model_->addPendingMessageToStore(msg);
    
            // Do-nothing operator for all unhandled events
    
            template<typename T>
            void operator()(const mtx::events::Event<T> &)
            {}
    
    trilene's avatar
    trilene committed
    
    
            // 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)
    
    trilene's avatar
    trilene committed
                    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
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            void operator()(mtx::events::RoomEvent<mtx::events::msg::Reaction> msg)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    msg.type = mtx::events::EventType::Reaction;
    
                    emit model_->addPendingMessageToStore(msg);
    
    trilene's avatar
    trilene committed
            void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &event)
    
    trilene's avatar
    trilene committed
                    sendRoomEvent<mtx::events::msg::CallInvite, mtx::events::EventType::CallInvite>(
                      event);
    
    trilene's avatar
    trilene committed
    
            void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &event)
    
    trilene's avatar
    trilene committed
                    sendRoomEvent<mtx::events::msg::CallCandidates,
                                  mtx::events::EventType::CallCandidates>(event);
    
    trilene's avatar
    trilene committed
    
            void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &event)
    
    trilene's avatar
    trilene committed
                    sendRoomEvent<mtx::events::msg::CallAnswer, mtx::events::EventType::CallAnswer>(