diff --git a/chatterino.pro b/chatterino.pro index 63551a23b..bd34241de 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -175,6 +175,7 @@ SOURCES += \ src/providers/irc/IrcMessageBuilder.cpp \ src/providers/irc/IrcServer.cpp \ src/providers/LinkResolver.cpp \ + src/providers/twitch/ChannelPointReward.cpp \ src/providers/twitch/api/Helix.cpp \ src/providers/twitch/api/Kraken.cpp \ src/providers/twitch/IrcMessageHandler.cpp \ @@ -379,6 +380,7 @@ HEADERS += \ src/providers/irc/IrcMessageBuilder.hpp \ src/providers/irc/IrcServer.hpp \ src/providers/LinkResolver.hpp \ + src/providers/twitch/ChannelPointReward.hpp \ src/providers/twitch/api/Helix.hpp \ src/providers/twitch/api/Kraken.hpp \ src/providers/twitch/EmoteValue.hpp \ diff --git a/lib/signals b/lib/signals index 1c38746b0..6665ccad9 160000 --- a/lib/signals +++ b/lib/signals @@ -1 +1 @@ -Subproject commit 1c38746b05d9311e73c8c8acdfdc4d36c9c551be +Subproject commit 6665ccad90461c01b7fe704a98a835953d644156 diff --git a/src/Application.cpp b/src/Application.cpp index 6934ebbd5..814ba92f5 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -27,6 +27,7 @@ #include "singletons/WindowManager.hpp" #include "util/IsBigEndian.hpp" #include "util/PostToThread.hpp" +#include "util/RapidjsonHelpers.hpp" #include "widgets/Notebook.hpp" #include "widgets/Window.hpp" #include "widgets/splits/Split.hpp" @@ -284,6 +285,21 @@ void Application::initPubsub() chan->deleteMessage(msg->id); }); + this->twitch.pubsub->signals_.pointReward.redeemed.connect([&](auto &data) { + QString channelId; + if (rj::getSafe(data, "channel_id", channelId)) + { + const auto &chan = + this->twitch.server->getChannelOrEmptyByID(channelId); + auto channel = dynamic_cast(chan.get()); + channel->addChannelPointReward(ChannelPointReward(data)); + } + else + { + qDebug() << "Couldn't find channel id of point reward"; + } + }); + this->twitch.pubsub->start(); auto RequestModerationActions = [=]() { diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index e9456dc87..b5c97b553 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -35,6 +35,7 @@ enum class MessageFlag : uint32_t { Debug = (1 << 18), Similar = (1 << 19), RedeemedHighlight = (1 << 20), + RedeemedChannelPointReward = (1 << 21), }; using MessageFlags = FlagsEnum; diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 9cc4c9ed7..a8e04b628 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -32,6 +32,7 @@ struct MessageParseArgs { bool isSentWhisper = false; bool trimSubscriberUsername = false; bool isStaffOrBroadcaster = false; + QString channelPointRewardId = ""; }; class MessageBuilder diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index 42c0204af..e3318511a 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -676,4 +676,43 @@ void IrcTextElement::addToContainer(MessageLayoutContainer &container, } } +LinebreakElement::LinebreakElement(MessageElementFlags flags) + : MessageElement(flags) +{ +} + +void LinebreakElement::addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) +{ + if (flags.hasAny(this->getFlags())) + { + container.breakLine(); + } +} + +ScalingImageElement::ScalingImageElement(ImageSet images, + MessageElementFlags flags) + : MessageElement(flags) + , images_(images) +{ +} + +void ScalingImageElement::addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) +{ + if (flags.hasAny(this->getFlags())) + { + const auto &image = + this->images_.getImageOrLoaded(container.getScale()); + if (image->isEmpty()) + return; + + auto size = QSize(image->width() * container.getScale(), + image->height() * container.getScale()); + + container.addElement((new ImageLayoutElement(*this, image, size)) + ->setLink(this->getLink())); + } +} + } // namespace chatterino diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index 7a93bbc74..034938450 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -4,6 +4,7 @@ #include "messages/Link.hpp" #include "messages/MessageColor.hpp" #include "singletons/Fonts.hpp" +#include "src/messages/ImageSet.hpp" #include #include @@ -39,6 +40,9 @@ enum class MessageElementFlag { BttvEmoteImage = (1 << 6), BttvEmoteText = (1 << 7), BttvEmote = BttvEmoteImage | BttvEmoteText, + + ChannelPointReward = (1 << 8), + FfzEmoteImage = (1 << 10), FfzEmoteText = (1 << 11), FfzEmote = FfzEmoteImage | FfzEmoteText, @@ -321,4 +325,26 @@ private: std::vector words_; }; +// Forces a linebreak +class LinebreakElement : public MessageElement +{ +public: + LinebreakElement(MessageElementFlags flags); + + void addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) override; +}; + +// Image element which will pick the quality of the image based on ui scale +class ScalingImageElement : public MessageElement +{ +public: + ScalingImageElement(ImageSet images, MessageElementFlags flags); + + void addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) override; + +private: + ImageSet images_; +}; } // namespace chatterino diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index e7eaa2efa..0e7547ffa 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -169,7 +169,7 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags) // Painting void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex, Selection &selection, bool isLastReadMessage, - bool isWindowFocused) + bool isWindowFocused, bool isMentions) { auto app = getApp(); QPixmap *pixmap = this->buffer_.get(); @@ -220,6 +220,14 @@ void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex, app->themes->messages.disabled); } + if (!isMentions && + this->message_->flags.has(MessageFlag::RedeemedChannelPointReward)) + { + painter.fillRect( + 0, y, this->scale_ * 4, pixmap->height(), + *ColorProvider::instance().color(ColorType::Subscription)); + } + // draw selection if (!selection.isEmpty()) { diff --git a/src/messages/layouts/MessageLayout.hpp b/src/messages/layouts/MessageLayout.hpp index cd9bdeb5d..6048aca6b 100644 --- a/src/messages/layouts/MessageLayout.hpp +++ b/src/messages/layouts/MessageLayout.hpp @@ -47,7 +47,7 @@ public: // Painting void paint(QPainter &painter, int width, int y, int messageIndex, Selection &selection, bool isLastReadMessage, - bool isWindowFocused); + bool isWindowFocused, bool isMentions); void invalidateBuffer(); void deleteBuffer(); void deleteCache(); diff --git a/src/providers/twitch/ChannelPointReward.cpp b/src/providers/twitch/ChannelPointReward.cpp new file mode 100644 index 000000000..95d8d9890 --- /dev/null +++ b/src/providers/twitch/ChannelPointReward.cpp @@ -0,0 +1,127 @@ +#include "ChannelPointReward.hpp" +#include "util/RapidjsonHelpers.hpp" + +namespace chatterino { + +QString parseRewardImage(const rapidjson::Value &obj, const char *key, + bool &result) +{ + QString url; + if (!(result = rj::getSafe(obj, key, url))) + { + qDebug() << "No url value found for key in reward image object:" << key; + return ""; + } + + return url; +} + +ChannelPointReward::ChannelPointReward(rapidjson::Value &redemption) +{ + rapidjson::Value user; + if (!(this->hasParsedSuccessfully = + rj::getSafeObject(redemption, "user", user))) + { + qDebug() << "No user info found for redemption"; + return; + } + + rapidjson::Value reward; + if (!(this->hasParsedSuccessfully = + rj::getSafeObject(redemption, "reward", reward))) + { + qDebug() << "No reward info found for redemption"; + return; + } + + if (!(this->hasParsedSuccessfully = rj::getSafe(reward, "id", this->id))) + { + qDebug() << "No id found for reward"; + return; + } + + if (!(this->hasParsedSuccessfully = + rj::getSafe(reward, "channel_id", this->channelId))) + { + qDebug() << "No channel_id found for reward"; + return; + } + + if (!(this->hasParsedSuccessfully = + rj::getSafe(reward, "title", this->title))) + { + qDebug() << "No title found for reward"; + return; + } + + if (!(this->hasParsedSuccessfully = + rj::getSafe(reward, "cost", this->cost))) + { + qDebug() << "No cost found for reward"; + return; + } + + if (!(this->hasParsedSuccessfully = rj::getSafe( + reward, "is_user_input_required", this->isUserInputRequired))) + { + qDebug() << "No information if user input is required found for reward"; + return; + } + + // 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 + if (!this->isUserInputRequired) + { + this->parseUser(user); + } + + rapidjson::Value obj; + if (rj::getSafeObject(reward, "image", obj) && !obj.IsNull() && + obj.IsObject()) + { + this->image = ImageSet{ + Image::fromUrl( + {parseRewardImage(obj, "url_1x", this->hasParsedSuccessfully)}, + 1), + Image::fromUrl( + {parseRewardImage(obj, "url_2x", this->hasParsedSuccessfully)}, + 0.5), + Image::fromUrl( + {parseRewardImage(obj, "url_4x", this->hasParsedSuccessfully)}, + 0.25), + }; + } + else + { + static const ImageSet defaultImage{ + Image::fromUrl({TWITCH_CHANNEL_POINT_REWARD_URL("1.png")}, 1), + Image::fromUrl({TWITCH_CHANNEL_POINT_REWARD_URL("2.png")}, 0.5), + Image::fromUrl({TWITCH_CHANNEL_POINT_REWARD_URL("4.png")}, 0.25)}; + this->image = defaultImage; + } +} + +void ChannelPointReward::parseUser(rapidjson::Value &user) +{ + if (!(this->hasParsedSuccessfully = rj::getSafe(user, "id", this->user.id))) + { + qDebug() << "No id found for user in reward"; + return; + } + + if (!(this->hasParsedSuccessfully = + rj::getSafe(user, "login", this->user.login))) + { + qDebug() << "No login name found for user in reward"; + return; + } + + if (!(this->hasParsedSuccessfully = + rj::getSafe(user, "display_name", this->user.displayName))) + { + qDebug() << "No display name found for user in reward"; + return; + } +} + +} // namespace chatterino diff --git a/src/providers/twitch/ChannelPointReward.hpp b/src/providers/twitch/ChannelPointReward.hpp new file mode 100644 index 000000000..fcd9ccd6f --- /dev/null +++ b/src/providers/twitch/ChannelPointReward.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "common/Aliases.hpp" +#include "messages/Image.hpp" +#include "messages/ImageSet.hpp" + +#define TWITCH_CHANNEL_POINT_REWARD_URL(x) \ + QString("https://static-cdn.jtvnw.net/custom-reward-images/default-%1") \ + .arg(x) + +namespace chatterino { +struct ChannelPointReward { + ChannelPointReward(rapidjson::Value &reward); + ChannelPointReward() = delete; + QString id; + QString channelId; + QString title; + int cost; + ImageSet image; + bool hasParsedSuccessfully = false; + bool isUserInputRequired = false; + + struct { + QString id; + QString login; + QString displayName; + } user; + +private: + void parseUser(rapidjson::Value &user); +}; + +} // namespace chatterino diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index fc113a145..5853d02fc 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -1,4 +1,4 @@ -#include "IrcMessageHandler.hpp" +#include "IrcMessageHandler.hpp" #include "Application.hpp" #include "controllers/accounts/AccountController.hpp" @@ -215,6 +215,32 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message, args.isStaffOrBroadcaster = true; } + auto channel = dynamic_cast(chan.get()); + + const auto &tags = _message->tags(); + if (const auto &it = tags.find("custom-reward-id"); it != tags.end()) + { + const auto rewardId = it.value().toString(); + if (!channel->isChannelPointRewardKnown(rewardId)) + { + // Need to wait for pubsub reward notification + auto clone = _message->clone(); + channel->channelPointRewardAdded.connect( + [=, &server](ChannelPointReward reward) { + if (reward.id == rewardId) + { + this->addMessage(clone, target, content, server, isSub, + isAction); + clone->deleteLater(); + return true; + } + return false; + }); + return; + } + args.channelPointRewardId = rewardId; + } + TwitchMessageBuilder builder(chan.get(), _message, args, content, isAction); if (isSub || !builder.isIgnored()) @@ -224,7 +250,6 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message, builder->flags.set(MessageFlag::Subscription); builder->flags.unset(MessageFlag::Highlighted); } - auto msg = builder.build(); IrcMessageHandler::setSimilarityFlags(msg, chan); @@ -399,9 +424,9 @@ void IrcMessageHandler::handleClearMessageMessage(Communi::IrcMessage *message) if (chan->isEmpty()) { - qDebug() - << "[IrcMessageHandler:handleClearMessageMessage] Twitch channel" - << chanName << "not found"; + qDebug() << "[IrcMessageHandler:handleClearMessageMessage] Twitch " + "channel" + << chanName << "not found"; return; } diff --git a/src/providers/twitch/PubsubClient.cpp b/src/providers/twitch/PubsubClient.cpp index 59526cb21..71a309d5c 100644 --- a/src/providers/twitch/PubsubClient.cpp +++ b/src/providers/twitch/PubsubClient.cpp @@ -804,6 +804,26 @@ void PubSub::listenToChannelModerationActions( this->listenToTopic(topic, account); } +void PubSub::listenToChannelPointRewards(const QString &channelID, + std::shared_ptr account) +{ + static const QString topicFormat("community-points-channel-v1.%1"); + assert(!channelID.isEmpty()); + assert(account != nullptr); + QString userID = account->getUserId(); + + auto topic = topicFormat.arg(channelID); + + if (this->isListeningToTopic(topic)) + { + return; + } + + qDebug() << "Listen to topic" << topic; + + this->listenToTopic(topic, account); +} + void PubSub::listenToTopic(const QString &topic, std::shared_ptr account) { @@ -1093,6 +1113,34 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) // Invoke handler function handlerIt->second(data, topicParts[2]); } + else if (topic.startsWith("community-points-channel-v1.")) + { + std::string pointEventType; + if (!rj::getSafe(msg, "type", pointEventType)) + { + qDebug() << "Bad channel point event data"; + return; + } + + if (pointEventType == "reward-redeemed") + { + if (!rj::getSafeObject(msg, "data", msg)) + { + qDebug() << "No data found for redeemed reward"; + return; + } + if (!rj::getSafeObject(msg, "redemption", msg)) + { + qDebug() << "No redemption info found for redeemed reward"; + return; + } + this->signals_.pointReward.redeemed.invoke(msg); + } + else + { + qDebug() << "Invalid point event type:" << pointEventType.c_str(); + } + } else { qDebug() << "Unknown topic:" << topic; diff --git a/src/providers/twitch/PubsubClient.hpp b/src/providers/twitch/PubsubClient.hpp index 0400683e2..3c76533b3 100644 --- a/src/providers/twitch/PubsubClient.hpp +++ b/src/providers/twitch/PubsubClient.hpp @@ -122,6 +122,10 @@ public: Signal received; Signal sent; } whisper; + + struct { + Signal redeemed; + } pointReward; } signals_; void listenToWhispers(std::shared_ptr account); @@ -131,6 +135,9 @@ public: void listenToChannelModerationActions( const QString &channelID, std::shared_ptr account); + void listenToChannelPointRewards(const QString &channelID, + std::shared_ptr account); + std::vector> requests; private: diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 6fe5b5acd..c559ff29d 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -235,6 +235,50 @@ void TwitchChannel::refreshFFZChannelEmotes(bool manualRefresh) manualRefresh); } +void TwitchChannel::addChannelPointReward(const ChannelPointReward &reward) +{ + if (!reward.hasParsedSuccessfully) + { + return; + } + + if (!reward.isUserInputRequired) + { + MessageBuilder builder; + TwitchMessageBuilder::appendChannelPointRewardMessage(reward, &builder); + this->addMessage(builder.release()); + return; + } + + bool result; + { + auto channelPointRewards = this->channelPointRewards_.access(); + result = channelPointRewards->try_emplace(reward.id, reward).second; + } + if (result) + { + this->channelPointRewardAdded.invoke(reward); + } +} + +bool TwitchChannel::isChannelPointRewardKnown(const QString &rewardId) +{ + const auto &pointRewards = this->channelPointRewards_.accessConst(); + const auto &it = pointRewards->find(rewardId); + return it != pointRewards->end(); +} + +boost::optional TwitchChannel::channelPointReward( + const QString &rewardId) const +{ + auto rewards = this->channelPointRewards_.accessConst(); + auto it = rewards->find(rewardId); + + if (it == rewards->end()) + return boost::none; + return it->second; +} + void TwitchChannel::sendMessage(const QString &message) { auto app = getApp(); @@ -660,6 +704,7 @@ void TwitchChannel::refreshPubsub() auto account = getApp()->accounts->twitch.getCurrent(); getApp()->twitch2->pubsub->listenToChannelModerationActions(roomId, account); + getApp()->twitch2->pubsub->listenToChannelPointRewards(roomId, account); } void TwitchChannel::refreshChatters() diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index 4ba7476f3..86ecaad2d 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -7,6 +7,7 @@ #include "common/Outcome.hpp" #include "common/UniqueAccess.hpp" #include "common/UsernameSet.hpp" +#include "providers/twitch/ChannelPointReward.hpp" #include "providers/twitch/TwitchEmotes.hpp" #include "providers/twitch/api/Helix.hpp" @@ -108,6 +109,14 @@ public: pajlada::Signals::NoArgSignal liveStatusChanged; pajlada::Signals::NoArgSignal roomModesChanged; + // Channel point rewards + pajlada::Signals::SelfDisconnectingSignal + channelPointRewardAdded; + void addChannelPointReward(const ChannelPointReward &reward); + bool isChannelPointRewardKnown(const QString &rewardId); + boost::optional channelPointReward( + const QString &rewardId) const; + private: struct NameOptions { QString displayName; @@ -158,6 +167,7 @@ private: UniqueAccess>> badgeSets_; // "subscribers": { "0": ... "3": ... "6": ... UniqueAccess> cheerEmoteSets_; + UniqueAccess> channelPointRewards_; bool mod_ = false; bool vip_ = false; diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index a5475c716..1d6849820 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -192,6 +192,17 @@ MessagePtr TwitchMessageBuilder::build() this->parseRoomID(); + // If it is a reward it has to be appended first + if (this->args.channelPointRewardId != "") + { + const auto &reward = this->twitchChannel->channelPointReward( + this->args.channelPointRewardId); + if (reward) + { + this->appendChannelPointRewardMessage(reward.get(), this); + } + } + this->appendChannelName(); if (this->tags.contains("rm-deleted")) @@ -1130,4 +1141,34 @@ Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string) return Success; } +void TwitchMessageBuilder::appendChannelPointRewardMessage( + const ChannelPointReward &reward, MessageBuilder *builder) +{ + QString redeemed = "Redeemed"; + if (!reward.isUserInputRequired) + { + builder->emplace( + reward.user.login, MessageElementFlag::ChannelPointReward, + MessageColor::Text, FontStyle::ChatMediumBold); + redeemed = "redeemed"; + } + builder->emplace(redeemed, + MessageElementFlag::ChannelPointReward); + builder->emplace( + reward.title, MessageElementFlag::ChannelPointReward, + MessageColor::Text, FontStyle::ChatMediumBold); + builder->emplace( + reward.image, MessageElementFlag::ChannelPointReward); + builder->emplace( + QString::number(reward.cost), MessageElementFlag::ChannelPointReward, + MessageColor::Text, FontStyle::ChatMediumBold); + if (reward.isUserInputRequired) + { + builder->emplace( + MessageElementFlag::ChannelPointReward); + } + + builder->message().flags.set(MessageFlag::RedeemedChannelPointReward); +} + } // namespace chatterino diff --git a/src/providers/twitch/TwitchMessageBuilder.hpp b/src/providers/twitch/TwitchMessageBuilder.hpp index 4bd72f149..5bdaebbf4 100644 --- a/src/providers/twitch/TwitchMessageBuilder.hpp +++ b/src/providers/twitch/TwitchMessageBuilder.hpp @@ -1,8 +1,9 @@ -#pragma once +#pragma once #include "common/Aliases.hpp" #include "common/Outcome.hpp" #include "messages/SharedMessageBuilder.hpp" +#include "providers/twitch/ChannelPointReward.hpp" #include "providers/twitch/TwitchBadge.hpp" #include @@ -42,6 +43,9 @@ public: void triggerHighlights() override; MessagePtr build() override; + static void appendChannelPointRewardMessage( + const ChannelPointReward &reward, MessageBuilder *builder); + private: void parseUsernameColor() override; void parseUsername() override; diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index 80a3ef45d..53ed0fad4 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -171,6 +171,7 @@ void WindowManager::updateWordTypeMask() : MEF::NonBoldUsername); flags.set(settings->lowercaseDomains ? MEF::LowercaseLink : MEF::OriginalLink); + flags.set(MEF::ChannelPointReward); // update flags MessageElementFlags newFlags = static_cast(flags); diff --git a/src/util/RapidjsonHelpers.cpp b/src/util/RapidjsonHelpers.cpp index 2a9742df2..b75f9ca4f 100644 --- a/src/util/RapidjsonHelpers.cpp +++ b/src/util/RapidjsonHelpers.cpp @@ -28,5 +28,22 @@ namespace rj { return std::string(buffer.GetString()); } + bool getSafeObject(rapidjson::Value &obj, const char *key, + rapidjson::Value &out) + { + if (!checkJsonValue(obj, key)) + { + return false; + } + + out = obj[key].Move(); + return true; + } + + bool checkJsonValue(const rapidjson::Value &obj, const char *key) + { + return obj.IsObject() && !obj.IsNull() && obj.HasMember(key); + } + } // namespace rj } // namespace chatterino diff --git a/src/util/RapidjsonHelpers.hpp b/src/util/RapidjsonHelpers.hpp index 84a4b8f7e..6088b44ca 100644 --- a/src/util/RapidjsonHelpers.hpp +++ b/src/util/RapidjsonHelpers.hpp @@ -67,20 +67,12 @@ namespace rj { arr.PushBack(pajlada::Serialize::get(value, a), a); } + bool checkJsonValue(const rapidjson::Value &obj, const char *key); + template bool getSafe(const rapidjson::Value &obj, const char *key, Type &out) { - if (!obj.IsObject()) - { - return false; - } - - if (!obj.HasMember(key)) - { - return false; - } - - if (obj.IsNull()) + if (!checkJsonValue(obj, key)) { return false; } @@ -100,6 +92,9 @@ namespace rj { return !error; } + bool getSafeObject(rapidjson::Value &obj, const char *key, + rapidjson::Value &out); + std::string stringify(const rapidjson::Value &value); } // namespace rj diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index 3712388bd..415224748 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -25,6 +25,8 @@ #include "widgets/splits/SplitContainer.hpp" #ifdef C_DEBUG +# include +# include "providers/twitch/PubsubClient.hpp" # include "util/SampleCheerMessages.hpp" # include "util/SampleLinks.hpp" #endif @@ -239,6 +241,12 @@ void Window::addDebugStuff() linkMessages.emplace_back(R"(@badge-info=subscriber/48;badges=broadcaster/1,subscriber/36,partner/1;color=#CC44FF;display-name=pajlada;emotes=;flags=;id=3c23cf3c-0864-4699-a76b-089350141147;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1577628844607;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada : Links that should pass: )" + getValidLinks().join(' ')); linkMessages.emplace_back(R"(@badge-info=subscriber/48;badges=broadcaster/1,subscriber/36,partner/1;color=#CC44FF;display-name=pajlada;emotes=;flags=;id=3c23cf3c-0864-4699-a76b-089350141147;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1577628844607;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada : Links that should NOT pass: )" + getInvalidLinks().join(' ')); linkMessages.emplace_back(R"(@badge-info=subscriber/48;badges=broadcaster/1,subscriber/36,partner/1;color=#CC44FF;display-name=pajlada;emotes=;flags=;id=3c23cf3c-0864-4699-a76b-089350141147;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1577628844607;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada : Links that should technically pass but we choose not to parse them: )" + getValidButIgnoredLinks().join(' ')); + + // channel point reward test + const char *channelRewardMessage = "{ \"type\": \"MESSAGE\", \"data\": { \"topic\": \"community-points-channel-v1.11148817\", \"message\": { \"type\": \"reward-redeemed\", \"data\": { \"timestamp\": \"2020-07-13T20:19:31.430785354Z\", \"redemption\": { \"id\": \"b9628798-1b4e-4122-b2a6-031658df6755\", \"user\": { \"id\": \"91800084\", \"login\": \"cranken1337\", \"display_name\": \"cranken1337\" }, \"channel_id\": \"11148817\", \"redeemed_at\": \"2020-07-13T20:19:31.345237005Z\", \"reward\": { \"id\": \"313969fe-cc9f-4a0a-83c6-172acbd96957\", \"channel_id\": \"11148817\", \"title\": \"annoying reward pogchamp\", \"prompt\": \"\", \"cost\": 3000, \"is_user_input_required\": true, \"is_sub_only\": false, \"image\": null, \"default_image\": { \"url_1x\": \"https://static-cdn.jtvnw.net/custom-reward-images/default-1.png\", \"url_2x\": \"https://static-cdn.jtvnw.net/custom-reward-images/default-2.png\", \"url_4x\": \"https://static-cdn.jtvnw.net/custom-reward-images/default-4.png\" }, \"background_color\": \"#52ACEC\", \"is_enabled\": true, \"is_paused\": false, \"is_in_stock\": true, \"max_per_stream\": { \"is_enabled\": false, \"max_per_stream\": 0 }, \"should_redemptions_skip_request_queue\": false, \"template_id\": null, \"updated_for_indicator_at\": \"2020-01-20T04:33:33.624956679Z\" }, \"user_input\": \"wow, amazing reward\", \"status\": \"UNFULFILLED\", \"cursor\": \"Yjk2Mjg3OTgtMWI0ZS00MTIyLWIyYTYtMDMxNjU4ZGY2NzU1X18yMDIwLTA3LTEzVDIwOjE5OjMxLjM0NTIzNzAwNVo=\" } } } } }"; + const char *channelRewardMessage2 = "{ \"type\": \"MESSAGE\", \"data\": { \"topic\": \"community-points-channel-v1.11148817\", \"message\": { \"type\": \"reward-redeemed\", \"data\": { \"timestamp\": \"2020-07-13T20:19:31.430785354Z\", \"redemption\": { \"id\": \"b9628798-1b4e-4122-b2a6-031658df6755\", \"user\": { \"id\": \"91800084\", \"login\": \"cranken1337\", \"display_name\": \"cranken1337\" }, \"channel_id\": \"11148817\", \"redeemed_at\": \"2020-07-13T20:19:31.345237005Z\", \"reward\": { \"id\": \"313969fe-cc9f-4a0a-83c6-172acbd96957\", \"channel_id\": \"11148817\", \"title\": \"annoying reward pogchamp\", \"prompt\": \"\", \"cost\": 3000, \"is_user_input_required\": false, \"is_sub_only\": false, \"image\": null, \"default_image\": { \"url_1x\": \"https://static-cdn.jtvnw.net/custom-reward-images/default-1.png\", \"url_2x\": \"https://static-cdn.jtvnw.net/custom-reward-images/default-2.png\", \"url_4x\": \"https://static-cdn.jtvnw.net/custom-reward-images/default-4.png\" }, \"background_color\": \"#52ACEC\", \"is_enabled\": true, \"is_paused\": false, \"is_in_stock\": true, \"max_per_stream\": { \"is_enabled\": false, \"max_per_stream\": 0 }, \"should_redemptions_skip_request_queue\": false, \"template_id\": null, \"updated_for_indicator_at\": \"2020-01-20T04:33:33.624956679Z\" }, \"status\": \"UNFULFILLED\", \"cursor\": \"Yjk2Mjg3OTgtMWI0ZS00MTIyLWIyYTYtMDMxNjU4ZGY2NzU1X18yMDIwLTA3LTEzVDIwOjE5OjMxLjM0NTIzNzAwNVo=\" } } } } }"; + const char *channelRewardIRCMessage(R"(@badge-info=subscriber/43;badges=subscriber/42;color=#1E90FF;custom-reward-id=313969fe-cc9f-4a0a-83c6-172acbd96957;display-name=Cranken1337;emotes=;flags=;id=3cee3f27-a1d0-44d1-a606-722cebdad08b;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1594756484132;turbo=0;user-id=91800084;user-type= :cranken1337!cranken1337@cranken1337.tmi.twitch.tv PRIVMSG #pajlada :wow, amazing reward)"); + // clang-format on createWindowShortcut(this, "F6", [=] { @@ -265,13 +273,28 @@ void Window::addDebugStuff() }); createWindowShortcut(this, "F9", [=] { - auto *dialog = new WelcomeDialog(); - dialog->setAttribute(Qt::WA_DeleteOnClose); - dialog->show(); + rapidjson::Document doc; + auto app = getApp(); + static bool alt = true; + if (alt) + { + doc.Parse(channelRewardMessage); + app->twitch.server->addFakeMessage(channelRewardIRCMessage); + app->twitch.pubsub->signals_.pointReward.redeemed.invoke( + doc["data"]["message"]["data"]["redemption"]); + alt = !alt; + } + else + { + doc.Parse(channelRewardMessage2); + app->twitch.pubsub->signals_.pointReward.redeemed.invoke( + doc["data"]["message"]["data"]["redemption"]); + alt = !alt; + } }); #endif -} +} // namespace chatterino void Window::addShortcuts() { diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 6cc53b415..abcce8038 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -856,6 +856,7 @@ MessageElementFlags ChannelView::getFlags() const if (this->channel_ == app->twitch.server->mentionsChannel) { flags.set(MessageElementFlag::ChannelName); + flags.unset(MessageElementFlag::ChannelPointReward); } } @@ -902,6 +903,9 @@ void ChannelView::drawMessages(QPainter &painter) MessageLayout *end = nullptr; bool windowFocused = this->window() == QApplication::activeWindow(); + auto app = getApp(); + bool isMentions = this->channel_ == app->twitch.server->mentionsChannel; + for (size_t i = start; i < messagesSnapshot.size(); ++i) { MessageLayout *layout = messagesSnapshot[i].get(); @@ -913,7 +917,7 @@ void ChannelView::drawMessages(QPainter &painter) } layout->paint(painter, DRAW_WIDTH, y, i, this->selection_, - isLastMessage, windowFocused); + isLastMessage, windowFocused, isMentions); y += layout->getHeight();