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
14 changed files with 344 additions and 12 deletions
+202 -1
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:;
}