mirror of
https://github.com/RetroShare/RetroShare.git
synced 2025-05-20 15:00:36 -04:00
756 lines
22 KiB
C++
756 lines
22 KiB
C++
/*******************************************************************************
|
|
* retroshare-gui/src/gui/gxschannels/GxsChannelPostsModel.cpp *
|
|
* *
|
|
* Copyright 2020 by Cyril Soler <csoler@users.sourceforge.net> *
|
|
* *
|
|
* 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 <https://www.gnu.org/licenses/>. *
|
|
* *
|
|
*******************************************************************************/
|
|
|
|
#include <QApplication>
|
|
#include <QFontMetrics>
|
|
#include <QModelIndex>
|
|
#include <QIcon>
|
|
|
|
#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<RsGxsChannelPost>& posts, std::vector<RsGxsComment>& comments)
|
|
{
|
|
// Store posts IDs in a std::map to avoid a quadratic cost
|
|
|
|
std::map<RsGxsMessageId,uint32_t> post_indices;
|
|
|
|
for(uint32_t i=0;i<posts.size();++i)
|
|
{
|
|
post_indices[posts[i].mMeta.mMsgId] = i;
|
|
posts[i].mCommentCount = 0; // should be 0 already, but we secure that value.
|
|
posts[i].mUnreadCommentCount = 0;
|
|
}
|
|
|
|
// now look into comments and increase the count
|
|
|
|
for(uint32_t i=0;i<comments.size();++i)
|
|
{
|
|
auto it = post_indices.find(comments[i].mMeta.mThreadId);
|
|
|
|
// This happens when because of sync periods, we receive
|
|
// the comments for a post, but not the post itself.
|
|
// In this case, the post the comment refers to is just not here.
|
|
// it->second>=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<ChannelPostFileInfo>& files)
|
|
{
|
|
// We use an intermediate map so as to remove duplicates
|
|
|
|
std::map<RsFileHash,ChannelPostFileInfo> files_map;
|
|
|
|
for(uint32_t i=0;i<mPosts.size();++i)
|
|
for(auto& file:mPosts[i].mFiles)
|
|
files_map.insert(std::make_pair(file.mHash,ChannelPostFileInfo(file,mPosts[i].mMeta.mPublishTs)));
|
|
|
|
files.clear();
|
|
|
|
for(auto& it:files_map)
|
|
files.push_back(it.second);
|
|
}
|
|
|
|
bool RsGxsChannelPostsModel::postPassesFilter(const RsGxsChannelPost& post,const QStringList& strings,bool only_unread) const
|
|
{
|
|
bool passes_strings = true;
|
|
|
|
for(auto& s:strings)
|
|
passes_strings = passes_strings && QString::fromStdString(post.mMeta.mMsgName).contains(s,Qt::CaseInsensitive);
|
|
|
|
if(strings.empty())
|
|
passes_strings = true;
|
|
|
|
if(passes_strings && (!only_unread || (IS_MSG_UNREAD(post.mMeta.mMsgStatus) || IS_MSG_NEW(post.mMeta.mMsgStatus))))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void RsGxsChannelPostsModel::setFilter(const QStringList& strings,bool only_unread, uint32_t& count)
|
|
{
|
|
mFilteredStrings = strings;
|
|
mFilterUnread = only_unread;
|
|
|
|
updateFilter(count);
|
|
}
|
|
|
|
void RsGxsChannelPostsModel::updateFilter(uint32_t& count)
|
|
{
|
|
preMods();
|
|
|
|
mFilteredPosts.clear();
|
|
|
|
for(size_t i=0;i<mPosts.size();++i)
|
|
if(postPassesFilter(mPosts[i],mFilteredStrings,mFilterUnread))
|
|
mFilteredPosts.push_back(i);
|
|
|
|
count = mFilteredPosts.size();
|
|
|
|
if (rowCount()>0)
|
|
{
|
|
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<RsGxsFile>& added_files,std::set<RsGxsFile>& 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<mPosts.size();++j)
|
|
if(new_post_meta.mMsgId == mPosts[j].mMeta.mMsgId) // same post updated
|
|
{
|
|
added_files.insert(post.mFiles.begin(),post.mFiles.end());
|
|
removed_files.insert(mPosts[j].mFiles.begin(),mPosts[j].mFiles.end());
|
|
|
|
auto save_ucc = mPosts[j].mUnreadCommentCount;
|
|
auto save_cc = mPosts[j].mCommentCount;
|
|
|
|
mPosts[j] = post;
|
|
|
|
mPosts[j].mUnreadCommentCount = save_ucc;
|
|
mPosts[j].mCommentCount = save_cc;
|
|
|
|
#ifdef DEBUG_CHANNEL_MODEL
|
|
RsDbg() << " post is an updated existing post." ;
|
|
#endif
|
|
found=true;
|
|
break;
|
|
}
|
|
else if( (new_post_meta.mOrigMsgId == mPosts[j].mMeta.mOrigMsgId || new_post_meta.mOrigMsgId == mPosts[j].mMeta.mMsgId)
|
|
&& mPosts[j].mMeta.mPublishTs < new_post_meta.mPublishTs) // new post version
|
|
{
|
|
added_files.insert(post.mFiles.begin(),post.mFiles.end());
|
|
removed_files.insert(mPosts[j].mFiles.begin(),mPosts[j].mFiles.end());
|
|
|
|
auto old_post_id = mPosts[j].mMeta.mMsgId;
|
|
auto save_ucc = mPosts[j].mUnreadCommentCount;
|
|
auto save_cc = mPosts[j].mCommentCount;
|
|
|
|
mPosts[j] = post;
|
|
|
|
mPosts[j].mCommentCount += save_cc;
|
|
mPosts[j].mUnreadCommentCount += save_ucc;
|
|
mPosts[j].mOlderVersions.insert(old_post_id);
|
|
#ifdef DEBUG_CHANNEL_MODEL
|
|
RsDbg() << " post is an new version of an existing post." ;
|
|
#endif
|
|
found=true;
|
|
break;
|
|
}
|
|
|
|
if(!found)
|
|
{
|
|
#ifdef DEBUG_CHANNEL_MODEL
|
|
RsDbg() << " post is an new post.";
|
|
#endif
|
|
added_files.insert(post.mFiles.begin(),post.mFiles.end());
|
|
mPosts.push_back(post);
|
|
}
|
|
std::sort(mPosts.begin(),mPosts.end());
|
|
|
|
uint32_t count;
|
|
updateFilter(count);
|
|
|
|
triggerViewUpdate(true,false);
|
|
}
|
|
|
|
void RsGxsChannelPostsModel::setPosts(const RsGxsChannelGroup& group, std::vector<RsGxsChannelPost>& posts)
|
|
{
|
|
preMods();
|
|
|
|
initEmptyHierarchy();
|
|
mChannelGroup = group;
|
|
|
|
// createPostsArray(posts);
|
|
|
|
mPosts = posts;
|
|
std::sort(mPosts.begin(),mPosts.end());
|
|
|
|
for(uint32_t i=0;i<mPosts.size();++i)
|
|
mFilteredPosts.push_back(i);
|
|
|
|
#ifdef DEBUG_CHANNEL_MODEL
|
|
// debug_dump();
|
|
#endif
|
|
|
|
if (rowCount()>0)
|
|
{
|
|
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<RsGxsGroupId> channelIds;
|
|
std::vector<RsMsgMetaData> msg_metas;
|
|
std::vector<RsGxsChannelGroup> 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<RsGxsChannelPost> *posts = new std::vector<RsGxsChannelPost>();
|
|
std::vector<RsGxsComment> *comments = new std::vector<RsGxsComment>();
|
|
std::vector<RsGxsVote> *votes = new std::vector<RsGxsVote>();
|
|
|
|
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<RsGxsGrpMsgIdPair> pairs;
|
|
|
|
for(uint32_t i=0;i<mPosts.size();++i)
|
|
{
|
|
bool post_status = !((IS_MSG_UNREAD(mPosts[i].mMeta.mMsgStatus) || IS_MSG_NEW(mPosts[i].mMeta.mMsgStatus)));
|
|
|
|
if(post_status != read_status)
|
|
pairs.push_back(RsGxsGrpMsgIdPair(mPosts[i].mMeta.mGroupId,mPosts[i].mMeta.mMsgId));
|
|
}
|
|
|
|
// 2 - then call the async methods
|
|
|
|
for(uint32_t i=0;i<pairs.size();++i)
|
|
RsThread::async([p=pairs[i], read_status]() // use async because each markRead() waits for the token to complete in order to properly acknowledge it.
|
|
{
|
|
if(!rsGxsChannels->setMessageReadStatus(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<mPosts.size();++i)
|
|
if(read_status)
|
|
mPosts[i].mMeta.mMsgStatus &= ~(GXS_SERV::GXS_MSG_STATUS_GUI_UNREAD | GXS_SERV::GXS_MSG_STATUS_GUI_NEW);
|
|
else
|
|
mPosts[i].mMeta.mMsgStatus |= GXS_SERV::GXS_MSG_STATUS_GUI_UNREAD ;
|
|
|
|
emit dataChanged(createIndex(0,0,(void*)NULL), createIndex(rowCount()-1,mColumns-1,(void*)NULL));
|
|
}
|
|
|
|
void RsGxsChannelPostsModel::updatePostWithNewComment(const RsGxsMessageId& msg_id)
|
|
{
|
|
for(uint32_t i=0;i<mPosts.size();++i)
|
|
if(mPosts[i].mMeta.mMsgId == msg_id)
|
|
{
|
|
++mPosts[i].mUnreadCommentCount;
|
|
emit dataChanged(createIndex(0,0,(void*)NULL), createIndex(rowCount()-1,mColumns-1,(void*)NULL)); // update everything because we don't know the index.
|
|
break;
|
|
}
|
|
}
|
|
void RsGxsChannelPostsModel::setMsgReadStatus(const QModelIndex& i,bool read_status)
|
|
{
|
|
if(!i.isValid())
|
|
return ;
|
|
|
|
// no need to call preMods()/postMods() here because we'renot changing the model
|
|
|
|
quintptr ref = i.internalId();
|
|
uint32_t entry = 0;
|
|
|
|
if(!convertRefPointerToTabEntry(ref,entry) || entry >= 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<mFilteredPosts.size();++i)
|
|
{
|
|
// First look into msg versions, in case the msg is a version of an existing message
|
|
|
|
for(auto& msg_id:mPosts[mFilteredPosts[i]].mOlderVersions)
|
|
if(msg_id == postId)
|
|
{
|
|
quintptr ref ;
|
|
convertTabEntryToRefPointer(i,ref); // we dont use i+1 here because i is not a row, but an index in the mPosts tab
|
|
|
|
if(mTreeMode == TREE_MODE_GRID)
|
|
return createIndex(i/mColumns,i%mColumns, ref);
|
|
else
|
|
return createIndex(i,0, ref);
|
|
}
|
|
|
|
if(mPosts[mFilteredPosts[i]].mMeta.mMsgId == postId)
|
|
{
|
|
quintptr ref ;
|
|
convertTabEntryToRefPointer(i,ref); // we dont use i+1 here because i is not a row, but an index in the mPosts tab
|
|
|
|
if(mTreeMode == TREE_MODE_GRID)
|
|
return createIndex(i/mColumns,i%mColumns, ref);
|
|
else
|
|
return createIndex(i,0, ref);
|
|
}
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
|