Added support for Twitch's Chat Replies (#3722)

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
Daniel Sage
2022-07-31 06:45:25 -04:00
committed by GitHub
parent a280089693
commit 20c974fdab
53 changed files with 2022 additions and 310 deletions
+146 -5
View File
@@ -44,6 +44,7 @@
#include "widgets/Scrollbar.hpp"
#include "widgets/TooltipWidget.hpp"
#include "widgets/Window.hpp"
#include "widgets/dialogs/ReplyThreadPopup.hpp"
#include "widgets/dialogs/SettingsDialog.hpp"
#include "widgets/dialogs/UserInfoPopup.hpp"
#include "widgets/helper/EffectLabel.hpp"
@@ -119,9 +120,11 @@ namespace {
}
} // namespace
ChannelView::ChannelView(BaseWidget *parent)
ChannelView::ChannelView(BaseWidget *parent, Split *split, Context context)
: BaseWidget(parent)
, scrollBar_(new Scrollbar(this))
, split_(split)
, context_(context)
{
this->setMouseTracking(true);
@@ -160,7 +163,7 @@ ChannelView::ChannelView(BaseWidget *parent)
// and tabbing to it from another widget. I don't currently know
// of any place where you can, or where it would make sense,
// to tab to a ChannelVieChannelView
this->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
this->setFocusPolicy(Qt::FocusPolicy::ClickFocus);
}
void ChannelView::initializeLayout()
@@ -1019,6 +1022,17 @@ MessageElementFlags ChannelView::getFlags() const
if (this->sourceChannel_ == app->twitch->mentionsChannel)
flags.set(MessageElementFlag::ChannelName);
if (this->context_ == Context::ReplyThread)
{
// Don't show inline replies within the ReplyThreadPopup
flags.unset(MessageElementFlag::RepliedMessage);
}
if (!this->canReplyToMessages())
{
flags.unset(MessageElementFlag::ReplyButton);
}
return flags;
}
@@ -1983,10 +1997,27 @@ void ChannelView::addMessageContextMenuItems(
menu.addAction("Copy full message", [layout] {
QString copyString;
layout->addSelectionText(copyString);
layout->addSelectionText(copyString, 0, INT_MAX,
CopyMode::EverythingButReplies);
crossPlatformCopy(copyString);
});
// Only display reply option where it makes sense
if (this->canReplyToMessages() && layout->isReplyable())
{
const auto &messagePtr = layout->getMessagePtr();
menu.addAction("Reply to message", [this, &messagePtr] {
this->setInputReply(messagePtr);
});
if (messagePtr->replyThread != nullptr)
{
menu.addAction("View thread", [this, &messagePtr] {
this->showReplyThreadPopup(messagePtr);
});
}
}
}
void ChannelView::addTwitchLinkContextMenuItems(
@@ -2224,8 +2255,8 @@ void ChannelView::showUserInfoPopup(const QString &userName,
{
auto *userCardParent =
static_cast<QWidget *>(&(getApp()->windows->getMainWindow()));
auto *userPopup =
new UserInfoPopup(getSettings()->autoCloseUserPopup, userCardParent);
auto *userPopup = new UserInfoPopup(getSettings()->autoCloseUserPopup,
userCardParent, this->split_);
auto contextChannel =
getApp()->twitch->getChannelOrEmpty(alternativePopoutChannel);
@@ -2351,6 +2382,14 @@ void ChannelView::handleLinkClick(QMouseEvent *event, const Link &link,
this->underlyingChannel_.get()->reconnect();
}
break;
case Link::ReplyToMessage: {
this->setInputReply(layout->getMessagePtr());
}
break;
case Link::ViewThread: {
this->showReplyThreadPopup(layout->getMessagePtr());
}
break;
default:;
}
@@ -2473,4 +2512,106 @@ void ChannelView::scrollUpdateRequested()
this->scrollBar_->offset(multiplier * offset);
}
void ChannelView::setInputReply(const MessagePtr &message)
{
if (message == nullptr)
{
return;
}
std::shared_ptr<MessageThread> thread;
if (message->replyThread == nullptr)
{
auto getThread = [&](TwitchChannel *tc) {
auto threadIt = tc->threads().find(message->id);
if (threadIt != tc->threads().end() && !threadIt->second.expired())
{
return threadIt->second.lock();
}
else
{
auto thread = std::make_shared<MessageThread>(message);
tc->addReplyThread(thread);
return thread;
}
};
if (auto tc =
dynamic_cast<TwitchChannel *>(this->underlyingChannel_.get()))
{
thread = getThread(tc);
}
else if (auto tc = dynamic_cast<TwitchChannel *>(this->channel_.get()))
{
thread = getThread(tc);
}
else
{
qCWarning(chatterinoCommon) << "Failed to create new reply thread";
// Unable to create new reply thread.
// TODO(dnsge): Should probably notify user?
return;
}
}
else
{
thread = message->replyThread;
}
this->split_->setInputReply(thread);
}
void ChannelView::showReplyThreadPopup(const MessagePtr &message)
{
if (message == nullptr || message->replyThread == nullptr)
{
return;
}
auto popupParent =
static_cast<QWidget *>(&(getApp()->windows->getMainWindow()));
auto popup = new ReplyThreadPopup(getSettings()->autoCloseThreadPopup,
popupParent, this->split_);
popup->setThread(message->replyThread);
QPoint offset(int(150 * this->scale()), int(70 * this->scale()));
popup->move(QCursor::pos() - offset);
popup->show();
popup->giveFocus(Qt::MouseFocusReason);
}
ChannelView::Context ChannelView::getContext() const
{
return this->context_;
}
bool ChannelView::canReplyToMessages() const
{
if (this->context_ == ChannelView::Context::ReplyThread ||
this->context_ == ChannelView::Context::Search)
{
return false;
}
if (this->channel_ == nullptr)
{
return false;
}
if (!this->channel_->isTwitchChannel())
{
return false;
}
if (this->channel_->getType() == Channel::Type::TwitchWhispers ||
this->channel_->getType() == Channel::Type::TwitchLive)
{
return false;
}
return true;
}
} // namespace chatterino
+19 -1
View File
@@ -39,6 +39,7 @@ class Scrollbar;
class EffectLabel;
struct Link;
class MessageLayoutElement;
class Split;
enum class PauseReason {
Mouse,
@@ -61,7 +62,15 @@ class ChannelView final : public BaseWidget
Q_OBJECT
public:
explicit ChannelView(BaseWidget *parent = nullptr);
enum class Context {
None,
UserCard,
ReplyThread,
Search,
};
explicit ChannelView(BaseWidget *parent = nullptr, Split *split = nullptr,
Context context = Context::None);
void queueUpdate();
Scrollbar &getScrollBar();
@@ -99,6 +108,8 @@ public:
void clearMessages();
Context getContext() const;
/**
* @brief Creates and shows a UserInfoPopup dialog
*
@@ -196,6 +207,10 @@ private:
void enableScrolling(const QPointF &scrollStart);
void disableScrolling();
void setInputReply(const MessagePtr &message);
void showReplyThreadPopup(const MessagePtr &message);
bool canReplyToMessages() const;
QTimer *layoutCooldown_;
bool layoutQueued_;
@@ -221,6 +236,7 @@ private:
ChannelPtr channel_ = nullptr;
ChannelPtr underlyingChannel_ = nullptr;
ChannelPtr sourceChannel_ = nullptr;
Split *split_ = nullptr;
Scrollbar *scrollBar_;
EffectLabel *goToBottom_;
@@ -264,6 +280,8 @@ private:
Selection selection_;
bool selecting_ = false;
const Context context_;
LimitedQueue<MessageLayoutPtr> messages_;
pajlada::Signals::SignalHolder signalHolder_;
+4 -2
View File
@@ -49,8 +49,9 @@ ChannelPtr SearchPopup::filter(const QString &text, const QString &channelName,
return channel;
}
SearchPopup::SearchPopup(QWidget *parent)
SearchPopup::SearchPopup(QWidget *parent, Split *split)
: BasePopup({}, parent)
, split_(split)
{
this->initLayout();
this->resize(400, 600);
@@ -238,7 +239,8 @@ void SearchPopup::initLayout()
// CHANNELVIEW
{
this->channelView_ = new ChannelView(this);
this->channelView_ = new ChannelView(this, this->split_,
ChannelView::Context::Search);
layout1->addWidget(this->channelView_);
}
+3 -1
View File
@@ -5,6 +5,7 @@
#include "messages/LimitedQueueSnapshot.hpp"
#include "messages/search/MessagePredicate.hpp"
#include "widgets/BasePopup.hpp"
#include "widgets/splits/Split.hpp"
#include <memory>
@@ -15,7 +16,7 @@ namespace chatterino {
class SearchPopup : public BasePopup
{
public:
SearchPopup(QWidget *parent);
SearchPopup(QWidget *parent, Split *split = nullptr);
virtual void addChannel(ChannelView &channel);
@@ -58,6 +59,7 @@ private:
QLineEdit *searchInput_{};
ChannelView *channelView_{};
QString channelName_{};
Split *split_ = nullptr;
QList<std::reference_wrapper<ChannelView>> searchChannels_;
};