feat: add Go to message action in various places (#3953)

* feat: add `Go to message` action in search popup

* chore: add changelog entry

* fix: only scroll if the scrollbar is shown

* fix: go to message when view isn't focused

* feat: animate highlighted message

* fix: missing includes

* fix: order of initialization

* fix: add `ChannelView::mayContainMessage` to filter messages

* feat: add `Go to message` action in `/mentions`

* fix: ignore any mentions channel when searching for split

* feat: add `Go to message` action in reply-threads

* fix: remove redundant `source` parameter

* feat: add `Go to message` action in user-cards

* feat: add link to deleted message

* fix: set current time to 0 when starting animation

* chore: update changelog

* fix: add default case (unreachable)

* chore: removed unused variable

* fix: search in mentions

* fix: always attempt to focus split

* fix: rename `Link::MessageId` to `Link::JumpToMessage`

* fix: rename `selectAndScrollToMessage` to `scrollToMessage`

* fix: rename internal `scrollToMessage` to `scrollToMessageLayout`

* fix: deleted message link in search popup

* chore: reword explanation

* fix: use for-loop instead of `std::find_if`

* refactor: define highlight colors in `BaseTheme`

* core: replace `iff` with `if`

* fix: only return if the message found

* Reword/phrase/dot changelog entries

Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
nerix 2022-09-11 16:37:13 +02:00 committed by GitHub
parent 5655a7d718
commit be72d73c3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 344 additions and 12 deletions

View file

@ -32,6 +32,8 @@
- Minor: Add settings to toggle BTTV/FFZ global/channel emotes (#3935)
- Minor: Add AutoMod message flag filter. (#3938)
- Minor: Added whitespace trim to username field in nicknames (#3946)
- Minor: Added `Go to message` context menu action to search popup, mentions, usercard and reply threads. (#3953)
- Minor: Added link back to original message that was deleted. (#3953)
- Bugfix: Fix crash that can occur when closing and quickly reopening a split, then running a command. (#3852)
- Bugfix: Connection to Twitch PubSub now recovers more reliably. (#3643, #3716)
- Bugfix: Fix crash that can occur when changing channels. (#3799)

View file

@ -168,6 +168,12 @@ void AB_THEME_CLASS::actuallyUpdate(double hue, double multiplier)
// this->messages.seperator =
// this->messages.seperatorInner =
int complementaryGray = this->isLightTheme() ? 20 : 230;
this->messages.highlightAnimationStart =
QColor(complementaryGray, complementaryGray, complementaryGray, 110);
this->messages.highlightAnimationEnd =
QColor(complementaryGray, complementaryGray, complementaryGray, 0);
// Scrollbar
this->scrollbars.background = QColor(0, 0, 0, 0);
// this->scrollbars.background = splits.background;

View file

@ -74,6 +74,9 @@ public:
// QColor seperator;
// QColor seperatorInner;
QColor selection;
QColor highlightAnimationStart;
QColor highlightAnimationEnd;
} messages;
/// SCROLLBAR

View file

@ -25,6 +25,7 @@ public:
CopyToClipboard,
ReplyToMessage,
ViewThread,
JumpToMessage,
};
Link();

View file

@ -66,6 +66,11 @@ int MessageLayout::getHeight() const
return container_->getHeight();
}
int MessageLayout::getWidth() const
{
return this->container_->getWidth();
}
// Layout
// return true if redraw is required
bool MessageLayout::layout(int width, float scale, MessageElementFlags flags)

View file

@ -40,6 +40,7 @@ public:
const MessagePtr &getMessagePtr() const;
int getHeight() const;
int getWidth() const;
MessageLayoutFlags flags;

View file

@ -1471,15 +1471,17 @@ void TwitchMessageBuilder::deletionMessage(const MessagePtr originalMessage,
MessageColor::System);
if (originalMessage->messageText.length() > 50)
{
builder->emplace<TextElement>(
originalMessage->messageText.left(50) + "",
MessageElementFlag::Text, MessageColor::Text);
builder
->emplace<TextElement>(originalMessage->messageText.left(50) + "",
MessageElementFlag::Text, MessageColor::Text)
->setLink({Link::JumpToMessage, originalMessage->id});
}
else
{
builder->emplace<TextElement>(originalMessage->messageText,
MessageElementFlag::Text,
MessageColor::Text);
builder
->emplace<TextElement>(originalMessage->messageText,
MessageElementFlag::Text, MessageColor::Text)
->setLink({Link::JumpToMessage, originalMessage->id});
}
builder->message().timeoutUser = "msg:" + originalMessage->id;
}
@ -1511,14 +1513,17 @@ void TwitchMessageBuilder::deletionMessage(const DeleteAction &action,
MessageColor::System);
if (action.messageText.length() > 50)
{
builder->emplace<TextElement>(action.messageText.left(50) + "",
MessageElementFlag::Text,
MessageColor::Text);
builder
->emplace<TextElement>(action.messageText.left(50) + "",
MessageElementFlag::Text, MessageColor::Text)
->setLink({Link::JumpToMessage, action.messageId});
}
else
{
builder->emplace<TextElement>(
action.messageText, MessageElementFlag::Text, MessageColor::Text);
builder
->emplace<TextElement>(action.messageText, MessageElementFlag::Text,
MessageColor::Text)
->setLink({Link::JumpToMessage, action.messageId});
}
builder->message().timeoutUser = "msg:" + action.messageId;
}

View file

@ -320,6 +320,11 @@ void WindowManager::select(SplitContainer *container)
this->selectSplitContainer.invoke(container);
}
void WindowManager::scrollToMessage(const MessagePtr &message)
{
this->scrollToMessageSignal.invoke(message);
}
QPoint WindowManager::emotePopupPos()
{
return this->emotePopupPos_;

View file

@ -15,6 +15,7 @@ class Settings;
class Paths;
class Window;
class SplitContainer;
class ChannelView;
enum class MessageElementFlag : int64_t;
using MessageElementFlags = FlagsEnum<MessageElementFlag>;
@ -66,6 +67,13 @@ public:
void select(Split *split);
void select(SplitContainer *container);
/**
* Scrolls to the message in a split that's not
* a mentions view and focuses the split.
*
* @param message Message to scroll to.
*/
void scrollToMessage(const MessagePtr &message);
QPoint emotePopupPos();
void setEmotePopupPos(QPoint pos);
@ -105,6 +113,7 @@ public:
pajlada::Signals::Signal<Split *> selectSplit;
pajlada::Signals::Signal<SplitContainer *> selectSplitContainer;
pajlada::Signals::Signal<const MessagePtr &> scrollToMessageSignal;
private:
static void encodeNodeRecursively(SplitContainer::Node *node,

View file

@ -8,6 +8,7 @@
#include "util/InitUpdateButton.hpp"
#include "widgets/Window.hpp"
#include "widgets/dialogs/SettingsDialog.hpp"
#include "widgets/helper/ChannelView.hpp"
#include "widgets/helper/NotebookButton.hpp"
#include "widgets/helper/NotebookTab.hpp"
#include "widgets/splits/Split.hpp"
@ -1006,6 +1007,29 @@ SplitNotebook::SplitNotebook(Window *parent)
[this](SplitContainer *sc) {
this->select(sc);
});
this->signalHolder_.managedConnect(
getApp()->windows->scrollToMessageSignal,
[this](const MessagePtr &message) {
for (auto &&item : this->items())
{
if (auto sc = dynamic_cast<SplitContainer *>(item.page))
{
for (auto *split : sc->getSplits())
{
if (split->getChannel()->getType() !=
Channel::Type::TwitchMentions)
{
if (split->getChannelView().scrollToMessage(
message))
{
return;
}
}
}
}
}
});
}
void SplitNotebook::showEvent(QShowEvent *)

View file

@ -1,13 +1,16 @@
#include "ChannelView.hpp"
#include <QClipboard>
#include <QColor>
#include <QDate>
#include <QDebug>
#include <QDesktopServices>
#include <QEasingCurve>
#include <QGraphicsBlurEffect>
#include <QMessageBox>
#include <QPainter>
#include <QScreen>
#include <QVariantAnimation>
#include <algorithm>
#include <chrono>
#include <cmath>
@ -118,12 +121,23 @@ namespace {
addPageLink("FFZ");
}
}
// Current function: https://www.desmos.com/calculator/vdyamchjwh
qreal highlightEasingFunction(qreal progress)
{
if (progress <= 0.1)
{
return 1.0 - pow(10.0 * progress, 3.0);
}
return 1.0 + pow((20.0 / 9.0) * (0.5 * progress - 0.5), 3.0);
}
} // namespace
ChannelView::ChannelView(BaseWidget *parent, Split *split, Context context)
: BaseWidget(parent)
, split_(split)
, scrollBar_(new Scrollbar(this))
, highlightAnimation_(this)
, context_(context)
{
this->setMouseTracking(true);
@ -164,6 +178,12 @@ ChannelView::ChannelView(BaseWidget *parent, Split *split, Context context)
// of any place where you can, or where it would make sense,
// to tab to a ChannelVieChannelView
this->setFocusPolicy(Qt::FocusPolicy::ClickFocus);
this->setupHighlightAnimationColors();
this->highlightAnimation_.setDuration(1500);
auto curve = QEasingCurve();
curve.setCustomType(highlightEasingFunction);
this->highlightAnimation_.setEasingCurve(curve);
}
void ChannelView::initializeLayout()
@ -339,9 +359,18 @@ void ChannelView::themeChangedEvent()
{
BaseWidget::themeChangedEvent();
this->setupHighlightAnimationColors();
this->queueLayout();
}
void ChannelView::setupHighlightAnimationColors()
{
this->highlightAnimation_.setStartValue(
this->theme->messages.highlightAnimationStart);
this->highlightAnimation_.setEndValue(
this->theme->messages.highlightAnimationEnd);
}
void ChannelView::scaleChangedEvent(float scale)
{
BaseWidget::scaleChangedEvent(scale);
@ -392,7 +421,8 @@ void ChannelView::performLayout(bool causedByScrollbar)
auto &messages = this->getMessagesSnapshot();
this->showingLatestMessages_ =
this->scrollBar_->isAtBottom() || !this->scrollBar_->isVisible();
this->scrollBar_->isAtBottom() ||
(!this->scrollBar_->isVisible() && !causedByScrollbar);
/// Layout visible messages
this->layoutVisibleMessages(messages);
@ -475,6 +505,7 @@ void ChannelView::updateScrollbar(
{
this->scrollBar_->setDesiredValue(0);
}
this->showScrollBar_ = showScrollbar;
this->scrollBar_->setMaximum(messages.size());
@ -1088,6 +1119,86 @@ MessageElementFlags ChannelView::getFlags() const
return flags;
}
bool ChannelView::scrollToMessage(const MessagePtr &message)
{
if (!this->mayContainMessage(message))
{
return false;
}
auto &messagesSnapshot = this->getMessagesSnapshot();
if (messagesSnapshot.size() == 0)
{
return false;
}
// TODO: Figure out if we can somehow binary-search here.
// Currently, a message only sometimes stores a QDateTime,
// but always a QTime (inaccurate on midnight).
//
// We're searching from the bottom since it's more likely for a user
// wanting to go to a message that recently scrolled out of view.
size_t messageIdx = messagesSnapshot.size() - 1;
for (; messageIdx < SIZE_MAX; messageIdx--)
{
if (messagesSnapshot[messageIdx]->getMessagePtr() == message)
{
break;
}
}
if (messageIdx == SIZE_MAX)
{
return false;
}
this->scrollToMessageLayout(messagesSnapshot[messageIdx].get(), messageIdx);
getApp()->windows->select(this->split_);
return true;
}
bool ChannelView::scrollToMessageId(const QString &messageId)
{
auto &messagesSnapshot = this->getMessagesSnapshot();
if (messagesSnapshot.size() == 0)
{
return false;
}
// We're searching from the bottom since it's more likely for a user
// wanting to go to a message that recently scrolled out of view.
size_t messageIdx = messagesSnapshot.size() - 1;
for (; messageIdx < SIZE_MAX; messageIdx--)
{
if (messagesSnapshot[messageIdx]->getMessagePtr()->id == messageId)
{
break;
}
}
if (messageIdx == SIZE_MAX)
{
return false;
}
this->scrollToMessageLayout(messagesSnapshot[messageIdx].get(), messageIdx);
getApp()->windows->select(this->split_);
return true;
}
void ChannelView::scrollToMessageLayout(MessageLayout *layout,
size_t messageIdx)
{
this->highlightedMessage_ = layout;
this->highlightAnimation_.setCurrentTime(0);
this->highlightAnimation_.start(QAbstractAnimation::KeepWhenStopped);
if (this->showScrollBar_)
{
this->getScrollBar().setDesiredValue(messageIdx);
}
}
void ChannelView::paintEvent(QPaintEvent * /*event*/)
{
// BenchmarkGuard benchmark("paint");
@ -1144,6 +1255,17 @@ void ChannelView::drawMessages(QPainter &painter)
layout->paint(painter, DRAW_WIDTH, y, i, this->selection_,
isLastMessage, windowFocused, isMentions);
if (this->highlightedMessage_ == layout)
{
painter.fillRect(
0, y, layout->getWidth(), layout->getHeight(),
this->highlightAnimation_.currentValue().value<QColor>());
if (this->highlightAnimation_.state() == QVariantAnimation::Stopped)
{
this->highlightedMessage_ = nullptr;
}
}
y += layout->getHeight();
end = layout;
@ -2070,6 +2192,46 @@ void ChannelView::addMessageContextMenuItems(
});
}
}
bool isSearch = this->context_ == Context::Search;
bool isReplyOrUserCard = (this->context_ == Context::ReplyThread ||
this->context_ == Context::UserCard) &&
this->split_;
bool isMentions =
this->channel()->getType() == Channel::Type::TwitchMentions;
if (isSearch || isMentions || isReplyOrUserCard)
{
const auto &messagePtr = layout->getMessagePtr();
menu.addAction("Go to message", [this, &messagePtr, isSearch,
isMentions, isReplyOrUserCard] {
if (isSearch)
{
if (const auto &search =
dynamic_cast<SearchPopup *>(this->parentWidget()))
{
search->goToMessage(messagePtr);
}
}
else if (isMentions)
{
getApp()->windows->scrollToMessage(messagePtr);
}
else if (isReplyOrUserCard)
{
// If the thread is in the mentions channel,
// we need to find the original split.
if (this->split_->getChannel()->getType() ==
Channel::Type::TwitchMentions)
{
getApp()->windows->scrollToMessage(messagePtr);
}
else
{
this->split_->getChannelView().scrollToMessage(messagePtr);
}
}
});
}
}
void ChannelView::addTwitchLinkContextMenuItems(
@ -2321,6 +2483,30 @@ void ChannelView::showUserInfoPopup(const QString &userName,
userPopup->show();
}
bool ChannelView::mayContainMessage(const MessagePtr &message)
{
switch (this->channel()->getType())
{
case Channel::Type::Direct:
case Channel::Type::Twitch:
case Channel::Type::TwitchWatching:
case Channel::Type::Irc:
return this->channel()->getName() == message->channelName;
case Channel::Type::TwitchWhispers:
return message->flags.has(MessageFlag::Whisper);
case Channel::Type::TwitchMentions:
return message->flags.has(MessageFlag::Highlighted);
case Channel::Type::TwitchLive:
return message->flags.has(MessageFlag::System);
case Channel::Type::TwitchEnd: // TODO: not used?
case Channel::Type::None: // Unspecific
case Channel::Type::Misc: // Unspecific
return true;
default:
return true; // unreachable
}
}
void ChannelView::handleLinkClick(QMouseEvent *event, const Link &link,
MessageLayout *layout)
{
@ -2442,6 +2628,21 @@ void ChannelView::handleLinkClick(QMouseEvent *event, const Link &link,
this->showReplyThreadPopup(layout->getMessagePtr());
}
break;
case Link::JumpToMessage: {
if (this->context_ == Context::Search)
{
if (auto search =
dynamic_cast<SearchPopup *>(this->parentWidget()))
{
search->goToMessageId(link.value);
}
}
else
{
this->scrollToMessageId(link.value);
}
}
break;
default:;
}

View file

@ -3,6 +3,7 @@
#include <QPaintEvent>
#include <QScroller>
#include <QTimer>
#include <QVariantAnimation>
#include <QWheelEvent>
#include <QWidget>
#include <pajlada/signals/signal.hpp>
@ -83,6 +84,17 @@ public:
const boost::optional<MessageElementFlags> &getOverrideFlags() const;
void updateLastReadMessage();
/**
* Attempts to scroll to a message in this channel.
* @return <code>true</code> if the message was found and highlighted.
*/
bool scrollToMessage(const MessagePtr &message);
/**
* Attempts to scroll to a message id in this channel.
* @return <code>true</code> if the message was found and highlighted.
*/
bool scrollToMessageId(const QString &id);
/// Pausing
bool pausable() const;
void setPausable(bool value);
@ -119,6 +131,13 @@ public:
void showUserInfoPopup(const QString &userName,
QString alternativePopoutChannel = QString());
/**
* @brief This method is meant to be used when filtering out channels.
* It <b>must</b> return true if a message belongs in this channel.
* It <b>might</b> return true if a message doesn't belong in this channel.
*/
bool mayContainMessage(const MessagePtr &message);
pajlada::Signals::Signal<QMouseEvent *> mouseDown;
pajlada::Signals::NoArgSignal selectionChanged;
pajlada::Signals::Signal<HighlightState> tabHighlightRequested;
@ -208,6 +227,14 @@ private:
void enableScrolling(const QPointF &scrollStart);
void disableScrolling();
/**
* Scrolls to a message layout that must be from this view.
*
* @param layout Must be from this channel.
* @param messageIdx Must be an index into this channel.
*/
void scrollToMessageLayout(MessageLayout *layout, size_t messageIdx);
void setInputReply(const MessagePtr &message);
void showReplyThreadPopup(const MessagePtr &message);
bool canReplyToMessages() const;
@ -241,6 +268,7 @@ private:
Scrollbar *scrollBar_;
EffectLabel *goToBottom_;
bool showScrollBar_ = false;
FilterSetPtr channelFilters_;
@ -272,6 +300,11 @@ private:
QPointF currentMousePosition_;
QTimer scrollTimer_;
// We're only interested in the pointer, not the contents
MessageLayout *highlightedMessage_;
QVariantAnimation highlightAnimation_;
void setupHighlightAnimationColors();
struct {
QCursor neutral;
QCursor up;

View file

@ -12,6 +12,7 @@
#include "messages/search/MessageFlagsPredicate.hpp"
#include "messages/search/RegexPredicate.hpp"
#include "messages/search/SubstringPredicate.hpp"
#include "singletons/WindowManager.hpp"
#include "widgets/helper/ChannelView.hpp"
namespace chatterino {
@ -106,6 +107,34 @@ void SearchPopup::addChannel(ChannelView &channel)
this->updateWindowTitle();
}
void SearchPopup::goToMessage(const MessagePtr &message)
{
for (const auto &view : this->searchChannels_)
{
if (view.get().channel()->getType() == Channel::Type::TwitchMentions)
{
getApp()->windows->scrollToMessage(message);
return;
}
if (view.get().scrollToMessage(message))
{
return;
}
}
}
void SearchPopup::goToMessageId(const QString &messageId)
{
for (const auto &view : this->searchChannels_)
{
if (view.get().scrollToMessageId(messageId))
{
return;
}
}
}
void SearchPopup::updateWindowTitle()
{
QString historyName;

View file

@ -19,6 +19,14 @@ public:
SearchPopup(QWidget *parent, Split *split = nullptr);
virtual void addChannel(ChannelView &channel);
void goToMessage(const MessagePtr &message);
/**
* This method should only be used for searches that
* don't include a mentions channel,
* since it will only search in the opened channels (not globally).
* @param messageId
*/
void goToMessageId(const QString &messageId);
protected:
virtual void updateWindowTitle();