nheko/src/Utils.cpp
Joseph Donofry 129beb57c9
Further Improve Reply Functionality
Quoted replies now include matrix.to links for the event and the user.
UI Rendering has been (slightly) improved... still very WIP.
Restructured the reply structure in the code for future usability
improvements.
2019-06-13 22:33:04 -04:00

505 lines
17 KiB
C++

#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;
QHash<QString, QString> authorColors_;
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(conf::strings::url_regex, conf::strings::url_html);
return doc;
}
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::getFormattedQuoteBody(const RelatedInfo &related, const QString &html)
{
return QString("<mx-reply><blockquote><a "
"href=\"https://matrix.to/#/!%1\">In reply "
"to</a><a href=\"https://matrix.to/#/%2\">%3</a><br "
"/>%4</blockquote></mx-reply>")
.arg(QString::fromStdString(related.related_event),
related.quoted_user,
related.quoted_user,
related.quoted_body) +
html;
}
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();
}
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);
}
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 = 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;
}
}
}
}
// 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)
{
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;
}