feat: add countdown for slow mode and timeouts (#6782)

This commit is contained in:
Ava Chow
2026-02-01 19:54:24 +08:00
committed by GitHub
parent 301417488f
commit f61cd1068c
12 changed files with 228 additions and 18 deletions
+1
View File
@@ -2,6 +2,7 @@
## Unversioned
- Minor: Added a countdown timer to the input box to show when you can chat again during slow mode or after a timeout. This feature is disabled by default, and can be enabled with the "Show countdown on slow mode or when timed out" setting. (#6782)
- Minor: Add search engine selection for context menu search action. (#6743, #6770)
- Minor: Add a separate highlight option for watchstreak notifications. (#6571, #6581)
- Minor: Badges now link to their home page like emotes in the context menu. (#6437)
+72 -1
View File
@@ -222,7 +222,9 @@ std::optional<ClearChatMessage> parseClearChatMessage(
calculateMessageTime(message))
.release();
return ClearChatMessage{.message = timeoutMsg, .disableAllMessages = false};
return ClearChatMessage{.message = timeoutMsg,
.disableAllMessages = false,
.username = username};
}
/**
@@ -403,6 +405,22 @@ void IrcMessageHandler::parsePrivMessageInto(
channel->setVIP(parsedBadges.contains("vip"));
channel->setStaff(parsedBadges.contains("staff"));
}
if (!channel->isLoadingRecentMessages())
{
// Clear the send wait timer when we are able to send a message
channel->setSendWait(0);
// Update send wait timer with slow mode timeout if this user is not a mod or vip.
if (!channel->hasHighRateLimit())
{
auto roomModes = *channel->accessRoomModes();
if (roomModes.slowMode > 0)
{
channel->setSendWait(roomModes.slowMode);
}
}
}
}
IrcMessageHandler::addMessage(
@@ -510,6 +528,25 @@ void IrcMessageHandler::handleClearChatMessage(Communi::IrcMessage *message)
}
else
{
// Set send wait timer when the user is timed out
const auto currentUsername =
getApp()->getAccounts()->twitch.getCurrent()->getUserName();
if (currentUsername == clearChat.username)
{
bool ok = false;
int remainingTime =
message->tags().value("ban-duration").toInt(&ok);
if (ok)
{
auto *tc = dynamic_cast<TwitchChannel *>(chan.get());
assert(tc != nullptr);
if (tc != nullptr)
{
tc->setSendWait(remainingTime);
}
}
}
chan->addOrReplaceTimeout(std::move(clearChat.message), time);
}
@@ -617,6 +654,13 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
tc->setMod(modTag == "1");
}
}
// When a user is updated to mod or vip status, any previous
// slow mode or timeout timers are no longer in effect.
if (tc->hasHighRateLimit())
{
tc->setSendWait(0);
}
}
}
@@ -979,6 +1023,33 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message)
{
channel->addMessage(msg, MessageContext::Original);
}
auto handleSendWait = [&channel](const QString &remaining) {
bool ok = false;
int seconds = remaining.toInt(&ok);
if (ok)
{
auto *tc = dynamic_cast<TwitchChannel *>(channel.get());
assert(tc != nullptr);
if (tc != nullptr)
{
tc->setSendWait(seconds);
}
}
};
if (tags == "msg_slowmode")
{
// Notice received when the user sends a message too quickly during slow mode.
// @msg-id=msg_slowmode :tmi.twitch.tv NOTICE #channel :This room is in slow mode and you are sending messages too quickly. You will be able to talk again in 10 seconds.
handleSendWait(message->content().split(u' ').value(21));
}
else if (tags == "msg_timedout")
{
// Notice received when the user sends a message while timed out.
// @msg-id=msg_timedout :tmi.twitch.tv NOTICE #twitch :You are timed out for 3600 more seconds.
handleSendWait(message->content().split(u' ').value(5));
}
}
void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message)
@@ -23,6 +23,7 @@ class MessageSink;
struct ClearChatMessage {
MessagePtr message;
bool disableAllMessages;
std::optional<QString> username;
};
class IrcMessageHandler
+57
View File
@@ -46,6 +46,7 @@
#include "singletons/StreamerMode.hpp"
#include "singletons/Toasts.hpp"
#include "singletons/WindowManager.hpp"
#include "util/FormatTime.hpp"
#include "util/Helpers.hpp"
#include "util/PostToThread.hpp"
#include "util/QStringHash.hpp"
@@ -65,6 +66,7 @@
namespace chatterino {
using namespace literals;
using namespace std::chrono_literals;
namespace detail {
@@ -212,6 +214,11 @@ TwitchChannel::TwitchChannel(const QString &name)
}
});
QObject::connect(&this->sendWaitTimer_, &QTimer::timeout,
&this->lifetimeGuard_, [this] {
this->syncSendWaitTimer();
});
// debugging
#if 0
for (int i = 0; i < 1000; i++) {
@@ -997,6 +1004,12 @@ void TwitchChannel::setRoomModes(const RoomModes &newRoomModes)
{
this->roomModes = newRoomModes;
// Clear send wait timer when slow mode is disabled
if (newRoomModes.slowMode == 0)
{
this->setSendWait(newRoomModes.slowMode);
}
this->roomModesChanged.invoke();
}
@@ -2342,4 +2355,48 @@ void TwitchChannel::listenSevenTVCosmetics() const
}
}
void TwitchChannel::syncSendWaitTimer()
{
auto now = std::chrono::steady_clock::now();
const auto remaining =
this->sendWaitEnd_.has_value()
? std::chrono::duration_cast<std::chrono::seconds>(
this->sendWaitEnd_.value() - now)
: 0s;
if (remaining <= 0s)
{
this->sendWaitTimer_.stop();
this->sendWaitUpdate.invoke("");
}
else
{
this->sendWaitUpdate.invoke(formatTime(remaining, 2));
}
}
void TwitchChannel::setSendWait(int seconds)
{
if (seconds <= 0)
{
if (this->sendWaitEnd_.has_value())
{
this->sendWaitEnd_ = std::nullopt;
this->syncSendWaitTimer();
}
return;
}
this->sendWaitEnd_ =
std::chrono::steady_clock::now() + std::chrono::seconds(seconds);
if (!this->sendWaitTimer_.isActive())
{
this->sendWaitTimer_.start(1s);
this->syncSendWaitTimer();
}
}
bool TwitchChannel::isLoadingRecentMessages() const
{
return this->loadingRecentMessages_.test();
}
} // namespace chatterino
+21
View File
@@ -338,6 +338,8 @@ public:
pajlada::Signals::NoArgSignal destroyed;
pajlada::Signals::Signal<const QString &> sendWaitUpdate;
// Channel point rewards
void addQueuedRedemption(const QString &rewardId,
const QString &originalContent,
@@ -368,6 +370,21 @@ public:
const QString &getDisplayName() const override;
void updateDisplayName(const QString &displayName);
/**
* Sync the text of the send wait timer to the actual time remaining.
*/
void syncSendWaitTimer();
/**
* Set the send wait timer to given number of seconds
*
* When a channel is in slow mode or the user is timed out, calling
* this function sets the timer to show how long the user will need
* to wait before they can send again.
*/
void setSendWait(int seconds);
bool isLoadingRecentMessages() const;
private:
struct NameOptions {
// displayName is the non-CJK-display name for this user
@@ -572,6 +589,10 @@ private:
friend class Commands_E2E_Test;
friend class ::TestIrcMessageHandlerP;
friend class ::TestEventSubMessagesP;
QTimer sendWaitTimer_;
// Timepoint at which the user can send messages again
std::optional<std::chrono::steady_clock::time_point> sendWaitEnd_;
};
} // namespace chatterino
+2
View File
@@ -170,6 +170,8 @@ public:
BoolSetting showEmptyInput = {"/appearance/showEmptyInputBox", true};
BoolSetting showMessageLength = {"/appearance/messages/showMessageLength",
false};
BoolSetting showSendWaitTimer = {"/appearance/messages/showSendWaitTimer",
false};
EnumSetting<MessageOverflow> messageOverflow = {
"/appearance/messages/messageOverflow", MessageOverflow::Highlight};
BoolSetting separateMessages = {"/appearance/messages/separateMessages",
+19 -13
View File
@@ -125,7 +125,7 @@ BalancedDuration durationBetween(const QDateTime &a, const QDateTime &b)
} // namespace
QString formatTime(int totalSeconds)
QString formatTime(int totalSeconds, int components)
{
QString res;
@@ -135,46 +135,52 @@ QString formatTime(int totalSeconds)
int timeoutHours = timeoutMinutes / 60;
int hours = timeoutHours % 24;
int days = timeoutHours / 24;
if (days > 0)
if (days > 0 && components > 0)
{
appendShortDuration(days, 'd', res);
components--;
}
if (hours > 0)
if (hours > 0 && components > 0)
{
appendShortDuration(hours, 'h', res);
components--;
}
if (minutes > 0)
if (minutes > 0 && components > 0)
{
appendShortDuration(minutes, 'm', res);
components--;
}
if (seconds > 0)
if (seconds > 0 && components > 0)
{
appendShortDuration(seconds, 's', res);
components--;
}
return res;
}
QString formatTime(const QString &totalSecondsString)
QString formatTime(const QString &totalSecondsString, int components)
{
bool ok = true;
int totalSeconds(totalSecondsString.toInt(&ok));
if (ok)
{
return formatTime(totalSeconds);
return formatTime(totalSeconds, components);
}
return "n/a";
}
QString formatTime(std::chrono::seconds totalSeconds)
QString formatTime(std::chrono::seconds totalSeconds, int components)
{
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()))));
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()))),
components);
}
QString formatLongFriendlyDuration(const QDateTime &from, const QDateTime &to)
+4 -3
View File
@@ -12,9 +12,10 @@
namespace chatterino {
// format: 1h 23m 42s
QString formatTime(int totalSeconds);
QString formatTime(const QString &totalSecondsString);
QString formatTime(std::chrono::seconds totalSeconds);
// 'components' controls how many most significant components the formatted time will have
QString formatTime(int totalSeconds, int components = 4);
QString formatTime(const QString &totalSecondsString, int components = 4);
QString formatTime(std::chrono::seconds totalSeconds, int components = 4);
/// Formats a duration that's expected to be long (sevreal months or years) like
/// "4 years, 5 days and 8 hours".
@@ -381,6 +381,14 @@ void GeneralPage::initLayout(GeneralPageView &layout)
"limit, or a lower limit enforced by a moderation bot")
->addTo(layout);
SettingWidget::checkbox("Show countdown on slow mode or when timed out",
s.showSendWaitTimer)
->setTooltip("Show how long you may need to wait before being able to "
"send in a Twitch channel again if the channel is in slow "
"mode or if you have been timed out")
->addKeywords({"slowmode", "timeout"})
->addTo(layout);
SettingWidget::checkbox("Allow sending duplicate messages",
s.allowDuplicateMessages)
->setTooltip(
+5
View File
@@ -847,6 +847,11 @@ void Split::setChannel(IndirectChannel newChannel)
this->roomModeChangedConnection_ = tc->roomModesChanged.connect([this] {
this->header_->updateRoomModes();
});
this->channelSignalHolder_.managedConnect(
tc->sendWaitUpdate, [this](const QString &text) {
this->getInput().setSendWaitStatus(text);
});
}
this->indirectChannelChangedConnection_ =
+30 -1
View File
@@ -211,10 +211,16 @@ void SplitInput::initLayout()
auto box = hboxLayout.emplace<QVBoxLayout>().withoutMargin();
box->setSpacing(0);
{
auto hbox = box.emplace<QHBoxLayout>().withoutMargin();
this->ui_.textEditLength = new QLabel();
// Right-align the labels contents
this->ui_.textEditLength->setAlignment(Qt::AlignRight);
box->addWidget(this->ui_.textEditLength);
hbox->addWidget(this->ui_.textEditLength);
this->ui_.sendWaitStatus = new QLabel();
this->ui_.sendWaitStatus->setAlignment(Qt::AlignRight);
this->ui_.sendWaitStatus->setHidden(true);
hbox->addWidget(this->ui_.sendWaitStatus);
this->ui_.emoteButton = new SvgButton(
{
@@ -266,6 +272,16 @@ void SplitInput::initLayout()
this->editTextChanged();
},
this->managedConnections_);
// sendWaitStatus visibility
getSettings()->showSendWaitTimer.connect(
[this](bool value, const auto &) {
if (!this->ui_.sendWaitStatus->text().isEmpty())
{
this->ui_.sendWaitStatus->setHidden(!value);
}
},
this->managedConnections_);
}
void SplitInput::triggerSelfMessageReceived()
@@ -1427,4 +1443,17 @@ void SplitInput::updateFonts()
app->getFonts()->getFont(FontStyle::ChatMediumBold, this->scale()));
}
void SplitInput::setSendWaitStatus(const QString &text) const
{
this->ui_.sendWaitStatus->setText(text);
if (text.isEmpty())
{
this->ui_.sendWaitStatus->setHidden(true);
}
else
{
this->ui_.sendWaitStatus->setHidden(!getSettings()->showSendWaitTimer);
}
}
} // namespace chatterino
+8
View File
@@ -82,6 +82,13 @@ public:
*/
void setInputText(const QString &newInputText);
/**
* @brief Sets a formatted time to sendWaitStatus
*
* This method is used to update the text of the timeout and slow mode timer
*/
void setSendWaitStatus(const QString &text) const;
void triggerSelfMessageReceived();
std::optional<bool> checkSpellingOverride() const;
@@ -161,6 +168,7 @@ protected:
ResizingTextEdit *textEdit;
QLabel *textEditLength;
LabelButton *sendButton;
QLabel *sendWaitStatus;
SvgButton *emoteButton;
} ui_;