diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6f8c167c..c15093ad 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -171,6 +171,7 @@ set(SRC_FILES
src/ui/Badge.cc
src/ui/LoadingIndicator.cc
src/ui/FlatButton.cc
+ src/ui/FloatingButton.cc
src/ui/Label.cc
src/ui/OverlayModal.cc
src/ui/ScrollBar.cc
@@ -224,6 +225,7 @@ qt5_wrap_cpp(MOC_HEADERS
include/EmojiItemDelegate.h
include/EmojiPanel.h
include/EmojiPickButton.h
+ include/ui/FloatingButton.h
include/ImageItem.h
include/ImageOverlayDialog.h
include/JoinRoomDialog.h
diff --git a/include/TimelineView.h b/include/TimelineView.h
index 400b0db0..83247948 100644
--- a/include/TimelineView.h
+++ b/include/TimelineView.h
@@ -34,6 +34,8 @@
#include "RoomInfoListItem.h"
#include "Text.h"
+class FloatingButton;
+
namespace msgs = matrix::events::messages;
namespace events = matrix::events;
@@ -155,6 +157,8 @@ private:
int oldPosition_;
int oldHeight_;
+ FloatingButton *scrollDownBtn_;
+
TimelineDirection lastMessageDirection_;
// The events currently rendered. Used for duplicate detection.
diff --git a/include/ui/FloatingButton.h b/include/ui/FloatingButton.h
new file mode 100644
index 00000000..91e99ebb
--- /dev/null
+++ b/include/ui/FloatingButton.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "RaisedButton.h"
+
+constexpr int DIAMETER = 40;
+constexpr int ICON_SIZE = 18;
+
+constexpr int OFFSET_X = 30;
+constexpr int OFFSET_Y = 20;
+
+class FloatingButton : public RaisedButton
+{
+ Q_OBJECT
+
+public:
+ FloatingButton(const QIcon &icon, QWidget *parent = nullptr);
+
+ QSize sizeHint() const override { return QSize(DIAMETER, DIAMETER); };
+ QRect buttonGeometry() const;
+
+protected:
+ bool event(QEvent *event) override;
+ bool eventFilter(QObject *obj, QEvent *event) override;
+
+ void paintEvent(QPaintEvent *event) override;
+};
diff --git a/resources/icons/ui/angle-arrow-down.png b/resources/icons/ui/angle-arrow-down.png
new file mode 100644
index 00000000..e40ebca5
Binary files /dev/null and b/resources/icons/ui/angle-arrow-down.png differ
diff --git a/resources/icons/ui/angle-arrow-down@2x.png b/resources/icons/ui/angle-arrow-down@2x.png
new file mode 100644
index 00000000..ed095bfe
Binary files /dev/null and b/resources/icons/ui/angle-arrow-down@2x.png differ
diff --git a/resources/res.qrc b/resources/res.qrc
index 59d6559d..55962275 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -22,6 +22,8 @@
icons/ui/paper-clip-outline@2x.png
icons/ui/angle-pointing-to-left.png
icons/ui/angle-pointing-to-left@2x.png
+ icons/ui/angle-arrow-down.png
+ icons/ui/angle-arrow-down@2x.png
icons/emoji-categories/people.png
icons/emoji-categories/people@2x.png
diff --git a/src/TimelineView.cc b/src/TimelineView.cc
index 2142f546..13209062 100644
--- a/src/TimelineView.cc
+++ b/src/TimelineView.cc
@@ -27,6 +27,7 @@
#include "MessageEvent.h"
#include "MessageEventContent.h"
+#include "FloatingButton.h"
#include "ImageItem.h"
#include "TimelineItem.h"
#include "TimelineView.h"
@@ -140,6 +141,16 @@ TimelineView::sliderMoved(int position)
if (!scroll_area_->verticalScrollBar()->isVisible())
return;
+ const int maxScroll = scroll_area_->verticalScrollBar()->maximum();
+ const int currentScroll = scroll_area_->verticalScrollBar()->value();
+
+ if (maxScroll - currentScroll > SCROLL_BAR_GAP) {
+ scrollDownBtn_->show();
+ scrollDownBtn_->raise();
+ } else {
+ scrollDownBtn_->hide();
+ }
+
// The scrollbar is high enough so we can start retrieving old events.
if (position < SCROLL_BAR_GAP) {
if (isTimelineFinished)
@@ -376,6 +387,18 @@ TimelineView::init()
QSettings settings;
local_user_ = settings.value("auth/user_id").toString();
+ QIcon icon;
+ icon.addFile(":/icons/icons/ui/angle-arrow-down.png");
+ scrollDownBtn_ = new FloatingButton(icon, this);
+ scrollDownBtn_->setBackgroundColor(QColor("#F5F5F5"));
+ scrollDownBtn_->setForegroundColor(QColor("black"));
+ scrollDownBtn_->hide();
+
+ connect(scrollDownBtn_, &QPushButton::clicked, this, [=]() {
+ const int max = scroll_area_->verticalScrollBar()->maximum();
+ scroll_area_->verticalScrollBar()->setValue(max);
+ });
+
top_layout_ = new QVBoxLayout(this);
top_layout_->setSpacing(0);
top_layout_->setMargin(0);
diff --git a/src/ui/FloatingButton.cc b/src/ui/FloatingButton.cc
new file mode 100644
index 00000000..74dcd482
--- /dev/null
+++ b/src/ui/FloatingButton.cc
@@ -0,0 +1,95 @@
+#include
+
+#include "FloatingButton.h"
+
+FloatingButton::FloatingButton(const QIcon &icon, QWidget *parent)
+ : RaisedButton(parent)
+{
+ setFixedSize(DIAMETER, DIAMETER);
+ setGeometry(buttonGeometry());
+
+ if (parentWidget())
+ parentWidget()->installEventFilter(this);
+
+ setFixedRippleRadius(50);
+ setIcon(icon);
+ raise();
+}
+
+QRect
+FloatingButton::buttonGeometry() const
+{
+ QWidget *parent = parentWidget();
+
+ if (!parent)
+ return QRect();
+
+ return QRect(parent->width() - (OFFSET_X + DIAMETER),
+ parent->height() - (OFFSET_Y + DIAMETER),
+ DIAMETER,
+ DIAMETER);
+}
+
+bool
+FloatingButton::event(QEvent *event)
+{
+ if (!parent())
+ return RaisedButton::event(event);
+
+ switch (event->type()) {
+ case QEvent::ParentChange: {
+ parent()->installEventFilter(this);
+ setGeometry(buttonGeometry());
+ break;
+ }
+ case QEvent::ParentAboutToChange: {
+ parent()->installEventFilter(this);
+ break;
+ }
+ default:
+ break;
+ }
+
+ return RaisedButton::event(event);
+}
+
+bool
+FloatingButton::eventFilter(QObject *obj, QEvent *event)
+{
+ const QEvent::Type type = event->type();
+
+ if (QEvent::Move == type || QEvent::Resize == type)
+ setGeometry(buttonGeometry());
+
+ return RaisedButton::eventFilter(obj, event);
+}
+
+void
+FloatingButton::paintEvent(QPaintEvent *event)
+{
+ Q_UNUSED(event);
+
+ QRect square = QRect(0, 0, DIAMETER, DIAMETER);
+ square.moveCenter(rect().center());
+
+ QPainter p(this);
+ p.setRenderHints(QPainter::Antialiasing);
+
+ QBrush brush;
+ brush.setStyle(Qt::SolidPattern);
+ brush.setColor(backgroundColor());
+
+ p.setBrush(brush);
+ p.setPen(Qt::NoPen);
+ p.drawEllipse(square);
+
+ QRect iconGeometry(0, 0, ICON_SIZE, ICON_SIZE);
+ iconGeometry.moveCenter(square.center());
+
+ QPixmap pixmap = icon().pixmap(QSize(ICON_SIZE, ICON_SIZE));
+ QPainter icon(&pixmap);
+ icon.setCompositionMode(QPainter::CompositionMode_SourceIn);
+ icon.fillRect(pixmap.rect(), foregroundColor());
+
+ p.drawPixmap(iconGeometry, pixmap);
+}