diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6d1bf0ea..7f9204c9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -91,6 +91,7 @@ set(SRC_FILES
src/MatrixClient.cc
src/Profile.cc
src/RoomInfoListItem.cc
+ src/RoomMessages.cc
src/RoomList.cc
src/RoomState.cc
src/Register.cc
diff --git a/include/MatrixClient.h b/include/MatrixClient.h
index 741294c4..79813c95 100644
--- a/include/MatrixClient.h
+++ b/include/MatrixClient.h
@@ -21,6 +21,7 @@
#include <QtNetwork/QNetworkAccessManager>
#include "Profile.h"
+#include "RoomMessages.h"
#include "Sync.h"
/*
@@ -43,6 +44,7 @@ public:
void fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url);
void fetchOwnAvatar(const QUrl &avatar_url);
void downloadImage(const QString &event_id, const QUrl &url);
+ void messages(const QString &room_id, const QString &from_token) noexcept;
inline QUrl getHomeServer();
inline int transactionId();
@@ -77,19 +79,21 @@ signals:
void syncCompleted(const SyncResponse &response);
void syncFailed(const QString &msg);
void messageSent(const QString &event_id, const QString &roomid, const int txn_id);
+ void messagesRetrieved(const QString &room_id, const RoomMessages &msgs);
private slots:
void onResponse(QNetworkReply *reply);
private:
enum class Endpoint {
- GetOwnProfile,
GetOwnAvatar,
+ GetOwnProfile,
GetProfile,
Image,
InitialSync,
Login,
Logout,
+ Messages,
Register,
RoomAvatar,
SendTextMessage,
@@ -109,6 +113,7 @@ private:
void onSyncResponse(QNetworkReply *reply);
void onRoomAvatarResponse(QNetworkReply *reply);
void onImageResponse(QNetworkReply *reply);
+ void onMessagesResponse(QNetworkReply *reply);
// Client API prefix.
QString api_url_;
diff --git a/include/RoomMessages.h b/include/RoomMessages.h
new file mode 100644
index 00000000..695580b3
--- /dev/null
+++ b/include/RoomMessages.h
@@ -0,0 +1,56 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef ROOM_MESSAGES_H
+#define ROOM_MESSAGES_H
+
+#include <QJsonArray>
+#include <QJsonDocument>
+
+#include "Deserializable.h"
+
+class RoomMessages : public Deserializable
+{
+public:
+ void deserialize(const QJsonDocument &data) override;
+
+ inline QString start() const;
+ inline QString end() const;
+ inline QJsonArray chunk() const;
+
+private:
+ QString start_;
+ QString end_;
+ QJsonArray chunk_;
+};
+
+inline QString RoomMessages::start() const
+{
+ return start_;
+}
+
+inline QString RoomMessages::end() const
+{
+ return end_;
+}
+
+inline QJsonArray RoomMessages::chunk() const
+{
+ return chunk_;
+}
+
+#endif // ROOM_MESSAGES_H
diff --git a/include/TimelineView.h b/include/TimelineView.h
index 1808d735..dbc73bbf 100644
--- a/include/TimelineView.h
+++ b/include/TimelineView.h
@@ -51,32 +51,50 @@ struct PendingMessage {
}
};
+// In which place new TimelineItems should be inserted.
+enum class TimelineDirection {
+ Top,
+ Bottom,
+};
+
class TimelineView : public QWidget
{
Q_OBJECT
public:
- TimelineView(QSharedPointer<MatrixClient> client, QWidget *parent = 0);
- TimelineView(const QJsonArray &events, QSharedPointer<MatrixClient> client, QWidget *parent = 0);
- ~TimelineView();
+ TimelineView(const Timeline &timeline, QSharedPointer<MatrixClient> client, const QString &room_id, QWidget *parent = 0);
- void addHistoryItem(const events::MessageEvent<msgs::Image> &e, const QString &color, bool with_sender);
- void addHistoryItem(const events::MessageEvent<msgs::Notice> &e, const QString &color, bool with_sender);
- void addHistoryItem(const events::MessageEvent<msgs::Text> &e, const QString &color, bool with_sender);
+ TimelineItem *createTimelineItem(const events::MessageEvent<msgs::Image> &e, const QString &color, bool with_sender);
+ TimelineItem *createTimelineItem(const events::MessageEvent<msgs::Notice> &e, const QString &color, bool with_sender);
+ TimelineItem *createTimelineItem(const events::MessageEvent<msgs::Text> &e, const QString &color, bool with_sender);
- int addEvents(const QJsonArray &events);
+ // Add new events at the end of the timeline.
+ int addEvents(const Timeline &timeline);
void addUserTextMessage(const QString &msg, int txn_id);
void updatePendingMessage(int txn_id, QString event_id);
+ void scrollDown();
void clear();
public slots:
void sliderRangeChanged(int min, int max);
+ void sliderMoved(int position);
+
+ // Add old events at the top of the timeline.
+ void addBackwardsEvents(const QString &room_id, const RoomMessages &msgs);
private:
void init();
void removePendingMessage(const events::MessageEvent<msgs::Text> &e);
+ void addTimelineItem(TimelineItem *item, TimelineDirection direction);
+ void updateLastSender(const QString &user_id, TimelineDirection direction);
+
+ // Used to determine whether or not we should prefix a message with the sender's name.
+ bool isSenderRendered(const QString &user_id, TimelineDirection direction);
bool isPendingMessage(const events::MessageEvent<msgs::Text> &e, const QString &userid);
+ // Return nullptr if the event couldn't be parsed.
+ TimelineItem *parseMessageEvent(const QJsonObject &event, TimelineDirection direction);
+
QVBoxLayout *top_layout_;
QVBoxLayout *scroll_layout_;
@@ -84,6 +102,19 @@ private:
QWidget *scroll_widget_;
QString last_sender_;
+ QString last_sender_backwards_;
+ QString room_id_;
+ QString prev_batch_token_;
+ QString local_user_;
+
+ bool isPaginationInProgress_ = false;
+ bool isInitialized = false;
+ bool isTimelineFinished = false;
+
+ const int SCROLL_BAR_GAP = 300;
+
+ int scroll_height_ = 0;
+ int previous_max_height_ = 0;
QList<PendingMessage> pending_msgs_;
QSharedPointer<MatrixClient> client_;
diff --git a/src/MainWindow.cc b/src/MainWindow.cc
index ce9d1110..bc64198c 100644
--- a/src/MainWindow.cc
+++ b/src/MainWindow.cc
@@ -108,6 +108,7 @@ void MainWindow::showChatPage(QString userid, QString homeserver, QString token)
if (progress_modal_ == nullptr) {
progress_modal_ = new OverlayModal(this, spinner_);
progress_modal_->fadeIn();
+ progress_modal_->setDuration(300);
}
login_page_->reset();
diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc
index ac321d94..015e9809 100644
--- a/src/MatrixClient.cc
+++ b/src/MatrixClient.cc
@@ -333,6 +333,32 @@ void MatrixClient::onImageResponse(QNetworkReply *reply)
emit imageDownloaded(event_id, pixmap);
}
+void MatrixClient::onMessagesResponse(QNetworkReply *reply)
+{
+ reply->deleteLater();
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ if (status == 0 || status >= 400) {
+ qWarning() << reply->errorString();
+ return;
+ }
+
+ auto data = reply->readAll();
+ auto room_id = reply->property("room_id").toString();
+
+ RoomMessages msgs;
+
+ try {
+ msgs.deserialize(QJsonDocument::fromJson(data));
+ } catch (const DeserializationException &e) {
+ qWarning() << "Room messages from" << room_id << e.what();
+ return;
+ }
+
+ emit messagesRetrieved(room_id, msgs);
+}
+
void MatrixClient::onResponse(QNetworkReply *reply)
{
switch (static_cast<Endpoint>(reply->property("endpoint").toInt())) {
@@ -369,6 +395,9 @@ void MatrixClient::onResponse(QNetworkReply *reply)
case Endpoint::GetOwnAvatar:
onGetOwnAvatarResponse(reply);
break;
+ case Endpoint::Messages:
+ onMessagesResponse(reply);
+ break;
default:
break;
}
@@ -581,3 +610,21 @@ void MatrixClient::fetchOwnAvatar(const QUrl &avatar_url)
QNetworkReply *reply = get(avatar_request);
reply->setProperty("endpoint", static_cast<int>(Endpoint::GetOwnAvatar));
}
+
+void MatrixClient::messages(const QString &room_id, const QString &from_token) noexcept
+{
+ QUrlQuery query;
+ query.addQueryItem("access_token", token_);
+ query.addQueryItem("from", from_token);
+ query.addQueryItem("dir", "b");
+
+ QUrl endpoint(server_);
+ endpoint.setPath(api_url_ + QString("/rooms/%1/messages").arg(room_id));
+ endpoint.setQuery(query);
+
+ QNetworkRequest request(QString(endpoint.toEncoded()));
+
+ QNetworkReply *reply = get(request);
+ reply->setProperty("endpoint", static_cast<int>(Endpoint::Messages));
+ reply->setProperty("room_id", room_id);
+}
diff --git a/src/RoomMessages.cc b/src/RoomMessages.cc
new file mode 100644
index 00000000..0aa020a4
--- /dev/null
+++ b/src/RoomMessages.cc
@@ -0,0 +1,42 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "RoomMessages.h"
+
+void RoomMessages::deserialize(const QJsonDocument &data)
+{
+ if (!data.isObject())
+ throw DeserializationException("response is not a JSON object");
+
+ QJsonObject object = data.object();
+
+ if (!object.contains("start"))
+ throw DeserializationException("start key is missing");
+
+ if (!object.contains("end"))
+ throw DeserializationException("end key is missing");
+
+ if (!object.contains("chunk"))
+ throw DeserializationException("chunk key is missing");
+
+ if (!object.value("chunk").isArray())
+ throw DeserializationException("chunk isn't a JSON array");
+
+ start_ = object.value("start").toString();
+ end_ = object.value("end").toString();
+ chunk_ = object.value("chunk").toArray();
+}
diff --git a/src/TimelineView.cc b/src/TimelineView.cc
index c57d8f7b..9f80db66 100644
--- a/src/TimelineView.cc
+++ b/src/TimelineView.cc
@@ -34,19 +34,19 @@
namespace events = matrix::events;
namespace msgs = matrix::events::messages;
-TimelineView::TimelineView(const QJsonArray &events, QSharedPointer<MatrixClient> client, QWidget *parent)
+TimelineView::TimelineView(const Timeline &timeline,
+ QSharedPointer<MatrixClient> client,
+ const QString &room_id,
+ QWidget *parent)
: QWidget(parent)
+ , room_id_{room_id}
, client_{client}
{
- init();
- addEvents(events);
-}
+ QSettings settings;
+ local_user_ = settings.value("auth/user_id").toString();
-TimelineView::TimelineView(QSharedPointer<MatrixClient> client, QWidget *parent)
- : QWidget(parent)
- , client_{client}
-{
init();
+ addEvents(timeline);
}
void TimelineView::clear()
@@ -58,83 +58,175 @@ void TimelineView::clear()
void TimelineView::sliderRangeChanged(int min, int max)
{
Q_UNUSED(min);
- scroll_area_->verticalScrollBar()->setValue(max);
+
+ if (!scroll_area_->verticalScrollBar()->isVisible())
+ return;
+
+ if (max - scroll_area_->verticalScrollBar()->value() < SCROLL_BAR_GAP)
+ scroll_area_->verticalScrollBar()->setValue(max);
}
-int TimelineView::addEvents(const QJsonArray &events)
+void TimelineView::scrollDown()
{
- QSettings settings;
- auto local_user = settings.value("auth/user_id").toString();
+ int current = scroll_area_->verticalScrollBar()->value();
+ int max = scroll_area_->verticalScrollBar()->maximum();
- int message_count = 0;
- events::EventType ty;
+ // The first time we enter the room move the scroll bar to the bottom.
+ if (!isInitialized) {
+ scroll_area_->ensureVisible(0, scroll_widget_->size().height(), 0, 0);
+ isInitialized = true;
+ return;
+ }
- for (const auto &event : events) {
- ty = events::extractEventType(event.toObject());
+ // If the gap is small enough move the scroll bar down. e.g when a new message appears.
+ if (max - current < SCROLL_BAR_GAP)
+ scroll_area_->verticalScrollBar()->setValue(max);
+}
- if (ty == events::EventType::RoomMessage) {
- events::MessageEventType msg_type = events::extractMessageEventType(event.toObject());
+void TimelineView::sliderMoved(int position)
+{
+ if (!scroll_area_->verticalScrollBar()->isVisible())
+ return;
- if (msg_type == events::MessageEventType::Text) {
- events::MessageEvent<msgs::Text> text;
+ // The scrollbar is high enough so we can start retrieving old events.
+ if (position < SCROLL_BAR_GAP) {
+ if (isTimelineFinished)
+ return;
- try {
- text.deserialize(event.toObject());
- } catch (const DeserializationException &e) {
- qWarning() << e.what() << event;
- continue;
- }
+ // Prevent user from moving up when there is pagination in progress.
+ if (isPaginationInProgress_) {
+ scroll_area_->verticalScrollBar()->setValue(SCROLL_BAR_GAP);
+ return;
+ }
- if (isPendingMessage(text, local_user)) {
- removePendingMessage(text);
- continue;
- }
+ isPaginationInProgress_ = true;
+ scroll_height_ = scroll_area_->verticalScrollBar()->value();
+ previous_max_height_ = scroll_area_->verticalScrollBar()->maximum();
- auto with_sender = last_sender_ != text.sender();
- auto color = TimelineViewManager::getUserColor(text.sender());
+ // FIXME: Maybe move this to TimelineViewManager to remove the extra calls?
+ client_.data()->messages(room_id_, prev_batch_token_);
+ }
+}
- addHistoryItem(text, color, with_sender);
- last_sender_ = text.sender();
+void TimelineView::addBackwardsEvents(const QString &room_id, const RoomMessages &msgs)
+{
+ if (room_id_ != room_id)
+ return;
- message_count += 1;
- } else if (msg_type == events::MessageEventType::Notice) {
- events::MessageEvent<msgs::Notice> notice;
+ if (msgs.chunk().count() == 0) {
+ isTimelineFinished = true;
+ return;
+ }
- try {
- notice.deserialize(event.toObject());
- } catch (const DeserializationException &e) {
- qWarning() << e.what() << event;
- continue;
- }
+ isTimelineFinished = false;
+ last_sender_backwards_.clear();
+ QList<TimelineItem *> items;
- auto with_sender = last_sender_ != notice.sender();
- auto color = TimelineViewManager::getUserColor(notice.sender());
+ // Parse in reverse order to determine where we should not show sender's name.
+ auto it = msgs.chunk().constEnd();
+ while (it != msgs.chunk().constBegin()) {
+ --it;
- addHistoryItem(notice, color, with_sender);
- last_sender_ = notice.sender();
+ TimelineItem *item = parseMessageEvent((*it).toObject(), TimelineDirection::Top);
- message_count += 1;
- } else if (msg_type == events::MessageEventType::Image) {
- events::MessageEvent<msgs::Image> img;
+ if (item != nullptr)
+ items.push_back(item);
+ }
+
+ // Reverse again to render them.
+ std::reverse(items.begin(), items.end());
- try {
- img.deserialize(event.toObject());
- } catch (const DeserializationException &e) {
- qWarning() << e.what() << event;
- continue;
- }
+ for (const auto &item : items)
+ addTimelineItem(item, TimelineDirection::Top);
- auto with_sender = last_sender_ != img.sender();
- auto color = TimelineViewManager::getUserColor(img.sender());
+ prev_batch_token_ = msgs.end();
+ isPaginationInProgress_ = false;
+}
+
+TimelineItem *TimelineView::parseMessageEvent(const QJsonObject &event, TimelineDirection direction)
+{
+ events::EventType ty = events::extractEventType(event);
- addHistoryItem(img, color, with_sender);
+ if (ty == events::EventType::RoomMessage) {
+ events::MessageEventType msg_type = events::extractMessageEventType(event);
- last_sender_ = img.sender();
- message_count += 1;
- } else if (msg_type == events::MessageEventType::Unknown) {
- qWarning() << "Unknown message type" << event.toObject();
- continue;
+ if (msg_type == events::MessageEventType::Text) {
+ events::MessageEvent<msgs::Text> text;
+
+ try {
+ text.deserialize(event);
+ } catch (const DeserializationException &e) {
+ qWarning() << e.what() << event;
+ return nullptr;
+ }
+
+ if (isPendingMessage(text, local_user_)) {
+ removePendingMessage(text);
+ return nullptr;
}
+
+ auto with_sender = isSenderRendered(text.sender(), direction);
+ updateLastSender(text.sender(), direction);
+
+ auto color = TimelineViewManager::getUserColor(text.sender());
+ last_sender_ = text.sender();
+
+ return createTimelineItem(text, color, with_sender);
+ } else if (msg_type == events::MessageEventType::Notice) {
+ events::MessageEvent<msgs::Notice> notice;
+
+ try {
+ notice.deserialize(event);
+ } catch (const DeserializationException &e) {
+ qWarning() << e.what() << event;
+ return nullptr;
+ }
+
+ auto with_sender = isSenderRendered(notice.sender(), direction);
+ updateLastSender(notice.sender(), direction);
+
+ auto color = TimelineViewManager::getUserColor(notice.sender());
+ last_sender_ = notice.sender();
+
+ return createTimelineItem(notice, color, with_sender);
+ } else if (msg_type == events::MessageEventType::Image) {
+ events::MessageEvent<msgs::Image> img;
+
+ try {
+ img.deserialize(event);
+ } catch (const DeserializationException &e) {
+ qWarning() << e.what() << event;
+ return nullptr;
+ }
+
+ auto with_sender = isSenderRendered(img.sender(), direction);
+ updateLastSender(img.sender(), direction);
+
+ auto color = TimelineViewManager::getUserColor(img.sender());
+ last_sender_ = img.sender();
+
+ return createTimelineItem(img, color, with_sender);
+ } else if (msg_type == events::MessageEventType::Unknown) {
+ qWarning() << "Unknown message type" << event;
+ return nullptr;
+ }
+ }
+
+ return nullptr;
+}
+
+int TimelineView::addEvents(const Timeline &timeline)
+{
+ int message_count = 0;
+
+ prev_batch_token_ = timeline.previousBatch();
+
+ for (const auto &event : timeline.events()) {
+ TimelineItem *item = parseMessageEvent(event.toObject(), TimelineDirection::Bottom);
+
+ if (item != nullptr) {
+ message_count += 1;
+ addTimelineItem(item, TimelineDirection::Bottom);
}
}
@@ -165,35 +257,59 @@ void TimelineView::init()
setLayout(top_layout_);
- connect(scroll_area_->verticalScrollBar(),
- SIGNAL(rangeChanged(int, int)),
- this,
- SLOT(sliderRangeChanged(int, int)));
+ connect(client_.data(), &MatrixClient::messagesRetrieved, this, &TimelineView::addBackwardsEvents);
+
+ connect(scroll_area_->verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(sliderMoved(int)));
+ connect(scroll_area_->verticalScrollBar(), SIGNAL(rangeChanged(int, int)), this, SLOT(sliderRangeChanged(int, int)));
+}
+
+void TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction)
+{
+ if (direction == TimelineDirection::Bottom)
+ last_sender_ = user_id;
+ else
+ last_sender_backwards_ = user_id;
}
-void TimelineView::addHistoryItem(const events::MessageEvent<msgs::Image> &event, const QString &color, bool with_sender)
+bool TimelineView::isSenderRendered(const QString &user_id, TimelineDirection direction)
+{
+ if (direction == TimelineDirection::Bottom)
+ return last_sender_ != user_id;
+ else
+ return last_sender_backwards_ != user_id;
+}
+
+TimelineItem *TimelineView::createTimelineItem(const events::MessageEvent<msgs::Image> &event, const QString &color, bool with_sender)
{
auto image = new ImageItem(client_, event);
if (with_sender) {
auto item = new TimelineItem(image, event, color, scroll_widget_);
- scroll_layout_->addWidget(item);
- } else {
- auto item = new TimelineItem(image, event, scroll_widget_);
- scroll_layout_->addWidget(item);
+ return item;
}
+
+ auto item = new TimelineItem(image, event, scroll_widget_);
+ return item;
}
-void TimelineView::addHistoryItem(const events::MessageEvent<msgs::Notice> &event, const QString &color, bool with_sender)
+TimelineItem *TimelineView::createTimelineItem(const events::MessageEvent<msgs::Notice> &event, const QString &color, bool with_sender)
{
TimelineItem *item = new TimelineItem(event, with_sender, color, scroll_widget_);
- scroll_layout_->addWidget(item);
+ return item;
}
-void TimelineView::addHistoryItem(const events::MessageEvent<msgs::Text> &event, const QString &color, bool with_sender)
+TimelineItem *TimelineView::createTimelineItem(const events::MessageEvent<msgs::Text> &event, const QString &color, bool with_sender)
{
TimelineItem *item = new TimelineItem(event, with_sender, color, scroll_widget_);
- scroll_layout_->addWidget(item);
+ return item;
+}
+
+void TimelineView::addTimelineItem(TimelineItem *item, TimelineDirection direction)
+{
+ if (direction == TimelineDirection::Bottom)
+ scroll_layout_->addWidget(item);
+ else
+ scroll_layout_->insertWidget(0, item);
}
void TimelineView::updatePendingMessage(int txn_id, QString event_id)
@@ -254,7 +370,3 @@ void TimelineView::addUserTextMessage(const QString &body, int txn_id)
pending_msgs_.push_back(message);
}
-
-TimelineView::~TimelineView()
-{
-}
diff --git a/src/TimelineViewManager.cc b/src/TimelineViewManager.cc
index 84bf20b2..d07e8075 100644
--- a/src/TimelineViewManager.cc
+++ b/src/TimelineViewManager.cc
@@ -78,10 +78,9 @@ void TimelineViewManager::initialize(const Rooms &rooms)
{
for (auto it = rooms.join().constBegin(); it != rooms.join().constEnd(); it++) {
auto roomid = it.key();
- auto events = it.value().timeline().events();
// Create a history view with the room events.
- TimelineView *view = new TimelineView(events, client_);
+ TimelineView *view = new TimelineView(it.value().timeline(), client_, it.key());
views_.insert(it.key(), view);
// Add the view in the widget stack.
@@ -100,9 +99,8 @@ void TimelineViewManager::sync(const Rooms &rooms)
}
auto view = views_.value(roomid);
- auto events = it.value().timeline().events();
- int msgs_added = view->addEvents(events);
+ int msgs_added = view->addEvents(it.value().timeline());
if (msgs_added > 0) {
// TODO: When the app window gets active the current
@@ -124,6 +122,7 @@ void TimelineViewManager::setHistoryView(const QString &room_id)
active_room_ = room_id;
auto widget = views_.value(room_id);
+ widget->scrollDown();
setCurrentWidget(widget);
}
diff --git a/src/main.cc b/src/main.cc
index 867ed204..8f5dba1e 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -43,7 +43,7 @@ int main(int argc, char *argv[])
app.setStyleSheet(
"QScrollBar:vertical { background-color: #f8fbfe; width: 8px; border: none; margin: 2px; }"
- "QScrollBar::handle:vertical { background-color : #d6dde3; }"
+ "QScrollBar::handle:vertical { min-height: 40px; background-color : #d6dde3; }"
"QScrollBar::add-line:vertical { border: none; background: none; }"
"QScrollBar::sub-line:vertical { border: none; background: none; }");
|