feat: add option to suppress live notifications on startup (#5388)

This commit is contained in:
nerix 2024-07-20 14:19:27 +02:00 committed by GitHub
parent 4a7a5b09ce
commit 0495fbca43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 398 additions and 225 deletions

View file

@ -17,6 +17,7 @@
- Minor: Add channel points indication for new bits power-up redemptions. (#5471) - 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 option to log streams by their ID, allowing for easier "per-stream" log analyzing. (#5507)
- Minor: Added `/warn <username> <reason>` command for mods. This prevents the user from chatting until they acknowledge the warning. (#5474) - Minor: Added `/warn <username> <reason>` 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: Improve appearance of reply button. (#5491)
- Minor: Introduce HTTP API for plugins. (#5383, #5492, #5494) - Minor: Introduce HTTP API for plugins. (#5383, #5492, #5494)
- Minor: Support more Firefox variants for incognito link opening. (#5503) - Minor: Support more Firefox variants for incognito link opening. (#5503)

View file

@ -587,7 +587,7 @@ QString injectStreamUpdateNoStream(const CommandContext &ctx)
return ""; return "";
} }
ctx.twitchChannel->updateStreamStatus(std::nullopt); ctx.twitchChannel->updateStreamStatus(std::nullopt, false);
return ""; return "";
} }

View file

@ -13,23 +13,14 @@
#include "singletons/Toasts.hpp" #include "singletons/Toasts.hpp"
#include "singletons/WindowManager.hpp" #include "singletons/WindowManager.hpp"
#include "util/Helpers.hpp" #include "util/Helpers.hpp"
#include "widgets/Window.hpp"
#ifdef Q_OS_WIN
# include <wintoastlib.h>
#endif
#include <QDesktopServices>
#include <QDir>
#include <QUrl> #include <QUrl>
#include <unordered_set> namespace ranges = std::ranges;
namespace chatterino { namespace chatterino {
void NotificationController::initialize(Settings &settings, const Paths &paths) void NotificationController::initialize(Settings &settings, const Paths &paths)
{ {
this->initialized_ = true;
for (const QString &channelName : this->twitchSetting_.getValue()) for (const QString &channelName : this->twitchSetting_.getValue())
{ {
this->channelMap[Platform::Twitch].append(channelName); this->channelMap[Platform::Twitch].append(channelName);
@ -43,40 +34,33 @@ void NotificationController::initialize(Settings &settings, const Paths &paths)
this->channelMap[Platform::Twitch].raw()); this->channelMap[Platform::Twitch].raw());
}); });
liveStatusTimer_ = new QTimer();
this->fetchFakeChannels(); this->fetchFakeChannels();
QObject::connect(this->liveStatusTimer_, &QTimer::timeout, [this] { QObject::connect(&this->liveStatusTimer_, &QTimer::timeout, [this] {
this->fetchFakeChannels(); this->fetchFakeChannels();
}); });
this->liveStatusTimer_->start(60 * 1000); this->liveStatusTimer_.start(60 * 1000);
} }
void NotificationController::updateChannelNotification( void NotificationController::updateChannelNotification(
const QString &channelName, Platform p) const QString &channelName, Platform p)
{ {
if (isChannelNotified(channelName, p)) if (this->isChannelNotified(channelName, p))
{ {
removeChannelNotification(channelName, p); this->removeChannelNotification(channelName, p);
} }
else else
{ {
addChannelNotification(channelName, p); this->addChannelNotification(channelName, p);
} }
} }
bool NotificationController::isChannelNotified(const QString &channelName, bool NotificationController::isChannelNotified(const QString &channelName,
Platform p) Platform p) const
{ {
for (const auto &channel : this->channelMap[p]) return ranges::any_of(channelMap.at(p).raw(), [&](const auto &name) {
{ return name.compare(channelName, Qt::CaseInsensitive) == 0;
if (channelName.toLower() == channel.toLower()) });
{
return true;
}
}
return false;
} }
void NotificationController::addChannelNotification(const QString &channelName, 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(); for (std::vector<int>::size_type i = 0; i != channelMap[p].raw().size();
i++) 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--; i--;
} }
} }
} }
void NotificationController::playSound()
void NotificationController::playSound() const
{ {
QUrl highlightSoundUrl = QUrl highlightSoundUrl =
getSettings()->notificationCustomSound getSettings()->notificationCustomSound
@ -112,23 +98,93 @@ void NotificationController::playSound()
NotificationModel *NotificationController::createModel(QObject *parent, NotificationModel *NotificationController::createModel(QObject *parent,
Platform p) Platform p)
{ {
NotificationModel *model = new NotificationModel(parent); auto *model = new NotificationModel(parent);
model->initialize(&this->channelMap[p]); model->initialize(&this->channelMap[p]);
return model; 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() void NotificationController::fetchFakeChannels()
{ {
qCDebug(chatterinoNotification) << "fetching fake channels"; qCDebug(chatterinoNotification) << "fetching fake channels";
QStringList channels; QStringList channels;
for (std::vector<int>::size_type i = 0; for (size_t i = 0; i < channelMap[Platform::Twitch].raw().size(); i++)
i < channelMap[Platform::Twitch].raw().size(); i++)
{ {
auto chan = getIApp()->getTwitchAbstract()->getChannelOrEmpty( const auto &name = channelMap[Platform::Twitch].raw()[i];
channelMap[Platform::Twitch].raw()[i]); auto chan = getIApp()->getTwitchAbstract()->getChannelOrEmpty(name);
if (chan->isEmpty()) 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( getHelix()->fetchStreams(
{}, batch, {}, batch,
[batch, this](std::vector<HelixStream> streams) { [batch, this](const auto &streams) {
std::unordered_set<QString> liveStreams; std::map<QString, std::optional<HelixStream>,
QCompareCaseInsensitive>
liveStreams;
for (const auto &stream : streams) for (const auto &stream : streams)
{ {
liveStreams.insert(stream.userLogin); liveStreams.emplace(stream.userLogin, stream);
} }
for (const auto &name : batch) for (const auto &name : batch)
{ {
auto it = liveStreams.find(name.toLower()); auto it = liveStreams.find(name);
this->checkStream(it != liveStreams.end(), name); if (it == liveStreams.end())
{
this->updateFakeChannel(name, std::nullopt);
}
else
{
this->updateFakeChannel(name, it->second);
}
} }
}, },
[batch]() { [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) bool live = stream.has_value();
<< "[TwitchChannel" << channelName << "] Refreshing live status"; 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) if (!live)
{ {
// Stream is offline // Stream is offline
this->removeFakeChannel(channelName); this->notifyTwitchChannelOffline(channelIt->second.id);
return; return;
} }
// Stream is online this->notifyTwitchChannelLive({
auto i = std::find(fakeTwitchChannels.begin(), fakeTwitchChannels.end(), .channelId = stream->userId,
channelName); .channelName = channelName,
.displayName = stream->userName,
if (i != fakeTwitchChannels.end()) .title = stream->title,
{ .isInitialUpdate = isInitialUpdate,
// 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;
}
}
}
} }
} // namespace chatterino } // namespace chatterino

View file

@ -3,6 +3,7 @@
#include "common/ChatterinoSetting.hpp" #include "common/ChatterinoSetting.hpp"
#include "common/SignalVector.hpp" #include "common/SignalVector.hpp"
#include "common/Singleton.hpp" #include "common/Singleton.hpp"
#include "util/QCompareCaseInsensitive.hpp"
#include <QTimer> #include <QTimer>
@ -10,6 +11,7 @@ namespace chatterino {
class Settings; class Settings;
class Paths; class Paths;
struct HelixStream;
class NotificationModel; class NotificationModel;
@ -17,34 +19,58 @@ enum class Platform : uint8_t {
Twitch, // 0 Twitch, // 0
}; };
class NotificationController final : public Singleton, private QObject class NotificationController final : public Singleton
{ {
public: public:
void initialize(Settings &settings, const Paths &paths) override; 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 updateChannelNotification(const QString &channelName, Platform p);
void addChannelNotification(const QString &channelName, Platform p); void addChannelNotification(const QString &channelName, Platform p);
void removeChannelNotification(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<QString> 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<Platform, SignalVector<QString>> 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); NotificationModel *createModel(QObject *parent, Platform p);
private: private:
bool initialized_ = false;
void fetchFakeChannels(); void fetchFakeChannels();
void removeFakeChannel(const QString channelName); void removeFakeChannel(const QString &channelName);
void checkStream(bool live, QString channelName); void updateFakeChannel(const QString &channelName,
const std::optional<HelixStream> &stream);
// fakeTwitchChannels is a list of streams who are live that we have already sent out a notification for struct FakeChannel {
std::vector<QString> fakeTwitchChannels; QString id;
QTimer *liveStatusTimer_; 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<QString, FakeChannel, QCompareCaseInsensitive> fakeChannels_;
QTimer liveStatusTimer_;
std::map<Platform, SignalVector<QString>> channelMap;
ChatterinoSetting<std::vector<QString>> twitchSetting_ = { ChatterinoSetting<std::vector<QString>> twitchSetting_ = {
"/notifications/twitch"}; "/notifications/twitch"};

View file

@ -56,7 +56,7 @@ void TwitchLiveController::add(const std::shared_ptr<TwitchChannel> &newChannel)
{ {
std::unique_lock lock(this->channelsMutex); 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<QStringList> optChannelIDs)
auto it = this->channels.find(result.first); auto it = this->channels.find(result.first);
if (it != channels.end()) 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 else
{ {
@ -159,7 +161,7 @@ void TwitchLiveController::request(std::optional<QStringList> optChannelIDs)
auto it = this->channels.find(helixChannel.userId); auto it = this->channels.find(helixChannel.userId);
if (it != this->channels.end()) 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->updateStreamTitle(helixChannel.title);
channel->updateDisplayName(helixChannel.name); channel->updateDisplayName(helixChannel.name);

View file

@ -49,6 +49,11 @@ public:
void add(const std::shared_ptr<TwitchChannel> &newChannel) override; void add(const std::shared_ptr<TwitchChannel> &newChannel) override;
private: private:
struct ChannelEntry {
std::weak_ptr<TwitchChannel> ptr;
bool wasChecked = false;
};
/** /**
* Run batched Helix Channels & Stream requests for channels * 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 * These channels will have their stream status updated every REFRESH_INTERVAL seconds
**/ **/
std::unordered_map<QString, std::weak_ptr<TwitchChannel>> channels; std::unordered_map<QString, ChannelEntry> channels;
std::shared_mutex channelsMutex; std::shared_mutex channelsMutex;
/** /**

View file

@ -133,86 +133,6 @@ TwitchChannel::TwitchChannel(const QString &name)
}); });
this->threadClearTimer_.start(5 * 60 * 1000); 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<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);
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 // debugging
#if 0 #if 0
for (int i = 0; i < 1000; i++) { for (int i = 0; i < 1000; i++) {
@ -450,7 +370,7 @@ std::optional<ChannelPointReward> TwitchChannel::channelPointReward(
} }
void TwitchChannel::updateStreamStatus( void TwitchChannel::updateStreamStatus(
const std::optional<HelixStream> &helixStream) const std::optional<HelixStream> &helixStream, bool isInitialUpdate)
{ {
if (helixStream) if (helixStream)
{ {
@ -483,7 +403,7 @@ void TwitchChannel::updateStreamStatus(
} }
if (this->setLive(true)) if (this->setLive(true))
{ {
this->liveStatusChanged.invoke(true); this->onLiveStatusChanged(true, isInitialUpdate);
} }
this->streamStatusChanged.invoke(); this->streamStatusChanged.invoke();
} }
@ -491,12 +411,52 @@ void TwitchChannel::updateStreamStatus(
{ {
if (this->setLive(false)) if (this->setLive(false))
{ {
this->liveStatusChanged.invoke(false); this->onLiveStatusChanged(false, isInitialUpdate);
this->streamStatusChanged.invoke(); 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) void TwitchChannel::updateStreamTitle(const QString &title)
{ {
{ {

View file

@ -238,14 +238,6 @@ public:
// Only TwitchChannel may invoke this signal // Only TwitchChannel may invoke this signal
pajlada::Signals::NoArgSignal userStateChanged; 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<bool> liveStatusChanged;
/** /**
* This signal fires whenever the stream status is changed * This signal fires whenever the stream status is changed
* *
@ -270,7 +262,8 @@ public:
const QString &rewardId) const; const QString &rewardId) const;
// Live status // Live status
void updateStreamStatus(const std::optional<HelixStream> &helixStream); void updateStreamStatus(const std::optional<HelixStream> &helixStream,
bool isInitialUpdate);
void updateStreamTitle(const QString &title); void updateStreamTitle(const QString &title);
/** /**
@ -338,6 +331,8 @@ private:
void setDisplayName(const QString &name); void setDisplayName(const QString &name);
void setLocalizedName(const QString &name); void setLocalizedName(const QString &name);
void onLiveStatusChanged(bool isLive, bool isInitialUpdate);
/** /**
* Returns the localized name of the user * Returns the localized name of the user
**/ **/

View file

@ -475,6 +475,8 @@ public:
"qrc:/sounds/ping3.wav"}; "qrc:/sounds/ping3.wav"};
BoolSetting notificationOnAnyChannel = {"/notifications/onAnyChannel", BoolSetting notificationOnAnyChannel = {"/notifications/onAnyChannel",
false}; false};
BoolSetting suppressInitialLiveNotification = {
"/notifications/suppressInitialLive", false};
BoolSetting notificationToast = {"/notifications/enableToast", false}; BoolSetting notificationToast = {"/notifications/enableToast", false};
IntSetting openFromToast = {"/notifications/openFromToast", IntSetting openFromToast = {"/notifications/openFromToast",

View file

@ -0,0 +1,144 @@
#pragma once
#include <QLatin1String>
#include <QString>
#include <QStringView>
#include <QtGlobal>
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
# include <QUtf8StringView>
#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

View file

@ -42,6 +42,10 @@ NotificationPage::NotificationPage()
settings.append(this->createCheckBox( settings.append(this->createCheckBox(
"Play sound for any channel going live", "Play sound for any channel going live",
getSettings()->notificationOnAnyChannel)); getSettings()->notificationOnAnyChannel));
settings.append(this->createCheckBox(
"Suppress live notifications on startup",
getSettings()->suppressInitialLiveNotification));
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
settings.append(this->createCheckBox( settings.append(this->createCheckBox(
"Show notification", getSettings()->notificationToast)); "Show notification", getSettings()->notificationToast));
@ -111,10 +115,8 @@ NotificationPage::NotificationPage()
// We can safely ignore this signal connection since we own the view // We can safely ignore this signal connection since we own the view
std::ignore = view->addButtonPressed.connect([] { std::ignore = view->addButtonPressed.connect([] {
getApp() getApp()->getNotifications()->addChannelNotification(
->getNotifications() "channel", Platform::Twitch);
->channelMap[Platform::Twitch]
.append("channel");
}); });
} }
} }