`just release` used `sed -i ''`, which is BSD/macOS syntax. In the nix dev
shell GNU sed shadows it and reads `''` as an empty script and the s/.../
expression as a filename, aborting the version bump. Use `-i.bak` plus a
cleanup `rm`, which works on both BSD and GNU sed.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/83
Bump ForgejoKit to 0.8.1, which classifies Forgejo's "Basic authorization
is not allowed while having security keys enrolled" 401 as a dedicated
error instead of "Invalid username or password". Accounts with a passkey
or security key now get told to use a personal access token.
Add the new .basicAuthBlockedBySecurityKey case to the session-restore
error mapping (routed through the auth-category path like other 401s).
Closes#78
Reviewed-on: https://codeberg.org/secana/Forji/pulls/82
The description row in DescriptionEditorSection is a Button whose label
renders a MarkdownPreview when a body exists. The preview enables text
selection and tappable links, which intercepted the tap inside the Button
label so the editor sheet never opened. Existing issues (with a body)
could therefore not have their description edited; only the empty 'None'
placeholder was tappable.
Disable hit testing on the preview so the tap falls through to the Button.
This fixes IssueEditView, PullRequestEditView, and the create flows, which
all share DescriptionEditorSection.
Extend IssueMutatingUITests to edit an issue description and verify it
persists, closing the coverage gap (the prior test only edited the title).
Closes#80
Reviewed-on: https://codeberg.org/secana/Forji/pulls/81
Adds `AttachmentGallery` to show image thumbnails and file rows on
issue/PR
detail views and inline in comment bodies. A `PhotosPicker` button in
the
markdown toolbar lets users attach images when composing or editing; the
selected image is uploaded via the Forgejo assets API and a markdown
reference is inserted into the editor.
Closes#79
test(ui): add AttachmentUITests — upload image and verify display
Adds a combined UI test that uploads a 1×1 PNG to an issue via the
markdown
toolbar, submits a comment, re-enters the issue, and asserts that the
Attachments section and gallery are visible.
- AttachmentGallery: add accessibilityIdentifier("attachment-gallery")
- MarkdownComponents: add -dev_testImageUpload launch-arg branch that
bypasses PhotosPicker and uploads a hardcoded PNG, keeping the test
deterministic without requiring photos in the simulator's library
test(attachment): fix gallery element query and image detection for
Forgejo uploads
Move the attachment-gallery accessibility identifier from the VStack
container
to the horizontal ScrollView, which XCTest reliably exposes as a scroll
view
element. Update the UI test to use
app.scrollViews["attachment-gallery"].
test(attachment): verify multiple image uploads render as distinct
thumbnails
Upload two images from the comment sheet, assert two markdown references
are
inserted, and after reload assert both render as thumbnails in the
gallery.
Confirms the ForEach(imageAttachments) gallery handles multiple
attachments.
test: add unit tests for attachmentMarkdown image vs link syntax
Cover the pure attachmentMarkdown(for:) function directly: images (by
MIME
type and by extension) produce embed syntax, non-image files (log, pdf)
produce link syntax. Faster and more focused than the UI test, which
only
exercised this indirectly.
feat: wrap markdown toolbar icons and add file attachment upload
The markdown toolbar used a horizontal scroll view, so trailing icons
(link,
list, quote, task, attach) were hidden off-screen with no visual cue.
Replace
it with a wrapping flow layout (MarkdownToolbarFlow) so every icon is
always
visible, wrapping to a second row on narrow widths.
Add general file-attachment upload: the attach button is now a menu
offering
"Photo Library" (images via PhotosPicker) and "Choose File" (any file
via
.fileImporter). Picked files are read through a security-scoped resource
and
uploaded with a MIME type derived from the extension; non-image files
insert
link markdown via the existing attachmentMarkdown(for:) path.
**Why.** The Issues and Pull Requests overviews fire one search request per involvement flag (created/assigned/mentioned/review-requested) and merge the results, so the same issue can resurface on a later page — created on page 1, assigned on page 2. The merged multi-instance overview already de-duplicates across pages with a pagination dedupe key, but the single-instance overview never set one, so loading more appended duplicate rows with duplicate Identifiable ids.
**What changed.** `SearchableOverviewView.init` sets the same `dedupeKey` the merged overview sets (`MergedSearchableOverviewView` line 71), routing pages through `PaginationState`'s existing cross-page dedupe. The `dedupeKey` doc comment is updated to match. `hasMore` switches to the contributed-new-items heuristic, which fits combined per-flag sources whose merged page size never matches the request limit.
**Verification.** New `PaginationStateDedupeTests` pin the keyed path: a key repeated across pages is filtered out on the later page, `hasMore` follows `!newItems.isEmpty` when `dedupeKey` is set (including a full page of already-seen keys ending pagination), and a reload resets the seen keys. Full `ForjiTests` suite passes (0 failed, iPhone 17 Pro, iOS 26.5). SwiftLint clean with the repo config.
I grant Stefan Hausotte an irrevocable, worldwide, royalty-free license to use, sublicense, and distribute my contribution, including through Apple's App Store under the project's App Store exception.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/70
When the Forgejo API returns 404 for the issues endpoint (issues
disabled for the repository), display a ContentUnavailableView instead
of surfacing the raw HTTP error alert.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
**Why.** Tapping a push notification bumps `NavigationState.notificationsRefreshTrigger` so the notifications tab reloads on arrival. The single-instance `NotificationsOverviewView` observes the trigger; the merged multi-instance overview never did, so multi-instance users landed on a stale list that still showed the tapped thread as unread until a manual pull-to-refresh.
**What changed.** `MergedNotificationsOverviewView` observes the same trigger with the same `onChange` the single-instance view uses, and reloads.
**Verification.** Full `ForjiTests` suite passes (218 passed / 0 failed, iPhone 17 Pro, iOS 26.5, rebased onto current main). SwiftLint and SwiftFormat clean. The push-open path needs a real APNs round trip, so I couldn't exercise it end-to-end locally; the change mirrors the single-instance wiring line for line.
I grant Stefan Hausotte an irrevocable, worldwide, royalty-free license to use, sublicense, and distribute my contribution, including through Apple's App Store under the project's App Store exception.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/71
The integration seeder gave no visible progress and could hang
indefinitely. A `docker compose exec` for the admin-user step wedged on
a transient Docker-on-macOS flake, and `DockerExec.run` waited on it
with no timeout, blocking the whole UI test suite forever with no output
(stdout was block-buffered, so the existing phase prints never flushed).
- Line-buffer the seeder's stdout so phase progress streams live instead
of being dumped all at once when stdout is a pipe.
- Add numbered "[n/7] <phase> (Ns elapsed)" headers for a clear progress
and timing signal.
- Add a 60s timeout to `docker compose exec` and retry the admin-user
step, so a hung exec fails fast and recovers instead of wedging the
suite.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/69
## What
Notification rows now put the repository name and timestamp on one line ("repo · time") instead of two stacked caption lines, so the list scans like Mail and fits more per screen. The unread dot drops its `glassEffect` for a plain filled circle (cleaner, and avoids GPU glass compositing on a 10pt element inside a scrolling list).
Before: title / repo / time on three lines.
After: title, then "repo · time", with the repo truncating in the middle and the timestamp fixed-width.
Verified: builds clean (Xcode 26, iPhone 17 Pro simulator), SwiftLint passes, confirmed in the simulator with preview data.
---
I grant Stefan Hausotte an irrevocable, worldwide, royalty-free license to use, sublicense, and distribute my contribution, including through Apple's App Store under the project's App Store exception.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/55
## What
The Issues and Pull Requests overviews pinned a centered filter-summary caption ("Open") below the title even in the default state, which just duplicated the filter icon. Now the summary appears only when a non-default state or involvement scope is active, so the default view carries no extra chrome and the strip becomes a clear signal that the list is filtered.
Verified in the iPhone 17 Pro simulator: default state shows no strip and no gap, and the filter icon still reflects state. Builds clean on Xcode 26, SwiftLint passes.
---
I grant Stefan Hausotte an irrevocable, worldwide, royalty-free license to use, sublicense, and distribute my contribution, including through Apple's App Store under the project's App Store exception.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/60
## What
The repository detail's segmented control has four segments (Code, Issues, Pull Requests, Actions). "Pull Requests" dominated the width, squeezed "Code", and truncated on narrower iPhones. Shortening it to "PRs" balances the segments and keeps them legible.
Verified in the iPhone 17 Pro simulator (segments now even); builds clean on Xcode 26, SwiftLint passes.
---
I grant Stefan Hausotte an irrevocable, worldwide, royalty-free license to use, sublicense, and distribute my contribution, including through Apple's App Store under the project's App Store exception.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/59
The trailing Dismiss swipe called the same setNotificationRead path as the
leading Mark Read action, since ForgejoKit's NotificationService only exposes
markAsRead (PATCH to-status=read). Dismiss therefore did nothing distinct and
left the row visible under the All/Read filters, just relabeled.
Drop the redundant action in both NotificationsOverviewView and
MergedNotificationsOverviewView. The leading Mark Read swipe (shown for unread
threads, full-swipe by default) remains the single, honest action.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/64
logout deleted keychain credentials first, then removed the SwiftData instance
with a swallowed `try? modelContext.save()`. If the save failed, the instance
survived with its credentials gone, leaving an account that could not
authenticate. A `nil` default modelContext also let the instance removal be
skipped entirely while credentials were still wiped.
Reorder to delete + save the SwiftData instance first (matching
InstanceListView.deleteInstance) and only delete credentials once that succeeds;
bail out on save failure so the account stays usable. Make modelContext
required to remove the latent orphan path (all call sites already pass one).
Reviewed-on: https://codeberg.org/secana/Forji/pulls/65
IssueEditView and PullRequestEditView save by running several API calls in
sequence (replaceLabels, editIssue, editPullRequest, requestReviewers,
removeReviewers). Forgejo has no transaction, so if a later call fails the
earlier ones stay committed, yet the catch block only showed an error and never
fired onSaved, leaving the detail view with stale data.
On failure, re-fetch the issue/PR and push the authoritative server state via
onSaved (without dismissing, so the user can retry) before surfacing the error.
True rollback is not possible against the REST API; reflecting the real
committed state is the correct remedy.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/66
PaginationState computed hasMore as `fetched.count >= pageSize`, but merged
views return the combined, de-duplicated result of N instances, so the merged
count cleared the single-source threshold and kept hasMore true past the real
end (extra empty fetches). loadMore also appended without de-duping against
already-loaded items, so overlapping involvement queries and shifting pages
could produce duplicate TaggedItem.id entries in the ForEach.
Add an optional dedupeKey to PaginationState that de-duplicates across pages via
a running seen-set, and base hasMore on whether a page contributed new items
when dedupeKey is set. Single-source pagination keeps the page-size heuristic
unchanged. Merged Issues/PRs, Repositories, and Notifications views opt in with
dedupeKey = { $0.id }.
`deleteInstance` cleared only the keychain password, never the token (`logout` deletes both), so swipe-to-delete left a live API token orphaned; this routes both paths through a new `KeychainManager.deleteCredentials` and adds regression tests (186 ForjiTests green).
I grant Stefan Hausotte an irrevocable, worldwide, royalty-free license to use, sublicense, and distribute my contribution, including through Apple's App Store under the project's App Store exception.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/40
Adding the same server and username twice gives two connections one `sourceKey`, and `rehydrate`'s `Dictionary(uniqueKeysWithValues:)` traps on it, crashing every merged overview on construction; this keeps the first source and adds a regression test (185 ForjiTests green).
I grant Stefan Hausotte an irrevocable, worldwide, royalty-free license to use, sublicense, and distribute my contribution, including through Apple's App Store under the project's App Store exception.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/39
Fixes#32. Opening a notification now marks its thread read (via the detail's `.onAppear`) so the tab and icon badges clear; swipe actions unchanged. Adds a UI regression test covering open-to-clear.
I grant Stefan Hausotte an irrevocable, worldwide, royalty-free license to use, sublicense, and distribute my contribution, including through Apple's App Store under the project's App Store exception.
## Summary
This changes the multi-instance fallback map to key bootstrapped connections by account instead of server URL only.
Previously, `connect(instances:)` built a dictionary keyed only by `instance.serverURL`. If two accounts used the same Forgejo instance, `Dictionary(uniqueKeysWithValues:)` could trap on duplicate keys, and fallback lookup could also reuse the wrong account for a failed restore.
## Changes
- Add an account key based on normalized server URL + username.
- Use that key for bootstrapped connection fallback in `MultiInstanceManager`.
- Add a regression test covering two token-auth accounts on the same server URL.
## Verification
- `git diff --check` passes.
- I could not run `xcodebuild` locally because the available Xcode install has not accepted the license on this machine (`xcodebuild` exits before build/test execution).
Co-authored-by: Piotr Durlej <pdurlej@users.noreply.github.com>
Reviewed-on: https://codeberg.org/secana/Forji/pulls/29
Reviewed-by: secana <secana@noreply.codeberg.org>