Skip to content
Snippets Groups Projects
roommodel.cpp 20.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • #include "roommodel.h"
    
    
    #include <QGuiApplication>
    
    
    #include <QMetaType>
    #include <QSharedPointer>
    
    #include <QtMath>
    
    #include <QDebug>
    
    
    #include "client.h"
    #include "debug_out.h"
    
    Q_DECLARE_METATYPE(std::vector<mtx::events::collections::TimelineEvents>);
    
    Q_DECLARE_METATYPE(QSharedPointer<Room>);
    
    using namespace mtx::events;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    template <class T> std::string eventUrl(const Event<T> &) { return ""; }
    template <class T> auto eventUrl(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.url) {
        return e.content.url;
    }
    
    template <class T> std::string eventBody(const Event<T> &) { return ""; }
    template <class T> auto eventBody(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.body) {
        return e.content.body;
    }
    
    std::string eventBody(const StateEvent<state::Member> &e) {
        switch (e.content.membership) {
        case state::Membership::Invite:
            return QCoreApplication::translate("Room", "member_invited: %1 %2")
                .arg(QString::fromStdString(e.state_key))
                .arg(QString::fromStdString(e.content.display_name))
                .toStdString();
        case state::Membership::Ban:
            return QCoreApplication::translate("Room", "member_banned: %1 %2")
                .arg(QString::fromStdString(e.state_key))
                .arg(QString::fromStdString(e.content.display_name))
                .toStdString();
        case state::Membership::Leave:
            return QCoreApplication::translate("Room", "member_left: %1 %2")
                .arg(QString::fromStdString(e.state_key))
                .arg(QString::fromStdString(e.content.display_name))
                .toStdString();
        case state::Membership::Knock:
            return QCoreApplication::translate("Room", "member_knocked: %1 %2")
                .arg(QString::fromStdString(e.state_key))
                .arg(QString::fromStdString(e.content.display_name))
                .toStdString();
        case state::Membership::Join:
        default:
            return QCoreApplication::translate("Room", "member_joined: %1 %2")
                .arg(QString::fromStdString(e.state_key))
                .arg(QString::fromStdString(e.content.display_name))
                .toStdString();
        }
    }
    
    
    template <class T> std::string eventFormattedBody(const Event<T> &) { return ""; }
    template <class T> auto eventFormattedBody(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.formatted_body) {
        auto temp = e.content.formatted_body;
    
        if (!temp.empty()) {
            auto pos = temp.find("<mx-reply>");
            if (pos != std::string::npos)
                temp.erase(pos, std::string("<mx-reply>").size());
            pos = temp.find("</mx-reply>");
            if (pos != std::string::npos)
                temp.erase(pos, std::string("</mx-reply>").size());
    
            return temp;
    
            return e.content.body;
    }
    
    
    template <class T> auto eventUserId(const mtx::events::RoomEvent<T> &e) -> std::string { return e.sender; }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    template <class T> auto eventTimestamp(const mtx::events::Event<T> &e) -> uint64_t { return 0; }
    template <class T> auto eventTimestamp(const mtx::events::RoomEvent<T> &e) -> uint64_t { return e.origin_server_ts; }
    
    template <class T> auto eventEventId(const mtx::events::Event<T> &e) -> std::string { return ""; }
    template <class T> auto eventEventId(const mtx::events::StateEvent<T> &e) -> std::string { return e.event_id; }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    template <class T>::EventType::Type toRoomEventType(const Event<T> &e) {
        using mtx::events::EventType;
        switch (e.type) {
        case EventType::RoomKeyRequest:
            return ::EventType::KeyRequest;
        case EventType::RoomAliases:
            return ::EventType::Aliases;
        case EventType::RoomAvatar:
            return ::EventType::Avatar;
        case EventType::RoomCanonicalAlias:
            return ::EventType::CanonicalAlias;
        case EventType::RoomCreate:
            return ::EventType::Create;
        case EventType::RoomEncrypted:
            return ::EventType::Encrypted;
        case EventType::RoomEncryption:
            return ::EventType::Encryption;
        case EventType::RoomGuestAccess:
            return ::EventType::GuestAccess;
        case EventType::RoomHistoryVisibility:
            return ::EventType::HistoryVisibility;
        case EventType::RoomJoinRules:
            return ::EventType::JoinRules;
        case EventType::RoomMember:
            return ::EventType::Member;
        case EventType::RoomMessage:
            return ::EventType::UnknownMessage;
        case EventType::RoomName:
            return ::EventType::Name;
        case EventType::RoomPowerLevels:
            return ::EventType::PowerLevels;
        case EventType::RoomTopic:
            return ::EventType::Topic;
        case EventType::RoomRedaction:
            return ::EventType::Redaction;
        case EventType::RoomPinnedEvents:
            return ::EventType::PinnedEvents;
        case EventType::Sticker:
            return ::EventType::Sticker;
        case EventType::Tag:
            return ::EventType::Tag;
        case EventType::Unsupported:
        default:
            return ::EventType::Unsupported;
        }
    }
    
    ::EventType::Type toRoomEventType(const Event<mtx::events::msg::Audio> &e) { return ::EventType::AudioMessage; }
    ::EventType::Type toRoomEventType(const Event<mtx::events::msg::Emote> &e) { return ::EventType::EmoteMessage; }
    ::EventType::Type toRoomEventType(const Event<mtx::events::msg::File> &e) { return ::EventType::FileMessage; }
    ::EventType::Type toRoomEventType(const Event<mtx::events::msg::Image> &e) { return ::EventType::ImageMessage; }
    // ::EventType::Type toRoomEventType(const Event<mtx::events::msg::Location> &e) { return
    // ::EventType::LocationMessage; }
    ::EventType::Type toRoomEventType(const Event<mtx::events::msg::Notice> &e) { return ::EventType::NoticeMessage; }
    ::EventType::Type toRoomEventType(const Event<mtx::events::msg::Text> &e) { return ::EventType::TextMessage; }
    ::EventType::Type toRoomEventType(const Event<mtx::events::msg::Video> &e) { return ::EventType::VideoMessage; }
    
    } // namespace
    
    Room::Room(QObject *parent) {
        Q_UNUSED(parent);
        connect(this, &Room::newEvents, this, &Room::addEvents, Qt::QueuedConnection);
        this->moveToThread(QGuiApplication::instance()->thread());
    }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    
    std::string Room::name() {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
        qDebug() << "called name";
    
    Nicolas Werner's avatar
    Nicolas Werner committed
        if (!name_.empty()) {
            return name_;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
        }
        if (!canonical_alias.empty()) {
            qDebug() << "return alias" << QString::fromStdString(canonical_alias);
            return canonical_alias;
        }
        std::string heroes;
        for (const auto &m : members) {
            if (!heroes.empty())
                heroes += ", ";
            std::string hero_name = memberInfos[m].display_name;
            heroes += hero_name.empty() ? m : hero_name;
        }
    
        qDebug() << "return heroes" << QString::fromStdString(heroes);
        return heroes;
    }
    
    
    void Room::addEvents(std::vector<mtx::events::collections::TimelineEvents> events, std::string prev_batch,
                         std::string next_batch) {
    
        if (events.empty())
            return;
    
    
        qDebug() << "p: " << QString::fromStdString(prev_batch);
        qDebug() << "n: " << QString::fromStdString(next_batch);
        qDebug() << "tp: " << QString::fromStdString(this->prev_batch);
        qDebug() << "tn: " << QString::fromStdString(this->next_batch);
    
    
        using namespace mtx::events;
    
        if (this->prev_batch == next_batch) {
            qDebug() << "New events at beginning of timeline";
    
            for (const auto &event : events) {
    
                std::string event_id =
                    boost::apply_visitor([](const auto &e) -> std::string { return eventEventId(e); }, event);
                if (!event_id.empty() && eventIds.count(event_id)) {
    
                    for (int i = 0; i < (int)this->events.size(); i++) {
    
                        if (boost::apply_visitor([](const auto &e) -> std::string { return eventEventId(e); },
                                                 this->events[i]) == event_id) {
    
                            beginRemoveRows(QModelIndex(), i, i);
                            this->events.erase(this->events.begin() + i);
                            endRemoveRows();
                            break;
                        }
                    }
                }
    
            beginInsertRows(QModelIndex(), (int)0, (int)events.size() - 1);
            this->events.insert(this->events.begin(), events.begin(), events.end());
    
            this->prev_batch = prev_batch;
            endInsertRows();
        } else {
            for (const mtx::events::collections::TimelineEvents &e : events) {
    
                this->eventIds.insert(
                    boost::apply_visitor([](const auto &e) -> std::string { return eventEventId(e); }, e));
    
                if (const auto ev = boost::get<StateEvent<state::Aliases>>(&e)) {
                    if (this->canonical_alias.empty() && !ev->content.aliases.empty())
                        this->canonical_alias = ev->content.aliases.front();
                } else if (const auto ev = boost::get<StateEvent<state::Avatar>>(&e)) {
                    this->avatar_url = ev->content.url;
                } else if (const auto ev = boost::get<StateEvent<state::CanonicalAlias>>(&e)) {
                    this->canonical_alias = ev->content.alias;
                } else if (const auto ev = boost::get<StateEvent<state::Create>>(&e)) {
                    this->members.insert(ev->content.creator);
                } else if (const auto ev = boost::get<StateEvent<state::Encryption>>(&e)) {
                } else if (const auto ev = boost::get<StateEvent<state::GuestAccess>>(&e)) {
                } else if (const auto ev = boost::get<StateEvent<state::HistoryVisibility>>(&e)) {
                } else if (const auto ev = boost::get<StateEvent<state::JoinRules>>(&e)) {
                    this->join_rule = ev->content.join_rule;
                } else if (const auto ev = boost::get<StateEvent<state::Member>>(&e)) {
                    switch (ev->content.membership) {
                    case state::Membership::Join:
                    case state::Membership::Invite:
    
                        this->members.insert(ev->state_key);
                        this->memberInfos[ev->state_key] =
                            MemberInfo{ev->content.avatar_url, ev->content.display_name, ev->state_key};
                        break;
                    case state::Membership::Knock:
                        this->memberInfos[ev->state_key] =
                            MemberInfo{ev->content.avatar_url, ev->content.display_name, ev->state_key};
    
                        break;
                    case state::Membership::Ban:
                    case state::Membership::Leave:
    
                        this->members.erase(ev->state_key);
    
                } else if (const auto ev = boost::get<StateEvent<state::Name>>(&e)) {
    
                    this->name_ = ev->content.name;
    
                    emit roomNameChanged(QString::fromStdString(name_));
                } else if (const auto ev = boost::get<StateEvent<state::PinnedEvents>>(&e)) {
    
                } else if (const auto ev = boost::get<StateEvent<state::PowerLevels>>(&e)) {
                } else if (const auto ev = boost::get<StateEvent<state::Topic>>(&e)) {
                    this->topic = ev->content.topic;
                } else if (const auto ev = boost::get<EncryptedEvent<msg::Encrypted>>(&e)) {
                } else if (const auto ev = boost::get<RedactionEvent<msg::Redaction>>(&e)) {
                } else if (const auto ev = boost::get<Sticker>(&e)) {
                } else if (const auto ev = boost::get<RoomEvent<msg::Redacted>>(&e)) {
                } else if (const auto ev = boost::get<RoomEvent<msg::Audio>>(&e)) {
                    this->msgInfo.body = QString::fromStdString(ev->content.body);
                    this->lastMessage = ev->content.body;
                    this->msgInfo.userid = QString::fromStdString(ev->sender);
                } else if (const auto ev = boost::get<RoomEvent<msg::Emote>>(&e)) {
                    this->msgInfo.body = QString::fromStdString(ev->content.body);
                    this->lastMessage = ev->content.body;
                    this->msgInfo.userid = QString::fromStdString(ev->sender);
                } else if (const auto ev = boost::get<RoomEvent<msg::File>>(&e)) {
                    this->msgInfo.body = QString::fromStdString(ev->content.body);
                    this->lastMessage = ev->content.body;
                    this->msgInfo.userid = QString::fromStdString(ev->sender);
                } else if (const auto ev = boost::get<RoomEvent<msg::Image>>(&e)) {
                    this->msgInfo.body = QString::fromStdString(ev->content.body);
                    this->lastMessage = ev->content.body;
                    this->msgInfo.userid = QString::fromStdString(ev->sender);
                } else if (const auto ev = boost::get<RoomEvent<msg::Notice>>(&e)) {
                    this->msgInfo.body = QString::fromStdString(ev->content.body);
                    this->lastMessage = ev->content.body;
                    this->msgInfo.userid = QString::fromStdString(ev->sender);
                }
    
                else if (const auto ev = boost::get<RoomEvent<msg::Text>>(&e)) {
                    this->msgInfo.body = QString::fromStdString(ev->content.body);
                    this->lastMessage = ev->content.body;
                    this->msgInfo.userid = QString::fromStdString(ev->sender);
                } else if (const auto ev = boost::get<RoomEvent<msg::Video>>(&e)) {
                    this->msgInfo.body = QString::fromStdString(ev->content.body);
                    this->lastMessage = ev->content.body;
                    this->msgInfo.userid = QString::fromStdString(ev->sender);
                }
    
    
            qDebug() << "New events at end of timeline";
            beginInsertRows(QModelIndex(), (int)this->events.size(), (int)this->events.size() + events.size() - 1);
            this->events.insert(this->events.end(), events.begin(), events.end());
            this->next_batch = next_batch;
            endInsertRows();
    
        if (this->prev_batch.empty())
            this->prev_batch = prev_batch;
        if (this->next_batch.empty())
            this->next_batch = next_batch;
    }
    
    void Room::fetchOlderMessages() {
        mtx::http::MessagesOpts opts = {};
        opts.from = this->prev_batch;
        opts.limit = 100;
        opts.room_id = this->id;
        http::client().messages(opts, [this](const mtx::responses::Messages &res, mtx::http::RequestErr err) {
            if (err) {
                qDebug() << "failed to retrieve messages: " << *err;
                return;
            }
    
            emit newEvents({res.chunk.rbegin(), res.chunk.rend()}, res.end, res.start);
        });
    
    void Room::sendTextMessage(QString message) {
        using mtx::events::EventType;
    
        mtx::events::msg::Text text;
        text.body = message.trimmed().toStdString();
    
        http::client().send_room_message<msg::Text, EventType::RoomMessage>(
            this->id, http::client().generate_txn_id(), text,
            [this](const mtx::responses::EventId &res, mtx::http::RequestErr err) {
                if (err) {
                    qDebug() << "failed to send message: " << *err;
                    return;
                }
    
                qDebug() << "send event as: $" << QString::fromStdString(res.event_id.to_string());
            });
    }
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    QHash<int, QByteArray> Room::roleNames() const {
        QHash<int, QByteArray> roles;
        roles[Type] = "Type";
        roles[Body] = "Body";
    
        roles[FormattedBody] = "FormattedBody";
    
    Nicolas Werner's avatar
    Nicolas Werner committed
        roles[UserId] = "UserId";
        roles[UserName] = "UserName";
    
    Nicolas Werner's avatar
    Nicolas Werner committed
        roles[Timestamp] = "Timestamp";
    
    Nicolas Werner's avatar
    Nicolas Werner committed
        roles[Url] = "Url";
    
    Nicolas Werner's avatar
    Nicolas Werner committed
        return roles;
    }
    
    int Room::rowCount(const QModelIndex &parent) const { return (int)events.size(); }
    
    QVariant Room::data(const QModelIndex &index, int role) const {
        if (index.row() > (int)events.size() || index.row() < 0)
            return QVariant();
    
        auto event = events.at(index.row());
        switch (role) {
    
        case Type:
    
            return boost::apply_visitor([](const auto &e) -> ::EventType::Type { return toRoomEventType(e); }, event);
    
        case Body:
            return QString::fromStdString(
    
                boost::apply_visitor([](const auto &e) -> std::string { return eventBody(e); }, event));
    
        case FormattedBody:
            return QString::fromStdString(
    
                boost::apply_visitor([](const auto &e) -> std::string { return eventFormattedBody(e); }, event));
    
        case UserId:
            return QString::fromStdString(
    
                boost::apply_visitor([](const auto &e) -> std::string { return eventUserId(e); }, event));
    
        case UserName:
            return QString::fromStdString(boost::apply_visitor(
                [this](const auto &e) -> std::string {
                    try {
                        return this->memberInfos.at(eventUserId(e)).display_name;
                    } catch (...) {
                        return "";
                    }
                },
    
    Nicolas Werner's avatar
    Nicolas Werner committed
        case Timestamp:
            return QDateTime::fromMSecsSinceEpoch(
    
                boost::apply_visitor([](const auto &e) -> uint64_t { return eventTimestamp(e); }, event));
    
    Nicolas Werner's avatar
    Nicolas Werner committed
        case Url:
            return QString::fromStdString(
                boost::apply_visitor([](const auto &e) -> std::string { return eventUrl(e); }, event));
    
    Nicolas Werner's avatar
    Nicolas Werner committed
        default:
            return QVariant();
        }
    }
    
    
    QString Room::userIdToUserName(QString id) {
        try {
            return QString::fromStdString(this->memberInfos.at(id.toStdString()).display_name);
        } catch (...) {
            return "";
        }
    }
    
    // from nheko
    QColor Room::userColor(QString id, QColor background) {
        if (userColors.count(id))
            return userColors.at(id);
    
        auto luminance = [](const QColor &col) -> qreal {
            int colRgb[3] = {col.red(), col.green(), col.blue()};
            qreal lumRgb[3];
    
            for (int i = 0; i < 3; i++) {
                qreal v = colRgb[i] / 255.0;
                v <= 0.03928 ? lumRgb[i] = v / 12.92 : lumRgb[i] = qPow((v + 0.055) / 1.055, 2.4);
            }
    
            auto lum = lumRgb[0] * 0.2126 + lumRgb[1] * 0.7152 + lumRgb[2] * 0.0722;
    
            return lum;
        };
    
        auto computeContrast = [](const qreal &one, const qreal &two) -> qreal {
            auto ratio = (one + 0.05) / (two + 0.05);
    
            if (two > one) {
                ratio = 1 / ratio;
            }
    
            return ratio;
        };
    
        auto hashQString = [](const QString &input) {
            unsigned hash = 0;
    
            for (int i = 0; i < input.length(); i++) {
                hash = input.at(i).digitValue() + ((hash << 5) - hash);
            }
    
            return (int)hash;
        };
    
        const qreal backgroundLum = luminance(background);
    
        // Create a color for the input
        auto hash = hashQString(id);
        // create a hue value based on the hash of the input.
        auto userHue = qAbs(hash % 360);
        // start with moderate saturation and lightness values.
        auto sat = 220;
        auto lightness = 125;
    
        // converting to a QColor makes the luminance calc easier.
        QColor inputColor = QColor::fromHsl(userHue, sat, lightness);
    
        // calculate the initial luminance and contrast of the
        // generated color.  It's possible that no additional
        // work will be necessary.
        auto lum = luminance(inputColor);
        auto contrast = computeContrast(lum, backgroundLum);
    
        // If the contrast doesn't meet our criteria,
        // try again and again until they do by modifying first
        // the lightness and then the saturation of the color.
        while (contrast < 5) {
            // if our lightness is at it's bounds, try changing
            // saturation instead.
            if (lightness == 242 || lightness == 13) {
                qreal newSat = qBound(26.0, sat * 1.25, 242.0);
    
                inputColor.setHsl(userHue, qFloor(newSat), lightness);
                auto tmpLum = luminance(inputColor);
                auto higherContrast = computeContrast(tmpLum, backgroundLum);
                if (higherContrast > contrast) {
                    contrast = higherContrast;
                    sat = newSat;
                } else {
                    newSat = qBound(26.0, sat / 1.25, 242.0);
                    inputColor.setHsl(userHue, qFloor(newSat), lightness);
                    tmpLum = luminance(inputColor);
                    auto lowerContrast = computeContrast(tmpLum, backgroundLum);
                    if (lowerContrast > contrast) {
                        contrast = lowerContrast;
                        sat = newSat;
                    }
                }
            } else {
                qreal newLightness = qBound(13.0, lightness * 1.25, 242.0);
    
                inputColor.setHsl(userHue, sat, qFloor(newLightness));
    
                auto tmpLum = luminance(inputColor);
                auto higherContrast = computeContrast(tmpLum, backgroundLum);
    
                // Check to make sure we have actually improved contrast
                if (higherContrast > contrast) {
                    contrast = higherContrast;
                    lightness = newLightness;
                    // otherwise, try going the other way instead.
                } else {
                    newLightness = qBound(13.0, lightness / 1.25, 242.0);
                    inputColor.setHsl(userHue, sat, qFloor(newLightness));
                    tmpLum = luminance(inputColor);
                    auto lowerContrast = computeContrast(tmpLum, backgroundLum);
                    if (lowerContrast > contrast) {
                        contrast = lowerContrast;
                        lightness = newLightness;
                    }
                }
            }
        }
    
        userColors[id] = inputColor;
    
        return inputColor;
    }