diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a52f28c22..ecc29e6ddd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unversioned +- Minor: Added support for FrankerFaceZ animated emotes. (#4434) - Dev: Only log debug messages when NDEBUG is not defined. (#4442) - Dev: Cleaned up theme related code. (#4450) diff --git a/src/providers/ffz/FfzEmotes.cpp b/src/providers/ffz/FfzEmotes.cpp index 5591f9d512b..180628545dc 100644 --- a/src/providers/ffz/FfzEmotes.cpp +++ b/src/providers/ffz/FfzEmotes.cpp @@ -19,7 +19,7 @@ namespace { Url getEmoteLink(const QJsonObject &urls, const QString &emoteScale) { - auto emote = urls.value(emoteScale); + auto emote = urls[emoteScale]; if (emote.isUndefined() || emote.isNull()) { return {""}; @@ -47,6 +47,7 @@ namespace { : Image::fromUrl(url3x, 0.25)}; emoteData.tooltip = {tooltip}; } + EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id) { static std::unordered_map> cache; @@ -54,25 +55,57 @@ namespace { return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id); } - std::pair parseGlobalEmotes( - const QJsonObject &jsonRoot, const EmoteMap ¤tEmotes) + + void parseEmoteSetInto(const QJsonObject &emoteSet, const QString &kind, + EmoteMap &map) + { + for (const auto emoteRef : emoteSet["emoticons"].toArray()) + { + const auto emoteJson = emoteRef.toObject(); + + // margins + auto id = EmoteId{QString::number(emoteJson["id"].toInt())}; + auto name = EmoteName{emoteJson["name"].toString()}; + auto author = + EmoteAuthor{emoteJson["owner"]["display_name"].toString()}; + auto urls = emoteJson["urls"].toObject(); + if (emoteJson["animated"].isObject()) + { + // prefer animated images if available + urls = emoteJson["animated"].toObject(); + } + + Emote emote; + fillInEmoteData(urls, name, + QString("%1
%2 FFZ Emote
By: %3") + .arg(name.string, kind, author.string), + emote); + emote.homePage = + Url{QString("https://www.frankerfacez.com/emoticon/%1-%2") + .arg(id.string) + .arg(name.string)}; + + map[name] = cachedOrMake(std::move(emote), id); + } + } + + EmoteMap parseGlobalEmotes(const QJsonObject &jsonRoot) { // Load default sets from the `default_sets` object std::unordered_set defaultSets{}; - auto jsonDefaultSets = jsonRoot.value("default_sets").toArray(); + auto jsonDefaultSets = jsonRoot["default_sets"].toArray(); for (auto jsonDefaultSet : jsonDefaultSets) { defaultSets.insert(jsonDefaultSet.toInt()); } - auto jsonSets = jsonRoot.value("sets").toObject(); auto emotes = EmoteMap(); - for (auto jsonSet : jsonSets) + for (const auto emoteSetRef : jsonRoot["sets"].toObject()) { - auto jsonSetObject = jsonSet.toObject(); - const auto emoteSetID = jsonSetObject.value("id").toInt(); - if (defaultSets.find(emoteSetID) == defaultSets.end()) + const auto emoteSet = emoteSetRef.toObject(); + auto emoteSetID = emoteSet["id"].toInt(); + if (!defaultSets.contains(emoteSetID)) { qCDebug(chatterinoFfzemotes) << "Skipping global emote set" << emoteSetID @@ -80,35 +113,14 @@ namespace { continue; } - auto jsonEmotes = jsonSetObject.value("emoticons").toArray(); - - for (auto jsonEmoteValue : jsonEmotes) - { - auto jsonEmote = jsonEmoteValue.toObject(); - - auto name = EmoteName{jsonEmote.value("name").toString()}; - auto id = - EmoteId{QString::number(jsonEmote.value("id").toInt())}; - auto urls = jsonEmote.value("urls").toObject(); - - auto emote = Emote(); - fillInEmoteData(urls, name, - name.string + "
Global FFZ Emote", emote); - emote.homePage = - Url{QString("https://www.frankerfacez.com/emoticon/%1-%2") - .arg(id.string) - .arg(name.string)}; - - emotes[name] = - cachedOrMakeEmotePtr(std::move(emote), currentEmotes); - } + parseEmoteSetInto(emoteSet, "Global", emotes); } - return {Success, std::move(emotes)}; + return emotes; } boost::optional parseAuthorityBadge(const QJsonObject &badgeUrls, - const QString tooltip) + const QString &tooltip) { boost::optional authorityBadge; @@ -140,40 +152,11 @@ namespace { EmoteMap parseChannelEmotes(const QJsonObject &jsonRoot) { - auto jsonSets = jsonRoot.value("sets").toObject(); auto emotes = EmoteMap(); - for (auto jsonSet : jsonSets) + for (const auto emoteSetRef : jsonRoot["sets"].toObject()) { - auto jsonEmotes = jsonSet.toObject().value("emoticons").toArray(); - - for (auto _jsonEmote : jsonEmotes) - { - auto jsonEmote = _jsonEmote.toObject(); - - // margins - auto id = - EmoteId{QString::number(jsonEmote.value("id").toInt())}; - auto name = EmoteName{jsonEmote.value("name").toString()}; - auto author = EmoteAuthor{jsonEmote.value("owner") - .toObject() - .value("display_name") - .toString()}; - auto urls = jsonEmote.value("urls").toObject(); - - Emote emote; - fillInEmoteData(urls, name, - QString("%1
Channel FFZ Emote
By: %2") - .arg(name.string) - .arg(author.string), - emote); - emote.homePage = - Url{QString("https://www.frankerfacez.com/emoticon/%1-%2") - .arg(id.string) - .arg(name.string)}; - - emotes[name] = cachedOrMake(std::move(emote), id); - } + parseEmoteSetInto(emoteSetRef.toObject(), "Channel", emotes); } return emotes; @@ -195,7 +178,9 @@ boost::optional FfzEmotes::emote(const EmoteName &name) const auto emotes = this->global_.get(); auto it = emotes->find(name); if (it != emotes->end()) + { return it->second; + } return boost::none; } @@ -213,41 +198,38 @@ void FfzEmotes::loadEmotes() .timeout(30000) .onSuccess([this](auto result) -> Outcome { - auto emotes = this->emotes(); - auto pair = parseGlobalEmotes(result.parseJson(), *emotes); - if (pair.first) - this->global_.set( - std::make_shared(std::move(pair.second))); - return pair.first; + auto parsedSet = parseGlobalEmotes(result.parseJson()); + this->global_.set(std::make_shared(std::move(parsedSet))); + + return Success; }) .execute(); } void FfzEmotes::loadChannel( - std::weak_ptr channel, const QString &channelId, + std::weak_ptr channel, const QString &channelID, std::function emoteCallback, std::function)> modBadgeCallback, std::function)> vipBadgeCallback, bool manualRefresh) { qCDebug(chatterinoFfzemotes) - << "[FFZEmotes] Reload FFZ Channel Emotes for channel" << channelId; + << "[FFZEmotes] Reload FFZ Channel Emotes for channel" << channelID; - NetworkRequest("https://api.frankerfacez.com/v1/room/id/" + channelId) + NetworkRequest("https://api.frankerfacez.com/v1/room/id/" + channelID) .timeout(20000) .onSuccess([emoteCallback = std::move(emoteCallback), modBadgeCallback = std::move(modBadgeCallback), vipBadgeCallback = std::move(vipBadgeCallback), channel, - manualRefresh](auto result) -> Outcome { - auto json = result.parseJson(); + manualRefresh](const auto &result) -> Outcome { + const auto json = result.parseJson(); + auto emoteMap = parseChannelEmotes(json); auto modBadge = parseAuthorityBadge( - json.value("room").toObject().value("mod_urls").toObject(), - "Moderator"); + json["room"]["mod_urls"].toObject(), "Moderator"); auto vipBadge = parseAuthorityBadge( - json.value("room").toObject().value("vip_badge").toObject(), - "VIP"); + json["room"]["vip_badge"].toObject(), "VIP"); bool hasEmotes = !emoteMap.empty(); @@ -270,22 +252,27 @@ void FfzEmotes::loadChannel( return Success; }) - .onError([channelId, channel, manualRefresh](NetworkResult result) { + .onError([channelID, channel, manualRefresh](const auto &result) { auto shared = channel.lock(); if (!shared) + { return; + } + if (result.status() == 404) { // User does not have any FFZ emotes 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(chatterinoFfzemotes) - << "Fetching FFZ emotes for channel" << channelId + << "Fetching FFZ emotes for channel" << channelID << "failed due to timeout"; shared->addMessage( makeSystemMessage("Failed to fetch FrankerFaceZ channel " @@ -294,7 +281,7 @@ void FfzEmotes::loadChannel( else { qCWarning(chatterinoFfzemotes) - << "Error fetching FFZ emotes for channel" << channelId + << "Error fetching FFZ emotes for channel" << channelID << ", error" << result.status(); shared->addMessage( makeSystemMessage("Failed to fetch FrankerFaceZ channel "