Newer
Older
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
Konstantinos Sideris
committed
#include "Utils.h"
#include <QApplication>
#include <QCryptographicHash>
#include <QGuiApplication>
#include <QProcessEnvironment>
#include <QSettings>
#include <QTextDocument>
#include <QXmlStreamReader>
Konstantinos Sideris
committed
#include "Cache.h"
#include "EventAccessors.h"
#include "MatrixClient.h"
Konstantinos Sideris
committed
using TimelineEvent = mtx::events::collections::TimelineEvents;
template<class T, class Event>
static DescInfo
createDescriptionInfo(const Event &event, const QString &localUser, const QString &displayName)
const auto msg = std::get<T>(event);
const auto sender = QString::fromStdString(msg.sender);
const auto username = displayName;
const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts);
auto body = utils::event_body(event).trimmed();
if (mtx::accessors::relations(event).reply_to())
body = QString::fromStdString(utils::stripReplyFromBody(body.toStdString()));
return DescInfo{QString::fromStdString(msg.event_id),
sender,
utils::messageDescription<T>(username, body, sender == localUser),
utils::descriptiveTime(ts),
msg.origin_server_ts,
ts};
std::string
utils::stripReplyFromBody(const std::string &bodyi)
{
QString body = QString::fromStdString(bodyi);
if (body.startsWith(QLatin1String("> <"))) {
auto segments = body.split('\n');
while (!segments.isEmpty() && segments.begin()->startsWith('>'))
segments.erase(segments.begin());
if (!segments.empty() && segments.first().isEmpty())
segments.erase(segments.begin());
body = segments.join('\n');
}
body.replace(QLatin1String("@room"), QString::fromUtf8("@\u2060room"));
}
std::string
utils::stripReplyFromFormattedBody(const std::string &formatted_bodyi)
{
QString formatted_body = QString::fromStdString(formatted_bodyi);
formatted_body.remove(QRegularExpression(QStringLiteral("<mx-reply>.*</mx-reply>"),
QRegularExpression::DotMatchesEverythingOption));
formatted_body.replace(QLatin1String("@room"), QString::fromUtf8("@\u2060room"));
}
RelatedInfo
utils::stripReplyFallbacks(const TimelineEvent &event, std::string id, QString room_id_)
{
RelatedInfo related = {};
related.quoted_user = QString::fromStdString(mtx::accessors::sender(event));
related.related_event = std::move(id);
related.type = mtx::accessors::msg_type(event);
// get body, strip reply fallback, then transform the event to text, if it is a media event
// etc
related.quoted_body = QString::fromStdString(mtx::accessors::body(event));
related.quoted_body =
QString::fromStdString(stripReplyFromBody(related.quoted_body.toStdString()));
related.quoted_body = utils::getQuoteBody(related);
// get quoted body and strip reply fallback
related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event);
related.quoted_formatted_body = QString::fromStdString(
stripReplyFromFormattedBody(related.quoted_formatted_body.toStdString()));
related.room = room_id_;
QString
utils::localUser()
{
return QString::fromStdString(http::client()->user_id().to_string());
}
bool
utils::codepointIsEmoji(uint code)
{
// TODO: Be more precise here.
return (code >= 0x2600 && code <= 0x27bf) || (code >= 0x2b00 && code <= 0x2bff) ||
(code >= 0x1f000 && code <= 0x1faff) || code == 0x200d || code == 0xfe0f;
}
QString
utils::replaceEmoji(const QString &body)
{
QString fmtBody;
fmtBody.reserve(body.size());
QVector<uint> utf32_string = body.toUcs4();
bool insideFontBlock = false;
for (auto &code : utf32_string) {
if (utils::codepointIsEmoji(code)) {
if (!insideFontBlock) {
fmtBody += QStringLiteral("<font face=\"") % UserSettings::instance()->emojiFont() %
QStringLiteral("\">");
insideFontBlock = true;
} else if (code == 0xfe0f) {
// BUG(Nico):
// Workaround https://bugreports.qt.io/browse/QTBUG-97401
// See also https://github.com/matrix-org/matrix-react-sdk/pull/1458/files
// Nheko bug: https://github.com/Nheko-Reborn/nheko/issues/439
continue;
}
} else {
if (insideFontBlock) {
fmtBody += QStringLiteral("</font>");
if (QChar::requiresSurrogates(code)) {
QChar emoji[] = {static_cast<ushort>(QChar::highSurrogate(code)),
static_cast<ushort>(QChar::lowSurrogate(code))};
fmtBody.append(emoji, 2);
} else {
fmtBody.append(QChar(static_cast<ushort>(code)));
}
}
if (insideFontBlock) {
fmtBody += QStringLiteral("</font>");
}
void
utils::setScaleFactor(float factor)
{
if (factor < 1 || factor > 3)
return;
settings.setValue(QStringLiteral("settings/scale_factor"), factor);
}
float
utils::scaleFactor()
{
return settings.value(QStringLiteral("settings/scale_factor"), -1).toFloat();
Konstantinos Sideris
committed
QString
utils::descriptiveTime(const QDateTime &then)
{
const auto now = QDateTime::currentDateTime();
const auto days = then.daysTo(now);
Konstantinos Sideris
committed
if (days == 0)
return QLocale::system().toString(then.time(), QLocale::ShortFormat);
else if (days < 2)
return QString(QCoreApplication::translate("descriptiveTime", "Yesterday"));
else if (days < 7)
return then.toString(QStringLiteral("dddd"));
Konstantinos Sideris
committed
return QLocale::system().toString(then.date(), QLocale::ShortFormat);
Konstantinos Sideris
committed
}
DescInfo
utils::getMessageDescription(const TimelineEvent &event,
const QString &localUser,
Konstantinos Sideris
committed
{
using Audio = mtx::events::RoomEvent<mtx::events::msg::Audio>;
using Emote = mtx::events::RoomEvent<mtx::events::msg::Emote>;
using File = mtx::events::RoomEvent<mtx::events::msg::File>;
using Image = mtx::events::RoomEvent<mtx::events::msg::Image>;
using Notice = mtx::events::RoomEvent<mtx::events::msg::Notice>;
using Text = mtx::events::RoomEvent<mtx::events::msg::Text>;
using Video = mtx::events::RoomEvent<mtx::events::msg::Video>;
using CallInvite = mtx::events::RoomEvent<mtx::events::voip::CallInvite>;
using CallAnswer = mtx::events::RoomEvent<mtx::events::voip::CallAnswer>;
using CallHangUp = mtx::events::RoomEvent<mtx::events::voip::CallHangUp>;
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>;
if (std::holds_alternative<Audio>(event)) {
return createDescriptionInfo<Audio>(event, localUser, displayName);
} else if (std::holds_alternative<Emote>(event)) {
return createDescriptionInfo<Emote>(event, localUser, displayName);
} else if (std::holds_alternative<File>(event)) {
return createDescriptionInfo<File>(event, localUser, displayName);
} else if (std::holds_alternative<Image>(event)) {
return createDescriptionInfo<Image>(event, localUser, displayName);
} else if (std::holds_alternative<Notice>(event)) {
return createDescriptionInfo<Notice>(event, localUser, displayName);
} else if (std::holds_alternative<Text>(event)) {
return createDescriptionInfo<Text>(event, localUser, displayName);
} else if (std::holds_alternative<Video>(event)) {
return createDescriptionInfo<Video>(event, localUser, displayName);
} else if (std::holds_alternative<CallInvite>(event)) {
return createDescriptionInfo<CallInvite>(event, localUser, displayName);
} else if (std::holds_alternative<CallAnswer>(event)) {
return createDescriptionInfo<CallAnswer>(event, localUser, displayName);
} else if (std::holds_alternative<CallHangUp>(event)) {
return createDescriptionInfo<CallHangUp>(event, localUser, displayName);
} else if (std::holds_alternative<mtx::events::Sticker>(event)) {
return createDescriptionInfo<mtx::events::Sticker>(event, localUser, displayName);
} else if (auto msg = std::get_if<Encrypted>(&event); msg != nullptr) {
const auto sender = QString::fromStdString(msg->sender);
const auto username = displayName;
const auto ts = QDateTime::fromMSecsSinceEpoch(msg->origin_server_ts);
DescInfo info;
info.userid = sender;
info.body = QStringLiteral(" %1").arg(
messageDescription<Encrypted>(username, QLatin1String(""), sender == localUser));
info.timestamp = msg->origin_server_ts;
info.descriptiveTime = utils::descriptiveTime(ts);
info.event_id = QString::fromStdString(msg->event_id);
info.datetime = ts;
return info;
}
Konstantinos Sideris
committed
Konstantinos Sideris
committed
}
QString
utils::firstChar(const QString &input)
{
for (auto const &c : input.toStdU32String()) {
if (QString::fromUcs4(&c, 1) != QStringLiteral("#"))
return QString::fromUcs4(&c, 1).toUpper();
}
return QString::fromUcs4(&input.toUcs4().at(0), 1).toUpper();
constexpr static const char *units[] = {"B", "KiB", "MiB", "GiB", "TiB"};
constexpr static const int length = sizeof(units) / sizeof(units[0]);
int u = 0;
double size = static_cast<double>(bytes);
while (size >= 1024.0 && u < length) {
++u;
size /= 1024.0;
}
return QString::number(size, 'g', 4) + ' ' + units[u];
int
utils::levenshtein_distance(const std::string &s1, const std::string &s2)
{
const auto nlen = s1.size();
const auto hlen = s2.size();
if (hlen == 0)
return -1;
if (nlen == 1)
return (int)s2.find(s1);
for (size_t i = 0; i < nlen; ++i) {
std::vector<int> row2(1, (int)i + 1);
for (size_t j = 0; j < hlen; ++j) {
const int cost = s1[i] != s2[j];
row2.push_back(std::min(row1[j + 1] + 1, std::min(row2[j] + 1, row1[j] + cost)));
row1.swap(row2);
}
return *std::min_element(row1.begin(), row1.end());
QString
utils::event_body(const mtx::events::collections::TimelineEvents &e)
using namespace mtx::events;
if (auto ev = std::get_if<RoomEvent<msg::Audio>>(&e); ev != nullptr)
return QString::fromStdString(ev->content.body);
if (auto ev = std::get_if<RoomEvent<msg::Emote>>(&e); ev != nullptr)
return QString::fromStdString(ev->content.body);
if (auto ev = std::get_if<RoomEvent<msg::File>>(&e); ev != nullptr)
return QString::fromStdString(ev->content.body);
if (auto ev = std::get_if<RoomEvent<msg::Image>>(&e); ev != nullptr)
return QString::fromStdString(ev->content.body);
if (auto ev = std::get_if<RoomEvent<msg::Notice>>(&e); ev != nullptr)
return QString::fromStdString(ev->content.body);
if (auto ev = std::get_if<RoomEvent<msg::Text>>(&e); ev != nullptr)
return QString::fromStdString(ev->content.body);
if (auto ev = std::get_if<RoomEvent<msg::Video>>(&e); ev != nullptr)
return QString::fromStdString(ev->content.body);
QPixmap
utils::scaleImageToPixmap(const QImage &img, int size)
{
// Deprecated in 5.13: const double sz =
// std::ceil(QApplication::desktop()->screen()->devicePixelRatioF() * (double)size);
const double sz =
std::ceil(QGuiApplication::primaryScreen()->devicePixelRatio() * (double)size);
return QPixmap::fromImage(img.scaled(sz, sz, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
QPixmap
utils::scaleDown(uint64_t maxWidth, uint64_t maxHeight, const QPixmap &source)
{
if (source.isNull())
return QPixmap();
const double widthRatio = (double)maxWidth / (double)source.width();
const double heightRatio = (double)maxHeight / (double)source.height();
const double minAspectRatio = std::min(widthRatio, heightRatio);
// Size of the output image.
int w, h = 0;
if (minAspectRatio > 1) {
w = source.width();
h = source.height();
} else {
w = source.width() * minAspectRatio;
h = source.height() * minAspectRatio;
}
return source.scaled(w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
QString
utils::mxcToHttp(const QUrl &url, const QString &server, int port)
{
auto mxcParts = mtx::client::utils::parse_mxc_url(url.toString().toStdString());
return QStringLiteral("https://%1:%2/_matrix/media/r0/download/%3/%4")
.arg(QString::fromStdString(mxcParts.server), QString::fromStdString(mxcParts.media_id));
QString
utils::humanReadableFingerprint(const std::string &ed25519)
{
return humanReadableFingerprint(QString::fromStdString(ed25519));
}
QString
utils::humanReadableFingerprint(const QString &ed25519)
{
QString fingerprint;
for (int i = 0; i < ed25519.length(); i = i + 4) {
fingerprint.append('\n');
else if (i < ed25519.length())
fingerprint.append(' ');
}
return fingerprint;
QString
utils::linkifyMessage(const QString &body)
{
// Convert to valid XML.
auto doc = body;
doc.replace(conf::strings::url_regex, conf::strings::url_html);
doc.replace(
QRegularExpression(QStringLiteral("\\b(?<![\"'])(?>(matrix:[\\S]{5,}))(?![\"'])\\b")),
conf::strings::url_html);
QString
utils::escapeBlacklistedHtml(const QString &rawStr)
Nicolas Werner
committed
static const std::set<QByteArray> allowedTags = {
"font", "/font", "del", "/del", "h1", "/h1", "h2", "/h2",
"h3", "/h3", "h4", "/h4", "h5", "/h5", "h6", "/h6",
"blockquote", "/blockquote", "p", "/p", "a", "/a", "ul", "/ul",
"ol", "/ol", "sup", "/sup", "sub", "/sub", "li", "/li",
"b", "/b", "i", "/i", "u", "/u", "strong", "/strong",
"em", "/em", "strike", "/strike", "code", "/code", "hr", "/hr",
"br", "br/", "div", "/div", "table", "/table", "thead", "/thead",
"tbody", "/tbody", "tr", "/tr", "th", "/th", "td", "/td",
"caption", "/caption", "pre", "/pre", "span", "/span", "img", "/img",
"details", "/details", "summary", "/summary"};
Nicolas Werner
committed
constexpr static const std::array tagNameEnds = {' ', '>'};
constexpr static const std::array attrNameEnds = {' ', '>', '=', '\t', '\r', '\n', '/', '\f'};
constexpr static const std::array spaceChars = {' ', '\t', '\r', '\n', '\f'};
QByteArray data = rawStr.toUtf8();
QByteArray buffer;
const int length = data.size();
buffer.reserve(length);
Nicolas Werner
committed
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
const auto end = data.cend();
for (auto pos = data.cbegin(); pos < end;) {
auto tagStart = std::find(pos, end, '<');
buffer.append(pos, tagStart - pos);
if (tagStart == end)
break;
const auto tagNameStart = tagStart + 1;
const auto tagNameEnd =
std::find_first_of(tagNameStart, end, tagNameEnds.begin(), tagNameEnds.end());
if (allowedTags.find(QByteArray(tagNameStart, tagNameEnd - tagNameStart).toLower()) ==
allowedTags.end()) {
// not allowed -> escape
buffer.append("<");
pos = tagNameStart;
continue;
} else {
buffer.append(tagStart, tagNameEnd - tagStart);
pos = tagNameEnd;
if (tagNameEnd != end) {
auto attrStart = tagNameEnd;
auto attrsEnd = std::find(attrStart, end, '>');
if (*(attrsEnd - 1) == '/')
attrsEnd -= 1;
pos = attrsEnd;
auto consumeSpaces = [attrsEnd](auto p) {
while (p < attrsEnd &&
std::find(spaceChars.begin(), spaceChars.end(), *p) != spaceChars.end())
p++;
return p;
};
attrStart = consumeSpaces(attrStart);
while (attrStart < attrsEnd) {
auto attrEnd = std::find_first_of(
attrStart, attrsEnd, attrNameEnds.begin(), attrNameEnds.end());
auto attrName = QByteArray(attrStart, attrEnd - attrStart).toLower();
auto sanitizeValue = [&attrName](QByteArray val) {
if (attrName == QByteArrayLiteral("src") && !val.startsWith("mxc://"))
return QByteArray();
else
return val;
};
attrStart = consumeSpaces(attrEnd);
if (attrStart < attrsEnd) {
if (*attrStart == '=') {
attrStart = consumeSpaces(attrStart + 1);
if (attrStart < attrsEnd) {
// we fall through here if the value is empty to transform attr=""
// into attr, because otherwise we can't style it
if (*attrStart == '"') {
attrStart += 1;
auto valueEnd = std::find(attrStart, attrsEnd, '"');
if (valueEnd == attrsEnd)
break;
auto val =
sanitizeValue(QByteArray(attrStart, valueEnd - attrStart));
attrStart = consumeSpaces(valueEnd + 1);
if (!val.isEmpty()) {
buffer.append(' ');
buffer.append(attrName);
buffer.append("=\"");
buffer.append(val);
buffer.append('"');
continue;
}
} else if (*attrStart == '\'') {
attrStart += 1;
auto valueEnd = std::find(attrStart, attrsEnd, '\'');
if (valueEnd == attrsEnd)
break;
auto val =
sanitizeValue(QByteArray(attrStart, valueEnd - attrStart));
attrStart = consumeSpaces(valueEnd + 1);
if (!val.isEmpty()) {
buffer.append(' ');
buffer.append(attrName);
buffer.append("=\'");
buffer.append(val);
buffer.append('\'');
continue;
}
} else {
attrStart += 1;
auto valueEnd = std::find_first_of(attrStart,
attrsEnd,
attrNameEnds.begin(),
attrNameEnds.end());
buffer.append(' ');
buffer.append(attrName);
buffer.append("=");
buffer.append(attrStart, valueEnd - attrStart);
attrStart = valueEnd;
continue;
}
}
}
}
buffer.append(' ');
buffer.append(attrName);
Nicolas Werner
committed
utils::markdownToHtml(const QString &text, bool rainbowify)
const auto str = text.toUtf8();
cmark_node *const node = cmark_parse_document(str.constData(), str.size(), CMARK_OPT_UNSAFE);
if (rainbowify) {
// create iterator over node
cmark_iter *iter = cmark_iter_new(node);
// First loop to get total text length
int textLen = 0;
while (cmark_iter_next(iter) != CMARK_EVENT_DONE) {
cmark_node *cur = cmark_iter_get_node(iter);
// only text nodes (no code or semilar)
if (cmark_node_get_type(cur) != CMARK_NODE_TEXT)
continue;
// count up by length of current node's text
QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme,
QString(cmark_node_get_literal(cur)));
while (tbf.toNextBoundary() != -1)
textLen++;
}
// create new iter to start over
cmark_iter_free(iter);
iter = cmark_iter_new(node);
// Second loop to rainbowify
int charIdx = 0;
while (cmark_iter_next(iter) != CMARK_EVENT_DONE) {
cmark_node *cur = cmark_iter_get_node(iter);
// only text nodes (no code or semilar)
if (cmark_node_get_type(cur) != CMARK_NODE_TEXT)
continue;
// get text in current node
QString nodeText(cmark_node_get_literal(cur));
// create buffer to append rainbow text to
QString buf;
int boundaryStart = 0;
int boundaryEnd = 0;
// use QTextBoundaryFinder to iterate ofer graphemes
QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme, nodeText);
while ((boundaryEnd = tbf.toNextBoundary()) != -1) {
charIdx++;
// Split text to get current char
auto curChar =
QStringView(nodeText).mid(boundaryStart, boundaryEnd - boundaryStart);
boundaryStart = boundaryEnd;
// Don't rainbowify whitespaces
if (curChar.trimmed().isEmpty() || codepointIsEmoji(curChar.toUcs4().at(0))) {
// get correct color for char index
// Use colors as described here:
// https://shark.comfsm.fm/~dleeling/cis/hsl_rainbow.html
auto color = QColor::fromHslF((charIdx - 1.0) / textLen * (5. / 6.), 0.9, 0.5);
// format color for HTML
auto colorString = color.name(QColor::NameFormat::HexRgb);
// create HTML element for current char
auto curCharColored =
QStringLiteral("<font color=\"%0\">%1</font>").arg(colorString).arg(curChar);
// append colored HTML element to buffer
buf.append(curCharColored);
}
// create HTML_INLINE node to prevent HTML from being escaped
auto htmlNode = cmark_node_new(CMARK_NODE_HTML_INLINE);
// set content of HTML node to buffer contents
cmark_node_set_literal(htmlNode, buf.toUtf8().data());
// replace current node with HTML node
cmark_node_replace(cur, htmlNode);
// free memory of old node
cmark_node_free(cur);
const char *tmp_buf = cmark_render_html(node, CMARK_OPT_UNSAFE);
// Copy the null terminated output buffer.
std::string html(tmp_buf);
// The buffer is no longer needed.
free((char *)tmp_buf);
auto result = linkifyMessage(escapeBlacklistedHtml(QString::fromStdString(html))).trimmed();
if (result.count(QStringLiteral("<p>")) == 1 && result.startsWith(QLatin1String("<p>")) &&
result.endsWith(QLatin1String("</p>"))) {
result = result.mid(3, result.size() - 3 - 4);
}
utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html)
auto getFormattedBody = [related]() -> QString {
using MsgType = mtx::events::MessageType;
switch (related.type) {
case MsgType::File: {
return QStringLiteral("sent a file.");
}
case MsgType::Image: {
return QStringLiteral("sent an image.");
}
case MsgType::Audio: {
return QStringLiteral("sent an audio file.");
}
case MsgType::Video: {
return QStringLiteral("sent a video");
};
return QString("<mx-reply><blockquote><a "
"href=\"https://matrix.to/#/%1/%2\">In reply "
"to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br"
"/>%5</blockquote></mx-reply>")
.arg(related.room,
QString::fromStdString(related.related_event),
related.quoted_user,
related.quoted_user,
getFormattedBody()) +
html;
}
QString
utils::getQuoteBody(const RelatedInfo &related)
{
using MsgType = mtx::events::MessageType;
switch (related.type) {
case MsgType::File: {
return QStringLiteral("sent a file.");
return QStringLiteral("sent an image.");
return QStringLiteral("sent an audio file.");
return QStringLiteral("sent a video");
}
default: {
return related.quoted_body;
}
}
QString
utils::linkColor()
{
const auto theme = UserSettings::instance()->theme();
if (theme == QLatin1String("light")) {
return QStringLiteral("#0077b5");
} else if (theme == QLatin1String("dark")) {
return QStringLiteral("#38A3D8");
} else {
return QPalette().color(QPalette::Link).name();
}
utils::hashQString(const QString &input)
auto h = QCryptographicHash::hash(input.toUtf8(), QCryptographicHash::Sha1);
return (static_cast<uint32_t>(h[0]) << 24) ^ (static_cast<uint32_t>(h[1]) << 16) ^
(static_cast<uint32_t>(h[2]) << 8) ^ static_cast<uint32_t>(h[3]);
QColor
utils::generateContrastingHexColor(const QString &input, const QColor &backgroundCol)
const qreal backgroundLum = luminance(backgroundCol);
// Create a color for the input
auto hash = hashQString(input);
// create a hue value based on the hash of the input.
// Adapted to make Nico blue
auto userHue =
static_cast<int>(static_cast<double>(hash - static_cast<uint32_t>(0x60'00'00'00)) /
std::numeric_limits<uint32_t>::max() * 360.);
// start with moderate saturation and lightness values.
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.
int iterationCount = 9;
// 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;
}
}
// don't loop forever, just give up at some point!
// Someone smart may find a better solution
if (--iterationCount < 0)
break;
}
// get the hex value of the generated color.
auto colorHex = inputColor.name();
return colorHex;
}
qreal
utils::computeContrast(const qreal &one, const qreal &two)
{
auto ratio = (one + 0.05) / (two + 0.05);
if (two > one) {
ratio = 1 / ratio;
}
}
qreal
utils::luminance(const QColor &col)
{
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;
lumRgb[i] = v <= 0.03928 ? v / 12.92 : qPow((v + 0.055) / 1.055, 2.4);
}
auto lum = lumRgb[0] * 0.2126 + lumRgb[1] * 0.7152 + lumRgb[2] * 0.0722;
utils::centerWidget(QWidget *widget, QWindow *parent)
widget->window()->windowHandle()->setTransientParent(parent);
auto findCenter = [childRect = widget->rect()](QRect hostRect) -> QPoint {
return QPoint(hostRect.center().x() - (childRect.width() * 0.5),
hostRect.center().y() - (childRect.height() * 0.5));
};
widget->move(findCenter(QGuiApplication::primaryScreen()->geometry()));
void
utils::restoreCombobox(QComboBox *combo, const QString &value)
{
for (auto i = 0; i < combo->count(); ++i) {
if (value == combo->itemText(i)) {
combo->setCurrentIndex(i);
break;
QImage
utils::readImageFromFile(const QString &filename)
{
QImageReader reader(filename);
reader.setAutoTransform(true);
return reader.read();
utils::readImage(const QByteArray &data)
QBuffer buf;
buf.setData(data);
QImageReader reader(&buf);
reader.setAutoTransform(true);
return reader.read();
bool
utils::isReply(const mtx::events::collections::TimelineEvents &e)
{
return mtx::accessors::relations(e).reply_to().has_value();
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
void
utils::removeDirectFromRoom(QString roomid)
{
http::client()->get_account_data<mtx::events::account_data::Direct>(
[roomid](mtx::events::account_data::Direct ev, mtx::http::RequestErr e) {
if (e && e->status_code == 404)
ev = {};
else if (e) {
nhlog::net()->error("Failed to retrieve m.direct: {}", *e);
return;
}
auto r = roomid.toStdString();
for (auto it = ev.user_to_rooms.begin(); it != ev.user_to_rooms.end();) {
for (auto rit = it->second.begin(); rit != it->second.end();) {
if (r == *rit)
rit = it->second.erase(rit);
else
++rit;
}
if (it->second.empty())
it = ev.user_to_rooms.erase(it);
else
++it;
}
http::client()->put_account_data(ev, [r](mtx::http::RequestErr e) {
if (e)
nhlog::net()->error("Failed to update m.direct: {}", *e);
});
});
}
void
utils::markRoomAsDirect(QString roomid, std::vector<RoomMember> members)
{
http::client()->get_account_data<mtx::events::account_data::Direct>(
[roomid, members](mtx::events::account_data::Direct ev, mtx::http::RequestErr e) {
if (e && e->status_code == 404)
ev = {};
else if (e) {
nhlog::net()->error("Failed to retrieve m.direct: {}", *e);
return;
}
auto local = utils::localUser();
auto r = roomid.toStdString();
for (const auto &m : members) {
if (m.user_id != local) {
ev.user_to_rooms[m.user_id.toStdString()].push_back(r);
}
}
http::client()->put_account_data(ev, [r](mtx::http::RequestErr e) {
if (e)
nhlog::net()->error("Failed to update m.direct: {}", *e);
});
});
}
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
std::vector<std::string>
utils::roomVias(const std::string &roomid)
{
std::vector<std::string> vias;
{
auto members = cache::getMembers(roomid, 0, 100);
if (!members.empty()) {
vias.push_back(http::client()->user_id().hostname());
for (const auto &m : members) {
if (vias.size() >= 4)
break;
auto user_id =
mtx::identifiers::parse<mtx::identifiers::User>(m.user_id.toStdString());
auto server = user_id.hostname();
if (std::find(begin(vias), end(vias), server) == vias.end())
vias.push_back(server);
}
}
}
if (vias.empty()) {
auto members = cache::getMembersFromInvite(roomid, 0, 100);
if (!members.empty()) {
vias.push_back(http::client()->user_id().hostname());
for (const auto &m : members) {