mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
feat: add option to suppress live notifications on startup (#5388)
This commit is contained in:
parent
4a7a5b09ce
commit
0495fbca43
|
@ -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)
|
||||||
|
|
|
@ -587,7 +587,7 @@ QString injectStreamUpdateNoStream(const CommandContext &ctx)
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.twitchChannel->updateStreamStatus(std::nullopt);
|
ctx.twitchChannel->updateStreamStatus(std::nullopt, false);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
|
||||||
**/
|
**/
|
||||||
|
|
|
@ -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",
|
||||||
|
|
144
src/util/QCompareCaseInsensitive.hpp
Normal file
144
src/util/QCompareCaseInsensitive.hpp
Normal 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
|
|
@ -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");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue