#include "Utils.h" #include <QApplication> #include <QComboBox> #include <QDesktopWidget> #include <QSettings> #include <QTextDocument> #include <QXmlStreamReader> #include <cmath> #include <boost/variant.hpp> #include <cmark.h> #include "Config.h" using TimelineEvent = mtx::events::collections::TimelineEvents; QString utils::localUser() { QSettings settings; return settings.value("auth/user_id").toString(); } void utils::setScaleFactor(float factor) { if (factor < 1 || factor > 3) return; QSettings settings; settings.setValue("settings/scale_factor", factor); } float utils::scaleFactor() { QSettings settings("nheko", "nheko"); return settings.value("settings/scale_factor", -1).toFloat(); } bool utils::respondsToKeyRequests(const std::string &roomId) { return respondsToKeyRequests(QString::fromStdString(roomId)); } bool utils::respondsToKeyRequests(const QString &roomId) { if (roomId.isEmpty()) return false; QSettings settings; return settings.value("rooms/respond_to_key_requests/" + roomId, false).toBool(); } void utils::setKeyRequestsPreference(QString roomId, bool value) { if (roomId.isEmpty()) return; QSettings settings; settings.setValue("rooms/respond_to_key_requests/" + roomId, value); } QString utils::descriptiveTime(const QDateTime &then) { const auto now = QDateTime::currentDateTime(); const auto days = then.daysTo(now); if (days == 0) return then.toString("HH:mm"); else if (days < 2) return QString("Yesterday"); else if (days < 365) return then.toString("dd/MM"); return then.toString("dd/MM/yy"); } DescInfo utils::getMessageDescription(const TimelineEvent &event, const QString &localUser, const QString &room_id) { 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 Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>; if (boost::get<Audio>(&event) != nullptr) { return createDescriptionInfo<Audio>(event, localUser, room_id); } else if (boost::get<Emote>(&event) != nullptr) { return createDescriptionInfo<Emote>(event, localUser, room_id); } else if (boost::get<File>(&event) != nullptr) { return createDescriptionInfo<File>(event, localUser, room_id); } else if (boost::get<Image>(&event) != nullptr) { return createDescriptionInfo<Image>(event, localUser, room_id); } else if (boost::get<Notice>(&event) != nullptr) { return createDescriptionInfo<Notice>(event, localUser, room_id); } else if (boost::get<Text>(&event) != nullptr) { return createDescriptionInfo<Text>(event, localUser, room_id); } else if (boost::get<Video>(&event) != nullptr) { return createDescriptionInfo<Video>(event, localUser, room_id); } else if (boost::get<mtx::events::Sticker>(&event) != nullptr) { return createDescriptionInfo<mtx::events::Sticker>(event, localUser, room_id); } else if (boost::get<Encrypted>(&event) != nullptr) { const auto msg = boost::get<Encrypted>(event); const auto sender = QString::fromStdString(msg.sender); const auto username = Cache::displayName(room_id, sender); const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); DescInfo info; if (sender == localUser) info.username = "You"; else info.username = username; info.userid = sender; info.body = QString(" %1").arg(messageDescription<Encrypted>()); info.timestamp = utils::descriptiveTime(ts); info.event_id = QString::fromStdString(msg.event_id); info.datetime = ts; return info; } return DescInfo{}; } QString utils::firstChar(const QString &input) { if (input.isEmpty()) return 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(); } QString utils::humanReadableFileSize(uint64_t bytes) { 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 int nlen = s1.size(); const int hlen = s2.size(); if (hlen == 0) return -1; if (nlen == 1) return s2.find(s1); std::vector<int> row1(hlen + 1, 0); for (int i = 0; i < nlen; ++i) { std::vector<int> row2(1, i + 1); for (int 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 &event) { using namespace mtx::events; using namespace mtx::events::msg; if (boost::get<RoomEvent<Audio>>(&event) != nullptr) { return message_body<RoomEvent<Audio>>(event); } else if (boost::get<RoomEvent<Emote>>(&event) != nullptr) { return message_body<RoomEvent<Emote>>(event); } else if (boost::get<RoomEvent<File>>(&event) != nullptr) { return message_body<RoomEvent<File>>(event); } else if (boost::get<RoomEvent<Image>>(&event) != nullptr) { return message_body<RoomEvent<Image>>(event); } else if (boost::get<RoomEvent<Notice>>(&event) != nullptr) { return message_body<RoomEvent<Notice>>(event); } else if (boost::get<Sticker>(&event) != nullptr) { return message_body<Sticker>(event); } else if (boost::get<RoomEvent<Text>>(&event) != nullptr) { return message_body<RoomEvent<Text>>(event); } else if (boost::get<RoomEvent<Video>>(&event) != nullptr) { return message_body<RoomEvent<Video>>(event); } return QString(); } QPixmap utils::scaleImageToPixmap(const QImage &img, int size) { if (img.isNull()) return QPixmap(); const double sz = std::ceil(QApplication::desktop()->screen()->devicePixelRatioF() * (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) { QStringList fingerprintList; for (int i = 0; i < ed25519.length(); i = i + 4) { fingerprintList << ed25519.mid(i, 4); } return fingerprintList.join(" "); } QString utils::linkifyMessage(const QString &body) { // Convert to valid XML. auto doc = QString("<html>%1</html>").arg(body); doc.replace("<mx-reply>", ""); doc.replace("</mx-reply>", ""); doc.replace("<br>", "<br></br>"); QXmlStreamReader xml{doc}; QString textString; while (!xml.atEnd() && !xml.hasError()) { auto t = xml.readNext(); switch (t) { case QXmlStreamReader::Characters: { auto text = xml.text().toString(); text.replace(conf::strings::url_regex, conf::strings::url_html); textString += text; break; } case QXmlStreamReader::StartDocument: case QXmlStreamReader::EndDocument: break; case QXmlStreamReader::StartElement: { if (xml.name() == "html") break; textString += QString("<%1").arg(xml.name().toString()); const auto attrs = xml.attributes(); for (const auto &e : attrs) textString += QString(" %1=\"%2\"") .arg(e.name().toString()) .arg(e.value().toString()); textString += ">"; break; } case QXmlStreamReader::EndElement: { if (xml.name() == "html") break; textString += QString("</%1>").arg(xml.name().toString()); break; } default: { break; } } } if (xml.hasError()) { qWarning() << "error while parsing xml" << xml.errorString() << doc; doc.replace("<html>", ""); doc.replace("</html>", ""); return doc; } return textString; } QString utils::markdownToHtml(const QString &text) { const auto str = text.toUtf8(); const char *tmp_buf = cmark_markdown_to_html(str.constData(), str.size(), CMARK_OPT_DEFAULT); // Copy the null terminated output buffer. std::string html(tmp_buf); // The buffer is no longer needed. free((char *)tmp_buf); auto result = QString::fromStdString(html).trimmed(); return result; } QString utils::linkColor() { QSettings settings; const auto theme = settings.value("user/theme", "light").toString(); if (theme == "light") return "#0077b5"; else if (theme == "dark") return "#38A3D8"; return QPalette().color(QPalette::Link).name(); } QString utils::generateHexColor(const int hash) { QString colour("#"); for (int i = 0; i < 3; i++) { int value = (hash >> (i * 8)) & 0xFF; colour.append(("00" + QString::number(value, 16)).right(2)); } // nhlog::ui()->debug("Hex Generated {} -> {}", QString::number(hash).toStdString(), // colour.toStdString()); return colour.toUpper(); } int utils::hashQString(const QString &input) { auto hash = 0; for (int i = 0; i < input.length(); i++) { hash = input.at(i).digitValue() + ((hash << 5) - hash); } hash *= 13; return hash; } QString utils::generateContrastingHexColor(const QString &input, const QString &background) { nhlog::ui()->debug("Background hex {}", background.toStdString()); const QColor backgroundCol(background); const qreal backgroundLum = luminance(background); // Create a color for the input auto hash = hashQString(input); auto colorHex = generateHexColor(hash); // converting to a QColor makes the luminance calc easier. QColor inputColor = QColor(colorHex); // attempt to score both the luminance and the contrast. // contrast should have a higher precedence, but luminance // helps dictate how exciting the colors are. auto colorLum = luminance(inputColor); auto contrast = computeContrast(colorLum, backgroundLum); // If the contrast or luminance don't meet our criteria, // try again and again until they do. After 10 tries, // the best-scoring color will be chosen. int att = 0; while ((contrast < 5 || (colorLum < 0.05 || colorLum > 0.95)) && ++att < 10) { hash = hashQString(input) + ((hash << 2) * 13); auto newHex = generateHexColor(hash); inputColor.setNamedColor(newHex); auto tmpLum = luminance(inputColor); auto tmpContrast = computeContrast(tmpLum, backgroundLum); // Prioritize contrast over luminance // If both values are better, it's a no brainer. if (tmpContrast > contrast && (tmpLum > 0.05 && tmpLum < 0.95)) { contrast = tmpContrast; colorHex = newHex; colorLum = tmpLum; } // Otherwise, if we still can get a more // vibrant color and have met our contrast // threshold, pick the more vibrant color, // even if contrast will drop somewhat. // choosing 50% luminance as ideal. else if ((qAbs(tmpLum - 0.50) < qAbs(colorLum - 0.50)) && tmpContrast >= 5) { contrast = tmpContrast; colorHex = newHex; colorLum = tmpLum; } // Otherwise, just take the better contrast. else if (tmpContrast > contrast) { contrast = tmpContrast; colorHex = newHex; colorLum = tmpLum; } } nhlog::ui()->debug("Hex Generated for {}: [hex: {}, contrast: {}, luminance: {}]", input.toStdString(), colorHex.toStdString(), QString::number(contrast).toStdString(), QString::number(colorLum).toStdString()); 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; } return 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; v <= 0.03928 ? lumRgb[i] = v / 12.92 : lumRgb[i] = qPow((v + 0.055) / 1.055, 2.4); } return 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(findCenter(parent->geometry())); return; } widget->move(findCenter(QApplication::desktop()->screenGeometry())); } 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; } } } utils::SideBarSizes utils::calculateSidebarSizes(const QFont &f) { const auto height = static_cast<double>(QFontMetrics{f}.lineSpacing()); SideBarSizes sz; sz.small = std::ceil(3.5 * height + height / 4.0); sz.normal = std::ceil(16 * height); sz.groups = std::ceil(3 * height); sz.collapsePoint = 2 * sz.normal; return sz; }