Compare commits

...

10 commits

Author SHA1 Message Date
iProdigy 6d376b5416 chore: reformat emote initializer 2024-10-21 15:23:52 -07:00
iProdigy c7512dc734 chore: remove emote name for badge 2024-10-21 15:20:15 -07:00
iProdigy 0163474a93 refactor: utilize TwitchUsers::resolveID 2024-10-21 15:11:25 -07:00
iProdigy d2681a0b43 fix: capture channelId by value instead of ref 2024-10-21 13:27:30 -07:00
iProdigy e326ce428c chore: update snapshot tests 2024-10-21 13:19:26 -07:00
iProdigy 59bb2fe2e9 chore: remove userinfo link on badge 2024-10-21 12:19:59 -07:00
iProdigy d80dd6d8a2 Merge branch 'main' into feature/shared-chat-badge 2024-10-21 11:23:04 -07:00
iProdigy de40d42a60 chore: prevent mention on right click 2024-10-21 11:22:05 -07:00
pajlada 2ec8fa8723
refactor: remove unused ReplyContext.highlight (#5669) 2024-10-21 19:22:23 +02:00
pajlada 45d2c292d0
fix: subscribed threads not being marked as subscribed (#5668) 2024-10-21 13:19:08 +02:00
14 changed files with 96 additions and 158 deletions

View file

@ -110,7 +110,7 @@
- Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616) - Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616)
- Dev: Move plugins to Sol2. (#5622) - Dev: Move plugins to Sol2. (#5622)
- Dev: Refactored static `MessageBuilder` helpers to standalone functions. (#5652) - 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) - Dev: Refactored IRC message building. (#5663)
## 2.5.1 ## 2.5.1

View file

@ -71,17 +71,6 @@ 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) const value_t &get(const key_t &key)
{ {
auto it = _cache_items_map.find(key); auto it = _cache_items_map.find(key);

View file

@ -3,6 +3,7 @@
#include "common/Args.hpp" #include "common/Args.hpp"
#include "mocks/DisabledStreamerMode.hpp" #include "mocks/DisabledStreamerMode.hpp"
#include "mocks/EmptyApplication.hpp" #include "mocks/EmptyApplication.hpp"
#include "mocks/TwitchUsers.hpp"
#include "providers/bttv/BttvLiveUpdates.hpp" #include "providers/bttv/BttvLiveUpdates.hpp"
#include "singletons/Fonts.hpp" #include "singletons/Fonts.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
@ -55,6 +56,11 @@ public:
return &this->fonts; return &this->fonts;
} }
ITwitchUsers *getTwitchUsers() override
{
return &this->twitchUsers;
}
BttvLiveUpdates *getBttvLiveUpdates() override BttvLiveUpdates *getBttvLiveUpdates() override
{ {
return nullptr; return nullptr;
@ -71,6 +77,7 @@ public:
DisabledStreamerMode streamerMode; DisabledStreamerMode streamerMode;
Theme theme; Theme theme;
Fonts fonts; Fonts fonts;
TwitchUsers twitchUsers;
}; };
} // namespace chatterino::mock } // namespace chatterino::mock

View file

@ -26,7 +26,6 @@ public:
, mentionsChannel(std::shared_ptr<Channel>(new MockChannel("forsen3"))) , mentionsChannel(std::shared_ptr<Channel>(new MockChannel("forsen3")))
, liveChannel(std::shared_ptr<Channel>(new MockChannel("forsen"))) , liveChannel(std::shared_ptr<Channel>(new MockChannel("forsen")))
, automodChannel(std::shared_ptr<Channel>(new MockChannel("forsen2"))) , automodChannel(std::shared_ptr<Channel>(new MockChannel("forsen2")))
, channelNamesById_(1)
{ {
} }
@ -50,18 +49,6 @@ public:
return {}; 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 void addFakeMessage(const QString &data) override
{ {
} }
@ -161,7 +148,6 @@ public:
ChannelPtr mentionsChannel; ChannelPtr mentionsChannel;
ChannelPtr liveChannel; ChannelPtr liveChannel;
ChannelPtr automodChannel; ChannelPtr automodChannel;
UniqueAccess<cache::lru_cache<QString, QString>> channelNamesById_;
QString lastUserThatWhisperedMe{"forsen"}; QString lastUserThatWhisperedMe{"forsen"};
std::unordered_map<QString, std::weak_ptr<Channel>> mockChannels; std::unordered_map<QString, std::weak_ptr<Channel>> mockChannels;

View file

@ -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<TwitchUser> resolveID(const UserId &id)
{
TwitchUser u = {
.id = id.string,
.name = {},
.displayName = {},
};
return std::make_shared<TwitchUser>(u);
}
};
} // namespace chatterino::mock

View file

@ -32,6 +32,7 @@
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrc.hpp" #include "providers/twitch/TwitchIrc.hpp"
#include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "providers/twitch/TwitchUsers.hpp"
#include "singletons/Emotes.hpp" #include "singletons/Emotes.hpp"
#include "singletons/Resources.hpp" #include "singletons/Resources.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
@ -382,13 +383,14 @@ EmotePtr makeAutoModBadge()
EmotePtr makeSharedChatBadge(const QString &sourceName) EmotePtr makeSharedChatBadge(const QString &sourceName)
{ {
return std::make_shared<Emote>( return std::make_shared<Emote>(Emote{
Emote{"SharedChat_" + sourceName, .name = EmoteName{},
ImageSet{Image::fromResourcePixmap( .images = ImageSet{Image::fromResourcePixmap(
getResources().twitch.sharedChat, 0.25)}, getResources().twitch.sharedChat, 0.25)},
Tooltip{"Shared Message" + .tooltip = Tooltip{"Shared Message" +
(sourceName.isEmpty() ? "" : " from " + sourceName)}, (sourceName.isEmpty() ? "" : " from " + sourceName)},
Url{"https://link.twitch.tv/SharedChatViewer"}}); .homePage = Url{"https://link.twitch.tv/SharedChatViewer"},
});
} }
std::tuple<std::optional<EmotePtr>, MessageElementFlags, bool> parseEmote( std::tuple<std::optional<EmotePtr>, MessageElementFlags, bool> parseEmote(
@ -2393,6 +2395,11 @@ void MessageBuilder::parseThread(const QString &messageContent,
this->message().replyParent = parent; this->message().replyParent = parent;
thread->addToThread(std::weak_ptr{this->message_}); thread->addToThread(std::weak_ptr{this->message_});
if (thread->subscribed())
{
this->message().flags.set(MessageFlag::SubscribedThread);
}
// enable reply flag // enable reply flag
this->message().flags.set(MessageFlag::ReplyMessage); this->message().flags.set(MessageFlag::ReplyMessage);
@ -2760,28 +2767,23 @@ void MessageBuilder::appendTwitchBadges(const QVariantMap &tags,
if (this->message().flags.has(MessageFlag::SharedMessage)) if (this->message().flags.has(MessageFlag::SharedMessage))
{ {
const QString sourceId = tags["source-room-id"].toString(); const QString sourceId = tags["source-room-id"].toString();
std::optional<QString> sourceName; QString sourceName;
if (twitchChannel->roomId() == sourceId) if (sourceId.isEmpty())
{
sourceName = "";
}
else if (twitchChannel->roomId() == sourceId)
{ {
sourceName = twitchChannel->getName(); sourceName = twitchChannel->getName();
} }
else else
{ {
sourceName = sourceName =
getApp()->getTwitch()->getOrPopulateChannelCache(sourceId); getApp()->getTwitchUsers()->resolveID({sourceId})->displayName;
} }
if (sourceName.has_value()) this->emplace<BadgeElement>(makeSharedChatBadge(sourceName),
{
const auto &name = sourceName.value();
auto *badge = this->emplace<BadgeElement>(
makeSharedChatBadge(name),
MessageElementFlag::BadgeSharedChannel); MessageElementFlag::BadgeSharedChannel);
if (!name.isEmpty())
{
badge->setLink({Link::UserInfo, name});
}
}
} }
auto badgeInfos = parseBadgeInfoTag(tags); auto badgeInfos = parseBadgeInfoTag(tags);

View file

@ -123,47 +123,34 @@ int stripLeadingReplyMention(const QVariantMap &tags, QString &content)
return 0; return 0;
} }
[[nodiscard]] bool shouldHighlightReplyThread( void checkThreadSubscription(const QVariantMap &tags,
const QVariantMap &tags, const QString &senderLogin, const QString &senderLogin,
std::shared_ptr<MessageThread> &thread, bool isNew) std::shared_ptr<MessageThread> &thread)
{ {
const auto &currentLogin = if (thread->subscribed() || thread->unsubscribed())
getApp()->getAccounts()->twitch.getCurrent()->getUserName();
if (thread->subscribed())
{ {
return true; return;
}
if (thread->unsubscribed())
{
return false;
} }
if (getSettings()->autoSubToParticipatedThreads) if (getSettings()->autoSubToParticipatedThreads)
{ {
if (isNew) const auto &currentLogin =
getApp()->getAccounts()->twitch.getCurrent()->getUserName();
if (senderLogin == currentLogin)
{ {
if (const auto it = tags.find("reply-parent-user-login"); thread->markSubscribed();
}
else if (const auto it = tags.find("reply-parent-user-login");
it != tags.end()) it != tags.end())
{ {
auto name = it.value().toString(); auto name = it.value().toString();
if (name == currentLogin) if (name == currentLogin)
{ {
thread->markSubscribed(); thread->markSubscribed();
return true; // already marked as participated
} }
} }
} }
if (senderLogin == currentLogin)
{
thread->markSubscribed();
// don't set the highlight here
}
}
return false;
} }
ChannelPtr channelOrEmptyByTarget(const QString &target, ChannelPtr channelOrEmptyByTarget(const QString &target,
@ -243,7 +230,6 @@ QMap<QString, QString> parseBadges(const QString &badgesString)
struct ReplyContext { struct ReplyContext {
std::shared_ptr<MessageThread> thread; std::shared_ptr<MessageThread> thread;
MessagePtr parent; MessagePtr parent;
bool highlight = false;
}; };
[[nodiscard]] ReplyContext getReplyContext( [[nodiscard]] ReplyContext getReplyContext(
@ -265,8 +251,7 @@ struct ReplyContext {
if (owned) if (owned)
{ {
// Thread already exists (has a reply) // Thread already exists (has a reply)
ctx.highlight = shouldHighlightReplyThread( checkThreadSubscription(tags, message->nick(), owned);
tags, message->nick(), owned, false);
ctx.thread = owned; ctx.thread = owned;
rootThread = owned; rootThread = owned;
} }
@ -301,8 +286,7 @@ struct ReplyContext {
{ {
std::shared_ptr<MessageThread> newThread = std::shared_ptr<MessageThread> newThread =
std::make_shared<MessageThread>(foundMessage); std::make_shared<MessageThread>(foundMessage);
ctx.highlight = shouldHighlightReplyThread( checkThreadSubscription(tags, message->nick(), newThread);
tags, message->nick(), newThread, true);
ctx.thread = newThread; ctx.thread = newThread;
rootThread = newThread; rootThread = newThread;
@ -724,10 +708,6 @@ std::vector<MessagePtr> IrcMessageHandler::parseMessageWithReply(
if (built) if (built)
{ {
if (replyCtx.highlight)
{
built->flags.set(MessageFlag::SubscribedThread);
}
builtMessages.emplace_back(built); builtMessages.emplace_back(built);
MessageBuilder::triggerHighlights(channel, alert); MessageBuilder::triggerHighlights(channel, alert);
} }
@ -1552,8 +1532,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message,
{ {
// Thread already exists (has a reply) // Thread already exists (has a reply)
auto thread = threadIt->second.lock(); auto thread = threadIt->second.lock();
replyCtx.highlight = shouldHighlightReplyThread( checkThreadSubscription(tags, message->nick(), thread);
tags, message->nick(), thread, false);
replyCtx.thread = thread; replyCtx.thread = thread;
rootThread = thread; rootThread = thread;
} }
@ -1565,8 +1544,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message,
{ {
// Found root reply message // Found root reply message
auto newThread = std::make_shared<MessageThread>(root); auto newThread = std::make_shared<MessageThread>(root);
replyCtx.highlight = shouldHighlightReplyThread( checkThreadSubscription(tags, message->nick(), newThread);
tags, message->nick(), newThread, true);
replyCtx.thread = newThread; replyCtx.thread = newThread;
rootThread = newThread; rootThread = newThread;
@ -1621,10 +1599,6 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message,
msg->flags.set(MessageFlag::Subscription); msg->flags.set(MessageFlag::Subscription);
msg->flags.unset(MessageFlag::Highlighted); msg->flags.unset(MessageFlag::Highlighted);
} }
if (replyCtx.highlight)
{
msg->flags.set(MessageFlag::SubscribedThread);
}
IrcMessageHandler::setSimilarityFlags(msg, chan); IrcMessageHandler::setSimilarityFlags(msg, chan);

View file

@ -153,7 +153,6 @@ TwitchIrcServer::TwitchIrcServer()
, liveChannel(new Channel("/live", Channel::Type::TwitchLive)) , liveChannel(new Channel("/live", Channel::Type::TwitchLive))
, automodChannel(new Channel("/automod", Channel::Type::TwitchAutomod)) , automodChannel(new Channel("/automod", Channel::Type::TwitchAutomod))
, watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching) , watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching)
, channelNamesById_(512)
{ {
// Initialize the connections // Initialize the connections
// XXX: don't create write connection if there is no separate write connection. // XXX: don't create write connection if there is no separate write connection.
@ -1129,38 +1128,6 @@ std::shared_ptr<Channel> TwitchIrcServer::getChannelOrEmptyByID(
return Channel::getEmpty(); 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) QString TwitchIrcServer::cleanChannelName(const QString &dirtyChannelName)
{ {
if (dirtyChannelName.startsWith('#')) if (dirtyChannelName.startsWith('#'))

View file

@ -3,12 +3,10 @@
#include "common/Atomic.hpp" #include "common/Atomic.hpp"
#include "common/Channel.hpp" #include "common/Channel.hpp"
#include "common/Common.hpp" #include "common/Common.hpp"
#include "common/UniqueAccess.hpp"
#include "providers/irc/IrcConnection2.hpp" #include "providers/irc/IrcConnection2.hpp"
#include "util/RatelimitBucket.hpp" #include "util/RatelimitBucket.hpp"
#include <IrcMessage> #include <IrcMessage>
#include <lrucache/lrucache.hpp>
#include <pajlada/signals/signal.hpp> #include <pajlada/signals/signal.hpp>
#include <pajlada/signals/signalholder.hpp> #include <pajlada/signals/signalholder.hpp>
@ -45,9 +43,6 @@ public:
virtual ChannelPtr getOrAddChannel(const QString &dirtyChannelName) = 0; virtual ChannelPtr getOrAddChannel(const QString &dirtyChannelName) = 0;
virtual ChannelPtr getChannelOrEmpty(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 addFakeMessage(const QString &data) = 0;
virtual void addGlobalSystemMessage(const QString &messageText) = 0; virtual void addGlobalSystemMessage(const QString &messageText) = 0;
@ -100,14 +95,6 @@ public:
std::shared_ptr<Channel> getChannelOrEmptyByID( std::shared_ptr<Channel> getChannelOrEmptyByID(
const QString &channelID) override; 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 reloadAllBTTVChannelEmotes();
void reloadAllFFZChannelEmotes(); void reloadAllFFZChannelEmotes();
void reloadAllSevenTVChannelEmotes(); void reloadAllSevenTVChannelEmotes();
@ -203,9 +190,6 @@ private:
// https://dev.twitch.tv/docs/irc/guide#rate-limits // https://dev.twitch.tv/docs/irc/guide#rate-limits
QObjectPtr<RatelimitBucket> joinBucket_; QObjectPtr<RatelimitBucket> joinBucket_;
// cached channel id => name for resolving Shared Chat members
UniqueAccess<cache::lru_cache<QString, QString>> channelNamesById_;
QTimer reconnectTimer_; QTimer reconnectTimer_;
int falloffCounter_ = 1; int falloffCounter_ = 1;

View file

@ -2408,6 +2408,11 @@ void ChannelView::handleMouseClick(QMouseEvent *event,
return; return;
} }
if (link.value.startsWith("id:"))
{
return;
}
// Insert @username into split input // Insert @username into split input
const bool commaMention = const bool commaMention =
getSettings()->mentionUsersWithComma; getSettings()->mentionUsersWithComma;

View file

@ -70,15 +70,15 @@
"images": { "images": {
"1x": "" "1x": ""
}, },
"name": "SharedChat_shared_chat_test_01", "name": "",
"tooltip": "Shared Message from shared_chat_test_01" "tooltip": "Shared Message"
}, },
"flags": "BadgeSharedChannel", "flags": "BadgeSharedChannel",
"link": { "link": {
"type": "UserInfo", "type": "None",
"value": "shared_chat_test_01" "value": ""
}, },
"tooltip": "Shared Message from shared_chat_test_01", "tooltip": "Shared Message",
"trailingSpace": true, "trailingSpace": true,
"type": "BadgeElement" "type": "BadgeElement"
}, },

View file

@ -70,13 +70,13 @@
"images": { "images": {
"1x": "" "1x": ""
}, },
"name": "SharedChat_twitchdev", "name": "",
"tooltip": "Shared Message from twitchdev" "tooltip": "Shared Message from twitchdev"
}, },
"flags": "BadgeSharedChannel", "flags": "BadgeSharedChannel",
"link": { "link": {
"type": "UserInfo", "type": "None",
"value": "twitchdev" "value": ""
}, },
"tooltip": "Shared Message from twitchdev", "tooltip": "Shared Message from twitchdev",
"trailingSpace": true, "trailingSpace": true,

View file

@ -70,13 +70,13 @@
"images": { "images": {
"1x": "" "1x": ""
}, },
"name": "SharedChat_twitchdev", "name": "",
"tooltip": "Shared Message from twitchdev" "tooltip": "Shared Message from twitchdev"
}, },
"flags": "BadgeSharedChannel", "flags": "BadgeSharedChannel",
"link": { "link": {
"type": "UserInfo", "type": "None",
"value": "twitchdev" "value": ""
}, },
"tooltip": "Shared Message from twitchdev", "tooltip": "Shared Message from twitchdev",
"trailingSpace": true, "trailingSpace": true,

View file

@ -70,15 +70,15 @@
"images": { "images": {
"1x": "" "1x": ""
}, },
"name": "SharedChat_shared_chat_test_01", "name": "",
"tooltip": "Shared Message from shared_chat_test_01" "tooltip": "Shared Message"
}, },
"flags": "BadgeSharedChannel", "flags": "BadgeSharedChannel",
"link": { "link": {
"type": "UserInfo", "type": "None",
"value": "shared_chat_test_01" "value": ""
}, },
"tooltip": "Shared Message from shared_chat_test_01", "tooltip": "Shared Message",
"trailingSpace": true, "trailingSpace": true,
"type": "BadgeElement" "type": "BadgeElement"
}, },