diff --git a/CHANGELOG.md b/CHANGELOG.md
index e788b34fc..e90ed99ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,7 @@
- Major: Improve high-DPI support on Windows. (#4868, #5391, #5664, #5666)
- Major: Added transparent overlay window (default keybind: CTRL + ALT + N). (#4746, #5643, #5659)
- Minor: Removed the Ctrl+Shift+L hotkey for toggling the "live only" tab visibility state. (#5530)
-- Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606, #5625)
+- Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606, #5625, #5661)
- Minor: Moved tab visibility control to a submenu, without any toggle actions. (#5530)
- Minor: Add option to customise Moderation buttons with images. (#5369)
- Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300)
@@ -55,6 +55,7 @@
- Bugfix: Fixed event emotes not showing up in autocomplete and popups. (#5239, #5580, #5582, #5632)
- Bugfix: Fixed tab visibility being controllable in the emote popup. (#5530)
- Bugfix: Fixed account switch not being saved if no other settings were changed. (#5558)
+- Bugfix: Fixed 7TV badges being inadvertently animated. (#5674)
- Bugfix: Fixed some tooltips not being readable. (#5578)
- Bugfix: Fixed log files being locked longer than needed. (#5592)
- Bugfix: Fixed global badges not showing in anonymous mode. (#5599)
@@ -105,13 +106,13 @@
- Dev: Added more tests for input completion. (#5604)
- Dev: Refactored legacy Unicode zero-width-joiner replacement. (#5594)
- Dev: The JSON output when copying a message (SHIFT + right-click) is now more extensive. (#5600)
-- Dev: Added more tests for message building. (#5598, #5654, #5656)
+- Dev: Added more tests for message building. (#5598, #5654, #5656, #5671)
- Dev: Twitch messages are now sent using Twitch's Helix API instead of IRC by default. (#5607)
- Dev: `GIFTimer` is no longer initialized in tests. (#5608)
- Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616)
- Dev: Move plugins to Sol2. (#5622)
- Dev: Refactored static `MessageBuilder` helpers to standalone functions. (#5652)
-- Dev: Decoupled reply parsing from `MessageBuilder`. (#5660)
+- Dev: Decoupled reply parsing from `MessageBuilder`. (#5660, #5668)
- Dev: Refactored IRC message building. (#5663)
## 2.5.1
diff --git a/lib/expected-lite b/lib/expected-lite
index 88ee08eb3..5b5caad7c 160000
--- a/lib/expected-lite
+++ b/lib/expected-lite
@@ -1 +1 @@
-Subproject commit 88ee08eb3c3f3627ca54b90dafd1d63a6d4da96b
+Subproject commit 5b5caad7cd57d5ba3ca796bf1521b131d73ca405
diff --git a/lib/settings b/lib/settings
index c58874c1a..4a0a1e599 160000
--- a/lib/settings
+++ b/lib/settings
@@ -1 +1 @@
-Subproject commit c58874c1aa5d0619df2c975bcb87433941b46920
+Subproject commit 4a0a1e599377cdcdc91b0fbbefc312936b48730c
diff --git a/mocks/include/mocks/BaseApplication.hpp b/mocks/include/mocks/BaseApplication.hpp
index 2ba9f949c..619203afc 100644
--- a/mocks/include/mocks/BaseApplication.hpp
+++ b/mocks/include/mocks/BaseApplication.hpp
@@ -3,6 +3,7 @@
#include "common/Args.hpp"
#include "mocks/DisabledStreamerMode.hpp"
#include "mocks/EmptyApplication.hpp"
+#include "mocks/TwitchUsers.hpp"
#include "providers/bttv/BttvLiveUpdates.hpp"
#include "singletons/Fonts.hpp"
#include "singletons/Settings.hpp"
@@ -55,6 +56,11 @@ public:
return &this->fonts;
}
+ ITwitchUsers *getTwitchUsers() override
+ {
+ return &this->twitchUsers;
+ }
+
BttvLiveUpdates *getBttvLiveUpdates() override
{
return nullptr;
@@ -71,6 +77,7 @@ public:
DisabledStreamerMode streamerMode;
Theme theme;
Fonts fonts;
+ TwitchUsers twitchUsers;
};
} // namespace chatterino::mock
diff --git a/mocks/include/mocks/TwitchUsers.hpp b/mocks/include/mocks/TwitchUsers.hpp
new file mode 100644
index 000000000..14f6bf7e6
--- /dev/null
+++ b/mocks/include/mocks/TwitchUsers.hpp
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "providers/twitch/TwitchUser.hpp"
+#include "providers/twitch/TwitchUsers.hpp"
+
+namespace chatterino::mock {
+
+class TwitchUsers : public ITwitchUsers
+{
+public:
+ TwitchUsers() = default;
+
+ std::shared_ptr resolveID(const UserId &id)
+ {
+ TwitchUser u = {
+ .id = id.string,
+ .name = {},
+ .displayName = {},
+ };
+ return std::make_shared(u);
+ }
+};
+
+} // namespace chatterino::mock
diff --git a/resources/twitch/sharedChat.png b/resources/twitch/sharedChat.png
new file mode 100644
index 000000000..f9a66b17c
Binary files /dev/null and b/resources/twitch/sharedChat.png differ
diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp
index db479adf3..601ba8ec9 100644
--- a/src/messages/MessageBuilder.cpp
+++ b/src/messages/MessageBuilder.cpp
@@ -32,6 +32,7 @@
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrc.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
+#include "providers/twitch/TwitchUsers.hpp"
#include "singletons/Emotes.hpp"
#include "singletons/Resources.hpp"
#include "singletons/Settings.hpp"
@@ -380,6 +381,18 @@ EmotePtr makeAutoModBadge()
Url{"https://dashboard.twitch.tv/settings/moderation/automod"}});
}
+EmotePtr makeSharedChatBadge(const QString &sourceName)
+{
+ return std::make_shared(Emote{
+ .name = EmoteName{},
+ .images = ImageSet{Image::fromResourcePixmap(
+ getResources().twitch.sharedChat, 0.25)},
+ .tooltip = Tooltip{"Shared Message" +
+ (sourceName.isEmpty() ? "" : " from " + sourceName)},
+ .homePage = Url{"https://link.twitch.tv/SharedChatViewer"},
+ });
+}
+
std::tuple, MessageElementFlags, bool> parseEmote(
TwitchChannel *twitchChannel, const EmoteName &name)
{
@@ -2382,6 +2395,11 @@ void MessageBuilder::parseThread(const QString &messageContent,
this->message().replyParent = parent;
thread->addToThread(std::weak_ptr{this->message_});
+ if (thread->subscribed())
+ {
+ this->message().flags.set(MessageFlag::SubscribedThread);
+ }
+
// enable reply flag
this->message().flags.set(MessageFlag::ReplyMessage);
@@ -2746,6 +2764,28 @@ void MessageBuilder::appendTwitchBadges(const QVariantMap &tags,
return;
}
+ if (this->message().flags.has(MessageFlag::SharedMessage))
+ {
+ const QString sourceId = tags["source-room-id"].toString();
+ QString sourceName;
+ if (sourceId.isEmpty())
+ {
+ sourceName = "";
+ }
+ else if (twitchChannel->roomId() == sourceId)
+ {
+ sourceName = twitchChannel->getName();
+ }
+ else
+ {
+ sourceName =
+ getApp()->getTwitchUsers()->resolveID({sourceId})->displayName;
+ }
+
+ this->emplace(makeSharedChatBadge(sourceName),
+ MessageElementFlag::BadgeSharedChannel);
+ }
+
auto badgeInfos = parseBadgeInfoTag(tags);
auto badges = parseBadgeTag(tags);
appendBadges(this, badges, badgeInfos, twitchChannel);
diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp
index 0e6c07b93..c19ed5c0c 100644
--- a/src/messages/MessageElement.hpp
+++ b/src/messages/MessageElement.hpp
@@ -66,6 +66,10 @@ enum class MessageElementFlag : int64_t {
BitsStatic = (1LL << 11),
BitsAnimated = (1LL << 12),
+ // Slot 0: Twitch
+ // - Shared Channel indicator badge
+ BadgeSharedChannel = (1LL << 37),
+
// Slot 1: Twitch
// - Staff badge
// - Admin badge
@@ -119,7 +123,7 @@ enum class MessageElementFlag : int64_t {
Badges = BadgeGlobalAuthority | BadgePredictions | BadgeChannelAuthority |
BadgeSubscription | BadgeVanity | BadgeChatterino | BadgeSevenTV |
- BadgeFfz,
+ BadgeFfz | BadgeSharedChannel,
ChannelName = (1LL << 20),
diff --git a/src/providers/seventv/SeventvBadges.cpp b/src/providers/seventv/SeventvBadges.cpp
index 2f09bd087..e217fa914 100644
--- a/src/providers/seventv/SeventvBadges.cpp
+++ b/src/providers/seventv/SeventvBadges.cpp
@@ -59,7 +59,7 @@ void SeventvBadges::registerBadge(const QJsonObject &badgeJson)
auto emote = Emote{
.name = EmoteName{},
- .images = SeventvEmotes::createImageSet(badgeJson),
+ .images = SeventvEmotes::createImageSet(badgeJson, true),
.tooltip = Tooltip{badgeJson["tooltip"].toString()},
.homePage = Url{},
.id = EmoteId{badgeID},
diff --git a/src/providers/seventv/SeventvEmotes.cpp b/src/providers/seventv/SeventvEmotes.cpp
index 969b0e5f5..3df627934 100644
--- a/src/providers/seventv/SeventvEmotes.cpp
+++ b/src/providers/seventv/SeventvEmotes.cpp
@@ -106,12 +106,18 @@ CreateEmoteResult createEmote(const QJsonObject &activeEmote,
? createAliasedTooltip(emoteName.string, baseEmoteName.string,
author.string, isGlobal)
: createTooltip(emoteName.string, author.string, isGlobal);
- auto imageSet = SeventvEmotes::createImageSet(emoteData);
+ auto imageSet = SeventvEmotes::createImageSet(emoteData, false);
- auto emote =
- Emote({emoteName, imageSet, tooltip,
- Url{EMOTE_LINK_FORMAT.arg(emoteId.string)}, zeroWidth, emoteId,
- author, makeConditionedOptional(aliasedName, baseEmoteName)});
+ auto emote = Emote({
+ emoteName,
+ imageSet,
+ tooltip,
+ Url{EMOTE_LINK_FORMAT.arg(emoteId.string)},
+ zeroWidth,
+ emoteId,
+ author,
+ makeConditionedOptional(aliasedName, baseEmoteName),
+ });
return {emote, emoteId, emoteName, !emote.images.getImage1()->isEmpty()};
}
@@ -427,7 +433,8 @@ void SeventvEmotes::getEmoteSet(
});
}
-ImageSet SeventvEmotes::createImageSet(const QJsonObject &emoteData)
+ImageSet SeventvEmotes::createImageSet(const QJsonObject &emoteData,
+ bool useStatic)
{
auto host = emoteData["host"].toObject();
// "//cdn.7tv[...]"
@@ -463,9 +470,21 @@ ImageSet SeventvEmotes::createImageSet(const QJsonObject &emoteData)
baseWidth = width;
}
- auto image = Image::fromUrl(
- {QString("https:%1/%2").arg(baseUrl, file["name"].toString())},
- scale, {static_cast(width), file["height"].toInt(16)});
+ auto name = [&] {
+ if (useStatic)
+ {
+ auto staticName = file["static_name"].toString();
+ if (!staticName.isEmpty())
+ {
+ return staticName;
+ }
+ }
+ return file["name"].toString();
+ }();
+
+ auto image =
+ Image::fromUrl({QString("https:%1/%2").arg(baseUrl, name)}, scale,
+ {static_cast(width), file["height"].toInt(16)});
sizes.at(nextSize) = image;
nextSize++;
diff --git a/src/providers/seventv/SeventvEmotes.hpp b/src/providers/seventv/SeventvEmotes.hpp
index 03b966f9c..79a8500a9 100644
--- a/src/providers/seventv/SeventvEmotes.hpp
+++ b/src/providers/seventv/SeventvEmotes.hpp
@@ -153,8 +153,10 @@ public:
* Creates an image set from a 7TV emote or badge.
*
* @param emoteData { host: { files: [], url } }
+ * @param useStatic use static version if possible
*/
- static ImageSet createImageSet(const QJsonObject &emoteData);
+ static ImageSet createImageSet(const QJsonObject &emoteData,
+ bool useStatic);
private:
Atomic> global_;
diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp
index fb9d2fa13..0c345da83 100644
--- a/src/providers/twitch/IrcMessageHandler.cpp
+++ b/src/providers/twitch/IrcMessageHandler.cpp
@@ -123,47 +123,34 @@ int stripLeadingReplyMention(const QVariantMap &tags, QString &content)
return 0;
}
-[[nodiscard]] bool shouldHighlightReplyThread(
- const QVariantMap &tags, const QString &senderLogin,
- std::shared_ptr &thread, bool isNew)
+void checkThreadSubscription(const QVariantMap &tags,
+ const QString &senderLogin,
+ std::shared_ptr &thread)
{
- const auto ¤tLogin =
- getApp()->getAccounts()->twitch.getCurrent()->getUserName();
-
- if (thread->subscribed())
+ if (thread->subscribed() || thread->unsubscribed())
{
- return true;
- }
-
- if (thread->unsubscribed())
- {
- return false;
+ return;
}
if (getSettings()->autoSubToParticipatedThreads)
{
- if (isNew)
- {
- if (const auto it = tags.find("reply-parent-user-login");
- it != tags.end())
- {
- auto name = it.value().toString();
- if (name == currentLogin)
- {
- thread->markSubscribed();
- return true; // already marked as participated
- }
- }
- }
+ const auto ¤tLogin =
+ getApp()->getAccounts()->twitch.getCurrent()->getUserName();
if (senderLogin == currentLogin)
{
thread->markSubscribed();
- // don't set the highlight here
+ }
+ else if (const auto it = tags.find("reply-parent-user-login");
+ it != tags.end())
+ {
+ auto name = it.value().toString();
+ if (name == currentLogin)
+ {
+ thread->markSubscribed();
+ }
}
}
-
- return false;
}
ChannelPtr channelOrEmptyByTarget(const QString &target,
@@ -243,7 +230,6 @@ QMap parseBadges(const QString &badgesString)
struct ReplyContext {
std::shared_ptr thread;
MessagePtr parent;
- bool highlight = false;
};
[[nodiscard]] ReplyContext getReplyContext(
@@ -265,8 +251,7 @@ struct ReplyContext {
if (owned)
{
// Thread already exists (has a reply)
- ctx.highlight = shouldHighlightReplyThread(
- tags, message->nick(), owned, false);
+ checkThreadSubscription(tags, message->nick(), owned);
ctx.thread = owned;
rootThread = owned;
}
@@ -301,8 +286,7 @@ struct ReplyContext {
{
std::shared_ptr newThread =
std::make_shared(foundMessage);
- ctx.highlight = shouldHighlightReplyThread(
- tags, message->nick(), newThread, true);
+ checkThreadSubscription(tags, message->nick(), newThread);
ctx.thread = newThread;
rootThread = newThread;
@@ -724,10 +708,6 @@ std::vector IrcMessageHandler::parseMessageWithReply(
if (built)
{
- if (replyCtx.highlight)
- {
- built->flags.set(MessageFlag::SubscribedThread);
- }
builtMessages.emplace_back(built);
MessageBuilder::triggerHighlights(channel, alert);
}
@@ -1552,8 +1532,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message,
{
// Thread already exists (has a reply)
auto thread = threadIt->second.lock();
- replyCtx.highlight = shouldHighlightReplyThread(
- tags, message->nick(), thread, false);
+ checkThreadSubscription(tags, message->nick(), thread);
replyCtx.thread = thread;
rootThread = thread;
}
@@ -1565,8 +1544,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message,
{
// Found root reply message
auto newThread = std::make_shared(root);
- replyCtx.highlight = shouldHighlightReplyThread(
- tags, message->nick(), newThread, true);
+ checkThreadSubscription(tags, message->nick(), newThread);
replyCtx.thread = newThread;
rootThread = newThread;
@@ -1621,10 +1599,6 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message,
msg->flags.set(MessageFlag::Subscription);
msg->flags.unset(MessageFlag::Highlighted);
}
- if (replyCtx.highlight)
- {
- msg->flags.set(MessageFlag::SubscribedThread);
- }
IrcMessageHandler::setSimilarityFlags(msg, chan);
diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp
index f64eb47e7..88d5ec565 100644
--- a/src/singletons/WindowManager.cpp
+++ b/src/singletons/WindowManager.cpp
@@ -195,6 +195,7 @@ void WindowManager::updateWordTypeMask()
flags.set(settings->animateEmotes ? MEF::BitsAnimated : MEF::BitsStatic);
// badges
+ flags.set(MEF::BadgeSharedChannel);
flags.set(settings->showBadgesGlobalAuthority ? MEF::BadgeGlobalAuthority
: MEF::None);
flags.set(settings->showBadgesPredictions ? MEF::BadgePredictions
diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp
index 7e2f37409..eb33cef3b 100644
--- a/src/widgets/helper/ChannelView.cpp
+++ b/src/widgets/helper/ChannelView.cpp
@@ -2414,6 +2414,11 @@ void ChannelView::handleMouseClick(QMouseEvent *event,
return;
}
+ if (link.value.startsWith("id:"))
+ {
+ return;
+ }
+
// Insert @username into split input
const bool commaMention =
getSettings()->mentionUsersWithComma;
diff --git a/tests/snapshots/IrcMessageHandler/clearchat.json b/tests/snapshots/IrcMessageHandler/clearchat.json
new file mode 100644
index 000000000..d40f9e704
--- /dev/null
+++ b/tests/snapshots/IrcMessageHandler/clearchat.json
@@ -0,0 +1,157 @@
+{
+ "input": "@room-id=11148817;rm-received-ts=1729627607652;tmi-sent-ts=1729627607545;historical=1 :tmi.twitch.tv CLEARCHAT #pajlada",
+ "output": [
+ {
+ "badgeInfos": {
+ },
+ "badges": [
+ ],
+ "channelName": "",
+ "count": 1,
+ "displayName": "",
+ "elements": [
+ {
+ "element": {
+ "color": "System",
+ "flags": "Timestamp",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "20:06"
+ ]
+ },
+ "flags": "Timestamp",
+ "format": "",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "time": "20:06:47",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TimestampElement"
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "Chat"
+ ]
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "has"
+ ]
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "been"
+ ]
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "cleared"
+ ]
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "by"
+ ]
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "a"
+ ]
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "moderator."
+ ]
+ }
+ ],
+ "flags": "System|DoNotTriggerNotification",
+ "id": "",
+ "localizedName": "",
+ "loginName": "",
+ "messageText": "Chat has been cleared by a moderator.",
+ "searchText": "Chat has been cleared by a moderator.",
+ "serverReceivedTime": "",
+ "timeoutUser": "",
+ "usernameColor": "#ff000000"
+ }
+ ]
+}
diff --git a/tests/snapshots/IrcMessageHandler/emoteonly-on.json b/tests/snapshots/IrcMessageHandler/emoteonly-on.json
new file mode 100644
index 000000000..d3e0c4980
--- /dev/null
+++ b/tests/snapshots/IrcMessageHandler/emoteonly-on.json
@@ -0,0 +1,157 @@
+{
+ "input": "@historical=1;rm-received-ts=1729627965650;msg-id=emote_only_on :tmi.twitch.tv NOTICE #pajlada :This room is now in emote-only mode.",
+ "output": [
+ {
+ "badgeInfos": {
+ },
+ "badges": [
+ ],
+ "channelName": "",
+ "count": 1,
+ "displayName": "",
+ "elements": [
+ {
+ "element": {
+ "color": "System",
+ "flags": "Timestamp",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "20:12"
+ ]
+ },
+ "flags": "Timestamp",
+ "format": "",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "time": "20:12:45",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TimestampElement"
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "This"
+ ]
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "room"
+ ]
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "is"
+ ]
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "now"
+ ]
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "in"
+ ]
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "emote-only"
+ ]
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "mode."
+ ]
+ }
+ ],
+ "flags": "System|DoNotTriggerNotification",
+ "id": "",
+ "localizedName": "",
+ "loginName": "",
+ "messageText": "This room is now in emote-only mode.",
+ "searchText": "This room is now in emote-only mode.",
+ "serverReceivedTime": "",
+ "timeoutUser": "",
+ "usernameColor": "#ff000000"
+ }
+ ]
+}
diff --git a/tests/snapshots/IrcMessageHandler/raid.json b/tests/snapshots/IrcMessageHandler/raid.json
new file mode 100644
index 000000000..1e4e84d42
--- /dev/null
+++ b/tests/snapshots/IrcMessageHandler/raid.json
@@ -0,0 +1,145 @@
+{
+ "input": "@badges=subscriber/24;login=nerixyz;msg-param-displayName=nerixyz;user-type=;tmi-sent-ts=1729626466361;system-msg=2\\sraiders\\sfrom\\snerixyz\\shave\\sjoined!;room-id=11148817;user-id=129546453;display-name=nerixyz;subscriber=1;historical=1;rm-received-ts=1729626466492;msg-id=raid;vip=0;id=7299b7bc-61ce-423c-85ce-8d651b56cce4;msg-param-login=nerixyz;color=#FF0000;mod=0;msg-param-viewerCount=2;flags=;msg-param-profileImageURL=https://static-cdn.jtvnw.net/jtv_user_pictures/e065218b-49df-459d-afd3-c6557870f551-profile_image-%s.png;emotes=;badge-info=subscriber/28 :tmi.twitch.tv USERNOTICE #pajlada",
+ "output": [
+ {
+ "badgeInfos": {
+ },
+ "badges": [
+ ],
+ "channelName": "",
+ "count": 1,
+ "displayName": "",
+ "elements": [
+ {
+ "element": {
+ "color": "System",
+ "flags": "Timestamp",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "19:47"
+ ]
+ },
+ "flags": "Timestamp",
+ "format": "",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "time": "19:47:46",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TimestampElement"
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "2"
+ ]
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "raiders"
+ ]
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "from"
+ ]
+ },
+ {
+ "color": "Text",
+ "fallbackColor": "System",
+ "flags": "Text|Mention",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "MentionElement",
+ "userColor": "#ffff0000",
+ "userLoginName": "nerixyz",
+ "words": [
+ "nerixyz"
+ ]
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "have"
+ ]
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "joined!"
+ ]
+ }
+ ],
+ "flags": "System|DoNotTriggerNotification|Subscription",
+ "id": "",
+ "localizedName": "",
+ "loginName": "",
+ "messageText": "2 raiders from nerixyz have joined!",
+ "searchText": "2 raiders from nerixyz have joined!",
+ "serverReceivedTime": "",
+ "timeoutUser": "",
+ "usernameColor": "#ff000000"
+ }
+ ]
+}
diff --git a/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json b/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json
index 9c6fa2f65..a6877c365 100644
--- a/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json
+++ b/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json
@@ -64,6 +64,24 @@
"trailingSpace": true,
"type": "TwitchModerationElement"
},
+ {
+ "emote": {
+ "homePage": "https://link.twitch.tv/SharedChatViewer",
+ "images": {
+ "1x": ""
+ },
+ "name": "",
+ "tooltip": "Shared Message"
+ },
+ "flags": "BadgeSharedChannel",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "tooltip": "Shared Message",
+ "trailingSpace": true,
+ "type": "BadgeElement"
+ },
{
"emote": {
"homePage": "https://www.twitch.tv/jobs?ref=chat_badge",
diff --git a/tests/snapshots/IrcMessageHandler/shared-chat-emotes.json b/tests/snapshots/IrcMessageHandler/shared-chat-emotes.json
index b7f9256d4..ba6485c21 100644
--- a/tests/snapshots/IrcMessageHandler/shared-chat-emotes.json
+++ b/tests/snapshots/IrcMessageHandler/shared-chat-emotes.json
@@ -64,6 +64,24 @@
"trailingSpace": true,
"type": "TwitchModerationElement"
},
+ {
+ "emote": {
+ "homePage": "https://link.twitch.tv/SharedChatViewer",
+ "images": {
+ "1x": ""
+ },
+ "name": "",
+ "tooltip": "Shared Message from twitchdev"
+ },
+ "flags": "BadgeSharedChannel",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "tooltip": "Shared Message from twitchdev",
+ "trailingSpace": true,
+ "type": "BadgeElement"
+ },
{
"color": "#ffff0000",
"flags": "Username",
diff --git a/tests/snapshots/IrcMessageHandler/shared-chat-known.json b/tests/snapshots/IrcMessageHandler/shared-chat-known.json
index 6a13adcb4..c4a3f3e2f 100644
--- a/tests/snapshots/IrcMessageHandler/shared-chat-known.json
+++ b/tests/snapshots/IrcMessageHandler/shared-chat-known.json
@@ -64,6 +64,24 @@
"trailingSpace": true,
"type": "TwitchModerationElement"
},
+ {
+ "emote": {
+ "homePage": "https://link.twitch.tv/SharedChatViewer",
+ "images": {
+ "1x": ""
+ },
+ "name": "",
+ "tooltip": "Shared Message from twitchdev"
+ },
+ "flags": "BadgeSharedChannel",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "tooltip": "Shared Message from twitchdev",
+ "trailingSpace": true,
+ "type": "BadgeElement"
+ },
{
"emote": {
"homePage": "https://www.twitch.tv/jobs?ref=chat_badge",
diff --git a/tests/snapshots/IrcMessageHandler/shared-chat-raid.json b/tests/snapshots/IrcMessageHandler/shared-chat-raid.json
new file mode 100644
index 000000000..d3edb3741
--- /dev/null
+++ b/tests/snapshots/IrcMessageHandler/shared-chat-raid.json
@@ -0,0 +1,5 @@
+{
+ "input": "@color=#FF0000;emotes=;subscriber=0;msg-id=sharedchatnotice;historical=1;msg-param-profileImageURL=https://static-cdn.jtvnw.net/jtv_user_pictures/e065218b-49df-459d-afd3-c6557870f551-profile_image-%s.png;tmi-sent-ts=1729627237027;rm-received-ts=1729627237138;msg-param-displayName=nerixyz;id=c585cb3e-cb4f-4a48-a251-b568d217587e;display-name=nerixyz;badges=;user-id=129546453;source-id=d86cdfb2-e138-48e2-985f-5b8efb765ba4;source-room-id=955766119;room-id=11148817;user-type=;msg-param-login=nerixyz;flags=;source-badge-info=;mod=0;vip=0;system-msg=2\\sraiders\\sfrom\\snerixyz\\shave\\sjoined!;login=nerixyz;msg-param-viewerCount=2;source-badges=;source-msg-id=raid;badge-info= :tmi.twitch.tv USERNOTICE #pajlada",
+ "output": [
+ ]
+}
diff --git a/tests/snapshots/IrcMessageHandler/shared-chat-unknown.json b/tests/snapshots/IrcMessageHandler/shared-chat-unknown.json
index accbb76b0..6ea80b2ce 100644
--- a/tests/snapshots/IrcMessageHandler/shared-chat-unknown.json
+++ b/tests/snapshots/IrcMessageHandler/shared-chat-unknown.json
@@ -64,6 +64,24 @@
"trailingSpace": true,
"type": "TwitchModerationElement"
},
+ {
+ "emote": {
+ "homePage": "https://link.twitch.tv/SharedChatViewer",
+ "images": {
+ "1x": ""
+ },
+ "name": "",
+ "tooltip": "Shared Message"
+ },
+ "flags": "BadgeSharedChannel",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "tooltip": "Shared Message",
+ "trailingSpace": true,
+ "type": "BadgeElement"
+ },
{
"emote": {
"homePage": "https://www.twitch.tv/jobs?ref=chat_badge",
diff --git a/tests/snapshots/IrcMessageHandler/timeout.json b/tests/snapshots/IrcMessageHandler/timeout.json
new file mode 100644
index 000000000..1de84089d
--- /dev/null
+++ b/tests/snapshots/IrcMessageHandler/timeout.json
@@ -0,0 +1,87 @@
+{
+ "input": "@tmi-sent-ts=1729628658012;rm-received-ts=1729628658106;historical=1;ban-duration=1;room-id=11148817;target-user-id=129546453 :tmi.twitch.tv CLEARCHAT #pajlada nerixyz",
+ "output": [
+ {
+ "badgeInfos": {
+ },
+ "badges": [
+ ],
+ "channelName": "",
+ "count": 1,
+ "displayName": "",
+ "elements": [
+ {
+ "element": {
+ "color": "System",
+ "flags": "Timestamp",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "20:24"
+ ]
+ },
+ "flags": "Timestamp",
+ "format": "",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "time": "20:24:18",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TimestampElement"
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "UserInfo",
+ "value": "nerixyz"
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "nerixyz"
+ ]
+ },
+ {
+ "color": "System",
+ "flags": "Text",
+ "link": {
+ "type": "None",
+ "value": ""
+ },
+ "style": "ChatMedium",
+ "tooltip": "",
+ "trailingSpace": true,
+ "type": "TextElement",
+ "words": [
+ "has",
+ "been",
+ "timed",
+ "out",
+ "for",
+ "1s."
+ ]
+ }
+ ],
+ "flags": "System|Timeout|DoNotTriggerNotification",
+ "id": "",
+ "localizedName": "",
+ "loginName": "",
+ "messageText": "nerixyz has been timed out for 1s. ",
+ "searchText": "nerixyz has been timed out for 1s. ",
+ "serverReceivedTime": "",
+ "timeoutUser": "nerixyz",
+ "usernameColor": "#ff000000"
+ }
+ ]
+}