Add thumbnails to link tooltips if available (#1664)

This feature is off by default and can be enabled in the settings with the "Show link thumbnail" setting. This feature also requires the "Show link info when hovering" setting to be enabled.

thumbnails support is only there for direct image links, twitch clips, and youtube links. can be expanded in the future in the /api repo

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
Daniel Pasch 2020-05-10 12:11:10 +02:00 committed by GitHub
parent 1a4a468ab1
commit 8532c6d3bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 140 additions and 27 deletions

View file

@ -1,6 +1,7 @@
# Changelog # Changelog
## Unversioned ## Unversioned
- Major: We now support image thumbnails coming from the link resolver. This feature is off by default and can be enabled in the settings with the "Show link thumbnail" setting. This feature also requires the "Show link info when hovering" setting to be enabled (#1664)
- Major: Added image upload functionality to i.nuuls.com. This works by dragging and dropping an image into a split, or pasting an image into the text edit field. (#1332) - Major: Added image upload functionality to i.nuuls.com. This works by dragging and dropping an image into a split, or pasting an image into the text edit field. (#1332)
- Minor: Emotes in the emote popup are now sorted in the same order as the tab completion (#1549) - Minor: Emotes in the emote popup are now sorted in the same order as the tab completion (#1549)
- Minor: Removed "Online Logs" functionality as services are shut down (#1640) - Minor: Removed "Online Logs" functionality as services are shut down (#1640)

View file

@ -419,7 +419,8 @@ void MessageBuilder::addLink(const QString &origLink,
LinkResolver::getLinkInfo( LinkResolver::getLinkInfo(
matchedLink, nullptr, matchedLink, nullptr,
[weakMessage = this->weakOf(), linkMELowercase, linkMEOriginal, [weakMessage = this->weakOf(), linkMELowercase, linkMEOriginal,
matchedLink](QString tooltipText, Link originalLink) { matchedLink](QString tooltipText, Link originalLink,
ImagePtr thumbnail) {
auto shared = weakMessage.lock(); auto shared = weakMessage.lock();
if (!shared) if (!shared)
{ {
@ -436,6 +437,12 @@ void MessageBuilder::addLink(const QString &origLink,
linkMELowercase->setLink(originalLink)->updateLink(); linkMELowercase->setLink(originalLink)->updateLink();
linkMEOriginal->setLink(originalLink)->updateLink(); linkMEOriginal->setLink(originalLink)->updateLink();
} }
linkMELowercase->setThumbnail(thumbnail);
linkMELowercase->setThumbnailType(
MessageElement::ThumbnailType::Link_Thumbnail);
linkMEOriginal->setThumbnail(thumbnail);
linkMEOriginal->setThumbnailType(
MessageElement::ThumbnailType::Link_Thumbnail);
}); });
} }

View file

@ -40,6 +40,18 @@ MessageElement *MessageElement::setTooltip(const QString &tooltip)
return this; return this;
} }
MessageElement *MessageElement::setThumbnail(const ImagePtr &thumbnail)
{
this->thumbnail_ = thumbnail;
return this;
}
MessageElement *MessageElement::setThumbnailType(const ThumbnailType type)
{
this->thumbnailType_ = type;
return this;
}
MessageElement *MessageElement::setTrailingSpace(bool value) MessageElement *MessageElement::setTrailingSpace(bool value)
{ {
this->trailingSpace = value; this->trailingSpace = value;
@ -51,6 +63,16 @@ const QString &MessageElement::getTooltip() const
return this->tooltip_; return this->tooltip_;
} }
const ImagePtr &MessageElement::getThumbnail() const
{
return this->thumbnail_;
}
const MessageElement::ThumbnailType &MessageElement::getThumbnailType() const
{
return this->thumbnailType_;
}
const Link &MessageElement::getLink() const const Link &MessageElement::getLink() const
{ {
return this->link_; return this->link_;

View file

@ -122,14 +122,23 @@ public:
Update_Images = 4, Update_Images = 4,
Update_All = Update_Text | Update_Emotes | Update_Images Update_All = Update_Text | Update_Emotes | Update_Images
}; };
enum ThumbnailType : char {
Link_Thumbnail = 1,
};
virtual ~MessageElement(); virtual ~MessageElement();
MessageElement *setLink(const Link &link); MessageElement *setLink(const Link &link);
MessageElement *setText(const QString &text); MessageElement *setText(const QString &text);
MessageElement *setTooltip(const QString &tooltip); MessageElement *setTooltip(const QString &tooltip);
MessageElement *setThumbnailType(const ThumbnailType type);
MessageElement *setThumbnail(const ImagePtr &thumbnail);
MessageElement *setTrailingSpace(bool value); MessageElement *setTrailingSpace(bool value);
const QString &getTooltip() const; const QString &getTooltip() const;
const ImagePtr &getThumbnail() const;
const ThumbnailType &getThumbnailType() const;
const Link &getLink() const; const Link &getLink() const;
bool hasTrailingSpace() const; bool hasTrailingSpace() const;
MessageElementFlags getFlags() const; MessageElementFlags getFlags() const;
@ -148,6 +157,8 @@ private:
QString text_; QString text_;
Link link_; Link link_;
QString tooltip_; QString tooltip_;
ImagePtr thumbnail_;
ThumbnailType thumbnailType_;
MessageElementFlags flags_; MessageElementFlags flags_;
}; };

View file

@ -3,6 +3,7 @@
#include "common/Common.hpp" #include "common/Common.hpp"
#include "common/Env.hpp" #include "common/Env.hpp"
#include "common/NetworkRequest.hpp" #include "common/NetworkRequest.hpp"
#include "messages/Image.hpp"
#include "messages/Link.hpp" #include "messages/Link.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
@ -12,11 +13,11 @@ namespace chatterino {
void LinkResolver::getLinkInfo( void LinkResolver::getLinkInfo(
const QString url, QObject *caller, const QString url, QObject *caller,
std::function<void(QString, Link)> successCallback) std::function<void(QString, Link, ImagePtr)> successCallback)
{ {
if (!getSettings()->linkInfoTooltip) if (!getSettings()->linkInfoTooltip)
{ {
successCallback("No link info loaded", Link(Link::Url, url)); successCallback("No link info loaded", Link(Link::Url, url), nullptr);
return; return;
} }
// Uncomment to test crashes // Uncomment to test crashes
@ -25,14 +26,18 @@ void LinkResolver::getLinkInfo(
QUrl::toPercentEncoding(url, "", "/:")))) QUrl::toPercentEncoding(url, "", "/:"))))
.caller(caller) .caller(caller)
.timeout(30000) .timeout(30000)
.onSuccess([successCallback, url](auto result) mutable -> Outcome { .onSuccess(
[successCallback, url](NetworkResult result) mutable -> Outcome {
auto root = result.parseJson(); auto root = result.parseJson();
auto statusCode = root.value("status").toInt(); auto statusCode = root.value("status").toInt();
QString response = QString(); QString response = QString();
QString linkString = url; QString linkString = url;
ImagePtr thumbnail = nullptr;
if (statusCode == 200) if (statusCode == 200)
{ {
response = root.value("tooltip").toString(); response = root.value("tooltip").toString();
thumbnail =
Image::fromUrl({root.value("thumbnail").toString()});
if (getSettings()->unshortLinks) if (getSettings()->unshortLinks)
{ {
linkString = root.value("link").toString(); linkString = root.value("link").toString();
@ -43,12 +48,13 @@ void LinkResolver::getLinkInfo(
response = root.value("message").toString(); response = root.value("message").toString();
} }
successCallback(QUrl::fromPercentEncoding(response.toUtf8()), successCallback(QUrl::fromPercentEncoding(response.toUtf8()),
Link(Link::Url, linkString)); Link(Link::Url, linkString), thumbnail);
return Success; return Success;
}) })
.onError([successCallback, url](auto /*result*/) { .onError([successCallback, url](auto /*result*/) {
successCallback("No link info found", Link(Link::Url, url)); successCallback("No link info found", Link(Link::Url, url),
nullptr);
}) })
.execute(); .execute();
// }); // });

View file

@ -3,6 +3,7 @@
#include <QString> #include <QString>
#include <functional> #include <functional>
#include "messages/Image.hpp"
#include "messages/Link.hpp" #include "messages/Link.hpp"
namespace chatterino { namespace chatterino {
@ -10,8 +11,9 @@ namespace chatterino {
class LinkResolver class LinkResolver
{ {
public: public:
static void getLinkInfo(const QString url, QObject *caller, static void getLinkInfo(
std::function<void(QString, Link)> callback); const QString url, QObject *caller,
std::function<void(QString, Link, ImagePtr)> callback);
}; };
} // namespace chatterino } // namespace chatterino

View file

@ -161,6 +161,7 @@ public:
/// Links /// Links
BoolSetting linksDoubleClickOnly = {"/links/doubleClickToOpen", false}; BoolSetting linksDoubleClickOnly = {"/links/doubleClickToOpen", false};
BoolSetting linkInfoTooltip = {"/links/linkInfoTooltip", false}; BoolSetting linkInfoTooltip = {"/links/linkInfoTooltip", false};
IntSetting thumbnailSize = {"/appearance/thumbnailSize", 0};
BoolSetting unshortLinks = {"/links/unshortLinks", false}; BoolSetting unshortLinks = {"/links/unshortLinks", false};
BoolSetting lowercaseDomains = {"/links/linkLowercase", true}; BoolSetting lowercaseDomains = {"/links/linkLowercase", true};

View file

@ -38,6 +38,14 @@ void TooltipPreviewImage::setImage(ImagePtr image)
this->refreshTooltipWidgetPixmap(); this->refreshTooltipWidgetPixmap();
} }
void TooltipPreviewImage::setImageScale(int w, int h)
{
this->imageWidth_ = w;
this->imageHeight_ = h;
this->refreshTooltipWidgetPixmap();
}
void TooltipPreviewImage::refreshTooltipWidgetPixmap() void TooltipPreviewImage::refreshTooltipWidgetPixmap()
{ {
auto tooltipWidget = TooltipWidget::instance(); auto tooltipWidget = TooltipWidget::instance();
@ -45,8 +53,18 @@ void TooltipPreviewImage::refreshTooltipWidgetPixmap()
if (this->image_ && !tooltipWidget->isHidden()) if (this->image_ && !tooltipWidget->isHidden())
{ {
if (auto pixmap = this->image_->pixmapOrLoad()) if (auto pixmap = this->image_->pixmapOrLoad())
{
if (this->imageWidth_ != 0 && this->imageHeight_)
{
tooltipWidget->setImage(pixmap->scaled(this->imageWidth_,
this->imageHeight_,
Qt::KeepAspectRatio));
}
else
{ {
tooltipWidget->setImage(*pixmap); tooltipWidget->setImage(*pixmap);
}
this->attemptRefresh = false; this->attemptRefresh = false;
} }
else else

View file

@ -9,6 +9,7 @@ class TooltipPreviewImage
public: public:
static TooltipPreviewImage &instance(); static TooltipPreviewImage &instance();
void setImage(ImagePtr image); void setImage(ImagePtr image);
void setImageScale(int w, int h);
TooltipPreviewImage(const TooltipPreviewImage &) = delete; TooltipPreviewImage(const TooltipPreviewImage &) = delete;
@ -17,6 +18,9 @@ private:
private: private:
ImagePtr image_ = nullptr; ImagePtr image_ = nullptr;
int imageWidth_ = 0;
int imageHeight_ = 0;
std::vector<pajlada::Signals::ScopedConnection> connections_; std::vector<pajlada::Signals::ScopedConnection> connections_;
// attemptRefresh is set to true in case we want to preview an image that has not loaded yet (if pixmapOrLoad fails) // attemptRefresh is set to true in case we want to preview an image that has not loaded yet (if pixmapOrLoad fails)

View file

@ -1259,6 +1259,7 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
else else
{ {
auto &tooltipPreviewImage = TooltipPreviewImage::instance(); auto &tooltipPreviewImage = TooltipPreviewImage::instance();
tooltipPreviewImage.setImageScale(0, 0);
auto emoteElement = dynamic_cast<const EmoteElement *>( auto emoteElement = dynamic_cast<const EmoteElement *>(
&hoverLayoutElement->getCreator()); &hoverLayoutElement->getCreator());
auto badgeElement = dynamic_cast<const BadgeElement *>( auto badgeElement = dynamic_cast<const BadgeElement *>(
@ -1287,9 +1288,24 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
} }
} }
else else
{
auto element = &hoverLayoutElement->getCreator();
auto thumbnailSize = getSettings()->thumbnailSize;
if (thumbnailSize == 0)
{ {
tooltipPreviewImage.setImage(nullptr); tooltipPreviewImage.setImage(nullptr);
} }
else
{
tooltipPreviewImage.setImage(element->getThumbnail());
if (element->getThumbnailType() ==
MessageElement::ThumbnailType::Link_Thumbnail)
{
tooltipPreviewImage.setImageScale(thumbnailSize,
thumbnailSize);
}
}
}
tooltipWidget->moveTo(this, event->globalPos()); tooltipWidget->moveTo(this, event->globalPos());
tooltipWidget->setWordWrap(isLinkValid); tooltipWidget->setWordWrap(isLinkValid);

View file

@ -521,6 +521,31 @@ void GeneralPage::initLayout(SettingsLayout &layout)
}, },
[](auto args) { return fuzzyToFloat(args.value, 63.f); }); [](auto args) { return fuzzyToFloat(args.value, 63.f); });
layout.addCheckbox("Show link info when hovering", s.linkInfoTooltip); layout.addCheckbox("Show link info when hovering", s.linkInfoTooltip);
layout.addDropdown<int>(
"Show link thumbnail", {"Off", "Small", "Medium", "Large"},
s.thumbnailSize,
[](auto val) {
if (val == 0)
return QString("Off");
else if (val == 100)
return QString("Small");
else if (val == 200)
return QString("Medium");
else if (val == 300)
return QString("Large");
else
return QString::number(val);
},
[](auto args) {
if (args.value == "Small")
return 100;
else if (args.value == "Medium")
return 200;
else if (args.value == "Large")
return 300;
return fuzzyToInt(args.value, 0);
});
layout.addCheckbox("Double click to open links and other elements in chat", layout.addCheckbox("Double click to open links and other elements in chat",
s.linksDoubleClickOnly); s.linksDoubleClickOnly);
layout.addCheckbox("Unshorten links", s.unshortLinks); layout.addCheckbox("Unshorten links", s.unshortLinks);