- GxsForumModel / PostedPostsModel: hand the id list to the background
worker through a pointer deleted after the call, so the lambda capture
does not deep copy it.
- PostedPostsModel: setAllMsgReadStatus() no longer updates the model
directly. The batched READ_STATUS_CHANGED event now carries the
affected ids and their new state (libretroshare side), and the event
handler applies them locally — so a batch initiated by another
frontend (e.g. webUI) updates the board view exactly the same way,
and no post is re-read from the database.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Profiling code removed from the PR, as requested in review of the sibling
PRs. This reverts commit 8845aa4636e630977f4a6817e8a267735e128e28.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Set RS_GUI_PROFILE to a threshold in milliseconds (0 reports
everything) to get one line per measured operation on stderr, plus a
50 ms watchdog on the GUI thread that reports every stall of the event
loop wherever the blocking code lives, naming the last probed
operation. This is what located the actual freeze of the read/unread
path (a whole-model dataChanged() measured at 1072 ms) after several
plausible-from-code-reading fixes had failed to: the numbers say where
the time goes instead of a story explaining where it might go.
All probes are inert unless the environment variable is set.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The event means the subscription status changed, not that it was
toggled: guessing the new value by inverting the current flag goes
wrong as soon as one event is duplicated, lost, or refers to a state
the widget already holds. When the guess lands on 'unsubscribed', the
widget silently refuses to mark posts read or unread (the very first
check of markMsgAsReadUnread) until the next group reload puts the flag
right again. updateGroupData() was already called anyway; let it be the
only writer of the flag.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
setMsgReadStatus() only emits dataChanged(); it never resets the model
nor changes its layout, so the expanded items and the current index
survive it untouched. Saving and restoring them on every toggle was
pure overhead, and an expensive one: the restore walks every expanded
item and pays, for each, a linear scan of the post array plus a linear
scan of the view items -- quadratic work on the GUI thread for a single
post marked read or unread on a large forum.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
280e51f53 restricted the dataChanged() refresh to the affected post
unless more than 5 messages changed, in which case it refreshes the
whole view. But changed_msgs counts message ids, and a post carries one
id per stored version of itself, all sharing one read status: marking a
single post read or unread on a post edited 46 times queued 47 ids and
triggered the whole-model refresh. Measured on a 8259 post forum: one
click, one row repainted, 1072 ms of frozen GUI in the dataChanged()
handling; with this fix the same click costs about 1 ms.
Count the changed rows separately and size the refresh on that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The coalescing timer was allocated near the end of the constructor, but
setGroupId(forumId) is called earlier and synchronously reaches
groupIdChanged() -> updateDisplay(true) -> mDeferredReloadTimer->stop(),
dereferencing the still-uninitialised pointer and crashing the moment a forum
was opened. Allocate the timer right after setupUi(), before setGroupId().
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
handleEvent_main_thread() ran a full updateDisplay(true) (updateForum ->
getForumPostsHierarchy -> setPosts full reset -> proxy re-sort/re-filter ->
tree rebuild) for every incoming GXS event. A sync batch delivers many
NEW_MESSAGE events back to back, so the forum was reloaded once per post,
freezing the UI for seconds and resetting the model out from under the user's
current selection (and racing the async read-status persistence, making a
just-read post pop back to bold).
Route those events through a single-shot QTimer (300 ms) that is restarted on
each event, so a whole burst collapses into one reload once the events settle.
An explicit reload (forum switch) cancels any pending deferred one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
setPosts() called beginResetModel()/endResetModel() *before* swapping mPosts,
so endResetModel() fired while the model still exposed the previous hierarchy,
then emitted an extra beginInsertRows()/endInsertRows() on top of the reset.
That double-signalling left the view/proxy row->source mapping stale, so a
post that had just arrived could map to an invalid source index: selecting it
neither displayed its content nor cleared its unread (bold) state.
Bracket the actual data swap with beginResetModel()/endResetModel() and drop
the bogus row-insertion signal; a full reset already tells the views to
re-query everything. Same anti-pattern as the channels grid-empty-on-new-post
fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Routing every read through the batched markRead(grpId, vector, read) also
routed single interactive reads (selecting/opening one post) through its
waitToken(): the READ_STATUS_CHANGED event, and thus the forum-list unread
counter refresh, was only emitted once the DB write completed, adding
~100ms plus one GXS tick of latency versus the previous behaviour.
Send a single changed message through the per-message markRead(pair),
which emits the event immediately, and keep the batched call only for
larger sets (mark-all, with-children, versioned posts) where deferring the
single event until completion is what avoids the freeze.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GxsChannelPostsModel and PostedPostsModel spawned one detached std::thread (and
got one event) per post on "mark all as read/unread", freezing the UI on large
channels/boards - the same problem just fixed for forums, and worse here given
the much larger channel/board databases.
Both now collect the affected message ids and issue a single background batch
call (setMessageReadStatus / setPostReadStatus with a vector) plus a single view
refresh. PostedPostsModel additionally updates its local model up front (it
previously relied entirely on the per-message events).
Requires the matching libretroshare bulk overloads.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
recursSetMsgReadStatus() used to spawn one detached std::thread per changed
post (each blocking up to 5s in markRead) and emit one dataChanged() per post.
On a forum with thousands of unread posts this created thousands of threads
and signals driven from the GUI thread, freezing (and sometimes crashing) the
UI - the reported symptom being an hour-long hourglass on an 8000-post forum.
Now the recursion only collects the affected message ids and updates the
in-memory model; setMsgReadStatus() then issues a single background batch call
(rsGxsForums->markRead(group, ids, read)) and a single view refresh. The GUI
thread does O(n) in-memory work plus one thread and one signal, so it stays
responsive no matter how large the forum is.
Requires the matching libretroshare bulk markRead() overload.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Requested in review: the RS_GXS_PROFILE instrumentation of update_posts()
and setPosts() served to measure the freeze and verify the fix, but it
should not stay in the optimised code. The libretroshare profiling header
is no longer included.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
setPosts() runs in the Qt thread, through postToObject(). It copied the whole
post array a fourth time and then sorted it there, so for a channel with a few
thousand posts the interface was frozen for the duration of both.
Move the sort into the loader thread in update_posts(), where the array is
already sitting after the service call, and have setPosts() take its argument
by rvalue reference so the array is moved in rather than copied. The sort is
kept in setPosts() as a safety net: on an already ordered array it only runs
comparisons and moves nothing.
Combined with the libretroshare side making posts movable, the array is now
handed from the data store to the model without a single deep copy of a
thumbnail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Complements the GXS side instrumentation (libretroshare, gxs/rsgxsprofiler.h)
with the two phases that happen in the GUI: the service calls made from the
loader thread in update_posts(), and the model update in setPosts(), which
runs in the Qt thread and is what actually freezes the interface.
setPosts() reports its three phases separately (array copy, sort, view update)
since they have very different causes.
Enabled by the same RS_GXS_PROFILE environment variable, off by default.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The FeedReader message widget already offered "Mark all as read"
(toolbar button + context menu) but had no way to mark every visible
message as unread again. Add the symmetric "Mark all as unread" entry
to the message context menu.
Factor the shared iterate-over-visible-items logic out of
markAllAsReadMsg() into setAllMsgAsReadUnread(bool read); the read and
unread slots now just call it. Like the existing "mark all as read", the
action honours the current filter (acts on non-hidden rows only) and
goes through the existing per-message setMessageRead() path, so no
backend change is required.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The mirror workflow cloned with --mirror and pushed all refs, including
GitHub's refs/pull/* and upstream/pr/*, which GitLab rejects as hidden
refs ("deny updating a hidden ref" on upstream/pr/3213), failing the
whole push. Clone --bare and push only refs/heads/* and refs/tags/*
with --prune, keeping deletion sync while never touching PR refs.
Document portable archive creation (7-Zip / ZIP fallback), setup installer generation using NSIS, and how to execute and configure the deploy-windows.sh script.
Explain how RS_GUI_VERSION, the libRetroShare version and RS_LIB_VERSION_HASH
are each derived from git describe at configure time, the exact commands to
read them without building, the --dirty submodule-pointer gotcha, the --long
always-suffixed libretroshare format, and the non-git fallback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The super-project pins rapidjson at f54b0e47, whose GenericStringRef::operator=
assigns to the const member `length`. That fails to compile on modern
toolchains (Debian 13 / GCC 14: "assignment of read-only member 'length'").
Add a note after the submodule init step telling testers to check out the
upstream fix 9bd618f5 (PR #719 on Tencent/rapidjson master).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New 'Debug build (Linux)' subsection: CMAKE_BUILD_TYPE=Debug with -g3 -O0 -fno-omit-frame-pointer for full symbols and exact backtraces, a Qt6 note, a RelWithDebInfo caveat, the gdb invocation with the real binary path (Build-cmake/retroshare-gui/retroshare), and an AddressSanitizer variant for use-after-free / heap bugs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
find_package(QT NAMES Qt6 Qt5 ...) does not honour the NAMES order on
distros that ship Qt5 and Qt6 side by side in the same prefix (Debian/
Ubuntu multiarch /usr/lib/<arch>/cmake): CMake globs the sibling
Qt5*/Qt6* config dirs and stops at whichever <name>Config.cmake it
reaches first, which is filesystem-order dependent and routinely lands
on Qt5 even though Qt6 is listed first.
Probe Qt6 explicitly (find_package(Qt6 QUIET COMPONENTS Core)) and fall
back to Qt5. This makes the Qt6 preference deterministic and also makes
-DCMAKE_DISABLE_FIND_PACKAGE_Qt6=ON work as a reliable Qt5-force switch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RS_WEBUI=ON alone builds the WebUI machinery, but retroshare-service then
has no way to turn it on: enableWebUI defaults to false and only the -W
flag ever sets it, and -W/-B are gated behind
RS_SERVICE_TERMINAL_WEBUI_PASSWORD (a cmake_dependent_option requiring
RS_WEBUI=ON). Document the flag and add it to every build command block.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Windows hard-coded -j3; nproc is available in MSYS2, so use -j$(nproc) like
Linux. Added a note that MinGW C++ compilation is RAM-hungry and may need a
manually lower -j on low-memory machines (the reason for the old -j3 cap).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
zsh interactive (interactive_comments off by default) rejects # in pasted
command lines. Moved the explanations to prose; the bash blocks are now
paste-clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The blanket -I$(brew --prefix)/opt/*/include harvest pulled BOTH qt (Qt6)
and qt@5 headers into the same translation unit when both are installed,
causing Qt5/Qt6 header collisions ("Qt major version not 6 or 7",
qGetPtrHelper exception-spec mismatch). Skip every qt* keg in the harvest
loop on macOS; Qt is provided explicitly via Qt6_DIR / Qt5_DIR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per-platform (Linux/macOS/Windows) CMake build commands for the
super-project, documenting Qt5 vs Qt6 selection and the
-DCMAKE_DISABLE_FIND_PACKAGE_Qt6=ON Qt5-force switch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GUI reported RS_HUMAN_READABLE_VERSION (a libretroshare macro) as "the
RetroShare version". In the CMake build libretroshare exports RS_MAJOR_VERSION
as a PUBLIC define, so the GUI inherited the engine's version and displayed it
as its own: About showed the same number for both.
Give the GUI its own version, as discussed:
- Add retroshare-gui/src/rsguiversion.h defining RS_GUI_VERSION with a built-in
default, so every build path (including qmake/Android) compiles even without
version injection.
- Inject RS_GUI_VERSION from 'git describe' of the super-project in both CMake
(retroshare-gui/CMakeLists.txt) and qmake (retroshare.pri).
- RsApplication::retroshareVersion() now returns the GUI's own version instead
of libretroshare's.
- About (HelpDialog) shows the GUI version at the top; libretroshare's version
is listed among the other libraries, like any dependency. AboutWidget already
displayed both lines and is now correct automatically.
libretroshare needs no change: it already exposes its own version through
RsInit::libRetroShareVersion() in both build systems.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The RsAutoUpdatePage(4000) base class polled getGroupStatistics() every
4 seconds. That call issues blocking GXS token requests (up to 5s per
group) and had no anti-reentrancy guard, so background refreshes piled
up, saturated the GXS queue and froze the whole UI.
Revert to MainPage and drive updates on demand instead:
- a Refresh button
- showEvent(), so the page refreshes when navigated to
- settingsChanged(), kept so a date-format change re-renders
Date normalization (formatDateTime) is preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>