Initial support for backwards pagination

This commit is contained in:
Konstantinos Sideris 2017-05-12 15:43:35 +03:00
parent ff611c1b39
commit 0368d854cf
10 changed files with 388 additions and 94 deletions

View File

@ -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

View File

@ -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_;

56
include/RoomMessages.h Normal file
View File

@ -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

View File

@ -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_;

View File

@ -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();

View File

@ -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);
}

42
src/RoomMessages.cc Normal file
View File

@ -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();
}

View File

@ -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);
}
try {
img.deserialize(event.toObject());
} catch (const DeserializationException &e) {
qWarning() << e.what() << event;
continue;
}
// Reverse again to render them.
std::reverse(items.begin(), items.end());
auto with_sender = last_sender_ != img.sender();
auto color = TimelineViewManager::getUserColor(img.sender());
for (const auto &item : items)
addTimelineItem(item, TimelineDirection::Top);
addHistoryItem(img, color, with_sender);
prev_batch_token_ = msgs.end();
isPaginationInProgress_ = false;
}
last_sender_ = img.sender();
message_count += 1;
} else if (msg_type == events::MessageEventType::Unknown) {
qWarning() << "Unknown message type" << event.toObject();
continue;
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);
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::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()
{
}

View File

@ -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);
}

View File

@ -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; }");