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
+193
View File
@@ -0,0 +1,193 @@
#include "ReplyThreadPopup.hpp"
#include "Application.hpp"
#include "common/Channel.hpp"
#include "common/QLogging.hpp"
#include "controllers/hotkeys/HotkeyController.hpp"
#include "messages/MessageThread.hpp"
#include "util/LayoutCreator.hpp"
#include "widgets/Scrollbar.hpp"
#include "widgets/helper/ChannelView.hpp"
#include "widgets/helper/ResizingTextEdit.hpp"
#include "widgets/splits/Split.hpp"
#include "widgets/splits/SplitInput.hpp"
const QString TEXT_TITLE("Reply Thread - @%1 in #%2");
namespace chatterino {
ReplyThreadPopup::ReplyThreadPopup(bool closeAutomatically, QWidget *parent,
Split *split)
: DraggablePopup(closeAutomatically, parent)
, split_(split)
{
this->setWindowTitle(QStringLiteral("Reply Thread"));
this->setStayInScreenRect(true);
HotkeyController::HotkeyMap actions{
{"delete",
[this](std::vector<QString>) -> QString {
this->deleteLater();
return "";
}},
{"scrollPage",
[this](std::vector<QString> arguments) -> QString {
if (arguments.empty())
{
qCWarning(chatterinoHotkeys)
<< "scrollPage hotkey called without arguments!";
return "scrollPage hotkey called without arguments!";
}
auto direction = arguments.at(0);
auto &scrollbar = this->ui_.threadView->getScrollBar();
if (direction == "up")
{
scrollbar.offset(-scrollbar.getLargeChange());
}
else if (direction == "down")
{
scrollbar.offset(scrollbar.getLargeChange());
}
else
{
qCWarning(chatterinoHotkeys) << "Unknown scroll direction";
}
return "";
}},
// these actions make no sense in the context of a reply thread, so they aren't implemented
{"execModeratorAction", nullptr},
{"reject", nullptr},
{"accept", nullptr},
{"openTab", nullptr},
{"search", nullptr},
};
this->shortcuts_ = getApp()->hotkeys->shortcutsForCategory(
HotkeyCategory::PopupWindow, actions, this);
auto layout = LayoutCreator<QWidget>(this->getLayoutContainer())
.setLayoutType<QVBoxLayout>();
// initialize UI
this->ui_.threadView =
new ChannelView(this, this->split_, ChannelView::Context::ReplyThread);
this->ui_.threadView->setMinimumSize(400, 100);
this->ui_.threadView->setSizePolicy(QSizePolicy::Expanding,
QSizePolicy::Expanding);
this->ui_.threadView->mouseDown.connect([this](QMouseEvent *) {
this->giveFocus(Qt::MouseFocusReason);
});
// Create SplitInput with inline replying disabled
this->ui_.replyInput = new SplitInput(this, this->split_, false);
this->bSignals_.emplace_back(
getApp()->accounts->twitch.currentUserChanged.connect([this] {
this->updateInputUI();
}));
layout->setSpacing(0);
// provide draggable margin if frameless
layout->setMargin(closeAutomatically ? 15 : 1);
layout->addWidget(this->ui_.threadView, 1);
layout->addWidget(this->ui_.replyInput);
}
void ReplyThreadPopup::setThread(std::shared_ptr<MessageThread> thread)
{
this->thread_ = std::move(thread);
this->ui_.replyInput->setReply(this->thread_);
this->addMessagesFromThread();
this->updateInputUI();
}
void ReplyThreadPopup::addMessagesFromThread()
{
this->ui_.threadView->clearMessages();
if (!this->thread_)
{
return;
}
const auto &sourceChannel = this->split_->getChannel();
this->setWindowTitle(TEXT_TITLE.arg(this->thread_->root()->loginName,
sourceChannel->getName()));
ChannelPtr virtualChannel;
if (sourceChannel->isTwitchChannel())
{
virtualChannel =
std::make_shared<TwitchChannel>(sourceChannel->getName());
}
else
{
virtualChannel = std::make_shared<Channel>(sourceChannel->getName(),
Channel::Type::None);
}
this->ui_.threadView->setChannel(virtualChannel);
this->ui_.threadView->setSourceChannel(sourceChannel);
virtualChannel->addMessage(this->thread_->root());
for (const auto &msgRef : this->thread_->replies())
{
if (auto msg = msgRef.lock())
{
virtualChannel->addMessage(msg);
}
}
this->messageConnection_ =
std::make_unique<pajlada::Signals::ScopedConnection>(
sourceChannel->messageAppended.connect(
[this, virtualChannel](MessagePtr &message, auto) {
if (message->replyThread == this->thread_)
{
// same reply thread, add message
virtualChannel->addMessage(message);
}
}));
}
void ReplyThreadPopup::updateInputUI()
{
auto channel = this->split_->getChannel();
// Bail out if not a twitch channel.
// Special twitch channels will hide their reply input box.
if (!channel || !channel->isTwitchChannel())
{
return;
}
this->ui_.replyInput->setVisible(channel->isWritable());
auto user = getApp()->accounts->twitch.getCurrent();
QString placeholderText;
if (user->isAnon())
{
placeholderText = QStringLiteral("Log in to send messages...");
}
else
{
placeholderText =
QStringLiteral("Reply as %1...")
.arg(getApp()->accounts->twitch.getCurrent()->getUserName());
}
this->ui_.replyInput->setPlaceholderText(placeholderText);
}
void ReplyThreadPopup::giveFocus(Qt::FocusReason reason)
{
this->ui_.replyInput->giveFocus(reason);
}
void ReplyThreadPopup::focusInEvent(QFocusEvent *event)
{
this->giveFocus(event->reason());
}
} // namespace chatterino
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include "ForwardDecl.hpp"
#include "widgets/DraggablePopup.hpp"
#include <boost/signals2.hpp>
#include <pajlada/signals/scoped-connection.hpp>
#include <pajlada/signals/signal.hpp>
namespace chatterino {
class MessageThread;
class Split;
class SplitInput;
class ReplyThreadPopup final : public DraggablePopup
{
Q_OBJECT
public:
ReplyThreadPopup(bool closeAutomatically, QWidget *parent, Split *split);
void setThread(std::shared_ptr<MessageThread> thread);
void giveFocus(Qt::FocusReason reason);
protected:
void focusInEvent(QFocusEvent *event) override;
private:
void addMessagesFromThread();
void updateInputUI();
// The message reply thread
std::shared_ptr<MessageThread> thread_;
// The channel that the reply thread is in
ChannelPtr channel_;
Split *split_;
struct {
ChannelView *threadView = nullptr;
SplitInput *replyInput = nullptr;
} ui_;
std::unique_ptr<pajlada::Signals::ScopedConnection> messageConnection_;
std::vector<boost::signals2::scoped_connection> bSignals_;
};
} // namespace chatterino
+17 -72
View File
@@ -91,8 +91,16 @@ namespace {
LimitedQueueSnapshot<MessagePtr> snapshot =
channel->getMessageSnapshot();
ChannelPtr channelPtr(
new Channel(channel->getName(), Channel::Type::None));
ChannelPtr channelPtr;
if (channel->isTwitchChannel())
{
channelPtr = std::make_shared<TwitchChannel>(channel->getName());
}
else
{
channelPtr = std::make_shared<Channel>(channel->getName(),
Channel::Type::None);
}
for (size_t i = 0; i < snapshot.size(); i++)
{
@@ -118,33 +126,14 @@ namespace {
} // namespace
#ifdef Q_OS_LINUX
FlagsEnum<BaseWindow::Flags> userInfoPopupFlags{BaseWindow::Dialog,
BaseWindow::EnableCustomFrame};
FlagsEnum<BaseWindow::Flags> userInfoPopupFlagsCloseAutomatically{
BaseWindow::EnableCustomFrame};
#else
FlagsEnum<BaseWindow::Flags> userInfoPopupFlags{BaseWindow::EnableCustomFrame};
FlagsEnum<BaseWindow::Flags> userInfoPopupFlagsCloseAutomatically{
BaseWindow::EnableCustomFrame, BaseWindow::Frameless,
BaseWindow::FramelessDraggable};
#endif
UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent)
: BaseWindow(closeAutomatically ? userInfoPopupFlagsCloseAutomatically
: userInfoPopupFlags,
parent)
, hack_(new bool)
, dragTimer_(this)
UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent,
Split *split)
: DraggablePopup(closeAutomatically, parent)
, split_(split)
{
this->setWindowTitle("Usercard");
this->setStayInScreenRect(true);
if (closeAutomatically)
this->setActionOnFocusLoss(BaseWindow::Delete);
else
this->setAttribute(Qt::WA_DeleteOnClose);
HotkeyController::HotkeyMap actions{
{"delete",
[this](std::vector<QString>) -> QString {
@@ -498,7 +487,8 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent)
this->ui_.noMessagesLabel = new Label("No recent messages");
this->ui_.noMessagesLabel->setVisible(false);
this->ui_.latestMessages = new ChannelView(this);
this->ui_.latestMessages =
new ChannelView(this, this->split_, ChannelView::Context::UserCard);
this->ui_.latestMessages->setMinimumSize(400, 275);
this->ui_.latestMessages->setSizePolicy(QSizePolicy::Expanding,
QSizePolicy::Expanding);
@@ -510,21 +500,6 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent)
this->installEvents();
this->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Policy::Ignored);
this->dragTimer_.callOnTimeout(
[this, hack = std::weak_ptr<bool>(this->hack_)] {
if (!hack.lock())
{
// Ensure this timer is never called after the object has been destroyed
return;
}
if (!this->isMoving_)
{
return;
}
this->move(this->requestedDragPos_);
});
}
void UserInfoPopup::themeChangedEvent()
@@ -550,36 +525,6 @@ void UserInfoPopup::scaleChangedEvent(float /*scale*/)
});
}
void UserInfoPopup::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::MouseButton::LeftButton)
{
this->dragTimer_.start(std::chrono::milliseconds(17));
this->startPosDrag_ = event->pos();
this->movingRelativePos = event->localPos();
}
}
void UserInfoPopup::mouseReleaseEvent(QMouseEvent *event)
{
this->dragTimer_.stop();
this->isMoving_ = false;
}
void UserInfoPopup::mouseMoveEvent(QMouseEvent *event)
{
// Drag the window by the amount changed from inital position
// Note that we provide a few *units* of deadzone so people don't
// start dragging the window if they are slow at clicking.
auto movePos = event->pos() - this->startPosDrag_;
if (this->isMoving_ || movePos.manhattanLength() > 10.0)
{
this->requestedDragPos_ =
(event->screenPos() - this->movingRelativePos).toPoint();
this->isMoving_ = true;
}
}
void UserInfoPopup::installEvents()
{
std::shared_ptr<bool> ignoreNext = std::make_shared<bool>(false);
@@ -765,7 +710,7 @@ void UserInfoPopup::updateLatestMessages()
void UserInfoPopup::updateUserData()
{
std::weak_ptr<bool> hack = this->hack_;
std::weak_ptr<bool> hack = this->lifetimeHack_;
auto currentUser = getApp()->accounts->twitch.getCurrent();
const auto onUserFetchFailed = [this, hack] {
+6 -21
View File
@@ -1,6 +1,6 @@
#pragma once
#include "widgets/BaseWindow.hpp"
#include "widgets/DraggablePopup.hpp"
#include "widgets/helper/ChannelView.hpp"
#include <pajlada/signals/scoped-connection.hpp>
@@ -16,12 +16,13 @@ class Channel;
using ChannelPtr = std::shared_ptr<Channel>;
class Label;
class UserInfoPopup final : public BaseWindow
class UserInfoPopup final : public DraggablePopup
{
Q_OBJECT
public:
UserInfoPopup(bool closeAutomatically, QWidget *parent);
UserInfoPopup(bool closeAutomatically, QWidget *parent,
Split *split = nullptr);
void setData(const QString &name, const ChannelPtr &channel);
void setData(const QString &name, const ChannelPtr &contextChannel,
@@ -30,9 +31,6 @@ public:
protected:
virtual void themeChangedEvent() override;
virtual void scaleChangedEvent(float scale) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
private:
void installEvents();
@@ -43,6 +41,8 @@ private:
bool isMod_;
bool isBroadcaster_;
Split *split_;
QString userName_;
QString userId_;
QString avatarUrl_;
@@ -51,25 +51,10 @@ private:
// The channel the messages are rendered from (e.g. #forsen). Can be a special channel, but will try to not be where possible.
ChannelPtr underlyingChannel_;
// isMoving_ is set to true if the user is holding the left mouse button down and has moved the mouse a small amount away from the original click point (startPosDrag_)
bool isMoving_ = false;
// startPosDrag_ is the coordinates where the user originally pressed the mouse button down to start dragging
QPoint startPosDrag_;
// requestDragPos_ is the final screen coordinates where the widget should be moved to.
// Takes the relative position of where the user originally clicked the widget into account
QPoint requestedDragPos_;
// dragTimer_ is called ~60 times per second once the user has initiated dragging
QTimer dragTimer_;
pajlada::Signals::NoArgSignal userStateChanged_;
std::unique_ptr<pajlada::Signals::ScopedConnection> refreshConnection_;
std::shared_ptr<bool> hack_;
struct {
Button *avatarButton = nullptr;
Button *localizedNameCopyButton = nullptr;