<feed xmlns='http://www.w3.org/2005/Atom'>
<title>hutch.git, branch v3.8.0</title>
<subtitle>ios client for sourcehut. swiftui.
</subtitle>
<id>http://git.krz.sh/krz/hutch.git/atom?h=v3.8.0</id>
<link rel='self' href='http://git.krz.sh/krz/hutch.git/atom?h=v3.8.0'/>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/hutch.git/'/>
<updated>2026-07-16T06:21:33+00:00</updated>
<entry>
<title>chore: remove the artifacts debug probe</title>
<updated>2026-07-16T06:21:33+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-16T06:21:33+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/hutch.git/commit/?id=21be03e6ca54c1a15607faab905b119eda9a548f'/>
<id>urn:sha1:21be03e6ca54c1a15607faab905b119eda9a548f</id>
<content type='text'>
It did its job: tags=1 confirmed the menu was disabled by its own tags check
while the explanation sat unreachable inside it.
</content>
</entry>
<entry>
<title>fix: deleted repositories linger until the cache expires</title>
<updated>2026-07-16T06:16:39+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-16T06:16:39+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/hutch.git/commit/?id=ad108c76d062099af9e698e1f0ba0d8cecfeab1f'/>
<id>urn:sha1:ad108c76d062099af9e698e1f0ba0d8cecfeab1f</id>
<content type='text'>
Deleting a repository left it on the list, and pulling to refresh did not shift
it. Two independent reasons, both cache-related.

deleteRepository never invalidated anything. Creation invalidates the
repositories and home prefixes; deletion was written without it, so the list and
Home kept serving a repository that no longer exists.

And forceRefresh only ever reached the build statuses — its own doc comment says
so — while the repository list itself was pinned to useCache: true. So a pull to
refresh re-served the same cache it already had. fetchPage already takes
useCache and falls through to an uncached fetch; it was simply never told.
</content>
</entry>
<entry>
<title>fix: download artifacts through the API instead of handing them to Safari</title>
<updated>2026-07-16T06:06:46+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-16T06:06:46+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/hutch.git/commit/?id=871b04159aa47c0b0e62e2520c2f30e81f0f024b'/>
<id>urn:sha1:871b04159aa47c0b0e62e2520c2f30e81f0f024b</id>
<content type='text'>
Tapping download opened Artifact.url in the browser, which answered with
"Authorization header is required". That URL is not a web page: git.sr.ht
resolves it to &lt;api origin&gt;/query/artifact/&lt;checksum&gt;/&lt;filename&gt;, which demands
a bearer token. Safari has none and no way to get one, so the download could
never have worked — this predates the upload work.

Fetch it with the client that already holds the token and hand the user the file
through a share sheet. fetchData mirrors fetchText, including its host guard, so
an authenticated request still cannot be aimed anywhere but *.sr.ht over https.

Also guards zero-byte uploads. sr.ht streams into S3, which rejects a zero-part
multipart completion with "MalformedXML: UnknownError" — an error that says
nothing about the cause and cost a round of testing to identify. Empty files are
now refused by name before the request is made.
</content>
</entry>
<entry>
<title>fix: make artifact upload actually fire, and show it when it fails</title>
<updated>2026-07-16T05:54:59+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-16T05:54:59+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/hutch.git/commit/?id=d32ad837ac9eb153598c10c3cf47fbdb4e51e3ba'/>
<id>urn:sha1:d32ad837ac9eb153598c10c3cf47fbdb4e51e3ba</id>
<content type='text'>
Picking a file did nothing. The fileImporter's isPresented binding was derived
from uploadTargetRef and nilled it on dismissal, but dismissal happens before
the completion runs — so the completion read nil and returned without
uploading. Presentation state and payload state cannot be the same state. A
plain isImporting bool drives presentation now; the tag survives in
uploadTargetRef until the completion consumes it.

The upload menu was also disabled when the repository has no tags, while the
explanation for that state lived inside the menu — unreachable exactly when it
applies, so the tap died with no reason given. sr.ht requires revspec to match a
tag, so having none is a real state worth explaining rather than hiding.

Failures were invisible too. uploadArtifact and deleteArtifact set error, but
the overlay only renders it when the list is empty, so a rejection on a
repository that already has artifacts — a duplicate filename is the likely one,
since sr.ht requires filenames to be unique per repository — set an error nobody
saw. The tab carries an error banner now.
</content>
</entry>
<entry>
<title>fix: push mailing lists locally, fix upload menu, drop the events feed</title>
<updated>2026-07-16T05:42:38+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-16T05:42:38+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/hutch.git/commit/?id=77cd5b5e56ec054b451a6165162fef655873a5d6'/>
<id>urn:sha1:77cd5b5e56ec054b451a6165162fef655873a5d6</id>
<content type='text'>
Opening a mailing list from More → Projects still blanked. The cause was not in
handleTabNavigation: the row called openMailingList and then dismiss(), so a
path rebuild and a pop of this very view raced each other. Projects already
lives in the More tab, so there is nothing to navigate to — push
MailingListDetailView directly, which also lands back on the project rather than
on Mailing Lists. Sources and trackers keep routing, because they really do land
in other tabs.

The upload controls did nothing. Two .confirmationDialog modifiers on one view
leave one silently dead, and this view already had one for delete, so the tag
picker never presented. It is a Menu now, which also puts the tags one tap away
instead of two.

The ticket activity feed is removed. todo.sr.ht's root events resolver joins
event.participant_id, which references participant(id), against
participant.user_id — different id spaces — so it returns an empty list for
every user. The rows exist; that join cannot find them. Ticket.events is
unaffected because it filters on ticket_id, which is why ticket timelines work.
No client can fix this, and a screen that is permanently empty while blaming the
token's scopes is worse than no screen. Recorded in SCOPE.md with the query.
</content>
</entry>
<entry>
<title>fix: blank mailing list from Projects, swipe flicker, hidden upload</title>
<updated>2026-07-16T05:29:34+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-16T05:29:34+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/hutch.git/commit/?id=6898bc3fc00decee7224895ea75908daf0f97f59'/>
<id>urn:sha1:6898bc3fc00decee7224895ea75908daf0f97f59</id>
<content type='text'>
Three problems from manual testing.

Opening a mailing list from More → Projects showed a blank screen, while the
same tap on a project pinned to Home worked. handleTabNavigation reset the
target path and appended to it two Task.yields later. When the target tab is
already on screen — Projects lives under More — the reset starts an animated pop
of the view the user is standing on and the appends land mid-animation. From
Home the tab actually changes, so the More stack is quiescent and the appends
land cleanly. Each case now builds its path and assigns it once, so SwiftUI gets
a single diff with nothing to race.

Destructive swipe actions made the row vanish and spring back while the
confirmation was still up. role: .destructive makes SwiftUI perform the row
removal on activation, which allowsFullSwipe: false does not prevent — the
report was a tap, not a full swipe. These buttons only record pending state and
wait for an answer, so they are plain buttons tinted red instead. Six sites: the
two added here, plus trackers, pastes, and tracker ACLs and labels, which had
the same flicker already.

The artifacts upload control was invisible. It was declared as a toolbar item
from a view that is a segment inside RepositoryDetailView's tab switch rather
than its own navigation destination, so it never reached the navigation bar. It
is a row in the list now, and also an action on the empty state — the overlay
covers the list, and a repository with no artifacts is precisely the one that
needs uploading.
</content>
</entry>
<entry>
<title>fix: stop destructive swipes animating rows out before confirmation</title>
<updated>2026-07-16T05:18:53+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-16T05:18:53+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/hutch.git/commit/?id=2514b58a96ccb73b4feea8f74a2367c72ba824c8'/>
<id>urn:sha1:2514b58a96ccb73b4feea8f74a2367c72ba824c8</id>
<content type='text'>
Swiping to delete made the row vanish and then spring back while the
confirmation was still on screen. A destructive swipe action left to full-swipe
performs itself on the gesture and animates the row away, but these actions only
set pending state and wait for an answer, so the row returned when the data had
not changed.

allowsFullSwipe: false, which PasteListView already uses for exactly this
confirm-then-delete shape. Both new swipes had the same omission.
</content>
</entry>
<entry>
<title>chore: bump to 3.8.0 and record Phase 3 API features</title>
<updated>2026-07-16T05:03:06+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-16T05:03:06+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/hutch.git/commit/?id=76cb006b87d30928ec23a3c4bf95bd145aad8e9c'/>
<id>urn:sha1:76cb006b87d30928ec23a3c4bf95bd145aad8e9c</id>
<content type='text'>
MARKETING_VERSION 3.7.0 -&gt; 3.8.0, build 89 -&gt; 90.

SCOPE.md gains the items that did not survive contact with the API: archiveMessage
and mailingListSubscribe are blocked, while webhooks, shareSecret, and build
groups are reachable but declined on judgement. The reasoning is recorded so
they do not get re-proposed as gaps.

ROADMAP.md notes that Phase 3 is several releases rather than one, with the
measured size of each.
</content>
</entry>
<entry>
<title>feat: add a ticket activity feed</title>
<updated>2026-07-16T05:00:52+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-16T05:00:52+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/hutch.git/commit/?id=8ee93a6a6a0e771b687d1f48a59fb896ae0456e5'/>
<id>urn:sha1:8ee93a6a6a0e771b687d1f48a59fb896ae0456e5</id>
<content type='text'>
todo.sr.ht's root events query returns what the authenticated user is
subscribed to or implicated in, newest first, across every tracker including
ones they do not own. It was never called, so the only way to notice a reply was
to open the ticket.

Reuses EventChange, which already decodes todo's polymorphic EventDetail for
ticket timelines, so the same inline fragments describe both surfaces. Rows push
straight to the ticket.

events is nullable and comes back null when the token lacks the EVENTS scope, so
that case reports a scope problem rather than an empty feed, which would read as
"nothing has happened".

Paginates rather than fetching everything: an active account's history is
unbounded and the top of it is the whole point.

archiveMessage, the other half of this roadmap item, is not here. It is marked
@internal in the schema and inaccessible, like revokePersonalAccessToken already
recorded in SCOPE.md.
</content>
</entry>
<entry>
<title>feat: create, edit, and delete mailing lists</title>
<updated>2026-07-16T04:58:26+00:00</updated>
<author>
<name>Christian Cleberg</name>
<email>hello@cleberg.net</email>
</author>
<published>2026-07-16T04:58:26+00:00</published>
<link rel='alternate' type='text/html' href='http://git.krz.sh/krz/hutch.git/commit/?id=59622556deb4de7843faa493c8d5a6ef66591d91'/>
<id>urn:sha1:59622556deb4de7843faa493c8d5a6ef66591d91</id>
<content type='text'>
createMailingList, updateMailingList, and deleteMailingList existed in the API
but were never called, so lists could only be read.

Editing needed a read first. InboxMailingListReference carries only
id/rid/name/owner, so a settings sheet seeded from it would have offered an
empty description and Public visibility, and saving would have blanked the real
description and quietly changed who can see the list. The sheet now reads the
current values and refuses to save until it has them.

Clearing a description sends an explicit null via updateValue rather than a nil
subscript assignment, which would drop the key and leave the old text in place.
permitMime and rejectMime are left untouched rather than sent empty, which would
wipe the list's mime filters.

Edit and delete are gated on ownership: the subscriptions query that builds this
view returns lists the user follows, which is not the same as lists they own.
Non-owners keep the unsubscribe action instead. Deleting destroys the archive
for everyone, so the confirmation says exactly that.
</content>
</entry>
</feed>
