mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
refactor: Turn link-info into its own element and class (#5178)
This commit is contained in:
parent
42e4559910
commit
e130c48f76
|
@ -83,6 +83,7 @@
|
|||
- 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: 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: Change clang-format from v14 to v16. (#4929)
|
||||
- Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791)
|
||||
|
|
|
@ -221,6 +221,13 @@ public:
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
ILinkResolver *getLinkResolver() override
|
||||
{
|
||||
assert(false && "EmptyApplication::getLinkResolver was called without "
|
||||
"being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
Paths paths_;
|
||||
Args args_;
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
#include "controllers/sound/ISoundController.hpp"
|
||||
#include "providers/bttv/BttvEmotes.hpp"
|
||||
#include "providers/ffz/FfzEmotes.hpp"
|
||||
#include "providers/links/LinkResolver.hpp"
|
||||
#include "providers/seventv/SeventvAPI.hpp"
|
||||
#include "providers/seventv/SeventvEmotes.hpp"
|
||||
#include "providers/twitch/TwitchBadges.hpp"
|
||||
|
@ -142,6 +143,7 @@ Application::Application(Settings &_settings, const Paths &paths,
|
|||
, ffzEmotes(new FfzEmotes)
|
||||
, seventvEmotes(new SeventvEmotes)
|
||||
, logging(new Logging(_settings))
|
||||
, linkResolver(new LinkResolver)
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
, plugins(&this->emplace(new PluginController(paths)))
|
||||
#endif
|
||||
|
@ -494,6 +496,13 @@ Logging *Application::getChatLogger()
|
|||
return this->logging.get();
|
||||
}
|
||||
|
||||
ILinkResolver *Application::getLinkResolver()
|
||||
{
|
||||
assertInGuiThread();
|
||||
|
||||
return this->linkResolver.get();
|
||||
}
|
||||
|
||||
BttvEmotes *Application::getBttvEmotes()
|
||||
{
|
||||
assertInGuiThread();
|
||||
|
|
|
@ -54,6 +54,7 @@ class CrashHandler;
|
|||
class BttvEmotes;
|
||||
class FfzEmotes;
|
||||
class SeventvEmotes;
|
||||
class ILinkResolver;
|
||||
|
||||
class IApplication
|
||||
{
|
||||
|
@ -95,6 +96,7 @@ public:
|
|||
virtual BttvEmotes *getBttvEmotes() = 0;
|
||||
virtual FfzEmotes *getFfzEmotes() = 0;
|
||||
virtual SeventvEmotes *getSeventvEmotes() = 0;
|
||||
virtual ILinkResolver *getLinkResolver() = 0;
|
||||
};
|
||||
|
||||
class Application : public IApplication
|
||||
|
@ -162,6 +164,7 @@ private:
|
|||
std::unique_ptr<FfzEmotes> ffzEmotes;
|
||||
std::unique_ptr<SeventvEmotes> seventvEmotes;
|
||||
const std::unique_ptr<Logging> logging;
|
||||
std::unique_ptr<ILinkResolver> linkResolver;
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
PluginController *const plugins{};
|
||||
#endif
|
||||
|
@ -212,6 +215,8 @@ public:
|
|||
FfzEmotes *getFfzEmotes() override;
|
||||
SeventvEmotes *getSeventvEmotes() override;
|
||||
|
||||
ILinkResolver *getLinkResolver() override;
|
||||
|
||||
pajlada::Signals::NoArgSignal streamerModeChanged;
|
||||
|
||||
private:
|
||||
|
|
|
@ -297,8 +297,6 @@ set(SOURCE_FILES
|
|||
|
||||
providers/IvrApi.cpp
|
||||
providers/IvrApi.hpp
|
||||
providers/LinkResolver.cpp
|
||||
providers/LinkResolver.hpp
|
||||
providers/NetworkConfigurationProvider.cpp
|
||||
providers/NetworkConfigurationProvider.hpp
|
||||
|
||||
|
@ -345,6 +343,11 @@ set(SOURCE_FILES
|
|||
providers/irc/IrcServer.cpp
|
||||
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/BasicPubSubManager.hpp
|
||||
providers/liveupdates/BasicPubSubWebsocket.hpp
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageColor.hpp"
|
||||
#include "messages/MessageElement.hpp"
|
||||
#include "providers/LinkResolver.hpp"
|
||||
#include "providers/links/LinkResolver.hpp"
|
||||
#include "providers/twitch/PubSubActions.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
|
@ -528,12 +528,12 @@ MessageBuilder::MessageBuilder(ImageUploaderResultTag /*unused*/,
|
|||
this->emplace<TimestampElement>();
|
||||
|
||||
using MEF = MessageElementFlag;
|
||||
auto addText = [this](QString text, MessageElementFlags mefs = MEF::Text,
|
||||
auto addText = [this](QString text,
|
||||
MessageColor color =
|
||||
MessageColor::System) -> TextElement * {
|
||||
this->message().searchText += 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");
|
||||
|
@ -541,16 +541,14 @@ MessageBuilder::MessageBuilder(ImageUploaderResultTag /*unused*/,
|
|||
// 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.
|
||||
// This also ensures that the LinkResolver doesn't get these links.
|
||||
addText(imageLink, {MEF::OriginalLink, MEF::LowercaseLink},
|
||||
MessageColor::Link)
|
||||
addText(imageLink, MessageColor::Link)
|
||||
->setLink({Link::Url, imageLink})
|
||||
->setTrailingSpace(false);
|
||||
|
||||
if (!deletionLink.isEmpty())
|
||||
{
|
||||
addText("(Deletion link:");
|
||||
addText(deletionLink, {MEF::OriginalLink, MEF::LowercaseLink},
|
||||
MessageColor::Link)
|
||||
addText(deletionLink, MessageColor::Link)
|
||||
->setLink({Link::Url, deletionLink})
|
||||
->setTrailingSpace(false);
|
||||
addText(")")->setTrailingSpace(false);
|
||||
|
@ -634,46 +632,13 @@ void MessageBuilder::addLink(const ParsedLink &parsedLink)
|
|||
lowercaseLinkString += parsedLink.host.toString().toLower();
|
||||
lowercaseLinkString += parsedLink.rest;
|
||||
|
||||
auto linkElement = Link(Link::Url, matchedLink);
|
||||
|
||||
auto textColor = MessageColor(MessageColor::Link);
|
||||
auto *linkMELowercase =
|
||||
this->emplace<TextElement>(lowercaseLinkString,
|
||||
MessageElementFlag::LowercaseLink, textColor)
|
||||
->setLink(linkElement);
|
||||
auto *linkMEOriginal =
|
||||
this->emplace<TextElement>(origLink, MessageElementFlag::OriginalLink,
|
||||
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);
|
||||
});
|
||||
auto *el = this->emplace<LinkElement>(
|
||||
LinkElement::Parsed{.lowercase = lowercaseLinkString,
|
||||
.original = matchedLink},
|
||||
MessageElementFlag::Text, textColor);
|
||||
el->setLink({Link::Url, matchedLink});
|
||||
getIApp()->getLinkResolver()->resolve(el->linkInfo());
|
||||
}
|
||||
|
||||
void MessageBuilder::addIrcMessageText(const QString &text)
|
||||
|
|
|
@ -63,18 +63,6 @@ MessageElement *MessageElement::setTooltip(const QString &tooltip)
|
|||
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)
|
||||
{
|
||||
this->trailingSpace = value;
|
||||
|
@ -86,17 +74,7 @@ const QString &MessageElement::getTooltip() const
|
|||
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
|
||||
Link MessageElement::getLink() const
|
||||
{
|
||||
return this->link_;
|
||||
}
|
||||
|
@ -116,12 +94,6 @@ void MessageElement::addFlags(MessageElementFlags flags)
|
|||
this->flags_.set(flags);
|
||||
}
|
||||
|
||||
MessageElement *MessageElement::updateLink()
|
||||
{
|
||||
this->linkChanged.invoke();
|
||||
return this;
|
||||
}
|
||||
|
||||
// Empty
|
||||
EmptyElement::EmptyElement()
|
||||
: MessageElement(MessageElementFlag::None)
|
||||
|
@ -155,8 +127,8 @@ void ImageElement::addToContainer(MessageLayoutContainer &container,
|
|||
auto size = QSize(this->image_->width() * container.getScale(),
|
||||
this->image_->height() * container.getScale());
|
||||
|
||||
container.addElement((new ImageLayoutElement(*this, this->image_, size))
|
||||
->setLink(this->getLink()));
|
||||
container.addElement(
|
||||
(new ImageLayoutElement(*this, this->image_, size)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -178,10 +150,8 @@ void CircularImageElement::addToContainer(MessageLayoutContainer &container,
|
|||
auto imgSize = QSize(this->image_->width(), this->image_->height()) *
|
||||
container.getScale();
|
||||
|
||||
container.addElement((new ImageWithCircleBackgroundLayoutElement(
|
||||
*this, this->image_, imgSize,
|
||||
this->background_, this->padding_))
|
||||
->setLink(this->getLink()));
|
||||
container.addElement(new ImageWithCircleBackgroundLayoutElement(
|
||||
*this, this->image_, imgSize, this->background_, this->padding_));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -222,8 +192,7 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container,
|
|||
QSize(int(container.getScale() * image->width() * emoteScale),
|
||||
int(container.getScale() * image->height() * emoteScale));
|
||||
|
||||
container.addElement(this->makeImageLayoutElement(image, size)
|
||||
->setLink(this->getLink()));
|
||||
container.addElement(this->makeImageLayoutElement(image, size));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -284,8 +253,7 @@ void LayeredEmoteElement::addToContainer(MessageLayoutContainer &container,
|
|||
}
|
||||
|
||||
container.addElement(this->makeImageLayoutElement(
|
||||
images, individualSizes, largestSize)
|
||||
->setLink(this->getLink()));
|
||||
images, individualSizes, largestSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -441,8 +409,7 @@ EmotePtr BadgeElement::getEmote() const
|
|||
MessageLayoutElement *BadgeElement::makeImageLayoutElement(
|
||||
const ImagePtr &image, const QSize &size)
|
||||
{
|
||||
auto *element =
|
||||
(new ImageLayoutElement(*this, image, size))->setLink(this->getLink());
|
||||
auto *element = new ImageLayoutElement(*this, image, size);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
@ -459,9 +426,8 @@ MessageLayoutElement *ModBadgeElement::makeImageLayoutElement(
|
|||
{
|
||||
static const QColor modBadgeBackgroundColor("#34AE0A");
|
||||
|
||||
auto *element = (new ImageWithBackgroundLayoutElement(
|
||||
*this, image, size, modBadgeBackgroundColor))
|
||||
->setLink(this->getLink());
|
||||
auto *element = new ImageWithBackgroundLayoutElement(
|
||||
*this, image, size, modBadgeBackgroundColor);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
@ -476,8 +442,7 @@ VipBadgeElement::VipBadgeElement(const EmotePtr &data,
|
|||
MessageLayoutElement *VipBadgeElement::makeImageLayoutElement(
|
||||
const ImagePtr &image, const QSize &size)
|
||||
{
|
||||
auto *element =
|
||||
(new ImageLayoutElement(*this, image, size))->setLink(this->getLink());
|
||||
auto *element = new ImageLayoutElement(*this, image, size);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
@ -494,8 +459,7 @@ MessageLayoutElement *FfzBadgeElement::makeImageLayoutElement(
|
|||
const ImagePtr &image, const QSize &size)
|
||||
{
|
||||
auto *element =
|
||||
(new ImageWithBackgroundLayoutElement(*this, image, size, this->color))
|
||||
->setLink(this->getLink());
|
||||
new ImageWithBackgroundLayoutElement(*this, image, size, this->color);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
@ -507,11 +471,8 @@ TextElement::TextElement(const QString &text, MessageElementFlags flags,
|
|||
, color_(color)
|
||||
, style_(style)
|
||||
{
|
||||
for (const auto &word : text.split(' '))
|
||||
{
|
||||
this->words_.push_back({word, -1});
|
||||
// fourtf: add logic to store multiple spaces after message
|
||||
}
|
||||
this->words_ = text.split(' ');
|
||||
// fourtf: add logic to store multiple spaces after message
|
||||
}
|
||||
|
||||
void TextElement::addToContainer(MessageLayoutContainer &container,
|
||||
|
@ -524,39 +485,29 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
|
|||
QFontMetrics metrics =
|
||||
app->getFonts()->getFontMetrics(this->style_, container.getScale());
|
||||
|
||||
for (Word &word : this->words_)
|
||||
for (const auto &word : this->words_)
|
||||
{
|
||||
auto getTextLayoutElement = [&](QString text, int width,
|
||||
bool hasTrailingSpace) {
|
||||
auto color = this->color_.getColor(*app->getThemes());
|
||||
app->getThemes()->normalizeColor(color);
|
||||
|
||||
auto *e = (new TextLayoutElement(
|
||||
*this, text, QSize(width, metrics.height()),
|
||||
color, this->style_, container.getScale()))
|
||||
->setLink(this->getLink());
|
||||
auto *e = new TextLayoutElement(
|
||||
*this, text, QSize(width, metrics.height()), color,
|
||||
this->style_, container.getScale());
|
||||
e->setTrailingSpace(hasTrailingSpace);
|
||||
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;
|
||||
};
|
||||
|
||||
// fourtf: add again
|
||||
// if (word.width == -1) {
|
||||
word.width = metrics.horizontalAdvance(word.text);
|
||||
// }
|
||||
auto width = metrics.horizontalAdvance(word);
|
||||
|
||||
// see if the text fits in the current line
|
||||
if (container.fitsInLine(word.width))
|
||||
if (container.fitsInLine(width))
|
||||
{
|
||||
container.addElementNoLineBreak(getTextLayoutElement(
|
||||
word.text, word.width, this->hasTrailingSpace()));
|
||||
word, width, this->hasTrailingSpace()));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -565,35 +516,34 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
|
|||
{
|
||||
container.breakLine();
|
||||
|
||||
if (container.fitsInLine(word.width))
|
||||
if (container.fitsInLine(width))
|
||||
{
|
||||
container.addElementNoLineBreak(getTextLayoutElement(
|
||||
word.text, word.width, this->hasTrailingSpace()));
|
||||
word, width, this->hasTrailingSpace()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// we done goofed, we need to wrap the text
|
||||
QString text = word.text;
|
||||
int textLength = text.length();
|
||||
auto textLength = word.length();
|
||||
int wordStart = 0;
|
||||
int width = 0;
|
||||
width = 0;
|
||||
|
||||
// QChar::isHighSurrogate(text[0].unicode()) ? 2 : 1
|
||||
|
||||
for (int i = 0; i < textLength; i++)
|
||||
{
|
||||
auto isSurrogate = text.size() > i + 1 &&
|
||||
QChar::isHighSurrogate(text[i].unicode());
|
||||
auto isSurrogate = word.size() > i + 1 &&
|
||||
QChar::isHighSurrogate(word[i].unicode());
|
||||
|
||||
auto charWidth = isSurrogate
|
||||
? metrics.horizontalAdvance(text.mid(i, 2))
|
||||
: metrics.horizontalAdvance(text[i]);
|
||||
? metrics.horizontalAdvance(word.mid(i, 2))
|
||||
: metrics.horizontalAdvance(word[i]);
|
||||
|
||||
if (!container.fitsInLine(width + charWidth))
|
||||
{
|
||||
container.addElementNoLineBreak(getTextLayoutElement(
|
||||
text.mid(wordStart, i - wordStart), width, false));
|
||||
word.mid(wordStart, i - wordStart), width, false));
|
||||
container.breakLine();
|
||||
|
||||
wordStart = i;
|
||||
|
@ -615,7 +565,7 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
|
|||
}
|
||||
//add the final piece of wrapped text
|
||||
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());
|
||||
app->getThemes()->normalizeColor(color);
|
||||
|
||||
auto *e = (new TextLayoutElement(
|
||||
*this, text, QSize(width, metrics.height()), color,
|
||||
this->style_, container.getScale()))
|
||||
->setLink(this->getLink());
|
||||
auto *e = new TextLayoutElement(
|
||||
*this, text, QSize(width, metrics.height()), color,
|
||||
this->style_, container.getScale());
|
||||
e->setTrailingSpace(hasTrailingSpace);
|
||||
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;
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
TimestampElement::TimestampElement(QTime time)
|
||||
: MessageElement(MessageElementFlag::Timestamp)
|
||||
|
@ -853,8 +819,7 @@ void ScalingImageElement::addToContainer(MessageLayoutContainer &container,
|
|||
auto size = QSize(image->width() * container.getScale(),
|
||||
image->height() * container.getScale());
|
||||
|
||||
container.addElement((new ImageLayoutElement(*this, image, size))
|
||||
->setLink(this->getLink()));
|
||||
container.addElement(new ImageLayoutElement(*this, image, size));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "messages/ImageSet.hpp"
|
||||
#include "messages/Link.hpp"
|
||||
#include "messages/MessageColor.hpp"
|
||||
#include "providers/links/LinkInfo.hpp"
|
||||
#include "singletons/Fonts.hpp"
|
||||
|
||||
#include <pajlada/signals/signalholder.hpp>
|
||||
|
@ -136,10 +137,9 @@ enum class MessageElementFlag : int64_t {
|
|||
BoldUsername = (1LL << 27),
|
||||
NonBoldUsername = (1LL << 28),
|
||||
|
||||
// for links
|
||||
LowercaseLink = (1LL << 29),
|
||||
OriginalLink = (1LL << 30),
|
||||
|
||||
// used to check if links should be lowercased
|
||||
LowercaseLinks = (1LL << 29),
|
||||
// Unused = (1LL << 30)
|
||||
// Unused: (1LL << 31)
|
||||
|
||||
// for elements of the message reply
|
||||
|
@ -166,9 +166,6 @@ public:
|
|||
Update_Images = 4,
|
||||
Update_All = Update_Text | Update_Emotes | Update_Images
|
||||
};
|
||||
enum ThumbnailType : char {
|
||||
Link_Thumbnail = 1,
|
||||
};
|
||||
|
||||
virtual ~MessageElement();
|
||||
|
||||
|
@ -181,25 +178,18 @@ public:
|
|||
MessageElement *setLink(const Link &link);
|
||||
MessageElement *setText(const QString &text);
|
||||
MessageElement *setTooltip(const QString &tooltip);
|
||||
MessageElement *setThumbnailType(const ThumbnailType type);
|
||||
MessageElement *setThumbnail(const ImagePtr &thumbnail);
|
||||
|
||||
MessageElement *setTrailingSpace(bool value);
|
||||
const QString &getTooltip() const;
|
||||
const ImagePtr &getThumbnail() const;
|
||||
const ThumbnailType &getThumbnailType() const;
|
||||
|
||||
const Link &getLink() const;
|
||||
virtual Link getLink() const;
|
||||
bool hasTrailingSpace() const;
|
||||
MessageElementFlags getFlags() const;
|
||||
void addFlags(MessageElementFlags flags);
|
||||
MessageElement *updateLink();
|
||||
|
||||
virtual void addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags) = 0;
|
||||
|
||||
pajlada::Signals::NoArgSignal linkChanged;
|
||||
|
||||
protected:
|
||||
MessageElement(MessageElementFlags flags);
|
||||
bool trailingSpace = true;
|
||||
|
@ -208,8 +198,6 @@ private:
|
|||
QString text_;
|
||||
Link link_;
|
||||
QString tooltip_;
|
||||
ImagePtr thumbnail_;
|
||||
ThumbnailType thumbnailType_{};
|
||||
MessageElementFlags flags_;
|
||||
};
|
||||
|
||||
|
@ -269,15 +257,12 @@ public:
|
|||
void addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags) override;
|
||||
|
||||
protected:
|
||||
QStringList words_;
|
||||
|
||||
private:
|
||||
MessageColor color_;
|
||||
FontStyle style_;
|
||||
|
||||
struct Word {
|
||||
QString text;
|
||||
int width = -1;
|
||||
};
|
||||
std::vector<Word> words_;
|
||||
};
|
||||
|
||||
// contains a text that will be truncated to one line
|
||||
|
@ -303,6 +288,40 @@ private:
|
|||
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 :
|
||||
// a) are images for the emote type enabled
|
||||
// b) which size it wants
|
||||
|
|
|
@ -77,9 +77,9 @@ MessageLayoutElement *MessageLayoutElement::setTrailingSpace(bool value)
|
|||
return this;
|
||||
}
|
||||
|
||||
MessageLayoutElement *MessageLayoutElement::setLink(const Link &_link)
|
||||
MessageLayoutElement *MessageLayoutElement::setLink(const Link &link)
|
||||
{
|
||||
this->link_ = _link;
|
||||
this->link_ = link;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -89,9 +89,13 @@ MessageLayoutElement *MessageLayoutElement::setText(const QString &_text)
|
|||
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
|
||||
|
@ -406,14 +410,6 @@ TextLayoutElement::TextLayoutElement(MessageElement &_creator, QString &_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,
|
||||
uint32_t to) const
|
||||
{
|
||||
|
|
|
@ -44,7 +44,12 @@ public:
|
|||
void setLine(size_t line);
|
||||
|
||||
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_);
|
||||
|
||||
virtual void addCopyTextToString(QString &str, uint32_t from = 0,
|
||||
|
@ -57,7 +62,12 @@ public:
|
|||
virtual int getMouseOverIndex(const QPoint &abs) const = 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;
|
||||
FlagsEnum<MessageElementFlag> getFlags() const;
|
||||
|
||||
|
@ -67,7 +77,7 @@ protected:
|
|||
private:
|
||||
QString text_;
|
||||
QRect rect_;
|
||||
Link link_;
|
||||
std::optional<Link> link_;
|
||||
MessageElement &creator_;
|
||||
/**
|
||||
* 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
|
106
src/providers/links/LinkInfo.cpp
Normal file
106
src/providers/links/LinkInfo.cpp
Normal 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
src/providers/links/LinkInfo.hpp
Normal file
135
src/providers/links/LinkInfo.hpp
Normal 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
src/providers/links/LinkResolver.cpp
Normal file
72
src/providers/links/LinkResolver.cpp
Normal 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
src/providers/links/LinkResolver.hpp
Normal file
36
src/providers/links/LinkResolver.hpp
Normal 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
|
|
@ -185,8 +185,7 @@ void WindowManager::updateWordTypeMask()
|
|||
flags.set(MEF::Collapsed);
|
||||
flags.set(settings->boldUsernames ? MEF::BoldUsername
|
||||
: MEF::NonBoldUsername);
|
||||
flags.set(settings->lowercaseDomains ? MEF::LowercaseLink
|
||||
: MEF::OriginalLink);
|
||||
flags.set(MEF::LowercaseLinks, settings->lowercaseDomains);
|
||||
flags.set(MEF::ChannelPointReward);
|
||||
|
||||
// update flags
|
||||
|
|
|
@ -19,7 +19,8 @@
|
|||
#include "messages/MessageElement.hpp"
|
||||
#include "messages/MessageThread.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/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
|
@ -1937,55 +1938,16 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
|
|||
}
|
||||
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;
|
||||
if (thumbnailSize == 0)
|
||||
auto *linkElement = dynamic_cast<LinkElement *>(element);
|
||||
if (linkElement)
|
||||
{
|
||||
// "Show thumbnails" is set to "Off", show text only
|
||||
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)
|
||||
if (linkElement->linkInfo()->isPending())
|
||||
{
|
||||
this->tooltipWidget_->setOne({
|
||||
std::move(thumb),
|
||||
element->getTooltip(),
|
||||
thumbnailSize,
|
||||
thumbnailSize,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
this->tooltipWidget_->setOne({std::move(thumb), ""});
|
||||
getIApp()->getLinkResolver()->resolve(
|
||||
linkElement->linkInfo());
|
||||
}
|
||||
this->setLinkInfoTooltip(linkElement->linkInfo());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3103,4 +3065,62 @@ bool ChannelView::canReplyToMessages() const
|
|||
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
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
#include <pajlada/signals/signal.hpp>
|
||||
#include <QMenu>
|
||||
#include <QPaintEvent>
|
||||
#include <QPointer>
|
||||
#include <QScroller>
|
||||
#include <QTimer>
|
||||
#include <QVariantAnimation>
|
||||
|
@ -47,6 +48,8 @@ class Split;
|
|||
class FilterSet;
|
||||
using FilterSetPtr = std::shared_ptr<FilterSet>;
|
||||
|
||||
class LinkInfo;
|
||||
|
||||
enum class PauseReason {
|
||||
Mouse,
|
||||
Selection,
|
||||
|
@ -366,6 +369,17 @@ private:
|
|||
void scrollUpdateRequested();
|
||||
|
||||
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
|
||||
|
|
|
@ -38,6 +38,7 @@ set(test_SOURCES
|
|||
${CMAKE_CURRENT_LIST_DIR}/src/Selection.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/NotebookTab.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/SplitInput.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/LinkInfo.cpp
|
||||
# Add your new file above this line!
|
||||
)
|
||||
|
||||
|
|
102
tests/src/LinkInfo.cpp
Normal file
102
tests/src/LinkInfo.cpp
Normal file
|
@ -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());
|
||||
}
|
60
tests/src/SignalSpy.hpp
Normal file
60
tests/src/SignalSpy.hpp
Normal file
|
@ -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_;
|
||||
};
|
Loading…
Reference in a new issue