Skip to content
Snippets Groups Projects
Commit 0368d854 authored by Konstantinos Sideris's avatar Konstantinos Sideris
Browse files

Initial support for backwards pagination

parent ff611c1b
No related branches found
No related tags found
No related merge requests found
......@@ -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
......
......@@ -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_;
......
/*
* 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
......@@ -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_;
......
......@@ -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();
......
......@@ -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);
}
/*
* 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();
}
......@@ -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();
// 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;
}
int message_count = 0;
events::EventType ty;
// 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);
}
for (const auto &event : events) {
ty = events::extractEventType(event.toObject());
void TimelineView::sliderMoved(int position)
{
if (!scroll_area_->verticalScrollBar()->isVisible())
return;
// The scrollbar is high enough so we can start retrieving old events.
if (position < SCROLL_BAR_GAP) {
if (isTimelineFinished)
return;
// Prevent user from moving up when there is pagination in progress.
if (isPaginationInProgress_) {
scroll_area_->verticalScrollBar()->setValue(SCROLL_BAR_GAP);
return;
}
if (ty == events::EventType::RoomMessage) {
events::MessageEventType msg_type = events::extractMessageEventType(event.toObject());
isPaginationInProgress_ = true;
scroll_height_ = scroll_area_->verticalScrollBar()->value();
previous_max_height_ = scroll_area_->verticalScrollBar()->maximum();
if (msg_type == events::MessageEventType::Text) {
events::MessageEvent<msgs::Text> text;
// FIXME: Maybe move this to TimelineViewManager to remove the extra calls?
client_.data()->messages(room_id_, prev_batch_token_);
}
}
try {
text.deserialize(event.toObject());
} catch (const DeserializationException &e) {
qWarning() << e.what() << event;
continue;
}
void TimelineView::addBackwardsEvents(const QString &room_id, const RoomMessages &msgs)
{
if (room_id_ != room_id)
return;
if (isPendingMessage(text, local_user)) {
removePendingMessage(text);
continue;
}
if (msgs.chunk().count() == 0) {
isTimelineFinished = true;
return;
}
isTimelineFinished = false;
last_sender_backwards_.clear();
QList<TimelineItem *> items;
// 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;
TimelineItem *item = parseMessageEvent((*it).toObject(), TimelineDirection::Top);
if (item != nullptr)
items.push_back(item);
}
// Reverse again to render them.
std::reverse(items.begin(), items.end());
for (const auto &item : items)
addTimelineItem(item, TimelineDirection::Top);
auto with_sender = last_sender_ != text.sender();
auto color = TimelineViewManager::getUserColor(text.sender());
prev_batch_token_ = msgs.end();
isPaginationInProgress_ = false;
}
TimelineItem *TimelineView::parseMessageEvent(const QJsonObject &event, TimelineDirection direction)
{
events::EventType ty = events::extractEventType(event);
if (ty == events::EventType::RoomMessage) {
events::MessageEventType msg_type = events::extractMessageEventType(event);
addHistoryItem(text, color, with_sender);
last_sender_ = text.sender();
if (msg_type == events::MessageEventType::Text) {
events::MessageEvent<msgs::Text> text;
message_count += 1;
} else if (msg_type == events::MessageEventType::Notice) {
events::MessageEvent<msgs::Notice> notice;
try {
text.deserialize(event);
} catch (const DeserializationException &e) {
qWarning() << e.what() << event;
return nullptr;
}
if (isPendingMessage(text, local_user_)) {
removePendingMessage(text);
return nullptr;
}
try {
notice.deserialize(event.toObject());
} catch (const DeserializationException &e) {
qWarning() << e.what() << event;
continue;
}
auto with_sender = isSenderRendered(text.sender(), direction);
updateLastSender(text.sender(), direction);
auto with_sender = last_sender_ != notice.sender();
auto color = TimelineViewManager::getUserColor(notice.sender());
auto color = TimelineViewManager::getUserColor(text.sender());
last_sender_ = text.sender();
addHistoryItem(notice, color, with_sender);
last_sender_ = notice.sender();
return createTimelineItem(text, color, with_sender);
} else if (msg_type == events::MessageEventType::Notice) {
events::MessageEvent<msgs::Notice> notice;
message_count += 1;
} else if (msg_type == events::MessageEventType::Image) {
events::MessageEvent<msgs::Image> img;
try {
notice.deserialize(event);
} catch (const DeserializationException &e) {
qWarning() << e.what() << event;
return nullptr;
}
try {
img.deserialize(event.toObject());
} catch (const DeserializationException &e) {
qWarning() << e.what() << event;
continue;
}
auto with_sender = isSenderRendered(notice.sender(), direction);
updateLastSender(notice.sender(), direction);
auto with_sender = last_sender_ != img.sender();
auto color = TimelineViewManager::getUserColor(img.sender());
auto color = TimelineViewManager::getUserColor(notice.sender());
last_sender_ = notice.sender();
addHistoryItem(img, color, with_sender);
return createTimelineItem(notice, color, with_sender);
} else if (msg_type == events::MessageEventType::Image) {
events::MessageEvent<msgs::Image> img;
last_sender_ = img.sender();
message_count += 1;
} else if (msg_type == events::MessageEventType::Unknown) {
qWarning() << "Unknown message type" << event.toObject();
continue;
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::addHistoryItem(const events::MessageEvent<msgs::Image> &event, const QString &color, bool with_sender)
void TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction)
{
if (direction == TimelineDirection::Bottom)
last_sender_ = user_id;
else
last_sender_backwards_ = user_id;
}
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()
{
}
......@@ -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);
}
......
......@@ -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; }");
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment