-
Nicolas Werner authoredNicolas Werner authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
roommodel.cpp 17.12 KiB
#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 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::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";
int i = 0;
for (const auto &event : this->events) {
if (boost::apply_visitor([](const auto &e) -> uint64_t { return eventTimestamp(e); }, event) != 0)
break;
i++;
}
beginInsertRows(QModelIndex(), (int)i, (int)events.size() + i - 1);
this->events.insert(this->events.begin() + i, events.begin(), events.end());
this->prev_batch = prev_batch;
endInsertRows();
} else {
for (const mtx::events::collections::TimelineEvents &e : events) {
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->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))
this->name_ = ev->content.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);
});
}
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);
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 "";
}
},
event));
case Timestamp:
return QDateTime::fromMSecsSinceEpoch(
boost::apply_visitor([](const auto &e) -> uint64_t { return eventTimestamp(e); }, event));
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;
}