Commit Graph

514 Commits

Author SHA1 Message Date
jolavillette
00459bbbcb webui v150: Leave Chat reported a closed tunnel whatever the core answered
rsJsonApiRequest hands the callback the HTTP status, not the answer, and the
button read that as "the tunnel is closed": any reply at all, including one that
says nothing was closed, ended the conversation on screen. The core does report
it, in retval, so that is what is read now, and the ended card says which of the
two happened -- closed on request, or already gone before the click.

The status poll had the same deafness and is what let it happen. Once a tunnel
is gone from the core -- died of inaction, closed by the peer --
getDistantChatStatus answers false, and the poll only ever looked at the branch
where it answers true. So the last known status stayed on screen for good: a
conversation whose tunnel had been dead for a while kept its green dot and its
"You can talk", and Leave Chat then had, quite correctly, nothing to close. Two
consecutive false answers now end the conversation, with a line saying so, which
is the state the user was in.

Needs the libretroshare side to be meaningful:
fix/distant-chat-close-keeps-contact makes closeDistantChatConnexion report
whether it closed anything, and drop the distant chat contact it used to leave
behind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 09:26:52 +02:00
jolavillette
6f2b72c3f1 webui v149: selecting a contact in People opened a distant chat tunnel
The right pane keeps its last tab in State.activeTab, and that state is global
rather than per contact. So the list item handler ends on "if the chat tab is
the one showing, connect" -- which is never true on a fresh page, and always
true afterwards. Once a conversation has been opened, every later click in the
contact list requests a GXS tunnel toward whoever was just selected: a network
action the other side sees, from a click that only meant "show me this profile".

Selecting somebody now shows their profile, and the tunnel waits for the Chat
Conversation tab. The three places that do mean it -- the tab itself, the Start
Chat button, the Chats list and its context menu -- are untouched.

The mirror case was broken the other way round. "Start private chat" from a chat
room calls setSelectedId(id, 'chat'), which preselects the chat tab on a page
that is not mounted yet; nothing ever opened the tunnel there, so the pane sat
on its Connecting spinner for good. That intent is explicit, so it is now
remembered and honoured once the own identities are loaded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 09:03:23 +02:00
jolavillette
ee3836cfab webui v148: the Chats badge counted a sweep of every identity the node knows
Clicking Chats sets the counter climbing for minutes. It is not counting
conversations, it is watching a probe walk the whole identity list: the tab
re-runs preloadAllChatHistory(), which asks /rsHistory/getMessages once per
known identity, and the badge is recomputed from the answers as they land. On a
node that has seen twenty thousand identities that is twenty thousand requests,
four at a time, against a JSON API that answers one at a time and closes the
connection after each -- and it starts over at every click, on top of the three
calls the layout and the sidebar already make when the page opens.

Neither half of the probe can find anything.

Distant chat history is not stored under the peer's GXS id. The core keys it by
the tunnel id, sha1 of the two GXS ids sorted and truncated to sixteen bytes
(p3GxsTunnelService::makeGxsTunnelId), so the query only ever answers an empty
list and the previews appear solely for chats opened in this session. There is
no API to derive that id and no crypto.subtle outside a secure context, the web
UI being served over plain HTTP, so people_util now computes the digest and the
probe asks for the tunnels of our own identities against that peer.

Private chat history is keyed by a location, not by a person: the query passed
the PGP id where an RsPeerId is expected. It is half the length, the core cannot
parse it, so it builds a null id -- which is the key of the public broadcast
history -- and prints a stack trace for every request. 1714 of them in one log
here, over 979 distinct PGP keys. It now asks per location of the friend behind
the identity, which is where the messages actually are.

So the probe covers the peers a conversation can exist with -- contacts, peers
already in the map, identities belonging to our friends -- instead of every
identity ever seen, and it will not run twice within thirty seconds however many
times the tab is clicked. The badge and the list read chatHistoryMap directly:
it only ever holds peers with a real message, so the sweep of the identity list
on every redraw bought nothing, and identity details are fetched for the handful
of peers actually listed. The History dialog was asking the same two impossible
questions and now asks the right ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 20:51:51 +02:00
jolavillette
cd1875547f webui v147: the People distant chat displayed nothing of what the peer answered
Select a contact, Profile details, Start chat: the tunnel goes green, what you
type leaves and is echoed, and the answers never appear. They do arrive -- the
core posts an RsChatMessageEvent for every incoming message and the handler runs
-- they are just written into a list nobody displays.

Giving each peer its own session pointed State.chatMessages, the array the view
renders, at session.messages. loadChatMessages() then kept doing
State.chatMessages = data.msgs: that reassignment does not update the session,
it detaches from it. From that answer on there are two arrays -- the visible one
holding the history, your echo and the status notices, and the session one where
the event handler pushes everything the peer says. The `else if` fallback of the
handler cannot save it either, since the session is always found first. It fires
every time: the JSON API answers msgs: [] rather than nothing, so an empty
history detaches just as surely as a full one.

The session array is now the only one: it is mutated in place and
State.chatMessages is never reassigned, only re-pointed at it. Messages carry a
key -- direction, send time and text -- shared by the three sources, so the same
message coming from the history and from the live event is stored once, and the
status notices no longer need their manual text scan.

Two windows were losing messages besides. The core emits its "starting distant
chat" notice from inside initiateDistantChatConnexion, before the caller knows
the pid, and rswebui buffers every chat event it cannot deliver; the tab also
unregisters its handler when the user walks away. Both are covered by draining
that buffer -- when the chat opens, when the tab mounts, and when the tunnel
turns secure -- and by adopting the visible conversation for a message that
names the pid the view is already showing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 20:38:03 +02:00
jolavillette
9428fc5542 webui v146: the attach dialog must be escapable while it hashes
Once hashing starts there is no way out: Cancel is disabled, the backdrop click
is guarded by the same flag, and the overlay covers the whole viewport, nav
included. The only exits are the browser's Back button and a reload -- and Back
does not even stop the poll, since the setTimeout chain runs off module state
that nothing resets, so it keeps asking once a second for the rest of the
session and the dialog is waiting again on the way back.

That would be harmless if the wait always ended, but the core drops a file it
failed to hash without a word: ftExtraList only records successes, so the file
never enters mHashedList and ExtraFileStatus() answers "not ready" for ever.
hashExtraFile() has already refused missing paths and directories by then, so
this is the file that exists but cannot be read -- permissions, a share
unmounted, an I/O error.

The red button therefore stays enabled and reads Stop while hashing, the
backdrop closes again, and leaving the view stops the poll: it writes its file
link into that view's textarea, so once the view is gone the answer has nowhere
to land anyway. The poll also backs off from one second to ten, since a large
file legitimately takes minutes and each request opens a new connection.
2026-08-16 17:44:51 +02:00
defnax
cdd29ab86f Fixing for mobile phone 2026-08-16 14:16:40 +02:00
defnax
d659977275 Added avatars before identity nicknames in the chat-room join selection. 2026-08-16 13:50:58 +02:00
defnax
ea1dbbd450
Merge branch 'improvements_v2' into fix/modal-dismiss-details-for-121 2026-08-16 13:39:38 +02:00
defnax
e203558443 Improved the “Add new file” popup for phones. 2026-08-16 13:36:15 +02:00
defnax
74aff9da5e
Merge branch 'improvements_v2' into perf/phone-request-storms-for-121 2026-08-16 13:32:19 +02:00
defnax
84b11b6320
Merge pull request #17 from jolavillette/fix/pending-friends-ghost-for-121
Remembered friends must expire, and go when the friend goes
2026-08-16 13:21:18 +02:00
defnax
62079e1fd7 Improved the ShareManager mobile layout:
Uses nearly the full phone viewport with safe spacing.
Keeps the folder list independently scrollable.
Keeps Add New and Edit buttons visible at the bottom.
Converts folder rows into labeled mobile cards.
Replaces the stray 0 with “No shared folders yet.”
Ensures nested dialogs appear above ShareManager.
2026-08-16 13:17:18 +02:00
jolavillette
b3bb96e55c webui v145: the web help dialog stays open, and the close button drifts
The Ok button of the web help confirmation opens the documentation in another
tab and left the dialog standing behind it, with nothing left to ask. It closes
now, like the one in the copy confirmation.

Every modal that gets a phone layout drops its padding from 1.5rem to 1rem, but
.close-btn is positioned against the box rather than the content, so it stayed
1.5rem from the edge and lined up with nothing. Corrected once in the modal
layout instead of in each of the five pages that override the padding.
2026-08-16 13:09:18 +02:00
jolavillette
2748b44fb2 webui v144: remembered friends must expire, and go when the friend goes
refreshGpgDetails() re-injects every remembered friend the core does not return,
and nothing ever removed one: a short-invite peer that never validates, or one
the user deletes, came back on the next refresh and survived a browser restart,
with no way to get rid of it from the web UI.

A remembered friend is a placeholder for the seconds the core needs to catch up
with an add that has just returned, so it now carries the time it was made and
is dropped once that window has passed. Removing a friend drops it immediately,
before the refresh that would have restored it -- and that refresh now waits for
removeFriend to answer instead of racing it.

The store is also keyed per node: it lives in localStorage, which is shared by
every RetroShare profile reached from the same browser, and their friend lists
have nothing to do with each other.
2026-08-16 13:08:25 +02:00
jolavillette
6b988dffa8 webui v143: the channel list never refreshed, and three different periods
The scope predicate of the channel list background task was entirely commented
out, so it returned undefined: setBackgroundTask stopped after the first
interval and the list was loaded once, never refreshed while the page stayed
open. It now tests the route like the boards one does.

The three group lists polled at three different periods: forums every 5 s,
boards every 30 s, channels never. Each poll fetches the whole summaries list,
and on a phone it costs a fresh TCP handshake on a server that answers one
request at a time. All three now share the boards period, named per page.
2026-08-16 13:06:50 +02:00
jolavillette
543a65f210 webui v142: stop three request storms that only hurt on a phone
Every JSON API answer carries `Connection: close` and the server runs its
service on a single thread, so each request costs a full TCP handshake and they
are answered one at a time. Over loopback a round trip is 0.1 ms and none of
this shows; over WiFi or 4G it is 20 to 80 ms and the three patterns below turn
into seconds of blank screen.

Chat room list: one getChatLobbyInfo per subscribed room, and nothing was
painted until the last answer arrived -- then only did the public room list get
asked for. Paint each room as it lands and start the public list immediately.

Forum post bodies: loadPostContent() is called from the view, and the body stays
null for the whole round trip, so every redraw fired the same getForumContent
again -- and each of the other posts' answers causes a redraw. An open thread
multiplied one request per post into one per post per redraw. Guarded by an
in-flight set, kept on failure so an unavailable post is asked for once per
visit rather than forever.

Chat history preload: two getMessages per known identity, hundreds of them at
once, filling the browser's six sockets and the single service thread while the
user waits for something else. Run four at a time. The duplicated response
handling of the two branches is now one function.
2026-08-16 13:05:57 +02:00
defnax
3190b4cb58 Fixes for phones
- The Graph shortcut is hidden from the default friend list.
- After selecting a friend, Network Graph remains available in the detail tabs.
- Desktop behavior is unchanged.
- File and picture attachments now share one paperclip button.
- The message field gains more horizontal space on phones.
2026-08-16 12:06:20 +02:00
defnax
d9a0428a26 Fixed mail composer popup heigth 2026-08-16 11:36:37 +02:00
jolavillette
2c38f04fa1 webui v141: stop the endless getIdDetails retry, and one copy path
fetchIdDetails() restored `undefined` after its five attempts, which is the
very value that makes it fire a request -- and two of its callers sit inside
a view (people_sidebar's filter and map). Every redraw therefore restarted the
whole six request chain, forever, for any identity the core never resolves.
Mark the give-up state with `null` instead, like the loading state: attempted,
not to be asked again.

The copy icon still ran the old inline execCommand path, so copyId() was only
reachable through shareId()'s fallback and its clipboard call could reject with
no feedback at all. The icon now calls copyId(), the Clipboard API failure falls
back to execCommand, and the popup reports a refused copy instead of claiming
success. Same role/tabindex/keyboard treatment as the share icon next to it.
2026-08-16 09:45:02 +02:00
defnax
4b523cb8ec Fixed issue on phone to view parcipants list 2026-08-15 22:59:21 +02:00
defnax
71688feac2 Fixes Ui issues
- Correct full-certificate friends being incorrectly marked “Pending validation.”
- Refresh identity lists immediately after creating an identity.
Update open chat-room and distant-chat identity selectors automatically.
- Replace the broken signed-identity password popup with a responsive mobile form.
- Allow creation of the first signed identity without requiring an existing signed identity.
Filter temporary all-zero GXS IDs.
- Wait for RetroShare to expose the real generated identity ID.
- Retry identity-detail loading when RetroShare temporarily returns empty data.
- Display the fetched nickname instead of “Unknown.”
2026-08-15 22:43:19 +02:00
defnax
1c615bdb89 Fixing adding friends via Short ID 2026-08-15 17:33:08 +02:00
defnax
fceefc95bb fixed popup window for phone 2026-08-15 16:37:58 +02:00
jolavillette
241380ff2e webui v140: adapt the inside of the pages to a phone
Loaded last from main.scss, so it wins over the page stylesheets without
!important. Five sections, all scoped to the phone: multi column grids collapse,
wide tables become cards, the statusbar scrolls instead of truncating, form
fields stop triggering the iOS zoom, and anything only reachable on :hover gets
a touch fallback.

Not here any more, because improvements_v2 now does it itself: the navigation
(bottom tab bar and status sheet in main.js) and the master-detail switch of the
network, people and chat pages (.mobile-detail-open, driven from their own
state). This layer starts where those stop.

The two JS changes are the .mails-head and .comments-head markers those tables
need: mithril builds the DOM through the DOM API, so it never gets the implicit
<tbody> the HTML parser would insert, and neither tr:first-child nor a
`> tbody >` path can find the header row.
2026-08-15 15:44:43 +02:00
jolavillette
0ecb4649d2 webui: declare the phone breakpoints in a single place
The values are not new: they document the ones already hardcoded across the
stylesheets. The mobile mixin covers portrait phones and landscape ones, which
are wide enough to escape a width-only breakpoint but far too short for the
layouts the page stylesheets switch to.
2026-08-15 14:50:29 +02:00
defnax
209adb6564 Fixed Network active chats
- Fixed. Chat previews
- Implemented unread chat counts
2026-08-15 12:27:39 +02:00
defnax
c7011ff280 Improved chat, network & people pages for mobile phones 2026-08-15 11:50:17 +02:00
defnax
01ead3a0ce Fixed issue with mail composer button & mail composer height 2026-08-14 19:42:23 +02:00
defnax
46876eac9a improvements for mobile phone 2026-08-14 18:48:52 +02:00
jolavillette
2182afe5aa webui v139: bump the label for the network graph pass 2026-08-14 13:39:01 +02:00
jolavillette
007dc5cf8c one cleanRetroshareId, in rswebui
The Add friend wizard and the location details dialog each carried their own
copy, character for character the same except that one trimmed after decoding
and the other did not. The shared version keeps the trim: a RetroShare ID pasted
from a link often arrives with a trailing newline, and the dialog was displaying
it.
2026-08-14 13:38:52 +02:00
jolavillette
4831b6f3e6 network graph: keep the graph across visits, and let the newest load win
The tab only mounts the component while it is the active tab, so leaving the
graph and coming back rebuilt it from scratch: the discovery requests again, and
a layout that costs up to 729 ms. The graph state now lives at module level and
oninit only reloads when there is nothing to show or the last load is over a
minute old. The zoom, the search and the friendship level survive the trip too.

Changing the friendship level starts a load without waiting for the one already
running -- the Redraw button is disabled while loading, the select is not -- and
the two do not necessarily finish in order, so the slower one could overwrite
the fresher result. Each load now takes a token and drops its results if a newer
one has started since.
2026-08-14 13:38:04 +02:00
jolavillette
82a8c97ca1 network graph: discover friends six at a time
loadGraph() fired one /rsGossipDiscovery/getDiscPgpFriends per direct friend at
once, then one per second level peer at once -- up to two hundred parallel
requests from a single tab.

A browser opens about six connections per host. Asking for two hundred does not
make them arrive sooner: they queue in the tab, and past a certain point they
start failing outright. That is the request storm which made the channel list
unusable before it was batched.

Both fan-outs now go through discoverInBatches(), six at a time.
2026-08-14 13:36:48 +02:00
jolavillette
1a9442fae2 network graph: relayout when the slider is released, not on every pixel
layoutGraph() runs 140 iterations of an O(n^2) force loop. Measured by calling
it standalone with the same constants:

     20 nodes     24 ms
     50 nodes     57 ms
    100 nodes    199 ms
    200 nodes    729 ms   (NODE_LIMIT)

The edge length slider called it from oninput, which a range input fires dozens
of times per drag, each call blocking the main thread. Dragging it on a level 2
graph froze the tab for several seconds.

oninput now only moves the label, which is what has to stay live, and onchange
does the layout once, on release.
2026-08-14 13:36:15 +02:00
defnax
307981e960 Added network graph page 2026-08-14 11:46:11 +02:00
defnax
a0920d297d Added comments badge on the channel Posts thumbnail 2026-08-14 11:21:25 +02:00
defnax
cddee00b69 Added to show avatars for comments 2026-08-14 11:12:20 +02:00
defnax
7e1358e988 removed not needed label 2026-08-14 10:59:42 +02:00
defnax
622b48bbf5 Added option to change your Status in your own status dot 2026-08-14 10:54:07 +02:00
defnax
46a9eb38b2 Improved the Add Friend wizard to focus on textual RetroShare IDs.
Changes include:
- RetroShare ID paste field is now the primary action.
- Supports raw IDs and rsInvite= URL-formatted IDs.
- Empty submissions are disabled.
- Clearer invalid-ID messaging.
- File import and drag/drop remain available as secondary options.
- More polished, responsive wizard styling.
- Accepts text files even when the browser omits their MIME type.
2026-08-14 10:27:23 +02:00
jolavillette
c3f8115a19 forums: the emoji picker was drawing white emojis on a white panel
Clicking the emoji button opened the panel, and clicking inside it did insert an
emoji, but the panel looked empty: the emojis were being drawn, in white, over
white.

The global `button` rule applies the button() mixin, which sets `color: white`
to sit on a coloured background. The picker resets the parts of it that clash --
border, background, box-shadow -- but not the colour, and its own background is
#fff. The category row above the grid was invisible for the same reason.

Found by testing in a browser: nothing in the sources says a button is white
until you follow the mixin.
2026-08-13 14:28:59 +02:00
jolavillette
2ed361626f login: honour the ?Url= parameter again
urlParams.get('Url') || window.location.protocol === 'file:'
      ? 'http://127.0.0.1:9092'
      : <origin>

=== binds tighter than ||, so the test reads

    (Url || protocol === 'file:') ? default : origin

and passing ?Url= made the condition true, which selected the hardcoded default
and discarded the value given -- the one case the parameter exists for. Username
and Password, read the same way just above, worked; only Url did not.

Parenthesised so the parameter wins when present, and the file:// and served
cases keep the behaviour they had.
2026-08-13 13:51:46 +02:00
jolavillette
659f612e5d webui v138: bump the label for the audit pass 2026-08-13 13:50:09 +02:00
jolavillette
5efd9a24ca a failed request resolves to an empty answer, not to undefined
rsJsonApiRequest resolves undefined when a request fails. Around forty call
sites go straight for res.body.retval, so every failure threw a TypeError --
inside an onclick most of the time, where nothing catches it: the button does
nothing at all, and the console shows a stack about `body` rather than a failed
request.

It now resolves the same shape as a real answer, with an empty body. Every
defensive check in the code base tests res.body or res.body.retval, so an empty
body still reads as a failure to all of them, including the batch loaders that
key their split-retry off it.
2026-08-13 13:50:09 +02:00
jolavillette
db36a05a74 network: cache the location details, and stop waiting on a catch that never fires
The version and the short invite of a node do not change while the web UI is
open, but the dialog asked the core again on every open, showing "Loading..."
each time. They are now cached by node id.

The two .catch() handlers could not do what they were written for:
rsJsonApiRequest never rejects, it resolves undefined when a request fails. They
only ever ran by accident, when reading .body of that undefined threw a
TypeError inside the .then. The failure is now read off the resolved value,
where it actually is.
2026-08-13 13:50:09 +02:00
jolavillette
38684d7f0c eslint: drop the initialisers that are always overwritten
The last three no-useless-assignment errors. Each declares a value that every
path reassigns before reading it: the exhaustive if/else of customState, the
network/chats branch of displayFriends, and the quality loop of result, which
runs at least once and throws below it.

The web UI now lints clean: 0 errors, 0 warnings, where the branch point had 17.
2026-08-13 13:50:09 +02:00
jolavillette
51720e3024 remove the components left dead by the chat hub and comment rewrites
eslint reported them and nothing references them: they are the previous
generation of UI, superseded in place.

channels/channel_view.js
  displaycomment() and the AddComment form it opened, replaced by renderComment()
  and ChannelComments. displaycomment only referenced itself, recursively, which
  is why it looked used.

chat/chat.js
  LayoutSingle, the single chat room layout that predates the hub, and with it
  LobbyList, Lobby, SubscribedLobbies and PublicLobbies, which nothing else
  called. Six names imported from chat_state were unused as well.

Kept as its own commit so it can be reverted alone if any of it turns out to be
wanted again.
2026-08-13 13:50:09 +02:00
jolavillette
2ea0422039 channels: stop redownloading a channel on every visit
Since the list was reduced to metadata, ChannelView.oninit fetches the content
of the channel being opened. oninit runs again on every visit though, with no
guard, so stepping in and out of a 2000 item channel redownloaded all of it,
images included, each time.

The content is now pulled only when it is missing from memory or older than a
minute, so posts published meanwhile still appear without forcing the user to
reload the page. The call sites that publish or delete call updatedisplaychannels
directly and keep refreshing unconditionally.

The timestamps live at module level: the component is rebuilt at every visit, a
field of it would forget immediately.
2026-08-13 13:50:09 +02:00
jolavillette
b62095aaf3 forums: measure the composer limit in bytes, and build the body once
Three defects in the new thread composer.

The 199 000 limit of a GXS message is a *byte* count, but the composer compares
it against String.length, which counts UTF-16 units: an accented letter is two
bytes for one unit, an emoji four bytes for two. With an emoji picker one click
away in that very toolbar, the counter can report room left on a message the
core will refuse. It now measures UTF-8 through TextEncoder, and says bytes
rather than characters.

postBody() was called five times per render -- twice for the class, twice for
the counter text, once for the disabled state -- and it re-escapes the message
and re-joins every inline image, each of which is up to 175 KB of base64. That
ran on every global redraw, which the statusbar triggers continuously, while the
user is typing. It is now built once per pass.

pollFileHash() re-arms itself every 500 ms for up to a minute. Closing the
composer left it running: it kept polling and redrawing a component that is no
longer mounted. onremove now stops it.
2026-08-13 13:50:09 +02:00
defnax
a070df6ff2 Improved the Forum thread/reply composer: 2026-08-13 13:27:36 +02:00