Newer
Older
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
Konstantinos Sideris
committed
#include "Utils.h"
#include <QApplication>
#include <QDesktopWidget>
#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);
QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption);
while (body.startsWith(">"))
body.remove(plainQuote);
if (body.startsWith("\n"))
body.remove(0, 1);
body.replace("@room", QString::fromUtf8("@\u2060room"));
return body.toStdString();
}
std::string
utils::stripReplyFromFormattedBody(const std::string &formatted_bodyi)
{
QString formatted_body = QString::fromStdString(formatted_bodyi);
formatted_body.remove(QRegularExpression("<mx-reply>.*</mx-reply>",
QRegularExpression::DotMatchesEverythingOption));
formatted_body.replace("@room", QString::fromUtf8("@\u2060room"));
return formatted_body.toStdString();
}
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 (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;
QSettings settings;
settings.setValue("settings/scale_factor", factor);
}
float
utils::scaleFactor()
{
QSettings settings;
return settings.value("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("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
{
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
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
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::msg::CallInvite>;
using CallAnswer = mtx::events::RoomEvent<mtx::events::msg::CallAnswer>;
using CallHangUp = mtx::events::RoomEvent<mtx::events::msg::CallHangUp>;
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 =
QString(" %1").arg(messageDescription<Encrypted>(username, "", 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.toUcs4()) {
if (QString::fromUcs4(&c, 1) != QString("#"))
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);
return "";
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 QString("https://%1:%2/_matrix/media/r0/download/%3/%4")
.arg(server)
.arg(port)
.arg(QString::fromStdString(mxcParts.server))
.arg(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(ed25519.midRef(i, 4));
if (i > 0 && i % 16 == 12)
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("\\b(?<![\"'])(?>(matrix:[\\S]{5,}))(?![\"'])\\b"),
conf::strings::url_html);
QString
utils::escapeBlacklistedHtml(const QString &rawStr)
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
static const std::array 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"};
QByteArray data = rawStr.toUtf8();
QByteArray buffer;
const int length = data.size();
buffer.reserve(length);
bool escapingTag = false;
for (int pos = 0; pos != length; ++pos) {
switch (data.at(pos)) {
case '<': {
bool oneTagMatched = false;
const int endPos =
static_cast<int>(std::min(static_cast<size_t>(data.indexOf('>', pos)),
static_cast<size_t>(data.indexOf(' ', pos))));
auto mid = data.mid(pos + 1, endPos - pos - 1);
for (const auto &tag : allowedTags) {
// TODO: Check src and href attribute
if (mid.toLower() == tag) {
oneTagMatched = true;
}
if (oneTagMatched)
buffer.append('<');
else {
escapingTag = true;
buffer.append("<");
}
break;
case '>':
if (escapingTag) {
buffer.append(">");
escapingTag = false;
} else
buffer.append('>');
break;
default:
buffer.append(data.at(pos));
break;
}
}
return QString::fromUtf8(buffer);
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);
cmark_event_type ev_type;
// First loop to get total text length
int textLen = 0;
while ((ev_type = 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++;
}
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
// create new iter to start over
cmark_iter_free(iter);
iter = cmark_iter_new(node);
// Second loop to rainbowify
int charIdx = 0;
while ((ev_type = 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 = nodeText.midRef(boundaryStart, boundaryEnd - boundaryStart);
boundaryStart = boundaryEnd;
// Don't rainbowify whitespaces
if (curChar.trimmed().isEmpty() || codepointIsEmoji(curChar.toUcs4().first())) {
buf.append(curChar);
continue;
// 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 =
QString("<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("<p>") == 1 && result.startsWith("<p>") && result.endsWith("</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: {
}
case MsgType::Image: {
}
case MsgType::Audio: {
}
case MsgType::Video: {
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
};
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 "sent a file.";
}
case MsgType::Image: {
return "sent an image.";
}
case MsgType::Audio: {
return "sent an audio file.";
}
case MsgType::Video: {
return "sent a video";
}
default: {
return related.quoted_body;
}
}
QString
utils::linkColor()
{
const auto theme = UserSettings::instance()->theme();
if (theme == "light") {
return "#0077b5";
} else if (theme == "dark") {
return "#38A3D8";
} else {
return QPalette().color(QPalette::Link).name();
}
utils::hashQString(const QString &input)
for (int i = 0; i < input.length(); i++) {
hash = input.at(i).digitValue() + ((hash << 5) - hash);
}
QColor
utils::generateContrastingHexColor(const QString &input, const QColor &backgroundCol)
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
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.
auto userHue = static_cast<int>(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.
int iterationCount = 9;
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;
}
}
// 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;
void
utils::centerWidget(QWidget *widget, QWidget *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));
};
if (parent) {
widget->move(parent->window()->frameGeometry().topLeft() +
parent->window()->rect().center() - widget->rect().center());
return;
}
// Deprecated in 5.13: widget->move(findCenter(QApplication::desktop()->screenGeometry()));
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();