Compare commits

...

13 commits

Author SHA1 Message Date
iProdigy
617d47b7e7
Merge a9b446ffc7 into 867e3f3ab0 2024-10-21 05:11:21 +00:00
iProdigy
a9b446ffc7 Merge branch 'main' into feature/shared-chat-badge 2024-10-20 22:10:42 -07:00
iProdigy
021f1a0bfc fix: skip UserInfo link if name is empty 2024-10-20 21:48:05 -07:00
iProdigy
e87302ca2d docs: explain getOrPopulateChannelCache 2024-10-20 21:40:32 -07:00
iProdigy
64ccef94d1 Merge branch 'main' into feature/shared-chat-badge
# Conflicts:
#	src/messages/MessageBuilder.cpp
#	src/messages/MessageBuilder.hpp
2024-10-20 13:50:22 -07:00
iProdigy
6836105408 chore: crush png file 2024-10-19 19:14:40 -07:00
iProdigy
ee80dd5d27
Merge branch 'master' into feature/shared-chat-badge 2024-10-20 01:21:49 +00:00
iProdigy
5638346d7c chore: update shared chat icon 2024-10-19 17:27:09 -07:00
iProdigy
c64523f23c chore: fix snapshot tests 2024-10-19 16:12:25 -07:00
iProdigy
cde4144e30
Merge branch 'master' into feature/shared-chat-badge 2024-10-19 11:11:12 +00:00
iProdigy
c4faca50e0 chore: update mocks 2024-10-19 04:06:20 -07:00
iProdigy
7d0c473828
chore: update changelog 2024-10-19 03:48:56 -07:00
iProdigy
f4036b269b feat: add shared chat badge 2024-10-19 03:40:06 -07:00
13 changed files with 191 additions and 2 deletions

View file

@ -7,7 +7,7 @@
- Major: Improve high-DPI support on Windows. (#4868, #5391, #5664, #5666)
- Major: Added transparent overlay window (default keybind: <kbd>CTRL</kbd> + <kbd>ALT</kbd> + <kbd>N</kbd>). (#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)

View file

@ -71,6 +71,17 @@ public:
}
}
void remove(const key_t &key)
{
auto it = _cache_items_map.find(key);
if (it == _cache_items_map.end())
{
throw std::range_error("There is no such key in cache");
}
_cache_items_list.erase(it->second);
_cache_items_map.erase(it);
}
const value_t &get(const key_t &key)
{
auto it = _cache_items_map.find(key);

View file

@ -26,6 +26,7 @@ public:
, mentionsChannel(std::shared_ptr<Channel>(new MockChannel("forsen3")))
, liveChannel(std::shared_ptr<Channel>(new MockChannel("forsen")))
, automodChannel(std::shared_ptr<Channel>(new MockChannel("forsen2")))
, channelNamesById_(1)
{
}
@ -49,6 +50,18 @@ public:
return {};
}
std::optional<QString> getOrPopulateChannelCache(
const QString &channelId) override
{
if (channelId == "11148817")
return "pajlada";
if (channelId == "141981764")
return "twitchdev";
if (channelId == "1025594235")
return "shared_chat_test_01";
return {};
}
void addFakeMessage(const QString &data) override
{
}
@ -148,6 +161,7 @@ public:
ChannelPtr mentionsChannel;
ChannelPtr liveChannel;
ChannelPtr automodChannel;
UniqueAccess<cache::lru_cache<QString, QString>> channelNamesById_;
QString lastUserThatWhisperedMe{"forsen"};
std::unordered_map<QString, std::weak_ptr<Channel>> mockChannels;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -380,6 +380,17 @@ EmotePtr makeAutoModBadge()
Url{"https://dashboard.twitch.tv/settings/moderation/automod"}});
}
EmotePtr makeSharedChatBadge(const QString &sourceName)
{
return std::make_shared<Emote>(
Emote{"SharedChat_" + sourceName,
ImageSet{Image::fromResourcePixmap(
getResources().twitch.sharedChat, 0.25)},
Tooltip{"Shared Message" +
(sourceName.isEmpty() ? "" : " from " + sourceName)},
Url{"https://link.twitch.tv/SharedChatViewer"}});
}
std::tuple<std::optional<EmotePtr>, MessageElementFlags, bool> parseEmote(
TwitchChannel *twitchChannel, const EmoteName &name)
{
@ -2746,6 +2757,33 @@ void MessageBuilder::appendTwitchBadges(const QVariantMap &tags,
return;
}
if (this->message().flags.has(MessageFlag::SharedMessage))
{
const QString sourceId = tags["source-room-id"].toString();
std::optional<QString> sourceName;
if (twitchChannel->roomId() == sourceId)
{
sourceName = twitchChannel->getName();
}
else
{
sourceName =
getApp()->getTwitch()->getOrPopulateChannelCache(sourceId);
}
if (sourceName.has_value())
{
const auto &name = sourceName.value();
auto *badge = this->emplace<BadgeElement>(
makeSharedChatBadge(name),
MessageElementFlag::BadgeSharedChannel);
if (!name.isEmpty())
{
badge->setLink({Link::UserInfo, name});
}
}
}
auto badgeInfos = parseBadgeInfoTag(tags);
auto badges = parseBadgeTag(tags);
appendBadges(this, badges, badgeInfos, twitchChannel);

View file

@ -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),

View file

@ -153,6 +153,7 @@ TwitchIrcServer::TwitchIrcServer()
, liveChannel(new Channel("/live", Channel::Type::TwitchLive))
, automodChannel(new Channel("/automod", Channel::Type::TwitchAutomod))
, watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching)
, channelNamesById_(512)
{
// Initialize the connections
// XXX: don't create write connection if there is no separate write connection.
@ -1128,6 +1129,38 @@ std::shared_ptr<Channel> TwitchIrcServer::getChannelOrEmptyByID(
return Channel::getEmpty();
}
std::optional<QString> TwitchIrcServer::getOrPopulateChannelCache(
const QString &channelId)
{
{
const auto cache = this->channelNamesById_.access();
if (cache->exists(channelId))
{
return cache->get(channelId);
}
// prevent multiple helix requests for single user
cache->put(channelId, "");
}
getHelix()->getUserById(
channelId,
[this](const HelixUser &user) {
const auto cache = this->channelNamesById_.access();
cache->put(user.id, user.login);
},
[this, &channelId] {
const auto cache = this->channelNamesById_.access();
if (cache->exists(channelId) && cache->get(channelId).isEmpty())
{
// invalidate cache so another helix request can be attempted
cache->remove(channelId);
}
});
return {};
}
QString TwitchIrcServer::cleanChannelName(const QString &dirtyChannelName)
{
if (dirtyChannelName.startsWith('#'))

View file

@ -3,10 +3,12 @@
#include "common/Atomic.hpp"
#include "common/Channel.hpp"
#include "common/Common.hpp"
#include "common/UniqueAccess.hpp"
#include "providers/irc/IrcConnection2.hpp"
#include "util/RatelimitBucket.hpp"
#include <IrcMessage>
#include <lrucache/lrucache.hpp>
#include <pajlada/signals/signal.hpp>
#include <pajlada/signals/signalholder.hpp>
@ -43,6 +45,9 @@ public:
virtual ChannelPtr getOrAddChannel(const QString &dirtyChannelName) = 0;
virtual ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName) = 0;
virtual std::optional<QString> getOrPopulateChannelCache(
const QString &channelId) = 0;
virtual void addFakeMessage(const QString &data) = 0;
virtual void addGlobalSystemMessage(const QString &messageText) = 0;
@ -95,6 +100,14 @@ public:
std::shared_ptr<Channel> getChannelOrEmptyByID(
const QString &channelID) override;
/**
* Obtains the channel login name associated with the passed ID,
* so that Shared Chat messages can provide source channel context.
* Can yield an empty string if a helix request is already in-flight.
*/
std::optional<QString> getOrPopulateChannelCache(
const QString &channelId) override;
void reloadAllBTTVChannelEmotes();
void reloadAllFFZChannelEmotes();
void reloadAllSevenTVChannelEmotes();
@ -190,6 +203,9 @@ private:
// https://dev.twitch.tv/docs/irc/guide#rate-limits
QObjectPtr<RatelimitBucket> joinBucket_;
// cached channel id => name for resolving Shared Chat members
UniqueAccess<cache::lru_cache<QString, QString>> channelNamesById_;
QTimer reconnectTimer_;
int falloffCounter_ = 1;

View file

@ -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

View file

@ -64,6 +64,24 @@
"trailingSpace": true,
"type": "TwitchModerationElement"
},
{
"emote": {
"homePage": "https://link.twitch.tv/SharedChatViewer",
"images": {
"1x": ""
},
"name": "SharedChat_shared_chat_test_01",
"tooltip": "Shared Message from shared_chat_test_01"
},
"flags": "BadgeSharedChannel",
"link": {
"type": "UserInfo",
"value": "shared_chat_test_01"
},
"tooltip": "Shared Message from shared_chat_test_01",
"trailingSpace": true,
"type": "BadgeElement"
},
{
"emote": {
"homePage": "https://www.twitch.tv/jobs?ref=chat_badge",

View file

@ -64,6 +64,24 @@
"trailingSpace": true,
"type": "TwitchModerationElement"
},
{
"emote": {
"homePage": "https://link.twitch.tv/SharedChatViewer",
"images": {
"1x": ""
},
"name": "SharedChat_twitchdev",
"tooltip": "Shared Message from twitchdev"
},
"flags": "BadgeSharedChannel",
"link": {
"type": "UserInfo",
"value": "twitchdev"
},
"tooltip": "Shared Message from twitchdev",
"trailingSpace": true,
"type": "BadgeElement"
},
{
"color": "#ffff0000",
"flags": "Username",

View file

@ -64,6 +64,24 @@
"trailingSpace": true,
"type": "TwitchModerationElement"
},
{
"emote": {
"homePage": "https://link.twitch.tv/SharedChatViewer",
"images": {
"1x": ""
},
"name": "SharedChat_twitchdev",
"tooltip": "Shared Message from twitchdev"
},
"flags": "BadgeSharedChannel",
"link": {
"type": "UserInfo",
"value": "twitchdev"
},
"tooltip": "Shared Message from twitchdev",
"trailingSpace": true,
"type": "BadgeElement"
},
{
"emote": {
"homePage": "https://www.twitch.tv/jobs?ref=chat_badge",

View file

@ -64,6 +64,24 @@
"trailingSpace": true,
"type": "TwitchModerationElement"
},
{
"emote": {
"homePage": "https://link.twitch.tv/SharedChatViewer",
"images": {
"1x": ""
},
"name": "SharedChat_shared_chat_test_01",
"tooltip": "Shared Message from shared_chat_test_01"
},
"flags": "BadgeSharedChannel",
"link": {
"type": "UserInfo",
"value": "shared_chat_test_01"
},
"tooltip": "Shared Message from shared_chat_test_01",
"trailingSpace": true,
"type": "BadgeElement"
},
{
"emote": {
"homePage": "https://www.twitch.tv/jobs?ref=chat_badge",