refactor: Turn link-info into its own element and class (#5178)
This commit is contained in:
@@ -83,6 +83,7 @@
|
|||||||
- Bugfix: Fixed _Copy message_ copying the channel name in global search. (#5106)
|
- Bugfix: Fixed _Copy message_ copying the channel name in global search. (#5106)
|
||||||
- Bugfix: Reply contexts now use the color of the replied-to message. (#5145)
|
- Bugfix: Reply contexts now use the color of the replied-to message. (#5145)
|
||||||
- Bugfix: Fixed top-level window getting stuck after opening settings. (#5161, #5166)
|
- Bugfix: Fixed top-level window getting stuck after opening settings. (#5161, #5166)
|
||||||
|
- Bugfix: Fixed link info not updating without moving the cursor. (#5178)
|
||||||
- Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978)
|
- Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978)
|
||||||
- Dev: Change clang-format from v14 to v16. (#4929)
|
- Dev: Change clang-format from v14 to v16. (#4929)
|
||||||
- Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791)
|
- Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791)
|
||||||
|
|||||||
@@ -221,6 +221,13 @@ public:
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ILinkResolver *getLinkResolver() override
|
||||||
|
{
|
||||||
|
assert(false && "EmptyApplication::getLinkResolver was called without "
|
||||||
|
"being initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Paths paths_;
|
Paths paths_;
|
||||||
Args args_;
|
Args args_;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "controllers/sound/ISoundController.hpp"
|
#include "controllers/sound/ISoundController.hpp"
|
||||||
#include "providers/bttv/BttvEmotes.hpp"
|
#include "providers/bttv/BttvEmotes.hpp"
|
||||||
#include "providers/ffz/FfzEmotes.hpp"
|
#include "providers/ffz/FfzEmotes.hpp"
|
||||||
|
#include "providers/links/LinkResolver.hpp"
|
||||||
#include "providers/seventv/SeventvAPI.hpp"
|
#include "providers/seventv/SeventvAPI.hpp"
|
||||||
#include "providers/seventv/SeventvEmotes.hpp"
|
#include "providers/seventv/SeventvEmotes.hpp"
|
||||||
#include "providers/twitch/TwitchBadges.hpp"
|
#include "providers/twitch/TwitchBadges.hpp"
|
||||||
@@ -142,6 +143,7 @@ Application::Application(Settings &_settings, const Paths &paths,
|
|||||||
, ffzEmotes(new FfzEmotes)
|
, ffzEmotes(new FfzEmotes)
|
||||||
, seventvEmotes(new SeventvEmotes)
|
, seventvEmotes(new SeventvEmotes)
|
||||||
, logging(new Logging(_settings))
|
, logging(new Logging(_settings))
|
||||||
|
, linkResolver(new LinkResolver)
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
, plugins(&this->emplace(new PluginController(paths)))
|
, plugins(&this->emplace(new PluginController(paths)))
|
||||||
#endif
|
#endif
|
||||||
@@ -494,6 +496,13 @@ Logging *Application::getChatLogger()
|
|||||||
return this->logging.get();
|
return this->logging.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ILinkResolver *Application::getLinkResolver()
|
||||||
|
{
|
||||||
|
assertInGuiThread();
|
||||||
|
|
||||||
|
return this->linkResolver.get();
|
||||||
|
}
|
||||||
|
|
||||||
BttvEmotes *Application::getBttvEmotes()
|
BttvEmotes *Application::getBttvEmotes()
|
||||||
{
|
{
|
||||||
assertInGuiThread();
|
assertInGuiThread();
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class CrashHandler;
|
|||||||
class BttvEmotes;
|
class BttvEmotes;
|
||||||
class FfzEmotes;
|
class FfzEmotes;
|
||||||
class SeventvEmotes;
|
class SeventvEmotes;
|
||||||
|
class ILinkResolver;
|
||||||
|
|
||||||
class IApplication
|
class IApplication
|
||||||
{
|
{
|
||||||
@@ -95,6 +96,7 @@ public:
|
|||||||
virtual BttvEmotes *getBttvEmotes() = 0;
|
virtual BttvEmotes *getBttvEmotes() = 0;
|
||||||
virtual FfzEmotes *getFfzEmotes() = 0;
|
virtual FfzEmotes *getFfzEmotes() = 0;
|
||||||
virtual SeventvEmotes *getSeventvEmotes() = 0;
|
virtual SeventvEmotes *getSeventvEmotes() = 0;
|
||||||
|
virtual ILinkResolver *getLinkResolver() = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Application : public IApplication
|
class Application : public IApplication
|
||||||
@@ -162,6 +164,7 @@ private:
|
|||||||
std::unique_ptr<FfzEmotes> ffzEmotes;
|
std::unique_ptr<FfzEmotes> ffzEmotes;
|
||||||
std::unique_ptr<SeventvEmotes> seventvEmotes;
|
std::unique_ptr<SeventvEmotes> seventvEmotes;
|
||||||
const std::unique_ptr<Logging> logging;
|
const std::unique_ptr<Logging> logging;
|
||||||
|
std::unique_ptr<ILinkResolver> linkResolver;
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
PluginController *const plugins{};
|
PluginController *const plugins{};
|
||||||
#endif
|
#endif
|
||||||
@@ -212,6 +215,8 @@ public:
|
|||||||
FfzEmotes *getFfzEmotes() override;
|
FfzEmotes *getFfzEmotes() override;
|
||||||
SeventvEmotes *getSeventvEmotes() override;
|
SeventvEmotes *getSeventvEmotes() override;
|
||||||
|
|
||||||
|
ILinkResolver *getLinkResolver() override;
|
||||||
|
|
||||||
pajlada::Signals::NoArgSignal streamerModeChanged;
|
pajlada::Signals::NoArgSignal streamerModeChanged;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
+5
-2
@@ -297,8 +297,6 @@ set(SOURCE_FILES
|
|||||||
|
|
||||||
providers/IvrApi.cpp
|
providers/IvrApi.cpp
|
||||||
providers/IvrApi.hpp
|
providers/IvrApi.hpp
|
||||||
providers/LinkResolver.cpp
|
|
||||||
providers/LinkResolver.hpp
|
|
||||||
providers/NetworkConfigurationProvider.cpp
|
providers/NetworkConfigurationProvider.cpp
|
||||||
providers/NetworkConfigurationProvider.hpp
|
providers/NetworkConfigurationProvider.hpp
|
||||||
|
|
||||||
@@ -345,6 +343,11 @@ set(SOURCE_FILES
|
|||||||
providers/irc/IrcServer.cpp
|
providers/irc/IrcServer.cpp
|
||||||
providers/irc/IrcServer.hpp
|
providers/irc/IrcServer.hpp
|
||||||
|
|
||||||
|
providers/links/LinkInfo.cpp
|
||||||
|
providers/links/LinkInfo.hpp
|
||||||
|
providers/links/LinkResolver.cpp
|
||||||
|
providers/links/LinkResolver.hpp
|
||||||
|
|
||||||
providers/liveupdates/BasicPubSubClient.hpp
|
providers/liveupdates/BasicPubSubClient.hpp
|
||||||
providers/liveupdates/BasicPubSubManager.hpp
|
providers/liveupdates/BasicPubSubManager.hpp
|
||||||
providers/liveupdates/BasicPubSubWebsocket.hpp
|
providers/liveupdates/BasicPubSubWebsocket.hpp
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
#include "messages/Message.hpp"
|
#include "messages/Message.hpp"
|
||||||
#include "messages/MessageColor.hpp"
|
#include "messages/MessageColor.hpp"
|
||||||
#include "messages/MessageElement.hpp"
|
#include "messages/MessageElement.hpp"
|
||||||
#include "providers/LinkResolver.hpp"
|
#include "providers/links/LinkResolver.hpp"
|
||||||
#include "providers/twitch/PubSubActions.hpp"
|
#include "providers/twitch/PubSubActions.hpp"
|
||||||
#include "providers/twitch/TwitchAccount.hpp"
|
#include "providers/twitch/TwitchAccount.hpp"
|
||||||
#include "singletons/Emotes.hpp"
|
#include "singletons/Emotes.hpp"
|
||||||
@@ -528,12 +528,12 @@ MessageBuilder::MessageBuilder(ImageUploaderResultTag /*unused*/,
|
|||||||
this->emplace<TimestampElement>();
|
this->emplace<TimestampElement>();
|
||||||
|
|
||||||
using MEF = MessageElementFlag;
|
using MEF = MessageElementFlag;
|
||||||
auto addText = [this](QString text, MessageElementFlags mefs = MEF::Text,
|
auto addText = [this](QString text,
|
||||||
MessageColor color =
|
MessageColor color =
|
||||||
MessageColor::System) -> TextElement * {
|
MessageColor::System) -> TextElement * {
|
||||||
this->message().searchText += text;
|
this->message().searchText += text;
|
||||||
this->message().messageText += text;
|
this->message().messageText += text;
|
||||||
return this->emplace<TextElement>(text, mefs, color);
|
return this->emplace<TextElement>(text, MEF::Text, color);
|
||||||
};
|
};
|
||||||
|
|
||||||
addText("Your image has been uploaded to");
|
addText("Your image has been uploaded to");
|
||||||
@@ -541,16 +541,14 @@ MessageBuilder::MessageBuilder(ImageUploaderResultTag /*unused*/,
|
|||||||
// ASSUMPTION: the user gave this uploader configuration to the program
|
// ASSUMPTION: the user gave this uploader configuration to the program
|
||||||
// therefore they trust that the host is not wrong/malicious. This doesn't obey getSettings()->lowercaseDomains.
|
// therefore they trust that the host is not wrong/malicious. This doesn't obey getSettings()->lowercaseDomains.
|
||||||
// This also ensures that the LinkResolver doesn't get these links.
|
// This also ensures that the LinkResolver doesn't get these links.
|
||||||
addText(imageLink, {MEF::OriginalLink, MEF::LowercaseLink},
|
addText(imageLink, MessageColor::Link)
|
||||||
MessageColor::Link)
|
|
||||||
->setLink({Link::Url, imageLink})
|
->setLink({Link::Url, imageLink})
|
||||||
->setTrailingSpace(false);
|
->setTrailingSpace(false);
|
||||||
|
|
||||||
if (!deletionLink.isEmpty())
|
if (!deletionLink.isEmpty())
|
||||||
{
|
{
|
||||||
addText("(Deletion link:");
|
addText("(Deletion link:");
|
||||||
addText(deletionLink, {MEF::OriginalLink, MEF::LowercaseLink},
|
addText(deletionLink, MessageColor::Link)
|
||||||
MessageColor::Link)
|
|
||||||
->setLink({Link::Url, deletionLink})
|
->setLink({Link::Url, deletionLink})
|
||||||
->setTrailingSpace(false);
|
->setTrailingSpace(false);
|
||||||
addText(")")->setTrailingSpace(false);
|
addText(")")->setTrailingSpace(false);
|
||||||
@@ -634,46 +632,13 @@ void MessageBuilder::addLink(const ParsedLink &parsedLink)
|
|||||||
lowercaseLinkString += parsedLink.host.toString().toLower();
|
lowercaseLinkString += parsedLink.host.toString().toLower();
|
||||||
lowercaseLinkString += parsedLink.rest;
|
lowercaseLinkString += parsedLink.rest;
|
||||||
|
|
||||||
auto linkElement = Link(Link::Url, matchedLink);
|
|
||||||
|
|
||||||
auto textColor = MessageColor(MessageColor::Link);
|
auto textColor = MessageColor(MessageColor::Link);
|
||||||
auto *linkMELowercase =
|
auto *el = this->emplace<LinkElement>(
|
||||||
this->emplace<TextElement>(lowercaseLinkString,
|
LinkElement::Parsed{.lowercase = lowercaseLinkString,
|
||||||
MessageElementFlag::LowercaseLink, textColor)
|
.original = matchedLink},
|
||||||
->setLink(linkElement);
|
MessageElementFlag::Text, textColor);
|
||||||
auto *linkMEOriginal =
|
el->setLink({Link::Url, matchedLink});
|
||||||
this->emplace<TextElement>(origLink, MessageElementFlag::OriginalLink,
|
getIApp()->getLinkResolver()->resolve(el->linkInfo());
|
||||||
textColor)
|
|
||||||
->setLink(linkElement);
|
|
||||||
|
|
||||||
LinkResolver::getLinkInfo(
|
|
||||||
matchedLink, nullptr,
|
|
||||||
[weakMessage = this->weakOf(), linkMELowercase, linkMEOriginal,
|
|
||||||
matchedLink](QString tooltipText, Link originalLink,
|
|
||||||
ImagePtr thumbnail) {
|
|
||||||
auto shared = weakMessage.lock();
|
|
||||||
if (!shared)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!tooltipText.isEmpty())
|
|
||||||
{
|
|
||||||
linkMELowercase->setTooltip(tooltipText);
|
|
||||||
linkMEOriginal->setTooltip(tooltipText);
|
|
||||||
}
|
|
||||||
if (originalLink.value != matchedLink &&
|
|
||||||
!originalLink.value.isEmpty())
|
|
||||||
{
|
|
||||||
linkMELowercase->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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MessageBuilder::addIrcMessageText(const QString &text)
|
void MessageBuilder::addIrcMessageText(const QString &text)
|
||||||
|
|||||||
@@ -63,18 +63,6 @@ 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;
|
||||||
@@ -86,17 +74,7 @@ const QString &MessageElement::getTooltip() const
|
|||||||
return this->tooltip_;
|
return this->tooltip_;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImagePtr &MessageElement::getThumbnail() const
|
Link MessageElement::getLink() const
|
||||||
{
|
|
||||||
return this->thumbnail_;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MessageElement::ThumbnailType &MessageElement::getThumbnailType() const
|
|
||||||
{
|
|
||||||
return this->thumbnailType_;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Link &MessageElement::getLink() const
|
|
||||||
{
|
{
|
||||||
return this->link_;
|
return this->link_;
|
||||||
}
|
}
|
||||||
@@ -116,12 +94,6 @@ void MessageElement::addFlags(MessageElementFlags flags)
|
|||||||
this->flags_.set(flags);
|
this->flags_.set(flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageElement *MessageElement::updateLink()
|
|
||||||
{
|
|
||||||
this->linkChanged.invoke();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empty
|
// Empty
|
||||||
EmptyElement::EmptyElement()
|
EmptyElement::EmptyElement()
|
||||||
: MessageElement(MessageElementFlag::None)
|
: MessageElement(MessageElementFlag::None)
|
||||||
@@ -155,8 +127,8 @@ void ImageElement::addToContainer(MessageLayoutContainer &container,
|
|||||||
auto size = QSize(this->image_->width() * container.getScale(),
|
auto size = QSize(this->image_->width() * container.getScale(),
|
||||||
this->image_->height() * container.getScale());
|
this->image_->height() * container.getScale());
|
||||||
|
|
||||||
container.addElement((new ImageLayoutElement(*this, this->image_, size))
|
container.addElement(
|
||||||
->setLink(this->getLink()));
|
(new ImageLayoutElement(*this, this->image_, size)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,10 +150,8 @@ void CircularImageElement::addToContainer(MessageLayoutContainer &container,
|
|||||||
auto imgSize = QSize(this->image_->width(), this->image_->height()) *
|
auto imgSize = QSize(this->image_->width(), this->image_->height()) *
|
||||||
container.getScale();
|
container.getScale();
|
||||||
|
|
||||||
container.addElement((new ImageWithCircleBackgroundLayoutElement(
|
container.addElement(new ImageWithCircleBackgroundLayoutElement(
|
||||||
*this, this->image_, imgSize,
|
*this, this->image_, imgSize, this->background_, this->padding_));
|
||||||
this->background_, this->padding_))
|
|
||||||
->setLink(this->getLink()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,8 +192,7 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container,
|
|||||||
QSize(int(container.getScale() * image->width() * emoteScale),
|
QSize(int(container.getScale() * image->width() * emoteScale),
|
||||||
int(container.getScale() * image->height() * emoteScale));
|
int(container.getScale() * image->height() * emoteScale));
|
||||||
|
|
||||||
container.addElement(this->makeImageLayoutElement(image, size)
|
container.addElement(this->makeImageLayoutElement(image, size));
|
||||||
->setLink(this->getLink()));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -284,8 +253,7 @@ void LayeredEmoteElement::addToContainer(MessageLayoutContainer &container,
|
|||||||
}
|
}
|
||||||
|
|
||||||
container.addElement(this->makeImageLayoutElement(
|
container.addElement(this->makeImageLayoutElement(
|
||||||
images, individualSizes, largestSize)
|
images, individualSizes, largestSize));
|
||||||
->setLink(this->getLink()));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -441,8 +409,7 @@ EmotePtr BadgeElement::getEmote() const
|
|||||||
MessageLayoutElement *BadgeElement::makeImageLayoutElement(
|
MessageLayoutElement *BadgeElement::makeImageLayoutElement(
|
||||||
const ImagePtr &image, const QSize &size)
|
const ImagePtr &image, const QSize &size)
|
||||||
{
|
{
|
||||||
auto *element =
|
auto *element = new ImageLayoutElement(*this, image, size);
|
||||||
(new ImageLayoutElement(*this, image, size))->setLink(this->getLink());
|
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
@@ -459,9 +426,8 @@ MessageLayoutElement *ModBadgeElement::makeImageLayoutElement(
|
|||||||
{
|
{
|
||||||
static const QColor modBadgeBackgroundColor("#34AE0A");
|
static const QColor modBadgeBackgroundColor("#34AE0A");
|
||||||
|
|
||||||
auto *element = (new ImageWithBackgroundLayoutElement(
|
auto *element = new ImageWithBackgroundLayoutElement(
|
||||||
*this, image, size, modBadgeBackgroundColor))
|
*this, image, size, modBadgeBackgroundColor);
|
||||||
->setLink(this->getLink());
|
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
@@ -476,8 +442,7 @@ VipBadgeElement::VipBadgeElement(const EmotePtr &data,
|
|||||||
MessageLayoutElement *VipBadgeElement::makeImageLayoutElement(
|
MessageLayoutElement *VipBadgeElement::makeImageLayoutElement(
|
||||||
const ImagePtr &image, const QSize &size)
|
const ImagePtr &image, const QSize &size)
|
||||||
{
|
{
|
||||||
auto *element =
|
auto *element = new ImageLayoutElement(*this, image, size);
|
||||||
(new ImageLayoutElement(*this, image, size))->setLink(this->getLink());
|
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
@@ -494,8 +459,7 @@ MessageLayoutElement *FfzBadgeElement::makeImageLayoutElement(
|
|||||||
const ImagePtr &image, const QSize &size)
|
const ImagePtr &image, const QSize &size)
|
||||||
{
|
{
|
||||||
auto *element =
|
auto *element =
|
||||||
(new ImageWithBackgroundLayoutElement(*this, image, size, this->color))
|
new ImageWithBackgroundLayoutElement(*this, image, size, this->color);
|
||||||
->setLink(this->getLink());
|
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
@@ -507,11 +471,8 @@ TextElement::TextElement(const QString &text, MessageElementFlags flags,
|
|||||||
, color_(color)
|
, color_(color)
|
||||||
, style_(style)
|
, style_(style)
|
||||||
{
|
{
|
||||||
for (const auto &word : text.split(' '))
|
this->words_ = text.split(' ');
|
||||||
{
|
// fourtf: add logic to store multiple spaces after message
|
||||||
this->words_.push_back({word, -1});
|
|
||||||
// fourtf: add logic to store multiple spaces after message
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextElement::addToContainer(MessageLayoutContainer &container,
|
void TextElement::addToContainer(MessageLayoutContainer &container,
|
||||||
@@ -524,39 +485,29 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
|
|||||||
QFontMetrics metrics =
|
QFontMetrics metrics =
|
||||||
app->getFonts()->getFontMetrics(this->style_, container.getScale());
|
app->getFonts()->getFontMetrics(this->style_, container.getScale());
|
||||||
|
|
||||||
for (Word &word : this->words_)
|
for (const auto &word : this->words_)
|
||||||
{
|
{
|
||||||
auto getTextLayoutElement = [&](QString text, int width,
|
auto getTextLayoutElement = [&](QString text, int width,
|
||||||
bool hasTrailingSpace) {
|
bool hasTrailingSpace) {
|
||||||
auto color = this->color_.getColor(*app->getThemes());
|
auto color = this->color_.getColor(*app->getThemes());
|
||||||
app->getThemes()->normalizeColor(color);
|
app->getThemes()->normalizeColor(color);
|
||||||
|
|
||||||
auto *e = (new TextLayoutElement(
|
auto *e = new TextLayoutElement(
|
||||||
*this, text, QSize(width, metrics.height()),
|
*this, text, QSize(width, metrics.height()), color,
|
||||||
color, this->style_, container.getScale()))
|
this->style_, container.getScale());
|
||||||
->setLink(this->getLink());
|
|
||||||
e->setTrailingSpace(hasTrailingSpace);
|
e->setTrailingSpace(hasTrailingSpace);
|
||||||
e->setText(text);
|
e->setText(text);
|
||||||
|
|
||||||
// If URL link was changed,
|
|
||||||
// Should update it in MessageLayoutElement too!
|
|
||||||
if (this->getLink().type == Link::Url)
|
|
||||||
{
|
|
||||||
static_cast<TextLayoutElement *>(e)->listenToLinkChanges();
|
|
||||||
}
|
|
||||||
return e;
|
return e;
|
||||||
};
|
};
|
||||||
|
|
||||||
// fourtf: add again
|
auto width = metrics.horizontalAdvance(word);
|
||||||
// if (word.width == -1) {
|
|
||||||
word.width = metrics.horizontalAdvance(word.text);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// see if the text fits in the current line
|
// see if the text fits in the current line
|
||||||
if (container.fitsInLine(word.width))
|
if (container.fitsInLine(width))
|
||||||
{
|
{
|
||||||
container.addElementNoLineBreak(getTextLayoutElement(
|
container.addElementNoLineBreak(getTextLayoutElement(
|
||||||
word.text, word.width, this->hasTrailingSpace()));
|
word, width, this->hasTrailingSpace()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,35 +516,34 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
|
|||||||
{
|
{
|
||||||
container.breakLine();
|
container.breakLine();
|
||||||
|
|
||||||
if (container.fitsInLine(word.width))
|
if (container.fitsInLine(width))
|
||||||
{
|
{
|
||||||
container.addElementNoLineBreak(getTextLayoutElement(
|
container.addElementNoLineBreak(getTextLayoutElement(
|
||||||
word.text, word.width, this->hasTrailingSpace()));
|
word, width, this->hasTrailingSpace()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we done goofed, we need to wrap the text
|
// we done goofed, we need to wrap the text
|
||||||
QString text = word.text;
|
auto textLength = word.length();
|
||||||
int textLength = text.length();
|
|
||||||
int wordStart = 0;
|
int wordStart = 0;
|
||||||
int width = 0;
|
width = 0;
|
||||||
|
|
||||||
// QChar::isHighSurrogate(text[0].unicode()) ? 2 : 1
|
// QChar::isHighSurrogate(text[0].unicode()) ? 2 : 1
|
||||||
|
|
||||||
for (int i = 0; i < textLength; i++)
|
for (int i = 0; i < textLength; i++)
|
||||||
{
|
{
|
||||||
auto isSurrogate = text.size() > i + 1 &&
|
auto isSurrogate = word.size() > i + 1 &&
|
||||||
QChar::isHighSurrogate(text[i].unicode());
|
QChar::isHighSurrogate(word[i].unicode());
|
||||||
|
|
||||||
auto charWidth = isSurrogate
|
auto charWidth = isSurrogate
|
||||||
? metrics.horizontalAdvance(text.mid(i, 2))
|
? metrics.horizontalAdvance(word.mid(i, 2))
|
||||||
: metrics.horizontalAdvance(text[i]);
|
: metrics.horizontalAdvance(word[i]);
|
||||||
|
|
||||||
if (!container.fitsInLine(width + charWidth))
|
if (!container.fitsInLine(width + charWidth))
|
||||||
{
|
{
|
||||||
container.addElementNoLineBreak(getTextLayoutElement(
|
container.addElementNoLineBreak(getTextLayoutElement(
|
||||||
text.mid(wordStart, i - wordStart), width, false));
|
word.mid(wordStart, i - wordStart), width, false));
|
||||||
container.breakLine();
|
container.breakLine();
|
||||||
|
|
||||||
wordStart = i;
|
wordStart = i;
|
||||||
@@ -615,7 +565,7 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
|
|||||||
}
|
}
|
||||||
//add the final piece of wrapped text
|
//add the final piece of wrapped text
|
||||||
container.addElementNoLineBreak(getTextLayoutElement(
|
container.addElementNoLineBreak(getTextLayoutElement(
|
||||||
text.mid(wordStart), width, this->hasTrailingSpace()));
|
word.mid(wordStart), width, this->hasTrailingSpace()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -649,19 +599,12 @@ void SingleLineTextElement::addToContainer(MessageLayoutContainer &container,
|
|||||||
auto color = this->color_.getColor(*app->getThemes());
|
auto color = this->color_.getColor(*app->getThemes());
|
||||||
app->getThemes()->normalizeColor(color);
|
app->getThemes()->normalizeColor(color);
|
||||||
|
|
||||||
auto *e = (new TextLayoutElement(
|
auto *e = new TextLayoutElement(
|
||||||
*this, text, QSize(width, metrics.height()), color,
|
*this, text, QSize(width, metrics.height()), color,
|
||||||
this->style_, container.getScale()))
|
this->style_, container.getScale());
|
||||||
->setLink(this->getLink());
|
|
||||||
e->setTrailingSpace(hasTrailingSpace);
|
e->setTrailingSpace(hasTrailingSpace);
|
||||||
e->setText(text);
|
e->setText(text);
|
||||||
|
|
||||||
// If URL link was changed,
|
|
||||||
// Should update it in MessageLayoutElement too!
|
|
||||||
if (this->getLink().type == Link::Url)
|
|
||||||
{
|
|
||||||
static_cast<TextLayoutElement *>(e)->listenToLinkChanges();
|
|
||||||
}
|
|
||||||
return e;
|
return e;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -749,6 +692,29 @@ void SingleLineTextElement::addToContainer(MessageLayoutContainer &container,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LinkElement::LinkElement(const Parsed &parsed, MessageElementFlags flags,
|
||||||
|
const MessageColor &color, FontStyle style)
|
||||||
|
: TextElement({}, flags, color, style)
|
||||||
|
, linkInfo_(parsed.original)
|
||||||
|
, lowercase_({parsed.lowercase})
|
||||||
|
, original_({parsed.original})
|
||||||
|
{
|
||||||
|
this->setTooltip(parsed.original);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinkElement::addToContainer(MessageLayoutContainer &container,
|
||||||
|
MessageElementFlags flags)
|
||||||
|
{
|
||||||
|
this->words_ =
|
||||||
|
getSettings()->lowercaseDomains ? this->lowercase_ : this->original_;
|
||||||
|
TextElement::addToContainer(container, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
Link LinkElement::getLink() const
|
||||||
|
{
|
||||||
|
return {Link::Url, this->linkInfo_.url()};
|
||||||
|
}
|
||||||
|
|
||||||
// TIMESTAMP
|
// TIMESTAMP
|
||||||
TimestampElement::TimestampElement(QTime time)
|
TimestampElement::TimestampElement(QTime time)
|
||||||
: MessageElement(MessageElementFlag::Timestamp)
|
: MessageElement(MessageElementFlag::Timestamp)
|
||||||
@@ -853,8 +819,7 @@ void ScalingImageElement::addToContainer(MessageLayoutContainer &container,
|
|||||||
auto size = QSize(image->width() * container.getScale(),
|
auto size = QSize(image->width() * container.getScale(),
|
||||||
image->height() * container.getScale());
|
image->height() * container.getScale());
|
||||||
|
|
||||||
container.addElement((new ImageLayoutElement(*this, image, size))
|
container.addElement(new ImageLayoutElement(*this, image, size));
|
||||||
->setLink(this->getLink()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "messages/ImageSet.hpp"
|
#include "messages/ImageSet.hpp"
|
||||||
#include "messages/Link.hpp"
|
#include "messages/Link.hpp"
|
||||||
#include "messages/MessageColor.hpp"
|
#include "messages/MessageColor.hpp"
|
||||||
|
#include "providers/links/LinkInfo.hpp"
|
||||||
#include "singletons/Fonts.hpp"
|
#include "singletons/Fonts.hpp"
|
||||||
|
|
||||||
#include <pajlada/signals/signalholder.hpp>
|
#include <pajlada/signals/signalholder.hpp>
|
||||||
@@ -136,10 +137,9 @@ enum class MessageElementFlag : int64_t {
|
|||||||
BoldUsername = (1LL << 27),
|
BoldUsername = (1LL << 27),
|
||||||
NonBoldUsername = (1LL << 28),
|
NonBoldUsername = (1LL << 28),
|
||||||
|
|
||||||
// for links
|
// used to check if links should be lowercased
|
||||||
LowercaseLink = (1LL << 29),
|
LowercaseLinks = (1LL << 29),
|
||||||
OriginalLink = (1LL << 30),
|
// Unused = (1LL << 30)
|
||||||
|
|
||||||
// Unused: (1LL << 31)
|
// Unused: (1LL << 31)
|
||||||
|
|
||||||
// for elements of the message reply
|
// for elements of the message reply
|
||||||
@@ -166,9 +166,6 @@ 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();
|
||||||
|
|
||||||
@@ -181,25 +178,18 @@ public:
|
|||||||
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;
|
virtual Link getLink() const;
|
||||||
bool hasTrailingSpace() const;
|
bool hasTrailingSpace() const;
|
||||||
MessageElementFlags getFlags() const;
|
MessageElementFlags getFlags() const;
|
||||||
void addFlags(MessageElementFlags flags);
|
void addFlags(MessageElementFlags flags);
|
||||||
MessageElement *updateLink();
|
|
||||||
|
|
||||||
virtual void addToContainer(MessageLayoutContainer &container,
|
virtual void addToContainer(MessageLayoutContainer &container,
|
||||||
MessageElementFlags flags) = 0;
|
MessageElementFlags flags) = 0;
|
||||||
|
|
||||||
pajlada::Signals::NoArgSignal linkChanged;
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
MessageElement(MessageElementFlags flags);
|
MessageElement(MessageElementFlags flags);
|
||||||
bool trailingSpace = true;
|
bool trailingSpace = true;
|
||||||
@@ -208,8 +198,6 @@ private:
|
|||||||
QString text_;
|
QString text_;
|
||||||
Link link_;
|
Link link_;
|
||||||
QString tooltip_;
|
QString tooltip_;
|
||||||
ImagePtr thumbnail_;
|
|
||||||
ThumbnailType thumbnailType_{};
|
|
||||||
MessageElementFlags flags_;
|
MessageElementFlags flags_;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -269,15 +257,12 @@ public:
|
|||||||
void addToContainer(MessageLayoutContainer &container,
|
void addToContainer(MessageLayoutContainer &container,
|
||||||
MessageElementFlags flags) override;
|
MessageElementFlags flags) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QStringList words_;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
MessageColor color_;
|
MessageColor color_;
|
||||||
FontStyle style_;
|
FontStyle style_;
|
||||||
|
|
||||||
struct Word {
|
|
||||||
QString text;
|
|
||||||
int width = -1;
|
|
||||||
};
|
|
||||||
std::vector<Word> words_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// contains a text that will be truncated to one line
|
// contains a text that will be truncated to one line
|
||||||
@@ -303,6 +288,40 @@ private:
|
|||||||
std::vector<Word> words_;
|
std::vector<Word> words_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class LinkElement : public TextElement
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct Parsed {
|
||||||
|
QString lowercase;
|
||||||
|
QString original;
|
||||||
|
};
|
||||||
|
|
||||||
|
LinkElement(const Parsed &parsed, MessageElementFlags flags,
|
||||||
|
const MessageColor &color = MessageColor::Text,
|
||||||
|
FontStyle style = FontStyle::ChatMedium);
|
||||||
|
~LinkElement() override = default;
|
||||||
|
LinkElement(const LinkElement &) = delete;
|
||||||
|
LinkElement(LinkElement &&) = delete;
|
||||||
|
LinkElement &operator=(const LinkElement &) = delete;
|
||||||
|
LinkElement &operator=(LinkElement &&) = delete;
|
||||||
|
|
||||||
|
void addToContainer(MessageLayoutContainer &container,
|
||||||
|
MessageElementFlags flags) override;
|
||||||
|
|
||||||
|
Link getLink() const override;
|
||||||
|
|
||||||
|
[[nodiscard]] LinkInfo *linkInfo()
|
||||||
|
{
|
||||||
|
return &this->linkInfo_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
LinkInfo linkInfo_;
|
||||||
|
// these are implicitly shared
|
||||||
|
QStringList lowercase_;
|
||||||
|
QStringList original_;
|
||||||
|
};
|
||||||
|
|
||||||
// contains emote data and will pick the emote based on :
|
// contains emote data and will pick the emote based on :
|
||||||
// a) are images for the emote type enabled
|
// a) are images for the emote type enabled
|
||||||
// b) which size it wants
|
// b) which size it wants
|
||||||
|
|||||||
@@ -77,9 +77,9 @@ MessageLayoutElement *MessageLayoutElement::setTrailingSpace(bool value)
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageLayoutElement *MessageLayoutElement::setLink(const Link &_link)
|
MessageLayoutElement *MessageLayoutElement::setLink(const Link &link)
|
||||||
{
|
{
|
||||||
this->link_ = _link;
|
this->link_ = link;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,9 +89,13 @@ MessageLayoutElement *MessageLayoutElement::setText(const QString &_text)
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Link &MessageLayoutElement::getLink() const
|
Link MessageLayoutElement::getLink() const
|
||||||
{
|
{
|
||||||
return this->link_;
|
if (this->link_)
|
||||||
|
{
|
||||||
|
return *this->link_;
|
||||||
|
}
|
||||||
|
return this->creator_.getLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString &MessageLayoutElement::getText() const
|
const QString &MessageLayoutElement::getText() const
|
||||||
@@ -406,14 +410,6 @@ TextLayoutElement::TextLayoutElement(MessageElement &_creator, QString &_text,
|
|||||||
this->setText(_text);
|
this->setText(_text);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextLayoutElement::listenToLinkChanges()
|
|
||||||
{
|
|
||||||
this->managedConnections_.managedConnect(
|
|
||||||
static_cast<TextElement &>(this->getCreator()).linkChanged, [this]() {
|
|
||||||
this->setLink(this->getCreator().getLink());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextLayoutElement::addCopyTextToString(QString &str, uint32_t from,
|
void TextLayoutElement::addCopyTextToString(QString &str, uint32_t from,
|
||||||
uint32_t to) const
|
uint32_t to) const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -44,7 +44,12 @@ public:
|
|||||||
void setLine(size_t line);
|
void setLine(size_t line);
|
||||||
|
|
||||||
MessageLayoutElement *setTrailingSpace(bool value);
|
MessageLayoutElement *setTrailingSpace(bool value);
|
||||||
MessageLayoutElement *setLink(const Link &link_);
|
|
||||||
|
/// @brief Overwrites the link for this layout element
|
||||||
|
///
|
||||||
|
/// @sa #getLink()
|
||||||
|
MessageLayoutElement *setLink(const Link &link);
|
||||||
|
|
||||||
MessageLayoutElement *setText(const QString &text_);
|
MessageLayoutElement *setText(const QString &text_);
|
||||||
|
|
||||||
virtual void addCopyTextToString(QString &str, uint32_t from = 0,
|
virtual void addCopyTextToString(QString &str, uint32_t from = 0,
|
||||||
@@ -57,7 +62,12 @@ public:
|
|||||||
virtual int getMouseOverIndex(const QPoint &abs) const = 0;
|
virtual int getMouseOverIndex(const QPoint &abs) const = 0;
|
||||||
virtual int getXFromIndex(size_t index) = 0;
|
virtual int getXFromIndex(size_t index) = 0;
|
||||||
|
|
||||||
const Link &getLink() const;
|
/// @brief Returns the link this layout element has
|
||||||
|
///
|
||||||
|
/// If there isn't any, an empty link is returned (type: None).
|
||||||
|
/// The link is sourced from the creator, but can be overwritten with
|
||||||
|
/// #setLink().
|
||||||
|
Link getLink() const;
|
||||||
const QString &getText() const;
|
const QString &getText() const;
|
||||||
FlagsEnum<MessageElementFlag> getFlags() const;
|
FlagsEnum<MessageElementFlag> getFlags() const;
|
||||||
|
|
||||||
@@ -67,7 +77,7 @@ protected:
|
|||||||
private:
|
private:
|
||||||
QString text_;
|
QString text_;
|
||||||
QRect rect_;
|
QRect rect_;
|
||||||
Link link_;
|
std::optional<Link> link_;
|
||||||
MessageElement &creator_;
|
MessageElement &creator_;
|
||||||
/**
|
/**
|
||||||
* The line of the container this element is laid out at
|
* The line of the container this element is laid out at
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -185,8 +185,7 @@ void WindowManager::updateWordTypeMask()
|
|||||||
flags.set(MEF::Collapsed);
|
flags.set(MEF::Collapsed);
|
||||||
flags.set(settings->boldUsernames ? MEF::BoldUsername
|
flags.set(settings->boldUsernames ? MEF::BoldUsername
|
||||||
: MEF::NonBoldUsername);
|
: MEF::NonBoldUsername);
|
||||||
flags.set(settings->lowercaseDomains ? MEF::LowercaseLink
|
flags.set(MEF::LowercaseLinks, settings->lowercaseDomains);
|
||||||
: MEF::OriginalLink);
|
|
||||||
flags.set(MEF::ChannelPointReward);
|
flags.set(MEF::ChannelPointReward);
|
||||||
|
|
||||||
// update flags
|
// update flags
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
#include "messages/MessageElement.hpp"
|
#include "messages/MessageElement.hpp"
|
||||||
#include "messages/MessageThread.hpp"
|
#include "messages/MessageThread.hpp"
|
||||||
#include "providers/colors/ColorProvider.hpp"
|
#include "providers/colors/ColorProvider.hpp"
|
||||||
#include "providers/LinkResolver.hpp"
|
#include "providers/links/LinkInfo.hpp"
|
||||||
|
#include "providers/links/LinkResolver.hpp"
|
||||||
#include "providers/twitch/TwitchAccount.hpp"
|
#include "providers/twitch/TwitchAccount.hpp"
|
||||||
#include "providers/twitch/TwitchChannel.hpp"
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||||
@@ -1937,55 +1938,16 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (element->getTooltip() == "No link info loaded")
|
|
||||||
{
|
|
||||||
std::weak_ptr<MessageLayout> weakLayout = layout;
|
|
||||||
LinkResolver::getLinkInfo(
|
|
||||||
element->getLink().value, nullptr,
|
|
||||||
[weakLayout, element](QString tooltipText,
|
|
||||||
Link originalLink,
|
|
||||||
ImagePtr thumbnail) {
|
|
||||||
auto shared = weakLayout.lock();
|
|
||||||
if (!shared)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
element->setTooltip(tooltipText);
|
|
||||||
element->setThumbnail(thumbnail);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
auto thumbnailSize = getSettings()->thumbnailSize;
|
auto thumbnailSize = getSettings()->thumbnailSize;
|
||||||
if (thumbnailSize == 0)
|
auto *linkElement = dynamic_cast<LinkElement *>(element);
|
||||||
|
if (linkElement)
|
||||||
{
|
{
|
||||||
// "Show thumbnails" is set to "Off", show text only
|
if (linkElement->linkInfo()->isPending())
|
||||||
this->tooltipWidget_->setOne({nullptr, element->getTooltip()});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
const auto shouldHideThumbnail =
|
|
||||||
isInStreamerMode() &&
|
|
||||||
getSettings()->streamerModeHideLinkThumbnails &&
|
|
||||||
element->getThumbnail() != nullptr &&
|
|
||||||
!element->getThumbnail()->url().string.isEmpty();
|
|
||||||
auto thumb =
|
|
||||||
shouldHideThumbnail
|
|
||||||
? Image::fromResourcePixmap(getResources().streamerMode)
|
|
||||||
: element->getThumbnail();
|
|
||||||
|
|
||||||
if (element->getThumbnailType() ==
|
|
||||||
MessageElement::ThumbnailType::Link_Thumbnail)
|
|
||||||
{
|
{
|
||||||
this->tooltipWidget_->setOne({
|
getIApp()->getLinkResolver()->resolve(
|
||||||
std::move(thumb),
|
linkElement->linkInfo());
|
||||||
element->getTooltip(),
|
|
||||||
thumbnailSize,
|
|
||||||
thumbnailSize,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this->tooltipWidget_->setOne({std::move(thumb), ""});
|
|
||||||
}
|
}
|
||||||
|
this->setLinkInfoTooltip(linkElement->linkInfo());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3103,4 +3065,62 @@ bool ChannelView::canReplyToMessages() const
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChannelView::setLinkInfoTooltip(LinkInfo *info)
|
||||||
|
{
|
||||||
|
assert(info);
|
||||||
|
|
||||||
|
auto thumbnailSize = getSettings()->thumbnailSize;
|
||||||
|
|
||||||
|
ImagePtr thumbnail;
|
||||||
|
if (info->hasThumbnail() && thumbnailSize > 0)
|
||||||
|
{
|
||||||
|
if (isInStreamerMode() && getSettings()->streamerModeHideLinkThumbnails)
|
||||||
|
{
|
||||||
|
thumbnail = Image::fromResourcePixmap(getResources().streamerMode);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
thumbnail = info->thumbnail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this->tooltipWidget_->setOne({
|
||||||
|
.image = thumbnail,
|
||||||
|
.text = info->tooltip(),
|
||||||
|
.customWidth = thumbnailSize,
|
||||||
|
.customHeight = thumbnailSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (info->isLoaded())
|
||||||
|
{
|
||||||
|
this->pendingLinkInfo_.clear();
|
||||||
|
return; // Either resolved or errored (can't change anymore)
|
||||||
|
}
|
||||||
|
|
||||||
|
// listen to changes
|
||||||
|
|
||||||
|
if (this->pendingLinkInfo_.data() == info)
|
||||||
|
{
|
||||||
|
return; // same info - already registered
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->pendingLinkInfo_)
|
||||||
|
{
|
||||||
|
QObject::disconnect(this->pendingLinkInfo_.data(),
|
||||||
|
&LinkInfo::stateChanged, this, nullptr);
|
||||||
|
}
|
||||||
|
QObject::connect(info, &LinkInfo::stateChanged, this,
|
||||||
|
&ChannelView::pendingLinkInfoStateChanged);
|
||||||
|
this->pendingLinkInfo_ = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChannelView::pendingLinkInfoStateChanged()
|
||||||
|
{
|
||||||
|
if (!this->pendingLinkInfo_)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->setLinkInfoTooltip(this->pendingLinkInfo_.data());
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <pajlada/signals/signal.hpp>
|
#include <pajlada/signals/signal.hpp>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QPaintEvent>
|
#include <QPaintEvent>
|
||||||
|
#include <QPointer>
|
||||||
#include <QScroller>
|
#include <QScroller>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QVariantAnimation>
|
#include <QVariantAnimation>
|
||||||
@@ -47,6 +48,8 @@ class Split;
|
|||||||
class FilterSet;
|
class FilterSet;
|
||||||
using FilterSetPtr = std::shared_ptr<FilterSet>;
|
using FilterSetPtr = std::shared_ptr<FilterSet>;
|
||||||
|
|
||||||
|
class LinkInfo;
|
||||||
|
|
||||||
enum class PauseReason {
|
enum class PauseReason {
|
||||||
Mouse,
|
Mouse,
|
||||||
Selection,
|
Selection,
|
||||||
@@ -366,6 +369,17 @@ private:
|
|||||||
void scrollUpdateRequested();
|
void scrollUpdateRequested();
|
||||||
|
|
||||||
TooltipWidget *const tooltipWidget_{};
|
TooltipWidget *const tooltipWidget_{};
|
||||||
|
|
||||||
|
/// Pointer to a link info that hasn't loaded yet
|
||||||
|
QPointer<LinkInfo> pendingLinkInfo_;
|
||||||
|
|
||||||
|
/// @brief Sets the tooltip to contain the link info
|
||||||
|
///
|
||||||
|
/// If the info isn't loaded yet, it's tracked until it's resolved or errored.
|
||||||
|
void setLinkInfoTooltip(LinkInfo *info);
|
||||||
|
|
||||||
|
/// Slot for the LinkInfo::stateChanged signal.
|
||||||
|
void pendingLinkInfoStateChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ set(test_SOURCES
|
|||||||
${CMAKE_CURRENT_LIST_DIR}/src/Selection.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/Selection.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/NotebookTab.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/NotebookTab.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/SplitInput.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/SplitInput.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/src/LinkInfo.cpp
|
||||||
# Add your new file above this line!
|
# Add your new file above this line!
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#include "providers/links/LinkInfo.hpp"
|
||||||
|
|
||||||
|
#include "common/Literals.hpp"
|
||||||
|
#include "SignalSpy.hpp"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
using namespace literals;
|
||||||
|
|
||||||
|
using State = LinkInfo::State;
|
||||||
|
|
||||||
|
TEST(LinkInfo, initialState)
|
||||||
|
{
|
||||||
|
LinkInfo info(u"https://chatterino.com"_s);
|
||||||
|
ASSERT_EQ(info.url(), u"https://chatterino.com"_s);
|
||||||
|
ASSERT_EQ(info.originalUrl(), u"https://chatterino.com"_s);
|
||||||
|
ASSERT_EQ(info.state(), State::Created);
|
||||||
|
ASSERT_FALSE(info.hasThumbnail());
|
||||||
|
ASSERT_EQ(info.tooltip(), u"https://chatterino.com"_s);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(LinkInfo, states)
|
||||||
|
{
|
||||||
|
LinkInfo info(u"https://chatterino.com"_s);
|
||||||
|
SignalSpy<State> spy(&info, &LinkInfo::stateChanged);
|
||||||
|
|
||||||
|
info.setState(State::Created);
|
||||||
|
ASSERT_TRUE(spy.empty());
|
||||||
|
ASSERT_TRUE(info.isPending());
|
||||||
|
ASSERT_FALSE(info.isLoading());
|
||||||
|
ASSERT_FALSE(info.isLoaded());
|
||||||
|
ASSERT_FALSE(info.isResolved());
|
||||||
|
ASSERT_FALSE(info.hasError());
|
||||||
|
|
||||||
|
info.setState(State::Loading);
|
||||||
|
ASSERT_EQ(spy.size(), 1);
|
||||||
|
ASSERT_EQ(spy.last(), State::Loading);
|
||||||
|
ASSERT_EQ(info.state(), State::Loading);
|
||||||
|
ASSERT_FALSE(info.isPending());
|
||||||
|
ASSERT_TRUE(info.isLoading());
|
||||||
|
ASSERT_FALSE(info.isLoaded());
|
||||||
|
ASSERT_FALSE(info.isResolved());
|
||||||
|
ASSERT_FALSE(info.hasError());
|
||||||
|
|
||||||
|
info.setState(State::Loading);
|
||||||
|
ASSERT_EQ(spy.size(), 1);
|
||||||
|
|
||||||
|
info.setState(State::Resolved);
|
||||||
|
ASSERT_EQ(spy.size(), 2);
|
||||||
|
ASSERT_EQ(spy.last(), State::Resolved);
|
||||||
|
ASSERT_EQ(info.state(), State::Resolved);
|
||||||
|
ASSERT_FALSE(info.isPending());
|
||||||
|
ASSERT_FALSE(info.isLoading());
|
||||||
|
ASSERT_TRUE(info.isLoaded());
|
||||||
|
ASSERT_TRUE(info.isResolved());
|
||||||
|
ASSERT_FALSE(info.hasError());
|
||||||
|
|
||||||
|
info.setState(State::Errored);
|
||||||
|
ASSERT_EQ(spy.size(), 3);
|
||||||
|
ASSERT_EQ(spy.last(), State::Errored);
|
||||||
|
ASSERT_EQ(info.state(), State::Errored);
|
||||||
|
ASSERT_FALSE(info.isPending());
|
||||||
|
ASSERT_FALSE(info.isLoading());
|
||||||
|
ASSERT_TRUE(info.isLoaded());
|
||||||
|
ASSERT_FALSE(info.isResolved());
|
||||||
|
ASSERT_TRUE(info.hasError());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(LinkInfo, setters)
|
||||||
|
{
|
||||||
|
LinkInfo info(u"https://chatterino.com"_s);
|
||||||
|
SignalSpy<State> spy(&info, &LinkInfo::stateChanged);
|
||||||
|
|
||||||
|
ASSERT_EQ(info.url(), u"https://chatterino.com"_s);
|
||||||
|
ASSERT_EQ(info.originalUrl(), u"https://chatterino.com"_s);
|
||||||
|
ASSERT_FALSE(info.hasThumbnail());
|
||||||
|
ASSERT_EQ(info.tooltip(), u"https://chatterino.com"_s);
|
||||||
|
|
||||||
|
info.setTooltip(u"tooltip"_s);
|
||||||
|
ASSERT_EQ(info.tooltip(), u"tooltip"_s);
|
||||||
|
|
||||||
|
info.setResolvedUrl(u"https://www.chatterino.com"_s);
|
||||||
|
ASSERT_EQ(info.url(), u"https://www.chatterino.com"_s);
|
||||||
|
ASSERT_EQ(info.originalUrl(), u"https://chatterino.com"_s);
|
||||||
|
|
||||||
|
info.setThumbnail(nullptr);
|
||||||
|
ASSERT_FALSE(info.hasThumbnail());
|
||||||
|
|
||||||
|
info.setThumbnail(Image::getEmpty());
|
||||||
|
ASSERT_FALSE(info.hasThumbnail());
|
||||||
|
|
||||||
|
info.setThumbnail(Image::fromUrl(Url{""}));
|
||||||
|
ASSERT_FALSE(info.hasThumbnail());
|
||||||
|
|
||||||
|
auto image = Image::fromUrl(Url{"https://www.chatterino.com"});
|
||||||
|
info.setThumbnail(image);
|
||||||
|
ASSERT_TRUE(info.hasThumbnail());
|
||||||
|
ASSERT_EQ(info.thumbnail(), image);
|
||||||
|
|
||||||
|
ASSERT_TRUE(spy.empty());
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include <tuple>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
template <typename... Args>
|
||||||
|
class SignalSpy : public QObject
|
||||||
|
{
|
||||||
|
// Simplify the storage if there's only one argument to the signal handler.
|
||||||
|
using Call =
|
||||||
|
std::conditional_t<sizeof...(Args) == 1,
|
||||||
|
std::tuple_element_t<0, std::tuple<Args...>>,
|
||||||
|
std::tuple<Args...>>;
|
||||||
|
|
||||||
|
public:
|
||||||
|
SignalSpy(auto *sender, auto method)
|
||||||
|
{
|
||||||
|
auto conn =
|
||||||
|
QObject::connect(sender, method, this, &SignalSpy<Args...>::slot,
|
||||||
|
Qt::DirectConnection);
|
||||||
|
assert(conn && "Failed to connect");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool empty() const
|
||||||
|
{
|
||||||
|
return this->calls_.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t size() const
|
||||||
|
{
|
||||||
|
return this->calls_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<std::tuple<Args...>> &calls() const
|
||||||
|
{
|
||||||
|
return this->calls_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear()
|
||||||
|
{
|
||||||
|
this->calls_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
const Call &last() const
|
||||||
|
{
|
||||||
|
assert(!this->calls_.empty());
|
||||||
|
|
||||||
|
return this->calls_.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void slot(Args... args)
|
||||||
|
{
|
||||||
|
this->calls_.emplace_back(std::move(args)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Call> calls_;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user