Commit graph

100 commits

Author SHA1 Message Date
Stefan Hausotte
c5893ff638 fix: clearer login error for security-key (passkey) accounts (#82)
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
2026-06-15 18:49:46 +02:00
Stefan Hausotte
64145f50cc fix: make issue/PR description editable in edit form (#80) (#81)
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
2026-06-15 14:20:24 +02:00
Stefan Hausotte
69f7923a52 chore: formatting and lints 2026-06-15 12:19:00 +02:00
Stefan Hausotte
a7491daffc feat: display and upload image attachments in issues, PRs, and comments
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.
2026-06-15 09:52:26 +02:00
Stefan Hausotte
de5cb025b8 fix: show graceful empty state when repository has pull requests turned off (#77)
Reviewed-on: https://codeberg.org/secana/Forji/pulls/77
2026-06-13 14:12:38 +02:00
systemblue
100d7c412c fix: de-duplicate involved-scope results across pages in single-instance overviews (#70)
**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
2026-06-13 14:01:56 +02:00
secana
617e687e7b fix: show graceful empty state when repository has issues turned off 2026-06-11 21:58:21 +02:00
systemblue
6c053741c5 fix: render 3-digit shorthand label colors instead of falling back to gray 2026-06-11 21:47:08 +02:00
systemblue
c82dedf957 test(issues): add preview for notFound state and extend PaginationState debug init
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 14:07:52 -04:00
systemblue
f39e9f682d fix: show graceful empty state when repository has issues turned off
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>
2026-06-11 13:56:23 -04:00
systemblue
f8a050e484 fix: refresh the merged notifications list when a push notification is opened (#71)
**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
2026-06-10 20:17:57 +02:00
Stefan Hausotte
2a679140e6 test: stream seed progress and add a docker exec timeout (#69)
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
2026-06-09 12:55:45 +02:00
systemBlue
b1942df58d feat: compact notification rows (#55)
## 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
2026-06-09 12:21:05 +02:00
systemBlue
78c91cd2a9 feat: show the overview filter summary only when filters are active (#60)
## 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
2026-06-09 12:18:44 +02:00
systemBlue
ab393d0237 feat: shorten the Pull Requests tab to PRs in repository detail (#59)
## 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
2026-06-09 12:15:52 +02:00
Stefan Hausotte
d580156fa2 ci: add automatic changelog creation 2026-06-04 15:03:47 +02:00
Stefan Hausotte
ad35a6f4f7 docs: add a CHANGELOG.md file
Add a CHANGELOG.md file that uses the "keep a changelog" convention to
make changes to Forji more transparent
2026-06-04 14:44:51 +02:00
Stefan Hausotte
0bcb397805 chore: bump Forji version to 1.5 2026-06-04 14:39:31 +02:00
Stefan Hausotte
330d2bde4f fix: remove redundant notification Dismiss swipe action (#46)
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
2026-06-04 14:08:20 +02:00
Stefan Hausotte
745b7af45b fix: persist instance removal before deleting keychain on logout (#48)
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
2026-06-04 14:08:07 +02:00
Stefan Hausotte
d95ec8ddc5 fix: refresh issue/PR detail after a partial edit failure (#50)
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
2026-06-04 14:07:53 +02:00
secana
8e7f99b6e3 fix: settle merged pagination and drop duplicate rows (#47) 2026-06-04 14:07:32 +02:00
Stefan Hausotte
bec427d7da fix: settle merged pagination and drop duplicate rows (#47)
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 }.
2026-06-04 13:50:40 +02:00
Stefan Hausotte
66fe573cb6 chore: update ForgejoKit dependency to 0.7.0 2026-06-04 13:10:43 +02:00
systemBlue
c2454d3444 docs: add CONTRIBUTING.md (#42) 2026-06-04 13:01:59 +02:00
systemBlue
ed2051ae5d docs: fix typo in README App Store line (#43) 2026-06-04 13:01:41 +02:00
systemBlue
cbd4039e40 feat: add VoiceOver accessibility labels to icon-only buttons (#49) 2026-06-04 13:01:22 +02:00
systemBlue
e9b6be91f5 fix: don't open a duplicate PR when the reviewer request fails (#53) 2026-06-04 13:00:44 +02:00
systemBlue
4f6803cc03 fix: don't crash the background notification poll on an invalid instance URL (#54) 2026-06-04 13:00:25 +02:00
systemBlue
639812348b fix: refresh the merged issue/PR overview after a mutation (#56) 2026-06-04 12:59:59 +02:00
systemBlue
4ef76cf2d7 fix: update the app icon badge in multi-instance mode (#58) 2026-06-04 12:59:43 +02:00
systemBlue
8e56b9d722 feat: make tappable diff lines act as buttons for VoiceOver (#61) 2026-06-04 12:59:17 +02:00
Stefan Hausotte
b0f50eca38 Merge branch 'main' of ssh://codeberg.org/secana/Forji 2026-06-02 18:21:07 +02:00
systemBlue
31d9aeea35 fix: Delete the API token, not just the password, when removing an account (#40)
`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
2026-06-02 18:20:43 +02:00
Stefan Hausotte
8648f40c07 fix: guard that prevents edit of accounts to add the same account 2026-06-02 18:20:12 +02:00
systemBlue
89a0cd0bb2 fix: crash merged overviews when two accounts share a sourceKey (#39)
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
2026-06-02 18:05:53 +02:00
systemBlue
c6dda2cd70 fix: read messages not marked read
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.
2026-06-02 18:01:07 +02:00
Stefan Hausotte
992c628abd refactor: fix linting issues 2026-05-17 21:40:51 +02:00
Stefan Hausotte
6faf3ad986 chore: update ForgejoKit to 0.6.1 2026-05-17 21:40:36 +02:00
pdurlej
a93130ee09 refactor: use ForgejoKit error categories (#31)
Problem
- SessionRestoreError now duplicates HTTP status-code classification that ForgejoKit exposes in 0.6.0.

Change
- Uses ForgejoKit's httpErrorCategory/httpStatusCode for auth and service restore failures.
- Keeps the same user-facing SessionRestoreError outcomes.
- Leaves network and certificate handling unchanged.

Tests
- DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -quiet -project Forji/Forji.xcodeproj -scheme Forji -destination "platform=iOS Simulator,name=iPhone 17 Pro,OS=26.4.1" build-for-testing -only-testing:ForjiTests/SessionRestoreErrorTests
- DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -quiet -project Forji/Forji.xcodeproj -scheme Forji -destination "platform=iOS Simulator,name=iPhone 17 Pro,OS=26.4.1" test-without-building -only-testing:ForjiTests/SessionRestoreErrorTests

Co-authored-by: Piotr Durlej <pdurlej@users.noreply.github.com>
Reviewed-on: https://codeberg.org/secana/Forji/pulls/31
2026-05-17 21:29:12 +02:00
Stefan Hausotte
075669d87e test: fix warning in test 2026-05-12 18:20:09 +02:00
Stefan Hausotte
fd85a211f6 chore: update ForgejoKit to 0.6.0 2026-05-12 18:19:58 +02:00
pdurlej
5e22431d11 fix: preserve token restore error context (#30)
Problem
- Token-only session restore currently reports every token validation failure as an expired/revoked token, including permission, server, network, and invalid-response failures.

Change
- Preserves token validation error context for token-only instances.
- Maps auth/service/network failures to more specific user-facing restore errors.
- Leaves password fallback behavior unchanged for password-based instances.

Tests
- DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -quiet -project Forji/Forji.xcodeproj -scheme Forji -destination "platform=iOS Simulator,name=iPhone 17 Pro,OS=26.4.1" build-for-testing -only-testing:ForjiTests/SessionRestoreErrorTests
- DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -quiet -project Forji/Forji.xcodeproj -scheme Forji -destination "platform=iOS Simulator,name=iPhone 17 Pro,OS=26.4.1" test-without-building -only-testing:ForjiTests/SessionRestoreErrorTests

Co-authored-by: Piotr Durlej <pdurlej@users.noreply.github.com>
Reviewed-on: https://codeberg.org/secana/Forji/pulls/30
Reviewed-by: secana <secana@noreply.codeberg.org>
2026-05-12 17:45:02 +02:00
Stefan Hausotte
db4e54db6b chore: add "just sim-update" command
Updates the iOS simulators to the latest version in XCode
2026-05-10 11:01:40 +02:00
pdurlej
0b815335a9 fix: key multi-instance fallback by account (#29)
## 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>
2026-05-10 10:39:02 +02:00
Stefan Hausotte
f7eba701e2 chore: update Forji version 2026-05-07 19:28:42 +02:00
Stefan Hausotte
57bde0934f refactor: small name changes 2026-05-07 19:17:49 +02:00
Stefan Hausotte
2e07f6da4d test: make tests more stable 2026-05-07 19:07:40 +02:00
Stefan Hausotte
ea9c9825e0 test: fix race condition in tests 2026-05-07 18:21:09 +02:00
Voislav Vasiljevski
3d3de81f2b [Draft] feat: Actions tab — depends on ForgejoKit#1 (#28)
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
2026-05-07 18:06:17 +02:00