mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
Add support for non-highlight channel point rewards (#1809)
This commit is contained in:
parent
1bd3d10eef
commit
11b8948290
23 changed files with 498 additions and 25 deletions
|
@ -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 \
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 1c38746b05d9311e73c8c8acdfdc4d36c9c551be
|
||||
Subproject commit 6665ccad90461c01b7fe704a98a835953d644156
|
|
@ -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<TwitchChannel *>(chan.get());
|
||||
channel->addChannelPointReward(ChannelPointReward(data));
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Couldn't find channel id of point reward";
|
||||
}
|
||||
});
|
||||
|
||||
this->twitch.pubsub->start();
|
||||
|
||||
auto RequestModerationActions = [=]() {
|
||||
|
|
|
@ -35,6 +35,7 @@ enum class MessageFlag : uint32_t {
|
|||
Debug = (1 << 18),
|
||||
Similar = (1 << 19),
|
||||
RedeemedHighlight = (1 << 20),
|
||||
RedeemedChannelPointReward = (1 << 21),
|
||||
};
|
||||
using MessageFlags = FlagsEnum<MessageFlag>;
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ struct MessageParseArgs {
|
|||
bool isSentWhisper = false;
|
||||
bool trimSubscriberUsername = false;
|
||||
bool isStaffOrBroadcaster = false;
|
||||
QString channelPointRewardId = "";
|
||||
};
|
||||
|
||||
class MessageBuilder
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "messages/Link.hpp"
|
||||
#include "messages/MessageColor.hpp"
|
||||
#include "singletons/Fonts.hpp"
|
||||
#include "src/messages/ImageSet.hpp"
|
||||
|
||||
#include <QRect>
|
||||
#include <QString>
|
||||
|
@ -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<Word> 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
|
||||
|
|
|
@ -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())
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
|
127
src/providers/twitch/ChannelPointReward.cpp
Normal file
127
src/providers/twitch/ChannelPointReward.cpp
Normal file
|
@ -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
|
33
src/providers/twitch/ChannelPointReward.hpp
Normal file
33
src/providers/twitch/ChannelPointReward.hpp
Normal file
|
@ -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
|
|
@ -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<TwitchChannel *>(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,8 +424,8 @@ void IrcMessageHandler::handleClearMessageMessage(Communi::IrcMessage *message)
|
|||
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
qDebug()
|
||||
<< "[IrcMessageHandler:handleClearMessageMessage] Twitch channel"
|
||||
qDebug() << "[IrcMessageHandler:handleClearMessageMessage] Twitch "
|
||||
"channel"
|
||||
<< chanName << "not found";
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -804,6 +804,26 @@ void PubSub::listenToChannelModerationActions(
|
|||
this->listenToTopic(topic, account);
|
||||
}
|
||||
|
||||
void PubSub::listenToChannelPointRewards(const QString &channelID,
|
||||
std::shared_ptr<TwitchAccount> 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<TwitchAccount> 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;
|
||||
|
|
|
@ -122,6 +122,10 @@ public:
|
|||
Signal<const rapidjson::Value &> received;
|
||||
Signal<const rapidjson::Value &> sent;
|
||||
} whisper;
|
||||
|
||||
struct {
|
||||
Signal<rapidjson::Value &> redeemed;
|
||||
} pointReward;
|
||||
} signals_;
|
||||
|
||||
void listenToWhispers(std::shared_ptr<TwitchAccount> account);
|
||||
|
@ -131,6 +135,9 @@ public:
|
|||
void listenToChannelModerationActions(
|
||||
const QString &channelID, std::shared_ptr<TwitchAccount> account);
|
||||
|
||||
void listenToChannelPointRewards(const QString &channelID,
|
||||
std::shared_ptr<TwitchAccount> account);
|
||||
|
||||
std::vector<std::unique_ptr<rapidjson::Document>> requests;
|
||||
|
||||
private:
|
||||
|
|
|
@ -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<ChannelPointReward> 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()
|
||||
|
|
|
@ -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<ChannelPointReward>
|
||||
channelPointRewardAdded;
|
||||
void addChannelPointReward(const ChannelPointReward &reward);
|
||||
bool isChannelPointRewardKnown(const QString &rewardId);
|
||||
boost::optional<ChannelPointReward> channelPointReward(
|
||||
const QString &rewardId) const;
|
||||
|
||||
private:
|
||||
struct NameOptions {
|
||||
QString displayName;
|
||||
|
@ -158,6 +167,7 @@ private:
|
|||
UniqueAccess<std::map<QString, std::map<QString, EmotePtr>>>
|
||||
badgeSets_; // "subscribers": { "0": ... "3": ... "6": ...
|
||||
UniqueAccess<std::vector<CheerEmoteSet>> cheerEmoteSets_;
|
||||
UniqueAccess<std::map<QString, ChannelPointReward>> channelPointRewards_;
|
||||
|
||||
bool mod_ = false;
|
||||
bool vip_ = false;
|
||||
|
|
|
@ -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<TextElement>(
|
||||
reward.user.login, MessageElementFlag::ChannelPointReward,
|
||||
MessageColor::Text, FontStyle::ChatMediumBold);
|
||||
redeemed = "redeemed";
|
||||
}
|
||||
builder->emplace<TextElement>(redeemed,
|
||||
MessageElementFlag::ChannelPointReward);
|
||||
builder->emplace<TextElement>(
|
||||
reward.title, MessageElementFlag::ChannelPointReward,
|
||||
MessageColor::Text, FontStyle::ChatMediumBold);
|
||||
builder->emplace<ScalingImageElement>(
|
||||
reward.image, MessageElementFlag::ChannelPointReward);
|
||||
builder->emplace<TextElement>(
|
||||
QString::number(reward.cost), MessageElementFlag::ChannelPointReward,
|
||||
MessageColor::Text, FontStyle::ChatMediumBold);
|
||||
if (reward.isUserInputRequired)
|
||||
{
|
||||
builder->emplace<LinebreakElement>(
|
||||
MessageElementFlag::ChannelPointReward);
|
||||
}
|
||||
|
||||
builder->message().flags.set(MessageFlag::RedeemedChannelPointReward);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -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 <IrcMessage>
|
||||
|
@ -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;
|
||||
|
|
|
@ -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<MessageElementFlags>(flags);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -67,20 +67,12 @@ namespace rj {
|
|||
arr.PushBack(pajlada::Serialize<Type>::get(value, a), a);
|
||||
}
|
||||
|
||||
bool checkJsonValue(const rapidjson::Value &obj, const char *key);
|
||||
|
||||
template <typename Type>
|
||||
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
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
#include "widgets/splits/SplitContainer.hpp"
|
||||
|
||||
#ifdef C_DEBUG
|
||||
# include <rapidjson/document.h>
|
||||
# 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()
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
Loading…
Reference in a new issue