diff --git a/CHANGELOG.md b/CHANGELOG.md index b62c42912..5499ebfe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Major: Added support for Twitch's Chat Replies. [Wiki Page](https://wiki.chatterino.com/Features/#message-replies) (#3722, #3989, #4041, #4047, #4055) - Major: Added multi-channel searching to search dialog via keyboard shortcut. (Ctrl+Shift+F by default) (#3694, #3875) +- Major: Added support for emotes and badges from [7TV](https://7tv.app). [Wiki Page](https://wiki.chatterino.com/Third_party_services/#7tv) (#4002) - Minor: Added highlights for `Elevated Messages`. (#4016) - Minor: Removed total views from the usercard, as Twitch no longer updates the number. (#3792) - Minor: Load missing messages from Recent Messages API upon reconnecting (#3878, #3932) diff --git a/src/Application.cpp b/src/Application.cpp index bd37ce2e0..de2d49d84 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -18,6 +18,8 @@ #include "providers/ffz/FfzBadges.hpp" #include "providers/ffz/FfzEmotes.hpp" #include "providers/irc/Irc2.hpp" +#include "providers/seventv/SeventvBadges.hpp" +#include "providers/seventv/SeventvEmotes.hpp" #include "providers/twitch/PubSubManager.hpp" #include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp" @@ -72,6 +74,7 @@ Application::Application(Settings &_settings, Paths &_paths) , twitch(&this->emplace()) , chatterinoBadges(&this->emplace()) , ffzBadges(&this->emplace()) + , seventvBadges(&this->emplace()) , logging(&this->emplace()) { this->instance = this; @@ -199,6 +202,16 @@ int Application::run(QApplication &qtApp) this->twitch->reloadAllFFZChannelEmotes(); }, false); + getSettings()->enableSevenTVGlobalEmotes.connect( + [this] { + this->twitch->reloadSevenTVGlobalEmotes(); + }, + false); + getSettings()->enableSevenTVChannelEmotes.connect( + [this] { + this->twitch->reloadAllSevenTVChannelEmotes(); + }, + false); return qtApp.exec(); } diff --git a/src/Application.hpp b/src/Application.hpp index ee0d1417c..4d5bc93a2 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -29,6 +29,7 @@ class Fonts; class Toasts; class ChatterinoBadges; class FfzBadges; +class SeventvBadges; class IApplication { @@ -86,6 +87,7 @@ public: TwitchIrcServer *const twitch{}; ChatterinoBadges *const chatterinoBadges{}; FfzBadges *const ffzBadges{}; + SeventvBadges *const seventvBadges{}; /*[[deprecated]]*/ Logging *const logging{}; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c9618370e..ea627e44f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -214,6 +214,11 @@ set(SOURCE_FILES providers/irc/IrcServer.cpp providers/irc/IrcServer.hpp + providers/seventv/SeventvBadges.cpp + providers/seventv/SeventvBadges.hpp + providers/seventv/SeventvEmotes.cpp + providers/seventv/SeventvEmotes.hpp + providers/twitch/ChannelPointReward.cpp providers/twitch/ChannelPointReward.hpp providers/twitch/IrcMessageHandler.cpp diff --git a/src/common/CompletionModel.cpp b/src/common/CompletionModel.cpp index c03fc830d..7a544b41d 100644 --- a/src/common/CompletionModel.cpp +++ b/src/common/CompletionModel.cpp @@ -141,6 +141,11 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) } } + // 7TV Global + for (auto &emote : *getApp()->twitch->getSeventvEmotes().globalEmotes()) + { + addString(emote.first.string, TaggedString::Type::SeventvGlobalEmote); + } // Bttv Global for (auto &emote : *getApp()->twitch->getBttvEmotes().emotes()) { @@ -198,6 +203,11 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) } } + // 7TV Channel + for (auto &emote : *tc->seventvEmotes()) + { + addString(emote.first.string, TaggedString::Type::SeventvChannelEmote); + } // Bttv Channel for (auto &emote : *tc->bttvEmotes()) { diff --git a/src/common/CompletionModel.hpp b/src/common/CompletionModel.hpp index ee810bbe6..0d80c4aa6 100644 --- a/src/common/CompletionModel.hpp +++ b/src/common/CompletionModel.hpp @@ -22,6 +22,8 @@ class CompletionModel : public QAbstractListModel FFZChannelEmote, BTTVGlobalEmote, BTTVChannelEmote, + SeventvGlobalEmote, + SeventvChannelEmote, TwitchGlobalEmote, TwitchLocalEmote, TwitchSubscriberEmote, diff --git a/src/common/QLogging.cpp b/src/common/QLogging.cpp index 9f4346a39..ede6a45bc 100644 --- a/src/common/QLogging.cpp +++ b/src/common/QLogging.cpp @@ -34,6 +34,7 @@ Q_LOGGING_CATEGORY(chatterinoPubSub, "chatterino.pubsub", logThreshold); Q_LOGGING_CATEGORY(chatterinoRecentMessages, "chatterino.recentmessages", logThreshold); Q_LOGGING_CATEGORY(chatterinoSettings, "chatterino.settings", logThreshold); +Q_LOGGING_CATEGORY(chatterinoSeventv, "chatterino.seventv", logThreshold); Q_LOGGING_CATEGORY(chatterinoStreamerMode, "chatterino.streamermode", logThreshold); Q_LOGGING_CATEGORY(chatterinoStreamlink, "chatterino.streamlink", logThreshold); diff --git a/src/common/QLogging.hpp b/src/common/QLogging.hpp index 79ef9d69c..4f27d0ea9 100644 --- a/src/common/QLogging.hpp +++ b/src/common/QLogging.hpp @@ -26,6 +26,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoNuulsuploader); Q_DECLARE_LOGGING_CATEGORY(chatterinoPubSub); Q_DECLARE_LOGGING_CATEGORY(chatterinoRecentMessages); Q_DECLARE_LOGGING_CATEGORY(chatterinoSettings); +Q_DECLARE_LOGGING_CATEGORY(chatterinoSeventv); Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamerMode); Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamlink); Q_DECLARE_LOGGING_CATEGORY(chatterinoTokenizer); diff --git a/src/messages/Emote.hpp b/src/messages/Emote.hpp index d7c19d238..380f57dcb 100644 --- a/src/messages/Emote.hpp +++ b/src/messages/Emote.hpp @@ -14,6 +14,7 @@ struct Emote { ImageSet images; Tooltip tooltip; Url homePage; + bool zeroWidth; // FOURTF: no solution yet, to be refactored later const QString &getCopyString() const diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index 4cc47873a..a61816c4e 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -37,6 +37,7 @@ enum class MessageElementFlag : int64_t { TwitchEmoteImage = (1LL << 4), TwitchEmoteText = (1LL << 5), TwitchEmote = TwitchEmoteImage | TwitchEmoteText, + BttvEmoteImage = (1LL << 6), BttvEmoteText = (1LL << 7), BttvEmote = BttvEmoteImage | BttvEmoteText, @@ -47,8 +48,15 @@ enum class MessageElementFlag : int64_t { FfzEmoteImage = (1LL << 9), FfzEmoteText = (1LL << 10), FfzEmote = FfzEmoteImage | FfzEmoteText, - EmoteImages = TwitchEmoteImage | BttvEmoteImage | FfzEmoteImage, - EmoteText = TwitchEmoteText | BttvEmoteText | FfzEmoteText, + + SevenTVEmoteImage = (1LL << 34), + SevenTVEmoteText = (1LL << 35), + SevenTVEmote = SevenTVEmoteImage | SevenTVEmoteText, + + EmoteImages = + TwitchEmoteImage | BttvEmoteImage | FfzEmoteImage | SevenTVEmoteImage, + EmoteText = + TwitchEmoteText | BttvEmoteText | FfzEmoteText | SevenTVEmoteText, BitsStatic = (1LL << 11), BitsAnimated = (1LL << 12), @@ -89,6 +97,15 @@ enum class MessageElementFlag : int64_t { // - Chatterino gnome badge BadgeChatterino = (1LL << 18), + // Slot 7: 7TV + // - 7TV Admin + // - 7TV Dungeon Mistress + // - 7TV Moderator + // - 7TV Subscriber + // - 7TV Translator + // - 7TV Contributor + BadgeSevenTV = (1LL << 36), + // Slot 7: FrankerFaceZ // - FFZ developer badge // - FFZ bot badge @@ -96,7 +113,8 @@ enum class MessageElementFlag : int64_t { BadgeFfz = (1LL << 19), Badges = BadgeGlobalAuthority | BadgePredictions | BadgeChannelAuthority | - BadgeSubscription | BadgeVanity | BadgeChatterino | BadgeFfz, + BadgeSubscription | BadgeVanity | BadgeChatterino | BadgeSevenTV | + BadgeFfz, ChannelName = (1LL << 20), @@ -123,7 +141,7 @@ enum class MessageElementFlag : int64_t { OriginalLink = (1LL << 30), // ZeroWidthEmotes are emotes that are supposed to overlay over any pre-existing emotes - // e.g. BTTV's SoSnowy during christmas season + // e.g. BTTV's SoSnowy during christmas season or 7TV's RainTime ZeroWidthEmote = (1LL << 31), // for elements of the message reply @@ -132,9 +150,12 @@ enum class MessageElementFlag : int64_t { // for the reply button element ReplyButton = (1LL << 33), + // (1LL << 34) through (1LL << 36) are occupied by + // SevenTVEmoteImage, SevenTVEmoteText, and BadgeSevenTV, + Default = Timestamp | Badges | Username | BitsStatic | FfzEmoteImage | - BttvEmoteImage | TwitchEmoteImage | BitsAmount | Text | - AlwaysShow, + BttvEmoteImage | SevenTVEmoteImage | TwitchEmoteImage | + BitsAmount | Text | AlwaysShow, }; using MessageElementFlags = FlagsEnum; diff --git a/src/providers/seventv/SeventvBadges.cpp b/src/providers/seventv/SeventvBadges.cpp new file mode 100644 index 000000000..2fb7f4ef5 --- /dev/null +++ b/src/providers/seventv/SeventvBadges.cpp @@ -0,0 +1,76 @@ +#include "providers/seventv/SeventvBadges.hpp" + +#include "common/NetworkRequest.hpp" +#include "common/Outcome.hpp" +#include "messages/Emote.hpp" + +#include +#include + +#include + +namespace chatterino { + +void SeventvBadges::initialize(Settings & /*settings*/, Paths & /*paths*/) +{ + this->loadSeventvBadges(); +} + +boost::optional SeventvBadges::getBadge(const UserId &id) +{ + std::shared_lock lock(this->mutex_); + + auto it = this->badgeMap_.find(id.string); + if (it != this->badgeMap_.end()) + { + return this->emotes_[it->second]; + } + return boost::none; +} + +void SeventvBadges::loadSeventvBadges() +{ + // Cosmetics will work differently in v3, until this is ready + // we'll use this endpoint. + static QUrl url("https://7tv.io/v2/cosmetics"); + + static QUrlQuery urlQuery; + // valid user_identifier values: "object_id", "twitch_id", "login" + urlQuery.addQueryItem("user_identifier", "twitch_id"); + + url.setQuery(urlQuery); + + NetworkRequest(url) + .onSuccess([this](const NetworkResult &result) -> Outcome { + auto root = result.parseJson(); + + std::shared_lock lock(this->mutex_); + + int index = 0; + for (const auto &jsonBadge : root.value("badges").toArray()) + { + auto badge = jsonBadge.toObject(); + auto urls = badge.value("urls").toArray(); + auto emote = + Emote{EmoteName{}, + ImageSet{Url{urls.at(0).toArray().at(1).toString()}, + Url{urls.at(1).toArray().at(1).toString()}, + Url{urls.at(2).toArray().at(1).toString()}}, + Tooltip{badge.value("tooltip").toString()}, Url{}}; + + this->emotes_.push_back( + std::make_shared(std::move(emote))); + + for (const auto &user : badge.value("users").toArray()) + { + this->badgeMap_[user.toString()] = index; + } + ++index; + } + + return Success; + }) + .execute(); +} + +} // namespace chatterino diff --git a/src/providers/seventv/SeventvBadges.hpp b/src/providers/seventv/SeventvBadges.hpp new file mode 100644 index 000000000..2d6d021a2 --- /dev/null +++ b/src/providers/seventv/SeventvBadges.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "common/Aliases.hpp" +#include "util/QStringHash.hpp" + +#include +#include + +#include +#include +#include + +namespace chatterino { + +struct Emote; +using EmotePtr = std::shared_ptr; + +class SeventvBadges : public Singleton +{ +public: + void initialize(Settings &settings, Paths &paths) override; + + boost::optional getBadge(const UserId &id); + +private: + void loadSeventvBadges(); + + // Mutex for both `badgeMap_` and `emotes_` + std::shared_mutex mutex_; + + std::unordered_map badgeMap_; + std::vector emotes_; +}; + +} // namespace chatterino diff --git a/src/providers/seventv/SeventvEmotes.cpp b/src/providers/seventv/SeventvEmotes.cpp new file mode 100644 index 000000000..4bce11f38 --- /dev/null +++ b/src/providers/seventv/SeventvEmotes.cpp @@ -0,0 +1,365 @@ +#include "providers/seventv/SeventvEmotes.hpp" + +#include "common/Common.hpp" +#include "common/NetworkRequest.hpp" +#include "common/QLogging.hpp" +#include "messages/Emote.hpp" +#include "messages/Image.hpp" +#include "messages/ImageSet.hpp" +#include "messages/MessageBuilder.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "singletons/Settings.hpp" + +#include +#include +#include +#include + +/** + * # References + * + * - EmoteSet: https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote-set.model.go#L8-L18 + * - ActiveEmote: https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote-set.model.go#L20-L27 + * - EmotePartial (emoteData): https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote.model.go#L24-L34 + * - ImageHost: https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/model.go#L36-L39 + * - ImageFile: https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/model.go#L41-L48 + */ +namespace { + +using namespace chatterino; + +// These declarations won't throw an exception. +const QString CHANNEL_HAS_NO_EMOTES("This channel has no 7TV channel emotes."); +const QString EMOTE_LINK_FORMAT("https://7tv.app/emotes/%1"); + +// TODO(nerix): add links to documentation (7tv.io) +const QString API_URL_USER("https://7tv.io/v3/users/twitch/%1"); +const QString API_URL_GLOBAL_EMOTE_SET("https://7tv.io/v3/emote-sets/global"); + +struct CreateEmoteResult { + Emote emote; + EmoteId id; + EmoteName name; + bool hasImages; +}; + +EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id) +{ + static std::unordered_map> cache; + static std::mutex mutex; + + return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id); +} + +/** + * This decides whether an emote should be displayed + * as zero-width + */ +bool isZeroWidthActive(const QJsonObject &activeEmote) +{ + auto flags = SeventvActiveEmoteFlags( + SeventvActiveEmoteFlag(activeEmote.value("flags").toInt())); + return flags.has(SeventvActiveEmoteFlag::ZeroWidth); +} + +/** + * This is only an indicator if an emote should be added + * as zero-width or not. The user can still overwrite this. + */ +bool isZeroWidthRecommended(const QJsonObject &emoteData) +{ + auto flags = + SeventvEmoteFlags(SeventvEmoteFlag(emoteData.value("flags").toInt())); + return flags.has(SeventvEmoteFlag::ZeroWidth); +} + +ImageSet makeImageSet(const QJsonObject &emoteData) +{ + auto host = emoteData["host"].toObject(); + // "//cdn.7tv[...]" + auto baseUrl = host["url"].toString(); + auto files = host["files"].toArray(); + + // TODO: emit four images + std::array sizes; + double baseWidth = 0.0; + int nextSize = 0; + + for (auto fileItem : files) + { + if (nextSize >= sizes.size()) + { + break; + } + + auto file = fileItem.toObject(); + if (file["format"].toString() != "WEBP") + { + continue; // We only use webp + } + + double width = file["width"].toDouble(); + double scale = 1.0; // in relation to first image + if (baseWidth > 0.0) + { + scale = baseWidth / width; + } + else + { + // => this is the first image + baseWidth = width; + } + + auto image = Image::fromUrl( + {QString("https:%1/%2").arg(baseUrl, file["name"].toString())}, + scale); + + sizes.at(nextSize) = image; + nextSize++; + } + + if (nextSize < sizes.size()) + { + // this should be really rare + // this means we didn't get all sizes of an emote + if (nextSize == 0) + { + qCDebug(chatterinoSeventv) + << "Got file list without any eligible files"; + // When this emote is typed, chatterino will crash. + return ImageSet{}; + } + for (; nextSize < sizes.size(); nextSize++) + { + sizes.at(nextSize) = Image::getEmpty(); + } + } + + return ImageSet{sizes[0], sizes[1], sizes[2]}; +} + +Tooltip createTooltip(const QString &name, const QString &author, bool isGlobal) +{ + return Tooltip{QString("%1
%2 7TV Emote
By: %3") + .arg(name, isGlobal ? "Global" : "Channel", + author.isEmpty() ? "" : author)}; +} + +Tooltip createAliasedTooltip(const QString &name, const QString &baseName, + const QString &author, bool isGlobal) +{ + return Tooltip{QString("%1
Alias to %2
%3 7TV Emote
By: %4") + .arg(name, baseName, isGlobal ? "Global" : "Channel", + author.isEmpty() ? "" : author)}; +} + +CreateEmoteResult createEmote(const QJsonObject &activeEmote, + const QJsonObject &emoteData, bool isGlobal) +{ + auto emoteId = EmoteId{activeEmote["id"].toString()}; + auto emoteName = EmoteName{activeEmote["name"].toString()}; + auto author = + EmoteAuthor{emoteData["owner"].toObject()["display_name"].toString()}; + auto baseEmoteName = emoteData["name"].toString(); + bool zeroWidth = isZeroWidthActive(activeEmote); + bool aliasedName = emoteName.string != baseEmoteName; + auto tooltip = + aliasedName ? createAliasedTooltip(emoteName.string, baseEmoteName, + author.string, isGlobal) + : createTooltip(emoteName.string, author.string, isGlobal); + auto imageSet = makeImageSet(emoteData); + + auto emote = Emote({emoteName, imageSet, tooltip, + Url{EMOTE_LINK_FORMAT.arg(emoteId.string)}, zeroWidth}); + + return {emote, emoteId, emoteName, !emote.images.getImage1()->isEmpty()}; +} + +bool checkEmoteVisibility(const QJsonObject &emoteData) +{ + if (!emoteData["listed"].toBool() && + !getSettings()->showUnlistedSevenTVEmotes) + { + return false; + } + auto flags = + SeventvEmoteFlags(SeventvEmoteFlag(emoteData["flags"].toInt())); + return !flags.has(SeventvEmoteFlag::ContentTwitchDisallowed); +} + +EmoteMap parseEmotes(const QJsonArray &emoteSetEmotes, bool isGlobal) +{ + auto emotes = EmoteMap(); + + for (const auto &activeEmoteJson : emoteSetEmotes) + { + auto activeEmote = activeEmoteJson.toObject(); + auto emoteData = activeEmote["data"].toObject(); + + if (emoteData.empty() || !checkEmoteVisibility(emoteData)) + { + continue; + } + + auto result = createEmote(activeEmote, emoteData, isGlobal); + if (!result.hasImages) + { + // this shouldn't happen but if it does, it will crash, + // so we don't add the emote + qCDebug(chatterinoSeventv) + << "Emote without images:" << activeEmote; + continue; + } + auto ptr = cachedOrMake(std::move(result.emote), result.id); + emotes[result.name] = ptr; + } + + return emotes; +} + +} // namespace + +namespace chatterino { + +SeventvEmotes::SeventvEmotes() + : global_(std::make_shared()) +{ +} + +std::shared_ptr SeventvEmotes::globalEmotes() const +{ + return this->global_.get(); +} + +boost::optional SeventvEmotes::globalEmote( + const EmoteName &name) const +{ + auto emotes = this->global_.get(); + auto it = emotes->find(name); + + if (it == emotes->end()) + { + return boost::none; + } + return it->second; +} + +void SeventvEmotes::loadGlobalEmotes() +{ + if (!Settings::instance().enableSevenTVGlobalEmotes) + { + this->global_.set(EMPTY_EMOTE_MAP); + return; + } + + qCDebug(chatterinoSeventv) << "Loading 7TV Global Emotes"; + + NetworkRequest(API_URL_GLOBAL_EMOTE_SET, NetworkRequestType::Get) + .timeout(30000) + .onSuccess([this](const NetworkResult &result) -> Outcome { + QJsonArray parsedEmotes = result.parseJson()["emotes"].toArray(); + + auto emoteMap = parseEmotes(parsedEmotes, true); + qCDebug(chatterinoSeventv) + << "Loaded" << emoteMap.size() << "7TV Global Emotes"; + this->global_.set(std::make_shared(std::move(emoteMap))); + + return Success; + }) + .onError([](const NetworkResult &result) { + qCWarning(chatterinoSeventv) + << "Couldn't load 7TV global emotes" << result.getData(); + }) + .execute(); +} + +void SeventvEmotes::loadChannelEmotes(const std::weak_ptr &channel, + const QString &channelId, + std::function callback, + bool manualRefresh) +{ + qCDebug(chatterinoSeventv) + << "Reloading 7TV Channel Emotes" << channelId << manualRefresh; + + NetworkRequest(API_URL_USER.arg(channelId), NetworkRequestType::Get) + .timeout(20000) + .onSuccess([callback = std::move(callback), channel, channelId, + manualRefresh](const NetworkResult &result) -> Outcome { + auto json = result.parseJson(); + auto emoteSet = json["emote_set"].toObject(); + auto parsedEmotes = emoteSet["emotes"].toArray(); + + auto emoteMap = parseEmotes(parsedEmotes, false); + bool hasEmotes = !emoteMap.empty(); + + qCDebug(chatterinoSeventv) + << "Loaded" << emoteMap.size() << "7TV Channel Emotes for" + << channelId << "manual refresh:" << manualRefresh; + + if (hasEmotes) + { + callback(std::move(emoteMap)); + } + + auto shared = channel.lock(); + if (!shared) + { + return Success; + } + + if (manualRefresh) + { + if (hasEmotes) + { + shared->addMessage( + makeSystemMessage("7TV channel emotes reloaded.")); + } + else + { + shared->addMessage( + makeSystemMessage(CHANNEL_HAS_NO_EMOTES)); + } + } + return Success; + }) + .onError( + [channelId, channel, manualRefresh](const NetworkResult &result) { + auto shared = channel.lock(); + if (!shared) + { + return; + } + if (result.status() == 404) + { + qCWarning(chatterinoSeventv) + << "Error occurred fetching 7TV emotes: " + << result.parseJson(); + if (manualRefresh) + { + shared->addMessage( + makeSystemMessage(CHANNEL_HAS_NO_EMOTES)); + } + } + else if (result.status() == NetworkResult::timedoutStatus) + { + // TODO: Auto retry in case of a timeout, with a delay + qCWarning(chatterinoSeventv) + << "Fetching 7TV emotes for channel" << channelId + << "failed due to timeout"; + shared->addMessage(makeSystemMessage( + "Failed to fetch 7TV channel emotes. (timed out)")); + } + else + { + qCWarning(chatterinoSeventv) + << "Error fetching 7TV emotes for channel" << channelId + << ", error" << result.status(); + shared->addMessage( + makeSystemMessage("Failed to fetch 7TV channel " + "emotes. (unknown error)")); + } + }) + .execute(); +} + +} // namespace chatterino diff --git a/src/providers/seventv/SeventvEmotes.hpp b/src/providers/seventv/SeventvEmotes.hpp new file mode 100644 index 000000000..1569eae12 --- /dev/null +++ b/src/providers/seventv/SeventvEmotes.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include "boost/optional.hpp" +#include "common/Aliases.hpp" +#include "common/Atomic.hpp" +#include "providers/twitch/TwitchChannel.hpp" + +#include + +namespace chatterino { + +// https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote-set.model.go#L29-L36 +enum class SeventvActiveEmoteFlag : int64_t { + None = 0LL, + + // Emote is zero-width + ZeroWidth = (1LL << 0), + + // Overrides Twitch Global emotes with the same name + OverrideTwitchGlobal = (1 << 16), + // Overrides Twitch Subscriber emotes with the same name + OverrideTwitchSubscriber = (1 << 17), + // Overrides BetterTTV emotes with the same name + OverrideBetterTTV = (1 << 18), +}; + +// https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote.model.go#L57-L70 +enum class SeventvEmoteFlag : int64_t { + None = 0LL, + // The emote is private and can only be accessed by its owner, editors and moderators + Private = 1 << 0, + // The emote was verified to be an original creation by the uploader + Authentic = (1LL << 1), + // The emote is recommended to be enabled as Zero-Width + ZeroWidth = (1LL << 8), + + // Content Flags + + // Sexually Suggesive + ContentSexual = (1LL << 16), + // Rapid flashing + ContentEpilepsy = (1LL << 17), + // Edgy or distasteful, may be offensive to some users + ContentEdgy = (1 << 18), + // Not allowed specifically on the Twitch platform + ContentTwitchDisallowed = (1LL << 24), +}; + +using SeventvActiveEmoteFlags = FlagsEnum; +using SeventvEmoteFlags = FlagsEnum; + +struct Emote; +using EmotePtr = std::shared_ptr; +class EmoteMap; + +class SeventvEmotes final +{ +public: + SeventvEmotes(); + + std::shared_ptr globalEmotes() const; + boost::optional globalEmote(const EmoteName &name) const; + void loadGlobalEmotes(); + static void loadChannelEmotes(const std::weak_ptr &channel, + const QString &channelId, + std::function callback, + bool manualRefresh); + +private: + Atomic> global_; +}; + +} // namespace chatterino diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 317a86978..be98364bd 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -10,6 +10,7 @@ #include "providers/RecentMessagesApi.hpp" #include "providers/bttv/BttvEmotes.hpp" #include "providers/bttv/LoadBttvChannelEmote.hpp" +#include "providers/seventv/SeventvEmotes.hpp" #include "providers/twitch/IrcMessageHandler.hpp" #include "providers/twitch/PubSubManager.hpp" #include "providers/twitch/TwitchCommon.hpp" @@ -83,6 +84,7 @@ TwitchChannel::TwitchChannel(const QString &name) name) , bttvEmotes_(std::make_shared()) , ffzEmotes_(std::make_shared()) + , seventvEmotes_(std::make_shared()) , mod_(false) { qCDebug(chatterinoTwitch) << "[TwitchChannel" << name << "] Opened"; @@ -107,6 +109,7 @@ TwitchChannel::TwitchChannel(const QString &name) this->refreshCheerEmotes(); this->refreshFFZChannelEmotes(false); this->refreshBTTVChannelEmotes(false); + this->refreshSevenTVChannelEmotes(false); }); this->connected.connect([this]() { @@ -243,6 +246,26 @@ void TwitchChannel::refreshFFZChannelEmotes(bool manualRefresh) manualRefresh); } +void TwitchChannel::refreshSevenTVChannelEmotes(bool manualRefresh) +{ + if (!Settings::instance().enableSevenTVChannelEmotes) + { + this->seventvEmotes_.set(EMPTY_EMOTE_MAP); + return; + } + + SeventvEmotes::loadChannelEmotes( + weakOf(this), this->roomId(), + [this, weak = weakOf(this)](auto &&emoteMap) { + if (auto shared = weak.lock()) + { + this->seventvEmotes_.set(std::make_shared( + std::forward(emoteMap))); + } + }, + manualRefresh); +} + void TwitchChannel::addChannelPointReward(const ChannelPointReward &reward) { assertInGuiThread(); @@ -553,6 +576,19 @@ boost::optional TwitchChannel::ffzEmote(const EmoteName &name) const return it->second; } +boost::optional TwitchChannel::seventvEmote( + const EmoteName &name) const +{ + auto emotes = this->seventvEmotes_.get(); + auto it = emotes->find(name); + + if (it == emotes->end()) + { + return boost::none; + } + return it->second; +} + std::shared_ptr TwitchChannel::bttvEmotes() const { return this->bttvEmotes_.get(); @@ -563,6 +599,11 @@ std::shared_ptr TwitchChannel::ffzEmotes() const return this->ffzEmotes_.get(); } +std::shared_ptr TwitchChannel::seventvEmotes() const +{ + return this->seventvEmotes_.get(); +} + const QString &TwitchChannel::subscriptionUrl() { return this->subscriptionUrl_; diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index eb475e3e1..8b5db0e32 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -52,6 +52,7 @@ class EmoteMap; class TwitchBadges; class FfzEmotes; class BttvEmotes; +class SeventvEmotes; class TwitchIrcServer; @@ -109,11 +110,14 @@ public: // Emotes boost::optional bttvEmote(const EmoteName &name) const; boost::optional ffzEmote(const EmoteName &name) const; + boost::optional seventvEmote(const EmoteName &name) const; std::shared_ptr bttvEmotes() const; std::shared_ptr ffzEmotes() const; + std::shared_ptr seventvEmotes() const; virtual void refreshBTTVChannelEmotes(bool manualRefresh); virtual void refreshFFZChannelEmotes(bool manualRefresh); + virtual void refreshSevenTVChannelEmotes(bool manualRefresh); // Badges boost::optional ffzCustomModBadge() const; @@ -196,6 +200,7 @@ private: protected: Atomic> bttvEmotes_; Atomic> ffzEmotes_; + Atomic> seventvEmotes_; Atomic> ffzCustomModBadge_; Atomic> ffzCustomVipBadge_; diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index 2526898ca..c10b788af 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -54,6 +54,7 @@ void TwitchIrcServer::initialize(Settings &settings, Paths &paths) this->reloadBTTVGlobalEmotes(); this->reloadFFZGlobalEmotes(); + this->reloadSevenTVGlobalEmotes(); /* Refresh all twitch channel's live status in bulk every 30 seconds after starting chatterino */ QObject::connect(&this->bulkLiveStatusTimer_, &QTimer::timeout, [=] { @@ -467,6 +468,10 @@ const FfzEmotes &TwitchIrcServer::getFfzEmotes() const { return this->ffz; } +const SeventvEmotes &TwitchIrcServer::getSeventvEmotes() const +{ + return this->seventv_; +} void TwitchIrcServer::reloadBTTVGlobalEmotes() { @@ -497,4 +502,19 @@ void TwitchIrcServer::reloadAllFFZChannelEmotes() } }); } + +void TwitchIrcServer::reloadSevenTVGlobalEmotes() +{ + this->seventv_.loadGlobalEmotes(); +} + +void TwitchIrcServer::reloadAllSevenTVChannelEmotes() +{ + this->forEachChannel([](const auto &chan) { + if (auto *channel = dynamic_cast(chan.get())) + { + channel->refreshSevenTVChannelEmotes(false); + } + }); +} } // namespace chatterino diff --git a/src/providers/twitch/TwitchIrcServer.hpp b/src/providers/twitch/TwitchIrcServer.hpp index 7aa4212a5..dc5667c5a 100644 --- a/src/providers/twitch/TwitchIrcServer.hpp +++ b/src/providers/twitch/TwitchIrcServer.hpp @@ -7,6 +7,7 @@ #include "providers/bttv/BttvEmotes.hpp" #include "providers/ffz/FfzEmotes.hpp" #include "providers/irc/AbstractIrcServer.hpp" +#include "providers/seventv/SeventvEmotes.hpp" #include #include @@ -37,6 +38,8 @@ public: void reloadAllBTTVChannelEmotes(); void reloadFFZGlobalEmotes(); void reloadAllFFZChannelEmotes(); + void reloadSevenTVGlobalEmotes(); + void reloadAllSevenTVChannelEmotes(); Atomic lastUserThatWhisperedMe; @@ -49,6 +52,7 @@ public: const BttvEmotes &getBttvEmotes() const; const FfzEmotes &getFfzEmotes() const; + const SeventvEmotes &getSeventvEmotes() const; protected: virtual void initializeConnection(IrcConnection *connection, @@ -85,6 +89,7 @@ private: BttvEmotes bttv; FfzEmotes ffz; + SeventvEmotes seventv_; QTimer bulkLiveStatusTimer_; pajlada::Signals::SignalHolder signalHolder_; diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 47d4009e6..7b39698ab 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -7,6 +7,7 @@ #include "messages/Message.hpp" #include "providers/chatterino/ChatterinoBadges.hpp" #include "providers/ffz/FfzBadges.hpp" +#include "providers/seventv/SeventvBadges.hpp" #include "providers/twitch/TwitchBadge.hpp" #include "providers/twitch/TwitchBadges.hpp" #include "providers/twitch/TwitchChannel.hpp" @@ -280,6 +281,7 @@ MessagePtr TwitchMessageBuilder::build() this->appendChatterinoBadges(); this->appendFfzBadges(); + this->appendSeventvBadges(); this->appendUsername(); @@ -1028,6 +1030,7 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name) const auto &globalBttvEmotes = app->twitch->getBttvEmotes(); const auto &globalFfzEmotes = app->twitch->getFfzEmotes(); + const auto &globalSeventvEmotes = app->twitch->getSeventvEmotes(); auto flags = MessageElementFlags(); auto emote = boost::optional{}; @@ -1035,8 +1038,10 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name) // Emote order: // - FrankerFaceZ Channel // - BetterTTV Channel + // - 7TV Channel // - FrankerFaceZ Global // - BetterTTV Global + // - 7TV Global if (this->twitchChannel && (emote = this->twitchChannel->ffzEmote(name))) { flags = MessageElementFlag::FfzEmote; @@ -1046,6 +1051,15 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name) { flags = MessageElementFlag::BttvEmote; } + else if (this->twitchChannel != nullptr && + (emote = this->twitchChannel->seventvEmote(name))) + { + flags = MessageElementFlag::SevenTVEmote; + if (emote.value()->zeroWidth) + { + flags.set(MessageElementFlag::ZeroWidthEmote); + } + } else if ((emote = globalFfzEmotes.emote(name))) { flags = MessageElementFlag::FfzEmote; @@ -1059,6 +1073,14 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name) flags.set(MessageElementFlag::ZeroWidthEmote); } } + else if ((emote = globalSeventvEmotes.globalEmote(name))) + { + flags = MessageElementFlag::SevenTVEmote; + if (emote.value()->zeroWidth) + { + flags.set(MessageElementFlag::ZeroWidthEmote); + } + } if (emote) { @@ -1217,6 +1239,14 @@ void TwitchMessageBuilder::appendFfzBadges() } } +void TwitchMessageBuilder::appendSeventvBadges() +{ + if (auto badge = getApp()->seventvBadges->getBadge({this->userId_})) + { + this->emplace(*badge, MessageElementFlag::BadgeSevenTV); + } +} + Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string) { if (this->bitsLeft == 0) diff --git a/src/providers/twitch/TwitchMessageBuilder.hpp b/src/providers/twitch/TwitchMessageBuilder.hpp index 68da6a759..58ef17713 100644 --- a/src/providers/twitch/TwitchMessageBuilder.hpp +++ b/src/providers/twitch/TwitchMessageBuilder.hpp @@ -99,6 +99,7 @@ private: void appendTwitchBadges(); void appendChatterinoBadges(); void appendFfzBadges(); + void appendSeventvBadges(); Outcome tryParseCheermote(const QString &string); bool shouldAddModerationElements() const; diff --git a/src/singletons/Emotes.hpp b/src/singletons/Emotes.hpp index be7fdc480..51faac660 100644 --- a/src/singletons/Emotes.hpp +++ b/src/singletons/Emotes.hpp @@ -5,6 +5,7 @@ #include "providers/bttv/BttvEmotes.hpp" #include "providers/emoji/Emojis.hpp" #include "providers/ffz/FfzEmotes.hpp" +#include "providers/seventv/SeventvEmotes.hpp" #include "providers/twitch/TwitchEmotes.hpp" #include "singletons/helper/GifTimer.hpp" diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 44a8d2367..7eaa6b543 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -164,6 +164,7 @@ public: "/appearance/badges/useCustomFfzModeratorBadges", true}; BoolSetting useCustomFfzVipBadges = { "/appearance/badges/useCustomFfzVipBadges", true}; + BoolSetting showBadgesSevenTV = {"/appearance/badges/seventv", true}; /// Behaviour BoolSetting allowDuplicateMessages = {"/behaviour/allowDuplicateMessages", @@ -209,6 +210,8 @@ public: BoolSetting enableEmoteImages = {"/emotes/enableEmoteImages", true}; BoolSetting animateEmotes = {"/emotes/enableGifAnimations", true}; FloatSetting emoteScale = {"/emotes/scale", 1.f}; + BoolSetting showUnlistedSevenTVEmotes = { + "/emotes/showUnlistedSevenTVEmotes", false}; QStringSetting emojiSet = {"/emotes/emojiSet", "Twitter"}; @@ -220,6 +223,8 @@ public: BoolSetting enableBTTVChannelEmotes = {"/emotes/bttv/channel", true}; BoolSetting enableFFZGlobalEmotes = {"/emotes/ffz/global", true}; BoolSetting enableFFZChannelEmotes = {"/emotes/ffz/channel", true}; + BoolSetting enableSevenTVGlobalEmotes = {"/emotes/seventv/global", true}; + BoolSetting enableSevenTVChannelEmotes = {"/emotes/seventv/channel", true}; /// Links BoolSetting linksDoubleClickOnly = {"/links/doubleClickToOpen", false}; diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index 64d6cf7f4..4d4b61ae3 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -110,6 +110,7 @@ WindowManager::WindowManager() this->wordFlagsListener_.addSetting(settings->showBadgesVanity); this->wordFlagsListener_.addSetting(settings->showBadgesChatterino); this->wordFlagsListener_.addSetting(settings->showBadgesFfz); + this->wordFlagsListener_.addSetting(settings->showBadgesSevenTV); this->wordFlagsListener_.addSetting(settings->enableEmoteImages); this->wordFlagsListener_.addSetting(settings->boldUsernames); this->wordFlagsListener_.addSetting(settings->lowercaseDomains); @@ -179,6 +180,7 @@ void WindowManager::updateWordTypeMask() flags.set(settings->showBadgesChatterino ? MEF::BadgeChatterino : MEF::None); flags.set(settings->showBadgesFfz ? MEF::BadgeFfz : MEF::None); + flags.set(settings->showBadgesSevenTV ? MEF::BadgeSevenTV : MEF::None); // username flags.set(MEF::Username); diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index 317607480..fb08ca627 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -359,6 +359,12 @@ void EmotePopup::loadChannel(ChannelPtr channel) addEmotes(*globalChannel, *getApp()->twitch->getFfzEmotes().emotes(), "FrankerFaceZ", MessageElementFlag::FfzEmote); } + if (Settings::instance().enableSevenTVGlobalEmotes) + { + addEmotes(*globalChannel, + *getApp()->twitch->getSeventvEmotes().globalEmotes(), "7TV", + MessageElementFlag::SevenTVEmote); + } // channel if (Settings::instance().enableBTTVChannelEmotes) @@ -371,6 +377,11 @@ void EmotePopup::loadChannel(ChannelPtr channel) addEmotes(*channelChannel, *this->twitchChannel_->ffzEmotes(), "FrankerFaceZ", MessageElementFlag::FfzEmote); } + if (Settings::instance().enableSevenTVChannelEmotes) + { + addEmotes(*channelChannel, *this->twitchChannel_->seventvEmotes(), + "7TV", MessageElementFlag::SevenTVEmote); + } this->globalEmotesView_->setChannel(globalChannel); this->subEmotesView_->setChannel(subChannel); @@ -429,6 +440,8 @@ void EmotePopup::filterTwitchEmotes(std::shared_ptr searchChannel, searchText, getApp()->twitch->getBttvEmotes().emotes()); auto ffzGlobalEmotes = this->filterEmoteMap( searchText, getApp()->twitch->getFfzEmotes().emotes()); + auto *seventvGlobalEmotes = this->filterEmoteMap( + searchText, getApp()->twitch->getSeventvEmotes().globalEmotes()); // twitch addEmoteSets(twitchGlobalEmotes, *searchChannel, *searchChannel, @@ -451,6 +464,9 @@ void EmotePopup::filterTwitchEmotes(std::shared_ptr searchChannel, this->filterEmoteMap(searchText, this->twitchChannel_->bttvEmotes()); auto ffzChannelEmotes = this->filterEmoteMap(searchText, this->twitchChannel_->ffzEmotes()); + auto *seventvChannelEmotes = + this->filterEmoteMap(searchText, this->twitchChannel_->seventvEmotes()); + // channel if (bttvChannelEmotes->size() > 0) addEmotes(*searchChannel, *bttvChannelEmotes, "BetterTTV (Channel)", @@ -458,6 +474,11 @@ void EmotePopup::filterTwitchEmotes(std::shared_ptr searchChannel, if (ffzChannelEmotes->size() > 0) addEmotes(*searchChannel, *ffzChannelEmotes, "FrankerFaceZ (Channel)", MessageElementFlag::FfzEmote); + if (!seventvChannelEmotes->empty()) + { + addEmotes(*searchChannel, *seventvChannelEmotes, "SevenTV (Channel)", + MessageElementFlag::SevenTVEmote); + } } void EmotePopup::filterEmotes(const QString &searchText) diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 6042c18d6..94579bcc9 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -120,6 +120,10 @@ namespace { { addPageLink("FFZ"); } + else if (creatorFlags.has(MessageElementFlag::SevenTVEmote)) + { + addPageLink("7TV"); + } } // Current function: https://www.desmos.com/calculator/vdyamchjwh diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index e209c0f80..f8dbaccf2 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -5,6 +5,8 @@ #include "common/Version.hpp" #include "controllers/hotkeys/HotkeyCategory.hpp" #include "controllers/hotkeys/HotkeyController.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Fonts.hpp" #include "singletons/NativeMessaging.hpp" #include "singletons/Paths.hpp" @@ -340,6 +342,22 @@ void GeneralPage::initLayout(GeneralPageView &layout) layout.addCheckbox("Remove spaces between emotes", s.removeSpacesBetweenEmotes); + layout.addCheckbox("Show unlisted 7TV emotes", s.showUnlistedSevenTVEmotes); + s.showUnlistedSevenTVEmotes.connect( + []() { + getApp()->twitch->forEachChannelAndSpecialChannels( + [](const auto &c) { + if (c->isTwitchChannel()) + { + auto *channel = dynamic_cast(c.get()); + if (channel != nullptr) + { + channel->refreshSevenTVChannelEmotes(false); + } + } + }); + }, + false); layout.addDropdown( "Show info on hover", {"Don't show", "Always show", "Hold shift"}, s.emotesTooltipPreview, @@ -362,6 +380,8 @@ void GeneralPage::initLayout(GeneralPageView &layout) layout.addCheckbox("Show BTTV channel emotes", s.enableBTTVChannelEmotes); layout.addCheckbox("Show FFZ global emotes", s.enableFFZGlobalEmotes); 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.addTitle("Streamer Mode"); layout.addDescription( @@ -631,6 +651,7 @@ void GeneralPage::initLayout(GeneralPageView &layout) layout.addCheckbox("Chatterino", s.showBadgesChatterino); layout.addCheckbox("FrankerFaceZ (Bot, FFZ Supporter, FFZ Developer)", s.showBadgesFfz); + layout.addCheckbox("7TV", s.showBadgesSevenTV); layout.addSeperator(); layout.addCheckbox("Use custom FrankerFaceZ moderator badges", s.useCustomFfzModeratorBadges); diff --git a/src/widgets/splits/InputCompletionPopup.cpp b/src/widgets/splits/InputCompletionPopup.cpp index d20b9f88d..b53b75780 100644 --- a/src/widgets/splits/InputCompletionPopup.cpp +++ b/src/widgets/splits/InputCompletionPopup.cpp @@ -5,6 +5,7 @@ #include "messages/Emote.hpp" #include "providers/bttv/BttvEmotes.hpp" #include "providers/ffz/FfzEmotes.hpp" +#include "providers/seventv/SeventvEmotes.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Emotes.hpp" @@ -99,17 +100,25 @@ void InputCompletionPopup::updateEmotes(const QString &text, ChannelPtr channel) if (tc) { - // TODO extract "Channel BetterTTV" text into a #define. + // TODO extract "Channel {BetterTTV,7TV,FrankerFaceZ}" text into a #define. if (auto bttv = tc->bttvEmotes()) addEmotes(emotes, *bttv, text, "Channel BetterTTV"); if (auto ffz = tc->ffzEmotes()) addEmotes(emotes, *ffz, text, "Channel FrankerFaceZ"); + if (auto seventv = tc->seventvEmotes()) + { + addEmotes(emotes, *seventv, text, "Channel 7TV"); + } } if (auto bttvG = getApp()->twitch->getBttvEmotes().emotes()) addEmotes(emotes, *bttvG, text, "Global BetterTTV"); if (auto ffzG = getApp()->twitch->getFfzEmotes().emotes()) addEmotes(emotes, *ffzG, text, "Global FrankerFaceZ"); + if (auto seventvG = getApp()->twitch->getSeventvEmotes().globalEmotes()) + { + addEmotes(emotes, *seventvG, text, "Global 7TV"); + } } addEmojis(emotes, getApp()->emotes->emojis.emojis, text); diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index c7c0d7579..889561914 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -1198,6 +1198,7 @@ void Split::reloadChannelAndSubscriberEmotes() { twitchChannel->refreshBTTVChannelEmotes(true); twitchChannel->refreshFFZChannelEmotes(true); + twitchChannel->refreshSevenTVChannelEmotes(true); } } diff --git a/src/widgets/splits/SplitHeader.cpp b/src/widgets/splits/SplitHeader.cpp index a1ff94483..fe95aca5a 100644 --- a/src/widgets/splits/SplitHeader.cpp +++ b/src/widgets/splits/SplitHeader.cpp @@ -999,6 +999,7 @@ void SplitHeader::reloadChannelEmotes() { twitchChannel->refreshFFZChannelEmotes(true); twitchChannel->refreshBTTVChannelEmotes(true); + twitchChannel->refreshSevenTVChannelEmotes(true); } }