diff options
-rw-r--r-- | board.cpp | 132 | ||||
-rw-r--r-- | board.h | 8 | ||||
-rw-r--r-- | boardmodel.cpp | 10 | ||||
-rw-r--r-- | boardmodel.h | 1 | ||||
-rw-r--r-- | fetchforumsaction.cpp | 8 | ||||
-rw-r--r-- | fetchpostsaction.cpp | 89 | ||||
-rw-r--r-- | fetchpostsaction.h | 9 | ||||
-rw-r--r-- | fetchtopicsaction.cpp | 23 | ||||
-rw-r--r-- | forummodel.cpp | 34 | ||||
-rw-r--r-- | forummodel.h | 1 | ||||
-rw-r--r-- | global.h | 6 | ||||
-rw-r--r-- | qml/TopicPage.qml | 9 | ||||
-rw-r--r-- | topicmodel.cpp | 79 | ||||
-rw-r--r-- | topicmodel.h | 4 |
14 files changed, 341 insertions, 72 deletions
@@ -1,4 +1,5 @@ #include <QtCore/QDir> +#include <QtCore/QFileInfo> #include <QtCore/QDebug> #include <QtSql/QSqlQuery> #include <QtSql/QSqlError> @@ -55,6 +56,8 @@ Board::~Board() { disconnect(this, SLOT(handleActionFinished(Action*))); if (!_slug.isEmpty()) { + qDebug() << "Cleaning cache database"; + cleanDb(); QSqlDatabase::removeDatabase(_slug); } } @@ -257,12 +260,23 @@ void Board::notifyForumsChanged() emit forumsChanged(); } +void Board::notifyForumChanged(int forumId) +{ + emit forumChanged(forumId); +} + void Board::notifyForumTopicsChanged(int forumId, int start, int end) { qDebug() << "ForumTopics changed" << forumId << start << end; emit forumTopicsChanged(forumId, start, end); } +void Board::notifyForumTopicChanged(int forumId, int topicId) +{ + qDebug() << "ForumTopic changed" << forumId << topicId; + emit forumTopicChanged(forumId, topicId); +} + void Board::notifyTopicPostsChanged(int topicId, int start, int end) { qDebug() << "TopicPosts changed" << topicId << start << end; @@ -303,11 +317,29 @@ void Board::notifyLogout() } } -void Board::markPostAsRead(int postId) +void Board::markTopicAsRead(int topicId) { - _postsToMarkRead.insert(postId); - if (!_markReadDelay->isActive()) { - _markReadDelay->start(); + QSqlQuery q(_db); + q.prepare("UPDATE topics SET new_post = 0 WHERE topic_id = ? AND new_post = 1"); + q.bindValue(0, topicId); + if (q.exec()) { + if (q.numRowsAffected() > 0) { + q.prepare("SELECT forum_id FROM topics WHERE topic_id = ?"); + q.bindValue(0, topicId); + if (q.exec()) { + if (q.next()) { + int forum_id = q.value(0).toInt(); + notifyForumTopicChanged(forum_id, topicId); + updateForumReadState(forum_id); + } else { + qWarning() << "Could not get forum of topic"; + } + } else { + qWarning() << "Could not get forum of topic:" << q.lastError().text(); + } + } + } else { + qWarning() << "Could not mark topic as read:" << q.lastError().text(); } } @@ -337,6 +369,12 @@ QString Board::getTempDbPathFor(const QString& slug) return QDir::tempPath() + "/" + slug + ".sqlite"; } +int Board::dbSize() const +{ + QFileInfo info(getDbPathFor(_slug)); + return info.size(); +} + bool Board::checkCompatibleDb() { QString version = getConfig("tapasboard_db_version"); @@ -357,7 +395,7 @@ bool Board::initializeDb() return false; } - if (!q.exec("CREATE TABLE IF NOT EXISTS forums (forum_id INTEGER PRIMARY KEY, forum_name TEXT, description TEXT, parent_id INT, logo_url TEXT, new_post BOOL, is_protected BOOL, is_subscribed BOOL, can_subscribe BOOL, url TEXT, sub_only BOOL, sort_index INT)")) { + if (!q.exec("CREATE TABLE IF NOT EXISTS forums (forum_id INTEGER PRIMARY KEY, forum_name TEXT, description TEXT, parent_id INT, logo_url TEXT, new_post BOOL, is_protected BOOL, is_subscribed BOOL, can_subscribe BOOL, url TEXT, sub_only BOOL, position INT)")) { qWarning() << "Could not create forums table:" << q.lastError().text(); return false; } @@ -365,12 +403,12 @@ bool Board::initializeDb() qWarning() << "Could not create forums_parent index:" << q.lastError().text(); return false; } - if (!q.exec("CREATE UNIQUE INDEX IF NOT EXISTS forums_order ON forums (sort_index ASC)")) { + if (!q.exec("CREATE UNIQUE INDEX IF NOT EXISTS forums_order ON forums (position ASC)")) { qWarning() << "Could not create forums_order index:" << q.lastError().text(); return false; } - if (!q.exec("CREATE TABLE IF NOT EXISTS topics (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, reply_number INT, new_post BOOL, last_update_time TEXT)")) { + if (!q.exec("CREATE TABLE IF NOT EXISTS topics (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, reply_number INT, new_post BOOL, position INT, last_update_time TEXT)")) { qWarning() << "Could not create topics table:" << q.lastError().text(); return false; } @@ -382,8 +420,12 @@ bool Board::initializeDb() qWarning() << "Could not create topics_time index:" << q.lastError().text(); return false; } + if (!q.exec("CREATE INDEX IF NOT EXISTS topics_order ON topics (forum_id, position ASC)")) { + qWarning() << "Could not create topics_order index:" << q.lastError().text(); + 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)")) { + 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, position INT, last_update_time TEXT)")) { qWarning() << "Could not create posts table:" << q.lastError().text(); return false; } @@ -395,6 +437,10 @@ bool Board::initializeDb() qWarning() << "Could not create posts_time index:" << q.lastError().text(); return false; } + if (!q.exec("CREATE INDEX IF NOT EXISTS posts_order ON posts (topic_id, position ASC)")) { + qWarning() << "Could not create posts_order index:" << q.lastError().text(); + return false; + } return true; } @@ -430,11 +476,44 @@ bool Board::eraseDb() 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; + + int total_rows = 0; + + q.prepare("DELETE FROM topics WHERE last_update_time < ?"); + q.bindValue(0, QDateTime::currentDateTime().addDays(-FORUM_TOPICS_CACHE)); + if (q.exec()) { + total_rows += q.numRowsAffected(); + } else { + qWarning() << "Could not clean old topics:" << q.lastError().text(); } + + + q.prepare("DELETE FROM posts WHERE last_update_time < ?"); + q.bindValue(0, QDateTime::currentDateTime().addDays(-TOPIC_POSTS_CACHE)); + if (q.exec()) { + total_rows += q.numRowsAffected(); + } else { + qWarning() << "Could not clean old posts:" << q.lastError().text(); + } + + if (q.exec("DELETE FROM topics WHERE forum_id NOT IN (SELECT forum_id FROM forums)")) { + total_rows += q.numRowsAffected(); + } else { + qWarning() << "Could not clean unreferenced topics:" << q.lastError().text(); + } + if (q.exec("DELETE FROM posts WHERE topic_id NOT IN (SELECT topic_id FROM topics)")) { + total_rows += q.numRowsAffected(); + } else { + qWarning() << "Could not clean unreferenced posts:" << q.lastError().text(); + } + + if (total_rows > 100) { + qDebug() << "Vacuuming database"; + if (q.exec("VACUUM")) { + qWarning() << "Could not vacuum database:" << q.lastError().text(); + } + } + return true; } @@ -445,6 +524,7 @@ bool Board::removeFromActionQueue(Action *action) if (_queue.removeOne(action)) { if (!_queue.isEmpty() && head != _queue.head()) { // The head action was removed; advance the queue. + qDebug() << "Now running" << _queue.head(); executeActionFromQueue(); } action->deleteLater(); @@ -552,6 +632,34 @@ void Board::fetchForumsIfOutdated() } } +void Board::updateForumReadState(int forumId) +{ + QSqlQuery q(_db); + + q.prepare("SELECT COUNT() FROM topics WHERE forum_id = ? AND new_post = 1"); + q.bindValue(0, forumId); + if (!q.exec()) { + qWarning() << "Could not select unread topics from forum:" << q.lastError().text(); + return; + } + int unread_topics = q.value(0).toInt(); + int new_post = (unread_topics > 0) ? 1 : 0; + + q.prepare("UPDATE forums SET new_post = :new_post WHERE forum_id = :forum_id AND new_post != :cur_new_post"); + q.bindValue(":new_post", new_post); + q.bindValue(":forum_id", forumId); + q.bindValue(":cur_new_post", new_post); + + if (!q.exec()) { + qWarning() << "Could not update forum read status:" << q.lastError().text(); + return; + } + + if (q.numRowsAffected() > 0) { + notifyForumChanged(forumId); + } +} + void Board::handleActionFinished(Action *action) { qDebug() << action << "finished"; @@ -63,21 +63,25 @@ public slots: // These functions wrap emitting the signals below void notifyConfigChanged(const QString& key = QString()); void notifyForumsChanged(); + void notifyForumChanged(int forumId); void notifyForumTopicsChanged(int forumId, int start, int end); + void notifyForumTopicChanged(int forumId, int topicId); void notifyTopicPostsChanged(int topicId, int start, int end); void notifyTopicPostsUnread(int topicId, int position); void notifyLogin(const QMap<QString, QVariant>& info); void notifyLogout(); // Functions for marking posts as read - void markPostAsRead(int postId); + void markTopicAsRead(int topicId); signals: void busyChanged(); void loggedInChanged(); void configChanged(const QString& key); void forumsChanged(); + void forumChanged(int forumId); void forumTopicsChanged(int forumId, int start, int end); + void forumTopicChanged(int forumId, int topicId); void topicPostsChanged(int topicId, int start, int end); void topicPostsUnread(int topicId, int position); @@ -85,6 +89,7 @@ private: static QString createSlug(const QUrl& url); static QString getDbPathFor(const QString& slug); static QString getTempDbPathFor(const QString& slug); + int dbSize() const; bool checkCompatibleDb(); bool initializeDb(); bool eraseDb(); @@ -96,6 +101,7 @@ private: void initializeMarkRead(); void fetchConfigIfOutdated(); void fetchForumsIfOutdated(); + void updateForumReadState(int forumId); private slots: void handleActionFinished(Action *action); diff --git a/boardmodel.cpp b/boardmodel.cpp index 3e24769..8d37fa5 100644 --- a/boardmodel.cpp +++ b/boardmodel.cpp @@ -29,9 +29,11 @@ void BoardModel::setBoard(Board *board) { if (_board != board) { disconnect(this, SLOT(reload())); + disconnect(this, SLOT(handleForumChanged(int))); _board = board; if (_board) { connect(board, SIGNAL(forumsChanged()), SLOT(reload())); + connect(board, SIGNAL(forumChanged(int)), SLOT(handleForumChanged(int))); } reload(); emit boardChanged(); @@ -141,7 +143,7 @@ void BoardModel::reload() "LEFT JOIN forums f2 ON f2.forum_id = f1.parent_id " "WHERE (f1.parent_id=:parent_id_1 AND f1.sub_only = 0) OR f1.parent_id IN " "(SELECT forum_id from forums WHERE parent_id=:parent_id_2 AND sub_only=1) " - "ORDER by f1.sort_index ASC;"); + "ORDER by f1.position ASC;"); _query.bindValue(0, _forumId); _query.bindValue(1, _forumId); if (!_query.exec()) { @@ -152,3 +154,9 @@ void BoardModel::reload() endResetModel(); fetchMore(); // So that at least a few rows are sent } + +void BoardModel::handleForumChanged(int forumId) +{ + Q_UNUSED(forumId); + reload(); // TODO improve +} diff --git a/boardmodel.h b/boardmodel.h index 35ed447..ba4cda9 100644 --- a/boardmodel.h +++ b/boardmodel.h @@ -47,6 +47,7 @@ signals: private slots: void reload(); + void handleForumChanged(int forumId); private: Board *_board; diff --git a/fetchforumsaction.cpp b/fetchforumsaction.cpp index c3b77cd..8bbe23b 100644 --- a/fetchforumsaction.cpp +++ b/fetchforumsaction.cpp @@ -39,8 +39,8 @@ void FetchForumsAction::handleFinishedCall() handleDatabaseError("truncating forums table", db.lastError()); } QSqlQuery query(db); - query.prepare("INSERT INTO forums (forum_id, forum_name, description, parent_id, logo_url, new_post, is_protected, is_subscribed, can_subscribe, url, sub_only, sort_index)" - "VALUES (:forum_id, :forum_name, :description, :parent_id, :logo_url, :new_post, :is_protected, :is_subscribed, :can_subscribe, :url, :sub_only, :sort_index)"); + query.prepare("INSERT INTO forums (forum_id, forum_name, description, parent_id, logo_url, new_post, is_protected, is_subscribed, can_subscribe, url, sub_only, position)" + "VALUES (:forum_id, :forum_name, :description, :parent_id, :logo_url, :new_post, :is_protected, :is_subscribed, :can_subscribe, :url, :sub_only, :position)"); foreach (const QVariant& list_element, list) { QVariantMap map = list_element.toMap(); @@ -67,7 +67,7 @@ void FetchForumsAction::handleFinishedCall() query.bindValue(":can_subscribe", map["can_subscribed"].toBool() ? 1 : 0); query.bindValue(":url", map["url"].toString()); query.bindValue(":sub_only", map["sub_only"].toBool() ? 1 : 0); - query.bindValue(":sort_index", map["sort_index"]); + query.bindValue(":position", map["position"]); if (!query.exec()) { qWarning() << "Failed to store forum info for:" << forum_id; @@ -95,7 +95,7 @@ QList<QVariantMap> FetchForumsAction::flattenForumList(const QVariantList &list, QList<QVariantMap> flattened; foreach (const QVariant& list_element, list) { QVariantMap map = list_element.toMap(); - map["sort_index"] = (*order)++; + map["position"] = (*order)++; QVariantMap::iterator child_key = map.find("child"); if (child_key != map.end()) { // There are children, so flatten them too. diff --git a/fetchpostsaction.cpp b/fetchpostsaction.cpp index e38e423..1330f44 100644 --- a/fetchpostsaction.cpp +++ b/fetchpostsaction.cpp @@ -19,7 +19,15 @@ bool FetchPostsAction::isSupersetOf(Action *action) const FetchPostsAction *other = qobject_cast<FetchPostsAction*>(action); if (other) { if (other->_topicId == _topicId) { - if (_end == FetchAllPosts) { + if (_start == FetchUnreadPosts || other->_start == FetchUnreadPosts) { + if (_start == other->_start) { + // This action supersets if it fetches a larger number of posts + return _end >= other->_end; + } else { + // One fetches unread posts, the other does not. + return false; + } + } else if (_end == FetchAllPosts) { // If this is fetching all posts, then this is a superset // of every other action... except those that also fetch all. return other->_end != FetchAllPosts; @@ -37,15 +45,25 @@ bool FetchPostsAction::isSupersetOf(Action *action) const void FetchPostsAction::execute() { - int end = _end; - if (end == FetchAllPosts) { - // Fetch posts in blocks of size 50. - end = _start + MAX_TOPIC_PAGE_SIZE; - // After finishing this action, a new one will be enqueued with the next 50. + if (_start == FetchUnreadPosts) { + Q_ASSERT(_end >= 0); + qDebug() << "Fetch unread posts" << _end; + _call = _board->service()->asyncCall("get_thread_by_unread", + QString::number(_topicId), _end); + } else { + Q_ASSERT(_start >= 0); + int end = _end; + if (end == FetchAllPosts) { + // Fetch posts in blocks of size 50. + end = _start + MAX_TOPIC_PAGE_SIZE; + // After finishing this action, a new one will be enqueued with the next 50. + } + + qDebug() << "Fetch posts" << _start << end; + _call = _board->service()->asyncCall("get_thread", + QString::number(_topicId), _start, end); } - _call = _board->service()->asyncCall("get_thread", - QString::number(_topicId), _start, end); _call->setParent(this); connect(_call, SIGNAL(finished(XmlRpcPendingCall*)), SLOT(handleFinishedCall())); } @@ -65,11 +83,37 @@ void FetchPostsAction::handleFinishedCall() // Not fatal, just assume it's the one we requested topic_id = _topicId; } - QString topic_title = unencodePostContent(map["topic_title"]); + const QString topic_title = unencodePostText(map["topic_title"]); + const int unread_position = map["position"].toInt() - 1; + const int total_post_num = map["total_post_num"].toInt(); + + int start; + if (_start == FetchUnreadPosts) { + // This requires some calculations + const int page_size = _end; + Q_ASSERT(page_size >= 0); + if (unread_position < 0) { + qWarning() << "Invalid unread position in reply"; + posts.clear(); // Do not add any posts if this happens + } + const int unread_page = unread_position / page_size; + start = unread_page * page_size; + } else { + Q_ASSERT(_start >= 0); + start = _start; + } + if (start >= total_post_num) { + qDebug() << "No more posts"; + if (!posts.isEmpty()) { + qDebug() << "Yet we got posts from the service!"; + posts.clear(); // Sometimes + } + } + int position = start; 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)"); + 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, position, last_update_time) " + "VALUES (:topic_id, :post_id, :post_title, :post_content, :post_author_id, :post_author_name, :can_edit, :icon_url, :post_time, :position, :last_update_time)"); foreach (const QVariant& post_v, posts) { QVariantMap post = post_v.toMap(); @@ -89,8 +133,13 @@ void FetchPostsAction::handleFinishedCall() 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(":position", position); query.bindValue(":last_update_time", QDateTime::currentDateTime()); + Q_ASSERT(position < total_post_num); + + position++; + if (!query.exec()) { qWarning() << "Failed to store topic info for:" << topic_id; handleDatabaseError("storing topic info", query); @@ -99,15 +148,25 @@ void FetchPostsAction::handleFinishedCall() } db.commit(); - if (posts.size() > 0) { + + if (!posts.isEmpty()) { + Q_ASSERT(start >= 0); + Q_ASSERT(position - 1 >= start); _board->notifyTopicPostsChanged(_topicId, - _start, _start + posts.size() - 1); + start, position - 1); + } + if (_start == FetchUnreadPosts && unread_position >= 0) { + _board->notifyTopicPostsUnread(_topicId, unread_position); } if (_end == FetchAllPosts && posts.size() == MAX_TOPIC_PAGE_SIZE) { // Ok, let's prepare to fetch the next block of posts because // there are probably more of them - int start = _start + MAX_TOPIC_PAGE_SIZE; - _board->enqueueAction(new FetchPostsAction(_topicId, start, FetchAllPosts, _board)); + Q_ASSERT(_start >= 0); + int next_start = start + MAX_TOPIC_PAGE_SIZE; + _board->enqueueAction(new FetchPostsAction(_topicId, + next_start, + FetchAllPosts, + _board)); } } else { qWarning() << "Could not fetch posts"; diff --git a/fetchpostsaction.h b/fetchpostsaction.h index e122622..66e6c73 100644 --- a/fetchpostsaction.h +++ b/fetchpostsaction.h @@ -11,12 +11,15 @@ class FetchPostsAction : public Action { Q_OBJECT public: - explicit FetchPostsAction(int topicId, int start, int end, Board *board); - enum { - FetchAllPosts = -1 + /** Use as 'end' parameter; 'start' then indicates starting post. */ + FetchAllPosts = -1, + /** Use as 'start' parameter; 'end' then indicates number of posts. */ + FetchUnreadPosts = -2 }; + explicit FetchPostsAction(int topicId, int start, int end, Board *board); + bool isSupersetOf(Action *action) const; void execute(); diff --git a/fetchtopicsaction.cpp b/fetchtopicsaction.cpp index bc94965..13aebdd 100644 --- a/fetchtopicsaction.cpp +++ b/fetchtopicsaction.cpp @@ -63,8 +63,11 @@ void FetchTopicsAction::handleFinishedCall() db.transaction(); QSqlQuery query(db); - query.prepare("INSERT OR REPLACE INTO topics (forum_id, topic_id, topic_title, topic_author_id, topic_author_name, is_subscribed, is_closed, icon_url, last_reply_time, reply_number, new_post, last_update_time) " - "VALUES (:forum_id, :topic_id, :topic_title, :topic_author_id, :topic_author_name, :is_subscribed, :is_closed, :icon_url, :last_reply_time, :reply_number, :new_post, :last_update_time)"); + query.prepare("INSERT OR REPLACE INTO topics (forum_id, topic_id, topic_title, topic_author_id, topic_author_name, is_subscribed, is_closed, icon_url, last_reply_time, reply_number, new_post, position, last_update_time) " + "VALUES (:forum_id, :topic_id, :topic_title, :topic_author_id, :topic_author_name, :is_subscribed, :is_closed, :icon_url, :last_reply_time, :reply_number, :new_post, :position, :last_update_time)"); + + Q_ASSERT(_start >= 0); + int position = _start; foreach (const QVariant& topic_v, topics) { QVariantMap topic = topic_v.toMap(); @@ -91,8 +94,11 @@ void FetchTopicsAction::handleFinishedCall() query.bindValue(":last_reply_time", topic["last_reply_time"].toDateTime()); query.bindValue(":reply_number", topic["reply_number"].toInt()); query.bindValue(":new_post", topic["new_post"].toBool() ? 1 : 0); + query.bindValue(":position", position); query.bindValue(":last_update_time", QDateTime::currentDateTime()); + position++; + if (!query.exec()) { qWarning() << "Failed to store topic info for:" << topic_id; handleDatabaseError("storing topic info", query); @@ -101,15 +107,20 @@ void FetchTopicsAction::handleFinishedCall() } db.commit(); - if (topics.size() > 0) { + if (!topics.isEmpty()) { + Q_ASSERT(_start >= 0); + Q_ASSERT(position - 1 >= _start); _board->notifyForumTopicsChanged(_forumId, - _start, _start + topics.size() - 1); + _start, position - 1); } if (_end == FetchAllTopics && topics.size() == MAX_FORUM_PAGE_SIZE) { // Ok, let's prepare to fetch the next block of topics because // there are probably more of them - int start = _start + MAX_FORUM_PAGE_SIZE; - _board->enqueueAction(new FetchTopicsAction(_forumId, start, FetchAllTopics, _board)); + int next_start = _start + MAX_FORUM_PAGE_SIZE; + _board->enqueueAction(new FetchTopicsAction(_forumId, + next_start, + FetchAllTopics, + _board)); } } else { qWarning() << "Could not fetch topics"; diff --git a/forummodel.cpp b/forummodel.cpp index faf0b75..7606f93 100644 --- a/forummodel.cpp +++ b/forummodel.cpp @@ -28,6 +28,7 @@ void ForumModel::setBoard(Board *board) { if (_board != board) { disconnect(this, SLOT(handleForumTopicsChanged(int,int,int))); + disconnect(this, SLOT(handleForumTopicChanged(int,int))); clearModel(); _board = board; @@ -35,6 +36,8 @@ void ForumModel::setBoard(Board *board) if (_board) { connect(_board, SIGNAL(forumTopicsChanged(int,int,int)), SLOT(handleForumTopicsChanged(int,int,int))); + connect(_board, SIGNAL(forumTopicChanged(int,int)), + SLOT(handleForumTopicChanged(int,int))); if (_forumId >= 0) { update(); reload(); @@ -175,7 +178,7 @@ QDateTime ForumModel::lastTopPostUpdate() QSqlQuery query(db); query.prepare("SELECT last_update_time FROM topics " "WHERE forum_id = :forum_id " - "ORDER BY last_reply_time DESC " + "ORDER BY position ASC " "LIMIT 1"); query.bindValue(":forum_id", _forumId); if (query.exec()) { @@ -191,18 +194,16 @@ QDateTime ForumModel::lastTopPostUpdate() QList<ForumModel::Topic> ForumModel::loadTopics(int start, int end) { Q_ASSERT(_board); - const int rows = end - start + 1; QList<Topic> topics; QSqlQuery query(_board->database()); query.prepare("SELECT topic_id, topic_title, reply_number, new_post, last_reply_time, last_update_time FROM topics " - "WHERE forum_id = :forum_id " - "ORDER by last_reply_time DESC " - "LIMIT :start, :limit"); + "WHERE forum_id = :forum_id AND position BETWEEN :start AND :end " + "ORDER by position ASC "); query.bindValue(":forum_id", _forumId); query.bindValue(":start", start); - query.bindValue(":limit", rows); + query.bindValue(":end", end); if (query.exec()) { - topics.reserve(rows); + topics.reserve(end - start + 1); while (query.next()) { Topic topic; topic.topic_id = query.value(0).toInt(); @@ -272,6 +273,25 @@ void ForumModel::handleForumTopicsChanged(int forumId, int start, int end) } } +void ForumModel::handleForumTopicChanged(int forumId, int topicId) +{ + if (forumId == _forumId) {qDebug() << "Me topic cha"; + for (int i = 0; i < _data.size(); i++) { + Topic& topic = _data[i]; + if (topic.topic_id == topicId) {qDebug() << "Me topic cha cha"; + // Need to refresh this topic + QList<Topic> topics = loadTopics(i, i); + if (topics.size() == 1) { + _data[i] = topics[0]; + emit dataChanged(createIndex(i, 0), createIndex(i, 0)); + } else { + qWarning() << "Topic changed yet not in DB"; + } + } + } + } +} + void ForumModel::update() { if (!_board || _forumId < 0) return; diff --git a/forummodel.h b/forummodel.h index d0dd980..33a718e 100644 --- a/forummodel.h +++ b/forummodel.h @@ -64,6 +64,7 @@ private: private slots: void handleForumTopicsChanged(int forumId, int start, int end); + void handleForumTopicChanged(int forumId, int topicId); void update(); void reload(); @@ -18,6 +18,9 @@ /** Time we should consider other topics in a forum up to date, in seconds. */ #define FORUM_TOPICS_TLL 15 * 60 +/** Time we should keep topics in the cache, in days. */ +#define FORUM_TOPICS_CACHE 10 + /** Number of topics per "block" in subforum view */ #define FORUM_PAGE_SIZE 20 @@ -30,6 +33,9 @@ /** Time we should consider other posts in a topic up to date, in seconds. */ #define TOPIC_POSTS_TLL 15 * 60 +/** Time we should keep topics in the cache, in days. */ +#define TOPIC_POSTS_CACHE 10 + /** Number of posts per "block" in topic view */ #define TOPIC_PAGE_SIZE 20 diff --git a/qml/TopicPage.qml b/qml/TopicPage.qml index 689dce7..9835a06 100644 --- a/qml/TopicPage.qml +++ b/qml/TopicPage.qml @@ -36,6 +36,11 @@ Page { id: topicModel board: topicPage.board topicId: topicPage.topicId + + onFirstUnreadPostChanged: { + postsView.positionViewAtIndex(firstUnreadPost, ListView.Beginning); + topicModel.markAsRead(); + } } section.property: "humanDate" section.criteria: ViewSection.FullString @@ -56,9 +61,13 @@ Page { height: postItemColumn.height + UiConstants.DefaultMargin anchors.centerIn: parent + visible: model.postId >= 0 color: "white" radius: 20 + border.width: model.unread ? 2 : 0 + border.color: "black" + Column { id: postItemColumn anchors.left: parent.left diff --git a/topicmodel.cpp b/topicmodel.cpp index 6b4991d..d3032be 100644 --- a/topicmodel.cpp +++ b/topicmodel.cpp @@ -20,6 +20,7 @@ TopicModel::TopicModel(QObject *parent) : roles[DateTimeRole] = QByteArray("dateTime"); roles[HumanDateRole] = QByteArray("humanDate"); roles[HumanTimeRole] = QByteArray("humanTime"); + roles[UnreadRole] = QByteArray("unread"); setRoleNames(roles); } @@ -94,8 +95,23 @@ QVariant TopicModel::data(const QModelIndex &index, int role) const if (_data[row].post_id < 0) { // This post is a unfetched stub - this->fetchPost(row); - return QVariant(); // Will update the model once the post arrives + switch (role) { + case PostIdRole: + this->fetchPost(row); + return QVariant::fromValue(-1); + case TitleRole: + case ContentRole: + case UserNameRole: + case HumanDateRole: + case HumanTimeRole: + return QVariant::fromValue(QString()); + case DateTimeRole: + return QVariant::fromValue(QDateTime()); + case UnreadRole: + return QVariant::fromValue(_firstUnread >= 0 && row >= _firstUnread); + default: + return QVariant(); + } } switch (role) { // Mind the lack of break statements @@ -115,6 +131,8 @@ QVariant TopicModel::data(const QModelIndex &index, int role) const return _board->renderHumanDate(_data[row].time); case HumanTimeRole: return _board->renderHumanTime(_data[row].time); + case UnreadRole: + return _firstUnread >= 0 && row >= _firstUnread; } return QVariant(); @@ -180,6 +198,11 @@ void TopicModel::refresh() _board)); } +void TopicModel::markAsRead() +{ + _board->markTopicAsRead(_topicId); +} + QDateTime TopicModel::parseDbDateTime(const QVariant &v) { QString s = v.toString(); @@ -203,7 +226,7 @@ QDateTime TopicModel::lastTopPostUpdate() QSqlQuery query(db); query.prepare("SELECT last_update_time FROM posts " "WHERE topic_id = :topic_id " - "ORDER BY post_time ASC " + "ORDER BY position ASC " "LIMIT 1"); query.bindValue(":topic_id", _topicId); if (query.exec()) { @@ -219,19 +242,17 @@ QDateTime TopicModel::lastTopPostUpdate() 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_author_id, post_author_name, post_time, last_update_time FROM posts " - "WHERE topic_id = :topic_id " - "ORDER by post_time ASC " - "LIMIT :start, :limit"); + "WHERE topic_id = :topic_id AND position BETWEEN :start AND :end " + "ORDER by position ASC "); query.bindValue(":topic_id", _topicId); query.bindValue(":start", start); - query.bindValue(":limit", rows); + query.bindValue(":end", end); if (query.exec()) { int loaded = 0; - posts.reserve(rows); + posts.reserve(end - start + 1); while (query.next()) { Post post; post.post_id = query.value(0).toInt(); @@ -254,13 +275,12 @@ void TopicModel::fetchPost(int position) const { if (_board->service()->isAccessible()) { // There are lest posts on the DB than we wanted - qDebug() << "Fetching posts because of unfetched"; + qDebug() << "Fetching post" << position << "because of unfetched"; // Always fetch one page at least. - int fetch_start = position % FORUM_PAGE_SIZE; - int fetch_end = fetch_start + FORUM_PAGE_SIZE; - _board->enqueueAction(new FetchPostsAction(_topicId, - fetch_start, fetch_end, - _board)); + int start = (position / FORUM_PAGE_SIZE) * FORUM_PAGE_SIZE; + int end = (start + FORUM_PAGE_SIZE) - 1; + qDebug() << "From" << start << "to" << end; + _board->enqueueAction(new FetchPostsAction(_topicId, start, end, _board)); } } @@ -277,6 +297,7 @@ void TopicModel::enlargeModel(int end) _data.append(post); } endInsertRows(); + Q_ASSERT(_data.size() == end + 1); } } @@ -298,17 +319,19 @@ void TopicModel::handleTopicPostsChanged(int topicId, int start, int end) _eof = false; } if (start > _data.size() + 1) { - // We are still not interested into these posts. - qDebug() << "Posts too far"; - return; + enlargeModel(start); } QList<Post> posts = loadPosts(start, end); if (posts.size() < end - start + 1) { - _eof = true; // Short read - end = start + posts.size() - 1; + // Short read + qWarning() << "Short read while handling changed posts"; + return; // This should not happen, really. } + Q_ASSERT(start <= _data.size()); + Q_ASSERT(end >= 0); + if (end >= _data.size()) { qDebug() << "Call insert rows (changed):" << _data.size() << end; beginInsertRows(QModelIndex(), _data.size(), end); @@ -324,7 +347,7 @@ void TopicModel::handleTopicPostsChanged(int topicId, int start, int end) emit dataChanged(createIndex(start, 0), createIndex(_data.size() - 1, 0)); } else { qDebug() << "Just refresh the data"; - for (int i = start; i < end; i++) { + for (int i = start; i <= end; i++) { _data[i] = posts[i - start]; } emit dataChanged(createIndex(start, 0), createIndex(end, 0)); @@ -338,6 +361,7 @@ void TopicModel::handleTopicPostsUnread(int topicId, int position) if (position != _firstUnread) { enlargeModel(position); _firstUnread = position; + qDebug() << "Changing unread post to" << position; emit firstUnreadPostChanged(); } } @@ -351,8 +375,19 @@ void TopicModel::update() QDateTime last = lastTopPostUpdate(); if (!last.isValid() || last.secsTo(QDateTime::currentDateTime()) > TOPIC_TOP_TLL) { + qDebug() << "Fetching posts because the top are old"; // Outdated or empty, refresh. - _board->enqueueAction(new FetchPostsAction(_topicId, 0, TOPIC_PAGE_SIZE - 1, _board)); + if (_board->loggedIn() && _board->getConfig("goto_unread") == "1") { + _board->enqueueAction(new FetchPostsAction(_topicId, + FetchPostsAction::FetchUnreadPosts, + TOPIC_PAGE_SIZE, + _board)); + } else { + _board->enqueueAction(new FetchPostsAction(_topicId, + 0, + TOPIC_PAGE_SIZE - 1, + _board)); + } } else { qDebug() << "Topics not outdated"; } diff --git a/topicmodel.h b/topicmodel.h index 67e48c4..d0c60bb 100644 --- a/topicmodel.h +++ b/topicmodel.h @@ -27,7 +27,8 @@ public: UserNameRole, DateTimeRole, HumanDateRole, - HumanTimeRole + HumanTimeRole, + UnreadRole }; Board * board() const; @@ -46,6 +47,7 @@ public: public slots: void refresh(); + void markAsRead(); signals: void boardChanged(); |