feat: Add Live Emote Updates for BTTV (#4147)
This feature is enabled by default and can be disabled in settings with the "Enable BTTV live emotes updates" setting. Co-authored-by: Felanbird <41973452+Felanbird@users.noreply.github.com> Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
#include "messages/Image.hpp"
|
||||
#include "messages/ImageSet.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
||||
@@ -21,6 +22,12 @@ namespace {
|
||||
|
||||
QString emoteLinkFormat("https://betterttv.com/emotes/%1");
|
||||
|
||||
struct CreateEmoteResult {
|
||||
EmoteId id;
|
||||
EmoteName name;
|
||||
Emote emote;
|
||||
};
|
||||
|
||||
Url getEmoteLink(QString urlTemplate, const EmoteId &id,
|
||||
const QString &emoteScale)
|
||||
{
|
||||
@@ -70,6 +77,70 @@ namespace {
|
||||
|
||||
return {Success, std::move(emotes)};
|
||||
}
|
||||
|
||||
CreateEmoteResult createChannelEmote(const QString &channelDisplayName,
|
||||
const QJsonObject &jsonEmote)
|
||||
{
|
||||
auto id = EmoteId{jsonEmote.value("id").toString()};
|
||||
auto name = EmoteName{jsonEmote.value("code").toString()};
|
||||
auto author = EmoteAuthor{
|
||||
jsonEmote.value("user").toObject().value("displayName").toString()};
|
||||
|
||||
auto emote = Emote({
|
||||
name,
|
||||
ImageSet{
|
||||
Image::fromUrl(getEmoteLinkV3(id, "1x"), 1),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25),
|
||||
},
|
||||
Tooltip{
|
||||
QString("%1<br>%2 BetterTTV Emote<br>By: %3")
|
||||
.arg(name.string)
|
||||
// when author is empty, it is a channel emote created by the broadcaster
|
||||
.arg(author.string.isEmpty() ? "Channel" : "Shared")
|
||||
.arg(author.string.isEmpty() ? channelDisplayName
|
||||
: author.string)},
|
||||
Url{emoteLinkFormat.arg(id.string)},
|
||||
false,
|
||||
id,
|
||||
});
|
||||
|
||||
return {id, name, emote};
|
||||
}
|
||||
|
||||
bool updateChannelEmote(Emote &emote, const QString &channelDisplayName,
|
||||
const QJsonObject &jsonEmote)
|
||||
{
|
||||
bool anyModifications = false;
|
||||
|
||||
if (jsonEmote.contains("code"))
|
||||
{
|
||||
emote.name = EmoteName{jsonEmote.value("code").toString()};
|
||||
anyModifications = true;
|
||||
}
|
||||
if (jsonEmote.contains("user"))
|
||||
{
|
||||
emote.author = EmoteAuthor{jsonEmote.value("user")
|
||||
.toObject()
|
||||
.value("displayName")
|
||||
.toString()};
|
||||
anyModifications = true;
|
||||
}
|
||||
|
||||
if (anyModifications)
|
||||
{
|
||||
emote.tooltip = Tooltip{
|
||||
QString("%1<br>%2 BetterTTV Emote<br>By: %3")
|
||||
.arg(emote.name.string)
|
||||
// when author is empty, it is a channel emote created by the broadcaster
|
||||
.arg(emote.author.string.isEmpty() ? "Channel" : "Shared")
|
||||
.arg(emote.author.string.isEmpty() ? channelDisplayName
|
||||
: emote.author.string)};
|
||||
}
|
||||
|
||||
return anyModifications;
|
||||
}
|
||||
|
||||
std::pair<Outcome, EmoteMap> parseChannelEmotes(
|
||||
const QJsonObject &jsonRoot, const QString &channelDisplayName)
|
||||
{
|
||||
@@ -80,33 +151,11 @@ namespace {
|
||||
auto jsonEmotes = jsonRoot.value(key).toArray();
|
||||
for (auto jsonEmote_ : jsonEmotes)
|
||||
{
|
||||
auto jsonEmote = jsonEmote_.toObject();
|
||||
auto emote = createChannelEmote(channelDisplayName,
|
||||
jsonEmote_.toObject());
|
||||
|
||||
auto id = EmoteId{jsonEmote.value("id").toString()};
|
||||
auto name = EmoteName{jsonEmote.value("code").toString()};
|
||||
auto author = EmoteAuthor{jsonEmote.value("user")
|
||||
.toObject()
|
||||
.value("displayName")
|
||||
.toString()};
|
||||
|
||||
auto emote = Emote({
|
||||
name,
|
||||
ImageSet{
|
||||
Image::fromUrl(getEmoteLinkV3(id, "1x"), 1),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5),
|
||||
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25),
|
||||
},
|
||||
Tooltip{
|
||||
QString("%1<br>%2 BetterTTV Emote<br>By: %3")
|
||||
.arg(name.string)
|
||||
// when author is empty, it is a channel emote created by the broadcaster
|
||||
.arg(author.string.isEmpty() ? "Channel" : "Shared")
|
||||
.arg(author.string.isEmpty() ? channelDisplayName
|
||||
: author.string)},
|
||||
Url{emoteLinkFormat.arg(id.string)},
|
||||
});
|
||||
|
||||
emotes[name] = cachedOrMake(std::move(emote), id);
|
||||
emotes[emote.name] =
|
||||
cachedOrMake(std::move(emote.emote), emote.id);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -227,6 +276,78 @@ void BttvEmotes::loadChannel(std::weak_ptr<Channel> channel,
|
||||
.execute();
|
||||
}
|
||||
|
||||
EmotePtr BttvEmotes::addEmote(
|
||||
const QString &channelDisplayName,
|
||||
Atomic<std::shared_ptr<const EmoteMap>> &channelEmoteMap,
|
||||
const BttvLiveUpdateEmoteUpdateAddMessage &message)
|
||||
{
|
||||
// This copies the map.
|
||||
EmoteMap updatedMap = *channelEmoteMap.get();
|
||||
auto result = createChannelEmote(channelDisplayName, message.jsonEmote);
|
||||
|
||||
auto emote = std::make_shared<const Emote>(std::move(result.emote));
|
||||
updatedMap[result.name] = emote;
|
||||
channelEmoteMap.set(std::make_shared<EmoteMap>(std::move(updatedMap)));
|
||||
|
||||
return emote;
|
||||
}
|
||||
|
||||
boost::optional<std::pair<EmotePtr, EmotePtr>> BttvEmotes::updateEmote(
|
||||
const QString &channelDisplayName,
|
||||
Atomic<std::shared_ptr<const EmoteMap>> &channelEmoteMap,
|
||||
const BttvLiveUpdateEmoteUpdateAddMessage &message)
|
||||
{
|
||||
// This copies the map.
|
||||
EmoteMap updatedMap = *channelEmoteMap.get();
|
||||
|
||||
// Step 1: remove the existing emote
|
||||
auto it = updatedMap.findEmote(QString(), message.emoteID);
|
||||
if (it == updatedMap.end())
|
||||
{
|
||||
// We already copied the map at this point and are now discarding the copy.
|
||||
// This is fine, because this case should be really rare.
|
||||
return boost::none;
|
||||
}
|
||||
auto oldEmotePtr = it->second;
|
||||
// copy the existing emote, to not change the original one
|
||||
auto emote = *oldEmotePtr;
|
||||
updatedMap.erase(it);
|
||||
|
||||
// Step 2: update the emote
|
||||
if (!updateChannelEmote(emote, channelDisplayName, message.jsonEmote))
|
||||
{
|
||||
// The emote wasn't actually updated
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
auto name = emote.name;
|
||||
auto emotePtr = std::make_shared<const Emote>(std::move(emote));
|
||||
updatedMap[name] = emotePtr;
|
||||
channelEmoteMap.set(std::make_shared<EmoteMap>(std::move(updatedMap)));
|
||||
|
||||
return std::make_pair(oldEmotePtr, emotePtr);
|
||||
}
|
||||
|
||||
boost::optional<EmotePtr> BttvEmotes::removeEmote(
|
||||
Atomic<std::shared_ptr<const EmoteMap>> &channelEmoteMap,
|
||||
const BttvLiveUpdateEmoteRemoveMessage &message)
|
||||
{
|
||||
// This copies the map.
|
||||
EmoteMap updatedMap = *channelEmoteMap.get();
|
||||
auto it = updatedMap.findEmote(QString(), message.emoteID);
|
||||
if (it == updatedMap.end())
|
||||
{
|
||||
// We already copied the map at this point and are now discarding the copy.
|
||||
// This is fine, because this case should be really rare.
|
||||
return boost::none;
|
||||
}
|
||||
auto emote = it->second;
|
||||
updatedMap.erase(it);
|
||||
channelEmoteMap.set(std::make_shared<EmoteMap>(std::move(updatedMap)));
|
||||
|
||||
return emote;
|
||||
}
|
||||
|
||||
/*
|
||||
static Url getEmoteLink(QString urlTemplate, const EmoteId &id,
|
||||
const QString &emoteScale)
|
||||
|
||||
Reference in New Issue
Block a user