diff --git a/CHANGELOG.md b/CHANGELOG.md index f8d8cf426..fbe09aee9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Minor: Improved editing hotkeys. (#4628) - Minor: The input completion and quick switcher are now styled to match your theme. (#4671) - Minor: Added setting to only show tabs with live channels (default toggle hotkey: Ctrl+Shift+L). (#4358) +- Minor: Added option to subscribe to and unsubscribe from reply threads. (#4680) - Bugfix: Fixed generation of crashdumps by the browser-extension process when the browser was closed. (#4667) - Bugfix: Fix spacing issue with mentions inside RTL text. (#4677) - Bugfix: Fixed a crash when opening and closing a reply thread and switching the user. (#4675) diff --git a/src/controllers/highlights/HighlightController.cpp b/src/controllers/highlights/HighlightController.cpp index bd517863f..ae85fdea3 100644 --- a/src/controllers/highlights/HighlightController.cpp +++ b/src/controllers/highlights/HighlightController.cpp @@ -163,7 +163,7 @@ void rebuildReplyThreadHighlight(Settings &settings, const auto & /*senderName*/, const auto & /*originalMessage*/, const auto &flags, const auto self) -> boost::optional { - if (flags.has(MessageFlag::ParticipatedThread) && !self) + if (flags.has(MessageFlag::SubscribedThread) && !self) { return HighlightResult{ highlightAlert, diff --git a/src/controllers/highlights/HighlightModel.cpp b/src/controllers/highlights/HighlightModel.cpp index 13ff5ec6b..b49f6fbb4 100644 --- a/src/controllers/highlights/HighlightModel.cpp +++ b/src/controllers/highlights/HighlightModel.cpp @@ -210,7 +210,7 @@ void HighlightModel::afterInit() std::vector threadMessageRow = this->createRow(); setBoolItem(threadMessageRow[Column::Pattern], getSettings()->enableThreadHighlight.getValue(), true, false); - threadMessageRow[Column::Pattern]->setData("Participated Reply Threads", + threadMessageRow[Column::Pattern]->setData("Subscribed Reply Threads", Qt::DisplayRole); setBoolItem(threadMessageRow[Column::ShowInMentions], getSettings()->showThreadHighlightInMentions.getValue(), true, diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index bea40a1b1..83e311b1c 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -46,7 +46,7 @@ enum class MessageFlag : int64_t { FirstMessage = (1LL << 23), ReplyMessage = (1LL << 24), ElevatedMessage = (1LL << 25), - ParticipatedThread = (1LL << 26), + SubscribedThread = (1LL << 26), CheerMessage = (1LL << 27), LiveUpdatesAdd = (1LL << 28), LiveUpdatesRemove = (1LL << 29), diff --git a/src/messages/MessageThread.cpp b/src/messages/MessageThread.cpp index 0ffdc3f18..e1227ab09 100644 --- a/src/messages/MessageThread.cpp +++ b/src/messages/MessageThread.cpp @@ -1,4 +1,4 @@ -#include "MessageThread.hpp" +#include "messages/MessageThread.hpp" #include "messages/Message.hpp" #include "util/DebugCount.hpp" @@ -58,14 +58,26 @@ size_t MessageThread::liveCount( return count; } -bool MessageThread::participated() const +void MessageThread::markSubscribed() { - return this->participated_; + if (this->subscription_ == Subscription::Subscribed) + { + return; + } + + this->subscription_ = Subscription::Subscribed; + this->subscriptionUpdated(); } -void MessageThread::markParticipated() +void MessageThread::markUnsubscribed() { - this->participated_ = true; + if (this->subscription_ == Subscription::Unsubscribed) + { + return; + } + + this->subscription_ = Subscription::Unsubscribed; + this->subscriptionUpdated(); } } // namespace chatterino diff --git a/src/messages/MessageThread.hpp b/src/messages/MessageThread.hpp index ae0d24794..442db46a6 100644 --- a/src/messages/MessageThread.hpp +++ b/src/messages/MessageThread.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -11,6 +12,12 @@ struct Message; class MessageThread { public: + enum class Subscription : uint8_t { + None, + Subscribed, + Unsubscribed, + }; + MessageThread(std::shared_ptr rootMessage); ~MessageThread(); @@ -23,9 +30,22 @@ public: /// Returns the number of live reply references size_t liveCount(const std::shared_ptr &exclude) const; - bool participated() const; + bool subscribed() const + { + return this->subscription_ == Subscription::Subscribed; + } - void markParticipated(); + /// Returns true if and only if the user manually unsubscribed from the thread + /// @see #markUnsubscribed() + bool unsubscribed() const + { + return this->subscription_ == Subscription::Unsubscribed; + } + + /// Subscribe to this thread. + void markSubscribed(); + /// Unsubscribe from this thread. + void markUnsubscribed(); const QString &rootId() const { @@ -42,11 +62,14 @@ public: return replies_; } + boost::signals2::signal subscriptionUpdated; + private: const QString rootMessageId_; const std::shared_ptr rootMessage_; std::vector> replies_; - bool participated_ = false; + + Subscription subscription_ = Subscription::None; }; } // namespace chatterino diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index ce2752bf9..280d8a95f 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -119,31 +119,40 @@ void updateReplyParticipatedStatus(const QVariantMap &tags, { const auto ¤tLogin = getApp()->accounts->twitch.getCurrent()->getUserName(); - if (thread->participated()) + + if (thread->subscribed()) { - builder.message().flags.set(MessageFlag::ParticipatedThread); + builder.message().flags.set(MessageFlag::SubscribedThread); return; } - if (isNew) + if (thread->unsubscribed()) { - if (const auto it = tags.find("reply-parent-user-login"); - it != tags.end()) - { - auto name = it.value().toString(); - if (name == currentLogin) - { - thread->markParticipated(); - builder.message().flags.set(MessageFlag::ParticipatedThread); - return; // already marked as participated - } - } + return; } - if (senderLogin == currentLogin) + if (getSettings()->autoSubToParticipatedThreads) { - thread->markParticipated(); - // don't set the highlight here + if (isNew) + { + if (const auto it = tags.find("reply-parent-user-login"); + it != tags.end()) + { + auto name = it.value().toString(); + if (name == currentLogin) + { + thread->markSubscribed(); + builder.message().flags.set(MessageFlag::SubscribedThread); + return; // already marked as participated + } + } + } + + if (senderLogin == currentLogin) + { + thread->markSubscribed(); + // don't set the highlight here + } } } diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index dafa0dc67..9440f8a97 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -194,6 +194,10 @@ public: BoolSetting autoCloseUserPopup = {"/behaviour/autoCloseUserPopup", true}; BoolSetting autoCloseThreadPopup = {"/behaviour/autoCloseThreadPopup", false}; + BoolSetting autoSubToParticipatedThreads = { + "/behaviour/autoSubToParticipatedThreads", + true, + }; // BoolSetting twitchSeperateWriteConnection = // {"/behaviour/twitchSeperateWriteConnection", false}; diff --git a/src/widgets/DraggablePopup.cpp b/src/widgets/DraggablePopup.cpp index ca015282b..157150a9a 100644 --- a/src/widgets/DraggablePopup.cpp +++ b/src/widgets/DraggablePopup.cpp @@ -1,4 +1,8 @@ -#include "DraggablePopup.hpp" +#include "widgets/DraggablePopup.hpp" + +#include "singletons/Resources.hpp" +#include "singletons/Theme.hpp" +#include "widgets/helper/Button.hpp" #include @@ -90,4 +94,29 @@ void DraggablePopup::mouseMoveEvent(QMouseEvent *event) } } +Button *DraggablePopup::createPinButton() +{ + auto *button = new Button(this); + button->setPixmap(getTheme()->buttons.pin); + button->setScaleIndependantSize(18, 18); + button->setToolTip("Pin Window"); + + bool pinned = false; + QObject::connect( + button, &Button::leftClicked, [this, button, pinned]() mutable { + pinned = !pinned; + if (pinned) + { + this->setActionOnFocusLoss(BaseWindow::Nothing); + button->setPixmap(getResources().buttons.pinEnabled); + } + else + { + this->setActionOnFocusLoss(BaseWindow::Delete); + button->setPixmap(getTheme()->buttons.pin); + } + }); + return button; +} + } // namespace chatterino diff --git a/src/widgets/DraggablePopup.hpp b/src/widgets/DraggablePopup.hpp index 65050e15b..578ec8dfa 100644 --- a/src/widgets/DraggablePopup.hpp +++ b/src/widgets/DraggablePopup.hpp @@ -26,6 +26,11 @@ protected: void mouseReleaseEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; + /// Creates a pin button that is scoped to this window. + /// When clicked, the user can toggle whether the window is pinned. + /// The window is considered unpinned at the start. + Button *createPinButton(); + // lifetimeHack_ is used to check that the window hasn't been destroyed yet std::shared_ptr lifetimeHack_; diff --git a/src/widgets/dialogs/ReplyThreadPopup.cpp b/src/widgets/dialogs/ReplyThreadPopup.cpp index 1a1c98717..16c37cdd7 100644 --- a/src/widgets/dialogs/ReplyThreadPopup.cpp +++ b/src/widgets/dialogs/ReplyThreadPopup.cpp @@ -7,16 +7,18 @@ #include "controllers/hotkeys/HotkeyController.hpp" #include "messages/Message.hpp" #include "messages/MessageThread.hpp" -#include "providers/twitch/ChannelPointReward.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchChannel.hpp" +#include "singletons/Settings.hpp" #include "util/LayoutCreator.hpp" +#include "widgets/helper/Button.hpp" #include "widgets/helper/ChannelView.hpp" -#include "widgets/helper/ResizingTextEdit.hpp" #include "widgets/Scrollbar.hpp" #include "widgets/splits/Split.hpp" #include "widgets/splits/SplitInput.hpp" +#include + const QString TEXT_TITLE("Reply Thread - @%1 in #%2"); namespace chatterino { @@ -72,9 +74,6 @@ ReplyThreadPopup::ReplyThreadPopup(bool closeAutomatically, QWidget *parent, this->shortcuts_ = getApp()->hotkeys->shortcutsForCategory( HotkeyCategory::PopupWindow, actions, this); - auto layout = LayoutCreator(this->getLayoutContainer()) - .setLayoutType(); - // initialize UI this->ui_.threadView = new ChannelView(this, this->split_, ChannelView::Context::ReplyThread); @@ -110,10 +109,57 @@ ReplyThreadPopup::ReplyThreadPopup(bool closeAutomatically, QWidget *parent, } }); + auto layout = LayoutCreator(this->getLayoutContainer()) + .setLayoutType(); + layout->setSpacing(0); // provide draggable margin if frameless auto marginPx = closeAutomatically ? 15 : 1; layout->setContentsMargins(marginPx, marginPx, marginPx, marginPx); + + // Top Row + bool addCheckbox = getSettings()->enableThreadHighlight; + if (addCheckbox || closeAutomatically) + { + auto *hbox = new QHBoxLayout(); + + if (addCheckbox) + { + this->ui_.notificationCheckbox = + new QCheckBox("Subscribe to thread", this); + QObject::connect(this->ui_.notificationCheckbox, + &QCheckBox::toggled, [this](bool checked) { + if (!this->thread_ || + this->thread_->subscribed() == checked) + { + return; + } + + if (checked) + { + this->thread_->markSubscribed(); + } + else + { + this->thread_->markUnsubscribed(); + } + }); + hbox->addWidget(this->ui_.notificationCheckbox, 1); + } + + if (closeAutomatically) + { + hbox->addWidget(this->createPinButton(), 0, Qt::AlignRight); + hbox->setContentsMargins(0, 0, 0, 5); + } + else + { + hbox->setContentsMargins(10, 0, 0, 4); + } + + layout->addLayout(hbox, 1); + } + layout->addWidget(this->ui_.threadView, 1); layout->addWidget(this->ui_.replyInput); } @@ -124,6 +170,24 @@ void ReplyThreadPopup::setThread(std::shared_ptr thread) this->ui_.replyInput->setReply(this->thread_); this->addMessagesFromThread(); this->updateInputUI(); + + if (!this->thread_) [[unlikely]] + { + this->replySubscriptionSignal_ = boost::signals2::scoped_connection{}; + return; + } + + auto updateCheckbox = [this]() { + if (this->ui_.notificationCheckbox) + { + this->ui_.notificationCheckbox->setChecked( + this->thread_->subscribed()); + } + }; + updateCheckbox(); + + this->replySubscriptionSignal_ = + this->thread_->subscriptionUpdated.connect(updateCheckbox); } void ReplyThreadPopup::addMessagesFromThread() diff --git a/src/widgets/dialogs/ReplyThreadPopup.hpp b/src/widgets/dialogs/ReplyThreadPopup.hpp index 613cf4eef..567812ce0 100644 --- a/src/widgets/dialogs/ReplyThreadPopup.hpp +++ b/src/widgets/dialogs/ReplyThreadPopup.hpp @@ -7,6 +7,8 @@ #include #include +class QCheckBox; + namespace chatterino { class MessageThread; @@ -41,10 +43,13 @@ private: struct { ChannelView *threadView = nullptr; SplitInput *replyInput = nullptr; + + QCheckBox *notificationCheckbox = nullptr; } ui_; std::unique_ptr messageConnection_; std::vector bSignals_; + boost::signals2::scoped_connection replySubscriptionSignal_; }; } // namespace chatterino diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index 9c0541d63..e69781002 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -142,7 +142,6 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent, "split being nullptr causes lots of bugs down the road"); this->setWindowTitle("Usercard"); this->setStayInScreenRect(true); - this->updateFocusLoss(); HotkeyController::HotkeyMap actions{ {"delete", @@ -361,17 +360,7 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent, // button to pin the window (only if we close automatically) if (this->closeAutomatically_) { - this->ui_.pinButton = box.emplace