diff --git a/retroshare-gui/src/gui/gxsforums/GxsForumModel.cpp b/retroshare-gui/src/gui/gxsforums/GxsForumModel.cpp index 91836723f..08eb676d8 100644 --- a/retroshare-gui/src/gui/gxsforums/GxsForumModel.cpp +++ b/retroshare-gui/src/gui/gxsforums/GxsForumModel.cpp @@ -2,15 +2,24 @@ #include #include +#include "util/qtthreadsutils.h" #include "GxsForumModel.h" +#include "retroshare/rsgxsforums.h" #define DEBUG_FORUMMODEL -#define COLUMN_TITLE 0 -#define COLUMN_READ_STATUS 1 -#define COLUMN_DATE 2 -#define COLUMN_AUTHOR 3 -#define COLUMN_COUNT 4 +#define COLUMN_THREAD_TITLE 0 +#define COLUMN_THREAD_READ 1 +#define COLUMN_THREAD_DATE 2 +#define COLUMN_THREAD_DISTRIBUTION 3 +#define COLUMN_THREAD_AUTHOR 4 +#define COLUMN_THREAD_SIGNED 5 +#define COLUMN_THREAD_CONTENT 6 +#define COLUMN_THREAD_COUNT 7 +#define COLUMN_THREAD_MSGID 8 +#define COLUMN_THREAD_NB_COLUMNS 9 + +#define COLUMN_THREAD_DATA 0 // column for storing the userdata like parentid Q_DECLARE_METATYPE(RsMsgMetaData); @@ -21,31 +30,29 @@ RsGxsForumModel::RsGxsForumModel(QObject *parent) { mPosts.resize(1); // adds a sentinel item - // adds some fake posts to debug - - int N=5 ; - mPosts.resize(N+1); - - for(int i=1;i<=N;++i) - { - mPosts[0].children.push_back(ForumModelIndex(i)); - mPosts[i].parent = ForumModelIndex(0); - mPosts[i].prow = i-1; - - RsMsgMetaData meta; - meta.mMsgName = std::string("message ") + QString::number(i).toStdString() ; - mPosts[i].meta_versions.push_back(meta); - } - - // add one child to last post - mPosts.resize(N+2); - mPosts[N].children.push_back(ForumModelIndex(N+1)); - mPosts[N+1].parent = ForumModelIndex(N); - mPosts[N+1].prow = 0; - - RsMsgMetaData meta; - meta.mMsgName = std::string("message ") + QString::number(N+1).toStdString() ; - mPosts[N+1].meta_versions.push_back(meta); +// // adds some fake posts to debug +// +// int N=5 ; +// mPosts.resize(N+1); +// +// for(int i=1;i<=N;++i) +// { +// mPosts[0].mChildren.push_back(ForumModelIndex(i)); +// mPosts[i].mParent = ForumModelIndex(0); +// mPosts[i].prow = i-1; +// +// RsMsgMetaData meta; +// meta.mMsgName = std::string("message ") + QString::number(i).toStdString() ; +// } +// +// // add one child to last post +// mPosts.resize(N+2); +// mPosts[N].mChildren.push_back(ForumModelIndex(N+1)); +// mPosts[N+1].mParent = ForumModelIndex(N); +// mPosts[N+1].prow = 0; +// +// RsMsgMetaData meta; +// meta.mMsgName = std::string("message ") + QString::number(N+1).toStdString() ; } int RsGxsForumModel::rowCount(const QModelIndex& parent) const @@ -63,7 +70,7 @@ int RsGxsForumModel::rowCount(const QModelIndex& parent) const } int RsGxsForumModel::columnCount(const QModelIndex &parent) const { - return COLUMN_COUNT ; + return COLUMN_THREAD_COUNT ; } bool RsGxsForumModel::hasChildren(const QModelIndex &parent) const @@ -83,9 +90,9 @@ bool RsGxsForumModel::hasChildren(const QModelIndex &parent) const } #ifdef DEBUG_FORUMMODEL - std::cerr << "hasChildren-3(" << parent << ") : " << !mPosts[entry].children.empty() << std::endl; + std::cerr << "hasChildren-3(" << parent << ") : " << !mPosts[entry].mChildren.empty() << std::endl; #endif - return !mPosts[entry].children.empty(); + return !mPosts[entry].mChildren.empty(); } bool RsGxsForumModel::convertTabEntryToRefPointer(uint32_t entry,void *& ref) @@ -119,7 +126,7 @@ bool RsGxsForumModel::convertRefPointerToTabEntry(void *ref,uint32_t& entry) QModelIndex RsGxsForumModel::index(int row, int column, const QModelIndex & parent) const { // if(!hasIndex(row,column,parent)) - if(row < 0 || column < 0 || column >= COLUMN_COUNT) + if(row < 0 || column < 0 || column >= COLUMN_THREAD_COUNT) return QModelIndex(); void *ref = getChildRef(parent.internalPointer(),row); @@ -161,10 +168,10 @@ void *RsGxsForumModel::getChildRef(void *ref,int row) const return NULL ; void *new_ref; - if(row >= mPosts[entry].children.size()) + if(row >= mPosts[entry].mChildren.size()) return NULL; - convertTabEntryToRefPointer(mPosts[entry].children[row],new_ref); + convertTabEntryToRefPointer(mPosts[entry].mChildren[row],new_ref); return new_ref; } @@ -176,7 +183,7 @@ void *RsGxsForumModel::getParentRef(void *ref,int& row) const if(!convertRefPointerToTabEntry(ref,ref_entry) || ref_entry >= mPosts.size()) return NULL ; - ForumModelIndex parent_entry = mPosts[ref_entry].parent; + ForumModelIndex parent_entry = mPosts[ref_entry].mParent; if(parent_entry == 0) // top level index { @@ -200,7 +207,7 @@ int RsGxsForumModel::getChildrenCount(void *ref) const if(!convertRefPointerToTabEntry(ref,entry) || entry >= mPosts.size()) return 0 ; - return mPosts[entry].children.size(); + return mPosts[entry].mChildren.size(); } @@ -237,10 +244,9 @@ QVariant RsGxsForumModel::headerData(int section, Qt::Orientation orientation, i switch(section) { default: - case COLUMN_TITLE: return tr("Title"); - case COLUMN_READ_STATUS: return tr("Read Status"); - case COLUMN_DATE: return tr("Date"); - case COLUMN_AUTHOR: return tr("Author"); + case COLUMN_THREAD_TITLE: return tr("Title"); + case COLUMN_THREAD_DATE: return tr("Date"); + case COLUMN_THREAD_AUTHOR: return tr("Author"); } } @@ -287,7 +293,7 @@ QVariant RsGxsForumModel::data(const QModelIndex &index, int role) const return QVariant() ; } - const RsMsgMetaData& meta(mPosts[entry].meta_versions[0]) ; + const ForumModelPostEntry& fmpe(mPosts[entry]); #ifdef DEBUG_FORUMMODEL std::cerr << " [ok]" << std::endl; @@ -295,60 +301,60 @@ QVariant RsGxsForumModel::data(const QModelIndex &index, int role) const switch(role) { - case Qt::DisplayRole: return displayRole (meta,index.column()) ; - case Qt::DecorationRole: return decorationRole(meta,index.column()) ; - case Qt::UserRole: return userRole (meta,index.column()) ; - case Qt::ToolTipRole: return toolTipRole (meta,index.column()) + case Qt::DisplayRole: return displayRole (fmpe,index.column()) ; + case Qt::DecorationRole: return decorationRole(fmpe,index.column()) ; + case Qt::UserRole: return userRole (fmpe,index.column()) ; + case Qt::ToolTipRole: return toolTipRole (fmpe,index.column()) ; - case ThreadPinnedRole: return pinnedRole(index.column()) ; - case MissingRole: return missingRole(index.column()) ; - case StatusRole: return statusRole(index.column()) ; + case ThreadPinnedRole: return pinnedRole (fmpe,index.column()) ; + case MissingRole: return missingRole (fmpe,index.column()) ; + case StatusRole: return statusRole (fmpe,index.column()) ; default: return QVariant(); } } -QVariant RsGxsForumModel::statusRole(int column,const ForumModelPostEntry& fmpe) +QVariant RsGxsForumModel::statusRole(const ForumModelPostEntry& fmpe,int column) const { if(column != COLUMN_THREAD_DATA) return QVariant(); - return QVariant(fmpe.mMsgStatus); + return QVariant(fmpe.mStatus); } -QVariant RsGxsForumModel::missingRole(int column,const ForumModelPostEntry& fmpe) +QVariant RsGxsForumModel::missingRole(const ForumModelPostEntry& fmpe,int column) const { if(column != COLUMN_THREAD_DATA) return QVariant(); - if(fmpe.mPostFlags & FLAG_POST_IS_MISSING) + if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_MISSING) return QVariant(true); else return QVariant(false); } -QVariant RsGxsForumModel::toolTipRole(int column,const ForumModelPostEntry& fmpe) +QVariant RsGxsForumModel::toolTipRole(const ForumModelPostEntry& fmpe,int column) const { if(column != COLUMN_THREAD_DISTRIBUTION) return QVariant(); - switch(fmpe.mReputationWaningLevel) + switch(fmpe.mReputationWarningLevel) { case 3: return QVariant(tr("Information for this identity is currently missing.")) ; case 2: return QVariant(tr("You have banned this ID. The message will not be\ndisplayed nor forwarded to your friends.")) ; case 1: return QVariant(tr("You have not set an opinion for this person,\n and your friends do not vote positively: Spam regulation \nprevents the message to be forwarded to your friends.")) ; - case 0: return QVariant(tr("Message will be forwarded to your friends.") ; + case 0: return QVariant(tr("Message will be forwarded to your friends.")) ; default: return QVariant("[ERROR: missing reputation level information - contact the developers]"); } } -QVariant RsGxsForumModel::pinnedRole(int column,const ForumModelPostEntry& fmpe) +QVariant RsGxsForumModel::pinnedRole(const ForumModelPostEntry& fmpe,int column) const { if(column != COLUMN_THREAD_DATE) return QVariant(); - if(fmpe.mFlags & ForumModelPostEntry::FLAG_POST_IS_PINNED) + if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_PINNED) return QVariant(true); else return QVariant(false); @@ -362,14 +368,13 @@ QVariant RsGxsForumModel::sizeHintRole(int col) const switch(col) { default: - case COLUMN_TITLE: return QVariant( QSize(factor * 170, factor*14 )); - case COLUMN_READ_STATUS: return QVariant( QSize(factor * 10 , factor*14 )); - case COLUMN_DATE: return QVariant( QSize(factor * 75 , factor*14 )); - case COLUMN_AUTHOR: return QVariant( QSize(factor * 75 , factor*14 )); + case COLUMN_THREAD_TITLE: return QVariant( QSize(factor * 170, factor*14 )); + case COLUMN_THREAD_DATE: return QVariant( QSize(factor * 75 , factor*14 )); + case COLUMN_THREAD_AUTHOR: return QVariant( QSize(factor * 75 , factor*14 )); } } -QVariant RsGxsForumModel::authorRole(int column,const ForumModelPostEntry& fmpe) +QVariant RsGxsForumModel::authorRole(const ForumModelPostEntry& fmpe,int column) const { if(column == COLUMN_THREAD_DATA) return QVariant(QString::fromStdString(msg.mMeta.mAuthorId.toStdString())); @@ -377,34 +382,34 @@ QVariant RsGxsForumModel::authorRole(int column,const ForumModelPostEntry& fmpe) return QVariant(); } -QVariant RsGxsForumModel::sortRole(int column,const ForumModelPostEntry& fmpe) +QVariant RsGxsForumModel::sortRole(const ForumModelPostEntry& fmpe,int column) const { if(column == COLUMN_THREAD_DATA) return QVariant(QString::number(fmpe.mPublishTs)); // we should probably have leading zeroes here } -QVariant RsGxsForumModel::displayRole(const RsMsgMetaData& meta,int col) const +QVariant RsGxsForumModel::displayRole(const ForumModelPostEntry& fmpe,int col) const { switch(col) { - case COLUMN_TITLE: if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_REDACTED) + case COLUMN_THREAD_TITLE: if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_REDACTED) return QVariant(tr("[ ... Redacted message ... ]")); else if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_PINNED) - return QVariant(tr("[PINNED] ") + QString::fromUtf8(msg.mMeta.mMsgName.c_str())); + return QVariant(tr("[PINNED] ") + QString::fromUtf8(fmpe.mTitle.c_str())); else - return QVariant(QString::fromUtf8(msg.mMeta.mMsgName.c_str())); + return QVariant(QString::fromUtf8(fmpe.mTitle.c_str())); - case COLUMN_READ_STATUS:return QVariant(meta.mMsgStatus); - case COLUMN_DATE: { + //case COLUMN_THREAD_READ_STATUS:return QVariant(fmpe.mMsgStatus); + case COLUMN_THREAD_DATE: { QDateTime qtime; - qtime.setTime_t(msg.mMeta.mPublishTs); + qtime.setTime_t(fmpe.mPublishTs); - return QVariant(DateTime::formatDateTime(qtime)); + return QVariant(QDateTime::formatDateTime(qtime)); } - case COLUMN_AUTHOR: return QVariant(QString::fromStdString(meta.mAuthorId.toStdString())); - case COLUMN_THREAD_MSGID: return QVariant(QString::fromStdString(meta.mMsgId.toStdString())); + case COLUMN_THREAD_AUTHOR: return QVariant(QString::fromStdString(fmpe.mAuthorId.toStdString())); + case COLUMN_THREAD_MSGID: return QVariant(QString::fromStdString(fmpe.mMsgId.toStdString())); #ifdef TODO if (filterColumn == COLUMN_THREAD_CONTENT) { // need content for filter @@ -421,51 +426,7 @@ QVariant RsGxsForumModel::displayRole(const RsMsgMetaData& meta,int col) const return QVariant("[ERROR]"); } -QVariant RsGxsForumModel::userRole(const RsMsgMetaData &meta, int col) const -{ -#ifdef TODO - switch(col) - { - case COLUMN_PROGRESS: - { - FileChunksInfo fcinfo; - if (!rsFiles->FileDownloadChunksDetails(fileInfo.hash, fcinfo)) - return QVariant(); - - FileProgressInfo pinfo; - pinfo.cmap = fcinfo.chunks; - pinfo.type = FileProgressInfo::DOWNLOAD_LINE; - pinfo.progress = (fileInfo.size == 0) ? 0 : (fileInfo.transfered * 100.0 / fileInfo.size); - pinfo.nb_chunks = pinfo.cmap._map.empty() ? 0 : fcinfo.chunks.size(); - - for (uint32_t i = 0; i < fcinfo.chunks.size(); ++i) - switch(fcinfo.chunks[i]) - { - case FileChunksInfo::CHUNK_CHECKING: pinfo.chunks_in_checking.push_back(i); - break ; - case FileChunksInfo::CHUNK_ACTIVE: pinfo.chunks_in_progress.push_back(i); - break ; - case FileChunksInfo::CHUNK_DONE: - case FileChunksInfo::CHUNK_OUTSTANDING: - break ; - } - - return QVariant::fromValue(pinfo); - } - - case COLUMN_ID: return QVariant(QString::fromStdString(fileInfo.hash.toStdString())); - - - default: - return QVariant(); - } - } -#endif - return QVariant(); - -} - -QVariant RsGxsForumModel::decorationRole(const RsMsgMetaData& meta,int col) const +QVariant RsGxsForumModel::decorationRole(const ForumModelPostEntry& fmpe,int col) const { if(col == COLUMN_THREAD_DISTRIBUTION) return QVariant(fmpe.mReputationWarningLevel); @@ -473,77 +434,451 @@ QVariant RsGxsForumModel::decorationRole(const RsMsgMetaData& meta,int col) cons return QVariant(); } -void RsGxsForumModel::update_posts() +void RsGxsForumModel::setForum(const RsGxsGroupId& forumGroup) { + if(mForumGroupId == forumGroup) + return ; + + mPosts.clear(); + mForumGroupId = forumGroup; + + update_posts(); +} + +void RsGxsForumModel::update_posts(const RsGxsGroupId& group_id) +{ + RsThread::async([this, group_id]() + { + // 1 - get message data from p3GxsForums + + std::list forumIds; + std::vector messages; + std::vector groups; + + forumIds.push_back(group_id); + + if(!rsGxsForums->getForumsInfo(forumIds,groups)) + { + std::cerr << __PRETTY_FUNCTION__ << " failed to retrieve forum group info for forum " << group_id << std::endl; + return; + } + + if(!rsGxsForums->getForumsContent(forumIds,messages)) + { + std::cerr << __PRETTY_FUNCTION__ << " failed to retrieve forum message info for forum " << group_id << std::endl; + return; + } + + // 2 - sort the messages into a proper hierarchy + + std::vector *vect = new std::vector(); + + computeMessagesHierarchy(groups[0],messages,*vect); + + // 3 - update the model in the UI thread. + + RsQThreadUtils::postToObject( [vect,this]() + { + /* Here it goes any code you want to be executed on the Qt Gui + * thread, for example to update the data model with new information + * after a blocking call to RetroShare API complete, note that + * Qt::QueuedConnection is important! + */ + + setPosts(*vect) ; + delete vect; + + + }, this ); + + }); +} + +ForumModelIndex RsGxsForumModel::addEntry(std::vector& posts,const ForumModelPostEntry& entry,ForumModelIndex parent) +{ + uint32_t N = posts.size(); + posts.push_back(entry); + + posts[N].mParent = parent; + posts[parent].mChildren.push_back(N); + + return ForumModelIndex(N); +} + +void RsGxsForumModel::generateMissingItem(const RsGxsMessageId &msgId,ForumModelPostEntry& entry) +{ + entry.mPostFlags = ForumModelPostEntry::FLAG_POST_IS_MISSING ; + entry.mTitle = std::string(tr("[ ... Missing Message ... ]").toUtf8()); + entry.mMsgId = msgId; + entry.mAuthorId.clear(); + entry.mPublishTs=0; + entry.mReputationWarningLevel = 3; +} + +void RsGxsForumModel::convertMsgToPostEntry(const RsGxsForumGroup& mForumGroup,const RsGxsForumMsg& msg, bool useChildTS, uint32_t filterColumn,ForumModelPostEntry& fentry) +{ + fentry.mMsgId = msg.mMeta.mMsgId; + fentry.mPublishTs = msg.mMeta.mPublishTs; + fentry.mStatus = msg.mMeta.mMsgStatus; + + if(mForumGroup.mPinnedPosts.ids.find(msg.mMeta.mMsgId) != mForumGroup.mPinnedPosts.ids.end()) + fentry.mPostFlags |= ForumModelPostEntry::FLAG_POST_IS_PINNED; + + // Early check for a message that should be hidden because its author + // is flagged with a bad reputation + + uint32_t idflags =0; + RsReputations::ReputationLevel reputation_level = rsReputations->overallReputationLevel(msg.mMeta.mAuthorId,&idflags) ; + bool redacted = false; + + if(reputation_level == RsReputations::REPUTATION_LOCALLY_NEGATIVE) + fentry.mPostFlags |= ForumModelPostEntry::FLAG_POST_IS_REDACTED; + + // We use a specific item model for forums in order to handle the post pinning. + + if(reputation_level == RsReputations::REPUTATION_UNKNOWN) + fentry.mReputationWarningLevel = 3 ; + else if(reputation_level == RsReputations::REPUTATION_LOCALLY_NEGATIVE) + fentry.mReputationWarningLevel = 2 ; + else if(reputation_level < rsGxsForums->minReputationForForwardingMessages(mForumGroup.mMeta.mSignFlags,idflags)) + fentry.mReputationWarningLevel = 1 ; + else + fentry.mReputationWarningLevel = 0 ; + #ifdef TODO - std::list downHashes; - rsFiles->FileDownloads(downHashes); + // This is an attempt to put pinned posts on the top. We should rather use a QSortFilterProxyModel here. + QString itemSort = QString::number(msg.mMeta.mPublishTs);//Don't need to format it as for sort. - size_t old_size = mDownloads.size(); - - mDownloads.resize(downHashes.size()) ; - - if(old_size < mDownloads.size()) + if (useChildTS) { - beginInsertRows(QModelIndex(), old_size, mDownloads.size()-1); - insertRows(old_size, mDownloads.size() - old_size); - endInsertRows(); - } - else if(mDownloads.size() < old_size) - { - beginRemoveRows(QModelIndex(), mDownloads.size(), old_size-1); - removeRows(mDownloads.size(), old_size - mDownloads.size()); - endRemoveRows(); - } - - uint32_t i=0; - - for(auto it(downHashes.begin());it!=downHashes.end();++it,++i) - { - FileInfo fileInfo(mDownloads[i]); // we dont update the data itself but only a copy of it.... - int old_size = fileInfo.peers.size() ; - - rsFiles->FileDetails(*it, RS_FILE_HINTS_DOWNLOAD, fileInfo); - - int new_size = fileInfo.peers.size() ; - - if(old_size < new_size) + for(QTreeWidgetItem *grandParent = parent; grandParent!=NULL; grandParent = grandParent->parent()) { - beginInsertRows(index(i,0), old_size, new_size-1); - insertRows(old_size, new_size - old_size,index(i,0)); -#ifdef DEBUG_DOWNLOADLIST - std::cerr << "called insert rows ( " << old_size << ", " << new_size - old_size << ",index(" << index(i,0)<< "))" << std::endl; + //Update Parent Child TimeStamp + QString oldTSSort = grandParent->data(COLUMN_THREAD_DATE, ROLE_THREAD_SORT).toString(); + + QString oldCTSSort = oldTSSort.split("|").at(0); + QString oldPTSSort = oldTSSort.contains("|") ? oldTSSort.split(" | ").at(1) : oldCTSSort; +#ifdef SHOW_COMBINED_DATES + QString oldTSText = grandParent->text(COLUMN_THREAD_DATE); + QString oldCTSText = oldTSText.split("|").at(0); + QString oldPTSText = oldTSText.contains("|") ? oldTSText.split(" | ").at(1) : oldCTSText;//If first time parent get only its mPublishTs + #endif + if (oldCTSSort.toDouble() < itemSort.toDouble()) + { +#ifdef SHOW_COMBINED_DATES + grandParent->setText(COLUMN_THREAD_DATE, DateTime::formatDateTime(qtime) + " | " + oldPTSText); #endif - endInsertRows(); + grandParent->setData(COLUMN_THREAD_DATE, ROLE_THREAD_SORT, itemSort + " | " + oldPTSSort); + } } - else if(new_size < old_size) - { - beginRemoveRows(index(i,0), new_size, old_size-1); - removeRows(new_size, old_size - new_size,index(i,0)); -#ifdef DEBUG_DOWNLOADLIST - std::cerr << "called remove rows ( " << old_size << ", " << old_size - new_size << ",index(" << index(i,0)<< "))" << std::endl; -#endif - endRemoveRows(); - } - - uint32_t old_status = mDownloads[i].downloadStatus ; - - mDownloads[i] = fileInfo ; // ... because insertRows() calls rowCount() which needs to be consistent with the *old* number of rows. - - if(fileInfo.downloadStatus == FT_STATE_DOWNLOADING || old_status != fileInfo.downloadStatus) - { - QModelIndex topLeft = createIndex(i,0), bottomRight = createIndex(i, COLUMN_COUNT-1); - emit dataChanged(topLeft, bottomRight); - } - - // This is apparently not needed. - // - // if(!mDownloads.empty()) - // { - // QModelIndex topLeft = createIndex(0,0), bottomRight = createIndex(mDownloads.size()-1, COLUMN_COUNT-1); - // emit dataChanged(topLeft, bottomRight); - // mDownloads[i] = fileInfo ; - // } } #endif } + +static bool decreasing_time_comp(const QPair& e1,const QPair& e2) { return e2.first < e1.first ; } + +void RsGxsForumModel::computeMessagesHierarchy(const RsGxsForumGroup& forum_group,const std::vector& msgs_array,std::vector& posts) +{ + std::cerr << "updating messages data with " << msgs_array.size() << " messages" << std::endl; + +//#ifdef DEBUG_FORUMS + std::cerr << "Retrieved group data: " << std::endl; + std::cerr << " Group ID: " << forum_group.mMeta.mGroupId << std::endl; + std::cerr << " Admin lst: " << forum_group.mAdminList.ids.size() << " elements." << std::endl; + for(auto it(forum_group.mAdminList.ids.begin());it!=forum_group.mAdminList.ids.end();++it) + std::cerr << " " << *it << std::endl; + std::cerr << " Pinned Post: " << forum_group.mPinnedPosts.ids.size() << " messages." << std::endl; + for(auto it(forum_group.mPinnedPosts.ids.begin());it!=forum_group.mPinnedPosts.ids.end();++it) + std::cerr << " " << *it << std::endl; +//#endif + + /* get messages */ + std::map msgs; + + for(uint32_t i=0;i > > mPostVersions ; + + // ThreadList contains the list of parent threads. The algorithm below iterates through all messages + // and tries to establish parenthood relationships between them, given that we only know the + // immediate parent of a message and now its children. Some messages have a missing parent and for them + // a fake top level parent is generated. + + // In order to be efficient, we first create a structure that lists the children of every mesage ID in the list. + // Then the hierarchy of message is build by attaching the kids to every message until all of them have been processed. + // The messages with missing parents will be the last ones remaining in the list. + + std::list > threadStack; + std::map > kids_array ; + std::set missing_parents; + + // First of all, remove all older versions of posts. This is done by first adding all posts into a hierarchy structure + // and then removing all posts which have a new versions available. The older versions are kept appart. + +#ifdef DEBUG_FORUMS + std::cerr << "GxsForumsFillThread::run() Collecting post versions" << std::endl; +#endif + mPostVersions.clear(); + std::list msg_stack ; + + for ( std::map::iterator msgIt = msgs.begin(); msgIt != msgs.end();++msgIt) + { + if(!msgIt->second.mMeta.mOrigMsgId.isNull() && msgIt->second.mMeta.mOrigMsgId != msgIt->second.mMeta.mMsgId) + { +#ifdef DEBUG_FORUMS + std::cerr << " Post " << msgIt->second.mMeta.mMsgId << " is a new version of " << msgIt->second.mMeta.mOrigMsgId << std::endl; +#endif + std::map::iterator msgIt2 = msgs.find(msgIt->second.mMeta.mOrigMsgId); + + // Ensuring that the post exists allows to only collect the existing data. + + if(msgIt2 == msgs.end()) + continue ; + + // Make sure that the author is the same than the original message, or is a moderator. This should always happen when messages are constructed using + // the UI but nothing can prevent a nasty user to craft a new version of a message with his own signature. + + if(msgIt2->second.mMeta.mAuthorId != msgIt->second.mMeta.mAuthorId) + { + if( !IS_FORUM_MSG_MODERATION(msgIt->second.mMeta.mMsgFlags) ) // if authors are different the moderation flag needs to be set on the editing msg + continue ; + + if( forum_group.mAdminList.ids.find(msgIt->second.mMeta.mAuthorId)==forum_group.mAdminList.ids.end()) // if author is not a moderator, continue + continue ; + } + + // always add the post a self version + + if(mPostVersions[msgIt->second.mMeta.mOrigMsgId].empty()) + mPostVersions[msgIt->second.mMeta.mOrigMsgId].push_back(QPair(msgIt2->second.mMeta.mPublishTs,msgIt2->second.mMeta.mMsgId)) ; + + mPostVersions[msgIt->second.mMeta.mOrigMsgId].push_back(QPair(msgIt->second.mMeta.mPublishTs,msgIt->second.mMeta.mMsgId)) ; + } + } + + // The following code assembles all new versions of a given post into the same array, indexed by the oldest version of the post. + + for(QMap > >::iterator it(mPostVersions.begin());it!=mPostVersions.end();++it) + { + QVector >& v(*it) ; + + for(int32_t i=0;i > >::iterator it2 = mPostVersions.find(sub_msg_id); + + if(it2 != mPostVersions.end()) + { + for(int32_t j=0;j<(*it2).size();++j) + if((*it2)[j].second != sub_msg_id) // dont copy it, since it is already present at slot i + v.append((*it2)[j]) ; + + mPostVersions.erase(it2) ; // it2 is never equal to it + } + } + } + } + + + // Now remove from msg ids, all posts except the most recent one. And make the mPostVersion be indexed by the most recent version of the post, + // which corresponds to the item in the tree widget. + +#ifdef DEBUG_FORUMS + std::cerr << "Final post versions: " << std::endl; +#endif + QMap > > mTmp; + std::map most_recent_versions ; + + for(QMap > >::iterator it(mPostVersions.begin());it!=mPostVersions.end();++it) + { +#ifdef DEBUG_FORUMS + std::cerr << "Original post: " << it.key() << std::endl; +#endif + // Finally, sort the posts from newer to older + + qSort((*it).begin(),(*it).end(),decreasing_time_comp) ; + +#ifdef DEBUG_FORUMS + std::cerr << " most recent version " << (*it)[0].first << " " << (*it)[0].second << std::endl; +#endif + for(int32_t i=1;i<(*it).size();++i) + { + msgs.erase((*it)[i].second) ; + +#ifdef DEBUG_FORUMS + std::cerr << " older version " << (*it)[i].first << " " << (*it)[i].second << std::endl; +#endif + } + + mTmp[(*it)[0].second] = *it ; // index the versions map by the ID of the most recent post. + + // Now make sure that message parents are consistent. Indeed, an old post may have the old version of a post as parent. So we need to change that parent + // to the newest version. So we create a map of which is the most recent version of each message, so that parent messages can be searched in it. + + for(int i=1;i<(*it).size();++i) + most_recent_versions[(*it)[i].second] = (*it)[0].second ; + } + mPostVersions = mTmp ; + + // The next step is to find the top level thread messages. These are defined as the messages without + // any parent message ID. + + // this trick is needed because while we remove messages, the parents a given msg may already have been removed + // and wrongly understand as a missing parent. + + std::map kept_msgs; + + for ( std::map::iterator msgIt = msgs.begin(); msgIt != msgs.end();++msgIt) + { + + if(mFlatView || msgIt->second.mMeta.mParentId.isNull()) + { + + /* add all threads */ + const RsGxsForumMsg& msg = msgIt->second; + +#ifdef DEBUG_FORUMS + std::cerr << "GxsForumsFillThread::run() Adding TopLevel Thread: mId: " << msg.mMeta.mMsgId << std::endl; +#endif + + ForumModelPostEntry entry; + convertMsgToThreadWidget(msg, mUseChildTS, mFilterColumn,NULL,entry); + + ForumModelIndex entry_index = addEntry(posts,entry,0); + + if (!mFlatView) + threadStack.push_back(std::make_pair(msg.mMeta.mMsgId,entry_index)) ; + + //calculateExpand(msg, item); + //mItems.append(entry_index); + } + else + { +#ifdef DEBUG_FORUMS + std::cerr << "GxsForumsFillThread::run() Storing kid " << msgIt->first << " of message " << msgIt->second.mMeta.mParentId << std::endl; +#endif + // The same missing parent may appear multiple times, so we first store them into a unique container. + + RsGxsMessageId parent_msg = msgIt->second.mMeta.mParentId; + + if(msgs.find(parent_msg) == msgs.end()) + { + // also check that the message is not versionned + + std::map::const_iterator mrit = most_recent_versions.find(parent_msg) ; + + if(mrit != most_recent_versions.end()) + parent_msg = mrit->second ; + else + missing_parents.insert(parent_msg); + } + + kids_array[parent_msg].push_back(msgIt->first) ; + kept_msgs.insert(*msgIt) ; + } + } + + msgs = kept_msgs; + + // Also create a list of posts by time, when they are new versions of existing posts. Only the last one will have an item created. + + // Add a fake toplevel item for the parent IDs that we dont actually have. + + for(std::set::const_iterator it(missing_parents.begin());it!=missing_parents.end();++it) + { + // add dummy parent item + ForumModelPostEntry e ; + generateMissingItem(RsGxsMessageId,e); + + ForumModelIndex e_index = addEntry(posts,e,0); // no parent -> parent is level 0 + //mItems.append( e_index ); + + threadStack.push_back(std::make_pair(*it,e_index)) ; + } +#ifdef DEBUG_FORUMS + std::cerr << "GxsForumsFillThread::run() Processing stack:" << std::endl; +#endif + // Now use a stack to go down the hierarchy + + while (!threadStack.empty()) + { + std::pair threadPair = threadStack.front(); + threadStack.pop_front(); + + std::map >::iterator it = kids_array.find(threadPair.first) ; + +#ifdef DEBUG_FORUMS + std::cerr << "GxsForumsFillThread::run() Node: " << threadPair.first << std::endl; +#endif + if(it == kids_array.end()) + continue ; + + + for(std::list::const_iterator it2(it->second.begin());it2!=it->second.end();++it2) + { + // We iterate through the top level thread items, and look for which message has the current item as parent. + // When found, the item is put in the thread list itself, as a potential new parent. + + std::map::iterator mit = msgs.find(*it2) ; + + if(mit == msgs.end()) + { + std::cerr << "GxsForumsFillThread::run() Cannot find submessage " << *it2 << " !!!" << std::endl; + continue ; + } + + const RsGxsForumMsg& msg(mit->second) ; +#ifdef DEBUG_FORUMS + std::cerr << "GxsForumsFillThread::run() adding sub_item " << msg.mMeta.mMsgId << std::endl; +#endif + + + ForumModelPostEntry e ; + convertMsgToPostEntry(forum_group,msg,mUseChildTS,mFilterColumn,e) ; + ForumModelIndex e_index = addEntry(posts,e, threadPair.second); + + //calculateExpand(msg, item); + + /* add item to process list */ + threadStack.push_back(std::make_pair(msg.mMeta.mMsgId, e_index)); + + msgs.erase(mit); + } + +#ifdef DEBUG_FORUMS + std::cerr << "GxsForumsFillThread::run() Erasing entry " << it->first << " from kids tab." << std::endl; +#endif + kids_array.erase(it) ; // This is not strictly needed, but it improves performance by reducing the search space. + } + +#ifdef DEBUG_FORUMS + std::cerr << "Kids array now has " << kids_array.size() << " elements" << std::endl; + for(std::map >::const_iterator it(kids_array.begin());it!=kids_array.end();++it) + { + std::cerr << "Node " << it->first << std::endl; + for(std::list::const_iterator it2(it->second.begin());it2!=it->second.end();++it2) + std::cerr << " " << *it2 << std::endl; + } + + std::cerr << "GxsForumsFillThread::run() stopped: " << (wasStopped() ? "yes" : "no") << std::endl; +#endif +} + + + + diff --git a/retroshare-gui/src/gui/gxsforums/GxsForumModel.h b/retroshare-gui/src/gui/gxsforums/GxsForumModel.h index 384367097..2de026f1f 100644 --- a/retroshare-gui/src/gui/gxsforums/GxsForumModel.h +++ b/retroshare-gui/src/gui/gxsforums/GxsForumModel.h @@ -1,5 +1,4 @@ #ifndef SUSPENDED_CODE -#include "retroshare/rsgxsifacetypes.h" #else #include #include @@ -14,6 +13,8 @@ struct RsMsgMetaData #endif +#include "retroshare/rsgxsforums.h" +#include "retroshare/rsgxsifacetypes.h" #include // This class holds the actual hierarchy of posts, represented by identifiers @@ -38,8 +39,7 @@ struct ForumModelPostEntry uint32_t mPublishTs; uint32_t mPostFlags; int mReputationWarningLevel; - - std::vector meta_versions; // maybe we don't need all this. Could be too large. + int mStatus; std::vector mChildren; ForumModelIndex mParent; @@ -62,6 +62,9 @@ public: StatusRole = Qt::UserRole+4, }; + // This method will asynchroneously update the data + void setForum(const RsGxsGroupId& forumGroup); + int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; @@ -73,12 +76,16 @@ public: QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - QVariant sizeHintRole(int col) const; - QVariant displayRole(const RsMsgMetaData& meta, int col) const; - QVariant userRole(const RsMsgMetaData& meta, int col) const; - QVariant decorationRole(const RsMsgMetaData &meta, int col) const; - - void update_posts(); + QVariant sizeHintRole (int col) const; + QVariant displayRole (const ForumModelPostEntry& fmpe, int col) const; + QVariant userRole (const ForumModelPostEntry& fmpe, int col) const; + QVariant decorationRole(const ForumModelPostEntry& fmpe, int col) const; + QVariant toolTipRole (const ForumModelPostEntry& fmpe, int col) const; + QVariant pinnedRole (const ForumModelPostEntry& fmpe, int col) const; + QVariant missingRole (const ForumModelPostEntry& fmpe, int col) const; + QVariant statusRole (const ForumModelPostEntry& fmpe, int col) const; + QVariant authorRole (const ForumModelPostEntry& fmpe, int col) const; + QVariant sortRole (const ForumModelPostEntry& fmpe, int col) const; private: void *getParentRef(void *ref,int& row) const; @@ -89,5 +96,15 @@ private: static bool convertTabEntryToRefPointer(uint32_t entry,void *& ref); static bool convertRefPointerToTabEntry(void *ref,uint32_t& entry); + void update_posts(const RsGxsGroupId &group_id); + void setForumMessageSummary(const std::vector& messages); + + static void computeMessagesHierarchy(const RsGxsForumGroup& forum_group,const std::vector& msgs_array,std::vector& posts); + static void generateMissingItem(const RsGxsMessageId &msgId,ForumModelPostEntry& entry); + static ForumModelIndex addEntry(std::vector& posts,const ForumModelPostEntry& entry,ForumModelIndex parent); + static void convertMsgToPostEntry(const RsGxsForumGroup &mForumGroup, const RsGxsForumMsg& msg, bool useChildTS, uint32_t filterColumn, ForumModelPostEntry& fentry); + + void setPosts(const std::vector& posts); + std::vector mPosts ; // store the list of posts updated from rsForums. }; diff --git a/retroshare-gui/src/gui/gxsforums/GxsForumThreadWidget.cpp b/retroshare-gui/src/gui/gxsforums/GxsForumThreadWidget.cpp index 48609a9c0..34f74cb2c 100644 --- a/retroshare-gui/src/gui/gxsforums/GxsForumThreadWidget.cpp +++ b/retroshare-gui/src/gui/gxsforums/GxsForumThreadWidget.cpp @@ -1428,19 +1428,6 @@ QTreeWidgetItem *GxsForumThreadWidget::convertMsgToThreadWidget(const RsGxsForum } -QTreeWidgetItem *GxsForumThreadWidget::generateMissingItem(const RsGxsMessageId &msgId) -{ - GxsIdRSTreeWidgetItem *item = new GxsIdRSTreeWidgetItem(mThreadCompareRole,GxsIdDetails::ICON_TYPE_AVATAR); - - item->setText(COLUMN_THREAD_TITLE, tr("[ ... Missing Message ... ]")); - item->setData(COLUMN_THREAD_MSGID,Qt::DisplayRole, QString::fromStdString(msgId.toStdString())); - item->setData(COLUMN_THREAD_DATA, ROLE_THREAD_MISSING, true); - - item->setId(RsGxsId(), COLUMN_THREAD_AUTHOR, false); // fixed up columnId() - - return item; -} - void GxsForumThreadWidget::insertThreads() { #ifdef DEBUG_FORUMS diff --git a/retroshare-gui/src/gui/gxsforums/GxsForumsFillThread.cpp b/retroshare-gui/src/gui/gxsforums/GxsForumsFillThread.cpp index f0c61d877..d0bd5ea26 100644 --- a/retroshare-gui/src/gui/gxsforums/GxsForumsFillThread.cpp +++ b/retroshare-gui/src/gui/gxsforums/GxsForumsFillThread.cpp @@ -554,75 +554,4 @@ void GxsForumsFillThread::run() deleteLater(); } -QTreeWidgetItem *GxsForumsFillThread::convertMsgToThreadWidget(const RsGxsForumMsg& msg, bool useChildTS, uint32_t filterColumn, ForumModelIndex parent) -{ - ForumModelPostEntry fentry; - fentry.mMsgId = msg.mMeta.mMsgId; - fentry.mPublishTs = msg.mMeta.mPublishTs; - fentry.mParent = parent; - - if(mForumGroup.mPinnedPosts.ids.find(msg.mMeta.mMsgId) != mForumGroup.mPinnedPosts.ids.end()) - fentry.mFlags |= ForumModelPostEntry::FLAG_POST_IS_PINNED; - - // Early check for a message that should be hidden because its author - // is flagged with a bad reputation - - uint32_t idflags =0; - RsReputations::ReputationLevel reputation_level = rsReputations->overallReputationLevel(msg.mMeta.mAuthorId,&idflags) ; - bool redacted = false; - - if(reputation_level == RsReputations::REPUTATION_LOCALLY_NEGATIVE) - fentry.mPostFlags |= ForumModelPostEntry::FLAG_POST_IS_REDACTED; - - // We use a specific item model for forums in order to handle the post pinning. - - if(reputation_level == RsReputations::REPUTATION_UNKNOWN) - fentry.mReputationWarningLevel = 3 ; - else if(reputation_level == RsReputations::REPUTATION_LOCALLY_NEGATIVE) - fentry.mReputationWarningLevel = 2 ; - else if(reputation_level < rsGxsForums->minReputationForForwardingMessages(mForumGroup.mMeta.mSignFlags,idflags)) - fentry.mReputationWarningLevel = 1 ; - else - fentry.mReputationWarningLevel = 0 ; - -#ifdef TODO - // This is an attempt to put pinned posts on the top. We should rather use a QSortFilterProxyModel here. - QString itemSort = QString::number(msg.mMeta.mPublishTs);//Don't need to format it as for sort. - - if (useChildTS) - { - for(QTreeWidgetItem *grandParent = parent; grandParent!=NULL; grandParent = grandParent->parent()) - { - //Update Parent Child TimeStamp - QString oldTSSort = grandParent->data(COLUMN_THREAD_DATE, ROLE_THREAD_SORT).toString(); - - QString oldCTSSort = oldTSSort.split("|").at(0); - QString oldPTSSort = oldTSSort.contains("|") ? oldTSSort.split(" | ").at(1) : oldCTSSort; -#ifdef SHOW_COMBINED_DATES - QString oldTSText = grandParent->text(COLUMN_THREAD_DATE); - QString oldCTSText = oldTSText.split("|").at(0); - QString oldPTSText = oldTSText.contains("|") ? oldTSText.split(" | ").at(1) : oldCTSText;//If first time parent get only its mPublishTs - #endif - if (oldCTSSort.toDouble() < itemSort.toDouble()) - { -#ifdef SHOW_COMBINED_DATES - grandParent->setText(COLUMN_THREAD_DATE, DateTime::formatDateTime(qtime) + " | " + oldPTSText); -#endif - grandParent->setData(COLUMN_THREAD_DATE, ROLE_THREAD_SORT, itemSort + " | " + oldPTSSort); - } - } - } -#endif -//#TODO -#if 0 - if (IS_GROUP_SUBSCRIBED(subscribeFlags) && !(msginfo.mMsgFlags & RS_DISTRIB_MISSING_MSG)) { - rsGxsForums->getMessageStatus(msginfo.forumId, msginfo.msgId, status); - } else { - // show message as read - status = RSGXS_MSG_STATUS_READ; - } -#endif - if (parent) parent->addChild(item); - return item; -}