mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
Add subtitle to Hype Chats (#4715)
* feat: more hype chat * Add `std::chrono::seconds` overload to formatTime * Move & rename it to HypeChat + some other mini things * Add changelog entry * fix formattime test --------- Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
parent
71594ad0d8
commit
703847c9ba
8 changed files with 207 additions and 8 deletions
|
@ -10,6 +10,7 @@
|
|||
- Minor: Improved editing hotkeys. (#4628)
|
||||
- Minor: The input completion and quick switcher are now styled to match your theme. (#4671)
|
||||
- Minor: Added setting to only show tabs with live channels (default toggle hotkey: Ctrl+Shift+L). (#4358)
|
||||
- Minor: Added better support for Twitch's Hype Chat feature. (#4715)
|
||||
- Minor: Added option to subscribe to and unsubscribe from reply threads. (#4680, #4739)
|
||||
- Minor: Added a message for when Chatterino joins a channel (#4616)
|
||||
- Minor: Add accelerators to the right click menu for messages (#4705)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include "IrcMessageHandler.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Literals.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "messages/LimitedQueue.hpp"
|
||||
|
@ -26,6 +27,8 @@
|
|||
#include "util/StreamerMode.hpp"
|
||||
|
||||
#include <IrcMessage>
|
||||
#include <QLocale>
|
||||
#include <QStringBuilder>
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
|
@ -156,9 +159,23 @@ void updateReplyParticipatedStatus(const QVariantMap &tags,
|
|||
}
|
||||
}
|
||||
|
||||
ChannelPtr channelOrEmptyByTarget(const QString &target,
|
||||
TwitchIrcServer &server)
|
||||
{
|
||||
QString channelName;
|
||||
if (!trimChannelName(target, channelName))
|
||||
{
|
||||
return Channel::getEmpty();
|
||||
}
|
||||
|
||||
return server.getChannelOrEmpty(channelName);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
namespace chatterino {
|
||||
|
||||
using namespace literals;
|
||||
|
||||
static float relativeSimilarity(const QString &str1, const QString &str2)
|
||||
{
|
||||
// Longest Common Substring Problem
|
||||
|
@ -314,6 +331,16 @@ std::vector<MessagePtr> IrcMessageHandler::parsePrivMessage(
|
|||
builtMessages.emplace_back(builder.build());
|
||||
builder.triggerHighlights();
|
||||
}
|
||||
|
||||
if (message->tags().contains(u"pinned-chat-paid-amount"_s))
|
||||
{
|
||||
auto ptr = TwitchMessageBuilder::buildHypeChatMessage(message);
|
||||
if (ptr)
|
||||
{
|
||||
builtMessages.emplace_back(std::move(ptr));
|
||||
}
|
||||
}
|
||||
|
||||
return builtMessages;
|
||||
}
|
||||
|
||||
|
@ -330,6 +357,21 @@ void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message,
|
|||
message, message->target(),
|
||||
message->content().replace(COMBINED_FIXER, ZERO_WIDTH_JOINER), server,
|
||||
false, message->isAction());
|
||||
|
||||
auto chan = channelOrEmptyByTarget(message->target(), server);
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (message->tags().contains(u"pinned-chat-paid-amount"_s))
|
||||
{
|
||||
auto ptr = TwitchMessageBuilder::buildHypeChatMessage(message);
|
||||
if (ptr)
|
||||
{
|
||||
chan->addMessage(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<MessagePtr> IrcMessageHandler::parseMessageWithReply(
|
||||
|
@ -442,13 +484,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
|
|||
TwitchIrcServer &server, bool isSub,
|
||||
bool isAction)
|
||||
{
|
||||
QString channelName;
|
||||
if (!trimChannelName(target, channelName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto chan = server.getChannelOrEmpty(channelName);
|
||||
auto chan = channelOrEmptyByTarget(target, server);
|
||||
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "Application.hpp"
|
||||
#include "common/LinkParser.hpp"
|
||||
#include "common/Literals.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/ignores/IgnoreController.hpp"
|
||||
|
@ -28,8 +29,10 @@
|
|||
#include "singletons/Settings.hpp"
|
||||
#include "singletons/Theme.hpp"
|
||||
#include "singletons/WindowManager.hpp"
|
||||
#include "util/FormatTime.hpp"
|
||||
#include "util/Helpers.hpp"
|
||||
#include "util/IrcHelpers.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
#include "util/Qt.hpp"
|
||||
#include "widgets/Window.hpp"
|
||||
|
||||
|
@ -38,8 +41,15 @@
|
|||
#include <QDebug>
|
||||
#include <QStringRef>
|
||||
|
||||
#include <chrono>
|
||||
#include <unordered_set>
|
||||
|
||||
using namespace chatterino::literals;
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
const QString regexHelpString("(\\w+)[.,!?;:]*?$");
|
||||
|
||||
// matches a mention with punctuation at the end, like "@username," or "@username!!!" where capture group would return "username"
|
||||
|
@ -53,6 +63,19 @@ const QSet<QString> zeroWidthEmotes{
|
|||
"ReinDeer", "CandyCane", "cvMask", "cvHazmat",
|
||||
};
|
||||
|
||||
struct HypeChatPaidLevel {
|
||||
std::chrono::seconds duration;
|
||||
uint8_t numeric;
|
||||
};
|
||||
|
||||
const std::unordered_map<QString, HypeChatPaidLevel> HYPE_CHAT_PAID_LEVEL{
|
||||
{u"ONE"_s, {30s, 1}}, {u"TWO"_s, {2min + 30s, 2}},
|
||||
{u"THREE"_s, {5min, 3}}, {u"FOUR"_s, {10min, 4}},
|
||||
{u"FIVE"_s, {30min, 5}}, {u"SIX"_s, {1h, 6}},
|
||||
{u"SEVEN"_s, {2h, 7}}, {u"EIGHT"_s, {3h, 8}},
|
||||
{u"NINE"_s, {4h, 9}}, {u"TEN"_s, {5h, 10}},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -1739,6 +1762,45 @@ void TwitchMessageBuilder::listOfUsersSystemMessage(
|
|||
builder->message().searchText = text;
|
||||
}
|
||||
|
||||
MessagePtr TwitchMessageBuilder::buildHypeChatMessage(
|
||||
Communi::IrcPrivateMessage *message)
|
||||
{
|
||||
auto level = message->tag(u"pinned-chat-paid-level"_s).toString();
|
||||
auto currency = message->tag(u"pinned-chat-paid-currency"_s).toString();
|
||||
bool okAmount = false;
|
||||
auto amount = message->tag(u"pinned-chat-paid-amount"_s).toInt(&okAmount);
|
||||
bool okExponent = false;
|
||||
auto exponent =
|
||||
message->tag(u"pinned-chat-paid-exponent"_s).toInt(&okExponent);
|
||||
if (!okAmount || !okExponent || currency.isEmpty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
// additionally, there's `pinned-chat-paid-is-system-message` which isn't used by Chatterino.
|
||||
|
||||
QString subtitle;
|
||||
auto levelIt = HYPE_CHAT_PAID_LEVEL.find(level);
|
||||
if (levelIt != HYPE_CHAT_PAID_LEVEL.end())
|
||||
{
|
||||
const auto &level = levelIt->second;
|
||||
subtitle = u"Level %1 Hype Chat (%2) "_s.arg(level.numeric)
|
||||
.arg(formatTime(level.duration));
|
||||
}
|
||||
else
|
||||
{
|
||||
subtitle = u"Hype Chat "_s;
|
||||
}
|
||||
|
||||
// actualAmount = amount * 10^(-exponent)
|
||||
double actualAmount = std::pow(10.0, double(-exponent)) * double(amount);
|
||||
subtitle += QLocale::system().toCurrencyString(actualAmount, currency);
|
||||
|
||||
MessageBuilder builder(systemMessage, parseTagString(subtitle),
|
||||
calculateMessageTime(message).time());
|
||||
builder->flags.set(MessageFlag::ElevatedMessage);
|
||||
return builder.release();
|
||||
}
|
||||
|
||||
void TwitchMessageBuilder::setThread(std::shared_ptr<MessageThread> thread)
|
||||
{
|
||||
this->thread_ = std::move(thread);
|
||||
|
|
|
@ -86,6 +86,8 @@ public:
|
|||
QString prefix, const std::vector<HelixModerator> &users,
|
||||
Channel *channel, MessageBuilder *builder);
|
||||
|
||||
static MessagePtr buildHypeChatMessage(Communi::IrcPrivateMessage *message);
|
||||
|
||||
// Shares some common logic from SharedMessageBuilder::parseBadgeTag
|
||||
static std::unordered_map<QString, QString> parseBadgeInfoTag(
|
||||
const QVariantMap &tags);
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
#include "FormatTime.hpp"
|
||||
#include "util/FormatTime.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
|
@ -57,4 +60,15 @@ QString formatTime(QString totalSecondsString)
|
|||
return "n/a";
|
||||
}
|
||||
|
||||
QString formatTime(std::chrono::seconds totalSeconds)
|
||||
{
|
||||
auto count = totalSeconds.count();
|
||||
|
||||
return formatTime(static_cast<int>(std::clamp(
|
||||
count,
|
||||
static_cast<std::chrono::seconds::rep>(std::numeric_limits<int>::min()),
|
||||
static_cast<std::chrono::seconds::rep>(
|
||||
std::numeric_limits<int>::max()))));
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -2,10 +2,13 @@
|
|||
|
||||
#include <QString>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
// format: 1h 23m 42s
|
||||
QString formatTime(int totalSeconds);
|
||||
QString formatTime(QString totalSecondsString);
|
||||
QString formatTime(std::chrono::seconds totalSeconds);
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -108,7 +108,13 @@ const QStringList &getSampleMiscMessages()
|
|||
R"(@badge-info=subscriber/47;badges=broadcaster/1,subscriber/3012,twitchconAmsterdam2020/1;color=#FF0000;display-name=Supinic;emotes=;flags=;id=8c26e1ab-b50c-4d9d-bc11-3fd57a941d90;login=supinic;mod=0;msg-id=announcement;msg-param-color=PRIMARY;room-id=31400525;subscriber=1;system-msg=;tmi-sent-ts=1648762219962;user-id=31400525;user-type= :tmi.twitch.tv USERNOTICE #supinic :mm test lol)",
|
||||
|
||||
// Elevated Message (Paid option for keeping a message in chat longer)
|
||||
// no level
|
||||
R"(@badge-info=subscriber/3;badges=subscriber/0,bits-charity/1;color=#0000FF;display-name=SnoopyTheBot;emotes=;first-msg=0;flags=;id=8779a9e5-cf1b-47b3-b9fe-67a5b1b605f6;mod=0;pinned-chat-paid-amount=500;pinned-chat-paid-canonical-amount=5;pinned-chat-paid-currency=USD;pinned-chat-paid-exponent=2;returning-chatter=0;room-id=36340781;subscriber=1;tmi-sent-ts=1664505974154;turbo=0;user-id=136881249;user-type= :snoopythebot!snoopythebot@snoopythebot.tmi.twitch.tv PRIVMSG #pajlada :-$5)",
|
||||
// level 1
|
||||
R"(@pinned-chat-paid-level=ONE;mod=0;flags=;pinned-chat-paid-amount=1400;pinned-chat-paid-exponent=2;tmi-sent-ts=1687970631828;subscriber=1;user-type=;color=#9DA364;emotes=;badges=predictions/blue-1,subscriber/60,twitchconAmsterdam2020/1;pinned-chat-paid-canonical-amount=1400;turbo=0;user-id=26753388;id=e6681ba0-cdc6-4482-93a3-515b74361e8b;room-id=36340781;first-msg=0;returning-chatter=0;pinned-chat-paid-currency=NOK;pinned-chat-paid-is-system-message=0;badge-info=predictions/Day\s53/53\sforsenSmug,subscriber/67;display-name=matrHS :matrhs!matrhs@matrhs.tmi.twitch.tv PRIVMSG #pajlada :Title: Beating the record. but who is recordingLOL)",
|
||||
R"(@flags=;pinned-chat-paid-amount=8761;turbo=0;user-id=35669184;pinned-chat-paid-level=ONE;user-type=;pinned-chat-paid-canonical-amount=8761;badge-info=subscriber/2;badges=subscriber/2,sub-gifter/1;emotes=;pinned-chat-paid-exponent=2;subscriber=1;mod=0;room-id=36340781;returning-chatter=0;id=289b614d-1837-4cff-ac22-ce33a9735323;first-msg=0;tmi-sent-ts=1687631719188;color=#00FF7F;pinned-chat-paid-currency=RUB;display-name=Danis;pinned-chat-paid-is-system-message=0 :danis!danis@danis.tmi.twitch.tv PRIVMSG #pajlada :-1 lulw)",
|
||||
// level 2
|
||||
R"(@room-id=36340781;tmi-sent-ts=1687970634371;flags=;id=39a80a3d-c16e-420f-9bbb-faba4976a3bb;badges=subscriber/6,premium/1;emotes=;display-name=rickharrisoncoc;pinned-chat-paid-level=TWO;turbo=0;pinned-chat-paid-amount=500;pinned-chat-paid-is-system-message=0;color=#FF69B4;subscriber=1;user-type=;first-msg=0;pinned-chat-paid-currency=USD;pinned-chat-paid-canonical-amount=500;user-id=518404689;badge-info=subscriber/10;pinned-chat-paid-exponent=2;returning-chatter=0;mod=0 :rickharrisoncoc!rickharrisoncoc@rickharrisoncoc.tmi.twitch.tv PRIVMSG #pajlada :forsen please read my super chat. Please.)",
|
||||
};
|
||||
return list;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
using namespace chatterino;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
TEST(FormatTime, Int)
|
||||
{
|
||||
|
@ -131,3 +134,75 @@ TEST(FormatTime, QString)
|
|||
<< ") did not match expected value " << qUtf8Printable(expected);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FormatTime, chrono)
|
||||
{
|
||||
struct TestCase {
|
||||
std::chrono::seconds input;
|
||||
QString expectedOutput;
|
||||
};
|
||||
|
||||
std::vector<TestCase> tests{
|
||||
{
|
||||
0s,
|
||||
"",
|
||||
},
|
||||
{
|
||||
1337s,
|
||||
"22m 17s",
|
||||
},
|
||||
{
|
||||
{22min + 17s},
|
||||
"22m 17s",
|
||||
},
|
||||
{
|
||||
623452s,
|
||||
"7d 5h 10m 52s",
|
||||
},
|
||||
{
|
||||
8345s,
|
||||
"2h 19m 5s",
|
||||
},
|
||||
{
|
||||
314034s,
|
||||
"3d 15h 13m 54s",
|
||||
},
|
||||
{
|
||||
27s,
|
||||
"27s",
|
||||
},
|
||||
{
|
||||
34589s,
|
||||
"9h 36m 29s",
|
||||
},
|
||||
{
|
||||
9h + 36min + 29s,
|
||||
"9h 36m 29s",
|
||||
},
|
||||
{
|
||||
3659s,
|
||||
"1h 59s",
|
||||
},
|
||||
{
|
||||
1h + 59s,
|
||||
"1h 59s",
|
||||
},
|
||||
{
|
||||
1045345s,
|
||||
"12d 2h 22m 25s",
|
||||
},
|
||||
{
|
||||
86432s,
|
||||
"1d 32s",
|
||||
},
|
||||
};
|
||||
|
||||
for (const auto &[input, expected] : tests)
|
||||
{
|
||||
const auto actual = formatTime(input);
|
||||
|
||||
EXPECT_EQ(actual, expected)
|
||||
<< qUtf8Printable(actual) << " did not match expected value "
|
||||
<< qUtf8Printable(expected);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue