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