feat: add option to suppress live notifications on startup (#5388)
This commit is contained in:
@@ -13,23 +13,14 @@
|
||||
#include "singletons/Toasts.hpp"
|
||||
#include "singletons/WindowManager.hpp"
|
||||
#include "util/Helpers.hpp"
|
||||
#include "widgets/Window.hpp"
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
# include <wintoastlib.h>
|
||||
#endif
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QUrl>
|
||||
|
||||
#include <unordered_set>
|
||||
|
||||
namespace ranges = std::ranges;
|
||||
namespace chatterino {
|
||||
|
||||
void NotificationController::initialize(Settings &settings, const Paths &paths)
|
||||
{
|
||||
this->initialized_ = true;
|
||||
for (const QString &channelName : this->twitchSetting_.getValue())
|
||||
{
|
||||
this->channelMap[Platform::Twitch].append(channelName);
|
||||
@@ -43,40 +34,33 @@ void NotificationController::initialize(Settings &settings, const Paths &paths)
|
||||
this->channelMap[Platform::Twitch].raw());
|
||||
});
|
||||
|
||||
liveStatusTimer_ = new QTimer();
|
||||
|
||||
this->fetchFakeChannels();
|
||||
|
||||
QObject::connect(this->liveStatusTimer_, &QTimer::timeout, [this] {
|
||||
QObject::connect(&this->liveStatusTimer_, &QTimer::timeout, [this] {
|
||||
this->fetchFakeChannels();
|
||||
});
|
||||
this->liveStatusTimer_->start(60 * 1000);
|
||||
this->liveStatusTimer_.start(60 * 1000);
|
||||
}
|
||||
|
||||
void NotificationController::updateChannelNotification(
|
||||
const QString &channelName, Platform p)
|
||||
{
|
||||
if (isChannelNotified(channelName, p))
|
||||
if (this->isChannelNotified(channelName, p))
|
||||
{
|
||||
removeChannelNotification(channelName, p);
|
||||
this->removeChannelNotification(channelName, p);
|
||||
}
|
||||
else
|
||||
{
|
||||
addChannelNotification(channelName, p);
|
||||
this->addChannelNotification(channelName, p);
|
||||
}
|
||||
}
|
||||
|
||||
bool NotificationController::isChannelNotified(const QString &channelName,
|
||||
Platform p)
|
||||
Platform p) const
|
||||
{
|
||||
for (const auto &channel : this->channelMap[p])
|
||||
{
|
||||
if (channelName.toLower() == channel.toLower())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return ranges::any_of(channelMap.at(p).raw(), [&](const auto &name) {
|
||||
return name.compare(channelName, Qt::CaseInsensitive) == 0;
|
||||
});
|
||||
}
|
||||
|
||||
void NotificationController::addChannelNotification(const QString &channelName,
|
||||
@@ -91,14 +75,16 @@ void NotificationController::removeChannelNotification(
|
||||
for (std::vector<int>::size_type i = 0; i != channelMap[p].raw().size();
|
||||
i++)
|
||||
{
|
||||
if (channelMap[p].raw()[i].toLower() == channelName.toLower())
|
||||
if (channelMap[p].raw()[i].compare(channelName, Qt::CaseInsensitive) ==
|
||||
0)
|
||||
{
|
||||
channelMap[p].removeAt(i);
|
||||
channelMap[p].removeAt(static_cast<int>(i));
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
void NotificationController::playSound()
|
||||
|
||||
void NotificationController::playSound() const
|
||||
{
|
||||
QUrl highlightSoundUrl =
|
||||
getSettings()->notificationCustomSound
|
||||
@@ -112,23 +98,93 @@ void NotificationController::playSound()
|
||||
NotificationModel *NotificationController::createModel(QObject *parent,
|
||||
Platform p)
|
||||
{
|
||||
NotificationModel *model = new NotificationModel(parent);
|
||||
auto *model = new NotificationModel(parent);
|
||||
model->initialize(&this->channelMap[p]);
|
||||
return model;
|
||||
}
|
||||
|
||||
void NotificationController::notifyTwitchChannelLive(
|
||||
const NotificationPayload &payload) const
|
||||
{
|
||||
bool showNotification =
|
||||
!(getSettings()->suppressInitialLiveNotification &&
|
||||
payload.isInitialUpdate) &&
|
||||
!(getIApp()->getStreamerMode()->isEnabled() &&
|
||||
getSettings()->streamerModeSuppressLiveNotifications);
|
||||
bool playedSound = false;
|
||||
|
||||
if (showNotification &&
|
||||
this->isChannelNotified(payload.channelName, Platform::Twitch))
|
||||
{
|
||||
if (Toasts::isEnabled())
|
||||
{
|
||||
getIApp()->getToasts()->sendChannelNotification(
|
||||
payload.channelName, payload.title, Platform::Twitch);
|
||||
}
|
||||
if (getSettings()->notificationPlaySound)
|
||||
{
|
||||
this->playSound();
|
||||
playedSound = true;
|
||||
}
|
||||
if (getSettings()->notificationFlashTaskbar)
|
||||
{
|
||||
getIApp()->getWindows()->sendAlert();
|
||||
}
|
||||
}
|
||||
|
||||
// Message in /live channel
|
||||
MessageBuilder builder;
|
||||
TwitchMessageBuilder::liveMessage(payload.displayName, &builder);
|
||||
builder.message().id = payload.channelId;
|
||||
getIApp()->getTwitch()->getLiveChannel()->addMessage(
|
||||
builder.release(), MessageContext::Original);
|
||||
|
||||
// Notify on all channels with a ping sound
|
||||
if (showNotification && !playedSound &&
|
||||
getSettings()->notificationOnAnyChannel)
|
||||
{
|
||||
this->playSound();
|
||||
}
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
|
||||
void NotificationController::notifyTwitchChannelOffline(const QString &id) const
|
||||
{
|
||||
// "delete" old 'CHANNEL is live' message
|
||||
LimitedQueueSnapshot<MessagePtr> snapshot =
|
||||
getIApp()->getTwitch()->getLiveChannel()->getMessageSnapshot();
|
||||
int snapshotLength = static_cast<int>(snapshot.size());
|
||||
|
||||
int end = std::max(0, snapshotLength - 200);
|
||||
|
||||
for (int i = snapshotLength - 1; i >= end; --i)
|
||||
{
|
||||
const auto &s = snapshot[i];
|
||||
|
||||
if (s->id == id)
|
||||
{
|
||||
s->flags.set(MessageFlag::Disabled);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationController::fetchFakeChannels()
|
||||
{
|
||||
qCDebug(chatterinoNotification) << "fetching fake channels";
|
||||
|
||||
QStringList channels;
|
||||
for (std::vector<int>::size_type i = 0;
|
||||
i < channelMap[Platform::Twitch].raw().size(); i++)
|
||||
for (size_t i = 0; i < channelMap[Platform::Twitch].raw().size(); i++)
|
||||
{
|
||||
auto chan = getIApp()->getTwitchAbstract()->getChannelOrEmpty(
|
||||
channelMap[Platform::Twitch].raw()[i]);
|
||||
const auto &name = channelMap[Platform::Twitch].raw()[i];
|
||||
auto chan = getIApp()->getTwitchAbstract()->getChannelOrEmpty(name);
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
channels.push_back(channelMap[Platform::Twitch].raw()[i]);
|
||||
channels.push_back(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->fakeChannels_.erase(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,17 +192,26 @@ void NotificationController::fetchFakeChannels()
|
||||
{
|
||||
getHelix()->fetchStreams(
|
||||
{}, batch,
|
||||
[batch, this](std::vector<HelixStream> streams) {
|
||||
std::unordered_set<QString> liveStreams;
|
||||
[batch, this](const auto &streams) {
|
||||
std::map<QString, std::optional<HelixStream>,
|
||||
QCompareCaseInsensitive>
|
||||
liveStreams;
|
||||
for (const auto &stream : streams)
|
||||
{
|
||||
liveStreams.insert(stream.userLogin);
|
||||
liveStreams.emplace(stream.userLogin, stream);
|
||||
}
|
||||
|
||||
for (const auto &name : batch)
|
||||
{
|
||||
auto it = liveStreams.find(name.toLower());
|
||||
this->checkStream(it != liveStreams.end(), name);
|
||||
auto it = liveStreams.find(name);
|
||||
if (it == liveStreams.end())
|
||||
{
|
||||
this->updateFakeChannel(name, std::nullopt);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->updateFakeChannel(name, it->second);
|
||||
}
|
||||
}
|
||||
},
|
||||
[batch]() {
|
||||
@@ -159,85 +224,56 @@ void NotificationController::fetchFakeChannels()
|
||||
});
|
||||
}
|
||||
}
|
||||
void NotificationController::checkStream(bool live, QString channelName)
|
||||
void NotificationController::updateFakeChannel(
|
||||
const QString &channelName, const std::optional<HelixStream> &stream)
|
||||
{
|
||||
qCDebug(chatterinoNotification)
|
||||
<< "[TwitchChannel" << channelName << "] Refreshing live status";
|
||||
bool live = stream.has_value();
|
||||
qCDebug(chatterinoNotification).nospace().noquote()
|
||||
<< "[FakeTwitchChannel " << channelName
|
||||
<< "] New live status: " << stream.has_value();
|
||||
|
||||
auto channelIt = this->fakeChannels_.find(channelName);
|
||||
bool isInitialUpdate = false;
|
||||
if (channelIt == this->fakeChannels_.end())
|
||||
{
|
||||
channelIt = this->fakeChannels_
|
||||
.emplace(channelName,
|
||||
FakeChannel{
|
||||
.id = {},
|
||||
.isLive = live,
|
||||
})
|
||||
.first;
|
||||
isInitialUpdate = true;
|
||||
}
|
||||
if (channelIt->second.isLive == live && !isInitialUpdate)
|
||||
{
|
||||
return; // nothing changed
|
||||
}
|
||||
|
||||
if (live && channelIt->second.id.isNull())
|
||||
{
|
||||
channelIt->second.id = stream->userId;
|
||||
}
|
||||
|
||||
channelIt->second.isLive = live;
|
||||
|
||||
// Similar code can be found in TwitchChannel::onLiveStatusChange.
|
||||
// Since this is a fake channel, we don't send a live message in the
|
||||
// TwitchChannel.
|
||||
if (!live)
|
||||
{
|
||||
// Stream is offline
|
||||
this->removeFakeChannel(channelName);
|
||||
this->notifyTwitchChannelOffline(channelIt->second.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stream is online
|
||||
auto i = std::find(fakeTwitchChannels.begin(), fakeTwitchChannels.end(),
|
||||
channelName);
|
||||
|
||||
if (i != fakeTwitchChannels.end())
|
||||
{
|
||||
// We have already pushed the live state of this stream
|
||||
// Could not find stream in fake Twitch channels!
|
||||
return;
|
||||
}
|
||||
|
||||
if (Toasts::isEnabled())
|
||||
{
|
||||
getIApp()->getToasts()->sendChannelNotification(channelName, QString(),
|
||||
Platform::Twitch);
|
||||
}
|
||||
bool inStreamerMode = getIApp()->getStreamerMode()->isEnabled();
|
||||
if (getSettings()->notificationPlaySound &&
|
||||
!(inStreamerMode &&
|
||||
getSettings()->streamerModeSuppressLiveNotifications))
|
||||
{
|
||||
getIApp()->getNotifications()->playSound();
|
||||
}
|
||||
if (getSettings()->notificationFlashTaskbar &&
|
||||
!(inStreamerMode &&
|
||||
getSettings()->streamerModeSuppressLiveNotifications))
|
||||
{
|
||||
getIApp()->getWindows()->sendAlert();
|
||||
}
|
||||
MessageBuilder builder;
|
||||
TwitchMessageBuilder::liveMessage(channelName, &builder);
|
||||
getIApp()->getTwitch()->getLiveChannel()->addMessage(
|
||||
builder.release(), MessageContext::Original);
|
||||
|
||||
// Indicate that we have pushed notifications for this stream
|
||||
fakeTwitchChannels.push_back(channelName);
|
||||
}
|
||||
|
||||
void NotificationController::removeFakeChannel(const QString channelName)
|
||||
{
|
||||
auto it = std::find(fakeTwitchChannels.begin(), fakeTwitchChannels.end(),
|
||||
channelName);
|
||||
if (it != fakeTwitchChannels.end())
|
||||
{
|
||||
fakeTwitchChannels.erase(it);
|
||||
// "delete" old 'CHANNEL is live' message
|
||||
LimitedQueueSnapshot<MessagePtr> snapshot =
|
||||
getIApp()->getTwitch()->getLiveChannel()->getMessageSnapshot();
|
||||
int snapshotLength = snapshot.size();
|
||||
|
||||
// MSVC hates this code if the parens are not there
|
||||
int end = (std::max)(0, snapshotLength - 200);
|
||||
// this assumes that channelName is a login name therefore will only delete messages from fake channels
|
||||
auto liveMessageSearchText = QString("%1 is live!").arg(channelName);
|
||||
|
||||
for (int i = snapshotLength - 1; i >= end; --i)
|
||||
{
|
||||
const auto &s = snapshot[i];
|
||||
|
||||
if (QString::compare(s->messageText, liveMessageSearchText,
|
||||
Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
s->flags.set(MessageFlag::Disabled);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->notifyTwitchChannelLive({
|
||||
.channelId = stream->userId,
|
||||
.channelName = channelName,
|
||||
.displayName = stream->userName,
|
||||
.title = stream->title,
|
||||
.isInitialUpdate = isInitialUpdate,
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
Reference in New Issue
Block a user