summaryrefslogtreecommitdiff
Commit message (Collapse)AuthorAgeFilesLines
* fix: render patchsets on a plain listv3.7.0Christian Cleberg2026-07-151-0/+6
| | | | | | | | | | | | | | | Collapsing patches shrank the layout loop from 3674pt/1647pt to 718pt/600pt but did not end it. The oscillating item is section 1 item 0 — the cover letter, not a patch — so size alone was not the cause. The log shows the cell laid out at width 390.0 while the content reports its preferred size at 390.333. That is inset grouped's 20pt insets landing on a fractional width: the Text reflows to a different height than the cell was sized for, each size triggers the other, and it never settles. ThreadDetailView renders the same bodies through the same DiffView with the same modifiers and does not loop. The difference is .listStyle(.plain), which this view never set and so inherited inset grouped.
* fix: collapse patches to stop a recursive layout loopChristian Cleberg2026-07-151-15/+89
| | | | | | | | | | | | | | | | | | | Opening a patchset wedged the app. UICollectionView reported a row oscillating between 3674pt and 1647pt and trapped in a recursive layout loop, leaving the UI unresponsive. The detail view rendered every patch in the series expanded, so a List held one enormous self-sizing row per patch, each with a full diff. Self-sizing cells that large do not settle. Patches now start collapsed and expand on tap, so at most the ones a reviewer opens are measured. This is what ThreadDetailView already does — it collapses every message but the last, and renders the same diffs through the same DiffView without trouble. Reviewing a series one patch at a time is also closer to how the reading actually goes. The rendering of a block list is shared between the cover letter and patches rather than duplicated.
* fix: push patchset views directly instead of by routeChristian Cleberg2026-07-154-8/+15
| | | | | | | | | | | | | | | | | | | Tapping a patch failed with "no matching navigationDestination declaration visible from the location of the link". MailingListDetailView is presented from four places, but only the More tab and Lookup declare a MoreRoute destination. Reached from a project, via ProjectMailingListView, there is no such destination in the surrounding stack, so a NavigationLink carrying MoreRoute.patchset had nowhere to resolve. The thread rows beside it already use the closure form for exactly this reason. Push PatchsetDetailView directly, from the rows and from the version-chain links inside the detail view, which inherits whatever stack presented it. That leaves MoreRoute.patchset with no users, so it and its two destinations are removed rather than left as a route nothing links to. Neither the compiler nor the tests catch this: it is a runtime SwiftUI resolution failure.
* chore: bump to 3.7.0 and record Phase 2Christian Cleberg2026-07-153-26/+38
| | | | | | | MARKETING_VERSION 3.6.0 -> 3.7.0, build 88 -> 89. The README feature list also picks up Phase 1's ticket editing, subscriptions, and email preferences, which it never gained.
* feat: review patchsetsChristian Cleberg2026-07-156-1/+810
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Patchsets are how contributions reach sourcehut, and Hutch had no reference to them anywhere. This adds review and triage: read a series, see its checks and version chain, and set its status. Two schema facts shaped the design. MailingList exposes no patchsets field, so a list's patchsets cannot be queried directly. They are reachable only through thread roots, so the existing threads query now also selects root.patchset — no extra request — and the Patches tab is derived from that. It appears only on lists that actually carry patches. Patch carries no diff. index, count, version, prefix, subject, and trailers are all it has; the diff exists only inside the email body. Patch bodies are split with the same InboxThreadUtilities.segmentMessageBody the inbox uses and rendered through the existing DiffView. Patches are ordered by their [PATCH n/m] index rather than receipt order, since mail arrives out of sequence. Patches with no index are kept at the end rather than dropped, because a one-off patch has no prefix. updatePatchset is nullable, so a null response is treated as a declined change and the local status is left alone rather than advanced optimistically. UNKNOWN and SUPERSEDED are not offered: the first is a sentinel, the second is set by the server when a newer version lands. Patch submission stays out of scope. It is a git send-email flow, not a GraphQL mutation.
* refactor: share the email body diff splitterChristian Cleberg2026-07-154-90/+223
| | | | | | | | | | | | | segmentMessageBody and its helpers were private to ThreadViewModel, reachable from tests only through a segmentMessageBodyForTesting shim. Patchset review needs the same splitting, because sr.ht's Patch type carries no diff — the diff only exists inside the email body — so this has to be shared rather than duplicated. Moved to InboxThreadUtilities. The shim is gone; the existing test calls the real function directly now. Also adds the Patchset model layer that the coming views build on.
* Phase 1: close the write gaps (#3)v3.6.0Christian Cleberg2026-07-1511-453/+862
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | * refactor: collapse duplicated request paths in SRHTClient Five request paths each repeated the token guard, header setup, status-code handling, and a ~35-line #if DEBUG logging block. The file carried that block five times over. Extract makeAuthorizedRequest, send, and encodedGraphQLBody, and route execute, executeAndCache, executeMultipartFiles, and performGraphQLRequest through them. executeMultipart is now the single-file case of executeMultipartFiles, which it already was byte for byte. 938 lines to 612, with one copy of the logging block. fetchText keeps its own guard: it is a GET to an allowlisted URL and must not run GraphQL error checks over what is usually a plain-text build log. One behavior change falls out. executeAndCache wrote the raw response to the cache before decoding, so a 200 carrying GraphQL errors was cached and then thrown. Routing it through performGraphQLRequest surfaces those errors first, so error payloads are no longer cached. * feat: edit and delete tickets updateTicket and deleteTicket both existed in todo.sr.ht's API but were never called, so a ticket could be filed and its status changed but its subject and body were frozen from the moment it was created, and it could never be removed. Edit opens a sheet seeded with the current subject and body. The input carries only fields that actually changed, so an edit cannot clobber a field the user did not touch, and Save stays disabled until something differs. Clearing the body sends an explicit null via updateValue rather than a nil subscript assignment, which would drop the key and silently leave the old body in place — the same trap fixed for repository descriptions in 7ffef07. Delete is destructive and irreversible, so it sits behind a confirmation dialog naming the ticket and pops the detail view on success. * feat: subscribe to and unsubscribe from tickets ticketSubscribe and ticketUnsubscribe existed in the API but were never called, so email notifications for a ticket could only be managed on the web. Ticket.subscription is null when the user is not subscribed, so the detail query now reads it and the menu reflects real server state rather than guessing. The toggle updates optimistically and reverts on failure, so the control never claims a subscription that did not take. Decoded into the private payload rather than TicketDetail, which is Codable and cached — adding a field there would have changed the cached shape and touched every optimistic-update construction site. * feat: subscribe to and unsubscribe from trackers trackerSubscribe and trackerUnsubscribe existed in the API but were never called. Tracker.subscription is null when not subscribed, so the state can be read rather than guessed. The read is a separate uncached query. The tickets query it sits beside is paginated and cached, and a per-user subscription has no business riding along in page payloads or being served stale from disk. Unsubscribe passes tickets: false, so leaving a tracker does not silently drop subscriptions to individual tickets the user opted into. * feat: unsubscribe from mailing lists mailingListUnsubscribe existed in the API but was never called, so the list of subscriptions was readable and nothing more. Scoped to unsubscribe. MailingList has no subscription field, unlike Ticket and Tracker, so per-list state is only knowable from the subscriptions query — which is exactly what builds this view. Subscribing would need a list the user is by definition not subscribed to, and sr.ht exposes no discovery API to find one (see SCOPE.md on hub.sr.ht), so there is nowhere honest to put that action yet. The row is removed optimistically and restored if the mutation fails. The confirmation says plainly that Hutch cannot resubscribe, since it cannot. * feat: manage todo and lists email preferences updatePreferences existed on both services but was never called, so these were web-only settings. The two services expose preferences/updatePreferences under identical names but with different fields — notifySelf on todo, copySelf on lists — and there is no shared preferences service, so both are read and written side by side. They load concurrently and one service being unreachable does not hide the other's toggle. These are server-side and apply beyond Hutch, unlike the @AppStorage toggles above them in Settings, so the footer says so and each toggle reverts if its mutation fails. * refactor: drop the memory-only cache path Two executeCached overloads existed with different return types and semantics: one doing stale-while-revalidate against the persistent cache with TTLs, the other only consulting the in-memory responseCache. The second was an easy thing to reach for by mistake, since the compiler picked it purely on argument labels. It turned out to be dead. All 38 call sites already used the TTL-aware overload, and the memory-only one was the sole caller of executeAndCache, so both are removed. Its doc comment promised refresh "via the onRefresh callback", which the signature has not had for some time. SRHTClient is now 569 lines, down from 938 before this branch. responseCache stays as the in-memory layer behind cachedPayload and the three view models that read it directly. * chore: bump to 3.6.0 and record Phase 1 MARKETING_VERSION 3.5.0 -> 3.6.0, build 87 -> 88. * fix: decode preferences responses on the main actor The module sets SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, so the response types are implicitly main-actor isolated and their Decodable conformances are too. Decoding straight from an `async let` used those conformances from a nonisolated context, which warns today and is an error in the Swift 6 language mode. Move each fetch into its own method and `async let` over those instead, so decoding stays on the main actor. This is what HomeViewModel.loadDashboard already does, and the concurrency is unaffected — the network work still overlaps, since execute suspends and frees the actor.
* docs: remove stray tags from roadmapChristian Cleberg2026-07-151-2/+0
| | | | Two closing XML tags were left at the end of the file when it was written.
* Merge pull request #2 from ↵Christian Cleberg2026-07-152-3/+3
|\ | | | | | | | | zerolabsco/dependabot/swift/github.com/apple/swift-markdown-0.8.0 chore(deps): bump github.com/apple/swift-markdown from 0.7.3 to 0.8.0
| * chore(deps): bump github.com/apple/swift-markdown from 0.7.3 to 0.8.0dependabot[bot]2026-07-162-3/+3
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Bumps [github.com/apple/swift-markdown](https://github.com/apple/swift-markdown) from 0.7.3 to 0.8.0. - [Release notes](https://github.com/apple/swift-markdown/releases) - [Commits](https://github.com/apple/swift-markdown/compare/0.7.3...3c6f9523da3a1ec2fd829673e472d95b8097a3b8) --- updated-dependencies: - dependency-name: github.com/apple/swift-markdown dependency-version: 0.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]>
* | Merge pull request #1 from ↵Christian Cleberg2026-07-151-2/+2
|\ \ | | | | | | | | | | | | zerolabsco/dependabot/swift/github.com/swiftlang/swift-cmark-0.8.0 chore(deps): bump github.com/swiftlang/swift-cmark from 0.7.1 to 0.8.0
| * | chore(deps): bump github.com/swiftlang/swift-cmark from 0.7.1 to 0.8.0dependabot[bot]2026-07-161-2/+2
| |/ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Bumps [github.com/swiftlang/swift-cmark](https://github.com/swiftlang/swift-cmark) from 0.7.1 to 0.8.0. - [Release notes](https://github.com/swiftlang/swift-cmark/releases) - [Changelog](https://github.com/swiftlang/swift-cmark/blob/gfm/changelog.txt) - [Commits](https://github.com/swiftlang/swift-cmark/compare/0.7.1...924936d0427cb25a61169739a7660230bffa6ea6) --- updated-dependencies: - dependency-name: github.com/swiftlang/swift-cmark dependency-version: 0.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]>
* | bump actions/checkout versionChristian Cleberg2026-07-151-1/+1
| |
* | ci: constrain GITHUB_TOKEN to contents: readChristian Cleberg2026-07-151-0/+5
|/ | | | | | | | | | The workflow set no permissions, so GITHUB_TOKEN inherited the repository default — read-write for repositories created before February 2023. Flagged by CodeQL as actions/missing-workflow-permissions (CWE-275). Checkout only reads the repo and xcodebuild uses no token, so contents: read covers the job. upload-artifact authenticates with the separate runtime token and is unaffected.
* Set package ecosystem to 'swift' for DependabotChristian Cleberg2026-07-151-0/+11
|
* docs: add roadmapv3.5.0Christian Cleberg2026-07-151-0/+112
| | | | | | Four phases ordered by dependency, with feature gaps identified by diffing the schema dumps in Docs/API against actual call sites. Records Phase 0 as done and what unblocking CI turned up.
* ci: run the test plan on macOSChristian Cleberg2026-07-151-0/+60
| | | | | | | | | | | | | The builds.sr.ht job runs on Ubuntu and can only do secret scanning and structure checks, so the 214 tests ran only when someone remembered to run them in Xcode. That is why the suite had rotted to ten failures. builds.sr.ht has no macOS image and its maintainer has ruled them out, so xcodebuild cannot run there. Add a job on the GitHub mirror instead. Pinned to macos-26 because macos-latest still points at macOS 15, which lacks the iOS 26 SDK. The simulator is resolved at runtime rather than pinned by name, since device names shift between runner images.
* fix: give inbox threads identity distinct from their grouping keyChristian Cleberg2026-07-155-13/+16
| | | | | | | | | | | | | | | | | InboxThreadSummary.id returned threadGroupingKey, which is listRID plus the subject with Re:/Fwd: stripped. Two unrelated threads on one list sharing a subject therefore shared an id — common on sourcehut, where "[PATCH] test" is an ordinary subject — which collides under Identifiable in every list that renders these summaries. Key id on the root Message-ID, which is unique per thread, and leave threadGroupingKey subject-based so replies still collapse into one conversation. Read state moves to threadGroupingKey at each call site. It was already keyed on that string via id, so persisted keys are unchanged and marking a conversation read still covers the whole subject group, matching how HomeViewModel already builds the key for isUnread.
* fix: render code span contents literallyChristian Cleberg2026-07-151-7/+13
| | | | | | | | | | | processInline protected allowlisted HTML tags before it handled code spans, so a `<b>` written inside backticks was carried through as a live tag and applied formatting instead of rendering as text. Every other inline pass ran against code span contents for the same reason, so `**x**` in backticks was emitted as bold. Protect code spans first with their contents escaped, which takes them out of reach of the tag, emphasis, and link passes.
* test: match cached incident fixture to its RSS sourceChristian Cleberg2026-07-151-1/+1
| | | | | | | The expected incident declared url: nil while the cachedIncidentRSS it stands in for carries <link>https://status.sr.ht/issues/1/</link>, so the fixture contradicted its own input. The repository parses and persists the link correctly.
* fix: stop shadowing serviceNotProvisioned classificationChristian Cleberg2026-07-151-3/+5
| | | | | | | | | | "No such repository or user found" matched the broad "no such" test for .notFound, which ran first and made the .serviceNotProvisioned rule below it unreachable. Users hitting a service they have not activated were told the content was no longer available rather than that the account needs to enable the service. Order the specific check ahead of the general one.
* fix: allow clearing a repository descriptionChristian Cleberg2026-07-151-1/+8
| | | | | | | | | | metadataInputForSave assigned nil through the dictionary subscript to clear a description. Swift removes the key on a nil subscript assignment, so the mutation was sent with no `description` field and the old value survived — clearing a description silently did nothing. Store the nil with updateValue so the key is retained; AnyCodable already encodes an unmatched value as a JSON null.
* test: assert image URLs are not double-escapedChristian Cleberg2026-07-151-1/+4
| | | | | | | | | | markdownImageQueryStringPreservesAmpersands rejected any "amp;metric" in the rendered HTML, but `&amp;` is the correct encoding for `&` in an attribute value and is what a browser needs to request a literal `&`. The assertion conflated the URL with its HTML encoding. Target the real failure mode instead: double-escaping, which would send "&amp;" through as part of the query string and break badge images.
* test: repair stale expectations and request-body captureChristian Cleberg2026-07-153-6/+28
| | | | | | | | | | | | | | | These tests drifted from the code and went unnoticed because CI never ran them. All four are test-side errors; no app behavior is involved. - TicketListViewModelTests asserted lowercase "resolved"/"fixed"/"reported" against TicketStatus/TicketResolution rawValues, which are uppercase to match the todo.sr.ht GraphQL enums. - HomeViewModelTests expected failedBuilds in ascending id order. Ordering moved to newest-first when sortBuildItemsForTriage landed in 5f6d545; the filtering the test covers is unchanged. - SettingsViewModelTests read request.httpBody inside a URLProtocol, where it is always nil because URLSession moves the body onto httpBodyStream. The stub now reads the body off the stream at capture time.
* fix: repair test plan reference in Hutch schemeChristian Cleberg2026-07-151-1/+1
| | | | | | | | | | The Hutch scheme referenced `container:HutchTests`, omitting the `.xctestplan` extension, so `xcodebuild test -scheme Hutch` failed with "the test plan HutchTests could not be read". HutchTests.xcscheme already used the correct reference. The README directs contributors to the Hutch scheme, so this path needs to work before it can be wired into CI.
* fix: drop stale website check from CIChristian Cleberg2026-07-151-1/+0
| | | | | | | repo-structure-check asserted `test -d "website"`, but the website/ tree was removed in 24c8bc6 when the privacy policy moved to its new location. The check has failed on every build since, so the builds.sr.ht badge has been red since April.
* fix: bump versionChristian Cleberg2026-05-141-12/+12
|
* feat: add App Intents for Hutch navigationChristian Cleberg2026-05-1416-126/+747
| | | | | | | | | | - add read-only App Intents for core Hutch workflows - route intents through the existing Hutch navigation/deep-link model - expose Work Queue, Recent Activity, System Status, pinned resources, projects, failed builds, assigned tickets, saved searches, and search where supported - keep App Intents non-mutating for the initial implementation - preserve existing widget, Safari extension, and hutch:// routing behavior References: https://todo.sr.ht/~ccleberg/hutch/71
* fix: center align medium widgetChristian Cleberg2026-05-142-45/+41
|
* feat: add Safari extension for sourcehut deep linksv3.4.0Christian Cleberg2026-05-1427-35/+1047
|
* fix: bump SECURITY.md support versionsChristian Cleberg2026-05-061-2/+2
|
* fix: update SCOPE.mdChristian Cleberg2026-05-061-2/+1
|
* feat(cache): persist read-only API responsesv3.3.1Christian Cleberg2026-05-0613-103/+304
| | | | | | | | | | | | Add a bounded stale-while-revalidate cache at the Sourcehut API boundary with stable keys, centralized TTLs, request coalescing, payload hashing, and LRU disk pruning. Cache high-value read-only repo, build, ticket, project, profile, paste, and Home/Work Queue data while keeping mutations network-only and invalidating related prefixes after successful writes. Add focused cache tests and implementation notes.
* feat: add persistent stale-while-revalidate API cacheChristian Cleberg2026-05-0616-94/+1359
| | | | | | | | | | | | | | | | | | | | | | | | | | | Introduce an actor-backed persistent cache layer at the SRHTClient boundary for read-only SourceHut data. Cache entries now store stable metadata including key, resource type, fetched/expires/access timestamps, payload hash, schema version, and payload size, with bounded memory and disk usage. Add centralized cache key builders and TTL defaults for repository, file, ticket, build, log, profile, status, and list-style resources. Support networkOnly, cacheOnly, cacheFirstThenRefresh, and refreshIgnoringCache policies, plus request coalescing for duplicate in-flight cache keys. Integrate first-pass caching into high-value low-risk read paths: - build detail and completed/active build logs - ticket detail - README lookup - repository tree, blob, and linked file reads Keep mutation paths network-only and add simple prefix invalidation after ticket and build mutations. Add compact cached/stale UI status rows and a Settings action to clear the persistent cache. Add focused cache tests covering round trips, expiration, stale fallback, policy behavior, request coalescing, prefix invalidation, size limits, LRU pruning, expired pruning, and mutation bypass behavior. Document storage, key, TTL, invalidation, limitations, and next recommended targets.
* fix: URI for personal access token generationChristian Cleberg2026-05-041-4/+4
| | | | Fixes: https://todo.sr.ht/~ccleberg/hutch/69
* Fix broken personal access token linkChris DeLuca2026-05-042-2/+2
|
* fix: profile loading error and user-timeline prefs updatev3.2.1Christian Cleberg2026-04-2311-21/+186
|
* fix: collapse work inbox messages into thread summariesv3.2.0Christian Cleberg2026-04-206-473/+112
|
* fix: add mark all read actions and avoid resending unchanged repo metadataChristian Cleberg2026-04-205-10/+116
|
* fix: ensure all branches and tags are accounted for and not cutoff due to ↵Christian Cleberg2026-04-202-19/+132
| | | | pagination
* feat: user-defined time period for recent builds cardChristian Cleberg2026-04-206-39/+170
| | | | | | | | | - limit Home failed-build counts to a configurable lookback window and add the setting under Behavior. - make the Recent and Builds rows fully tappable across the entire cell and add coverage for the new failed-build filtering. Fixes: https://todo.sr.ht/~ccleberg/hutch/67
* fix: change failed builds card on home tab to navigate to builds tab instead ↵Christian Cleberg2026-04-203-10/+22
| | | | | | of implementing its own view Fixes: https://todo.sr.ht/~ccleberg/hutch/66
* fix: Sonar issues in BundleUserAgentTests and accept README URL path joinv3.1.11Christian Cleberg2026-04-192-12/+14
|
* fix: finish half-completed storekit implementationChristian Cleberg2026-04-198-28/+338
|
* fix: fixes dead relative links in md/org with a new single-file viewer when ↵Christian Cleberg2026-04-156-58/+421
| | | | | | tapped Fixes: https://todo.sr.ht/~ccleberg/hutch/63
* feat: add custom hutch user-agent to api callsChristian Cleberg2026-04-157-18/+165
| | | | Implements: https://todo.sr.ht/~ccleberg/hutch/64
* bump version for IAPsv3.1.7Christian Cleberg2026-04-141-8/+8
|
* fix: remove prototype screens from developer menuv3.1.6Christian Cleberg2026-04-1410-157/+209
| | | | | | | | - removed prototype screens - limited Recent section to 3 items - added initial support for tips Fixes: https://todo.sr.ht/~ccleberg/hutch/65
* fix: more sonarqube quality fixesv3.1.5Christian Cleberg2026-04-136-31/+40
|
* fix: sonarqube code smell fixesv3.1.4Christian Cleberg2026-04-1333-262/+257
|