diff --git a/CHANGELOG.md b/CHANGELOG.md index bb1beeaf6..e77706f91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Minor: Moderators can now see when users are warned. (#5441) - Minor: Added support for Brave & google-chrome-stable browsers. (#5452) - Minor: Added drop indicator line while dragging in tables. (#5256) +- Minor: Add channel points indication for new bits power-up redemptions. (#5471) - Minor: Added `/warn ` command for mods. This prevents the user from chatting until they acknowledge the warning. (#5474) - Minor: Introduce HTTP API for plugins. (#5383) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426) diff --git a/src/providers/twitch/ChannelPointReward.cpp b/src/providers/twitch/ChannelPointReward.cpp index 62d93e3f0..658d498ff 100644 --- a/src/providers/twitch/ChannelPointReward.cpp +++ b/src/providers/twitch/ChannelPointReward.cpp @@ -14,6 +14,47 @@ ChannelPointReward::ChannelPointReward(const QJsonObject &redemption) this->title = reward.value("title").toString(); this->cost = reward.value("cost").toInt(); this->isUserInputRequired = reward.value("is_user_input_required").toBool(); + this->isBits = reward.value("pricing_type").toString() == "BITS"; + + // accommodate idiosyncrasies of automatic reward redemptions + const auto rewardType = reward.value("reward_type").toString(); + if (rewardType == "SEND_ANIMATED_MESSAGE") + { + this->id = "animated-message"; + this->isUserInputRequired = true; + this->title = "Message Effects"; + } + else if (rewardType == "SEND_GIGANTIFIED_EMOTE") + { + this->id = "gigantified-emote-message"; + this->isUserInputRequired = true; + this->title = "Gigantify an Emote"; + } + else if (rewardType == "CELEBRATION") + { + this->id = rewardType; + this->title = "On-Screen Celebration"; + const auto metadata = + redemption.value("redemption_metadata").toObject(); + const auto emote = metadata.value("celebration_emote_metadata") + .toObject() + .value("emote") + .toObject(); + this->emoteId = emote.value("id").toString(); + this->emoteName = emote.value("token").toString(); + } + + // use bits cost when channel points were not used + if (cost == 0) + { + this->cost = reward.value("bits_cost").toInt(); + } + + // workaround twitch bug where bits_cost is always 0 in practice + if (cost == 0) + { + this->cost = reward.value("default_bits_cost").toInt(); + } // We don't need to store user information for rewards with user input // because we will get the user info from a corresponding IRC message @@ -27,6 +68,13 @@ ChannelPointReward::ChannelPointReward(const QJsonObject &redemption) } auto imageValue = reward.value("image"); + + // automatic reward redemptions have specialized default images + if (imageValue.isNull() && this->isBits) + { + imageValue = reward.value("default_image"); + } + // From Twitch docs // The size is only an estimation, the actual size might vary. constexpr QSize baseSize(28, 28); diff --git a/src/providers/twitch/ChannelPointReward.hpp b/src/providers/twitch/ChannelPointReward.hpp index f9f9b6316..d4f428e92 100644 --- a/src/providers/twitch/ChannelPointReward.hpp +++ b/src/providers/twitch/ChannelPointReward.hpp @@ -19,6 +19,9 @@ struct ChannelPointReward { int cost; ImageSet image; bool isUserInputRequired = false; + bool isBits = false; + QString emoteId; // currently only for celebrations + QString emoteName; // currently only for celebrations struct { QString id; diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index d0600b8fa..4657438ad 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -1338,21 +1338,30 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, auto *channel = dynamic_cast(chan.get()); const auto &tags = message->tags(); + QString rewardId; if (const auto it = tags.find("custom-reward-id"); it != tags.end()) { - const auto rewardId = it.value().toString(); - if (!rewardId.isEmpty() && - !channel->isChannelPointRewardKnown(rewardId)) - { - // Need to wait for pubsub reward notification - qCDebug(chatterinoTwitch) << "TwitchChannel reward added ADD " - "callback since reward is not known:" - << rewardId; - channel->addQueuedRedemption(rewardId, originalContent, message); - return; - } - args.channelPointRewardId = rewardId; + rewardId = it.value().toString(); } + else if (const auto typeIt = tags.find("msg-id"); typeIt != tags.end()) + { + // slight hack to treat bits power-ups as channel point redemptions + const auto msgId = typeIt.value().toString(); + if (msgId == "animated-message" || msgId == "gigantified-emote-message") + { + rewardId = msgId; + } + } + if (!rewardId.isEmpty() && !channel->isChannelPointRewardKnown(rewardId)) + { + // Need to wait for pubsub reward notification + qCDebug(chatterinoTwitch) << "TwitchChannel reward added ADD " + "callback since reward is not known:" + << rewardId; + channel->addQueuedRedemption(rewardId, originalContent, message); + return; + } + args.channelPointRewardId = rewardId; QString content = originalContent; int messageOffset = stripLeadingReplyMention(tags, content); diff --git a/src/providers/twitch/PubSubManager.cpp b/src/providers/twitch/PubSubManager.cpp index cefde9169..447909812 100644 --- a/src/providers/twitch/PubSubManager.cpp +++ b/src/providers/twitch/PubSubManager.cpp @@ -1181,6 +1181,8 @@ void PubSub::handleMessageResponse(const PubSubMessageMessage &message) switch (innerMessage.type) { + case PubSubCommunityPointsChannelV1Message::Type:: + AutomaticRewardRedeemed: case PubSubCommunityPointsChannelV1Message::Type::RewardRedeemed: { auto redemption = innerMessage.data.value("redemption").toObject(); diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index f69264cb3..7fa77e494 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -1594,6 +1594,15 @@ void TwitchMessageBuilder::appendChannelPointRewardMessage( } builder->emplace(redeemed, MessageElementFlag::ChannelPointReward); + if (reward.id == "CELEBRATION") + { + const auto emotePtr = + getIApp()->getEmotes()->getTwitchEmotes()->getOrCreateEmote( + EmoteId{reward.emoteId}, EmoteName{reward.emoteName}); + builder->emplace(emotePtr, + MessageElementFlag::ChannelPointReward, + MessageColor::Text); + } builder->emplace( reward.title, MessageElementFlag::ChannelPointReward, MessageColor::Text, FontStyle::ChatMediumBold); @@ -1602,6 +1611,12 @@ void TwitchMessageBuilder::appendChannelPointRewardMessage( builder->emplace( QString::number(reward.cost), MessageElementFlag::ChannelPointReward, MessageColor::Text, FontStyle::ChatMediumBold); + if (reward.isBits) + { + builder->emplace( + "bits", MessageElementFlag::ChannelPointReward, MessageColor::Text, + FontStyle::ChatMediumBold); + } if (reward.isUserInputRequired) { builder->emplace( diff --git a/src/providers/twitch/pubsubmessages/ChannelPoints.hpp b/src/providers/twitch/pubsubmessages/ChannelPoints.hpp index be8d1bd68..fe2254a7a 100644 --- a/src/providers/twitch/pubsubmessages/ChannelPoints.hpp +++ b/src/providers/twitch/pubsubmessages/ChannelPoints.hpp @@ -8,6 +8,7 @@ namespace chatterino { struct PubSubCommunityPointsChannelV1Message { enum class Type { + AutomaticRewardRedeemed, RewardRedeemed, INVALID, @@ -30,6 +31,9 @@ constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name< { switch (value) { + case chatterino::PubSubCommunityPointsChannelV1Message::Type:: + AutomaticRewardRedeemed: + return "automatic-reward-redeemed"; case chatterino::PubSubCommunityPointsChannelV1Message::Type:: RewardRedeemed: return "reward-redeemed";