Skip to content
Snippets Groups Projects
TimelineModel.cpp 110 KiB
Newer Older
Nicolas Werner's avatar
Nicolas Werner committed
            emit widgetLinksChanged();
        else if (std::holds_alternative<StateEvent<state::PowerLevels>>(e)) {
            permissions_.invalidate();
            emit permissionsChanged();
        } else if (std::holds_alternative<StateEvent<state::Member>>(e)) {
            emit roomAvatarUrlChanged();
            emit roomNameChanged();
            emit roomMemberCountChanged();
        } else if (std::holds_alternative<StateEvent<state::Encryption>>(e)) {
            this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString());
            emit encryptionChanged();
        } else if (std::holds_alternative<StateEvent<state::space::Parent>>(e)) {
            this->parentChecked = false;
            emit parentSpaceChanged();
    updateLastMessage();
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::voip::CallInvite> &)
trilene's avatar
trilene committed
{
    return true;
isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallAnswer> &)
trilene's avatar
trilene committed
{
    return true;
trilene's avatar
trilene committed
}
auto
isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallHangUp> &)
trilene's avatar
trilene committed
{
    return true;
auto
isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallReject> &)
{
    return true;
}
auto
isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallSelectAnswer> &)
{
    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;
DescInfo
TimelineModel::lastMessage() const
{
    if (lastMessage_.event_id.isEmpty())
        QTimer::singleShot(0, this, &TimelineModel::updateLastMessage);

    return lastMessage_;
}

void
TimelineModel::updateLastMessage()
    // only try to generate a preview for the last 1000 messages
    auto end = std::max(events.size() - 1001, 0);
    for (auto it = events.size() - 1; it >= end; --it) {
        auto event = events.get(it, decryptDescription);
        if (!event)
            continue;

        if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) {
            auto time   = mtx::accessors::origin_server_ts(*event);
            uint64_t ts = time.toMSecsSinceEpoch();
            auto description =
              DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)),
                       QString::fromStdString(http::client()->user_id().to_string()),
                       tr("You joined this room."),
                       utils::descriptiveTime(time),
                       ts,
                       time};
            if (description != lastMessage_) {
                if (lastMessage_.timestamp == 0) {
                    cache::client()->updateLastMessageTimestamp(room_id_.toStdString(),
                                                                description.timestamp);
                }
                lastMessage_ = description;
                emit lastMessageChanged();
            }
            return;
        }
        if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event))
            continue;

        auto description = utils::getMessageDescription(
          *event,
          QString::fromStdString(http::client()->user_id().to_string()),
          cache::displayName(room_id_, QString::fromStdString(mtx::accessors::sender(*event))));
        if (description != lastMessage_) {
            if (lastMessage_.timestamp == 0) {
                cache::client()->updateLastMessageTimestamp(room_id_.toStdString(),
                                                            description.timestamp);
            }
            lastMessage_ = description;
            emit lastMessageChanged();
void
TimelineModel::setCurrentIndex(int index)
{
    auto oldIndex = idToIndex(currentId);
    currentId     = indexToId(index);
    if (index != oldIndex)
        emit currentIndexChanged(index);
Nicolas Werner's avatar
Nicolas Werner committed
    if (!QGuiApplication::focusWindow() || !QGuiApplication::focusWindow()->isActive() ||
        MainWindow::instance()->windowForRoom(roomId()) != QGuiApplication::focusWindow())
Nicolas Werner's avatar
Nicolas Werner committed
    if (!currentId.startsWith('m')) {
        auto oldReadIndex =
          cache::getEventIndex(roomId().toStdString(), currentReadId.toStdString());
        auto nextEventIndexAndId =
          cache::lastInvisibleEventAfter(roomId().toStdString(), currentId.toStdString());
        if (nextEventIndexAndId && (!oldReadIndex || *oldReadIndex < nextEventIndexAndId->first)) {
            readEvent(nextEventIndexAndId->second);
            currentReadId = QString::fromStdString(nextEventIndexAndId->second);
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());
          }
      },
      !UserSettings::instance()->readReceipts());
TimelineModel::displayName(const QString &id) const
    return cache::displayName(room_id_, id).toHtmlEscaped();
TimelineModel::avatarUrl(const 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(QStringLiteral("[^a-zA-Z]*y+[^a-zA-Z]*"));
        fmt = fmt.remove(rx);
    }
    return date.toString(fmt);
Nicolas Werner's avatar
Nicolas Werner committed

TimelineModel::viewRawMessage(const QString &id)
    auto e = events.get(id.toStdString(), "", false);
    if (!e)
        return;
    std::string ev = mtx::accessors::serialize_event(*e).dump(4);
    emit showRawMessageDialog(QString::fromStdString(ev));
targetakhil's avatar
targetakhil committed
void
MTRNord's avatar
MTRNord committed
TimelineModel::forwardMessage(const QString &eventId, QString roomId)
    auto e = events.get(eventId.toStdString(), "");
    if (!e)
        return;
TimelineModel::viewDecryptedRawMessage(const QString &id)
    auto e = events.get(id.toStdString(), "");
    if (!e)
        return;
    std::string ev = mtx::accessors::serialize_event(*e).dump(4);
    emit showRawMessageDialog(QString::fromStdString(ev));
    UserProfile *userProfile = new UserProfile(room_id_, std::move(userid), manager_, this);
    connect(this, &TimelineModel::roomAvatarUrlChanged, userProfile, &UserProfile::updateAvatarUrl);
    emit manager_->openProfile(userProfile);
Nicolas Werner's avatar
Nicolas Werner committed
void
TimelineModel::unpin(const QString &id)
Nicolas Werner's avatar
Nicolas Werner committed
{
    auto pinned =
      cache::client()->getStateEvent<mtx::events::state::PinnedEvents>(room_id_.toStdString());

    mtx::events::state::PinnedEvents content{};
    if (pinned)
        content = pinned->content;

    auto idStr = id.toStdString();

    for (auto it = content.pinned.begin(); it != content.pinned.end(); ++it) {
        if (*it == idStr) {
            content.pinned.erase(it);
            break;
        }
    }

    http::client()->send_state_event(
      room_id_.toStdString(),
      content,
      [idStr](const mtx::responses::EventId &, mtx::http::RequestErr err) {
          if (err)
              nhlog::net()->error("Failed to unpin {}: {}", idStr, *err);
          else
              nhlog::net()->debug("Unpinned {}", idStr);
      });
}

void
TimelineModel::pin(const QString &id)
Nicolas Werner's avatar
Nicolas Werner committed
{
    auto pinned =
      cache::client()->getStateEvent<mtx::events::state::PinnedEvents>(room_id_.toStdString());

    mtx::events::state::PinnedEvents content{};
    if (pinned)
        content = pinned->content;

    auto idStr = id.toStdString();
    content.pinned.push_back(idStr);

    http::client()->send_state_event(
      room_id_.toStdString(),
      content,
      [idStr](const mtx::responses::EventId &, mtx::http::RequestErr err) {
          if (err)
              nhlog::net()->error("Failed to pin {}: {}", idStr, *err);
          else
              nhlog::net()->debug("Pinned {}", idStr);
      });
}

Nicolas Werner's avatar
Nicolas Werner committed
RelatedInfo
TimelineModel::relatedInfo(const QString &id)
    auto event = events.get(id.toStdString(), "");
    if (!event)
        return {};
    return utils::stripReplyFallbacks(*event, id.toStdString(), room_id_);
TimelineModel::showReadReceipts(const QString &id)
    emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this});
void
TimelineModel::redactAllFromUser(const QString &userid, const QString &reason)
{
    auto user = userid.toStdString();
    std::vector<QString> toRedact;
    for (auto it = events.size() - 1; it >= 0; --it) {
        auto event = events.get(it, false);
        if (event && mtx::accessors::sender(*event) == user &&
            !std::holds_alternative<mtx::events::RoomEvent<mtx::events::msg::Redacted>>(*event)) {
            toRedact.push_back(QString::fromStdString(mtx::accessors::event_id(*event)));
        }
    }

    for (const auto &e : toRedact) {
        redactEvent(e, reason);
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}
Nicolas Werner's avatar
Nicolas Werner committed
void
TimelineModel::redactEvent(const QString &id, const QString &reason)
    if (!id.isEmpty()) {
        auto edits = events.edits(id.toStdString());
        http::client()->redact_event(
          room_id_.toStdString(),
          id.toStdString(),
          [this, id, reason](const mtx::responses::EventId &, mtx::http::RequestErr err) {
              if (err) {
                  if (err->status_code == 429 && err->matrix_error.retry_after.count() != 0) {
                      QTimer::singleShot(err->matrix_error.retry_after, this, [this, id, reason]() {
                          this->redactEvent(id, reason);
                      });
                      return;
                  }
                  emit redactionFailed(tr("Message redaction failed: %1")
                                         .arg(QString::fromStdString(err->matrix_error.error)));
                  return;
              }

              emit dataAtIdChanged(id);
          },
          reason.toStdString());

        // redact all edits to prevent leaks
        for (const auto &e : edits) {
            const auto &id_ = mtx::accessors::event_id(e);
            http::client()->redact_event(
              room_id_.toStdString(),
              id_,
              [this, id, 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 dataAtIdChanged(id);
              },
              reason.toStdString());
TimelineModel::idToIndex(const QString &id) const
    if (id.isEmpty())
        return -1;
    auto idx = events.idToIndex(id.toStdString());
    if (idx)
        return events.size() - *idx - 1;
    else
        return -1;
}

QString
TimelineModel::indexToId(int index) const
{
    auto id = events.indexToId(events.size() - index - 1);
    return id ? QString::fromStdString(*id) : QLatin1String("");
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));
    }
TimelineModel::updateLastReadId(const QString &currentRoomId)
{
    if (currentRoomId == room_id_) {
        last_event_id = cache::getFullyReadEventId(room_id_.toStdString());
        auto lastVisibleEventIndexAndId =
          cache::lastVisibleEvent(room_id_.toStdString(), last_event_id);
        if (lastVisibleEventIndexAndId) {
            fullyReadEventId_ = lastVisibleEventIndexAndId->second;
            emit fullyReadEventIdChanged();
        }
    }
}

void
TimelineModel::lastReadIdOnWindowFocus()
{
    /* this stops it from removing the line when focusing another window
     * and from removing the line when refocusing nheko */
    if (ChatPage::instance()->isRoomActive(room_id_) &&
        cache::calculateRoomReadStatus(room_id_.toStdString())) {
        updateLastReadId(room_id_);
    }
}

/*
 * if the event2order db didn't have the messages we needed when the room was opened
 * try again after these new messages were fetched
 */
void
TimelineModel::checkAfterFetch()
{
    if (fullyReadEventId_.empty()) {
        auto lastVisibleEventIndexAndId =
          cache::lastVisibleEvent(room_id_.toStdString(), last_event_id);
        if (lastVisibleEventIndexAndId) {
            fullyReadEventId_ = lastVisibleEventIndexAndId->second;
            emit fullyReadEventIdChanged();
        }
    }
}

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;
trilene's avatar
trilene committed

    nlohmann::json doc = {{"type", mtx::events::to_string(eventType)},
                          {"content", nlohmann::json(msg.content)},
                          {"room_id", room_id}};
trilene's avatar
trilene committed

    try {
        mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event;
        event.content  = olm::encrypt_group_message(room_id, http::client()->device_id(), doc);
        event.event_id = msg.event_id;
        event.room_id  = room_id;
        event.sender   = http::client()->user_id().to_string();
        event.type     = mtx::events::EventType::RoomEncrypted;
        event.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
        emit this->addPendingMessageToStore(event);
        // TODO: Let the user know about the errors.
    } catch (const lmdb::error &e) {
        nhlog::db()->critical("failed to open outbound megolm session ({}): {}", room_id, e.what());
        emit ChatPage::instance()->showNotification(
          tr("Failed to encrypt event, sending aborted!"));
    } catch (const mtx::crypto::olm_exception &e) {
        nhlog::crypto()->critical(
          "failed to open outbound megolm session ({}): {}", room_id, e.what());
        emit ChatPage::instance()->showNotification(
          tr("Failed to encrypt event, sending aborted!"));
    }
}
struct SendMessageVisitor
{
    explicit SendMessageVisitor(TimelineModel *model)
      : model_(model)
    template<typename T, mtx::events::EventType Event>
    void sendRoomEvent(mtx::events::RoomEvent<T> msg)
    {
        if (cache::isRoomEncrypted(model_->room_id_.toStdString())) {
            auto encInfo = mtx::accessors::file(msg);
            if (encInfo)
                emit model_->newEncryptedImage(encInfo.value());
            encInfo = mtx::accessors::thumbnail_file(msg);
            if (encInfo)
                emit model_->newEncryptedImage(encInfo.value());

            model_->sendEncryptedMessage(msg, Event);
        } else {
            msg.type = Event;
            emit model_->addPendingMessageToStore(msg);
        }
    }

    // Do-nothing operator for all unhandled events
    template<typename T>
    void operator()(const mtx::events::Event<T> &)

    // Operator for m.room.message events that contain a msgtype in their content
    template<typename T,
             std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0>
    void operator()(mtx::events::RoomEvent<T> msg)
    {
        sendRoomEvent<T, mtx::events::EventType::RoomMessage>(msg);
    }

    // Special operator for reactions, which are a type of m.room.message, but need to be
    // handled distinctly for their differences from normal room messages.  Specifically,
    // reactions need to have the relation outside of ciphertext, or synapse / the homeserver
    // cannot handle it correctly.  See the MSC for more details:
    // https://github.com/matrix-org/matrix-doc/blob/matthew/msc1849/proposals/1849-aggregations.md#end-to-end-encryption
    void operator()(mtx::events::RoomEvent<mtx::events::msg::Reaction> msg)
    {
        msg.type = mtx::events::EventType::Reaction;
        emit model_->addPendingMessageToStore(msg);
    }

    void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallInvite> &event)
        sendRoomEvent<mtx::events::voip::CallInvite, mtx::events::EventType::CallInvite>(event);
    void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallCandidates> &event)
        sendRoomEvent<mtx::events::voip::CallCandidates, mtx::events::EventType::CallCandidates>(
    void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallAnswer> &event)
        sendRoomEvent<mtx::events::voip::CallAnswer, mtx::events::EventType::CallAnswer>(event);
    void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallHangUp> &event)
        sendRoomEvent<mtx::events::voip::CallHangUp, mtx::events::EventType::CallHangUp>(event);
    void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallSelectAnswer> &event)
    {
        sendRoomEvent<mtx::events::voip::CallSelectAnswer,
                      mtx::events::EventType::CallSelectAnswer>(event);
    }

    void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallReject> &event)
    {
        sendRoomEvent<mtx::events::voip::CallReject, mtx::events::EventType::CallReject>(event);
    }

    void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallNegotiate> &event)
    {
        sendRoomEvent<mtx::events::voip::CallNegotiate, mtx::events::EventType::CallNegotiate>(
          event);
    }

    void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg)
    {
        sendRoomEvent<mtx::events::msg::KeyVerificationRequest,
                      mtx::events::EventType::RoomMessage>(msg);
    }

    void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg)
    {
        sendRoomEvent<mtx::events::msg::KeyVerificationReady,
                      mtx::events::EventType::KeyVerificationReady>(msg);
    }

    void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg)
    {
        sendRoomEvent<mtx::events::msg::KeyVerificationStart,
                      mtx::events::EventType::KeyVerificationStart>(msg);
    }

    void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg)
    {
        sendRoomEvent<mtx::events::msg::KeyVerificationAccept,
                      mtx::events::EventType::KeyVerificationAccept>(msg);
    }

    void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg)
    {
        sendRoomEvent<mtx::events::msg::KeyVerificationMac,
                      mtx::events::EventType::KeyVerificationMac>(msg);
    }

    void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg)
    {
        sendRoomEvent<mtx::events::msg::KeyVerificationKey,
                      mtx::events::EventType::KeyVerificationKey>(msg);
    }

    void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg)
    {
        sendRoomEvent<mtx::events::msg::KeyVerificationDone,
                      mtx::events::EventType::KeyVerificationDone>(msg);
    }

    void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg)
    {
        sendRoomEvent<mtx::events::msg::KeyVerificationCancel,
                      mtx::events::EventType::KeyVerificationCancel>(msg);
    }
    void operator()(mtx::events::Sticker msg)
    {
        msg.type = mtx::events::EventType::Sticker;
        if (cache::isRoomEncrypted(model_->room_id_.toStdString())) {
            model_->sendEncryptedMessage(msg, mtx::events::EventType::Sticker);
        } else
            emit model_->addPendingMessageToStore(msg);
    }
    TimelineModel *model_;
};

void
TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event)
{
    std::visit(
      [](auto &msg) {
          // gets overwritten for reactions and stickers in SendMessageVisitor
          msg.type             = mtx::events::EventType::RoomMessage;
          msg.event_id         = "m" + http::client()->generate_txn_id();
          msg.sender           = http::client()->user_id().to_string();
          msg.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
      },
      event);
    std::visit(SendMessageVisitor{this}, event);

    fullyReadEventId_ = this->EventId;
    emit fullyReadEventIdChanged();
MTRNord's avatar
MTRNord committed
    cacheMedia(eventId, [](const QString &filename) {
        QDesktopServices::openUrl(QUrl::fromLocalFile(filename));
    });
TimelineModel::saveMedia(const QString &eventId) const
    mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), "");
    if (!event)
        return false;
    QString mxcUrl           = QString::fromStdString(mtx::accessors::url(*event));
    QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event));
    QString mimeType         = QString::fromStdString(mtx::accessors::mimetype(*event));
    auto encryptionInfo = mtx::accessors::file(*event);
    qml_mtx_events::EventType eventType = toRoomEventType(*event);
    QString dialogTitle;
    if (eventType == qml_mtx_events::EventType::ImageMessage) {
        dialogTitle = tr("Save image");
    } else if (eventType == qml_mtx_events::EventType::VideoMessage) {
        dialogTitle = tr("Save video");
    } else if (eventType == qml_mtx_events::EventType::AudioMessage) {
        dialogTitle = tr("Save audio");
    } else {
        dialogTitle = tr("Save file");
    }
    const QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString();
    const QString downloadsFolder =
      QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
    const QString openLocation = downloadsFolder + "/" + originalFilename;
    const QString filename =
Nicolas Werner's avatar
Nicolas Werner committed
      QFileDialog::getSaveFileName(nullptr, dialogTitle, openLocation, filterString);
    if (filename.isEmpty())
        return false;
    const auto url = mxcUrl.toStdString();
    http::client()->download(url,
                             [filename, url, encryptionInfo](const std::string &data,
                                                             const std::string &,
                                                             const std::string &,
                                                             mtx::http::RequestErr err) {
                                 if (err) {
                                     nhlog::net()->warn("failed to retrieve image {}: {} {}",
                                                        url,
                                                        err->matrix_error.error,
                                                        static_cast<int>(err->status_code));
                                     return;
                                 }
                                 try {
                                     auto temp = data;
                                     if (encryptionInfo)
                                         temp = mtx::crypto::to_string(
                                           mtx::crypto::decrypt_file(temp, encryptionInfo.value()));

                                     QFile file(filename);

                                     if (!file.open(QIODevice::WriteOnly))
                                         return;

                                     file.write(QByteArray(temp.data(), (int)temp.size()));
                                     file.close();

                                     return;
                                 } catch (const std::exception &e) {
                                     nhlog::ui()->warn("Error while saving file to: {}", e.what());
                                 }
                             });
    return true;
MTRNord's avatar
MTRNord committed
TimelineModel::cacheMedia(const QString &eventId,
                          const std::function<void(const QString)> &callback)
    mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), "");
    if (!event)
        return;
    QString mxcUrl   = QString::fromStdString(mtx::accessors::url(*event));
    QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event));
    auto encryptionInfo = mtx::accessors::file(*event);
    // If the message is a link to a non mxcUrl, don't download it
    if (!mxcUrl.startsWith(QLatin1String("mxc://"))) {
        emit mediaCached(mxcUrl, mxcUrl);
        return;
    }

    QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix();

    const auto url  = mxcUrl.toStdString();
    const auto name = QString(mxcUrl).remove(QStringLiteral("mxc://"));
    QFileInfo filename(
      QStringLiteral("%1/media_cache/%2.%3")
        .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation), name, suffix));
    if (QDir::cleanPath(name) != name) {
        nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
        return;
    }
    QDir().mkpath(filename.path());
    if (filename.isReadable()) {
#if defined(Q_OS_WIN)
        emit mediaCached(mxcUrl, filename.filePath());
        emit mediaCached(mxcUrl, "file://" + filename.filePath());
        if (callback) {
            callback(filename.filePath());
        return;
    }

    http::client()->download(
      url,
      [this, callback, mxcUrl, filename, url, encryptionInfo](const std::string &data,
                                                              const std::string &,
                                                              const std::string &,
                                                              mtx::http::RequestErr err) {
          if (err) {
              nhlog::net()->warn("failed to retrieve image {}: {} {}",
                                 url,
                                 err->matrix_error.error,
                                 static_cast<int>(err->status_code));
              return;
          }

          try {
              auto temp = data;
              if (encryptionInfo)
                  temp =
                    mtx::crypto::to_string(mtx::crypto::decrypt_file(temp, encryptionInfo.value()));

              QFile file(filename.filePath());

              if (!file.open(QIODevice::WriteOnly))
                  return;

              file.write(QByteArray(temp.data(), (int)temp.size()));
              file.close();

              if (callback) {
                  callback(filename.filePath());
              }
          } catch (const std::exception &e) {
              nhlog::ui()->warn("Error while saving file to: {}", e.what());
          }
#if defined(Q_OS_WIN)
          emit mediaCached(mxcUrl, filename.filePath());
          emit mediaCached(mxcUrl, "file://" + filename.filePath());
TimelineModel::cacheMedia(const QString &eventId)
    cacheMedia(eventId, nullptr);
void
TimelineModel::showEvent(QString eventId)
{
    using namespace std::chrono_literals;
    // Direct to eventId
    if (eventId[0] == '$') {
        int idx = idToIndex(eventId);
        if (idx == -1) {
            nhlog::ui()->warn("Scrolling to event id {}, failed - no known index",
                              eventId.toStdString());
            return;
        eventIdToShow = eventId;
        emit scrollTargetChanged();
        showEventTimer.start(50ms);
        return;
    }
    // to message index
    eventId       = indexToId(eventId.toInt());
    eventIdToShow = eventId;
    emit scrollTargetChanged();
    showEventTimer.start(50ms);
    return;
}

void
TimelineModel::eventShown()
{
    eventIdToShow.clear();
    emit scrollTargetChanged();
}

QString
TimelineModel::scrollTarget() const
{
    return eventIdToShow;
}

void
TimelineModel::scrollTimerEvent()
{
    if (eventIdToShow.isEmpty() || showEventTimerCounter > 3) {
        showEventTimer.stop();
        showEventTimerCounter = 0;
    } else {
        emit scrollToIndex(idToIndex(eventIdToShow));
        showEventTimerCounter++;
    }
TimelineModel::requestKeyForEvent(const QString &id)
    auto encrypted_event = events.get(id.toStdString(), "", false);
    if (encrypted_event) {
        if (auto ev = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
              encrypted_event))
            events.requestSession(*ev, true);
    }
QString
TimelineModel::getBareRoomLink(const QString &roomId)
    auto alias =
      cache::client()->getStateEvent<mtx::events::state::CanonicalAlias>(roomId.toStdString());
    QString room;
    if (alias) {
        room = QString::fromStdString(alias->content.alias);
        if (room.isEmpty() && !alias->content.alt_aliases.empty()) {
            room = QString::fromStdString(alias->content.alt_aliases.front());
    if (room.isEmpty())
        room = roomId;

    return QStringLiteral("https://matrix.to/#/%1").arg(QString(QUrl::toPercentEncoding(room)));
}

QString
TimelineModel::getRoomVias(const QString &roomId)
{
    QStringList vias;
    for (const auto &m : utils::roomVias(roomId.toStdString())) {
        if (vias.size() >= 4)
            break;
        QString server =
          QStringLiteral("via=%1").arg(QString(QUrl::toPercentEncoding(QString::fromStdString(m))));
        if (!vias.contains(server))
            vias.push_back(server);
    }
    return vias.join("&");
}
void
TimelineModel::copyLinkToEvent(const QString &eventId) const
{
    auto link = QStringLiteral("%1/%2?%3")
                  .arg(getBareRoomLink(room_id_),
                       QString(QUrl::toPercentEncoding(eventId)),
                       getRoomVias(room_id_));
    QGuiApplication::clipboard()->setText(link);
QString
TimelineModel::formatTypingUsers(const std::vector<QString> &users, const QColor &bg)
    QString temp =
      tr("%1 and %2 are typing.",
         "Multiple users are typing. First argument is a comma separated list of potentially "
         "multiple users. Second argument is the last user of that list. (If only one user is "
         "typing, %1 is empty. You should still use it in your string though to silence Qt "
         "warnings.)",
         (int)users.size());
    if (users.empty()) {
    QStringList uidWithoutLast;
    auto formatUser = [this, bg](const QString &user_id) -> QString {
        auto uncoloredUsername = utils::replaceEmoji(displayName(user_id));
        QString prefix =
          QStringLiteral("<font color=\"%1\">").arg(manager_->userColor(user_id, bg).name());
        // color only parts that don't have a font already specified
        QString coloredUsername;
        int index = 0;
        do {
            auto startIndex = uncoloredUsername.indexOf(QLatin1String("<font"), index);
            if (startIndex - index != 0)
                coloredUsername +=
Nicolas Werner's avatar
Nicolas Werner committed
                  prefix + uncoloredUsername.mid(index, startIndex > 0 ? startIndex - index : -1) +
                  QStringLiteral("</font>");
            auto endIndex = uncoloredUsername.indexOf(QLatin1String("</font>"), startIndex);
            if (endIndex > 0)
                endIndex += sizeof("</font>") - 1;
            if (endIndex - startIndex != 0)
Nicolas Werner's avatar
Nicolas Werner committed
                coloredUsername +=
                  QStringView(uncoloredUsername).mid(startIndex, endIndex - startIndex);
            index = endIndex;
        } while (index > 0 && index < uncoloredUsername.size());
        return coloredUsername;
    };
    uidWithoutLast.reserve(static_cast<int>(users.size()));
    for (size_t i = 0; i + 1 < users.size(); i++) {
        uidWithoutLast.append(formatUser(users[i]));
    }
    return temp.arg(uidWithoutLast.join(QStringLiteral(", ")), formatUser(users.back()));