Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
roommodel.cpp 15.36 KiB
#include "roommodel.h"

#include <QGuiApplication>

#include <QMetaType>
#include <QSharedPointer>
#include <QtMath>

#include <QDebug>

Q_DECLARE_METATYPE(std::vector<mtx::events::collections::TimelineEvent>);
Q_DECLARE_METATYPE(QSharedPointer<Room>);

using mtx::events::Event;
namespace {
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;
}
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;
    } else
        return e.content.body;
}

template <class T> auto eventUserId(const mtx::events::Event<T> &e) -> std::string { return ""; }
template <class T> auto eventUserId(const mtx::events::RoomEvent<T> &e) -> std::string { return e.sender; }
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>::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());
}

std::string Room::name() {
    qDebug() << "called name";
    if (!name_.empty()) {
        return name_;
    }
    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::TimelineEvent> events) {
    if (events.empty())
        return;

    using namespace mtx::events;
    for (const mtx::events::collections::TimelineEvent &e : events) {
        if (const auto ev = boost::get<StateEvent<state::Aliases>>(&e.data)) {
            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.data)) {
            this->avatar_url = ev->content.url;
        } else if (const auto ev = boost::get<StateEvent<state::CanonicalAlias>>(&e.data)) {
            this->canonical_alias = ev->content.alias;
        } else if (const auto ev = boost::get<StateEvent<state::Create>>(&e.data)) {
            this->members.insert(ev->content.creator);
        } else if (const auto ev = boost::get<StateEvent<state::Encryption>>(&e.data)) {
        } else if (const auto ev = boost::get<StateEvent<state::GuestAccess>>(&e.data)) {
        } else if (const auto ev = boost::get<StateEvent<state::HistoryVisibility>>(&e.data)) {
        } else if (const auto ev = boost::get<StateEvent<state::JoinRules>>(&e.data)) {
            this->join_rule = ev->content.join_rule;
        } else if (const auto ev = boost::get<StateEvent<state::Member>>(&e.data)) {
            switch (ev->content.membership) {
            case state::Membership::Join:
            case state::Membership::Invite:
                this->members.insert(ev->sender);
                this->memberInfos[ev->sender] =
                    MemberInfo{ev->content.avatar_url, ev->content.display_name, ev->sender};
                break;
            case state::Membership::Ban:
            case state::Membership::Leave:
                this->members.erase(ev->sender);
                this->memberInfos.erase(ev->sender);
                break;
            }
        } else if (const auto ev = boost::get<StateEvent<state::Name>>(&e.data))
            this->name_ = ev->content.name;
        else if (const auto ev = boost::get<StateEvent<state::PinnedEvents>>(&e.data)) {
        } else if (const auto ev = boost::get<StateEvent<state::PowerLevels>>(&e.data)) {
        } else if (const auto ev = boost::get<StateEvent<state::Topic>>(&e.data)) {
            this->topic = ev->content.topic;
        } else if (const auto ev = boost::get<EncryptedEvent<msg::Encrypted>>(&e.data)) {
        } else if (const auto ev = boost::get<RedactionEvent<msg::Redaction>>(&e.data)) {
        } else if (const auto ev = boost::get<Sticker>(&e.data)) {
        } else if (const auto ev = boost::get<RoomEvent<msg::Redacted>>(&e.data)) {
        } else if (const auto ev = boost::get<RoomEvent<msg::Audio>>(&e.data)) {
            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.data)) {
            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.data)) {
            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.data)) {
            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.data)) {
            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.data)) {
            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.data)) {
            this->msgInfo.body = QString::fromStdString(ev->content.body);
            this->lastMessage = ev->content.body;
            this->msgInfo.userid = QString::fromStdString(ev->sender);
        }
    }

    beginInsertRows(QModelIndex(), (int)this->events.size(), (int)this->events.size() + events.size() - 1);
    this->events.insert(this->events.end(), events.begin(), events.end());
    endInsertRows();
}

QHash<int, QByteArray> Room::roleNames() const {
    QHash<int, QByteArray> roles;
    roles[Type] = "Type";
    roles[Body] = "Body";
    roles[FormattedBody] = "FormattedBody";
    roles[UserId] = "UserId";
    roles[UserName] = "UserName";
    roles[Timestamp] = "Timestamp";
    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.data);
    case Body:
        return QString::fromStdString(
            boost::apply_visitor([](const auto &e) -> std::string { return eventBody(e); }, event.data));
    case FormattedBody:
        return QString::fromStdString(
            boost::apply_visitor([](const auto &e) -> std::string { return eventFormattedBody(e); }, event.data));
    case UserId:
        return QString::fromStdString(
            boost::apply_visitor([](const auto &e) -> std::string { return eventUserId(e); }, event.data));
    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 "";
                }
            },
            event.data));
    case Timestamp:
        return QDateTime::fromMSecsSinceEpoch(
            boost::apply_visitor([](const auto &e) -> uint64_t { return eventTimestamp(e); }, event.data));
    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;
}