Commit e63584843 declared applicationFilePath inside the WINDOWS_SYS
block but uses it unconditionally to fill conf.main_executable_path,
so every non-Windows GUI build fails with 'applicationFilePath was
not declared in this scope'. Declare it at function scope, seeded
from argv[0] (the previous non-Windows behaviour); Windows still
overrides it with QCoreApplication::applicationFilePath().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The "Configure shared directories" dialog edits a local copy of the
shared directories and only commits it when Apply is pressed. Since
0ce2dd848 it reloads that copy from the core on *any* FRIEND_LIST event,
while the intent was only to follow friend-group changes. NODE_CONNECTED,
NODE_STATUS_CHANGED, NODE_STATE_STRING_CHANGED... fire every few seconds
as soon as a friend is online, so a directory added or removed in the
dialog reverts to the saved state before the user can press Apply.
Only react to GROUP_ADDED/REMOVED/CHANGED, and on those refresh the group
names column (dropping groups that no longer exist) instead of reloading
everything. Also do not reload when showYourself() is called while the
dialog is already open, for the same reason.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A distant chat started from the web UI pops the desktop chat window for
its tunnel (the core sends itself a "[Starting distant chat" message).
Leaving that conversation from the web UI then left the window as it was:
green LED, "You can talk", and on close the offer to end a conversation
the core no longer had -- closeEvent() ignored the false answer of
getDistantChatStatus() and read a default-initialised status.
On the new RsDistantChatEventCode::TUNNEL_STATUS_LOCALLY_CLOSED
(libretroshare fix/distant-chat-locally-closed-event) the window closes,
as it does when the conversation is ended from the window itself. And
closeEvent() asks no confirmation and calls no close for a tunnel the
core does not know any more.
The self-view preview in Preferences/VOIP scaled camera frames into the
display widget with Qt::IgnoreAspectRatio, forcing them into a 4:3 box.
Non-4:3 cameras (e.g. 16:9) were therefore stretched vertically.
Use Qt::KeepAspectRatio, matching the sibling showFrameOff() which
already scales the placeholder icon correctly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In the Activity feed, accepting an unknown peer (no PGP key in keyring)
adds it as an SSL-only friend via addSslOnlyFriend(), which sets
skip_pgp_signature_validation instead of the PGP-keyring accept_connection
flag. SecurityItem::updateItem() only tested details.accept_connection to
decide button visibility, so for an accepted SSL-only friend the condition
stayed false and the "Accept friend request" button kept being shown (and
"Remove friend" stayed hidden), even though the peer was already a friend.
Treat skip_pgp_signature_validation == true as accepted too, since that
flag is only set for a peer already present in the friend list.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The delegate took the Qt::FontRole of the model and assigned it to the style option as
is, replacing the font of the view instead of merging into it. A font stored in a model
usually only carries the one attribute the caller wanted to change — most of the time
bold — and expects family and size to keep coming from the view. Assigning it whole
dropped those items back to the application font, ignoring the font size configured in
the settings, which FontSizeHandler puts on the view.
That is easy to trigger: QTreeWidgetItem::font() returns a default constructed font when
the item has no Qt::FontRole yet, so the familiar "read the font, set it bold, write it
back" turned the whole list into the application font. On a remote X display where the
desktop font is 9pt and the RetroShare font size is 11pt, the list visibly shrank.
Merge with QFont::resolve() instead, which keeps only the attributes the model really
set and takes the rest from the view — the same merge QStyledItemDelegate::initStyleOption()
performs. Same change in the tooltip path of editorEvent(), which recomputes the elision
with that font. sizeHint() was already using the merged font from initStyleOption(), so
rows were being measured with one font and painted with another.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 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>