From 0495fbca43187947ca0bf4a8ed9b0015056644b5 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 20 Jul 2024 14:19:27 +0200 Subject: [PATCH] feat: add option to suppress live notifications on startup (#5388) --- CHANGELOG.md | 1 + src/controllers/commands/builtin/Misc.cpp | 2 +- .../notifications/NotificationController.cpp | 258 ++++++++++-------- .../notifications/NotificationController.hpp | 50 +++- src/controllers/twitch/LiveController.cpp | 10 +- src/controllers/twitch/LiveController.hpp | 7 +- src/providers/twitch/TwitchChannel.cpp | 126 +++------ src/providers/twitch/TwitchChannel.hpp | 13 +- src/singletons/Settings.hpp | 2 + src/util/QCompareCaseInsensitive.hpp | 144 ++++++++++ .../settingspages/NotificationPage.cpp | 10 +- 11 files changed, 398 insertions(+), 225 deletions(-) create mode 100644 src/util/QCompareCaseInsensitive.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index f8b104bcb..6c67b1856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Minor: Add channel points indication for new bits power-up redemptions. (#5471) - Minor: Added option to log streams by their ID, allowing for easier "per-stream" log analyzing. (#5507) - Minor: Added `/warn ` command for mods. This prevents the user from chatting until they acknowledge the warning. (#5474) +- Minor: Added option to suppress live notifictions on startup. (#5388) - Minor: Improve appearance of reply button. (#5491) - Minor: Introduce HTTP API for plugins. (#5383, #5492, #5494) - Minor: Support more Firefox variants for incognito link opening. (#5503) diff --git a/src/controllers/commands/builtin/Misc.cpp b/src/controllers/commands/builtin/Misc.cpp index a7fcc7662..9be6438ce 100644 --- a/src/controllers/commands/builtin/Misc.cpp +++ b/src/controllers/commands/builtin/Misc.cpp @@ -587,7 +587,7 @@ QString injectStreamUpdateNoStream(const CommandContext &ctx) return ""; } - ctx.twitchChannel->updateStreamStatus(std::nullopt); + ctx.twitchChannel->updateStreamStatus(std::nullopt, false); return ""; } diff --git a/src/controllers/notifications/NotificationController.cpp b/src/controllers/notifications/NotificationController.cpp index 1e30a7497..1fe0036a7 100644 --- a/src/controllers/notifications/NotificationController.cpp +++ b/src/controllers/notifications/NotificationController.cpp @@ -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 -#endif - -#include -#include #include -#include - +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::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(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 snapshot = + getIApp()->getTwitch()->getLiveChannel()->getMessageSnapshot(); + int snapshotLength = static_cast(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::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 streams) { - std::unordered_set liveStreams; + [batch, this](const auto &streams) { + std::map, + 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 &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 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 diff --git a/src/controllers/notifications/NotificationController.hpp b/src/controllers/notifications/NotificationController.hpp index ad70e029f..fa802c1ec 100644 --- a/src/controllers/notifications/NotificationController.hpp +++ b/src/controllers/notifications/NotificationController.hpp @@ -3,6 +3,7 @@ #include "common/ChatterinoSetting.hpp" #include "common/SignalVector.hpp" #include "common/Singleton.hpp" +#include "util/QCompareCaseInsensitive.hpp" #include @@ -10,6 +11,7 @@ namespace chatterino { class Settings; class Paths; +struct HelixStream; class NotificationModel; @@ -17,34 +19,58 @@ enum class Platform : uint8_t { Twitch, // 0 }; -class NotificationController final : public Singleton, private QObject +class NotificationController final : public Singleton { public: void initialize(Settings &settings, const Paths &paths) override; - bool isChannelNotified(const QString &channelName, Platform p); + bool isChannelNotified(const QString &channelName, Platform p) const; void updateChannelNotification(const QString &channelName, Platform p); void addChannelNotification(const QString &channelName, Platform p); void removeChannelNotification(const QString &channelName, Platform p); - void playSound(); + struct NotificationPayload { + QString channelId; + QString channelName; + QString displayName; + QString title; + bool isInitialUpdate = false; + }; - SignalVector getVector(Platform p); + /// @brief Sends out notifications for a channel that has gone live + /// + /// This doesn't check for duplicate notifications. + void notifyTwitchChannelLive(const NotificationPayload &payload) const; - std::map> channelMap; + /// @brief Sends out notifications for a channel that has gone offline + /// + /// This doesn't check for duplicate notifications. + void notifyTwitchChannelOffline(const QString &id) const; + + void playSound() const; NotificationModel *createModel(QObject *parent, Platform p); private: - bool initialized_ = false; - void fetchFakeChannels(); - void removeFakeChannel(const QString channelName); - void checkStream(bool live, QString channelName); + void removeFakeChannel(const QString &channelName); + void updateFakeChannel(const QString &channelName, + const std::optional &stream); - // fakeTwitchChannels is a list of streams who are live that we have already sent out a notification for - std::vector fakeTwitchChannels; - QTimer *liveStatusTimer_; + struct FakeChannel { + QString id; + bool isLive = false; + }; + + /// @brief This map tracks channels without an associated TwitchChannel + /// + /// These channels won't be tracked in LiveController. + /// Channels are identified by their login name (case insensitive). + std::map fakeChannels_; + + QTimer liveStatusTimer_; + + std::map> channelMap; ChatterinoSetting> twitchSetting_ = { "/notifications/twitch"}; diff --git a/src/controllers/twitch/LiveController.cpp b/src/controllers/twitch/LiveController.cpp index c531ebe6f..d574bf867 100644 --- a/src/controllers/twitch/LiveController.cpp +++ b/src/controllers/twitch/LiveController.cpp @@ -56,7 +56,7 @@ void TwitchLiveController::add(const std::shared_ptr &newChannel) { std::unique_lock lock(this->channelsMutex); - this->channels[channelID] = newChannel; + this->channels[channelID] = {.ptr = newChannel, .wasChecked = false}; } { @@ -120,9 +120,11 @@ void TwitchLiveController::request(std::optional optChannelIDs) auto it = this->channels.find(result.first); if (it != channels.end()) { - if (auto channel = it->second.lock(); channel) + if (auto channel = it->second.ptr.lock(); channel) { - channel->updateStreamStatus(result.second); + channel->updateStreamStatus( + result.second, !it->second.wasChecked); + it->second.wasChecked = true; } else { @@ -159,7 +161,7 @@ void TwitchLiveController::request(std::optional optChannelIDs) auto it = this->channels.find(helixChannel.userId); if (it != this->channels.end()) { - if (auto channel = it->second.lock(); channel) + if (auto channel = it->second.ptr.lock(); channel) { channel->updateStreamTitle(helixChannel.title); channel->updateDisplayName(helixChannel.name); diff --git a/src/controllers/twitch/LiveController.hpp b/src/controllers/twitch/LiveController.hpp index 79befd62d..94b538957 100644 --- a/src/controllers/twitch/LiveController.hpp +++ b/src/controllers/twitch/LiveController.hpp @@ -49,6 +49,11 @@ public: void add(const std::shared_ptr &newChannel) override; private: + struct ChannelEntry { + std::weak_ptr ptr; + bool wasChecked = false; + }; + /** * Run batched Helix Channels & Stream requests for channels * @@ -64,7 +69,7 @@ private: * * These channels will have their stream status updated every REFRESH_INTERVAL seconds **/ - std::unordered_map> channels; + std::unordered_map channels; std::shared_mutex channelsMutex; /** diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index cd293630d..4f553f039 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -133,86 +133,6 @@ TwitchChannel::TwitchChannel(const QString &name) }); this->threadClearTimer_.start(5 * 60 * 1000); - auto onLiveStatusChanged = [this](auto isLive) { - if (isLive) - { - qCDebug(chatterinoTwitch) - << "[TwitchChannel" << this->getName() << "] Online"; - if (getIApp()->getNotifications()->isChannelNotified( - this->getName(), Platform::Twitch)) - { - if (Toasts::isEnabled()) - { - getIApp()->getToasts()->sendChannelNotification( - this->getName(), this->accessStreamStatus()->title, - Platform::Twitch); - } - if (getSettings()->notificationPlaySound) - { - getIApp()->getNotifications()->playSound(); - } - if (getSettings()->notificationFlashTaskbar) - { - getIApp()->getWindows()->sendAlert(); - } - } - // Channel live message - MessageBuilder builder; - TwitchMessageBuilder::liveSystemMessage(this->getDisplayName(), - &builder); - builder.message().id = this->roomId(); - this->addMessage(builder.release(), MessageContext::Original); - - // Message in /live channel - MessageBuilder builder2; - TwitchMessageBuilder::liveMessage(this->getDisplayName(), - &builder2); - builder2.message().id = this->roomId(); - getIApp()->getTwitch()->getLiveChannel()->addMessage( - builder2.release(), MessageContext::Original); - - // Notify on all channels with a ping sound - if (getSettings()->notificationOnAnyChannel && - !(getIApp()->getStreamerMode()->isEnabled() && - getSettings()->streamerModeSuppressLiveNotifications)) - { - getIApp()->getNotifications()->playSound(); - } - } - else - { - qCDebug(chatterinoTwitch) - << "[TwitchChannel" << this->getName() << "] Offline"; - // Channel offline message - MessageBuilder builder; - TwitchMessageBuilder::offlineSystemMessage(this->getDisplayName(), - &builder); - this->addMessage(builder.release(), MessageContext::Original); - - // "delete" old 'CHANNEL is live' message - LimitedQueueSnapshot 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); - - for (int i = snapshotLength - 1; i >= end; --i) - { - const auto &s = snapshot[i]; - - if (s->id == this->roomId()) - { - s->flags.set(MessageFlag::Disabled); - break; - } - } - } - }; - - this->signalHolder_.managedConnect(this->liveStatusChanged, - onLiveStatusChanged); - // debugging #if 0 for (int i = 0; i < 1000; i++) { @@ -450,7 +370,7 @@ std::optional TwitchChannel::channelPointReward( } void TwitchChannel::updateStreamStatus( - const std::optional &helixStream) + const std::optional &helixStream, bool isInitialUpdate) { if (helixStream) { @@ -483,7 +403,7 @@ void TwitchChannel::updateStreamStatus( } if (this->setLive(true)) { - this->liveStatusChanged.invoke(true); + this->onLiveStatusChanged(true, isInitialUpdate); } this->streamStatusChanged.invoke(); } @@ -491,12 +411,52 @@ void TwitchChannel::updateStreamStatus( { if (this->setLive(false)) { - this->liveStatusChanged.invoke(false); + this->onLiveStatusChanged(false, isInitialUpdate); this->streamStatusChanged.invoke(); } } } +void TwitchChannel::onLiveStatusChanged(bool isLive, bool isInitialUpdate) +{ + // Similar code exists in NotificationController::updateFakeChannel. + // Since we're a TwitchChannel, we also send a message here. + if (isLive) + { + qCDebug(chatterinoTwitch).nospace().noquote() + << "[TwitchChannel " << this->getName() << "] Online"; + + getIApp()->getNotifications()->notifyTwitchChannelLive({ + .channelId = this->roomId(), + .channelName = this->getName(), + .displayName = this->getDisplayName(), + .title = this->accessStreamStatus()->title, + .isInitialUpdate = isInitialUpdate, + }); + + // Channel live message + MessageBuilder builder; + TwitchMessageBuilder::liveSystemMessage(this->getDisplayName(), + &builder); + builder.message().id = this->roomId(); + this->addMessage(builder.release(), MessageContext::Original); + } + else + { + qCDebug(chatterinoTwitch).nospace().noquote() + << "[TwitchChannel " << this->getName() << "] Offline"; + + // Channel offline message + MessageBuilder builder; + TwitchMessageBuilder::offlineSystemMessage(this->getDisplayName(), + &builder); + this->addMessage(builder.release(), MessageContext::Original); + + getIApp()->getNotifications()->notifyTwitchChannelOffline( + this->roomId()); + } +}; + void TwitchChannel::updateStreamTitle(const QString &title) { { diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index e3d1e96d8..2617d95a6 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -238,14 +238,6 @@ public: // Only TwitchChannel may invoke this signal pajlada::Signals::NoArgSignal userStateChanged; - /** - * This signals fires whenever the live status is changed - * - * Streams are counted as offline by default, so if a stream does not go online - * this signal will never fire - **/ - pajlada::Signals::Signal liveStatusChanged; - /** * This signal fires whenever the stream status is changed * @@ -270,7 +262,8 @@ public: const QString &rewardId) const; // Live status - void updateStreamStatus(const std::optional &helixStream); + void updateStreamStatus(const std::optional &helixStream, + bool isInitialUpdate); void updateStreamTitle(const QString &title); /** @@ -338,6 +331,8 @@ private: void setDisplayName(const QString &name); void setLocalizedName(const QString &name); + void onLiveStatusChanged(bool isLive, bool isInitialUpdate); + /** * Returns the localized name of the user **/ diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 98ec53ed8..29f9cce01 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -475,6 +475,8 @@ public: "qrc:/sounds/ping3.wav"}; BoolSetting notificationOnAnyChannel = {"/notifications/onAnyChannel", false}; + BoolSetting suppressInitialLiveNotification = { + "/notifications/suppressInitialLive", false}; BoolSetting notificationToast = {"/notifications/enableToast", false}; IntSetting openFromToast = {"/notifications/openFromToast", diff --git a/src/util/QCompareCaseInsensitive.hpp b/src/util/QCompareCaseInsensitive.hpp new file mode 100644 index 000000000..70e8a1a01 --- /dev/null +++ b/src/util/QCompareCaseInsensitive.hpp @@ -0,0 +1,144 @@ +#pragma once + +#include +#include +#include +#include + +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) +# include +#endif + +namespace chatterino { + +/// Case insensitive transparent comparator for Qt's string types +struct QCompareCaseInsensitive { + using is_transparent = void; + + // clang-format off + bool operator()(const QString & a, const QString & b) const noexcept; + bool operator()(QStringView a, QStringView b) const noexcept; + bool operator()(QLatin1String a, QLatin1String b) const noexcept; + + bool operator()(const QString & a, QStringView b) const noexcept; + bool operator()(const QString & a, QLatin1String b) const noexcept; + + bool operator()(QStringView a, const QString & b) const noexcept; + bool operator()(QLatin1String a, const QString & b) const noexcept; + + bool operator()(QStringView a, QLatin1String b) const noexcept; + bool operator()(QLatin1String a, QStringView b) const noexcept; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + bool operator()(QUtf8StringView a, QUtf8StringView b) const noexcept; + + bool operator()(const QString & a, QUtf8StringView b) const noexcept; + bool operator()(QStringView a, QUtf8StringView b) const noexcept; + bool operator()(QLatin1String a, QUtf8StringView b) const noexcept; + + bool operator()(QUtf8StringView a, const QString & b) const noexcept; + bool operator()(QUtf8StringView a, QStringView b) const noexcept; + bool operator()(QUtf8StringView a, QLatin1String b) const noexcept; +#endif + // clang-format on +}; + +inline bool QCompareCaseInsensitive::operator()(const QString &a, + const QString &b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QStringView a, + QStringView b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QLatin1String a, + QLatin1String b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(const QString &a, + QStringView b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(const QString &a, + QLatin1String b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QStringView a, + const QString &b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QLatin1String a, + const QString &b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QStringView a, + QLatin1String b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QLatin1String a, + QStringView b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) +inline bool QCompareCaseInsensitive::operator()( + QUtf8StringView a, QUtf8StringView b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()( + const QString &a, QUtf8StringView b) const noexcept +{ + return QStringView{a}.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()( + QStringView a, QUtf8StringView b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()( + QLatin1String a, QUtf8StringView b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QUtf8StringView a, + const QString &b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QUtf8StringView a, + QStringView b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QUtf8StringView a, + QLatin1String b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} +#endif + +} // namespace chatterino diff --git a/src/widgets/settingspages/NotificationPage.cpp b/src/widgets/settingspages/NotificationPage.cpp index d5bb2f7b8..c306e7176 100644 --- a/src/widgets/settingspages/NotificationPage.cpp +++ b/src/widgets/settingspages/NotificationPage.cpp @@ -42,6 +42,10 @@ NotificationPage::NotificationPage() settings.append(this->createCheckBox( "Play sound for any channel going live", getSettings()->notificationOnAnyChannel)); + + settings.append(this->createCheckBox( + "Suppress live notifications on startup", + getSettings()->suppressInitialLiveNotification)); #ifdef Q_OS_WIN settings.append(this->createCheckBox( "Show notification", getSettings()->notificationToast)); @@ -111,10 +115,8 @@ NotificationPage::NotificationPage() // We can safely ignore this signal connection since we own the view std::ignore = view->addButtonPressed.connect([] { - getApp() - ->getNotifications() - ->channelMap[Platform::Twitch] - .append("channel"); + getApp()->getNotifications()->addChannelNotification( + "channel", Platform::Twitch); }); } }