feat: add countdown for slow mode and timeouts (#6782)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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_ =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user