refactor: Turn link-info into its own element and class (#5178)

This commit is contained in:
nerix
2024-02-18 13:34:00 +01:00
committed by GitHub
parent 42e4559910
commit e130c48f76
22 changed files with 752 additions and 314 deletions
-64
View File
@@ -1,64 +0,0 @@
#include "providers/LinkResolver.hpp"
#include "common/Env.hpp"
#include "common/network/NetworkRequest.hpp"
#include "common/network/NetworkResult.hpp"
#include "messages/Image.hpp"
#include "messages/Link.hpp"
#include "singletons/Settings.hpp"
#include <QString>
namespace chatterino {
void LinkResolver::getLinkInfo(
const QString url, QObject *caller,
std::function<void(QString, Link, ImagePtr)> successCallback)
{
if (!getSettings()->linkInfoTooltip)
{
successCallback("No link info loaded", Link(Link::Url, url), nullptr);
return;
}
// Uncomment to test crashes
// QTimer::singleShot(3000, [=]() {
NetworkRequest(Env::get().linkResolverUrl.arg(QString::fromUtf8(
QUrl::toPercentEncoding(url, "", "/:"))))
.caller(caller)
.timeout(30000)
.onSuccess([successCallback, url](NetworkResult result) mutable {
auto root = result.parseJson();
auto statusCode = root.value("status").toInt();
QString response;
QString linkString = url;
ImagePtr thumbnail = nullptr;
if (statusCode == 200)
{
response = root.value("tooltip").toString();
if (root.contains("thumbnail"))
{
thumbnail =
Image::fromUrl({root.value("thumbnail").toString()});
}
if (getSettings()->unshortLinks)
{
linkString = root.value("link").toString();
}
}
else
{
response = root.value("message").toString();
}
successCallback(QUrl::fromPercentEncoding(response.toUtf8()),
Link(Link::Url, linkString), thumbnail);
})
.onError([successCallback, url](auto /*result*/) {
successCallback("No link info found", Link(Link::Url, url),
nullptr);
})
.execute();
// });
}
} // namespace chatterino
-23
View File
@@ -1,23 +0,0 @@
#pragma once
#include <QObject>
#include <QString>
#include <functional>
#include <memory>
namespace chatterino {
class Image;
struct Link;
using ImagePtr = std::shared_ptr<Image>;
class LinkResolver
{
public:
static void getLinkInfo(
const QString url, QObject *caller,
std::function<void(QString, Link, ImagePtr)> callback);
};
} // namespace chatterino
+106
View File
@@ -0,0 +1,106 @@
#include "providers/links/LinkInfo.hpp"
#include "debug/AssertInGuiThread.hpp"
#include <QString>
namespace chatterino {
LinkInfo::LinkInfo(QString url)
: QObject(nullptr)
, originalUrl_(url)
, url_(std::move(url))
, tooltip_(this->url_)
{
}
LinkInfo::~LinkInfo() = default;
LinkInfo::State LinkInfo::state() const
{
return this->state_;
}
QString LinkInfo::url() const
{
return this->url_;
}
QString LinkInfo::originalUrl() const
{
return this->originalUrl_;
}
bool LinkInfo::isPending() const
{
return this->state_ == State::Created;
}
bool LinkInfo::isLoading() const
{
return this->state_ == State::Loading;
}
bool LinkInfo::isLoaded() const
{
return this->state_ > State::Loading;
}
bool LinkInfo::isResolved() const
{
return this->state_ == State::Resolved;
}
bool LinkInfo::hasError() const
{
return this->state_ == State::Errored;
}
bool LinkInfo::hasThumbnail() const
{
return this->thumbnail_ && !this->thumbnail_->url().string.isEmpty();
}
QString LinkInfo::tooltip() const
{
return this->tooltip_;
}
ImagePtr LinkInfo::thumbnail() const
{
return this->thumbnail_;
}
void LinkInfo::setState(State state)
{
assertInGuiThread();
assert(state >= this->state_);
if (this->state_ == state)
{
return;
}
this->state_ = state;
this->stateChanged(state);
}
void LinkInfo::setResolvedUrl(QString resolvedUrl)
{
assertInGuiThread();
this->url_ = std::move(resolvedUrl);
}
void LinkInfo::setTooltip(QString tooltip)
{
assertInGuiThread();
this->tooltip_ = std::move(tooltip);
}
void LinkInfo::setThumbnail(ImagePtr thumbnail)
{
assertInGuiThread();
this->thumbnail_ = std::move(thumbnail);
}
} // namespace chatterino
+135
View File
@@ -0,0 +1,135 @@
#pragma once
#include "messages/Image.hpp"
namespace chatterino {
/// @brief Rich info about a URL with tooltip and thumbnail
///
/// This is only a data class - it doesn't do the resolving.
/// It can only be used from the GUI thread.
class LinkInfo : public QObject
{
Q_OBJECT
public:
/// @brief the state of a link info
///
/// The state of a link can only increase. For example, it's not possible
/// for the link to change from "Resolved" to "Loading".
enum class State {
/// @brief The object was created, no info is resolved
///
/// This is the initial state
Created,
/// Info is currently loading
Loading,
/// Info has been resolved and the properties have been updated
Resolved,
/// There has been an error resolving the link info (e.g. timeout)
Errored,
};
/// @brief Constructs a new link info for a URL
///
/// This doesn't load any link info.
/// @see #ensureLoadingStarted()
[[nodiscard]] explicit LinkInfo(QString url);
~LinkInfo() override;
LinkInfo(const LinkInfo &) = delete;
LinkInfo(LinkInfo &&) = delete;
LinkInfo &operator=(const LinkInfo &) = delete;
LinkInfo &operator=(LinkInfo &&) = delete;
/// @brief The URL of this link
///
/// If the "unshortLinks" setting is enabled, this can change after the
/// link is resolved.
[[nodiscard]] QString url() const;
/// @brief The URL of this link as seen in the message
///
/// If the "unshortLinks" setting doesn't affect this URL.
[[nodiscard]] QString originalUrl() const;
/// Returns the current state
[[nodiscard]] State state() const;
/// Returns true if this link has not yet been resolved (it's "Created")
[[nodiscard]] bool isPending() const;
/// Returns true if the info is loading
[[nodiscard]] bool isLoading() const;
/// Returns true if the info is loaded (resolved or errored)
[[nodiscard]] bool isLoaded() const;
/// Returns true if this link has been resolved
[[nodiscard]] bool isResolved() const;
/// Returns true if the info failed to resolve
[[nodiscard]] bool hasError() const;
/// Returns true if this link has a thumbnail
[[nodiscard]] bool hasThumbnail() const;
/// @brief Returns the tooltip of this link
///
/// The tooltip contains the URL of the link and any info added by the
/// resolver. Resolvers must include the URL.
[[nodiscard]] QString tooltip() const;
/// @brief Returns the thumbnail of this link
///
/// The thumbnail is provided by the resolver and might not have been
/// loaded yet.
///
/// @pre The caller must check #hasThumbnail() before calling this method
[[nodiscard]] ImagePtr thumbnail() const;
/// @brief Updates the state and emits #stateChanged accordingly
///
/// @pre The caller must be in the GUI thread.
/// @pre @a state must be greater or equal to the current state.
/// @see #state(), #stateChanged
void setState(State state);
/// @brief Updates the resolved url of this link
///
/// @pre The caller must be in the GUI thread.
/// @see #url()
void setResolvedUrl(QString resolvedUrl);
/// @brief Updates the tooltip of this link
///
/// @pre The caller must be in the GUI thread.
/// @see #tooltip()
void setTooltip(QString tooltip);
/// @brief Updates the thumbnail of this link
///
/// The thumbnail is allowed to be empty or nullptr.
///
/// @pre The caller must be in the GUI thread.
/// @see #hasThumbnail(), #thumbnail()
void setThumbnail(ImagePtr thumbnail);
signals:
/// @brief Emitted when this link's state changes
///
/// @param state The new state
void stateChanged(State state);
private:
const QString originalUrl_;
QString url_;
QString tooltip_;
ImagePtr thumbnail_;
State state_ = State::Created;
};
} // namespace chatterino
+72
View File
@@ -0,0 +1,72 @@
#include "providers/links/LinkResolver.hpp"
#include "common/Env.hpp"
#include "common/network/NetworkRequest.hpp"
#include "common/network/NetworkResult.hpp"
#include "providers/links/LinkInfo.hpp"
#include "singletons/Settings.hpp"
#include <QStringBuilder>
namespace chatterino {
void LinkResolver::resolve(LinkInfo *info)
{
using State = LinkInfo::State;
assert(info);
if (info->state() != State::Created)
{
// The link is already resolved or is currently loading
return;
}
if (!getSettings()->linkInfoTooltip)
{
return;
}
info->setTooltip("Loading...");
info->setState(State::Loading);
NetworkRequest(Env::get().linkResolverUrl.arg(QString::fromUtf8(
QUrl::toPercentEncoding(info->originalUrl(), {}, "/:"))))
.caller(info)
.timeout(30000)
.onSuccess([info](const NetworkResult &result) {
const auto root = result.parseJson();
QString response;
QString url;
ImagePtr thumbnail = nullptr;
if (root["status"].toInt() == 200)
{
response = root["tooltip"].toString();
if (root.contains("thumbnail"))
{
info->setThumbnail(
Image::fromUrl({root["thumbnail"].toString()}));
}
if (getSettings()->unshortLinks && root.contains("link"))
{
info->setResolvedUrl(root["link"].toString());
}
}
else
{
response = root["message"].toString();
}
info->setTooltip(QUrl::fromPercentEncoding(response.toUtf8()));
info->setState(State::Resolved);
})
.onError([info](const auto &result) {
info->setTooltip(u"No link info found (" % result.formatError() %
u')');
info->setState(State::Errored);
})
.execute();
}
} // namespace chatterino
+36
View File
@@ -0,0 +1,36 @@
#pragma once
namespace chatterino {
class LinkInfo;
class ILinkResolver
{
public:
ILinkResolver() = default;
virtual ~ILinkResolver() = default;
ILinkResolver(const ILinkResolver &) = delete;
ILinkResolver(ILinkResolver &&) = delete;
ILinkResolver &operator=(const ILinkResolver &) = delete;
ILinkResolver &operator=(ILinkResolver &&) = delete;
virtual void resolve(LinkInfo *info) = 0;
};
class LinkResolver : public ILinkResolver
{
public:
LinkResolver() = default;
/// @brief Loads and updates the link info
///
/// Calling this with an already resolved or currently loading info is a
/// no-op. Loading can be blocked by disabling the "linkInfoTooltip"
/// setting. URLs will be unshortened if the "unshortLinks" setting is
/// enabled. The resolver is set through Env::linkResolverUrl.
///
/// @pre @a info must not be nullptr
void resolve(LinkInfo *info) override;
};
} // namespace chatterino