Added support for Twitch's Chat Replies (#3722)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -23,15 +23,24 @@
|
||||
#include "widgets/splits/SplitContainer.hpp"
|
||||
#include "widgets/splits/SplitInput.hpp"
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <QCompleter>
|
||||
#include <QPainter>
|
||||
|
||||
namespace chatterino {
|
||||
const int TWITCH_MESSAGE_LIMIT = 500;
|
||||
|
||||
SplitInput::SplitInput(Split *_chatWidget)
|
||||
: BaseWidget(_chatWidget)
|
||||
SplitInput::SplitInput(Split *_chatWidget, bool enableInlineReplying)
|
||||
: SplitInput(_chatWidget, _chatWidget, enableInlineReplying)
|
||||
{
|
||||
}
|
||||
|
||||
SplitInput::SplitInput(QWidget *parent, Split *_chatWidget,
|
||||
bool enableInlineReplying)
|
||||
: BaseWidget(parent)
|
||||
, split_(_chatWidget)
|
||||
, enableInlineReplying_(enableInlineReplying)
|
||||
{
|
||||
this->installEventFilter(this);
|
||||
this->initLayout();
|
||||
@@ -66,17 +75,43 @@ void SplitInput::initLayout()
|
||||
LayoutCreator<SplitInput> layoutCreator(this);
|
||||
|
||||
auto layout =
|
||||
layoutCreator.setLayoutType<QHBoxLayout>().withoutMargin().assign(
|
||||
&this->ui_.hbox);
|
||||
layoutCreator.setLayoutType<QVBoxLayout>().withoutMargin().assign(
|
||||
&this->ui_.vbox);
|
||||
|
||||
// reply label stuff
|
||||
auto replyWrapper =
|
||||
layout.emplace<QWidget>().assign(&this->ui_.replyWrapper);
|
||||
this->ui_.replyWrapper->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto replyHbox = replyWrapper.emplace<QHBoxLayout>().withoutMargin().assign(
|
||||
&this->ui_.replyHbox);
|
||||
|
||||
auto replyLabel = replyHbox.emplace<QLabel>().assign(&this->ui_.replyLabel);
|
||||
replyLabel->setAlignment(Qt::AlignLeft);
|
||||
replyLabel->setFont(
|
||||
app->fonts->getFont(FontStyle::ChatMedium, this->scale()));
|
||||
|
||||
replyHbox->addStretch(1);
|
||||
|
||||
auto replyCancelButton = replyHbox.emplace<EffectLabel>(nullptr, 4)
|
||||
.assign(&this->ui_.cancelReplyButton);
|
||||
replyCancelButton->getLabel().setTextFormat(Qt::RichText);
|
||||
|
||||
replyCancelButton->hide();
|
||||
replyLabel->hide();
|
||||
|
||||
// hbox for input, right box
|
||||
auto hboxLayout =
|
||||
layout.emplace<QHBoxLayout>().withoutMargin().assign(&this->ui_.hbox);
|
||||
|
||||
// input
|
||||
auto textEdit =
|
||||
layout.emplace<ResizingTextEdit>().assign(&this->ui_.textEdit);
|
||||
hboxLayout.emplace<ResizingTextEdit>().assign(&this->ui_.textEdit);
|
||||
connect(textEdit.getElement(), &ResizingTextEdit::textChanged, this,
|
||||
&SplitInput::editTextChanged);
|
||||
|
||||
// right box
|
||||
auto box = layout.emplace<QVBoxLayout>().withoutMargin();
|
||||
auto box = hboxLayout.emplace<QVBoxLayout>().withoutMargin();
|
||||
box->setSpacing(0);
|
||||
{
|
||||
auto textEditLength =
|
||||
@@ -102,6 +137,8 @@ void SplitInput::initLayout()
|
||||
this->managedConnections_.managedConnect(app->fonts->fontChanged, [=]() {
|
||||
this->ui_.textEdit->setFont(
|
||||
app->fonts->getFont(FontStyle::ChatMedium, this->scale()));
|
||||
this->ui_.replyLabel->setFont(
|
||||
app->fonts->getFont(FontStyle::ChatMediumBold, this->scale()));
|
||||
});
|
||||
|
||||
// open emote popup
|
||||
@@ -109,6 +146,12 @@ void SplitInput::initLayout()
|
||||
this->openEmotePopup();
|
||||
});
|
||||
|
||||
// clear input and remove reply thread
|
||||
QObject::connect(this->ui_.cancelReplyButton, &EffectLabel::leftClicked,
|
||||
[=] {
|
||||
this->clearInput();
|
||||
});
|
||||
|
||||
// clear channelview selection when selecting in the input
|
||||
QObject::connect(this->ui_.textEdit, &QTextEdit::copyAvailable,
|
||||
[this](bool available) {
|
||||
@@ -129,8 +172,10 @@ void SplitInput::initLayout()
|
||||
|
||||
void SplitInput::scaleChangedEvent(float scale)
|
||||
{
|
||||
// update the icon size of the emote button
|
||||
auto app = getApp();
|
||||
// update the icon size of the buttons
|
||||
this->updateEmoteButton();
|
||||
this->updateCancelReplyButton();
|
||||
|
||||
// set maximum height
|
||||
if (!this->hidden)
|
||||
@@ -138,9 +183,11 @@ void SplitInput::scaleChangedEvent(float scale)
|
||||
this->setMaximumHeight(this->scaledMaxHeight());
|
||||
}
|
||||
this->ui_.textEdit->setFont(
|
||||
getApp()->fonts->getFont(FontStyle::ChatMedium, this->scale()));
|
||||
app->fonts->getFont(FontStyle::ChatMedium, this->scale()));
|
||||
this->ui_.textEditLength->setFont(
|
||||
getApp()->fonts->getFont(FontStyle::ChatMedium, this->scale()));
|
||||
app->fonts->getFont(FontStyle::ChatMedium, this->scale()));
|
||||
this->ui_.replyLabel->setFont(
|
||||
app->fonts->getFont(FontStyle::ChatMediumBold, this->scale()));
|
||||
}
|
||||
|
||||
void SplitInput::themeChangedEvent()
|
||||
@@ -155,6 +202,7 @@ void SplitInput::themeChangedEvent()
|
||||
#endif
|
||||
|
||||
this->updateEmoteButton();
|
||||
this->updateCancelReplyButton();
|
||||
this->ui_.textEditLength->setPalette(palette);
|
||||
|
||||
this->ui_.textEdit->setStyleSheet(this->theme->splits.input.styleSheet);
|
||||
@@ -162,10 +210,19 @@ void SplitInput::themeChangedEvent()
|
||||
this->ui_.textEdit->setPalette(placeholderPalette);
|
||||
#endif
|
||||
|
||||
this->ui_.hbox->setMargin(
|
||||
this->ui_.vbox->setMargin(
|
||||
int((this->theme->isLightTheme() ? 4 : 2) * this->scale()));
|
||||
|
||||
this->ui_.emoteButton->getLabel().setStyleSheet("color: #000");
|
||||
|
||||
if (this->theme->isLightTheme())
|
||||
{
|
||||
this->ui_.replyLabel->setStyleSheet("color: #333");
|
||||
}
|
||||
else
|
||||
{
|
||||
this->ui_.replyLabel->setStyleSheet("color: #ccc");
|
||||
}
|
||||
}
|
||||
|
||||
void SplitInput::updateEmoteButton()
|
||||
@@ -184,6 +241,24 @@ void SplitInput::updateEmoteButton()
|
||||
this->ui_.emoteButton->setFixedHeight(int(18 * scale));
|
||||
}
|
||||
|
||||
void SplitInput::updateCancelReplyButton()
|
||||
{
|
||||
float scale = this->scale();
|
||||
|
||||
QString text =
|
||||
QStringLiteral(
|
||||
"<img src=':/buttons/cancel.svg' width='%1' height='%1' />")
|
||||
.arg(QString::number(int(12 * scale)));
|
||||
|
||||
if (this->theme->isLightTheme())
|
||||
{
|
||||
text.replace("cancel", "cancelDark");
|
||||
}
|
||||
|
||||
this->ui_.cancelReplyButton->getLabel().setText(text);
|
||||
this->ui_.cancelReplyButton->setFixedHeight(int(12 * scale));
|
||||
}
|
||||
|
||||
void SplitInput::openEmotePopup()
|
||||
{
|
||||
if (!this->emotePopup_)
|
||||
@@ -217,6 +292,79 @@ void SplitInput::openEmotePopup()
|
||||
this->emotePopup_->activateWindow();
|
||||
}
|
||||
|
||||
QString SplitInput::handleSendMessage(std::vector<QString> &arguments)
|
||||
{
|
||||
auto c = this->split_->getChannel();
|
||||
if (c == nullptr)
|
||||
return "";
|
||||
|
||||
if (!c->isTwitchChannel() || this->replyThread_ == nullptr)
|
||||
{
|
||||
// standard message send behavior
|
||||
QString message = ui_.textEdit->toPlainText();
|
||||
|
||||
message = message.replace('\n', ' ');
|
||||
QString sendMessage =
|
||||
getApp()->commands->execCommand(message, c, false);
|
||||
|
||||
c->sendMessage(sendMessage);
|
||||
|
||||
this->postMessageSend(message, arguments);
|
||||
return "";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reply to message
|
||||
auto tc = dynamic_cast<TwitchChannel *>(c.get());
|
||||
if (!tc)
|
||||
{
|
||||
// this should not fail
|
||||
return "";
|
||||
}
|
||||
|
||||
QString message = this->ui_.textEdit->toPlainText();
|
||||
|
||||
if (this->enableInlineReplying_)
|
||||
{
|
||||
// Remove @username prefix that is inserted when doing inline replies
|
||||
message.remove(0, this->replyThread_->root()->displayName.length() +
|
||||
1); // remove "@username"
|
||||
|
||||
if (!message.isEmpty() && message.at(0) == ' ')
|
||||
{
|
||||
message.remove(0, 1); // remove possible space
|
||||
}
|
||||
}
|
||||
|
||||
message = message.replace('\n', ' ');
|
||||
QString sendMessage =
|
||||
getApp()->commands->execCommand(message, c, false);
|
||||
|
||||
// Reply within TwitchChannel
|
||||
tc->sendReply(sendMessage, this->replyThread_->rootId());
|
||||
|
||||
this->postMessageSend(message, arguments);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
void SplitInput::postMessageSend(const QString &message,
|
||||
const std::vector<QString> &arguments)
|
||||
{
|
||||
// don't add duplicate messages and empty message to message history
|
||||
if ((this->prevMsg_.isEmpty() || !this->prevMsg_.endsWith(message)) &&
|
||||
!message.trimmed().isEmpty())
|
||||
{
|
||||
this->prevMsg_.append(message);
|
||||
}
|
||||
|
||||
if (arguments.empty() || arguments.at(0) != "keepInput")
|
||||
{
|
||||
this->clearInput();
|
||||
}
|
||||
this->prevIndex_ = this->prevMsg_.size();
|
||||
}
|
||||
|
||||
int SplitInput::scaledMaxHeight() const
|
||||
{
|
||||
return int(150 * this->scale());
|
||||
@@ -302,37 +450,7 @@ void SplitInput::addShortcuts()
|
||||
}},
|
||||
{"sendMessage",
|
||||
[this](std::vector<QString> arguments) -> QString {
|
||||
auto c = this->split_->getChannel();
|
||||
if (c == nullptr)
|
||||
return "";
|
||||
|
||||
QString message = ui_.textEdit->toPlainText();
|
||||
|
||||
message = message.replace('\n', ' ');
|
||||
QString sendMessage =
|
||||
getApp()->commands->execCommand(message, c, false);
|
||||
|
||||
c->sendMessage(sendMessage);
|
||||
// don't add duplicate messages and empty message to message history
|
||||
if ((this->prevMsg_.isEmpty() ||
|
||||
!this->prevMsg_.endsWith(message)) &&
|
||||
!message.trimmed().isEmpty())
|
||||
{
|
||||
this->prevMsg_.append(message);
|
||||
}
|
||||
bool shouldClearInput = true;
|
||||
if (arguments.size() != 0 && arguments.at(0) == "keepInput")
|
||||
{
|
||||
shouldClearInput = false;
|
||||
}
|
||||
|
||||
if (shouldClearInput)
|
||||
{
|
||||
this->currMsg_ = QString();
|
||||
this->ui_.textEdit->setPlainText(QString());
|
||||
}
|
||||
this->prevIndex_ = this->prevMsg_.size();
|
||||
return "";
|
||||
return this->handleSendMessage(arguments);
|
||||
}},
|
||||
{"previousMessage",
|
||||
[this](std::vector<QString>) -> QString {
|
||||
@@ -456,8 +574,7 @@ void SplitInput::addShortcuts()
|
||||
}},
|
||||
{"clear",
|
||||
[this](std::vector<QString>) -> QString {
|
||||
this->ui_.textEdit->setText("");
|
||||
this->ui_.textEdit->moveCursor(QTextCursor::Start);
|
||||
this->clearInput();
|
||||
return "";
|
||||
}},
|
||||
{"selectAll",
|
||||
@@ -769,37 +886,71 @@ void SplitInput::editTextChanged()
|
||||
}
|
||||
|
||||
this->ui_.textEditLength->setText(labelText);
|
||||
|
||||
bool hasReply = false;
|
||||
if (this->enableInlineReplying_)
|
||||
{
|
||||
if (this->replyThread_ != nullptr)
|
||||
{
|
||||
// Check if the input still starts with @username. If not, don't reply.
|
||||
//
|
||||
// We need to verify that
|
||||
// 1. the @username prefix exists and
|
||||
// 2. if a character exists after the @username, it is a space
|
||||
QString replyPrefix = "@" + this->replyThread_->root()->displayName;
|
||||
if (!text.startsWith(replyPrefix) ||
|
||||
(text.length() > replyPrefix.length() &&
|
||||
text.at(replyPrefix.length()) != ' '))
|
||||
{
|
||||
this->replyThread_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide reply label if inline replies are possible
|
||||
hasReply = this->replyThread_ != nullptr;
|
||||
}
|
||||
|
||||
this->ui_.replyWrapper->setVisible(hasReply);
|
||||
this->ui_.replyLabel->setVisible(hasReply);
|
||||
this->ui_.cancelReplyButton->setVisible(hasReply);
|
||||
}
|
||||
|
||||
void SplitInput::paintEvent(QPaintEvent * /*event*/)
|
||||
{
|
||||
QPainter painter(this);
|
||||
|
||||
int s;
|
||||
QColor borderColor;
|
||||
|
||||
if (this->theme->isLightTheme())
|
||||
{
|
||||
int s = int(3 * this->scale());
|
||||
QRect rect = this->rect().marginsRemoved(QMargins(s - 1, s - 1, s, s));
|
||||
|
||||
painter.fillRect(rect, this->theme->splits.input.background);
|
||||
|
||||
painter.setPen(QColor("#ccc"));
|
||||
painter.drawRect(rect);
|
||||
s = int(3 * this->scale());
|
||||
borderColor = QColor("#ccc");
|
||||
}
|
||||
else
|
||||
{
|
||||
int s = int(1 * this->scale());
|
||||
QRect rect = this->rect().marginsRemoved(QMargins(s - 1, s - 1, s, s));
|
||||
|
||||
painter.fillRect(rect, this->theme->splits.input.background);
|
||||
|
||||
painter.setPen(QColor("#333"));
|
||||
painter.drawRect(rect);
|
||||
s = int(1 * this->scale());
|
||||
borderColor = QColor("#333");
|
||||
}
|
||||
|
||||
// int offset = 2;
|
||||
// painter.fillRect(offset, this->height() - offset, this->width() - 2 *
|
||||
// offset, 1,
|
||||
// getApp()->themes->splits.input.focusedLine);
|
||||
QMargins removeMargins(s - 1, s - 1, s, s);
|
||||
QRect baseRect = this->rect();
|
||||
|
||||
// completeAreaRect includes the reply label
|
||||
QRect completeAreaRect = baseRect.marginsRemoved(removeMargins);
|
||||
painter.fillRect(completeAreaRect, this->theme->splits.input.background);
|
||||
painter.setPen(borderColor);
|
||||
painter.drawRect(completeAreaRect);
|
||||
|
||||
if (this->enableInlineReplying_ && this->replyThread_ != nullptr)
|
||||
{
|
||||
// Move top of rect down to not include reply label
|
||||
baseRect.setTop(baseRect.top() + this->ui_.replyWrapper->height());
|
||||
|
||||
QRect onlyInputRect = baseRect.marginsRemoved(removeMargins);
|
||||
painter.setPen(borderColor);
|
||||
painter.drawRect(onlyInputRect);
|
||||
}
|
||||
}
|
||||
|
||||
void SplitInput::resizeEvent(QResizeEvent *)
|
||||
@@ -814,4 +965,41 @@ void SplitInput::resizeEvent(QResizeEvent *)
|
||||
}
|
||||
}
|
||||
|
||||
void SplitInput::giveFocus(Qt::FocusReason reason)
|
||||
{
|
||||
this->ui_.textEdit->setFocus(reason);
|
||||
}
|
||||
|
||||
void SplitInput::setReply(std::shared_ptr<MessageThread> reply,
|
||||
bool showReplyingLabel)
|
||||
{
|
||||
this->replyThread_ = std::move(reply);
|
||||
|
||||
if (this->enableInlineReplying_)
|
||||
{
|
||||
// Only enable reply label if inline replying
|
||||
this->ui_.textEdit->setPlainText(
|
||||
"@" + this->replyThread_->root()->displayName + " ");
|
||||
this->ui_.textEdit->moveCursor(QTextCursor::EndOfBlock);
|
||||
this->ui_.replyLabel->setText("Replying to @" +
|
||||
this->replyThread_->root()->displayName);
|
||||
}
|
||||
}
|
||||
|
||||
void SplitInput::setPlaceholderText(const QString &text)
|
||||
{
|
||||
this->ui_.textEdit->setPlaceholderText(text);
|
||||
}
|
||||
|
||||
void SplitInput::clearInput()
|
||||
{
|
||||
this->currMsg_ = "";
|
||||
this->ui_.textEdit->setText("");
|
||||
this->ui_.textEdit->moveCursor(QTextCursor::Start);
|
||||
if (this->enableInlineReplying_)
|
||||
{
|
||||
this->replyThread_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
Reference in New Issue
Block a user