From 978b30fcee696d7ccfbe07b68eec97c131bc3866 Mon Sep 17 00:00:00 2001 From: jolavillette Date: Mon, 20 Jul 2026 00:07:07 +0200 Subject: [PATCH] fix(gui): channel grid randomly blank after adding a post Adding a post to a channel sometimes left the grid empty; switching to another channel and back made the posts reappear. The model data was always correct, but the *view* was corrupted by malformed Qt model signals emitted on the incremental single-post path. RsGxsChannelPostsModel::updateSinglePost() emitted its own layoutAboutToBeChanged() and then called updateFilter(), which emitted a second layoutAboutToBeChanged()/layoutChanged() pair. That left one layoutAboutToBeChanged() unmatched, so the attached grid view stayed in a "layout changing" state. On top of that, updateFilter() faked a full beginInsertRows(0, rowCount()-1)/endInsertRows() over rows that already existed, lying to the view about the model structure. Whether this blanked the viewport depended on grid geometry (post count modulo the column count) and async/repaint timing, hence the intermittent behaviour. A full model reset (beginResetModel in initEmptyHierarchy, triggered by switching channels) always rebuilt the view correctly, which is why the workaround worked. Fix: emit a single balanced layout-change (preMods()/postMods()) and drop the bogus beginInsertRows(). layoutChanged() is the correct signal for the row<->post reflow that std::sort() produces. Also reset the active filter in initEmptyHierarchy() (run by setPosts() on every full reload): the widget clears the search box silently (whileBlocking) without notifying the model, so a filter left over from a previously viewed channel was otherwise re-applied by updateFilter() on the next incoming post and could hide every post. Placing the reset in initEmptyHierarchy() keeps the fix self-contained and independent of how setPosts() is implemented. Boards (Posted) carried the same beginInsertRows() anti-pattern in setFilter() and setPosts(); the lie-to-the-view is removed there too for correctness and consistency. setPostsInterval() (a legitimate paginated insert/remove) is left untouched. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/gui/Posted/PostedPostsModel.cpp | 19 +++++--------- .../gui/gxschannels/GxsChannelPostsModel.cpp | 26 ++++++++++++++----- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/retroshare-gui/src/gui/Posted/PostedPostsModel.cpp b/retroshare-gui/src/gui/Posted/PostedPostsModel.cpp index e827a2fbb..d4a81a3f6 100644 --- a/retroshare-gui/src/gui/Posted/PostedPostsModel.cpp +++ b/retroshare-gui/src/gui/Posted/PostedPostsModel.cpp @@ -223,12 +223,10 @@ void RsPostedPostsModel::setFilter(const QStringList& strings, uint32_t& count) std::cerr << "After filtering: " << count << " posts remain." << std::endl; - if (rowCount()>0) - { - beginInsertRows(QModelIndex(),0,rowCount()-1); - endInsertRows(); - } - + // No beginInsertRows() here: the filtered set is rebuilt from scratch above, so + // the surrounding reset + postMods() (layoutChanged) refresh the view. Faking a + // row insertion with a mismatched range is a lie to the view (same anti-pattern + // that could blank the channels grid) and is not needed. postMods(); } @@ -572,12 +570,9 @@ void RsPostedPostsModel::setPosts(const RsPostedGroup& group, std::vector0) - { - beginInsertRows(QModelIndex(),0,rowCount()-1); - endInsertRows(); - } - + // No beginInsertRows() here: setFilter() above already reset and repopulated the + // model, so postMods() (layoutChanged) is enough. Faking a row insertion with a + // mismatched range is the same anti-pattern that could blank the channels grid. postMods(); emit boardPostsLoaded(); diff --git a/retroshare-gui/src/gui/gxschannels/GxsChannelPostsModel.cpp b/retroshare-gui/src/gui/gxschannels/GxsChannelPostsModel.cpp index 22d1dbcba..7124d0317 100644 --- a/retroshare-gui/src/gui/gxschannels/GxsChannelPostsModel.cpp +++ b/retroshare-gui/src/gui/gxschannels/GxsChannelPostsModel.cpp @@ -75,6 +75,15 @@ void RsGxsChannelPostsModel::initEmptyHierarchy() mPosts.clear(); mFilteredPosts.clear(); + // Reset the active filter on every (re)load. initEmptyHierarchy() is run by + // setPosts(), and the widget clears the search box silently (whileBlocking) + // without notifying the model; a filter left over from a previously viewed + // channel would otherwise be re-applied by updateFilter() on the next incoming + // post and could hide every post. Doing it here keeps the fix independent of + // how setPosts() is implemented. + mFilteredStrings.clear(); + mFilterUnread = false; + endResetModel(); } @@ -147,12 +156,12 @@ void RsGxsChannelPostsModel::updateFilter(uint32_t& count) count = mFilteredPosts.size(); - if (rowCount()>0) - { - beginInsertRows(QModelIndex(),0,rowCount()-1); - endInsertRows(); - } - + // The row<->post mapping is fully rebuilt above (posts may have been added, + // removed or reordered by the caller), so we signal a layout change through the + // preMods()/postMods() pair rather than faking a row insertion. Emitting + // beginInsertRows() with a range that does not match a real insertion left the + // attached view inconsistent and could randomly blank the grid right after a + // post was added (only a full channel reload fixed it). postMods(); } @@ -461,7 +470,10 @@ void RsGxsChannelPostsModel::updateSinglePost(const RsGxsChannelPost& post,std:: added_files.clear(); removed_files.clear(); - emit layoutAboutToBeChanged(); + // NB: no emit layoutAboutToBeChanged() here. The single balanced layout-change + // signal is emitted by updateFilter() below (preMods()/postMods()). Opening a + // second, unmatched layoutAboutToBeChanged() here left the view stuck in a + // "layout changing" state and could randomly blank the grid after a new post. // linear search. Not good at all, but normally this is just for a single post.