#include #include #include "global.h" #include "board.h" #include "xmlrpcinterface.h" #include "fetchpostsaction.h" #include "topicmodel.h" TopicModel::TopicModel(QObject *parent) : QAbstractListModel(parent), _board(0), _topicId(-1), _eof(false), _firstUnread(-1) { QHash roles = roleNames(); roles[TitleRole] = QByteArray("title"); roles[ContentRole] = QByteArray("content"); roles[IconRole] = QByteArray("icon"); roles[PostIdRole] = QByteArray("postId"); roles[UserIdRole] = QByteArray("userId"); roles[UserNameRole] = QByteArray("userName"); roles[DateTimeRole] = QByteArray("dateTime"); roles[HumanDateRole] = QByteArray("humanDate"); roles[HumanTimeRole] = QByteArray("humanTime"); roles[UnreadRole] = QByteArray("unread"); setRoleNames(roles); } Board * TopicModel::board() const { return _board; } void TopicModel::setBoard(Board *board) { if (_board != board) { disconnect(this, SLOT(handleTopicPostsChanged(int,int,int))); disconnect(this, SLOT(handleTopicPostsUnread(int,int))); clearModel(); _board = board; if (_board) { connect(_board, SIGNAL(topicPostsChanged(int,int,int)), SLOT(handleTopicPostsChanged(int,int,int))); connect(_board, SIGNAL(topicPostsUnread(int,int)), SLOT(handleTopicPostsUnread(int,int))); if (_topicId >= 0) { update(); reload(); } } emit boardChanged(); } } 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::firstUnreadPost() const { return _firstUnread; } 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(); if (!_board) return QVariant(); const int row = index.row(); if (row >= _data.size()) { qWarning() << "Could not seek to" << row; return QVariant(); } if (_data[row].post_id < 0) { // This post is a unfetched stub 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 IconRole: return QVariant::fromValue(QUrl()); 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 case TitleRole: return _data[row].title; case ContentRole: return _data[row].content; case IconRole: return _data[row].icon; case PostIdRole: return _data[row].post_id; case UserIdRole: return _data[row].user_id; case UserNameRole: return _data[row].user_name; case DateTimeRole: return _data[row].time; case HumanDateRole: return _board->formatDateTime(_data[row].time, Board::RelativeDate); case HumanTimeRole: return _board->formatDateTime(_data[row].time, Board::ShowTime | Board::RelativeTime | Board::TodayYesterday); case UnreadRole: return _firstUnread >= 0 && row >= _firstUnread; } return QVariant(); } bool TopicModel::canFetchMore(const QModelIndex &parent) const { if (parent.isValid() || !_board || _topicId < 0) return false; // Invalid state return !_eof; } void TopicModel::fetchMore(const QModelIndex &parent) { if (parent.isValid()) return; if (!_board || _topicId < 0) return; if (_eof) return; const int start = _data.size(); QList posts = loadPosts(start, start + TOPIC_PAGE_SIZE - 1); const int new_end = start + posts.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 (!posts.empty()) { QDateTime last = oldestPostUpdate(posts); // If the posts we got from DB are too old, refresh online. if (last.secsTo(QDateTime::currentDateTimeUtc()) > TOPIC_POSTS_TLL) { qDebug() << "Fetching posts because of old"; Q_ASSERT(new_end >= 0); _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)); } } } void TopicModel::refresh() { // Forcefully refresh all posts on this topic _board->enqueueAction(new FetchPostsAction(_topicId, 0, FetchPostsAction::FetchAllPosts, _board)); } void TopicModel::markAsRead() { _board->markTopicAsRead(_topicId); } QDateTime TopicModel::parseDbDateTime(const QVariant &v) { QString s = v.toString(); QDateTime dt = QDateTime::fromString(s, Qt::ISODate); dt.setTimeSpec(Qt::UTC); return dt; } QDateTime TopicModel::oldestPostUpdate(const QList &posts) { if (posts.empty()) return QDateTime::currentDateTimeUtc(); 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 position ASC " "LIMIT 1"); query.bindValue(":topic_id", _topicId); if (query.exec()) { if (query.next()) { return parseDbDateTime(query.value(0)); } } else { qWarning() << "Could not load top posts:" << query.lastError().text(); } return QDateTime(); } QList TopicModel::loadPosts(int start, int end) { Q_ASSERT(_board); QList posts; QSqlQuery query(_board->database()); query.prepare("SELECT post_id, post_title, post_content, icon_url, post_author_id, post_author_name, post_time, last_update_time FROM posts " "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(":end", end); if (query.exec()) { int loaded = 0; posts.reserve(end - start + 1); 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.icon = query.value(3).toUrl(); post.user_id = query.value(4).toInt(); post.user_name = query.value(5).toString(); post.time = parseDbDateTime(query.value(6)); post.last_update_time = parseDbDateTime(query.value(7)); posts.append(post); loaded++; } } else { qWarning() << "Could not load posts:" << query.lastError().text(); } return posts; } void TopicModel::fetchPost(int position) const { if (_board->service()->isAccessible()) { // There are less posts on the DB than we wanted qDebug() << "Fetching post" << position << "because of unfetched"; // Always fetch one page at least. int start = (position / TOPIC_PAGE_SIZE) * TOPIC_PAGE_SIZE; int end = (start + TOPIC_PAGE_SIZE) - 1; qDebug() << "From" << start << "to" << end; _board->enqueueAction(new FetchPostsAction(_topicId, start, end, _board)); } } void TopicModel::enlargeModel(int end) { int start = _data.size(); if (end > start) { qDebug() << "Call insert rows (enlarge):" << start << end; beginInsertRows(QModelIndex(), start, end); Post post; post.post_id = -1; for (int i = start; i <= end; i++) { Q_ASSERT(_data.size() == i); _data.append(post); } endInsertRows(); Q_ASSERT(_data.size() == end + 1); } } 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. 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) { enlargeModel(start); } QList posts = loadPosts(start, end); if (posts.size() < end - start + 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); _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::handleTopicPostsUnread(int topicId, int position) { if (topicId == _topicId) { if (position != _firstUnread) { enlargeModel(position); _firstUnread = position; qDebug() << "Changing unread post to" << position; emit firstUnreadPostChanged(); } } } 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::currentDateTimeUtc()) > TOPIC_TOP_TLL) { qDebug() << "Fetching posts because the top are old"; // Outdated or empty, refresh. 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"; } } void TopicModel::reload() { Q_ASSERT(_data.empty()); Q_ASSERT(!_eof); fetchMore(); }