Added support for Twitch's Chat Replies (#3722)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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_);
|
||||
}
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user