diff --git a/src/Application.cpp b/src/Application.cpp index 556dde9bf63..6daa2e12989 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -24,6 +24,7 @@ #include "providers/seventv/SeventvBadges.hpp" #include "providers/seventv/SeventvEventAPI.hpp" #include "providers/seventv/SeventvPaints.hpp" +#include "providers/seventv/SeventvPersonalEmotes.hpp" #include "providers/twitch/ChannelPointReward.hpp" #include "providers/twitch/PubSubActions.hpp" #include "providers/twitch/PubSubManager.hpp" @@ -85,6 +86,7 @@ Application::Application(Settings &_settings, Paths &_paths) , ffzBadges(&this->emplace()) , seventvBadges(&this->emplace()) , seventvPaints(&this->emplace()) + , seventvPersonalEmotes(&this->emplace()) , userData(&this->emplace()) , sound(&this->emplace()) , logging(&this->emplace()) @@ -639,30 +641,54 @@ void Application::initSeventvEventAPI() this->twitch->seventvEventAPI->signals_.emoteAdded.connect( [&](const auto &data) { - postToThread([this, data] { - this->twitch->forEachSeventvEmoteSet( - data.emoteSetID, [data](TwitchChannel &chan) { - chan.addSeventvEmote(data); - }); - }); + if (this->seventvPersonalEmotes->hasEmoteSet(data.emoteSetID)) + { + this->seventvPersonalEmotes->updateEmoteSet(data.emoteSetID, + data); + } + else + { + postToThread([this, data] { + this->twitch->forEachSeventvEmoteSet( + data.emoteSetID, [data](TwitchChannel &chan) { + chan.addSeventvEmote(data); + }); + }); + } }); this->twitch->seventvEventAPI->signals_.emoteUpdated.connect( [&](const auto &data) { - postToThread([this, data] { - this->twitch->forEachSeventvEmoteSet( - data.emoteSetID, [data](TwitchChannel &chan) { - chan.updateSeventvEmote(data); - }); - }); + if (this->seventvPersonalEmotes->hasEmoteSet(data.emoteSetID)) + { + this->seventvPersonalEmotes->updateEmoteSet(data.emoteSetID, + data); + } + else + { + postToThread([this, data] { + this->twitch->forEachSeventvEmoteSet( + data.emoteSetID, [data](TwitchChannel &chan) { + chan.updateSeventvEmote(data); + }); + }); + } }); this->twitch->seventvEventAPI->signals_.emoteRemoved.connect( [&](const auto &data) { - postToThread([this, data] { - this->twitch->forEachSeventvEmoteSet( - data.emoteSetID, [data](TwitchChannel &chan) { - chan.removeSeventvEmote(data); - }); - }); + if (this->seventvPersonalEmotes->hasEmoteSet(data.emoteSetID)) + { + this->seventvPersonalEmotes->updateEmoteSet(data.emoteSetID, + data); + } + else + { + postToThread([this, data] { + this->twitch->forEachSeventvEmoteSet( + data.emoteSetID, [data](TwitchChannel &chan) { + chan.removeSeventvEmote(data); + }); + }); + } }); this->twitch->seventvEventAPI->signals_.userUpdated.connect( [&](const auto &data) { @@ -671,6 +697,19 @@ void Application::initSeventvEventAPI() chan.updateSeventvUser(data); }); }); + this->twitch->seventvEventAPI->signals_.personalEmoteSetAdded.connect( + [&](const auto &data) { + postToThread([this, data]() { + this->twitch->forEachChannelAndSpecialChannels([=](auto chan) { + if (auto *twitchChannel = + dynamic_cast(chan.get())) + { + twitchChannel->upsertPersonalSeventvEmotes(data.first, + data.second); + } + }); + }); + }); this->twitch->seventvEventAPI->start(); } diff --git a/src/Application.hpp b/src/Application.hpp index 225ac2ecb90..b2b858544c5 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -35,6 +35,7 @@ class SeventvBadges; class SeventvPaints; class FfzBadges; class SeventvBadges; +class SeventvPersonalEmotes; class IApplication { @@ -95,6 +96,7 @@ class Application : public IApplication FfzBadges *const ffzBadges{}; SeventvBadges *const seventvBadges{}; SeventvPaints *const seventvPaints{}; + SeventvPersonalEmotes *const seventvPersonalEmotes{}; UserDataController *const userData{}; SoundController *const sound{}; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c3604dc60fc..9f8d30922e1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -273,6 +273,8 @@ set(SOURCE_FILES providers/seventv/SeventvEmotes.hpp providers/seventv/SeventvEventAPI.cpp providers/seventv/SeventvEventAPI.hpp + providers/seventv/SeventvPersonalEmotes.cpp + providers/seventv/SeventvPersonalEmotes.hpp providers/seventv/eventapi/Client.cpp providers/seventv/eventapi/Client.hpp diff --git a/src/common/CompletionModel.cpp b/src/common/CompletionModel.cpp index 08bddf2e8b7..3fbca8e3af9 100644 --- a/src/common/CompletionModel.cpp +++ b/src/common/CompletionModel.cpp @@ -6,6 +6,7 @@ #include "controllers/commands/Command.hpp" #include "controllers/commands/CommandController.hpp" #include "messages/Emote.hpp" +#include "providers/seventv/SeventvPersonalEmotes.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchCommon.hpp" @@ -214,6 +215,17 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) } } + // 7TV Personal + if (const auto map = getApp()->seventvPersonalEmotes->getEmoteSetForUser( + getApp()->accounts->twitch.getCurrent()->getUserId())) + { + for (const auto &emote : *map.get()) + { + addString(emote.first.string, + TaggedString::Type::SeventvPersonalEmote); + } + } + // 7TV Channel for (const auto &emote : *tc->seventvEmotes()) { diff --git a/src/common/CompletionModel.hpp b/src/common/CompletionModel.hpp index c2670c08ee4..ebb1dffaa94 100644 --- a/src/common/CompletionModel.hpp +++ b/src/common/CompletionModel.hpp @@ -24,6 +24,7 @@ class CompletionModel : public QAbstractListModel BTTVChannelEmote, SeventvGlobalEmote, SeventvChannelEmote, + SeventvPersonalEmote, TwitchGlobalEmote, TwitchLocalEmote, TwitchSubscriberEmote, diff --git a/src/messages/Message.cpp b/src/messages/Message.cpp index cb005a84355..3f654d3714f 100644 --- a/src/messages/Message.cpp +++ b/src/messages/Message.cpp @@ -11,6 +11,9 @@ #include "util/IrcHelpers.hpp" #include "widgets/helper/ScrollbarHighlight.hpp" +#include +#include + using SBHighlight = chatterino::ScrollbarHighlight; namespace chatterino { @@ -62,6 +65,36 @@ SBHighlight Message::getScrollBarHighlight() const return SBHighlight(); } +std::shared_ptr Message::cloneWith( + const std::function &fn) const +{ + auto cloned = std::make_shared(); + cloned->flags = this->flags; + cloned->parseTime = this->parseTime; + cloned->id = this->id; + cloned->searchText = this->searchText; + cloned->messageText = this->messageText; + cloned->loginName = this->loginName; + cloned->displayName = this->displayName; + cloned->localizedName = this->localizedName; + cloned->timeoutUser = this->timeoutUser; + cloned->channelName = this->channelName; + cloned->usernameColor = this->usernameColor; + cloned->serverReceivedTime = this->serverReceivedTime; + cloned->badges = this->badges; + cloned->badgeInfos = this->badgeInfos; + cloned->highlightColor = this->highlightColor; + cloned->replyThread = this->replyThread; + cloned->count = this->count; + std::transform(this->elements.cbegin(), this->elements.cend(), + std::back_inserter(cloned->elements), + [](const auto &element) { + return element->clone(); + }); + fn(*cloned); + return std::move(cloned); +} + // Static namespace { diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index 1cc3ce9e053..2f7bb92b150 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -88,6 +89,15 @@ struct Message : boost::noncopyable { std::vector seventvEventTargetEmotes; ScrollbarHighlight getScrollBarHighlight() const; + + /** + * Clones this message. Before contructing the shared pointer, + * `fn` is called with a reference to the new message. + * + * @return An identical message, independent from this one. + */ + std::shared_ptr cloneWith( + const std::function &fn) const; }; using MessagePtr = std::shared_ptr; diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index ea76b591868..e8ea105de37 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -13,6 +13,8 @@ #include "singletons/Theme.hpp" #include "util/DebugCount.hpp" +#include + namespace chatterino { MessageElement::MessageElement(MessageElementFlags flags) @@ -77,6 +79,11 @@ const MessageElement::ThumbnailType &MessageElement::getThumbnailType() const return this->thumbnailType_; } +const QString &MessageElement::getText() const +{ + return this->text_; +} + const Link &MessageElement::getLink() const { return this->link_; @@ -98,6 +105,16 @@ MessageElement *MessageElement::updateLink() return this; } +void MessageElement::cloneFrom(const MessageElement &source) +{ + this->text_ = source.text_; + this->link_ = source.link_; + this->tooltip_ = source.tooltip_; + this->thumbnail_ = source.thumbnail_; + this->thumbnailType_ = source.thumbnailType_; + this->flags_ = source.flags_; +} + // Empty EmptyElement::EmptyElement() : MessageElement(MessageElementFlag::None) @@ -109,6 +126,13 @@ void EmptyElement::addToContainer(MessageLayoutContainer &container, { } +std::unique_ptr EmptyElement::clone() const +{ + auto el = std::make_unique(); + el->cloneFrom(*this); + return el; +} + EmptyElement &EmptyElement::instance() { static EmptyElement instance; @@ -136,6 +160,13 @@ void ImageElement::addToContainer(MessageLayoutContainer &container, } } +std::unique_ptr ImageElement::clone() const +{ + auto el = std::make_unique(this->image_, this->getFlags()); + el->cloneFrom(*this); + return el; +} + CircularImageElement::CircularImageElement(ImagePtr image, int padding, QColor background, MessageElementFlags flags) @@ -161,6 +192,14 @@ void CircularImageElement::addToContainer(MessageLayoutContainer &container, } } +std::unique_ptr CircularImageElement::clone() const +{ + auto el = std::make_unique( + this->image_, this->padding_, this->background_, this->getFlags()); + el->cloneFrom(*this); + return el; +} + // EMOTE EmoteElement::EmoteElement(const EmotePtr &emote, MessageElementFlags flags, const MessageColor &textElementColor) @@ -216,6 +255,15 @@ MessageLayoutElement *EmoteElement::makeImageLayoutElement( return new ImageLayoutElement(*this, image, size); } +std::unique_ptr EmoteElement::clone() const +{ + auto el = std::make_unique(this->emote_, this->getFlags()); + el->textElement_ = std::unique_ptr( + dynamic_cast(this->textElement_->clone().release())); + el->cloneFrom(*this); + return el; +} + // BADGE BadgeElement::BadgeElement(const EmotePtr &emote, MessageElementFlags flags) : MessageElement(flags) @@ -255,6 +303,13 @@ MessageLayoutElement *BadgeElement::makeImageLayoutElement( return element; } +std::unique_ptr BadgeElement::clone() const +{ + auto el = std::make_unique(this->emote_, this->getFlags()); + el->cloneFrom(*this); + return el; +} + // MOD BADGE ModBadgeElement::ModBadgeElement(const EmotePtr &data, MessageElementFlags flags_) @@ -274,6 +329,13 @@ MessageLayoutElement *ModBadgeElement::makeImageLayoutElement( return element; } +std::unique_ptr ModBadgeElement::clone() const +{ + auto el = std::make_unique(this->emote_, this->getFlags()); + el->cloneFrom(*this); + return el; +} + // VIP BADGE VipBadgeElement::VipBadgeElement(const EmotePtr &data, MessageElementFlags flags_) @@ -290,6 +352,13 @@ MessageLayoutElement *VipBadgeElement::makeImageLayoutElement( return element; } +std::unique_ptr VipBadgeElement::clone() const +{ + auto el = std::make_unique(this->emote_, this->getFlags()); + el->cloneFrom(*this); + return el; +} + // FFZ Badge FfzBadgeElement::FfzBadgeElement(const EmotePtr &data, MessageElementFlags flags_, QColor color_) @@ -308,6 +377,14 @@ MessageLayoutElement *FfzBadgeElement::makeImageLayoutElement( return element; } +std::unique_ptr FfzBadgeElement::clone() const +{ + auto el = std::make_unique(this->emote_, this->getFlags(), + this->color); + el->cloneFrom(*this); + return el; +} + // TEXT TextElement::TextElement(const QString &text, MessageElementFlags flags, const MessageColor &color, FontStyle style) @@ -322,6 +399,30 @@ TextElement::TextElement(const QString &text, MessageElementFlags flags, } } +TextElement::TextElement(std::vector &&words, MessageElementFlags flags, + const MessageColor &color, FontStyle style) + : MessageElement(flags) + , color_(color) + , style_(style) + , words_(std::move(words)) +{ +} + +MessageColor TextElement::color() const +{ + return this->color_; +} + +FontStyle TextElement::style() const +{ + return this->style_; +} + +const std::vector &TextElement::words() const +{ + return this->words_; +} + void TextElement::addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) { @@ -424,6 +525,15 @@ void TextElement::addToContainer(MessageLayoutContainer &container, } } +std::unique_ptr TextElement::clone() const +{ + auto el = std::make_unique(QString(), this->getFlags(), + this->color_, this->style_); + el->words_ = this->words_; + el->cloneFrom(*this); + return el; +} + SingleLineTextElement::SingleLineTextElement(const QString &text, MessageElementFlags flags, const MessageColor &color, @@ -572,6 +682,15 @@ void SingleLineTextElement::addToContainer(MessageLayoutContainer &container, } } +std::unique_ptr SingleLineTextElement::clone() const +{ + auto el = std::make_unique( + QString(), this->getFlags(), this->color_, this->style_); + el->words_ = this->words_; + el->cloneFrom(*this); + return el; +} + // TIMESTAMP TimestampElement::TimestampElement(QTime time) : MessageElement(MessageElementFlag::Timestamp) @@ -606,6 +725,13 @@ TextElement *TimestampElement::formatTime(const QTime &time) MessageColor::System, FontStyle::ChatMedium); } +std::unique_ptr TimestampElement::clone() const +{ + auto el = std::make_unique(this->time_); + el->cloneFrom(*this); + return el; +} + // TWITCH MODERATION TwitchModerationElement::TwitchModerationElement() : MessageElement(MessageElementFlag::ModeratorTools) @@ -640,6 +766,13 @@ void TwitchModerationElement::addToContainer(MessageLayoutContainer &container, } } +std::unique_ptr TwitchModerationElement::clone() const +{ + auto el = std::make_unique(); + el->cloneFrom(*this); + return el; +} + LinebreakElement::LinebreakElement(MessageElementFlags flags) : MessageElement(flags) { @@ -654,6 +787,13 @@ void LinebreakElement::addToContainer(MessageLayoutContainer &container, } } +std::unique_ptr LinebreakElement::clone() const +{ + auto el = std::make_unique(this->getFlags()); + el->cloneFrom(*this); + return el; +} + ScalingImageElement::ScalingImageElement(ImageSet images, MessageElementFlags flags) : MessageElement(flags) @@ -679,6 +819,14 @@ void ScalingImageElement::addToContainer(MessageLayoutContainer &container, } } +std::unique_ptr ScalingImageElement::clone() const +{ + auto el = + std::make_unique(this->images_, this->getFlags()); + el->cloneFrom(*this); + return el; +} + ReplyCurveElement::ReplyCurveElement() : MessageElement(MessageElementFlag::RepliedMessage) { @@ -701,4 +849,11 @@ void ReplyCurveElement::addToContainer(MessageLayoutContainer &container, } } +std::unique_ptr ReplyCurveElement::clone() const +{ + auto el = std::make_unique(); + el->cloneFrom(*this); + return el; +} + } // namespace chatterino diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index 0ce8443fcac..3a4f8a2128e 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -186,6 +186,7 @@ class MessageElement : boost::noncopyable const ImagePtr &getThumbnail() const; const ThumbnailType &getThumbnailType() const; + const QString &getText() const; const Link &getLink() const; bool hasTrailingSpace() const; MessageElementFlags getFlags() const; @@ -194,12 +195,16 @@ class MessageElement : boost::noncopyable virtual void addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) = 0; + virtual std::unique_ptr clone() const = 0; + pajlada::Signals::NoArgSignal linkChanged; protected: MessageElement(MessageElementFlags flags); bool trailingSpace = true; + void cloneFrom(const MessageElement &source); + private: QString text_; Link link_; @@ -218,6 +223,8 @@ class EmptyElement : public MessageElement void addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) override; + std::unique_ptr clone() const override; + static EmptyElement &instance(); private: @@ -233,6 +240,8 @@ class ImageElement : public MessageElement void addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) override; + std::unique_ptr clone() const override; + private: ImagePtr image_; }; @@ -247,6 +256,8 @@ class CircularImageElement : public MessageElement void addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) override; + std::unique_ptr clone() const override; + private: ImagePtr image_; int padding_; @@ -257,22 +268,33 @@ class CircularImageElement : public MessageElement class TextElement : public MessageElement { public: + struct Word { + QString text; + int width = -1; + }; + TextElement(const QString &text, MessageElementFlags flags, const MessageColor &color = MessageColor::Text, FontStyle style = FontStyle::ChatMedium); + TextElement(std::vector &&words, MessageElementFlags flags, + const MessageColor &color = MessageColor::Text, + FontStyle style = FontStyle::ChatMedium); ~TextElement() override = default; + MessageColor color() const; + FontStyle style() const; + + const std::vector &words() const; + void addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) override; + std::unique_ptr clone() const override; + private: MessageColor color_; FontStyle style_; - struct Word { - QString text; - int width = -1; - }; std::vector words_; }; @@ -288,6 +310,8 @@ class SingleLineTextElement : public MessageElement void addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) override; + std::unique_ptr clone() const override; + private: MessageColor color_; FontStyle style_; @@ -312,6 +336,8 @@ class EmoteElement : public MessageElement MessageElementFlags flags_) override; EmotePtr getEmote() const; + std::unique_ptr clone() const override; + protected: virtual MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image, const QSize &size); @@ -331,11 +357,11 @@ class BadgeElement : public MessageElement EmotePtr getEmote() const; + std::unique_ptr clone() const override; + protected: virtual MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image, const QSize &size); - -private: EmotePtr emote_; }; @@ -344,6 +370,8 @@ class ModBadgeElement : public BadgeElement public: ModBadgeElement(const EmotePtr &data, MessageElementFlags flags_); + std::unique_ptr clone() const override; + protected: MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image, const QSize &size) override; @@ -354,6 +382,8 @@ class VipBadgeElement : public BadgeElement public: VipBadgeElement(const EmotePtr &data, MessageElementFlags flags_); + std::unique_ptr clone() const override; + protected: MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image, const QSize &size) override; @@ -365,6 +395,8 @@ class FfzBadgeElement : public BadgeElement FfzBadgeElement(const EmotePtr &data, MessageElementFlags flags_, QColor color_); + std::unique_ptr clone() const override; + protected: MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image, const QSize &size) override; @@ -383,6 +415,8 @@ class TimestampElement : public MessageElement TextElement *formatTime(const QTime &time); + std::unique_ptr clone() const override; + private: QTime time_; std::unique_ptr element_; @@ -398,6 +432,8 @@ class TwitchModerationElement : public MessageElement void addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) override; + + std::unique_ptr clone() const override; }; // Forces a linebreak @@ -408,6 +444,8 @@ class LinebreakElement : public MessageElement void addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) override; + + std::unique_ptr clone() const override; }; // Image element which will pick the quality of the image based on ui scale @@ -419,6 +457,8 @@ class ScalingImageElement : public MessageElement void addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) override; + std::unique_ptr clone() const override; + private: ImageSet images_; }; @@ -430,6 +470,8 @@ class ReplyCurveElement : public MessageElement void addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) override; + + std::unique_ptr clone() const override; }; } // namespace chatterino diff --git a/src/providers/seventv/SeventvEmotes.cpp b/src/providers/seventv/SeventvEmotes.cpp index a5be72afdf3..fccf2baedde 100644 --- a/src/providers/seventv/SeventvEmotes.cpp +++ b/src/providers/seventv/SeventvEmotes.cpp @@ -77,23 +77,40 @@ bool isZeroWidthRecommended(const QJsonObject &emoteData) return flags.has(SeventvEmoteFlag::ZeroWidth); } -Tooltip createTooltip(const QString &name, const QString &author, bool isGlobal) +QString kindToString(SeventvEmoteSetKind kind) +{ + switch (kind) + { + case SeventvEmoteSetKind::Global: + return QStringLiteral("Global"); + case SeventvEmoteSetKind::Personal: + return QStringLiteral("Personal"); + case SeventvEmoteSetKind::Channel: + return QStringLiteral("Channel"); + default: + return QStringLiteral(""); + } +} + +Tooltip createTooltip(const QString &name, const QString &author, + SeventvEmoteSetKind kind) { return Tooltip{QString("%1
%2 7TV Emote
By: %3") - .arg(name, isGlobal ? "Global" : "Channel", + .arg(name, kindToString(kind), author.isEmpty() ? "" : author)}; } Tooltip createAliasedTooltip(const QString &name, const QString &baseName, - const QString &author, bool isGlobal) + const QString &author, SeventvEmoteSetKind kind) { return Tooltip{QString("%1
Alias of %2
%3 7TV Emote
By: %4") - .arg(name, baseName, isGlobal ? "Global" : "Channel", + .arg(name, baseName, kindToString(kind), author.isEmpty() ? "" : author)}; } CreateEmoteResult createEmote(const QJsonObject &activeEmote, - const QJsonObject &emoteData, bool isGlobal) + const QJsonObject &emoteData, + SeventvEmoteSetKind kind) { auto emoteId = EmoteId{activeEmote["id"].toString()}; auto emoteName = EmoteName{activeEmote["name"].toString()}; @@ -105,8 +122,8 @@ CreateEmoteResult createEmote(const QJsonObject &activeEmote, auto tooltip = aliasedName ? createAliasedTooltip(emoteName.string, baseEmoteName.string, - author.string, isGlobal) - : createTooltip(emoteName.string, author.string, isGlobal); + author.string, kind) + : createTooltip(emoteName.string, author.string, kind); auto imageSet = SeventvEmotes::createImageSet(emoteData); auto emote = @@ -117,19 +134,28 @@ CreateEmoteResult createEmote(const QJsonObject &activeEmote, return {emote, emoteId, emoteName, !emote.images.getImage1()->isEmpty()}; } -bool checkEmoteVisibility(const QJsonObject &emoteData) +bool checkEmoteVisibility(const QJsonObject &emoteData, + SeventvEmoteSetKind kind) { if (!emoteData["listed"].toBool() && !getSettings()->showUnlistedSevenTVEmotes) { return false; } + + // Only add allowed emotes + if (kind == SeventvEmoteSetKind::Personal && + !emoteData["state"].toArray().contains("PERSONAL")) + { + return false; + } + auto flags = SeventvEmoteFlags(SeventvEmoteFlag(emoteData["flags"].toInt())); return !flags.has(SeventvEmoteFlag::ContentTwitchDisallowed); } -EmoteMap parseEmotes(const QJsonArray &emoteSetEmotes, bool isGlobal) +EmoteMap parseEmotes(const QJsonArray &emoteSetEmotes, SeventvEmoteSetKind kind) { auto emotes = EmoteMap(); @@ -138,12 +164,12 @@ EmoteMap parseEmotes(const QJsonArray &emoteSetEmotes, bool isGlobal) auto activeEmote = activeEmoteJson.toObject(); auto emoteData = activeEmote["data"].toObject(); - if (emoteData.empty() || !checkEmoteVisibility(emoteData)) + if (emoteData.empty() || !checkEmoteVisibility(emoteData, kind)) { continue; } - auto result = createEmote(activeEmote, emoteData, isGlobal); + auto result = createEmote(activeEmote, emoteData, kind); if (!result.hasImages) { // this shouldn't happen but if it does, it will crash, @@ -160,7 +186,8 @@ EmoteMap parseEmotes(const QJsonArray &emoteSetEmotes, bool isGlobal) } EmotePtr createUpdatedEmote(const EmotePtr &oldEmote, - const EmoteUpdateDispatch &dispatch) + const EmoteUpdateDispatch &dispatch, + SeventvEmoteSetKind kind) { bool toNonAliased = oldEmote->baseName.has_value() && dispatch.emoteName == oldEmote->baseName->string; @@ -169,9 +196,9 @@ EmotePtr createUpdatedEmote(const EmotePtr &oldEmote, auto emote = std::make_shared(Emote( {EmoteName{dispatch.emoteName}, oldEmote->images, toNonAliased - ? createTooltip(dispatch.emoteName, oldEmote->author.string, false) + ? createTooltip(dispatch.emoteName, oldEmote->author.string, kind) : createAliasedTooltip(dispatch.emoteName, baseName.string, - oldEmote->author.string, false), + oldEmote->author.string, kind), oldEmote->homePage, oldEmote->zeroWidth, oldEmote->id, oldEmote->author, boost::make_optional(!toNonAliased, baseName)})); return emote; @@ -221,7 +248,8 @@ void SeventvEmotes::loadGlobalEmotes() .onSuccess([this](const NetworkResult &result) -> Outcome { QJsonArray parsedEmotes = result.parseJson()["emotes"].toArray(); - auto emoteMap = parseEmotes(parsedEmotes, true); + auto emoteMap = + parseEmotes(parsedEmotes, SeventvEmoteSetKind::Global); qCDebug(chatterinoSeventv) << "Loaded" << emoteMap.size() << "7TV Global Emotes"; this->global_.set(std::make_shared(std::move(emoteMap))); @@ -250,7 +278,8 @@ void SeventvEmotes::loadChannelEmotes( auto emoteSet = json["emote_set"].toObject(); auto parsedEmotes = emoteSet["emotes"].toArray(); - auto emoteMap = parseEmotes(parsedEmotes, false); + auto emoteMap = + parseEmotes(parsedEmotes, SeventvEmoteSetKind::Channel); bool hasEmotes = !emoteMap.empty(); qCDebug(chatterinoSeventv) @@ -339,18 +368,18 @@ void SeventvEmotes::loadChannelEmotes( boost::optional SeventvEmotes::addEmote( Atomic> &map, - const EmoteAddDispatch &dispatch) + const EmoteAddDispatch &dispatch, SeventvEmoteSetKind kind) { // Check for visibility first, so we don't copy the map. auto emoteData = dispatch.emoteJson["data"].toObject(); - if (emoteData.empty() || !checkEmoteVisibility(emoteData)) + if (emoteData.empty() || !checkEmoteVisibility(emoteData, kind)) { return boost::none; } // This copies the map. EmoteMap updatedMap = *map.get(); - auto result = createEmote(dispatch.emoteJson, emoteData, false); + auto result = createEmote(dispatch.emoteJson, emoteData, kind); if (!result.hasImages) { // Incoming emote didn't contain any images, abort @@ -367,7 +396,7 @@ boost::optional SeventvEmotes::addEmote( boost::optional SeventvEmotes::updateEmote( Atomic> &map, - const EmoteUpdateDispatch &dispatch) + const EmoteUpdateDispatch &dispatch, SeventvEmoteSetKind kind) { auto oldMap = map.get(); auto oldEmote = oldMap->findEmote(dispatch.emoteName, dispatch.emoteID); @@ -380,7 +409,7 @@ boost::optional SeventvEmotes::updateEmote( EmoteMap updatedMap = *map.get(); updatedMap.erase(oldEmote->second->name); - auto emote = createUpdatedEmote(oldEmote->second, dispatch); + auto emote = createUpdatedEmote(oldEmote->second, dispatch, kind); updatedMap[emote->name] = emote; map.set(std::make_shared(std::move(updatedMap))); @@ -421,7 +450,14 @@ void SeventvEmotes::getEmoteSet( auto json = result.parseJson(); auto parsedEmotes = json["emotes"].toArray(); - auto emoteMap = parseEmotes(parsedEmotes, false); + auto kind = SeventvEmoteSetKind::Channel; + if (SeventvEmoteSetFlags(SeventvEmoteSetFlag(json["flags"].toInt())) + .has(SeventvEmoteSetFlag::Personal)) + { + kind = SeventvEmoteSetKind::Personal; + } + + auto emoteMap = parseEmotes(parsedEmotes, kind); qCDebug(chatterinoSeventv) << "Loaded" << emoteMap.size() << "7TV Emotes from" << emoteSetId; diff --git a/src/providers/seventv/SeventvEmotes.hpp b/src/providers/seventv/SeventvEmotes.hpp index 9920ccb75ee..97c27966068 100644 --- a/src/providers/seventv/SeventvEmotes.hpp +++ b/src/providers/seventv/SeventvEmotes.hpp @@ -63,6 +63,20 @@ struct Emote; using EmotePtr = std::shared_ptr; class EmoteMap; +enum class SeventvEmoteSetKind : uint8_t { + Global, + Personal, + Channel, +}; + +enum class SeventvEmoteSetFlag : uint32_t { + Immutable = (1 << 0), + Privileged = (1 << 1), + Personal = (1 << 2), + Commercial = (1 << 3), +}; +using SeventvEmoteSetFlags = FlagsEnum; + class SeventvEmotes final { public: @@ -91,7 +105,8 @@ class SeventvEmotes final */ static boost::optional addEmote( Atomic> &map, - const seventv::eventapi::EmoteAddDispatch &dispatch); + const seventv::eventapi::EmoteAddDispatch &dispatch, + SeventvEmoteSetKind kind = SeventvEmoteSetKind::Channel); /** * Updates an emote in this `map`. @@ -102,7 +117,8 @@ class SeventvEmotes final */ static boost::optional updateEmote( Atomic> &map, - const seventv::eventapi::EmoteUpdateDispatch &dispatch); + const seventv::eventapi::EmoteUpdateDispatch &dispatch, + SeventvEmoteSetKind kind = SeventvEmoteSetKind::Channel); /** * Removes an emote from this `map`. diff --git a/src/providers/seventv/SeventvEventAPI.cpp b/src/providers/seventv/SeventvEventAPI.cpp index 1523f25da7a..f20c9d10150 100644 --- a/src/providers/seventv/SeventvEventAPI.cpp +++ b/src/providers/seventv/SeventvEventAPI.cpp @@ -7,6 +7,7 @@ #include "providers/seventv/SeventvBadges.hpp" #include "providers/seventv/SeventvCosmetics.hpp" #include "providers/seventv/SeventvPaints.hpp" +#include "providers/seventv/SeventvPersonalEmotes.hpp" #include "util/PostToThread.hpp" #include @@ -51,6 +52,7 @@ void SeventvEventAPI::subscribeTwitchChannel(const QString &id) {ChannelCondition{id}, SubscriptionType::CreateEntitlement}); this->subscribe( {ChannelCondition{id}, SubscriptionType::DeleteEntitlement}); + this->subscribe({ChannelCondition{id}, SubscriptionType::AnyEmoteSet}); } } @@ -82,6 +84,8 @@ void SeventvEventAPI::unsubscribeTwitchChannel(const QString &id) {ChannelCondition{id}, SubscriptionType::CreateEntitlement}); this->unsubscribe( {ChannelCondition{id}, SubscriptionType::DeleteEntitlement}); + this->unsubscribe( + {ChannelCondition{id}, SubscriptionType::AnyEmoteSet}); } } @@ -168,6 +172,10 @@ void SeventvEventAPI::handleDispatch(const Dispatch &dispatch) { switch (dispatch.type) { + case SubscriptionType::CreateEmoteSet: { + this->onEmoteSetCreate(dispatch); + } + break; case SubscriptionType::UpdateEmoteSet: { this->onEmoteSetUpdate(dispatch); } @@ -370,6 +378,16 @@ void SeventvEventAPI::onEntitlementCreate( entitlement.refID, UserName{entitlement.userName}); } break; + case CosmeticKind::EmoteSet: { + if (auto set = Application::instance->seventvPersonalEmotes + ->assignUserToEmoteSet(entitlement.refID, + entitlement.userID)) + { + this->signals_.personalEmoteSetAdded.invoke( + {entitlement.userName, *set}); + } + } + break; default: break; } @@ -396,6 +414,25 @@ void SeventvEventAPI::onEntitlementDelete( break; } } + +void SeventvEventAPI::onEmoteSetCreate(const Dispatch &dispatch) +{ + // We're using Application::instance, because we're not in the GUI thread. + // `seventvBadges` and `seventvPaints` do their own locking. + EmoteSetCreateDispatch createDispatch(dispatch.body["object"].toObject()); + if (!createDispatch.validate()) + { + qCDebug(chatterinoSeventvEventAPI) + << "Invalid dispatch" << dispatch.body; + return; + } + + if (createDispatch.isPersonal) + { + Application::instance->seventvPersonalEmotes->createEmoteSet( + createDispatch.emoteSetID); + } +} // NOLINTEND(readability-convert-member-functions-to-static) } // namespace chatterino diff --git a/src/providers/seventv/SeventvEventAPI.hpp b/src/providers/seventv/SeventvEventAPI.hpp index 56a4bef0a1d..d22f168a0b5 100644 --- a/src/providers/seventv/SeventvEventAPI.hpp +++ b/src/providers/seventv/SeventvEventAPI.hpp @@ -21,6 +21,7 @@ namespace seventv::eventapi { class SeventvBadges; class SeventvPaints; +class EmoteMap; class SeventvEventAPI : public BasicPubSubManager @@ -39,6 +40,8 @@ class SeventvEventAPI Signal emoteUpdated; Signal emoteRemoved; Signal userUpdated; + Signal>> + personalEmoteSetAdded; } signals_; // NOLINT(readability-identifier-naming) /** @@ -84,6 +87,7 @@ class SeventvEventAPI const seventv::eventapi::EntitlementCreateDeleteDispatch &entitlement); void onEntitlementDelete( const seventv::eventapi::EntitlementCreateDeleteDispatch &entitlement); + void onEmoteSetCreate(const seventv::eventapi::Dispatch &dispatch); /** emote-set ids */ std::unordered_set subscribedEmoteSets_; diff --git a/src/providers/seventv/SeventvPersonalEmotes.cpp b/src/providers/seventv/SeventvPersonalEmotes.cpp new file mode 100644 index 00000000000..b81f0d592d3 --- /dev/null +++ b/src/providers/seventv/SeventvPersonalEmotes.cpp @@ -0,0 +1,143 @@ +#include "providers/seventv/SeventvPersonalEmotes.hpp" + +#include "providers/seventv/SeventvEmotes.hpp" +#include "singletons/Settings.hpp" + +#include + +#include +#include +#include +#include + +namespace chatterino { + +void SeventvPersonalEmotes::initialize(Settings &settings, Paths & /*paths*/) +{ + settings.enableSevenTVPersonalEmotes.connect( + [this]() { + std::unique_lock lock(this->mutex_); + this->enabled_ = Settings::instance().enableSevenTVPersonalEmotes; + }, + this->signalHolder_); +} + +void SeventvPersonalEmotes::createEmoteSet(const QString &id) +{ + std::unique_lock lock(this->mutex_); + if (!this->emoteSets_.contains(id)) + { + this->emoteSets_.emplace(id, std::make_shared()); + } +} + +boost::optional> + SeventvPersonalEmotes::assignUserToEmoteSet(const QString &emoteSetID, + const QString &userTwitchID) +{ + std::unique_lock lock(this->mutex_); + if (!this->userEmoteSets_.contains(userTwitchID)) + { + this->userEmoteSets_.emplace(userTwitchID, emoteSetID); + + auto set = this->emoteSets_.find(emoteSetID); + if (set == this->emoteSets_.end()) + { + return boost::none; + } + return set->second.get(); // copy the shared_ptr + } + return boost::none; +} + +void SeventvPersonalEmotes::updateEmoteSet( + const QString &id, const seventv::eventapi::EmoteAddDispatch &dispatch) +{ + std::unique_lock lock(this->mutex_); + auto emoteSet = this->emoteSets_.find(id); + if (emoteSet != this->emoteSets_.end()) + { + // Make sure this emote is actually new to avoid copying the map + if (emoteSet->second.get()->contains( + EmoteName{dispatch.emoteJson["name"].toString()})) + { + return; + } + SeventvEmotes::addEmote(emoteSet->second, dispatch, + SeventvEmoteSetKind::Personal); + } +} +void SeventvPersonalEmotes::updateEmoteSet( + const QString &id, const seventv::eventapi::EmoteUpdateDispatch &dispatch) +{ + std::unique_lock lock(this->mutex_); + auto emoteSet = this->emoteSets_.find(id); + if (emoteSet != this->emoteSets_.end()) + { + SeventvEmotes::updateEmote(emoteSet->second, dispatch, + SeventvEmoteSetKind::Personal); + } +} +void SeventvPersonalEmotes::updateEmoteSet( + const QString &id, const seventv::eventapi::EmoteRemoveDispatch &dispatch) +{ + std::unique_lock lock(this->mutex_); + auto emoteSet = this->emoteSets_.find(id); + if (emoteSet != this->emoteSets_.end()) + { + SeventvEmotes::removeEmote(emoteSet->second, dispatch); + } +} + +void SeventvPersonalEmotes::addEmoteSetForUser(const QString &emoteSetID, + EmoteMap &&map, + const QString &userTwitchID) +{ + std::unique_lock lock(this->mutex_); + this->emoteSets_.emplace(emoteSetID, std::make_shared(map)); + this->userEmoteSets_[userTwitchID] = emoteSetID; +} + +bool SeventvPersonalEmotes::hasEmoteSet(const QString &id) const +{ + std::shared_lock lock(this->mutex_); + return this->emoteSets_.contains(id); +} + +boost::optional> + SeventvPersonalEmotes::getEmoteSetForUser(const QString &userID) const +{ + std::shared_lock lock(this->mutex_); + if (!this->enabled_) + { + return boost::none; + } + + auto id = this->userEmoteSets_.find(userID); + if (id == this->userEmoteSets_.end()) + { + return boost::none; + } + auto set = this->emoteSets_.find(id->second); + if (set == this->emoteSets_.end()) + { + return boost::none; + } + return set->second.get(); // copy the shared_ptr +} + +boost::optional SeventvPersonalEmotes::getEmoteForUser( + const QString &userID, const EmoteName &emoteName) const +{ + return this->getEmoteSetForUser(userID).flat_map( + [emoteName](const auto &map) -> boost::optional { + auto it = map->find(emoteName); + if (it == map->end()) + { + return boost::none; + } + return it->second; + }); +} + +} // namespace chatterino diff --git a/src/providers/seventv/SeventvPersonalEmotes.hpp b/src/providers/seventv/SeventvPersonalEmotes.hpp new file mode 100644 index 00000000000..dbdd7deb3e9 --- /dev/null +++ b/src/providers/seventv/SeventvPersonalEmotes.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "common/Atomic.hpp" +#include "common/Singleton.hpp" +#include "messages/Emote.hpp" +#include "providers/seventv/eventapi/Dispatch.hpp" + +#include +#include + +#include +#include +#include + +namespace chatterino { + +class SeventvPersonalEmotes : public Singleton +{ +public: + void initialize(Settings &settings, Paths &paths) override; + + void createEmoteSet(const QString &id); + + // Returns the emote-map of this set if it's new. + boost::optional> assignUserToEmoteSet( + const QString &emoteSetID, const QString &userTwitchID); + + void updateEmoteSet(const QString &id, + const seventv::eventapi::EmoteAddDispatch &dispatch); + void updateEmoteSet(const QString &id, + const seventv::eventapi::EmoteUpdateDispatch &dispatch); + void updateEmoteSet(const QString &id, + const seventv::eventapi::EmoteRemoveDispatch &dispatch); + + void addEmoteSetForUser(const QString &emoteSetID, EmoteMap &&map, + const QString &userTwitchID); + + bool hasEmoteSet(const QString &id) const; + + boost::optional> getEmoteSetForUser( + const QString &userID) const; + + boost::optional getEmoteForUser(const QString &userID, + const EmoteName &emoteName) const; + +private: + // emoteSetID => emoteSet + std::unordered_map>> + emoteSets_; + // userID => emoteSetID + std::unordered_map userEmoteSets_; + + bool enabled_ = true; + pajlada::Signals::SignalHolder signalHolder_; + + mutable std::shared_mutex mutex_; +}; + +} // namespace chatterino diff --git a/src/providers/seventv/eventapi/Dispatch.cpp b/src/providers/seventv/eventapi/Dispatch.cpp index 06609ac5cf1..797316b0863 100644 --- a/src/providers/seventv/eventapi/Dispatch.cpp +++ b/src/providers/seventv/eventapi/Dispatch.cpp @@ -138,4 +138,15 @@ bool EntitlementCreateDeleteDispatch::validate() const !this->refID.isEmpty() && this->kind != CosmeticKind::INVALID; } +EmoteSetCreateDispatch::EmoteSetCreateDispatch(const QJsonObject &emoteSet) + : emoteSetID(emoteSet["id"].toString()) + , isPersonal((emoteSet["flags"].toInt() & 4) != 0) +{ +} + +bool EmoteSetCreateDispatch::validate() const +{ + return !this->emoteSetID.isEmpty(); +} + } // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/Dispatch.hpp b/src/providers/seventv/eventapi/Dispatch.hpp index 04bad159bec..85011455ef4 100644 --- a/src/providers/seventv/eventapi/Dispatch.hpp +++ b/src/providers/seventv/eventapi/Dispatch.hpp @@ -90,4 +90,13 @@ struct EntitlementCreateDeleteDispatch { bool validate() const; }; +struct EmoteSetCreateDispatch { + QString emoteSetID; + bool isPersonal; + + EmoteSetCreateDispatch(const QJsonObject &emoteSet); + + bool validate() const; +}; + } // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/Subscription.hpp b/src/providers/seventv/eventapi/Subscription.hpp index a54f1ed1c08..1a36811a56b 100644 --- a/src/providers/seventv/eventapi/Subscription.hpp +++ b/src/providers/seventv/eventapi/Subscription.hpp @@ -12,7 +12,10 @@ namespace chatterino::seventv::eventapi { // https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#subscription-types enum class SubscriptionType { + AnyEmoteSet, + CreateEmoteSet, UpdateEmoteSet, + UpdateUser, AnyCosmetic, @@ -92,6 +95,10 @@ constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name< using chatterino::seventv::eventapi::SubscriptionType; switch (value) { + case SubscriptionType::AnyEmoteSet: + return "emote_set.*"; + case SubscriptionType::CreateEmoteSet: + return "emote_set.create"; case SubscriptionType::UpdateEmoteSet: return "emote_set.update"; case SubscriptionType::UpdateUser: diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index c0ddf88ff44..22c5e525ce1 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -12,6 +12,8 @@ #include "messages/MessageBuilder.hpp" #include "providers/irc/IrcMessageBuilder.hpp" #include "providers/IvrApi.hpp" +#include "providers/seventv/SeventvEmotes.hpp" +#include "providers/seventv/SeventvPersonalEmotes.hpp" #include "providers/twitch/api/Helix.hpp" #include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchUser.hpp" @@ -459,15 +461,50 @@ void TwitchAccount::loadSeventvUserID() static const QString seventvUserInfoUrl = QStringLiteral("https://7tv.io/v3/users/twitch/%1"); + const auto loadPersonalEmotes = [](const QString &twitchUserID, + const QString &emoteSetID) { + SeventvEmotes::getEmoteSet( + emoteSetID, + [twitchUserID, emoteSetID](auto &&emoteMap, + const auto & /*emoteSetName*/) { + Application::instance->seventvPersonalEmotes + ->addEmoteSetForUser( + emoteSetID, std::forward(emoteMap), + twitchUserID); + }, + [twitchUserID, emoteSetID](const auto &error) { + qCDebug(chatterinoSeventv) + << "Failed to fetch personal emote-set. emote-set-id:" + << emoteSetID << "twitch-user-id" << twitchUserID + << "error:" << error; + }); + }; + NetworkRequest(seventvUserInfoUrl.arg(this->getUserId()), NetworkRequestType::Get) .timeout(20000) - .onSuccess([this](const auto &response) { + .onSuccess([this, loadPersonalEmotes](const auto &response) { const auto json = response.parseJson(); - const auto id = json["user"].toObject()["id"].toString(); - if (!id.isEmpty()) + const auto user = json["user"].toObject(); + const auto id = user["id"].toString(); + if (id.isEmpty()) + { + return Success; + } + + this->seventvUserID_ = id; + + for (const auto &emoteSetJson : user["emote_sets"].toArray()) { - this->seventvUserID_ = id; + const auto emoteSet = emoteSetJson.toObject(); + if (SeventvEmoteSetFlags( + SeventvEmoteSetFlag(emoteSet["flags"].toInt())) + .has(SeventvEmoteSetFlag::Personal)) + { + loadPersonalEmotes(this->getUserId(), + emoteSet["id"].toString()); + break; + } } return Success; }) diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index de653eaba74..33c4d372d6f 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -8,6 +8,7 @@ #include "common/QLogging.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/notifications/NotificationController.hpp" +#include "debug/AssertInGuiThread.hpp" #include "messages/Emote.hpp" #include "messages/Image.hpp" #include "messages/Link.hpp" @@ -46,6 +47,8 @@ #include #include +#include + namespace chatterino { namespace { #if QT_VERSION < QT_VERSION_CHECK(6, 1, 0) @@ -1650,4 +1653,102 @@ void TwitchChannel::listenSevenTVCosmetics() } } +void TwitchChannel::upsertPersonalSeventvEmotes( + const QString &userLogin, const std::shared_ptr &emoteMap) +{ + assertInGuiThread(); + auto snapshot = this->getMessageSnapshot(); + if (snapshot.size() == 0) + { + return; + } + + const auto findMessage = [&]() -> std::optional { + auto end = std::max(0, (ptrdiff_t)snapshot.size() - 5); + + // explicitly using signed integers here to represent '-1' + for (ptrdiff_t i = (ptrdiff_t)snapshot.size() - 1; i >= end; i--) + { + const auto &message = snapshot[i]; + if (message->loginName == userLogin) + { + return message; + } + } + + return std::nullopt; + }; + + const auto message = findMessage(); + if (!message) + { + return; + } + + auto cloned = message.value()->cloneWith([&](Message &message) { + // We create a new vector of elements, + // if we encounter a `TextElement` that contains any emote, + // we insert an `EmoteElement` at the position. + std::vector> elements; + elements.reserve(message.elements.size()); + + std::for_each( + std::make_move_iterator(message.elements.begin()), + std::make_move_iterator(message.elements.end()), + [&](auto &&element) { + auto *elementPtr = element.get(); + auto *textElement = dynamic_cast(elementPtr); + + // Check if this contains the message text + if (textElement != nullptr && + textElement->getFlags().has(MessageElementFlag::Text)) + { + std::vector words; + // Append the text element and clear the vector. + const auto flush = [&]() { + elements.emplace_back(std::make_unique( + std::move(words), textElement->getFlags(), + textElement->color(), textElement->style())); + words.clear(); + }; + + // Search for a word that matches any emote. + for (const auto &word : textElement->words()) + { + auto emoteIt = emoteMap->find(EmoteName{word.text}); + if (emoteIt != emoteMap->cend()) + { + MessageElementFlags emoteFlags( + MessageElementFlag::SevenTVEmote); + if (emoteIt->second->zeroWidth) + { + emoteFlags.set( + MessageElementFlag::ZeroWidthEmote); + } + + flush(); + elements.emplace_back( + std::make_unique(emoteIt->second, + emoteFlags)); + } + else + { + words.emplace_back(word); + } + } + flush(); + } + else + { + elements.emplace_back( + std::forward(element)); + } + }); + + message.elements = std::move(elements); + }); + + this->replaceMessage(message.value(), cloned); +} + } // namespace chatterino diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index a604146c088..d8853d29d05 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -174,6 +174,11 @@ class TwitchChannel : public Channel, public ChannelChatters void updateSeventvData(const QString &newUserID, const QString &newEmoteSetID); + // Update the user's last message and insert the personal emotes if necessary. + void upsertPersonalSeventvEmotes( + const QString &userLogin, + const std::shared_ptr &emoteMap); + // Badges boost::optional ffzCustomModBadge() const; boost::optional ffzCustomVipBadge() const; diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index c8c4da40996..4aaeee49dd8 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -14,6 +14,7 @@ #include "providers/colors/ColorProvider.hpp" #include "providers/ffz/FfzBadges.hpp" #include "providers/seventv/SeventvBadges.hpp" +#include "providers/seventv/SeventvPersonalEmotes.hpp" #include "providers/twitch/api/Helix.hpp" #include "providers/twitch/ChannelPointReward.hpp" #include "providers/twitch/PubSubActions.hpp" @@ -1025,13 +1026,25 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name) auto emote = boost::optional{}; // Emote order: + // - 7TV Personal // - FrankerFaceZ Channel // - BetterTTV Channel // - 7TV Channel // - FrankerFaceZ Global // - BetterTTV Global // - 7TV Global - if (this->twitchChannel && (emote = this->twitchChannel->ffzEmote(name))) + if (this->twitchChannel != nullptr && + (emote = + app->seventvPersonalEmotes->getEmoteForUser(this->userId_, name))) + { + flags = MessageElementFlag::SevenTVEmote; + if (emote.value()->zeroWidth) + { + flags.set(MessageElementFlag::ZeroWidthEmote); + } + } + else if (this->twitchChannel && + (emote = this->twitchChannel->ffzEmote(name))) { flags = MessageElementFlag::FfzEmote; } diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 49586e5280e..688ee01f9fb 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -236,6 +236,8 @@ class Settings : public ABSettings, public ConcurrentSettings BoolSetting enableFFZChannelEmotes = {"/emotes/ffz/channel", true}; BoolSetting enableSevenTVGlobalEmotes = {"/emotes/seventv/global", true}; BoolSetting enableSevenTVChannelEmotes = {"/emotes/seventv/channel", true}; + BoolSetting enableSevenTVPersonalEmotes = {"/emotes/seventv/personal", + true}; BoolSetting enableSevenTVEventAPI = {"/emotes/seventv/eventapi", true}; /// Links diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index c81abd04d6c..f49855401a4 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -10,6 +10,7 @@ #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" #include "messages/MessageElement.hpp" +#include "providers/seventv/SeventvPersonalEmotes.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchIrcServer.hpp" @@ -429,6 +430,14 @@ void EmotePopup::loadChannel(ChannelPtr channel) "7TV", MessageElementFlag::SevenTVEmote); } + // personal + if (const auto map = getApp()->seventvPersonalEmotes->getEmoteSetForUser( + getApp()->accounts->twitch.getCurrent()->getUserId())) + { + addEmotes(*subChannel, *map.get(), "7TV", + MessageElementFlag::SevenTVEmote); + } + this->globalEmotesView_->setChannel(globalChannel); this->subEmotesView_->setChannel(subChannel); this->channelEmotesView_->setChannel(channelChannel); @@ -525,6 +534,17 @@ void EmotePopup::filterTwitchEmotes(std::shared_ptr searchChannel, addEmotes(*searchChannel, seventvChannelEmotes, "7TV (Channel)", MessageElementFlag::SevenTVEmote); } + + if (const auto map = getApp()->seventvPersonalEmotes->getEmoteSetForUser( + getApp()->accounts->twitch.getCurrent()->getUserId())) + { + auto seventvPersonalEmotes = filterEmoteMap(searchText, map.get()); + if (!seventvPersonalEmotes.empty()) + { + addEmotes(*searchChannel, seventvPersonalEmotes, + "SevenTV (Personal)", MessageElementFlag::SevenTVEmote); + } + } } void EmotePopup::filterEmotes(const QString &searchText) diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 6a1c324fb79..dbf6d929799 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -438,8 +438,14 @@ void GeneralPage::initLayout(GeneralPageView &layout) layout.addCheckbox("Show FFZ channel emotes", s.enableFFZChannelEmotes); layout.addCheckbox("Show 7TV global emotes", s.enableSevenTVGlobalEmotes); layout.addCheckbox("Show 7TV channel emotes", s.enableSevenTVChannelEmotes); - layout.addCheckbox("Enable 7TV live emote updates (requires restart)", - s.enableSevenTVEventAPI); + layout.addCheckbox("Show 7TV personal emotes", + s.enableSevenTVPersonalEmotes, false, + "This requires '7TV live updates' to work."); + layout.addCheckbox("Enable 7TV live updates (requires restart)", + s.enableSevenTVEventAPI, false, + "When enabled, channel emotes will get updated " + "automatically (no reload required) and cosmetics " + "(badges/paints/personal emotes) will get updated."); layout.addTitle("Streamer Mode"); layout.addDescription( diff --git a/src/widgets/splits/InputCompletionPopup.cpp b/src/widgets/splits/InputCompletionPopup.cpp index eb6a2ace747..02a476e5a2a 100644 --- a/src/widgets/splits/InputCompletionPopup.cpp +++ b/src/widgets/splits/InputCompletionPopup.cpp @@ -6,6 +6,7 @@ #include "providers/bttv/BttvEmotes.hpp" #include "providers/ffz/FfzEmotes.hpp" #include "providers/seventv/SeventvEmotes.hpp" +#include "providers/seventv/SeventvPersonalEmotes.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchIrcServer.hpp" @@ -100,6 +101,13 @@ void InputCompletionPopup::updateEmotes(const QString &text, ChannelPtr channel) if (tc) { + if (const auto map = + getApp()->seventvPersonalEmotes->getEmoteSetForUser( + getApp()->accounts->twitch.getCurrent()->getUserId())) + { + addEmotes(emotes, *map.get(), text, "Personal 7TV"); + } + // TODO extract "Channel {BetterTTV,7TV,FrankerFaceZ}" text into a #define. if (auto bttv = tc->bttvEmotes()) {