Skip to content
Snippets Groups Projects
TimelineModel.cpp 77.1 KiB
Newer Older
  • Learn to ignore specific revisions
  •                        tr("You joined this room."),
                           utils::descriptiveTime(time),
                           ts,
                           time};
                if (description != lastMessage_) {
                    lastMessage_ = description;
                    emit lastMessageChanged();
                }
                return;
            }
            if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event))
                continue;
    
            auto description = utils::getMessageDescription(
              *event,
              QString::fromStdString(http::client()->user_id().to_string()),
              cache::displayName(room_id_, QString::fromStdString(mtx::accessors::sender(*event))));
            if (description != lastMessage_) {
                lastMessage_ = description;
                emit lastMessageChanged();
    
    void
    TimelineModel::setCurrentIndex(int index)
    {
    
        auto oldIndex = idToIndex(currentId);
        currentId     = indexToId(index);
        if (index != oldIndex)
            emit currentIndexChanged(index);
    
        if (MainWindow::instance() != 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());
            }
        });
    
    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);
    
        setReply(id);
    
    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
    void
    TimelineModel::editAction(QString id)
    {
    
        setEdit(id);
    
    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(QString id)
    
        emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this});
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    void
    
    TimelineModel::redactEvent(const QString &id)
    
        if (!id.isEmpty()) {
            auto edits = events.edits(id.toStdString());
    
            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 dataAtIdChanged(id);
    
    
            // 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);
                  });
            }
        }
    
    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));
        }
    
    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
    
    
        json doc = {{"type", mtx::events::to_string(eventType)},
                    {"content", 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());
    
                model_->sendEncryptedMessage(msg, Event);
            } else {
                msg.type = Event;
                emit model_->addPendingMessageToStore(msg);
            }
        }
    
        // Do-nothing operator for all unhandled events
        template<typename T>
        void operator()(const mtx::events::Event<T> &)
        {}
    
        // Operator for m.room.message events that contain a msgtype in their content
        template<typename T,
                 std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0>
        void operator()(mtx::events::RoomEvent<T> msg)
        {
            sendRoomEvent<T, mtx::events::EventType::RoomMessage>(msg);
        }
    
        // Special operator for reactions, which are a type of m.room.message, but need to be
        // handled distinctly for their differences from normal room messages.  Specifically,
        // reactions need to have the relation outside of ciphertext, or synapse / the homeserver
        // cannot handle it correctly.  See the MSC for more details:
        // https://github.com/matrix-org/matrix-doc/blob/matthew/msc1849/proposals/1849-aggregations.md#end-to-end-encryption
        void operator()(mtx::events::RoomEvent<mtx::events::msg::Reaction> msg)
        {
            msg.type = mtx::events::EventType::Reaction;
            emit model_->addPendingMessageToStore(msg);
        }
    
        void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &event)
        {
            sendRoomEvent<mtx::events::msg::CallInvite, mtx::events::EventType::CallInvite>(event);
        }
    
        void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &event)
        {
            sendRoomEvent<mtx::events::msg::CallCandidates, mtx::events::EventType::CallCandidates>(
              event);
        }
    
        void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &event)
        {
            sendRoomEvent<mtx::events::msg::CallAnswer, mtx::events::EventType::CallAnswer>(event);
        }
    
        void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &event)
        {
            sendRoomEvent<mtx::events::msg::CallHangUp, mtx::events::EventType::CallHangUp>(event);
        }
    
        void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg)
        {
            sendRoomEvent<mtx::events::msg::KeyVerificationRequest,
                          mtx::events::EventType::RoomMessage>(msg);
        }
    
        void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg)
        {
            sendRoomEvent<mtx::events::msg::KeyVerificationReady,
                          mtx::events::EventType::KeyVerificationReady>(msg);
        }
    
        void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg)
        {
            sendRoomEvent<mtx::events::msg::KeyVerificationStart,
                          mtx::events::EventType::KeyVerificationStart>(msg);
        }
    
        void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg)
        {
            sendRoomEvent<mtx::events::msg::KeyVerificationAccept,
                          mtx::events::EventType::KeyVerificationAccept>(msg);
        }
    
        void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg)
        {
            sendRoomEvent<mtx::events::msg::KeyVerificationMac,
                          mtx::events::EventType::KeyVerificationMac>(msg);
        }
    
        void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg)
        {
            sendRoomEvent<mtx::events::msg::KeyVerificationKey,
                          mtx::events::EventType::KeyVerificationKey>(msg);
        }
    
        void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg)
        {
            sendRoomEvent<mtx::events::msg::KeyVerificationDone,
                          mtx::events::EventType::KeyVerificationDone>(msg);
        }
    
        void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg)
        {
            sendRoomEvent<mtx::events::msg::KeyVerificationCancel,
                          mtx::events::EventType::KeyVerificationCancel>(msg);
        }
        void operator()(mtx::events::Sticker msg)
        {
            msg.type = mtx::events::EventType::Sticker;
            if (cache::isRoomEncrypted(model_->room_id_.toStdString())) {
                model_->sendEncryptedMessage(msg, mtx::events::EventType::Sticker);
            } else
                emit model_->addPendingMessageToStore(msg);
        }
    
        TimelineModel *model_;
    
    };
    
    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);
    
    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);
        }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    void
    
    TimelineModel::copyLinkToEvent(const QString &eventId) const
    
        QStringList vias;
    
        auto alias = cache::client()->getRoomAliases(room_id_.toStdString());
        QString room;
        if (alias) {
            room = QString::fromStdString(alias->alias);
            if (room.isEmpty() && !alias->alt_aliases.empty()) {
                room = QString::fromStdString(alias->alt_aliases.front());
    
        if (room.isEmpty())
            room = room_id_;
    
        vias.push_back(QStringLiteral("via=%1").arg(QString(
    
          QUrl::toPercentEncoding(QString::fromStdString(http::client()->user_id().hostname())))));
        auto members = cache::getMembers(room_id_.toStdString(), 0, 100);
        for (const auto &m : members) {
            if (vias.size() >= 4)
                break;
    
            auto user_id   = mtx::identifiers::parse<mtx::identifiers::User>(m.user_id.toStdString());
    
            QString server = QStringLiteral("via=%1").arg(
    
              QString(QUrl::toPercentEncoding(QString::fromStdString(user_id.hostname()))));
    
            if (!vias.contains(server))
                vias.push_back(server);
        }
    
        auto link = QStringLiteral("https://matrix.to/#/%1/%2?%3")
    
                      .arg(QString(QUrl::toPercentEncoding(room)),
                           QString(QUrl::toPercentEncoding(eventId)),
                           vias.join('&'));
    
        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()));
    
    TimelineModel::formatJoinRuleEvent(const QString &id)
    
        mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
        if (!e)
    
    
        auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::JoinRules>>(e);
        if (!event)
    
    
        QString user = QString::fromStdString(event->sender);
        QString name = utils::replaceEmoji(displayName(user));
    
        switch (event->content.join_rule) {
        case mtx::events::state::JoinRule::Public:
            return tr("%1 opened the room to the public.").arg(name);
        case mtx::events::state::JoinRule::Invite:
            return tr("%1 made this room require and invitation to join.").arg(name);
        case mtx::events::state::JoinRule::Knock:
            return tr("%1 allowed to join this room by knocking.").arg(name);
        case mtx::events::state::JoinRule::Restricted: {
            QStringList rooms;
            for (const auto &r : event->content.allow) {
                if (r.type == mtx::events::state::JoinAllowanceType::RoomMembership)
                    rooms.push_back(QString::fromStdString(r.room_id));
            }
            return tr("%1 allowed members of the following rooms to automatically join this "
                      "room: %2")
    
              .arg(name, rooms.join(QStringLiteral(", ")));
    
        }
        default:
            // Currently, knock and private are reserved keywords and not implemented in Matrix.
    
    TimelineModel::formatGuestAccessEvent(const QString &id)
    
        mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
        if (!e)
    
        auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::GuestAccess>>(e);
        if (!event)
    
        QString user = QString::fromStdString(event->sender);
        QString name = utils::replaceEmoji(displayName(user));
    
        switch (event->content.guest_access) {
        case mtx::events::state::AccessState::CanJoin:
            return tr("%1 made the room open to guests.").arg(name);
        case mtx::events::state::AccessState::Forbidden:
            return tr("%1 has closed the room to guest access.").arg(name);
        default:
    
    TimelineModel::formatHistoryVisibilityEvent(const QString &id)
    
        mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
        if (!e)
    
        auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::HistoryVisibility>>(e);
    
        if (!event)
    
    
        QString user = QString::fromStdString(event->sender);
        QString name = utils::replaceEmoji(displayName(user));
    
        switch (event->content.history_visibility) {
        case mtx::events::state::Visibility::WorldReadable:
            return tr("%1 made the room history world readable. Events may be now read by "
                      "non-joined people.")
              .arg(name);
        case mtx::events::state::Visibility::Shared:
            return tr("%1 set the room history visible to members from this point on.").arg(name);
        case mtx::events::state::Visibility::Invited:
            return tr("%1 set the room history visible to members since they were invited.").arg(name);
        case mtx::events::state::Visibility::Joined:
            return tr("%1 set the room history visible to members since they joined the room.")
              .arg(name);
        default:
    
    TimelineModel::formatPowerLevelEvent(const QString &id)
    
        mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
        if (!e)
    
        auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(e);
        if (!event)
    
            return QString();
    
        QString user = QString::fromStdString(event->sender);
        QString name = utils::replaceEmoji(displayName(user));
    
        // TODO: power levels rendering is actually a bit complex. work on this later.
        return tr("%1 has changed the room's permissions.").arg(name);
    
    QVariantMap
    
    TimelineModel::formatRedactedEvent(const QString &id)
    
    {
        QVariantMap pair{{"first", ""}, {"second", ""}};
        mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
        if (!e)
            return pair;
    
        auto event = std::get_if<mtx::events::RoomEvent<mtx::events::msg::Redacted>>(e);
        if (!event)
            return pair;
    
        QString dateTime = QDateTime::fromMSecsSinceEpoch(event->origin_server_ts).toString();
    
        QString reason   = QLatin1String("");
    
        auto because     = event->unsigned_data.redacted_because;
        // User info about who actually sent the redacted event.
    
        QString redactedUser;
        QString redactedName;
    
    
        if (because.has_value()) {
    
            redactedUser = QString::fromStdString(because->sender).toHtmlEscaped();
            redactedName = utils::replaceEmoji(displayName(redactedUser));
            reason       = QString::fromStdString(because->content.reason).toHtmlEscaped();
    
        }
    
        if (reason.isEmpty()) {
    
            pair[QStringLiteral("first")] = tr("Removed by %1").arg(redactedName);
            pair[QStringLiteral("second")] =
    
              tr("%1 (%2) removed this message at %3").arg(redactedName, redactedUser, dateTime);
        } else {
    
            pair[QStringLiteral("first")]  = tr("Removed by %1 because: %2").arg(redactedName, reason);
            pair[QStringLiteral("second")] = tr("%1 (%2) removed this message at %3\nReason: %4")
    
                                               .arg(redactedName, redactedUser, dateTime, reason);
    
    TimelineModel::acceptKnock(const QString &id)
    
        mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
        if (!e)
            return;
    
        auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e);
        if (!event)
            return;
    
        if (!permissions_.canInvite())
            return;
    
        if (cache::isRoomMember(event->state_key, room_id_.toStdString()))
            return;
    
        using namespace mtx::events::state;
        if (event->content.membership != Membership::Knock)
            return;
    
        ChatPage::instance()->inviteUser(QString::fromStdString(event->state_key), QLatin1String(""));
    
    TimelineModel::showAcceptKnockButton(const QString &id)
    
        mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
        if (!e)
            return false;
    
        auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e);
        if (!event)
            return false;
    
        if (!permissions_.canInvite())
            return false;
    
        if (cache::isRoomMember(event->state_key, room_id_.toStdString()))
            return false;
    
        using namespace mtx::events::state;
        return event->content.membership == Membership::Knock;
    
    TimelineModel::formatMemberEvent(const QString &id)
    
        mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), "");
        if (!e)
    
    
        auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e);
        if (!event)
    
    
        mtx::events::StateEvent<mtx::events::state::Member> *prevEvent = nullptr;
        if (!event->unsigned_data.replaces_state.empty()) {
            auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id);
            if (tempPrevEvent) {
                prevEvent =
                  std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(tempPrevEvent);