/******************************************************************************* * retroshare-gui/src/gui/gxschannels/GxsChannelPostsModel.cpp * * * * Copyright 2020 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 "retroshare/rsgxsflags.h" #include "retroshare/rsgxschannels.h" #include "retroshare/rsexpr.h" #include "gui/MainWindow.h" #include "gui/mainpagestack.h" #include "gui/common/FilesDefs.h" #include "util/qtthreadsutils.h" #include "util/HandleRichText.h" #include "util/DateTime.h" #include "GxsChannelPostsModel.h" #include "GxsChannelPostFilesModel.h" //#define DEBUG_CHANNEL_MODEL_DATA #define DEBUG_CHANNEL_MODEL Q_DECLARE_METATYPE(RsMsgMetaData) Q_DECLARE_METATYPE(RsGxsChannelPost) std::ostream& operator<<(std::ostream& o, const QModelIndex& i);// defined elsewhere RsGxsChannelPostsModel::RsGxsChannelPostsModel(QObject *parent) : QAbstractItemModel(parent), mTreeMode(RsGxsChannelPostsModel::TREE_MODE_GRID), mColumns(6) { initEmptyHierarchy(); } RsGxsChannelPostsModel::~RsGxsChannelPostsModel() { rsEvents->unregisterEventsHandler(mEventHandlerId); } void RsGxsChannelPostsModel::setMode(TreeMode mode) { mTreeMode = mode; if(mode == TREE_MODE_LIST) setNumColumns(2); triggerViewUpdate(true,true); } void RsGxsChannelPostsModel::computeCommentCounts( std::vector& posts, std::vector& comments) { // Store posts IDs in a std::map to avoid a quadratic cost std::map post_indices; for(uint32_t i=0;isecond>=posts.size() is impossible by construction, since post_indices // is previously filled using posts ids. if(it == post_indices.end()) continue; ++posts[it->second].mCommentCount; if(IS_MSG_NEW(comments[i].mMeta.mMsgStatus)) ++posts[it->second].mUnreadCommentCount; } } void RsGxsChannelPostsModel::initEmptyHierarchy() { beginResetModel(); mPosts.clear(); mFilteredPosts.clear(); endResetModel(); } void RsGxsChannelPostsModel::preMods() { emit layoutAboutToBeChanged(); } void RsGxsChannelPostsModel::postMods() { emit layoutChanged(); } void RsGxsChannelPostsModel::triggerViewUpdate(bool data_changed, bool layout_changed) { if(data_changed) emit dataChanged(createIndex(0,0,(void*)NULL), createIndex(rowCount()-1,mColumns-1,(void*)NULL)); if(layout_changed) emit layoutChanged(); } void RsGxsChannelPostsModel::getFilesList(std::list& files) { // We use an intermediate map so as to remove duplicates std::map files_map; for(uint32_t i=0;i0) { beginInsertRows(QModelIndex(),0,rowCount()-1); endInsertRows(); } postMods(); } int RsGxsChannelPostsModel::rowCount(const QModelIndex& parent) const { if(parent.column() > 0) return 0; if(mFilteredPosts.empty()) // security. Should never happen. return 0; if(!parent.isValid()) { if(mTreeMode == TREE_MODE_GRID) return (mFilteredPosts.size() + mColumns-1)/mColumns; // mFilteredPosts always has an item at 0, so size()>=1, and mColumn>=1 else return mFilteredPosts.size(); } RsErr() << __PRETTY_FUNCTION__ << " rowCount cannot figure out the proper number of rows." ; return 0; } int RsGxsChannelPostsModel::columnCount(const QModelIndex &/*parent*/) const { if(mTreeMode == TREE_MODE_GRID) return std::min((int)mFilteredPosts.size(),(int)mColumns) ; else return 2; } bool RsGxsChannelPostsModel::getPostData(const QModelIndex& i,RsGxsChannelPost& fmpe) const { if(!i.isValid()) return true; quintptr ref = i.internalId(); uint32_t entry = 0; if(!convertRefPointerToTabEntry(ref,entry) || entry >= mFilteredPosts.size()) return false ; fmpe = mPosts[mFilteredPosts[entry]]; return true; } bool RsGxsChannelPostsModel::hasChildren(const QModelIndex &parent) const { if(!parent.isValid()) return true; return false; // by default, no channel post has children } bool RsGxsChannelPostsModel::convertTabEntryToRefPointer(uint32_t entry,quintptr& 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 = (intptr_t)(entry+1); return true; } bool RsGxsChannelPostsModel::convertRefPointerToTabEntry(quintptr 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 { RsErr() << "(EE) trying to make a ChannelPostsModelIndex out of a number that is larger than 2^32-1 !" << std::endl; return false ; } if(val==0) { RsErr() << "(EE) trying to make a ChannelPostsModelIndex out of index 0." << std::endl; return false; } entry = val - 1; return true; } QModelIndex RsGxsChannelPostsModel::index(int row, int column, const QModelIndex & parent) const { if(row < 0 || column < 0 || column >= (int)mColumns) return QModelIndex(); quintptr ref = getChildRef(parent.internalId(),(mTreeMode == TREE_MODE_GRID)?(column + row*mColumns):row); #ifdef DEBUG_CHANNEL_MODEL_DATA std::cerr << "index-3(" << row << "," << column << " parent=" << parent << ") : " << createIndex(row,column,ref) << std::endl; #endif return createIndex(row,column,ref) ; } QModelIndex RsGxsChannelPostsModel::parent(const QModelIndex& /*index*/) const { return QModelIndex(); // there's no hierarchy here. So nothing to do! } Qt::ItemFlags RsGxsChannelPostsModel::flags(const QModelIndex& index) const { if (!index.isValid()) return Qt::ItemFlags(); return QAbstractItemModel::flags(index); } bool RsGxsChannelPostsModel::setNumColumns(int n) { if(n < 1) { RsErr() << __PRETTY_FUNCTION__ << " Attempt to set a number of column of 0. This is wrong." << std::endl; return false; } if((int)mColumns == n) return false; preMods(); mColumns = n; postMods(); return true; } quintptr RsGxsChannelPostsModel::getChildRef(quintptr ref,int index) const { if (index < 0) return 0; if(ref == quintptr(0)) { quintptr new_ref; convertTabEntryToRefPointer(index,new_ref); return new_ref; } else return 0 ; } quintptr RsGxsChannelPostsModel::getParentRow(quintptr ref,int& row) const { ChannelPostsModelIndex ref_entry; if(!convertRefPointerToTabEntry(ref,ref_entry) || ref_entry >= mFilteredPosts.size()) return 0 ; if(ref_entry == 0) { RsErr() << "getParentRow() shouldn't be asked for the parent of NULL" << std::endl; row = 0; } else row = ref_entry-1; return 0; } int RsGxsChannelPostsModel::getChildrenCount(quintptr ref) const { if(ref == quintptr(0)) return rowCount()-1; return 0; } QVariant RsGxsChannelPostsModel::data(const QModelIndex &index, int role) const { #ifdef DEBUG_CHANNEL_MODEL_DATA 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; } quintptr ref = (index.isValid())?index.internalId():0 ; uint32_t entry = 0; #ifdef DEBUG_CHANNEL_MODEL_DATA std::cerr << "data(" << index << ")" ; #endif if(!ref) { #ifdef DEBUG_CHANNEL_MODEL_DATA std::cerr << " [empty]" << std::endl; #endif return QVariant() ; } if(!convertRefPointerToTabEntry(ref,entry) || entry >= mFilteredPosts.size()) { #ifdef DEBUG_CHANNEL_MODEL_DATA std::cerr << "Bad pointer: " << (void*)ref << std::endl; #endif return QVariant() ; } const RsGxsChannelPost& fmpe(mPosts[mFilteredPosts[entry]]); switch(role) { case Qt::DisplayRole: return displayRole (fmpe,index.column()) ; case Qt::UserRole: return userRole (fmpe,index.column()) ; default: return QVariant(); } } QVariant RsGxsChannelPostsModel::sizeHintRole(int /* col */) const { float factor = QFontMetricsF(QApplication::font()).height()/14.0f ; return QVariant( QSize(factor * 170, factor*14 )); } QVariant RsGxsChannelPostsModel::displayRole(const RsGxsChannelPost& fmpe,int col) const { switch(col) { default: return QString::fromUtf8(fmpe.mMeta.mMsgName.c_str()); } return QVariant("[ERROR]"); } QVariant RsGxsChannelPostsModel::userRole(const RsGxsChannelPost& fmpe,int col) const { switch(col) { default: return QVariant::fromValue(fmpe); } } const RsGxsGroupId& RsGxsChannelPostsModel::currentGroupId() const { return mChannelGroup.mMeta.mGroupId; } void RsGxsChannelPostsModel::updateChannel(const RsGxsGroupId& channel_group_id) { if(channel_group_id.isNull()) return; update_posts(channel_group_id); } void RsGxsChannelPostsModel::clear() { preMods(); initEmptyHierarchy(); postMods(); emit channelPostsLoaded(); } bool operator<(const RsGxsChannelPost& p1,const RsGxsChannelPost& p2) { return p1.mMeta.mPublishTs > p2.mMeta.mPublishTs; } void RsGxsChannelPostsModel::updateSinglePost(const RsGxsChannelPost& post,std::set& added_files,std::set& removed_files) { #ifdef DEBUG_CHANNEL_MODEL RsDbg() << "updating single post for group id=" << currentGroupId() << " and msg id=" << post.mMeta.mMsgId ; #endif added_files.clear(); removed_files.clear(); emit layoutAboutToBeChanged(); // linear search. Not good at all, but normally this is just for a single post. bool found = false; const auto& new_post_meta(post.mMeta); for(uint32_t j=0;j& posts) { preMods(); initEmptyHierarchy(); mChannelGroup = group; // createPostsArray(posts); mPosts = posts; std::sort(mPosts.begin(),mPosts.end()); for(uint32_t i=0;i0) { beginInsertRows(QModelIndex(),0,rowCount()-1); endInsertRows(); } postMods(); emit channelPostsLoaded(); } void RsGxsChannelPostsModel::update_posts(const RsGxsGroupId& group_id) { if(group_id.isNull()) return; MainWindow::getPage(MainWindow::Channels)->setCursor(Qt::WaitCursor) ; // Maybe we should pass that widget when calling update_posts RsThread::async([this, group_id]() { // 1 - get message data from p3GxsChannels std::list channelIds; std::vector msg_metas; std::vector groups; channelIds.push_back(group_id); if(!rsGxsChannels->getChannelsInfo(channelIds,groups) || groups.size() != 1) { std::cerr << __PRETTY_FUNCTION__ << " failed to retrieve channel group info for channel " << group_id << std::endl; return; } RsGxsChannelGroup group = groups[0]; // We use the heap because the arrays need to be stored accross async std::vector *posts = new std::vector(); std::vector *comments = new std::vector(); std::vector *votes = new std::vector(); if(!rsGxsChannels->getChannelAllContent(group_id, *posts,*comments,*votes)) { std::cerr << __PRETTY_FUNCTION__ << " failed to retrieve channel messages for channel " << group_id << std::endl; return; } std::cerr << "Got channel all content for channel " << group_id << std::endl; std::cerr << " posts : " << posts->size() << std::endl; std::cerr << " comments: " << comments->size() << std::endl; std::cerr << " votes : " << votes->size() << std::endl; // This shouldn't be needed normally. We need it until a background process computes the number of comments per // post and stores it in the service string. Since we request all data, this process isn't costing much anyway. computeCommentCounts(*posts,*comments); // 2 - update the model in the UI thread. RsQThreadUtils::postToObject( [group,posts,comments,votes,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,*posts) ; delete posts; delete comments; delete votes; MainWindow::getPage(MainWindow::Channels)->setCursor(Qt::ArrowCursor) ; }, this ); }); } void RsGxsChannelPostsModel::setAllMsgReadStatus(bool read_status) { // No need to call preMods()/postMods() here because we're not changing the model // All operations below are done async // 1 - copy all msg/grp id groups, so that changing the mPosts list while calling the async method will not break the work std::vector pairs; for(uint32_t i=0;isetMessageReadStatus(p,read_status)) RsErr() << "setAllMsgReadStatus: failed to change status of msg " << p.first << " in group " << p.second << " to status " << read_status << std::endl; }); // 3 - update the local model data, since we don't catch the READ_STATUS_CHANGED event later, to avoid re-loading the msg. for(uint32_t i=0;i= mFilteredPosts.size()) return ; rsGxsChannels->setMessageReadStatus(RsGxsGrpMsgIdPair(mPosts[mFilteredPosts[entry]].mMeta.mGroupId,mPosts[mFilteredPosts[entry]].mMeta.mMsgId),read_status); // Quick update to the msg itself. Normally setMsgReadStatus will launch an event, // that we can catch to update the msg, but all the information is already here. if(read_status) mPosts[mFilteredPosts[entry]].mMeta.mMsgStatus &= ~(GXS_SERV::GXS_MSG_STATUS_GUI_UNREAD | GXS_SERV::GXS_MSG_STATUS_GUI_NEW); else mPosts[mFilteredPosts[entry]].mMeta.mMsgStatus |= GXS_SERV::GXS_MSG_STATUS_GUI_UNREAD; mPosts[mFilteredPosts[entry]].mUnreadCommentCount = 0; emit dataChanged(i,i); } QModelIndex RsGxsChannelPostsModel::getIndexOfMessage(const RsGxsMessageId& mid) const { // Brutal search. This is not so nice, so dont call that in a loop! If too costly, we'll use a map. RsGxsMessageId postId = mid; for(uint32_t i=0;i