summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJavier S. Pedro <maemo@javispedro.com>2013-04-01 20:46:39 +0200
committerJavier S. Pedro <maemo@javispedro.com>2013-04-01 20:46:39 +0200
commita6e64fbf9404b201b04fbd1ab4b959a18d8f83a9 (patch)
tree6b97b6ac14716dbe51cb105819abde3e36ffd465
parent5ef8b38e55c1883224fe1f01f47aba45b7b42666 (diff)
downloadtapasboard-a6e64fbf9404b201b04fbd1ab4b959a18d8f83a9.tar.gz
tapasboard-a6e64fbf9404b201b04fbd1ab4b959a18d8f83a9.zip
add support for actually reading topics
-rw-r--r--action.h2
-rw-r--r--board.cpp81
-rw-r--r--board.h5
-rw-r--r--boardmodel.cpp2
-rw-r--r--fetchboardconfigaction.cpp6
-rw-r--r--fetchboardconfigaction.h2
-rw-r--r--fetchforumsaction.cpp6
-rw-r--r--fetchforumsaction.h2
-rw-r--r--fetchpostsaction.cpp98
-rw-r--r--fetchpostsaction.h33
-rw-r--r--fetchtopicsaction.cpp23
-rw-r--r--fetchtopicsaction.h3
-rw-r--r--forummodel.cpp9
-rw-r--r--global.h12
-rw-r--r--main.cpp2
-rw-r--r--qml/tapasboard/BoardPage.qml34
-rw-r--r--qml/tapasboard/EmptyListDelegate.qml29
-rw-r--r--qml/tapasboard/ForumPage.qml28
-rw-r--r--qml/tapasboard/TopicPage.qml63
-rw-r--r--tapasboard.pro8
-rw-r--r--topicmodel.cpp285
-rw-r--r--topicmodel.h72
-rw-r--r--xmlrpcinterface.cpp3
-rw-r--r--xmlrpcinterface.h2
-rw-r--r--xmlrpcpendingcall.cpp3
25 files changed, 793 insertions, 20 deletions
diff --git a/action.h b/action.h
index 421adea..acfbc05 100644
--- a/action.h
+++ b/action.h
@@ -13,6 +13,8 @@ class Action : public QObject
public:
explicit Action(Board *board);
+ virtual bool isSupersetOf(Action *action) const = 0;
+
signals:
void finished(Action *self);
void error(Action *self, const QString& messsage);
diff --git a/board.cpp b/board.cpp
index 887b3c6..4883ebc 100644
--- a/board.cpp
+++ b/board.cpp
@@ -28,8 +28,25 @@ Board::Board(const QString& forumUrl, QObject *parent) :
fetchForumsIfOutdated();
}
+Board::~Board()
+{
+ QSqlDatabase::removeDatabase(_slug);
+}
+
void Board::enqueueAction(Action *action)
{
+ // Let's find if a duplicate action is in there already.
+ qDebug() << "Enqueing" << action;
+
+ foreach (Action *a, _queue) {
+ if (a->isSupersetOf(action)) {
+ qDebug() << "Action superseded, ignoring";
+ delete action;
+ return;
+ }
+ }
+
+ // Otherwise, enqueue the action
connect(action, SIGNAL(finished(Action*)), SLOT(handleActionFinished(Action*)));
connect(action, SIGNAL(error(Action*,QString)), SLOT(handleActionError(Action*,QString)));
@@ -95,6 +112,12 @@ void Board::notifyForumTopicsChanged(int forumId, int start, int end)
emit forumTopicsChanged(forumId, start, end);
}
+void Board::notifyTopicPostsChanged(int topicId, int start, int end)
+{
+ qDebug() << "TopicPosts Changed" << topicId << start << end;
+ emit topicPostsChanged(topicId, start, end);
+}
+
QString Board::createSlug(const QString &forumUrl)
{
static const QRegExp regexp("[^a-z0-9]+");
@@ -157,6 +180,58 @@ bool Board::initializeDb()
return false;
}
+ if (!q.exec("CREATE TABLE IF NOT EXISTS posts (forum_id INTEGER, topic_id INTEGER, post_id INTEGER PRIMARY KEY, post_title TEXT, post_content TEXT, post_author_id INTEGER, post_author_name TEXT, can_edit BOOL, icon_url TEXT, post_time TEXT, last_update_time TEXT)")) {
+ qWarning() << "Could not create posts table:" << q.lastError().text();
+ return false;
+ }
+ if (!q.exec("CREATE INDEX IF NOT EXISTS posts_topic ON posts (topic_id)")) {
+ qWarning() << "Could not create posts_topic index:" << q.lastError().text();
+ return false;
+ }
+ if (!q.exec("CREATE INDEX IF NOT EXISTS posts_time ON posts (post_time)")) {
+ qWarning() << "Could not create posts_time index:" << q.lastError().text();
+ return false;
+ }
+
+ return true;
+}
+
+bool Board::eraseDb()
+{
+ QSqlQuery q(_db);
+
+ if (!q.exec("DROP TABLE IF EXISTS config")) {
+ qWarning() << "Could not drop config table:" << q.lastError().text();
+ return false;
+ }
+ if (!q.exec("DROP TABLE IF EXISTS forums")) {
+ qWarning() << "Could not drop forums table:" << q.lastError().text();
+ return false;
+ }
+ if (!q.exec("DROP TABLE IF EXISTS topics")) {
+ qWarning() << "Could not drop topics table:" << q.lastError().text();
+ return false;
+ }
+ if (!q.exec("DROP TABLE IF EXISTS posts")) {
+ qWarning() << "Could not drop posts table:" << q.lastError().text();
+ return false;
+ }
+ if (!q.exec("VACUUM")) {
+ qWarning() << "Could not vacuum database:" << q.lastError().text();
+ return false;
+ }
+
+ return true;
+}
+
+bool Board::cleanDb()
+{
+ QSqlQuery q(_db);
+ // TODO: Delete old posts from cache
+ if (!q.exec("VACUUM")) {
+ qWarning() << "Could not vacuum database:" << q.lastError().text();
+ return false;
+ }
return true;
}
@@ -210,7 +285,11 @@ void Board::fetchForumsIfOutdated()
void Board::handleActionFinished(Action *action)
{
- removeFromActionQueue(action);
+ qDebug() << action << "finished";
+ bool ok = removeFromActionQueue(action);
+ if (!ok) {
+ qWarning() << "Finished action not in queue";
+ }
}
void Board::handleActionError(Action *action, const QString& message)
diff --git a/board.h b/board.h
index 38e5297..fb28ea0 100644
--- a/board.h
+++ b/board.h
@@ -13,6 +13,7 @@ class Board : public QObject
Q_OBJECT
public:
explicit Board(const QString& boardUrl, QObject *parent = 0);
+ ~Board();
bool busy() const;
void enqueueAction(Action* action);
@@ -28,18 +29,22 @@ public:
void notifyConfigChanged();
void notifyForumsChanged();
void notifyForumTopicsChanged(int forumId, int start, int end);
+ void notifyTopicPostsChanged(int topicId, int start, int end);
signals:
void configChanged();
void forumsChanged();
void imageChanged(const QString& imageUrl);
void forumTopicsChanged(int forumId, int start, int end);
+ void topicPostsChanged(int topicId, int start, int end);
private:
static QString createSlug(const QString& forumUrl);
static QString getDbDir();
static QString getDbPathFor(const QString& slug);
bool initializeDb();
+ bool eraseDb();
+ bool cleanDb();
bool removeFromActionQueue(Action *action);
void executeActionFromQueue();
void fetchConfigIfOutdated();
diff --git a/boardmodel.cpp b/boardmodel.cpp
index 77778ad..f8da74e 100644
--- a/boardmodel.cpp
+++ b/boardmodel.cpp
@@ -12,7 +12,7 @@ BoardModel::BoardModel(QObject *parent) :
QHash<int, QByteArray> roles = roleNames();
roles[NameRole] = QByteArray("title");
roles[LogoRole] = QByteArray("logo");
- roles[DescriptionRole] = QByteArray("subtitle");
+ roles[DescriptionRole] = QByteArray("description");
roles[ForumIdRole] = QByteArray("forumId");
roles[ParentIdRole] = QByteArray("parentId");
roles[CategoryRole] = QByteArray("category");
diff --git a/fetchboardconfigaction.cpp b/fetchboardconfigaction.cpp
index 4d1cea2..0dd5e4c 100644
--- a/fetchboardconfigaction.cpp
+++ b/fetchboardconfigaction.cpp
@@ -12,6 +12,12 @@ FetchBoardConfigAction::FetchBoardConfigAction(Board *board) :
{
}
+bool FetchBoardConfigAction::isSupersetOf(Action *action) const
+{
+ // If 'action' is also a fetch board config action then yes, this supersets 'action'.
+ return qobject_cast<FetchBoardConfigAction*>(action) != 0;
+}
+
void FetchBoardConfigAction::execute()
{
_call = _board->service()->asyncCall("get_config");
diff --git a/fetchboardconfigaction.h b/fetchboardconfigaction.h
index 05bd65f..7f350f9 100644
--- a/fetchboardconfigaction.h
+++ b/fetchboardconfigaction.h
@@ -11,6 +11,8 @@ class FetchBoardConfigAction : public Action
public:
explicit FetchBoardConfigAction(Board *board);
+ bool isSupersetOf(Action *action) const;
+
void execute();
private slots:
diff --git a/fetchforumsaction.cpp b/fetchforumsaction.cpp
index 8b3e4c8..7b496c3 100644
--- a/fetchforumsaction.cpp
+++ b/fetchforumsaction.cpp
@@ -13,6 +13,12 @@ FetchForumsAction::FetchForumsAction(Board *board) :
{
}
+bool FetchForumsAction::isSupersetOf(Action *action) const
+{
+ // If 'action' is also a fetch forums list action then yes, this supersets 'action'.
+ return qobject_cast<FetchForumsAction*>(action) != 0;
+}
+
void FetchForumsAction::execute()
{
_call = _board->service()->asyncCall("get_forum");
diff --git a/fetchforumsaction.h b/fetchforumsaction.h
index b8625fa..b047bcb 100644
--- a/fetchforumsaction.h
+++ b/fetchforumsaction.h
@@ -12,6 +12,8 @@ class FetchForumsAction : public Action
public:
explicit FetchForumsAction(Board *board);
+ bool isSupersetOf(Action *action) const;
+
void execute();
private slots:
diff --git a/fetchpostsaction.cpp b/fetchpostsaction.cpp
new file mode 100644
index 0000000..16802fa
--- /dev/null
+++ b/fetchpostsaction.cpp
@@ -0,0 +1,98 @@
+#include <QtCore/QDateTime>
+#include <QtCore/QDebug>
+#include <QtSql/QSqlDatabase>
+#include <QtSql/QSqlQuery>
+
+#include "board.h"
+#include "xmlrpcinterface.h"
+#include "xmlrpcreply.h"
+#include "fetchpostsaction.h"
+
+FetchPostsAction::FetchPostsAction(int topicId, int start, int end, Board *board) :
+ Action(board), _topicId(topicId), _start(start), _end(end)
+{
+}
+
+bool FetchPostsAction::isSupersetOf(Action *action) const
+{
+ FetchPostsAction *other = qobject_cast<FetchPostsAction*>(action);
+ if (other) {
+ if (other->_topicId == _topicId) {
+ if (_start <= other->_start && _end >= other->_end) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+void FetchPostsAction::execute()
+{
+ _call = _board->service()->asyncCall("get_thread",
+ QString::number(_topicId), _start, _end);
+ connect(_call, SIGNAL(finished(XmlRpcPendingCall*)), SLOT(handleFinishedCall()));
+}
+
+void FetchPostsAction::handleFinishedCall()
+{
+ XmlRpcReply<QVariantMap> result(_call);
+ if (result.isValid()) {
+ QVariantMap map = result;
+ QVariantList posts = map["posts"].toList();
+ QSqlDatabase db = _board->database();
+ db.transaction();
+
+ QSqlQuery query(db);
+ query.prepare("INSERT OR REPLACE INTO posts (topic_id, post_id, post_title, post_content, post_author_id, post_author_name, can_edit, icon_url, post_time, last_update_time) "
+ "VALUES (:topic_id, :post_id, :post_title, :post_content, :post_author_id, :post_author_name, :can_edit, :icon_url, :post_time, :last_update_time)");
+
+ foreach (const QVariant& post_v, posts) {
+ QVariantMap post = post_v.toMap();
+ bool ok = false;
+ int topic_id = post["topic_id"].toInt(&ok);
+ if (!ok) {
+ // Not fatal, just assume it's the one we requested
+ topic_id = _topicId;
+ }
+ int post_id = post["post_id"].toInt(&ok);
+ if (!ok) {
+ qWarning() << "No post_id in" << post;
+ continue;
+ }
+
+ query.bindValue(":topic_id", topic_id);
+ query.bindValue(":post_id", post_id);
+ query.bindValue(":post_title", unencodePostText(post["post_title"]));
+ query.bindValue(":post_content", unencodePostText(post["post_content"]));
+ query.bindValue(":post_author_id", post["post_author_id"].toInt());
+ query.bindValue(":post_author_name", unencodePostText(post["post_author_name"]));
+ query.bindValue(":can_edit", post["can_edit"].toBool() ? 1 : 0);
+ query.bindValue(":icon_url", post["icon_url"].toString());
+ query.bindValue(":post_time", post["post_time"].toDateTime());
+ query.bindValue(":last_update_time", QDateTime::currentDateTime());
+
+ if (!query.exec()) {
+ qWarning() << "Failed to store topic info for:" << topic_id;
+ handleDatabaseError("storing topic info", query);
+ continue;
+ }
+ }
+
+ db.commit();
+ if (posts.size() > 0) {
+ _board->notifyTopicPostsChanged(_topicId,
+ _start, _start + posts.size() - 1);
+ }
+ } else {
+ qWarning() << "Could not fetch posts";
+ // TODO emit error ...
+ }
+ emit finished(this);
+ _call->deleteLater();
+}
+
+QString FetchPostsAction::unencodePostText(const QVariant &v)
+{
+ QByteArray ba = v.toByteArray();
+ return QString::fromUtf8(ba.constData(), ba.length());
+}
diff --git a/fetchpostsaction.h b/fetchpostsaction.h
new file mode 100644
index 0000000..63cb5f8
--- /dev/null
+++ b/fetchpostsaction.h
@@ -0,0 +1,33 @@
+#ifndef FETCHPOSTSACTION_H
+#define FETCHPOSTSACTION_H
+
+#include <QtCore/QVariant>
+#include <QtSql/QSqlQuery>
+#include "action.h"
+
+class XmlRpcPendingCall;
+
+class FetchPostsAction : public Action
+{
+ Q_OBJECT
+public:
+ explicit FetchPostsAction(int topicId, int start, int end, Board *board);
+
+ bool isSupersetOf(Action *action) const;
+
+ void execute();
+
+private slots:
+ void handleFinishedCall();
+
+private:
+ static QString unencodePostText(const QVariant& v);
+
+private:
+ XmlRpcPendingCall *_call;
+ int _topicId;
+ int _start;
+ int _end;
+};
+
+#endif // FETCHPOSTSACTION_H
diff --git a/fetchtopicsaction.cpp b/fetchtopicsaction.cpp
index 5661da8..dd59b4d 100644
--- a/fetchtopicsaction.cpp
+++ b/fetchtopicsaction.cpp
@@ -13,6 +13,19 @@ FetchTopicsAction::FetchTopicsAction(int forumId, int start, int end, Board *boa
{
}
+bool FetchTopicsAction::isSupersetOf(Action *action) const
+{
+ FetchTopicsAction *other = qobject_cast<FetchTopicsAction*>(action);
+ if (other) {
+ if (other->_forumId == _forumId) {
+ if (_start <= other->_start && _end >= other->_end) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
void FetchTopicsAction::execute()
{
_call = _board->service()->asyncCall("get_topic",
@@ -20,8 +33,6 @@ void FetchTopicsAction::execute()
connect(_call, SIGNAL(finished(XmlRpcPendingCall*)), SLOT(handleFinishedCall()));
}
-// forum_id INTEGER, topic_id INTEGER PRIMARY KEY, topic_title TEXT, topic_author_id INTEGER, topic_author_name TEXT, is_subscribed BOOL, is_closed BOOL, icon_url TEXT, last_reply_time TEXT, new_post BOOL
-
void FetchTopicsAction::handleFinishedCall()
{
XmlRpcReply<QVariantMap> result(_call);
@@ -40,12 +51,12 @@ void FetchTopicsAction::handleFinishedCall()
bool ok = false;
int forum_id = topic["forum_id"].toInt(&ok);
if (!ok) {
- qWarning() << "No forum_id in" << topic;
- continue;
+ // Not fatal, just use the one we expected.
+ forum_id = _forumId;
}
int topic_id = topic["topic_id"].toInt(&ok);
if (!ok) {
- qWarning() << "No parent_id in" << topic;
+ qWarning() << "No topic_id in" << topic;
continue;
}
@@ -72,6 +83,8 @@ void FetchTopicsAction::handleFinishedCall()
if (topics.size() > 0) {
_board->notifyForumTopicsChanged(_forumId,
_start, _start + topics.size() - 1);
+ } else {
+ qWarning() << "Asked for topics, got none...";
}
} else {
qWarning() << "Could not fetch topics";
diff --git a/fetchtopicsaction.h b/fetchtopicsaction.h
index 747ad93..0b02fa3 100644
--- a/fetchtopicsaction.h
+++ b/fetchtopicsaction.h
@@ -12,6 +12,8 @@ class FetchTopicsAction : public Action
Q_OBJECT
public:
explicit FetchTopicsAction(int forumId, int start, int end, Board *board);
+
+ bool isSupersetOf(Action *action) const;
void execute();
@@ -26,7 +28,6 @@ private:
int _forumId;
int _start;
int _end;
-
};
#endif // FETCHTOPICSACTION_H
diff --git a/forummodel.cpp b/forummodel.cpp
index 43cb0bd..c66d3a8 100644
--- a/forummodel.cpp
+++ b/forummodel.cpp
@@ -13,10 +13,7 @@ ForumModel::ForumModel(QObject *parent) :
QHash<int, QByteArray> roles = roleNames();
roles[TitleRole] = QByteArray("title");
roles[IconRole] = QByteArray("icon");
- //roles[DescriptionRole] = QByteArray("subtitle");
- //roles[ForumIdRole] = QByteArray("forumId");
- //roles[ParentIdRole] = QByteArray("parentId");
- //roles[CategoryRole] = QByteArray("category");
+ roles[TopicIdRole] = QByteArray("topicId");
setRoleNames(roles);
}
@@ -84,6 +81,9 @@ QVariant ForumModel::data(const QModelIndex &index, int role) const
case TitleRole:
return _data[row].title;
break;
+ case TopicIdRole:
+ return _data[row].topic_id;
+ break;
}
return QVariant();
@@ -266,6 +266,7 @@ void ForumModel::update()
if (!last.isValid() ||
last.secsTo(QDateTime::currentDateTime()) > FORUM_TOP_TLL) {
// Outdated or empty, refresh.
+ qDebug() << "Fetching topics because the top are old";
_board->enqueueAction(new FetchTopicsAction(_forumId, 0, FORUM_PAGE_SIZE - 1, _board));
} else {
qDebug() << "Topics not outdated";
diff --git a/global.h b/global.h
index db9a3e6..e2b0329 100644
--- a/global.h
+++ b/global.h
@@ -15,9 +15,19 @@
/** Time we should consider other topics in a forum up to date, in seconds. */
#define FORUM_TOPICS_TLL 15 * 60
-/** Number of topics per "block" in the forum view */
+/** Number of topics per "block" in subforum view */
#define FORUM_PAGE_SIZE 20
+/** Time we should consider the most recent posts in a topic up to date, in seconds. */
+#define TOPIC_TOP_TLL 5 * 60
+
+/** Time we should consider other posts in a topic up to date, in seconds. */
+#define TOPIC_POSTS_TLL 15 * 60
+
+/** Number of posts per "block" in topic view */
+#define TOPIC_PAGE_SIZE 20
+
+
extern BoardManager *board_manager;
#endif // GLOBAL_H
diff --git a/main.cpp b/main.cpp
index f15094d..5fd69ee 100644
--- a/main.cpp
+++ b/main.cpp
@@ -6,6 +6,7 @@
#include "board.h"
#include "boardmodel.h"
#include "forummodel.h"
+#include "topicmodel.h"
#include "xmlrpcinterface.h"
#include "xmlrpcreply.h"
@@ -28,6 +29,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
qmlRegisterType<BoardModel>("com.javispedro.tapasboard", 1, 0, "BoardModel");
qmlRegisterType<ForumModel>("com.javispedro.tapasboard", 1, 0, "ForumModel");
+ qmlRegisterType<TopicModel>("com.javispedro.tapasboard", 1, 0, "TopicModel");
viewer.setOrientation(QmlApplicationViewer::ScreenOrientationAuto);
viewer.setMainQmlFile(QLatin1String("qml/tapasboard/main.qml"));
diff --git a/qml/tapasboard/BoardPage.qml b/qml/tapasboard/BoardPage.qml
index 7c3f1cd..57d362d 100644
--- a/qml/tapasboard/BoardPage.qml
+++ b/qml/tapasboard/BoardPage.qml
@@ -35,8 +35,34 @@ Page {
text: section
}
- delegate: ListDelegate {
+ delegate: EmptyListDelegate {
+ id: forumItem
+
+ height: Math.max(forumItemColumn.height + UiConstants.ButtonSpacing * 2, UiConstants.ListItemHeightDefault)
+
+ Column {
+ id: forumItemColumn
+ anchors.left: parent.left
+ anchors.right: forumItemImage.left
+ anchors.verticalCenter: parent.verticalCenter
+
+ Text {
+ text: model.title
+ width: parent.width
+ font: UiConstants.TitleFont
+ }
+
+ Text {
+ text: model.description
+ width: parent.width
+ font: UiConstants.SubtitleFont
+ wrapMode: Text.Wrap
+ visible: text != ""
+ }
+ }
+
Image {
+ id: forumItemImage
source: "image://theme/icon-m-common-drilldown-arrow" + (theme.inverted ? "-inverse" : "")
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -44,9 +70,9 @@ Page {
onClicked: {
pageStack.push(Qt.resolvedUrl("ForumPage.qml"), {
- boardUrl: boardPage.boardUrl,
- forumId: model.forumId
- });
+ boardUrl: boardPage.boardUrl,
+ forumId: model.forumId
+ });
}
}
}
diff --git a/qml/tapasboard/EmptyListDelegate.qml b/qml/tapasboard/EmptyListDelegate.qml
new file mode 100644
index 0000000..9a9d63d
--- /dev/null
+++ b/qml/tapasboard/EmptyListDelegate.qml
@@ -0,0 +1,29 @@
+import QtQuick 1.1
+import com.nokia.meego 1.1
+
+Item {
+ id: listItem
+
+ signal clicked
+ property alias pressed: mouseArea.pressed
+
+ height: UiConstants.ListItemHeightDefault
+ width: parent.width
+
+ BorderImage {
+ id: background
+ anchors.fill: parent
+ anchors.leftMargin: -UiConstants.DefaultMargin
+ anchors.rightMargin: -UiConstants.DefaultMargin
+ visible: pressed
+ source: theme.inverted ? "image://theme/meegotouch-panel-inverted-background-pressed" : "image://theme/meegotouch-panel-background-pressed"
+ }
+
+ MouseArea {
+ id: mouseArea;
+ anchors.fill: parent
+ onClicked: {
+ listItem.clicked();
+ }
+ }
+}
diff --git a/qml/tapasboard/ForumPage.qml b/qml/tapasboard/ForumPage.qml
index a70fe74..6e39fe2 100644
--- a/qml/tapasboard/ForumPage.qml
+++ b/qml/tapasboard/ForumPage.qml
@@ -28,12 +28,38 @@ Page {
boardUrl: forumPage.boardUrl
forumId: forumPage.forumId
}
- delegate: ListDelegate {
+ delegate: EmptyListDelegate {
+ id: topicItem
+
+ height: Math.max(topicItemColumn.height + UiConstants.ButtonSpacing * 2, UiConstants.ListItemHeightDefault)
+
+ Column {
+ id: topicItemColumn
+ anchors.left: parent.left
+ anchors.right: topicItemImage.left
+ anchors.verticalCenter: parent.verticalCenter
+
+ Text {
+ text: model.title
+ width: parent.width
+ font: UiConstants.TitleFont
+ wrapMode: Text.Wrap
+ }
+ }
+
Image {
+ id: topicItemImage
source: "image://theme/icon-m-common-drilldown-arrow" + (theme.inverted ? "-inverse" : "")
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
+
+ onClicked: {
+ pageStack.push(Qt.resolvedUrl("TopicPage.qml"), {
+ boardUrl: forumPage.boardUrl,
+ topicId: model.topicId
+ });
+ }
}
}
diff --git a/qml/tapasboard/TopicPage.qml b/qml/tapasboard/TopicPage.qml
new file mode 100644
index 0000000..d39d36e
--- /dev/null
+++ b/qml/tapasboard/TopicPage.qml
@@ -0,0 +1,63 @@
+import QtQuick 1.1
+import com.nokia.meego 1.1
+import com.nokia.extras 1.1
+import com.javispedro.tapasboard 1.0
+
+Page {
+ id: topicPage
+
+ anchors.leftMargin: UiConstants.DefaultMargin
+ anchors.rightMargin: UiConstants.DefaultMargin
+
+ property string boardUrl;
+ property int topicId;
+
+ tools: ToolBarLayout {
+ ToolIcon {
+ id: backToolIcon
+ platformIconId: "toolbar-back"
+ anchors.left: parent.left
+ onClicked: pageStack.pop()
+ }
+ }
+
+ ListView {
+ id: postsView
+ anchors.fill: parent
+ model: TopicModel {
+ boardUrl: topicPage.boardUrl
+ topicId: topicPage.topicId
+ }
+ delegate: Item {
+ id: postItem
+
+ height: Math.max(postItemColumn.height + UiConstants.ButtonSpacing * 2, UiConstants.ListItemHeightDefault)
+ width: parent.width
+
+ Column {
+ id: postItemColumn
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+
+ Text {
+ text: model.title
+ width: parent.width
+ font: UiConstants.TitleFont
+ visible: text != ""
+ }
+
+ Text {
+ text: model.content
+ width: parent.width
+ font: UiConstants.SubtitleFont
+ wrapMode: Text.Wrap
+ }
+ }
+ }
+ }
+
+ ScrollDecorator {
+ flickableItem: postsView
+ }
+}
diff --git a/tapasboard.pro b/tapasboard.pro
index a3ab2f5..1ecefde 100644
--- a/tapasboard.pro
+++ b/tapasboard.pro
@@ -42,7 +42,9 @@ SOURCES += main.cpp \
fetchforumsaction.cpp \
boardmodel.cpp \
forummodel.cpp \
- fetchtopicsaction.cpp
+ fetchtopicsaction.cpp \
+ topicmodel.cpp \
+ fetchpostsaction.cpp
# Please do not modify the following two lines. Required for deployment.
include(qmlapplicationviewer/qmlapplicationviewer.pri)
@@ -69,4 +71,6 @@ HEADERS += \
global.h \
boardmodel.h \
forummodel.h \
- fetchtopicsaction.h
+ fetchtopicsaction.h \
+ topicmodel.h \
+ fetchpostsaction.h
diff --git a/topicmodel.cpp b/topicmodel.cpp
new file mode 100644
index 0000000..bbc2314
--- /dev/null
+++ b/topicmodel.cpp
@@ -0,0 +1,285 @@
+#include <QtCore/QDebug>
+#include <QtSql/QSqlError>
+
+#include "global.h"
+#include "board.h"
+#include "xmlrpcinterface.h"
+#include "fetchpostsaction.h"
+#include "topicmodel.h"
+
+TopicModel::TopicModel(QObject *parent) :
+ QAbstractListModel(parent), _boardUrl(), _board(0), _topicId(-1)
+{
+ QHash<int, QByteArray> roles = roleNames();
+ roles[TitleRole] = QByteArray("title");
+ roles[ContentRole] = QByteArray("content");
+ roles[IconRole] = QByteArray("icon");
+ roles[PostIdRole] = QByteArray("postcId");
+ setRoleNames(roles);
+}
+
+QString TopicModel::boardUrl() const
+{
+ return _boardUrl;
+}
+
+void TopicModel::setBoardUrl(const QString &url)
+{
+ if (_boardUrl != url) {
+ disconnect(this, SLOT(handleTopicPostsChanged(int,int,int)));
+ clearModel();
+ _board = 0;
+
+ _boardUrl = url;
+ if (!_boardUrl.isEmpty()) {
+ _board = board_manager->getBoard(_boardUrl);
+ connect(_board, SIGNAL(topicPostsChanged(int,int,int)),
+ SLOT(handleTopicPostsChanged(int,int,int)));
+ if (_topicId >= 0) {
+ update();
+ reload();
+ }
+ }
+ emit boardUrlChanged();
+ }
+}
+
+int TopicModel::topicId() const
+{
+ return _topicId;
+}
+
+void TopicModel::setTopicId(const int id)
+{
+ if (_topicId != id) {
+ clearModel();
+
+ _topicId = id;
+
+ if (_topicId >= 0 && _board) {
+ update();
+ reload();
+ }
+ emit topicIdChanged();
+ }
+}
+
+int TopicModel::rowCount(const QModelIndex &parent) const
+{
+ return parent.isValid() ? 0 : _data.size();
+}
+
+QVariant TopicModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid()) return QVariant();
+ const int row = index.row();
+ if (row >= _data.size()) {
+ qWarning() << "Could not seek to" << row;
+ return QVariant();
+ }
+
+ switch (role) {
+ case TitleRole:
+ return _data[row].title;
+ break;
+ case ContentRole:
+ return _data[row].content;
+ break;
+ case PostIdRole:
+ return _data[row].post_id;
+ break;
+ }
+
+ return QVariant();
+}
+
+bool TopicModel::canFetchMore(const QModelIndex &parent) const
+{
+ if (parent.isValid() || !_board) return false; // Invalid state
+ return !_eof;
+}
+
+void TopicModel::fetchMore(const QModelIndex &parent)
+{
+ if (parent.isValid()) return;
+ if (!_board) return;
+ if (_eof) return;
+
+ const int start = _data.size();
+ QList<Post> posts = loadPosts(start, start + TOPIC_PAGE_SIZE - 1);
+ const int new_end = start + _data.size() - 1;
+
+ if (posts.empty()) {
+ // We could not load anything more from DB!
+ _eof = true;
+ } else {
+ beginInsertRows(QModelIndex(), start, new_end);
+ _data.append(posts);
+ _eof = posts.size() < TOPIC_PAGE_SIZE; // If short read, we reached EOF.
+ endInsertRows();
+ }
+
+ if (_board->service()->isAccessible()) {
+ if (!_data.empty()) {
+ QDateTime last = oldestPostUpdate(posts);
+ // If the posts we got from DB are too old, refresh online.
+ if (last.secsTo(QDateTime::currentDateTime()) > TOPIC_POSTS_TLL) {
+ qDebug() << "Fetching posts because of old";
+ _board->enqueueAction(new FetchPostsAction(_topicId,
+ start,
+ new_end,
+ _board));
+ }
+ }
+
+ // Try to fetch more posts if board is online and we reached the end of DB
+ if (_eof) {
+ qDebug() << "Fetching posts because of EOF";
+ _board->enqueueAction(new FetchPostsAction(_topicId,
+ _data.size(),
+ _data.size() + FORUM_PAGE_SIZE - 1,
+ _board));
+ }
+ }
+}
+
+QDateTime TopicModel::parseDateTime(const QVariant &v)
+{
+ QString s = v.toString();
+ return QDateTime::fromString(s, Qt::ISODate);
+}
+
+QDateTime TopicModel::oldestPostUpdate(const QList<Post> &posts)
+{
+ if (posts.empty()) return QDateTime::currentDateTime();
+ QDateTime min = posts.first().last_update_time;
+ foreach (const Post& post, posts) {
+ if (min < post.last_update_time) min = post.last_update_time;
+ }
+ return min;
+}
+
+QDateTime TopicModel::lastTopPostUpdate()
+{
+ if (!_board) return QDateTime();
+ QSqlDatabase db = _board->database();
+ QSqlQuery query(db);
+ query.prepare("SELECT last_update_time FROM posts "
+ "WHERE topic_id = :topic_id "
+ "ORDER BY post_time ASC "
+ "LIMIT 1");
+ query.bindValue(":topic_id", _topicId);
+ if (query.exec()) {
+ if (query.next()) {
+ return parseDateTime(query.value(0));
+ }
+ } else {
+ qWarning() << "Could not fetch posts:" << query.lastError().text();
+ }
+ return QDateTime();
+}
+
+QList<TopicModel::Post> TopicModel::loadPosts(int start, int end)
+{
+ Q_ASSERT(_board);
+ const int rows = end - start + 1;
+ QList<Post> posts;
+ QSqlQuery query(_board->database());
+ query.prepare("SELECT post_id, post_title, post_content, post_time, last_update_time FROM posts "
+ "WHERE topic_id = :topic_id "
+ "ORDER by post_time ASC "
+ "LIMIT :start, :limit");
+ query.bindValue(":topic_id", _topicId);
+ query.bindValue(":start", start);
+ query.bindValue(":limit", rows);
+ if (query.exec()) {
+ posts.reserve(rows);
+ while (query.next()) {
+ Post post;
+ post.post_id = query.value(0).toInt();
+ post.title = query.value(1).toString();
+ post.content = query.value(2).toString();
+ post.time = parseDateTime(query.value(3));
+ post.last_update_time = parseDateTime(query.value(4));
+ posts.append(post);
+ }
+ } else {
+ qWarning() << "Could not load posts:" << query.lastError().text();
+ }
+ return posts;
+}
+
+void TopicModel::clearModel()
+{
+ beginResetModel();
+ _eof = false;
+ _data.clear();
+ endResetModel();
+}
+
+void TopicModel::handleTopicPostsChanged(int topicId, int start, int end)
+{
+ if (topicId == _topicId) {
+ // Yep, our posts list changed.
+ qDebug() << "My posts changed" << start << end;
+ if (end > _data.size()) {
+ // If for any reason we have more posts now, it means we might
+ // no longer be EOF...
+ _eof = false;
+ }
+ if (start > _data.size() + 1) {
+ // We are still not interested into these posts.
+ qDebug() << "Posts too far";
+ return;
+ }
+
+ QList<Post> posts = loadPosts(start, end);
+ if (posts.size() < end - start + 1) {
+ _eof = true; // Short read
+ end = start + posts.size() - 1;
+ }
+
+ if (end >= _data.size()) {
+ qDebug() << "Call insert rows" << _data.size() << end;
+ beginInsertRows(QModelIndex(), _data.size(), end);
+ _data.reserve(end + 1);
+ for (int i = start; i < _data.size(); i++) {
+ _data[i] = posts[i - start];
+ }
+ for (int i = _data.size(); i <= end; i++) {
+ Q_ASSERT(i >= start);
+ _data.append(posts[i - start]);
+ }
+ endInsertRows();
+ emit dataChanged(createIndex(start, 0), createIndex(_data.size() - 1, 0));
+ } else {
+ qDebug() << "Just refresh the data";
+ for (int i = start; i < end; i++) {
+ _data[i] = posts[i - start];
+ }
+ emit dataChanged(createIndex(start, 0), createIndex(end, 0));
+ }
+ }
+}
+
+void TopicModel::update()
+{
+ if (!_board || _topicId < 0) return;
+ // Start by requesting an update of the first 20 topics
+ if (!_board->service()->isAccessible()) return;
+ QDateTime last = lastTopPostUpdate();
+ if (!last.isValid() ||
+ last.secsTo(QDateTime::currentDateTime()) > TOPIC_TOP_TLL) {
+ // Outdated or empty, refresh.
+ _board->enqueueAction(new FetchPostsAction(_topicId, 0, TOPIC_PAGE_SIZE - 1, _board));
+ } else {
+ qDebug() << "Topics not outdated";
+ }
+}
+
+void TopicModel::reload()
+{
+ Q_ASSERT(_data.empty());
+ Q_ASSERT(!_eof);
+ fetchMore();
+}
diff --git a/topicmodel.h b/topicmodel.h
new file mode 100644
index 0000000..95a3f78
--- /dev/null
+++ b/topicmodel.h
@@ -0,0 +1,72 @@
+#ifndef TOPICMODEL_H
+#define TOPICMODEL_H
+
+#include <QtCore/QAbstractListModel>
+#include <QtCore/QDateTime>
+#include <QtSql/QSqlQuery>
+
+class Board;
+
+class TopicModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(QString boardUrl READ boardUrl WRITE setBoardUrl NOTIFY boardUrlChanged)
+ Q_PROPERTY(int topicId READ topicId WRITE setTopicId NOTIFY topicIdChanged)
+
+public:
+ TopicModel(QObject *parent = 0);
+
+ enum DataRoles {
+ TitleRole = Qt::DisplayRole,
+ IconRole = Qt::DecorationRole,
+ ContentRole = Qt::ToolTipRole,
+
+ PostIdRole = Qt::UserRole
+ };
+
+ QString boardUrl() const;
+ void setBoardUrl(const QString& url);
+
+ int topicId() const;
+ void setTopicId(const int id);
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const;
+ QVariant data(const QModelIndex &index, int role) const;
+
+ bool canFetchMore(const QModelIndex &parent = QModelIndex()) const;
+ void fetchMore(const QModelIndex &parent = QModelIndex());
+
+signals:
+ void boardUrlChanged();
+ void topicIdChanged();
+
+protected:
+ struct Post {
+ int post_id;
+ QString title;
+ QString content;
+ QDateTime time;
+ QDateTime last_update_time;
+ };
+
+private:
+ static QDateTime parseDateTime(const QVariant& v);
+ static QDateTime oldestPostUpdate(const QList<Post>& posts);
+ QDateTime lastTopPostUpdate();
+ QList<Post> loadPosts(int start, int end);
+ void clearModel();
+
+private slots:
+ void handleTopicPostsChanged(int forumId, int start, int end);
+ void update();
+ void reload();
+
+private:
+ QString _boardUrl;
+ Board *_board;
+ int _topicId;
+ QList<Post> _data;
+ bool _eof;
+};
+
+#endif // TOPICMODEL_H
diff --git a/xmlrpcinterface.cpp b/xmlrpcinterface.cpp
index bf61e12..4d55c8c 100644
--- a/xmlrpcinterface.cpp
+++ b/xmlrpcinterface.cpp
@@ -17,6 +17,9 @@ XmlRpcPendingCall *XmlRpcInterface::asyncCallWithArgumentList(const QString &met
QByteArray data = encodeCall(method, args);
request.setHeader(QNetworkRequest::ContentTypeHeader, "text/xml");
request.setRawHeader("User-Agent", "Tapasboard/1.0");
+#if XML_RPC_DEBUG
+ qDebug() << "Request:" << data;
+#endif
QNetworkReply *reply = _manager->post(request, data);
return new XmlRpcPendingCall(reply, this);
}
diff --git a/xmlrpcinterface.h b/xmlrpcinterface.h
index 1fce4f1..433d194 100644
--- a/xmlrpcinterface.h
+++ b/xmlrpcinterface.h
@@ -9,6 +9,8 @@
#include "xmlrpcpendingcall.h"
+#define XML_RPC_DEBUG 0
+
class XmlRpcInterface : public QObject
{
Q_OBJECT
diff --git a/xmlrpcpendingcall.cpp b/xmlrpcpendingcall.cpp
index d94b76d..8f318bd 100644
--- a/xmlrpcpendingcall.cpp
+++ b/xmlrpcpendingcall.cpp
@@ -157,6 +157,9 @@ void XmlRpcPendingCall::handleRequestFinished()
QNetworkReply::NetworkError error = _reply->error();
if (error == QNetworkReply::NoError) {
QByteArray data = _reply->readAll();
+#if XML_RPC_DEBUG
+ qDebug() << "Response:" << data;
+#endif
QXmlStreamReader reader(data);
bool parse_ok = false;
if (reader.readNextStartElement()) {