Skip to content
Snippets Groups Projects
Utils.cpp 24.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • #include <QApplication>
    
    #include <QBuffer>
    
    #include <QComboBox>
    
    #include <QDesktopWidget>
    
    #include <QGuiApplication>
    
    #include <QImageReader>
    
    #include <QProcessEnvironment>
    
    #include <QScreen>
    
    #include <QXmlStreamReader>
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    #include <variant>
    
    #include <cmark.h>
    
    #include "Config.h"
    
    using TimelineEvent = mtx::events::collections::TimelineEvents;
    
    
    Joe Donofry's avatar
    Joe Donofry committed
    QHash<QString, QString> authorColors_;
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    template<class T, class Event>
    static DescInfo
    createDescriptionInfo(const Event &event, const QString &localUser, const QString &room_id)
    {
            const auto msg    = std::get<T>(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);
    
    
    trilene's avatar
    trilene committed
            return DescInfo{QString::fromStdString(msg.event_id),
                            sender,
                            utils::messageDescription<T>(
                              username, utils::event_body(event).trimmed(), sender == localUser),
                            utils::descriptiveTime(ts),
                            msg.origin_server_ts,
                            ts};
    
            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 >= 0x1f300 && code <= 0x1f3ff) ||
                   (code >= 0x1f000 && code <= 0x1faff);
    }
    
    
    QString
    utils::replaceEmoji(const QString &body)
    {
            QString fmtBody = "";
    
            QVector<uint> utf32_string = body.toUcs4();
    
            QSettings settings;
            QString userFontFamily = settings.value("user/emoji_font_family", "emoji").toString();
    
    
            bool insideFontBlock = false;
    
            for (auto &code : utf32_string) {
    
                    if (utils::codepointIsEmoji(code)) {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            if (!insideFontBlock) {
                                    fmtBody += QString("<font face=\"" + userFontFamily + "\">");
                                    insideFontBlock = true;
                            }
    
                    } else {
                            if (insideFontBlock) {
                                    fmtBody += "</font>";
                                    insideFontBlock = false;
                            }
                    }
                    fmtBody += QString::fromUcs4(&code, 1);
    
            if (insideFontBlock) {
                    fmtBody += "</font>";
            }
    
    void
    utils::setScaleFactor(float factor)
    {
            if (factor < 1 || factor > 3)
                    return;
    
            QSettings settings;
            settings.setValue("settings/scale_factor", factor);
    }
    
    float
    utils::scaleFactor()
    {
    
            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)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    return QLocale::system().toString(then.time(), QLocale::ShortFormat);
    
                    return QString(QCoreApplication::translate("descriptiveTime", "Yesterday"));
            else if (days < 7)
                    return then.toString("dddd");
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            return QLocale::system().toString(then.date(), QLocale::ShortFormat);
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    utils::getMessageDescription(const TimelineEvent &event,
                                 const QString &localUser,
                                 const QString &room_id)
    
    trilene's avatar
    trilene 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::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>;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            if (std::holds_alternative<Audio>(event)) {
    
                    return createDescriptionInfo<Audio>(event, localUser, room_id);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            } else if (std::holds_alternative<Emote>(event)) {
    
                    return createDescriptionInfo<Emote>(event, localUser, room_id);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            } else if (std::holds_alternative<File>(event)) {
    
                    return createDescriptionInfo<File>(event, localUser, room_id);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            } else if (std::holds_alternative<Image>(event)) {
    
                    return createDescriptionInfo<Image>(event, localUser, room_id);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            } else if (std::holds_alternative<Notice>(event)) {
    
                    return createDescriptionInfo<Notice>(event, localUser, room_id);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            } else if (std::holds_alternative<Text>(event)) {
    
                    return createDescriptionInfo<Text>(event, localUser, room_id);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            } else if (std::holds_alternative<Video>(event)) {
    
                    return createDescriptionInfo<Video>(event, localUser, room_id);
    
    trilene's avatar
    trilene committed
            } else if (std::holds_alternative<CallInvite>(event)) {
                    return createDescriptionInfo<CallInvite>(event, localUser, room_id);
            } else if (std::holds_alternative<CallAnswer>(event)) {
                    return createDescriptionInfo<CallAnswer>(event, localUser, room_id);
            } else if (std::holds_alternative<CallHangUp>(event)) {
                    return createDescriptionInfo<CallHangUp>(event, localUser, room_id);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            } else if (std::holds_alternative<mtx::events::Sticker>(event)) {
    
                    return createDescriptionInfo<mtx::events::Sticker>(event, localUser, room_id);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            } else if (auto msg = std::get_if<Encrypted>(&event); msg != nullptr) {
                    const auto sender = QString::fromStdString(msg->sender);
    
                    const auto username = cache::displayName(room_id, sender);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    const auto ts       = QDateTime::fromMSecsSinceEpoch(msg->origin_server_ts);
    
                    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;
    
    
    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();
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
    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());
    }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    utils::event_body(const mtx::events::collections::TimelineEvents &e)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            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);
    
              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)
    {
            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 = body;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            doc.replace(conf::strings::url_regex, conf::strings::url_html);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            return doc;
    
    QString
    utils::escapeBlacklistedHtml(const QString &rawStr)
    
            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 size_t length = data.size();
            buffer.reserve(length);
    
            bool escapingTag = false;
    
            for (size_t pos = 0; pos != length; ++pos) {
                    switch (data.at(pos)) {
    
                    case '<': {
                            bool oneTagMatched = false;
                            size_t endPos      = 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("&lt;");
                            }
    
                            if (escapingTag) {
    
                                    buffer.append("&gt;");
                                    escapingTag = false;
    
                                    buffer.append('>');
    
                            break;
                    default:
                            buffer.append(data.at(pos));
                            break;
                    }
            }
    
            return QString::fromUtf8(buffer);
    
    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_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);
            }
    
    
            return result;
    
    QString
    utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html)
    {
    
            auto getFormattedBody = [related]() -> QString {
    
                    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_formatted_body;
                    }
                    }
            };
    
            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()) +
    
    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()
    {
            QSettings settings;
    
            // Default to system theme if QT_QPA_PLATFORMTHEME var is set.
            QString defaultTheme =
              QProcessEnvironment::systemEnvironment().value("QT_QPA_PLATFORMTHEME", "").isEmpty()
                ? "light"
                : "system";
            const auto theme = settings.value("user/theme", defaultTheme).toString();
    
            if (theme == "light") {
    
                    return "#0077b5";
    
            } else if (theme == "dark") {
    
                    return "#38A3D8";
    
            } else {
                    return QPalette().color(QPalette::Link).name();
            }
    
    utils::hashQString(const QString &input)
    
            uint32_t hash = 0;
    
    
            for (int i = 0; i < input.length(); i++) {
                    hash = input.at(i).digitValue() + ((hash << 5) - hash);
            }
    
    
            return hash;
    }
    
    QString
    utils::generateContrastingHexColor(const QString &input, const QString &background)
    {
            const QColor backgroundCol(background);
            const qreal backgroundLum = luminance(background);
    
            // 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>(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;
                                    }
                            }
    
    
                    // 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;
            }
    
            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);
    
            auto lum = lumRgb[0] * 0.2126 + lumRgb[1] * 0.7152 + lumRgb[2] * 0.0722;
    
            return lum;
    
    void
    utils::centerWidget(QWidget *widget, QWidget *parent)
    {
    
    Konstantinos Sideris's avatar
    Konstantinos Sideris committed
            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(parent->window()->frameGeometry().topLeft() +
                                 parent->window()->rect().center() - widget->rect().center());
    
            // 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::readImage(QByteArray *data)
    {
            QBuffer buf(data);
            QImageReader reader(&buf);
            reader.setAutoTransform(true);
            return reader.read();
    }