Commit Graph

12527 Commits

Author SHA1 Message Date
Claude 7845072f20 fix(nests): evict author from prior room when presence moves to a new room
kind-10312 is replaceable per author, but the room a presence points to
(`a`-tag) can change when a speaker hops between rooms. The replaceable
cache only swaps the addressable's content -- it doesn't know which channel
the old version was attached to, so the prior room kept the stale entry in
both `notes` and the new `presenceNotes` index. NestsFeedFilter would then
falsely surface the prior room as "live" via that author until the entry
dropped out of the freshness window.

Capture the prior room from the existing addressable before consumeBaseReplaceable
swaps it. When the new event is a true replacement (createdAt > prior) and
the room differs, drop the author from the prior channel's presenceNotes
and remove the prior version note from its main notes index.
2026-04-30 14:18:37 +00:00
Claude d87fc36d21 perf(nests): index room presence separately from chat for O(speakers) scans
LiveActivitiesChannel.notes is mixed-kind (chat, zaps, raids, clips, presence),
and chat dominates by volume in active rooms. The Nests feed filter scanned it
twice per room per recompute -- once for any-fresh-presence, once for fresh
on-stage presence -- doing an `is MeetingRoomPresenceEvent` cast on every chat
message just to find the speakers.

Add a presenceNotes index on LiveActivitiesChannel keyed by author pubkey
(presence is replaceable per author, so the key auto-collapses heartbeats).
Populate it from LocalCache.consume(MeetingRoomPresenceEvent). Switch
NestsFeedFilter to a single hasFreshSpeakers() pass over the index, dropping
the now-redundant isLiveByPresence() check (hasFreshSpeakers implies it).
Migrate NestsFeedLoaded's latest-presence flow to the same index.
2026-04-30 14:18:37 +00:00
Claude 7c61eaf4e3 feat(nests): hide rooms with no fresh speakers from drawer feed
Adds hasFreshSpeakers gate to NestsFeedFilter so OPEN/PRIVATE rooms
whose live speaker slate is empty are dropped — a room with no fresh
kind-10312 presence carrying onstage=1 has effectively ended even if
its kind-30312 status still says live.
2026-04-30 14:18:37 +00:00
Vitor Pamplona 4f45de9b86 Merge pull request #2654 from vitorpamplona/claude/add-tag-filters-YR1wO
feat(home): user-configurable home tabs (new threads, conversations, everything)
2026-04-30 08:51:02 -04:00
Vitor Pamplona bc1a5d98d6 Merge pull request #2655 from vitorpamplona/claude/mute-unmute-icons-Lcxuc
feat(nests): use mic icons for talk / stop talking
2026-04-30 08:49:39 -04:00
Vitor Pamplona 89aca22897 Merge pull request #2657 from davotoula/sonar-unused-lambda-param
Replace unused lambda parameters with `_`
2026-04-30 08:47:38 -04:00
Vitor Pamplona 0ad45dadf6 Merge pull request #2658 from davotoula/fix/identity-claim-tag-parse
fix(nip39): reject identity claim tags without a platform separator
2026-04-30 08:47:24 -04:00
Vitor Pamplona ea5d020b05 Merge pull request #2659 from davotoula/fix/strictmode-cleartext-localhost
fix(android): silence StrictMode cleartext violations for 127.0.0.1 (Tor SOCKS)
2026-04-30 08:47:06 -04:00
David Kaspar 13b0332a13 Merge pull request #2650 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-30 14:40:11 +02:00
davotoula 6b709981de fix(android): silence StrictMode cleartext violations for Tor SOCKS on 127.0.0.1
Add a network_security_config.xml that keeps cleartext globally permitted
(so user-configured ws:// relays still work) but adds an explicit
domain-config for 127.0.0.1 and localhost. This stops StrictMode's
detectCleartextNetwork from flooding logcat with CleartextNetworkViolation
stacks each time the app talks to the local Tor SOCKS proxy
(220+ per benchmark session previously).

Verified on a playBenchmark build: zero CleartextNetworkViolation lines
to 127.0.0.1 even though the app continued attempting Tor connections
on port 9050.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 11:48:33 +02:00
davotoula 0c2614fbf5 fix(nip39): reject identity claim tags without a platform separator
Malformed `i` tags whose platform-identity field has no `:` (observed in
production logcat as e.g. `["i", " ", " "]`) made `create()` throw
`IndexOutOfBoundsException` from destructuring `split(':')`. The exception
was caught but each event spammed multiple stack traces. Validate the
separator up front in `parse()` and return null silently. Also fix the
diagnostic `joinToString { "," }` typo so the log shows real tag contents.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 11:21:23 +02:00
davotoula 398652ef47 replace more unused lambda parameters with _ 2026-04-30 10:29:27 +02:00
Crowdin Bot 00386c242f New Crowdin translations by GitHub Action 2026-04-30 07:58:07 +00:00
davotoula 212fdf739c updated translations cz,se,de,pt 2026-04-30 09:54:46 +02:00
Claude bfd3ddaca6 feat(home): user-configurable home tabs (new threads, conversations, everything)
Adds a Home Tabs settings screen letting users pick which tabs appear on Home:
New Threads, Conversations, and a new combined Everything tab. The tab row is
hidden whenever only one tab is active, so users can keep a single feed view.
At least one tab always remains active.
2026-04-30 03:29:41 +00:00
Claude 6da9af2a81 feat(nests): use mic icons for talk / stop talking
The text Talk / Stop Talking buttons made it easy to misread the
broadcast state. Swap them for large filled mic-icon buttons so the
mic state is unmistakable: MicOff in primary color when idle (tap to
go live), Mic in error color when broadcasting (mic is open, tap to
stop).
2026-04-30 03:28:53 +00:00
Vitor Pamplona 9bc1e3d3ce Merge pull request #2652 from vitorpamplona/claude/add-audience-to-stage-HhJgJ
Add tap gesture support for audience member promotion
2026-04-29 19:53:20 -04:00
Claude 208d9a8f7f feat(nests): tap an audience avatar to open the participant sheet
The host's primary path to add an audience member to the stage was the
long-press → "Promote to Speaker" row, which is hard to discover and was
also being swallowed by ClickableUserPicture (which ignores onLongClick
when onClick is null). Wire a tap handler on audience cells so a single
tap opens the same per-participant sheet, surfacing Promote to Speaker
for hosts and View Profile / Follow / Mute for everyone else.
2026-04-29 22:43:23 +00:00
Vitor Pamplona d44baa7e8f Merge pull request #2651 from vitorpamplona/claude/fix-windows-test-failure-dY1Jv
Fix race condition in MoQ subscription registration
2026-04-29 18:35:56 -04:00
Vitor Pamplona c8327e829f Merge pull request #2647 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-29 18:26:59 -04:00
Vitor Pamplona 31a2705173 Merge pull request #2649 from vitorpamplona/claude/fix-hand-raise-sync-Yf4b4
Fix stage presence sync and relay propagation in meeting rooms
2026-04-29 18:26:21 -04:00
Vitor Pamplona 997f52a31a Merge branch 'claude/fix-hand-raise-sync-Yf4b4' of https://github.com/vitorpamplona/amethyst into claude/fix-hand-raise-sync-Yf4b4 2026-04-29 18:19:22 -04:00
Claude 19b534a9e2 fix(nestsClient): register inbound moq-lite subscription before sending Ok
The publisher's inbound-bidi handler wrote SubscribeOk to the bidi
before calling registerInboundSubscription. The peer's first
publisher.send() after observing Ok could race the registration on
dispatchers that resume the peer's continuation before the handler's
(notably Windows under Dispatchers.Default), causing send to observe
an empty inboundSubs and return false. Reordering makes the peer's
view of Ok a happens-after of the registration.

Fixes the Windows-only failure in
MoqLiteSessionTest.publisher_acks_subscribe_and_pushes_group_data_on_uni_stream.
2026-04-29 22:14:23 +00:00
Claude 8d228db440 fix(nests): preserve relays tag on participant updates + sync action bar with stage grid
Two related bugs the user hit while testing against nostrnests:

1. Approving a hand-raise made the Hands tab disappear (the host's
   local app saw the role grant) but the audience member did not
   appear on stage anywhere else.

   Cause: RoomParticipantActions.rebuild() rebuilt the kind-30312
   without the original room's `relays` tag. The recently-added
   broadcast fan-out path (Account.computeRelayListToBroadcast) keys
   off `event.allRelayUrls()` for kind 30312, so a republished room
   event with no relays tag only reaches the host's outbox — not
   nostrnests's fixed five reads or the audience member subscribing
   on those reads. The audience member never sees their SPEAKER tag
   and stays absent from the participant list.

   Fix: copy `original.relays()` into the rebuild so every republish
   (approve, demote, kick → re-broadcast) keeps the relay routing
   that lets the room reach its full audience.

2. Tapping "Leave Stage" as the host emptied the StageGrid
   ("Waiting for speakers…") but the action bar still showed the
   Talk + Leave Stage cluster.

   Cause: two different definitions of "on stage" were in play.
   StageGrid uses ParticipantGrid (role + presence.onstage flag),
   so flipping onStageNow=false drops the host out. The action bar
   gated on the role-only `onStage: List<ParticipantTag>` (the
   host's HOST tag never goes away), so the cluster stayed.

   Fix: derive isOnStageMe from participantGrid.onStage so both
   surfaces share the same "stepped off" semantics. The host who
   taps Leave Stage now drops to the audience-style mute toggle
   and can rejoin (kind-10312 onstage flips back to 1) without
   the controls lying about their state.

https://claude.ai/code/session_016G7oP5BotPjUBgvMYrrrxh
2026-04-29 22:13:36 +00:00
Vitor Pamplona 9628d17f5f Improves the relay list used for linked Notes to include their relay urls 2026-04-29 21:48:32 +00:00
Claude b0e7c62bb5 fix(nests): broadcast presence/chat to the linked room's full relay list
Hand-raise toggles in Amethyst weren't appearing in nostrnests's UI.

nostrnests's NestsUI v2 routes reads against a fixed five-relay list
(NestsUI-v2/src/lib/const.ts: relay.snort.social, nos.lol, relay.damus.io,
relay.ditto.pub, relay.primal.net) — no outbox model. Its presence query
is just `kinds:[10312], "#a":[roomATag]` over those five relays.

Commit 637174ef already taught computeRelayListToBroadcast to fan a kind
30312 *room* event out to its `relays` tag. But kind 10312 presence (and
chat / reactions, etc.) is a different event type that *links* to the
room via an `a` tag. For those, broadcast was only reaching:

  - the broadcaster's outbox relays
  - the single firstOrNull() hint baked into the `a` tag
  - the relay we happened to receive the room event from

If none of those overlap with nostrnests's fixed five reads, the
hand-raise update is silently dropped.

Extend the `linkedAddressIds` block in computeRelayListToBroadcast: when
the linked address resolves to a MeetingSpaceEvent / MeetingRoomEvent /
LiveActivitiesEvent, also add that linked event's allRelayUrls(). This
covers presence updates and any other room-scoped event that points at
a Nest via `#a`.

https://claude.ai/code/session_016G7oP5BotPjUBgvMYrrrxh
2026-04-29 21:48:32 +00:00
Vitor Pamplona 9b583d99ff Improves the relay list used for linked Notes to include their relay urls 2026-04-29 17:44:11 -04:00
Claude ecc1573606 fix(nests): broadcast presence/chat to the linked room's full relay list
Hand-raise toggles in Amethyst weren't appearing in nostrnests's UI.

nostrnests's NestsUI v2 routes reads against a fixed five-relay list
(NestsUI-v2/src/lib/const.ts: relay.snort.social, nos.lol, relay.damus.io,
relay.ditto.pub, relay.primal.net) — no outbox model. Its presence query
is just `kinds:[10312], "#a":[roomATag]` over those five relays.

Commit 637174ef already taught computeRelayListToBroadcast to fan a kind
30312 *room* event out to its `relays` tag. But kind 10312 presence (and
chat / reactions, etc.) is a different event type that *links* to the
room via an `a` tag. For those, broadcast was only reaching:

  - the broadcaster's outbox relays
  - the single firstOrNull() hint baked into the `a` tag
  - the relay we happened to receive the room event from

If none of those overlap with nostrnests's fixed five reads, the
hand-raise update is silently dropped.

Extend the `linkedAddressIds` block in computeRelayListToBroadcast: when
the linked address resolves to a MeetingSpaceEvent / MeetingRoomEvent /
LiveActivitiesEvent, also add that linked event's allRelayUrls(). This
covers presence updates and any other room-scoped event that points at
a Nest via `#a`.

https://claude.ai/code/session_016G7oP5BotPjUBgvMYrrrxh
2026-04-29 21:36:23 +00:00
Crowdin Bot 8dec96710f New Crowdin translations by GitHub Action 2026-04-29 21:31:01 +00:00
Vitor Pamplona f63e3b1c67 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-04-29 17:28:38 -04:00
Vitor Pamplona b77407f13d Merge pull request #2648 from vitorpamplona/claude/fix-nest-room-connection-H48ad
test(quic): add concurrent producer/consumer regression for SendBuffer
2026-04-29 17:19:17 -04:00
Vitor Pamplona df98235d31 Minor adjustments to remove warnings 2026-04-29 17:16:14 -04:00
Claude a84fbd2e57 test(quic): add concurrent producer/consumer regression for SendBuffer
The previous SendBuffer suite (FlowControlEnforcementTest) is entirely
single-threaded — every test calls enqueue and takeChunk sequentially
on the same coroutine, so the race that crashed the audio path in
production (NoSuchElementException from chunks.first() under
concurrent enqueue + takeChunk) stayed invisible. The whole :quic
commonTest tree had no concurrent test at all.

Three new tests run real-thread races on Dispatchers.Default:

  - concurrent_enqueue_and_takeChunk_does_not_throw drives multiple
    producer coroutines + a consumer coroutine and asserts the buffer
    drains cleanly with no exception.
  - concurrent_takeChunk_callers_never_double_drain_a_chunk fans out
    multiple consumers against a pre-populated buffer; asserts the
    sum of bytes handed out equals the bytes enqueued (i.e. no chunk
    is double-counted by overlapping head-peel paths).
  - concurrent_finish_with_inflight_enqueue_emits_correct_fin races
    finish() against in-flight writes and asserts the FIN comes
    AFTER every enqueued byte.

Tests pass against the synchronised SendBuffer; running them against
the pre-fix unsynchronised version corrupts state badly enough that
the consumer wedges (an explicit "this is what the bug looked like"
demonstration). With internal synchronisation in place the suite
finishes in <0.2 s.

Documents the concurrent-access contract so a future "let's drop the
sync, it's hot" refactor immediately fails CI.
2026-04-29 21:08:03 +00:00
Vitor Pamplona c88183809b Merge pull request #2646 from vitorpamplona/claude/fix-nest-room-connection-H48ad
Align nests servers with NIP-53 spec: separate relay and auth URLs
2026-04-29 17:03:27 -04:00
Claude 2d45c6ff4a fix(quic): make SendBuffer thread-safe to stop torn-state crash
QuicConnectionDriver.sendLoop holds the connection mutex while it calls
SendBuffer.takeChunk via QuicConnectionWriter.drainOutbound, but
WtPeerStreamDemux's per-stream `send` callback calls
SendBuffer.enqueue from arbitrary application coroutines without the
connection lock. The two paths concurrently mutate
(chunks, pendingBytes, headOffset, finPending, finSent, sentEnd,
nextOffset). Under load this surfaced as

  java.util.NoSuchElementException: ArrayDeque is empty.
    at kotlin.collections.ArrayDeque.first(ArrayDeque.kt:102)
    at com.vitorpamplona.quic.stream.SendBuffer.takeChunk(SendBuffer.kt:85)
    at com.vitorpamplona.quic.connection.QuicConnectionWriterKt.buildApplicationPacket(...)
    at com.vitorpamplona.quic.connection.QuicConnectionDriver.sendLoop(...)

The writer saw `pendingBytes > 0` (incremented by an in-flight
enqueue on another thread) before the matching `chunks.addLast`
became visible, fell into the head-peel branch, and tripped on
chunks.first().

Wrap every read and write of SendBuffer state in `synchronized(this)`,
including the cheap `readableBytes` / `sentOffset` / `finPending` /
`finSent` getters used by the writer's pre-flight checks (so they
can't read torn state either). The lock is uncontended in the common
case and short-held in the rare race; we already use synchronized
blocks elsewhere in commonMain (QuicConnectionDriver.kt).
2026-04-29 20:56:33 +00:00
Claude 2fae726e5e fix(nests): place Audience/Hands tab counter beside the label, not over it
PrimaryTabRow's BadgedBox places the count badge in the top-end corner
of its anchor, which on tight tab labels lands directly on top of the
text. Lay them out as a Row instead so "Audience  3" reads cleanly,
matching the tabbar comment's "Hands · 3" intent.
2026-04-29 20:53:38 +00:00
Claude 0fafd34537 fix(nests): emit canonical title tag and keep mute badge visible while muted
Two interrelated bugs from the deployed-schema flip:

1. Rooms created in Amethyst didn't appear in nostrnests's "Live Now"
   lobby. The lobby filter in NestsUI's skeleton.js requires a `title`
   tag on every kind:30312 — events without one are dropped from
   `live`/`planned`/`ended`. RoomNameTag still emitted the legacy
   `room` name, so our rooms passed our own readers (which tolerate
   both names) but were invisible to NestsUI. Flip canonical to
   `title`, keep `room` as legacy on read.

2. The mute badge over the avatar disappeared shortly after toggling
   mute, even though the broadcaster was still muted. Two causes:

   - ParticipantsGrid gated both the muted ring and the MicStateBadge
     on `member.publishing`. But per the deployed kind-10312 semantics
     `publishing=0` whenever `muted=1` (NestRoomPresencePublisher
     mirrors this — `publishingNow = broadcasting && !isMuted`), so
     the badge that was supposed to *indicate* mute was hidden
     exactly when it became relevant. Show the badge when on stage
     and either publishing OR muted; gate the muted ring on `muted`
     alone.

   - The presence heartbeat captured `micMutedTag` / `publishingTag`
     / `onstageTag` / `handRaised` at LaunchedEffect launch and never
     refreshed them. The 500 ms debounced LaunchedEffect did publish
     the new mute state, but ~30 s later the heartbeat overwrote it
     with the captured pre-toggle values, flipping `publishing`
     back to 1 and `muted` back to 0 server-side. Wrap the dynamic
     fields in `rememberUpdatedState` so the heartbeat reads the
     latest snapshot on every refresh; keep the existing key set
     (event/handRaised/onstage) for prompt re-emission on those
     transitions.

Tests updated for the new canonical `title` emission; both jvmTest
suites pass.
2026-04-29 20:45:32 +00:00
Claude aadb347b84 feat(nests): adopt deployed nostrnests schema (streaming/auth/live + paired 10112)
The deployed nostrnests reference (NestsUI v2 + moq-auth + moq-rs)
emits a different on-the-wire schema than the previous EGG-01 / EGG-09
drafts and Quartz writers. Verified by reading the production
NestsUI bundle. The deployed schema is now canonical:

kind:30312 (room event)
  - relay URL → ["streaming", url]   (was ["endpoint", url])
  - auth URL  → ["auth",      url]   (was ["service",  url])
  - live status → ["status", "live"]   (was "open")
  - ended status → ["status", "ended"] (was "closed")
  - room name → ["title", name]        (was ["room", name])

kind:10112 (user MoQ-server list)
  - 3-element entries: ["server", relay, auth]
  - first-element name "relay" accepted as a synonym (legacy)
  - 2-element entries: derive auth host by replacing leading "moq."
    with "moq-auth.", or prepending "moq-auth." otherwise

Implementation
- ServiceUrlTag.TAG_NAME = "auth", LEGACY_TAG_NAME = "service"
- EndpointUrlTag.TAG_NAME = "streaming", LEGACY_TAG_NAME = "endpoint"
- StatusTag enum: PLANNED/LIVE/PRIVATE/ENDED with code "live"/"ended";
  legacy "open"/"closed" still parsed on read
- NestsServersEvent: emit/read 3-element [server, relay, auth] tags
  with the legacy-shape tolerances above; expose a NestsServer pair
- Account.nestsServers.flow now produces List<NestsServer> pairs
- NestsServersScreen: rewritten edit-field asks for both URLs,
  recommended row shows the nostrnests pair, list rows show both URLs
- NestsScreen first-time setup writes the nostrnests pair, not a
  single URL; gate the create FAB on both URLs being parseable
- CreateNestViewModel: drop the resolveServerPair hardcoded mapping —
  the saved pair is now authoritative; defaults stay correct for an
  empty kind-10112 list

Specs
- EGG-01 rewritten to canonical streaming/auth/live/ended with a
  legacy-spelling table; example uses the real nostrnests pair
- EGG-02 references "auth" tag throughout; error taxonomy says "ended"
- EGG-09 rewritten to 3-element server tag with derivation fallback
- New nestsClient/specs/nip-53-proposed.md — a single self-contained
  proposed update to upstream NIP-53 covering kind 30312 + kind 10112
  with the deployed schema and the JWT-mint flow

All existing unit tests adjusted; quartz:jvmTest, amethyst:testPlayDebugUnitTest,
and nestsClient:jvmTest pass.
2026-04-29 19:32:53 +00:00
Claude 63555db48a fix(nests): use moq-auth.nostrnests.com for JWT mint
The previous defaults pointed both `service` and `endpoint` URLs at
`moq.nostrnests.com:4443`. That host only listens on UDP/QUIC for the
WebTransport relay — TCP :4443 is unreachable. OkHttp can't speak HTTP/3,
so the JWT-mint POST hangs and fails with `ConnectException`, leaving
the room screen stuck on "Reconnecting".

The real nostrnests deployment co-locates auth and relay on different
hosts (verified against the production NestsUI bundle):

    relay (WebTransport, QUIC only):  https://moq.nostrnests.com:4443
    auth  (JWT mint, regular HTTPS):  https://moq-auth.nostrnests.com

Split the defaults accordingly and add a `resolveServerPair` helper that
maps a single saved kind-10112 URL onto the (service, endpoint) pair the
kind-30312 event needs. The helper recognises both the auth-host and
the legacy relay-host the prior defaults wrote, so users upgrading from
the broken version migrate transparently the next time they open the
create-room sheet. Community deployments that genuinely co-locate keep
working via the fallback.

Update the recommended-servers list and the kind-10112 documentation to
match: the stored `server` URL is the moq-auth base (the URL that ends
up in the kind-30312 `service` tag).
2026-04-29 18:20:21 +00:00
Vitor Pamplona 47f700afb3 Merge pull request #2644 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-29 08:14:11 -04:00
Crowdin Bot 893ac76e3f New Crowdin translations by GitHub Action 2026-04-29 12:11:59 +00:00
Vitor Pamplona 94d3982f88 Merge pull request #2645 from greenart7c3/claude/add-payment-targets-settings-QgGFg
Add payment targets navigation to settings screen
2026-04-29 08:10:12 -04:00
Claude 8078e72bc1 feat: expose payment targets settings in account settings
Add a navigation row in AllSettingsScreen to open the existing
PaymentTargetsScreen, so users can configure their NIP-A3 payment
targets (kind 10133) from the account settings menu.
2026-04-29 11:59:39 +00:00
Vitor Pamplona aef0683bd6 Merge pull request #2639 from davotoula/fix-sonar-empty-functions
Fix sonar empty functions and string literals
2026-04-29 07:53:22 -04:00
Vitor Pamplona fe02f61d46 Merge pull request #2642 from mstrofnone/fix/last-seen-formatting
Fix Last Seen formatting for older timestamps ("Last seen Jan 14 ago" → "Last seen on Jan 14, 2026 (3 months ago)")
2026-04-29 07:53:07 -04:00
Vitor Pamplona da30e7ada0 Merge pull request #2643 from mstrofnone/fix/vlc-download-retry
ci(desktop): retry vlcDownload on transient network failures
2026-04-29 07:51:24 -04:00
David Kaspar 19e19192e3 Merge pull request #2641 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-29 12:32:55 +02:00
mstrofnone fd7ce48964 ci(desktop): retry vlcDownload on transient network failures
Every desktop packaging job (Windows MSI, macOS DMG, Linux DEB) currently
breaks on the smallest blip when fetching VLC from get.videolan.org. The
`ir.mahozad.vlc-setup` plugin registers `vlcDownload` / `upxDownload`
tasks that extend `de.undercouch.gradle.tasks.download.Download`, but it
does not configure retries or a generous read timeout, so a single
`SocketTimeoutException` aborts the build.

Recent main runs all failed at the same step:

    > Task :desktopApp:vlcDownload FAILED
    > A failure occurred while executing ...DefaultWorkAction
       > java.net.SocketTimeoutException: Read timed out

Configure all `Download` tasks in this project with:

  - 5 attempts total (initial + 4 retries),
  - 30 s connect timeout,
  - 5 min read timeout (VLC archives are 40-90 MB and the mirror can be
    slow under load),
  - `tempAndMove` so a partial download from one attempt cannot poison
    the next.

This is a CI-only change \u2014 it does not alter what gets bundled, just
makes the fetch resilient.
2026-04-29 20:21:18 +10:00
M 1df6504aac Fix Last Seen formatting for older timestamps
The profile 'Last seen' line was rendered by feeding timeAgo() output
into the 'Last seen %1$s ago' template. timeAgo() returns a bare
absolute date (e.g. 'Jan 14' or 'Jul 27, 2024') for anything older
than a month, so the result was nonsensical:

  - 'Last seen Jan 14 ago'
  - 'Last seen Jul 27, 2024 ago'

Replace the call with a new lastSeenSentence() helper that always
returns a self-contained, grammatical sentence:

  - 'Last seen 5 minutes ago'
  - 'Last seen 2 hours ago'
  - 'Last seen 3 days ago'
  - 'Last seen 2 weeks ago'
  - 'Last seen on Jul 27, 2024 (9 months ago)'
  - 'Last seen on Jan 14, 2024 (1 year ago)'

Anything older than a week now also shows the absolute date alongside
a coarse relative duration, so users can see exactly when the activity
happened without giving up the human-readable 'X ago' framing.

Adds plural-aware duration_minutes/hours/days/weeks/months/years
resources plus last_seen_on_date / last_seen_just_now / last_seen_never
helper strings. The existing 'last_seen' string keeps its placeholder
shape so existing translations continue to work for the recent-duration
case.
2026-04-29 19:11:09 +10:00
Crowdin Bot 5d314a3939 New Crowdin translations by GitHub Action 2026-04-29 08:42:00 +00:00