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>
Closes#3.
Adds an "Actions" tab to the repository view, surfacing Forgejo Actions runs and (experimentally) their jobs, steps, and logs.
## What's in this branch
1. ~~**`chore: point ForgejoKit at local checkout for Actions PR`** — temporary `XCLocalSwiftPackageReference` so the feature commit compiles against the unreleased ForgejoKit changes. Drop this commit before merge and replace with `chore: bump ForgejoKit to <new version>` once the ForgejoKit PR ships in a release.~~
**`chore: update ForgejoKit to released version 0.4.0`** + **`chore: add ForgejoKit 0.4.0 to Package.resolved`** — switches from local path override to a remote pin at `secana/ForgejoKit` `0.4.0`.
2. **`feat: add Actions tab to repository view`** — the actual feature.
## UI scope
- New `Actions` tab, shown only when `repository.hasActions == true`.
- Runs list with All / Running / Success / Failed filter, pagination, pull-to-refresh, empty state.
- Run detail with header, metadata (workflow file, event, trigger user, started/duration — zero-Date suppressed), and an experimental Jobs section.
- Job view with collapsible step rows; logs lazy-load on expand via `logCursors`.
- "Open in browser" toolbar item using each run's `html_url`.
## Experimental jobs/steps view
Forgejo's `/api/v1` doesn't expose jobs, steps, or step logs for a run. This PR opts into ForgejoKit's experimental `fetchRunView` (backed by Forgejo's web-UI route) so we can render a GitHub-Actions-style view today. The Jobs section and Steps screen are explicitly labelled "Experimental" in the UI, and a footer notes the output may change between Forgejo versions. Happy to drop this section if you'd rather ship public-API only first.
## Defensive handling
- Forgejo serialises an unset `time.Time` as `0001-01-01T00:00:00Z` (showed up as "Started 56 yrs, 4 mths" / "Duration 493 906h" on a cancelled run). Sanitised display helpers suppress any pre-2000 date.
- The experimental `fetchRunView` resolves Forgejo's `RedirectToLatestAttempt` server-side before POSTing, so attempts with non-zero attempt numbers work. (Without this, Forgejo returns 500 "task with job_id N and attempt 0: resource does not exist".)
## Tests
- Unit: `WorkflowRunFilterTests`, `WorkflowStatusIconTests` (filter mapping, status enum compatibility against Forgejo's documented values, status icon mapping, run helpers, zero-date guard).
- UI: `ActionsUITests` (read-only smoke test).
Co-authored-by: Voislav Vasiljevski <voislav@voioo.cz>
Reviewed-on: https://codeberg.org/secana/Forji/pulls/28
Reviewed-on: https://codeberg.org/secana/Forji/pulls/24
Co-authored-by: Stefan Hausotte <stefan.hausotte@gmx.de>
Co-committed-by: Stefan Hausotte <stefan.hausotte@gmx.de>
Naive implementation for iOS notifications.
Problem: Forgejo does not support push notifications. We need to pull every X minutes for new notifications. The even bigger problem: iOS does not support background polling. So this is more a "as good as possible" but not good approach.
Reviewed-on: https://codeberg.org/secana/Forji/pulls/23
Co-authored-by: Stefan Hausotte <stefan.hausotte@gmx.de>
Co-committed-by: Stefan Hausotte <stefan.hausotte@gmx.de>