Add option to subscribe to and pin reply threads (#4680)

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
nerix 2023-06-17 17:41:52 +02:00 committed by GitHub
parent 2d3d3ae46e
commit aff9342647
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 194 additions and 69 deletions

View file

@ -8,6 +8,7 @@
- Minor: Improved editing hotkeys. (#4628) - Minor: Improved editing hotkeys. (#4628)
- Minor: The input completion and quick switcher are now styled to match your theme. (#4671) - 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 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: 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: 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) - Bugfix: Fixed a crash when opening and closing a reply thread and switching the user. (#4675)

View file

@ -163,7 +163,7 @@ void rebuildReplyThreadHighlight(Settings &settings,
const auto & /*senderName*/, const auto & /*originalMessage*/, const auto & /*senderName*/, const auto & /*originalMessage*/,
const auto &flags, const auto &flags,
const auto self) -> boost::optional<HighlightResult> { const auto self) -> boost::optional<HighlightResult> {
if (flags.has(MessageFlag::ParticipatedThread) && !self) if (flags.has(MessageFlag::SubscribedThread) && !self)
{ {
return HighlightResult{ return HighlightResult{
highlightAlert, highlightAlert,

View file

@ -210,7 +210,7 @@ void HighlightModel::afterInit()
std::vector<QStandardItem *> threadMessageRow = this->createRow(); std::vector<QStandardItem *> threadMessageRow = this->createRow();
setBoolItem(threadMessageRow[Column::Pattern], setBoolItem(threadMessageRow[Column::Pattern],
getSettings()->enableThreadHighlight.getValue(), true, false); getSettings()->enableThreadHighlight.getValue(), true, false);
threadMessageRow[Column::Pattern]->setData("Participated Reply Threads", threadMessageRow[Column::Pattern]->setData("Subscribed Reply Threads",
Qt::DisplayRole); Qt::DisplayRole);
setBoolItem(threadMessageRow[Column::ShowInMentions], setBoolItem(threadMessageRow[Column::ShowInMentions],
getSettings()->showThreadHighlightInMentions.getValue(), true, getSettings()->showThreadHighlightInMentions.getValue(), true,

View file

@ -46,7 +46,7 @@ enum class MessageFlag : int64_t {
FirstMessage = (1LL << 23), FirstMessage = (1LL << 23),
ReplyMessage = (1LL << 24), ReplyMessage = (1LL << 24),
ElevatedMessage = (1LL << 25), ElevatedMessage = (1LL << 25),
ParticipatedThread = (1LL << 26), SubscribedThread = (1LL << 26),
CheerMessage = (1LL << 27), CheerMessage = (1LL << 27),
LiveUpdatesAdd = (1LL << 28), LiveUpdatesAdd = (1LL << 28),
LiveUpdatesRemove = (1LL << 29), LiveUpdatesRemove = (1LL << 29),

View file

@ -1,4 +1,4 @@
#include "MessageThread.hpp" #include "messages/MessageThread.hpp"
#include "messages/Message.hpp" #include "messages/Message.hpp"
#include "util/DebugCount.hpp" #include "util/DebugCount.hpp"
@ -58,14 +58,26 @@ size_t MessageThread::liveCount(
return count; 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 } // namespace chatterino

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <boost/signals2.hpp>
#include <QString> #include <QString>
#include <memory> #include <memory>
@ -11,6 +12,12 @@ struct Message;
class MessageThread class MessageThread
{ {
public: public:
enum class Subscription : uint8_t {
None,
Subscribed,
Unsubscribed,
};
MessageThread(std::shared_ptr<const Message> rootMessage); MessageThread(std::shared_ptr<const Message> rootMessage);
~MessageThread(); ~MessageThread();
@ -23,9 +30,22 @@ public:
/// Returns the number of live reply references /// Returns the number of live reply references
size_t liveCount(const std::shared_ptr<const Message> &exclude) const; size_t liveCount(const std::shared_ptr<const Message> &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 const QString &rootId() const
{ {
@ -42,11 +62,14 @@ public:
return replies_; return replies_;
} }
boost::signals2::signal<void()> subscriptionUpdated;
private: private:
const QString rootMessageId_; const QString rootMessageId_;
const std::shared_ptr<const Message> rootMessage_; const std::shared_ptr<const Message> rootMessage_;
std::vector<std::weak_ptr<const Message>> replies_; std::vector<std::weak_ptr<const Message>> replies_;
bool participated_ = false;
Subscription subscription_ = Subscription::None;
}; };
} // namespace chatterino } // namespace chatterino

View file

@ -119,31 +119,40 @@ void updateReplyParticipatedStatus(const QVariantMap &tags,
{ {
const auto &currentLogin = const auto &currentLogin =
getApp()->accounts->twitch.getCurrent()->getUserName(); getApp()->accounts->twitch.getCurrent()->getUserName();
if (thread->participated())
if (thread->subscribed())
{ {
builder.message().flags.set(MessageFlag::ParticipatedThread); builder.message().flags.set(MessageFlag::SubscribedThread);
return; return;
} }
if (isNew) if (thread->unsubscribed())
{ {
if (const auto it = tags.find("reply-parent-user-login"); return;
it != tags.end())
{
auto name = it.value().toString();
if (name == currentLogin)
{
thread->markParticipated();
builder.message().flags.set(MessageFlag::ParticipatedThread);
return; // already marked as participated
}
}
} }
if (senderLogin == currentLogin) if (getSettings()->autoSubToParticipatedThreads)
{ {
thread->markParticipated(); if (isNew)
// don't set the highlight here {
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
}
} }
} }

View file

@ -194,6 +194,10 @@ public:
BoolSetting autoCloseUserPopup = {"/behaviour/autoCloseUserPopup", true}; BoolSetting autoCloseUserPopup = {"/behaviour/autoCloseUserPopup", true};
BoolSetting autoCloseThreadPopup = {"/behaviour/autoCloseThreadPopup", BoolSetting autoCloseThreadPopup = {"/behaviour/autoCloseThreadPopup",
false}; false};
BoolSetting autoSubToParticipatedThreads = {
"/behaviour/autoSubToParticipatedThreads",
true,
};
// BoolSetting twitchSeperateWriteConnection = // BoolSetting twitchSeperateWriteConnection =
// {"/behaviour/twitchSeperateWriteConnection", false}; // {"/behaviour/twitchSeperateWriteConnection", false};

View file

@ -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 <QMouseEvent> #include <QMouseEvent>
@ -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 } // namespace chatterino

View file

@ -26,6 +26,11 @@ protected:
void mouseReleaseEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override;
void mouseMoveEvent(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 // lifetimeHack_ is used to check that the window hasn't been destroyed yet
std::shared_ptr<bool> lifetimeHack_; std::shared_ptr<bool> lifetimeHack_;

View file

@ -7,16 +7,18 @@
#include "controllers/hotkeys/HotkeyController.hpp" #include "controllers/hotkeys/HotkeyController.hpp"
#include "messages/Message.hpp" #include "messages/Message.hpp"
#include "messages/MessageThread.hpp" #include "messages/MessageThread.hpp"
#include "providers/twitch/ChannelPointReward.hpp"
#include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchAccount.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "singletons/Settings.hpp"
#include "util/LayoutCreator.hpp" #include "util/LayoutCreator.hpp"
#include "widgets/helper/Button.hpp"
#include "widgets/helper/ChannelView.hpp" #include "widgets/helper/ChannelView.hpp"
#include "widgets/helper/ResizingTextEdit.hpp"
#include "widgets/Scrollbar.hpp" #include "widgets/Scrollbar.hpp"
#include "widgets/splits/Split.hpp" #include "widgets/splits/Split.hpp"
#include "widgets/splits/SplitInput.hpp" #include "widgets/splits/SplitInput.hpp"
#include <QCheckBox>
const QString TEXT_TITLE("Reply Thread - @%1 in #%2"); const QString TEXT_TITLE("Reply Thread - @%1 in #%2");
namespace chatterino { namespace chatterino {
@ -72,9 +74,6 @@ ReplyThreadPopup::ReplyThreadPopup(bool closeAutomatically, QWidget *parent,
this->shortcuts_ = getApp()->hotkeys->shortcutsForCategory( this->shortcuts_ = getApp()->hotkeys->shortcutsForCategory(
HotkeyCategory::PopupWindow, actions, this); HotkeyCategory::PopupWindow, actions, this);
auto layout = LayoutCreator<QWidget>(this->getLayoutContainer())
.setLayoutType<QVBoxLayout>();
// initialize UI // initialize UI
this->ui_.threadView = this->ui_.threadView =
new ChannelView(this, this->split_, ChannelView::Context::ReplyThread); new ChannelView(this, this->split_, ChannelView::Context::ReplyThread);
@ -110,10 +109,57 @@ ReplyThreadPopup::ReplyThreadPopup(bool closeAutomatically, QWidget *parent,
} }
}); });
auto layout = LayoutCreator<QWidget>(this->getLayoutContainer())
.setLayoutType<QVBoxLayout>();
layout->setSpacing(0); layout->setSpacing(0);
// provide draggable margin if frameless // provide draggable margin if frameless
auto marginPx = closeAutomatically ? 15 : 1; auto marginPx = closeAutomatically ? 15 : 1;
layout->setContentsMargins(marginPx, marginPx, marginPx, marginPx); 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_.threadView, 1);
layout->addWidget(this->ui_.replyInput); layout->addWidget(this->ui_.replyInput);
} }
@ -124,6 +170,24 @@ void ReplyThreadPopup::setThread(std::shared_ptr<MessageThread> thread)
this->ui_.replyInput->setReply(this->thread_); this->ui_.replyInput->setReply(this->thread_);
this->addMessagesFromThread(); this->addMessagesFromThread();
this->updateInputUI(); 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() void ReplyThreadPopup::addMessagesFromThread()

View file

@ -7,6 +7,8 @@
#include <pajlada/signals/scoped-connection.hpp> #include <pajlada/signals/scoped-connection.hpp>
#include <pajlada/signals/signal.hpp> #include <pajlada/signals/signal.hpp>
class QCheckBox;
namespace chatterino { namespace chatterino {
class MessageThread; class MessageThread;
@ -41,10 +43,13 @@ private:
struct { struct {
ChannelView *threadView = nullptr; ChannelView *threadView = nullptr;
SplitInput *replyInput = nullptr; SplitInput *replyInput = nullptr;
QCheckBox *notificationCheckbox = nullptr;
} ui_; } ui_;
std::unique_ptr<pajlada::Signals::ScopedConnection> messageConnection_; std::unique_ptr<pajlada::Signals::ScopedConnection> messageConnection_;
std::vector<boost::signals2::scoped_connection> bSignals_; std::vector<boost::signals2::scoped_connection> bSignals_;
boost::signals2::scoped_connection replySubscriptionSignal_;
}; };
} // namespace chatterino } // namespace chatterino

View file

@ -142,7 +142,6 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent,
"split being nullptr causes lots of bugs down the road"); "split being nullptr causes lots of bugs down the road");
this->setWindowTitle("Usercard"); this->setWindowTitle("Usercard");
this->setStayInScreenRect(true); this->setStayInScreenRect(true);
this->updateFocusLoss();
HotkeyController::HotkeyMap actions{ HotkeyController::HotkeyMap actions{
{"delete", {"delete",
@ -361,17 +360,7 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent,
// button to pin the window (only if we close automatically) // button to pin the window (only if we close automatically)
if (this->closeAutomatically_) if (this->closeAutomatically_)
{ {
this->ui_.pinButton = box.emplace<Button>().getElement(); box->addWidget(this->createPinButton());
this->ui_.pinButton->setPixmap(
getApp()->themes->buttons.pin);
this->ui_.pinButton->setScaleIndependantSize(18, 18);
this->ui_.pinButton->setToolTip("Pin Window");
QObject::connect(this->ui_.pinButton, &Button::leftClicked,
[this]() {
this->closeAutomatically_ =
!this->closeAutomatically_;
this->updateFocusLoss();
});
} }
} }
@ -920,26 +909,6 @@ void UserInfoPopup::updateUserData()
this->ui_.ignoreHighlights->setEnabled(false); this->ui_.ignoreHighlights->setEnabled(false);
} }
void UserInfoPopup::updateFocusLoss()
{
if (this->closeAutomatically_)
{
this->setActionOnFocusLoss(BaseWindow::Delete);
if (this->ui_.pinButton != nullptr)
{
this->ui_.pinButton->setPixmap(getApp()->themes->buttons.pin);
}
}
else
{
this->setActionOnFocusLoss(BaseWindow::Nothing);
if (this->ui_.pinButton != nullptr)
{
this->ui_.pinButton->setPixmap(getResources().buttons.pinEnabled);
}
}
}
void UserInfoPopup::loadAvatar(const QUrl &url) void UserInfoPopup::loadAvatar(const QUrl &url)
{ {
QNetworkRequest req(url); QNetworkRequest req(url);

View file

@ -37,7 +37,6 @@ private:
void installEvents(); void installEvents();
void updateUserData(); void updateUserData();
void updateLatestMessages(); void updateLatestMessages();
void updateFocusLoss();
void loadAvatar(const QUrl &url); void loadAvatar(const QUrl &url);
bool isMod_; bool isMod_;
@ -73,8 +72,6 @@ private:
Label *followerCountLabel = nullptr; Label *followerCountLabel = nullptr;
Label *createdDateLabel = nullptr; Label *createdDateLabel = nullptr;
Label *userIDLabel = nullptr; Label *userIDLabel = nullptr;
// Can be uninitialized if usercard is not configured to close on focus loss
Button *pinButton = nullptr;
Label *followageLabel = nullptr; Label *followageLabel = nullptr;
Label *subageLabel = nullptr; Label *subageLabel = nullptr;

View file

@ -916,6 +916,13 @@ void GeneralPage::initLayout(GeneralPageView &layout)
"Highlight received inline whispers", s.highlightInlineWhispers, false, "Highlight received inline whispers", s.highlightInlineWhispers, false,
"Highlight the whispers shown in all splits.\nIf \"Show Twitch " "Highlight the whispers shown in all splits.\nIf \"Show Twitch "
"whispers inline\" is disabled, this setting will do nothing."); "whispers inline\" is disabled, this setting will do nothing.");
layout.addCheckbox(
"Automatically subscribe to participated reply threads",
s.autoSubToParticipatedThreads, false,
"When enabled, you will automatically subscribe to reply threads you "
"participate in.\n"
"This means reply threads you participate in will use your "
"\"Subscribed Reply Threads\" highlight settings.");
layout.addCheckbox("Load message history on connect", layout.addCheckbox("Load message history on connect",
s.loadTwitchMessageHistoryOnConnect); s.loadTwitchMessageHistoryOnConnect);
// TODO: Change phrasing to use better english once we can tag settings, right now it's kept as history instead of historical so that the setting shows up when the user searches for history // TODO: Change phrasing to use better english once we can tag settings, right now it's kept as history instead of historical so that the setting shows up when the user searches for history