/******************************************************************************* * retroshare-gui/src/gui/gxsforums/GxsForumModel.cpp * * * * Copyright 2018 by Cyril Soler * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU Affero General Public License as * * published by the Free Software Foundation, either version 3 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU Affero General Public License for more details. * * * * You should have received a copy of the GNU Affero General Public License * * along with this program. If not, see . * * * *******************************************************************************/ #include #include #include #include #include "gui/common/FilesDefs.h" #include "util/qtthreadsutils.h" #include "util/HandleRichText.h" #include "util/DateTime.h" #include "gui/gxs/GxsIdDetails.h" #include "gui/gxs/GxsIdTreeWidgetItem.h" #include "GxsForumModel.h" #include "retroshare/rsgxsflags.h" #include "retroshare/rsgxsforums.h" #include "retroshare/rsexpr.h" //#define DEBUG_FORUMMODEL Q_DECLARE_METATYPE(RsMsgMetaData); std::ostream& operator<<(std::ostream& o, const QModelIndex& i);// defined elsewhere const QString RsGxsForumModel::FilterString("filtered"); RsGxsForumModel::RsGxsForumModel(QObject *parent) : QAbstractItemModel(parent), mUseChildTS(false),mFilteringEnabled(false),mTreeMode(TREE_MODE_TREE) { initEmptyHierarchy(mPosts); } void RsGxsForumModel::preMods() { //emit layoutAboutToBeChanged(); //Generate SIGSEGV when click on button move next/prev. beginResetModel(); } void RsGxsForumModel::postMods() { endResetModel(); if(mTreeMode == TREE_MODE_FLAT) emit dataChanged(createIndex(0,0,(void*)NULL), createIndex(mPosts.size(),COLUMN_THREAD_NB_COLUMNS-1,(void*)NULL)); else emit dataChanged(createIndex(0,0,(void*)NULL), createIndex(mPosts[0].mChildren.size(),COLUMN_THREAD_NB_COLUMNS-1,(void*)NULL)); } void RsGxsForumModel::setTreeMode(TreeMode mode) { if(mode == mTreeMode) return; preMods(); if(mode == TREE_MODE_TREE) // means we were in FLAT mode, so the last rows are removed. { beginRemoveRows(QModelIndex(),mPosts[0].mChildren.size(),mPosts.size()-1); endRemoveRows(); } mTreeMode = mode; if(mode == TREE_MODE_FLAT) // means we were in tree mode, so the last rows are added. { beginInsertRows(QModelIndex(),mPosts[0].mChildren.size(),mPosts.size()-1); endInsertRows(); } postMods(); } void RsGxsForumModel::setSortMode(SortMode mode) { preMods(); mSortMode = mode; postMods(); } void RsGxsForumModel::initEmptyHierarchy(std::vector& posts) { preMods(); posts.resize(1); // adds a sentinel item posts[0].mTitle = "Root sentinel post" ; posts[0].mParent = 0; postMods(); } int RsGxsForumModel::rowCount(const QModelIndex& parent) const { if(parent.column() > 0) return 0; if(mPosts.empty()) // security. Should never happen. return 0; if(!parent.isValid()) return getChildrenCount(NULL); else return getChildrenCount(parent.internalPointer()); } int RsGxsForumModel::columnCount(const QModelIndex &/*parent*/) const { return COLUMN_THREAD_NB_COLUMNS ; } std::vector > RsGxsForumModel::getPostVersions(const RsGxsMessageId& mid) const { auto it = mPostVersions.find(mid); if(it != mPostVersions.end()) return it->second; else return std::vector >(); } bool RsGxsForumModel::getPostData(const QModelIndex& i,ForumModelPostEntry& fmpe) const { if(!i.isValid()) return true; void *ref = i.internalPointer(); uint32_t entry = 0; if(!convertRefPointerToTabEntry(ref,entry) || entry >= mPosts.size()) return false ; fmpe = mPosts[entry]; return true; } bool RsGxsForumModel::hasChildren(const QModelIndex &parent) const { if(!parent.isValid()) return true; if(mTreeMode == TREE_MODE_FLAT) return false; void *ref = parent.internalPointer(); uint32_t entry = 0; if(!convertRefPointerToTabEntry(ref,entry) || entry >= mPosts.size()) { #ifdef DEBUG_FORUMMODEL std::cerr << "hasChildren-2(" << parent << ") : " << false << std::endl; #endif return false ; } #ifdef DEBUG_FORUMMODEL std::cerr << "hasChildren-3(" << parent << ") : " << !mPosts[entry].mChildren.empty() << std::endl; #endif return !mPosts[entry].mChildren.empty(); } bool RsGxsForumModel::convertTabEntryToRefPointer(uint32_t entry,void *& ref) { // the pointer is formed the following way: // // [ 32 bits ] // // This means that the whole software has the following build-in limitation: // * 4 B simultaenous posts. Should be enough ! ref = reinterpret_cast( (intptr_t)entry ); return true; } bool RsGxsForumModel::convertRefPointerToTabEntry(void *ref,uint32_t& entry) { intptr_t val = (intptr_t)ref; if(val > (1<<30)) // make sure the pointer is an int that fits in 32bits and not too big which would look suspicious { std::cerr << "(EE) trying to make a ForumModelIndex out of a number that is larger than 2^32-1 !" << std::endl; return false ; } entry = uint32_t(val); return true; } QModelIndex RsGxsForumModel::index(int row, int column, const QModelIndex & parent) const { if(row < 0 || column < 0 || column >= COLUMN_THREAD_NB_COLUMNS) return QModelIndex(); void *ref = getChildRef(parent.internalPointer(),row); #ifdef DEBUG_FORUMMODEL std::cerr << "index-3(" << row << "," << column << " parent=" << parent << ") : " << createIndex(row,column,ref) << std::endl; #endif return createIndex(row,column,ref) ; } QModelIndex RsGxsForumModel::parent(const QModelIndex& index) const { if(!index.isValid()) return QModelIndex(); if(mTreeMode == TREE_MODE_FLAT) return QModelIndex(); void *child_ref = index.internalPointer(); int row=0; void *parent_ref = getParentRef(child_ref,row) ; if(parent_ref == NULL) // root return QModelIndex() ; return createIndex(row,0,parent_ref); } Qt::ItemFlags RsGxsForumModel::flags(const QModelIndex& index) const { if (!index.isValid()) return 0; return QAbstractItemModel::flags(index); } void *RsGxsForumModel::getChildRef(void *ref,int row) const { if (row < 0) return nullptr; ForumModelIndex entry ; if(!convertRefPointerToTabEntry(ref,entry) || entry >= mPosts.size()) return NULL ; void *new_ref; if(mTreeMode == TREE_MODE_FLAT) { if(entry == 0) { convertTabEntryToRefPointer(row+1,new_ref); return new_ref; } else { return NULL ; } } if(static_cast(row) >= mPosts[entry].mChildren.size()) return NULL; convertTabEntryToRefPointer(mPosts[entry].mChildren[row],new_ref); return new_ref; } void *RsGxsForumModel::getParentRef(void *ref,int& row) const { ForumModelIndex ref_entry; if(!convertRefPointerToTabEntry(ref,ref_entry) || ref_entry >= mPosts.size()) return NULL ; if(mTreeMode == TREE_MODE_FLAT) { if(ref_entry == 0) { RsErr() << "getParentRef() shouldn't be asked for the parent of NULL" << std::endl; row = 0; } else row = ref_entry-1; return NULL; } ForumModelIndex parent_entry = mPosts[ref_entry].mParent; if(parent_entry == 0) // top level index { row = 0; return NULL ; } else { void *parent_ref; convertTabEntryToRefPointer(parent_entry,parent_ref); row = mPosts[parent_entry].prow; return parent_ref; } } int RsGxsForumModel::getChildrenCount(void *ref) const { uint32_t entry = 0 ; if(!convertRefPointerToTabEntry(ref,entry) || entry >= mPosts.size()) return 0 ; if(mTreeMode == TREE_MODE_FLAT) if(entry == 0) { #ifdef DEBUG_FORUMMODEL std::cerr << "Children count (flat mode): " << mPosts.size()-1 << std::endl; #endif return ((int)mPosts.size())-1; } else return 0; else { #ifdef DEBUG_FORUMMODEL std::cerr << "Children count (tree mode): " << mPosts[entry].mChildren.size() << std::endl; #endif return mPosts[entry].mChildren.size(); } } QVariant RsGxsForumModel::headerData(int section, Qt::Orientation /*orientation*/, int role) const { if(role == Qt::DisplayRole) switch(section) { case COLUMN_THREAD_TITLE: return tr("Title"); case COLUMN_THREAD_DATE: return tr("Date"); case COLUMN_THREAD_AUTHOR: return tr("Author"); default: return QVariant(); } if(role == Qt::DecorationRole) switch(section) { case COLUMN_THREAD_DISTRIBUTION: return FilesDefs::getIconFromQtResourcePath(":/icons/flag-green.png"); case COLUMN_THREAD_READ: return FilesDefs::getIconFromQtResourcePath(":/images/message-state-read.png"); default: return QVariant(); } return QVariant(); } QVariant RsGxsForumModel::data(const QModelIndex &index, int role) const { #ifdef DEBUG_FORUMMODEL std::cerr << "calling data(" << index << ") role=" << role << std::endl; #endif if(!index.isValid()) return QVariant(); switch(role) { case Qt::SizeHintRole: return sizeHintRole(index.column()) ; case Qt::StatusTipRole:return QVariant(); default: break; } void *ref = (index.isValid())?index.internalPointer():NULL ; uint32_t entry = 0; #ifdef DEBUG_FORUMMODEL std::cerr << "data(" << index << ")" ; #endif if(!ref) { #ifdef DEBUG_FORUMMODEL std::cerr << " [empty]" << std::endl; #endif return QVariant() ; } if(!convertRefPointerToTabEntry(ref,entry) || entry >= mPosts.size()) { #ifdef DEBUG_FORUMMODEL std::cerr << "Bad pointer: " << (void*)ref << std::endl; #endif return QVariant() ; } const ForumModelPostEntry& fmpe(mPosts[entry]); if(role == Qt::FontRole) { QFont font ; font.setBold( (fmpe.mPostFlags & (ForumModelPostEntry::FLAG_POST_HAS_UNREAD_CHILDREN | ForumModelPostEntry::FLAG_POST_IS_PINNED)) || IS_MSG_UNREAD(fmpe.mMsgStatus)); return QVariant(font); } if(role == UnreadChildrenRole) return bool(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_HAS_UNREAD_CHILDREN); #ifdef DEBUG_FORUMMODEL std::cerr << " [ok]" << std::endl; #endif switch(role) { case Qt::DisplayRole: return displayRole (fmpe,index.column()) ; case Qt::DecorationRole: return decorationRole(fmpe,index.column()) ; case Qt::ToolTipRole: return toolTipRole (fmpe,index.column()) ; case Qt::UserRole: return userRole (fmpe,index.column()) ; case Qt::TextColorRole: return textColorRole (fmpe,index.column()) ; case Qt::BackgroundRole: return backgroundRole(fmpe,index.column()) ; case FilterRole: return filterRole (fmpe,index.column()) ; case ThreadPinnedRole: return pinnedRole (fmpe,index.column()) ; case MissingRole: return missingRole (fmpe,index.column()) ; case StatusRole: return statusRole (fmpe,index.column()) ; case SortRole: return sortRole (fmpe,index.column()) ; default: return QVariant(); } } QVariant RsGxsForumModel::textColorRole(const ForumModelPostEntry& fmpe,int /*column*/) const { if( (fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_MISSING)) return QVariant(mTextColorMissing); if(IS_MSG_UNREAD(fmpe.mMsgStatus)) return QVariant(mTextColorUnread); else if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_PINNED) return QVariant(mTextColorPinned); else return QVariant(mTextColorRead); return QVariant(); } QVariant RsGxsForumModel::statusRole(const ForumModelPostEntry& fmpe,int column) const { if(column != COLUMN_THREAD_DATA) return QVariant(); return QVariant(fmpe.mMsgStatus); } QVariant RsGxsForumModel::filterRole(const ForumModelPostEntry& fmpe,int /*column*/) const { if(!mFilteringEnabled || (fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_CHILDREN_PASSES_FILTER)) return QVariant(FilterString); return QVariant(QString()); } uint32_t RsGxsForumModel::recursUpdateFilterStatus(ForumModelIndex i,int column,const QStringList& strings) { QString s ; uint32_t count = 0; switch(column) { default: case COLUMN_THREAD_DATE: case COLUMN_THREAD_TITLE: s = displayRole(mPosts[i],column).toString(); break; case COLUMN_THREAD_AUTHOR: { QString comment ; QList icons; GxsIdDetails::MakeIdDesc(mPosts[i].mAuthorId, false,s, icons, comment,GxsIdDetails::ICON_TYPE_NONE); } break; } if(!strings.empty()) { mPosts[i].mPostFlags &= ~(ForumModelPostEntry::FLAG_POST_PASSES_FILTER | ForumModelPostEntry::FLAG_POST_CHILDREN_PASSES_FILTER); for(auto iter(strings.begin()); iter != strings.end(); ++iter) if(s.contains(*iter,Qt::CaseInsensitive)) { mPosts[i].mPostFlags |= ForumModelPostEntry::FLAG_POST_PASSES_FILTER | ForumModelPostEntry::FLAG_POST_CHILDREN_PASSES_FILTER; count++; break; } } else { mPosts[i].mPostFlags |= ForumModelPostEntry::FLAG_POST_PASSES_FILTER |ForumModelPostEntry::FLAG_POST_CHILDREN_PASSES_FILTER; count++; } for(uint32_t j=0;j 0) mPosts[i].mPostFlags |= ForumModelPostEntry::FLAG_POST_CHILDREN_PASSES_FILTER; } return count; } void RsGxsForumModel::setFilter(int column,const QStringList& strings,uint32_t& count) { preMods(); if(!strings.empty()) { count = recursUpdateFilterStatus(ForumModelIndex(0),column,strings); mFilteringEnabled = true; } else { count=0; mFilteringEnabled = false; } postMods(); } QVariant RsGxsForumModel::missingRole(const ForumModelPostEntry& fmpe,int /*column*/) const { if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_MISSING) return QVariant(true); else return QVariant(false); } QVariant RsGxsForumModel::toolTipRole(const ForumModelPostEntry& fmpe,int column) const { if(column == COLUMN_THREAD_DISTRIBUTION) 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.")) ; default: return QVariant("[ERROR: missing reputation level information - contact the developers]"); } if(column == COLUMN_THREAD_AUTHOR) { QString str,comment ; QList icons; if(!GxsIdDetails::MakeIdDesc(fmpe.mAuthorId, true, str, icons, comment,GxsIdDetails::ICON_TYPE_AVATAR)) return QVariant(); int S = QFontMetricsF(QApplication::font()).height(); QImage pix( (*icons.begin()).pixmap(QSize(4*S,4*S)).toImage()); QString embeddedImage; if(RsHtml::makeEmbeddedImage(pix.scaled(QSize(4*S,4*S), Qt::KeepAspectRatio, Qt::SmoothTransformation), embeddedImage, 8*S * 8*S)) comment = "
" + embeddedImage + "" + comment + "
"; return comment; } return QVariant(); } QVariant RsGxsForumModel::pinnedRole(const ForumModelPostEntry& fmpe,int /*column*/) const { if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_PINNED) return QVariant(true); else return QVariant(false); } QVariant RsGxsForumModel::backgroundRole(const ForumModelPostEntry& fmpe,int /*column*/) const { if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_PINNED) return QVariant(QBrush(QColor(255,200,180))); if(mFilteringEnabled && (fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_PASSES_FILTER)) return QVariant(QBrush(QColor(255,240,210))); return QVariant(); } QVariant RsGxsForumModel::sizeHintRole(int col) const { float factor = QFontMetricsF(QApplication::font()).height()/14.0f ; switch(col) { default: 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 )); case COLUMN_THREAD_DISTRIBUTION: return QVariant( QSize(factor * 15 , factor*14 )); } } QVariant RsGxsForumModel::authorRole(const ForumModelPostEntry& fmpe,int column) const { if(column == COLUMN_THREAD_DATA) return QVariant(QString::fromStdString(fmpe.mAuthorId.toStdString())); return QVariant(); } QVariant RsGxsForumModel::sortRole(const ForumModelPostEntry& fmpe,int column) const { switch(column) { case COLUMN_THREAD_DATE: if(mSortMode == SORT_MODE_PUBLISH_TS) return QVariant(QString::number(fmpe.mPublishTs)); // we should probably have leading zeroes here else return QVariant(QString::number(fmpe.mMostRecentTsInThread)); // we should probably have leading zeroes here case COLUMN_THREAD_READ: return QVariant((bool)IS_MSG_UNREAD(fmpe.mMsgStatus)); case COLUMN_THREAD_DISTRIBUTION: return decorationRole(fmpe,column); case COLUMN_THREAD_AUTHOR: { QString str,comment ; QList icons; GxsIdDetails::MakeIdDesc(fmpe.mAuthorId, false, str, icons, comment,GxsIdDetails::ICON_TYPE_NONE); return QVariant(str); } default: return displayRole(fmpe,column); } } QVariant RsGxsForumModel::displayRole(const ForumModelPostEntry& fmpe,int col) const { switch(col) { 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(fmpe.mTitle.c_str())); else return QVariant(QString::fromUtf8(fmpe.mTitle.c_str())); case COLUMN_THREAD_READ:return QVariant(); case COLUMN_THREAD_DATE:{ if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_MISSING) return QVariant(QString()); QDateTime qtime; qtime.setTime_t(fmpe.mPublishTs); return QVariant(DateTime::formatDateTime(qtime)); } case COLUMN_THREAD_DISTRIBUTION: case COLUMN_THREAD_AUTHOR:{ QString name; RsGxsId id = RsGxsId(fmpe.mAuthorId.toStdString()); if(id.isNull()) return QVariant(tr("[Notification]")); if(GxsIdTreeItemDelegate::computeName(id,name)) return name; return QVariant(tr("[Unknown]")); } case COLUMN_THREAD_MSGID: return QVariant(); #ifdef TODO if (filterColumn == COLUMN_THREAD_CONTENT) { // need content for filter QTextDocument doc; doc.setHtml(QString::fromUtf8(msg.mMsg.c_str())); item->setText(COLUMN_THREAD_CONTENT, doc.toPlainText().replace(QString("\n"), QString(" "))); } #endif default: return QVariant("[ TODO ]"); } return QVariant("[ERROR]"); } QVariant RsGxsForumModel::userRole(const ForumModelPostEntry& fmpe,int col) const { switch(col) { case COLUMN_THREAD_AUTHOR: return QVariant(QString::fromStdString(fmpe.mAuthorId.toStdString())); case COLUMN_THREAD_MSGID: return QVariant(QString::fromStdString(fmpe.mMsgId.toStdString())); default: return QVariant(); } } QVariant RsGxsForumModel::decorationRole(const ForumModelPostEntry& fmpe,int col) const { bool exist=false; switch(col) { case COLUMN_THREAD_DISTRIBUTION: return QVariant(fmpe.mReputationWarningLevel); case COLUMN_THREAD_READ: return QVariant(fmpe.mMsgStatus); case COLUMN_THREAD_AUTHOR://Return icon as place holder. return FilesDefs::getIconFromGxsIdCache(RsGxsId(fmpe.mAuthorId.toStdString()),QIcon(), exist); } return QVariant(); } const RsGxsGroupId& RsGxsForumModel::currentGroupId() const { return mForumGroup.mMeta.mGroupId; } void RsGxsForumModel::updateForum(const RsGxsGroupId& forum_group_id) { if(forum_group_id.isNull()) return; update_posts(forum_group_id); } void RsGxsForumModel::clear() { preMods(); mPosts.clear(); mPostVersions.clear(); postMods(); emit forumLoaded(); } void RsGxsForumModel::setPosts(const RsGxsForumGroup& group, const std::vector& posts,const std::map > >& post_versions) { preMods(); if(mTreeMode == TREE_MODE_FLAT) beginRemoveRows(QModelIndex(),0,mPosts.size()-1); else beginRemoveRows(QModelIndex(),0,mPosts[0].mChildren.size()-1); endRemoveRows(); mForumGroup = group; mPosts = posts; mPostVersions = post_versions; // now update prow for all posts for(uint32_t i=0;i forumIds; std::vector msg_metas; std::vector groups; forumIds.push_back(group_id); if(!rsGxsForums->getForumsInfo(forumIds,groups) || groups.size() != 1) { std::cerr << __PRETTY_FUNCTION__ << " failed to retrieve forum group info for forum " << group_id << std::endl; return; } if(!rsGxsForums->getForumMsgMetaData(group_id,msg_metas)) { 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 auto post_versions = new std::map > >() ; std::vector *vect = new std::vector(); RsGxsForumGroup group = groups[0]; computeMessagesHierarchy(group,msg_metas,*vect,*post_versions); // 3 - update the model in the UI thread. RsQThreadUtils::postToObject( [group,vect,post_versions,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(group,*vect,*post_versions) ; delete vect; delete post_versions; }, 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); #ifdef DEBUG_FORUMMODEL std::cerr << "Added new entry " << N << " children of " << parent << std::endl; #endif if(N == parent) std::cerr << "(EE) trying to add a post as its own parent!" << std::endl; 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 RsMsgMetaData& msg, bool /*useChildTS*/, ForumModelPostEntry& fentry) { fentry.mTitle = msg.mMsgName; fentry.mAuthorId = msg.mAuthorId; fentry.mMsgId = msg.mMsgId; fentry.mPublishTs = msg.mPublishTs; fentry.mPostFlags = 0; fentry.mMsgStatus = msg.mMsgStatus; if(mForumGroup.mPinnedPosts.ids.find(msg.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 computeReputationLevel(mForumGroup.mMeta.mSignFlags,fentry); } void RsGxsForumModel::computeReputationLevel(uint32_t forum_sign_flags,ForumModelPostEntry& fentry) { uint32_t idflags =0; RsReputationLevel reputation_level = rsReputations->overallReputationLevel(fentry.mAuthorId, &idflags); bool redacted = false; if(reputation_level == RsReputationLevel::LOCALLY_NEGATIVE) fentry.mPostFlags |= ForumModelPostEntry::FLAG_POST_IS_REDACTED; else 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 == RsReputationLevel::UNKNOWN) fentry.mReputationWarningLevel = 3 ; else if(reputation_level == RsReputationLevel::LOCALLY_NEGATIVE) fentry.mReputationWarningLevel = 2 ; else if(reputation_level < rsGxsForums->minReputationForForwardingMessages(forum_sign_flags,idflags)) fentry.mReputationWarningLevel = 1 ; else fentry.mReputationWarningLevel = 0 ; } static bool decreasing_time_comp(const std::pair& e1,const std::pair& e2) { return e2.first < e1.first ; } void RsGxsForumModel::computeMessagesHierarchy(const RsGxsForumGroup& forum_group, const std::vector& msgs_metas_array, std::vector& posts, std::map > >& mPostVersions ) { std::cerr << "updating messages data with " << msgs_metas_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 > 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 ( auto msgIt = msgs.begin(); msgIt != msgs.end();++msgIt) { if(!msgIt->second.mOrigMsgId.isNull() && msgIt->second.mOrigMsgId != msgIt->second.mMsgId) { #ifdef DEBUG_FORUMS std::cerr << " Post " << msgIt->second.mMeta.mMsgId << " is a new version of " << msgIt->second.mMeta.mOrigMsgId << std::endl; #endif auto msgIt2 = msgs.find(msgIt->second.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.mAuthorId != msgIt->second.mAuthorId) { if( !IS_FORUM_MSG_MODERATION(msgIt->second.mMsgFlags) ) // if authors are different the moderation flag needs to be set on the editing msg continue ; if( !forum_group.canEditPosts(msgIt->second.mAuthorId)) // if author is not a moderator, continue continue ; } // always add the post a self version if(mPostVersions[msgIt->second.mOrigMsgId].empty()) mPostVersions[msgIt->second.mOrigMsgId].push_back(std::make_pair(msgIt2->second.mPublishTs,msgIt2->second.mMsgId)) ; mPostVersions[msgIt->second.mOrigMsgId].push_back(std::make_pair(msgIt->second.mPublishTs,msgIt->second.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(auto it(mPostVersions.begin());it!=mPostVersions.end();++it) { auto& v(it->second) ; for(size_t i=0;ifirst) { RsGxsMessageId sub_msg_id = v[i].second ; auto it2 = mPostVersions.find(sub_msg_id); if(it2 != mPostVersions.end()) { for(size_t j=0;jsecond.size();++j) if(it2->second[j].second != sub_msg_id) // dont copy it, since it is already present at slot i v.push_back(it2->second[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 std::map > > mTmp; std::map most_recent_versions ; for(auto 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 std::sort(it->second.begin(),it->second.end(),decreasing_time_comp) ; #ifdef DEBUG_FORUMS std::cerr << " most recent version " << (*it)[0].first << " " << (*it)[0].second << std::endl; #endif for(size_t i=1;isecond.size();++i) { msgs.erase(it->second[i].second) ; #ifdef DEBUG_FORUMS std::cerr << " older version " << (*it)[i].first << " " << (*it)[i].second << std::endl; #endif } mTmp[it->second[0].second] = it->second ; // 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(size_t i=1;isecond.size();++i) most_recent_versions[it->second[i].second] = it->second[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 ( auto msgIt = msgs.begin(); msgIt != msgs.end();++msgIt) { if(msgIt->second.mParentId.isNull()) { /* add all threads */ const RsMsgMetaData& msg = msgIt->second; #ifdef DEBUG_FORUMS std::cerr << "GxsForumsFillThread::run() Adding TopLevel Thread: mId: " << msg.mMsgId << std::endl; #endif ForumModelPostEntry entry; convertMsgToPostEntry(forum_group,msg, mUseChildTS, entry); ForumModelIndex entry_index = addEntry(posts,entry,0); //if (!mFlatView) threadStack.push_back(std::make_pair(msg.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.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.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(*it,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. auto mit = msgs.find(*it2) ; if(mit == msgs.end()) { std::cerr << "GxsForumsFillThread::run() Cannot find submessage " << *it2 << " !!!" << std::endl; continue ; } const RsMsgMetaData& msg(mit->second) ; #ifdef DEBUG_FORUMS std::cerr << "GxsForumsFillThread::run() adding sub_item " << msg.mMsgId << std::endl; #endif ForumModelPostEntry e ; convertMsgToPostEntry(forum_group,msg,mUseChildTS,e) ; ForumModelIndex e_index = addEntry(posts,e, threadPair.second); //calculateExpand(msg, item); /* add item to process list */ threadStack.push_back(std::make_pair(msg.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 } void RsGxsForumModel::setMsgReadStatus(const QModelIndex& i,bool read_status,bool with_children) { if(!i.isValid()) return ; // no need to call preMods()/postMods() here because we'renot changing the model void *ref = i.internalPointer(); uint32_t entry = 0; if(!convertRefPointerToTabEntry(ref,entry) || entry >= mPosts.size()) return ; bool has_unread_below, has_read_below; recursSetMsgReadStatus(entry,read_status,with_children) ; recursUpdateReadStatusAndTimes(0,has_unread_below,has_read_below); } void RsGxsForumModel::recursSetMsgReadStatus(ForumModelIndex i,bool read_status,bool with_children) { int newStatus = (read_status ? mPosts[i].mMsgStatus & ~static_cast(GXS_SERV::GXS_MSG_STATUS_GUI_UNREAD) : mPosts[i].mMsgStatus | static_cast(GXS_SERV::GXS_MSG_STATUS_GUI_UNREAD)); bool bChanged = (mPosts[i].mMsgStatus != newStatus); mPosts[i].mMsgStatus = newStatus; //Remove Unprocessed and New flags mPosts[i].mMsgStatus &= ~(GXS_SERV::GXS_MSG_STATUS_UNPROCESSED | GXS_SERV::GXS_MSG_STATUS_GUI_NEW); if (bChanged) { //Don't recurs post versions as this should be done before, if no change. uint32_t token; auto s = getPostVersions(mPosts[i].mMsgId) ; if(!s.empty()) for(auto it(s.begin());it!=s.end();++it) { rsGxsForums->setMessageReadStatus(token,std::make_pair( mForumGroup.mMeta.mGroupId, it->second ), read_status); std::cerr << "Setting version " << it->second << " of post " << mPosts[i].mMsgId << " as read." << std::endl; } else rsGxsForums->setMessageReadStatus(token,std::make_pair( mForumGroup.mMeta.mGroupId, mPosts[i].mMsgId ), read_status); QModelIndex itemIndex = createIndex(i - 1, 0, &mPosts[i]); emit dataChanged(itemIndex, itemIndex); } if(!with_children) return; for(uint32_t j=0;jsecond.size();++i) if(it->second[i].second == mid) { postId = it->first; break; } for(uint32_t i=1;i& entries,ForumModelIndex index,int depth) { const ForumModelPostEntry& e(entries[index]); QDateTime qtime; qtime.setTime_t(e.mPublishTs); std::cerr << std::string(depth*2,' ') << index << " : " << e.mAuthorId.toStdString() << " " << QString("%1").arg((uint32_t)e.mPostFlags,8,16,QChar('0')).toStdString() << " " << QString("%1").arg((uint32_t)e.mMsgStatus,8,16,QChar('0')).toStdString() << " " << qtime.toString().toStdString() << " \"" << e.mTitle << "\"" << std::endl; for(uint32_t i=0;i= mPosts.size()) return ; std::cerr << "Setting own opinion for author " << mPosts[entry].mAuthorId << " to " << static_cast(op) << std::endl; RsGxsId author_id = mPosts[entry].mAuthorId; rsReputations->setOwnOpinion(author_id,op) ; // update opinions and distribution flags. No need to re-load all posts. for(uint32_t i=0;i