From 5adc0102ebe4bbeeb718045da7d76a107706b9c5 Mon Sep 17 00:00:00 2001 From: Stefan Hausotte Date: Sat, 28 Feb 2026 21:08:13 +0100 Subject: [PATCH] feat: inital commit Forji is an iOS app to interact with a Forgejo instance --- Forji/Forji.xcodeproj/project.pbxproj | 683 +++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/swiftpm/Package.resolved | 51 ++ .../UserInterfaceState.xcuserstate | Bin 0 -> 157339 bytes .../xcshareddata/xcschemes/Forji.xcscheme | 102 +++ .../xcschemes/xcschememanagement.plist | 22 + Forji/Forji/App/ContentView.swift | 100 +++ Forji/Forji/App/ForjiApp.swift | 25 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/AppIcon.png | Bin 0 -> 57766 bytes .../AppIcon.appiconset/Contents.json | 36 + Forji/Forji/Assets.xcassets/Contents.json | 6 + Forji/Forji/Helpers/DebouncedSearch.swift | 32 + Forji/Forji/Helpers/DiffParserSwiftUI.swift | 22 + Forji/Forji/Helpers/LanguageColor.swift | 10 + Forji/Forji/Helpers/MermaidParser.swift | 60 ++ Forji/Forji/Helpers/MetadataLoader.swift | 18 + Forji/Forji/Helpers/PRStatusStyle.swift | 33 + Forji/Forji/Helpers/PaginationState.swift | 92 +++ Forji/Forji/Helpers/PreviewData.swift | 350 +++++++++ Forji/Forji/Models/ForgejoInstance.swift | 24 + Forji/Forji/Models/ReviewState.swift | 53 ++ Forji/Forji/Models/State.swift | 62 ++ .../Services/AuthenticationService.swift | 131 ++++ Forji/Forji/Services/KeychainManager.swift | 114 +++ Forji/Forji/Views/CommentSheet.swift | 71 ++ Forji/Forji/Views/CommentView.swift | 118 +++ Forji/Forji/Views/CommitDetailView.swift | 104 +++ Forji/Forji/Views/CommitHistoryView.swift | 220 ++++++ Forji/Forji/Views/DiffView.swift | 151 ++++ Forji/Forji/Views/DisplaySections.swift | 58 ++ Forji/Forji/Views/ErrorAlert.swift | 22 + Forji/Forji/Views/FileViewerView.swift | 337 +++++++++ Forji/Forji/Views/FloatingButtons.swift | 45 ++ Forji/Forji/Views/FlowLayout.swift | 46 ++ Forji/Forji/Views/HomeView.swift | 180 +++++ Forji/Forji/Views/InlineCommentSheet.swift | 139 ++++ Forji/Forji/Views/InstanceFormView.swift | 267 +++++++ Forji/Forji/Views/InstanceListView.swift | 181 +++++ Forji/Forji/Views/IssueCreateView.swift | 160 ++++ Forji/Forji/Views/IssueDetailView.swift | 300 ++++++++ Forji/Forji/Views/IssueEditView.swift | 169 +++++ Forji/Forji/Views/IssueLabelView.swift | 59 ++ Forji/Forji/Views/IssueListView.swift | 199 +++++ Forji/Forji/Views/IssuesOverviewView.swift | 59 ++ Forji/Forji/Views/ListHelpers.swift | 47 ++ Forji/Forji/Views/MarkdownComponents.swift | 218 ++++++ .../Forji/Views/MentionableEditorField.swift | 88 +++ Forji/Forji/Views/MermaidWebView.swift | 132 ++++ Forji/Forji/Views/MetadataPickers.swift | 330 ++++++++ .../Views/NotificationsOverviewView.swift | 272 +++++++ Forji/Forji/Views/PullRequestCreateView.swift | 250 ++++++ Forji/Forji/Views/PullRequestDetailView.swift | 605 +++++++++++++++ Forji/Forji/Views/PullRequestEditView.swift | 216 ++++++ Forji/Forji/Views/PullRequestListView.swift | 206 +++++ Forji/Forji/Views/PullRequestMergeView.swift | 111 +++ .../Forji/Views/PullRequestReviewSheet.swift | 246 ++++++ .../Views/PullRequestsOverviewView.swift | 107 +++ Forji/Forji/Views/RepositoryDetailView.swift | 566 ++++++++++++++ Forji/Forji/Views/RepositoryListView.swift | 334 ++++++++ Forji/Forji/Views/RepositoryPickerView.swift | 183 +++++ Forji/Forji/Views/ReviewSummaryView.swift | 85 +++ .../Forji/Views/SearchableOverviewView.swift | 316 ++++++++ Forji/Forji/Views/StateAccent.swift | 22 + Forji/ForjiTests/CommentSheetTests.swift | 36 + Forji/ForjiTests/ForjiTests.swift | 26 + Forji/ForjiTests/KeychainManagerTests.swift | 136 ++++ Forji/ForjiTests/LanguageColorTests.swift | 48 ++ .../ForjiTests/MarkdownComponentsTests.swift | 32 + Forji/ForjiTests/MermaidParserTests.swift | 188 +++++ Forji/ForjiTests/PaginationStateTests.swift | 387 ++++++++++ Forji/ForjiTests/StateTests.swift | 136 ++++ Forji/ForjiUITests/CommitHistoryUITests.swift | 47 ++ .../ForgejoReadOnlyUITestBase.swift | 123 +++ Forji/ForjiUITests/ForgejoUITestBase.swift | 62 ++ .../ForjiUITestsLaunchTests.swift | 31 + Forji/ForjiUITests/HomeScreenUITests.swift | 34 + Forji/ForjiUITests/IssueMutatingUITests.swift | 88 +++ Forji/ForjiUITests/IssueUITests.swift | 123 +++ Forji/ForjiUITests/LoginUITests.swift | 33 + Forji/ForjiUITests/NotificationsUITests.swift | 52 ++ .../OverviewCreateMutatingUITests.swift | 49 ++ .../ForjiUITests/OverviewCreateUITests.swift | 75 ++ Forji/ForjiUITests/PaginationUITests.swift | 95 +++ Forji/ForjiUITests/PermissionUITests.swift | 80 ++ .../PullRequestMutatingUITests.swift | 139 ++++ Forji/ForjiUITests/PullRequestUITests.swift | 153 ++++ .../RepositoryMutatingUITests.swift | 73 ++ Forji/ForjiUITests/RepositoryUITests.swift | 86 +++ Forji/ForjiUITests/UITestNavigating.swift | 66 ++ Forji/Info.plist | 11 + LICENSE | 716 ++++++++++++++++++ README.md | 115 +++ flake.lock | 61 ++ flake.nix | 24 + integration/.forgejo-seed-hash | 1 + integration/.forgejo-seed-snapshot.tar.gz | Bin 0 -> 259960 bytes integration/docker-compose.yml | 38 + integration/setup.sh | 207 +++++ justfile | 257 +++++++ screenshots/01_repositories.png | Bin 0 -> 46073 bytes screenshots/02_code_browser.png | Bin 0 -> 52764 bytes screenshots/03_pull_request.png | Bin 0 -> 66166 bytes screenshots/04_notifications.png | Bin 0 -> 75796 bytes screenshots/app_icon.png | Bin 0 -> 57766 bytes 105 files changed, 13091 insertions(+) create mode 100644 Forji/Forji.xcodeproj/project.pbxproj create mode 100644 Forji/Forji.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Forji/Forji.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Forji/Forji.xcodeproj/project.xcworkspace/xcuserdata/hausi.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 Forji/Forji.xcodeproj/xcshareddata/xcschemes/Forji.xcscheme create mode 100644 Forji/Forji.xcodeproj/xcuserdata/hausi.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 Forji/Forji/App/ContentView.swift create mode 100644 Forji/Forji/App/ForjiApp.swift create mode 100644 Forji/Forji/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Forji/Forji/Assets.xcassets/AppIcon.appiconset/AppIcon.png create mode 100644 Forji/Forji/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Forji/Forji/Assets.xcassets/Contents.json create mode 100644 Forji/Forji/Helpers/DebouncedSearch.swift create mode 100644 Forji/Forji/Helpers/DiffParserSwiftUI.swift create mode 100644 Forji/Forji/Helpers/LanguageColor.swift create mode 100644 Forji/Forji/Helpers/MermaidParser.swift create mode 100644 Forji/Forji/Helpers/MetadataLoader.swift create mode 100644 Forji/Forji/Helpers/PRStatusStyle.swift create mode 100644 Forji/Forji/Helpers/PaginationState.swift create mode 100644 Forji/Forji/Helpers/PreviewData.swift create mode 100644 Forji/Forji/Models/ForgejoInstance.swift create mode 100644 Forji/Forji/Models/ReviewState.swift create mode 100644 Forji/Forji/Models/State.swift create mode 100644 Forji/Forji/Services/AuthenticationService.swift create mode 100644 Forji/Forji/Services/KeychainManager.swift create mode 100644 Forji/Forji/Views/CommentSheet.swift create mode 100644 Forji/Forji/Views/CommentView.swift create mode 100644 Forji/Forji/Views/CommitDetailView.swift create mode 100644 Forji/Forji/Views/CommitHistoryView.swift create mode 100644 Forji/Forji/Views/DiffView.swift create mode 100644 Forji/Forji/Views/DisplaySections.swift create mode 100644 Forji/Forji/Views/ErrorAlert.swift create mode 100644 Forji/Forji/Views/FileViewerView.swift create mode 100644 Forji/Forji/Views/FloatingButtons.swift create mode 100644 Forji/Forji/Views/FlowLayout.swift create mode 100644 Forji/Forji/Views/HomeView.swift create mode 100644 Forji/Forji/Views/InlineCommentSheet.swift create mode 100644 Forji/Forji/Views/InstanceFormView.swift create mode 100644 Forji/Forji/Views/InstanceListView.swift create mode 100644 Forji/Forji/Views/IssueCreateView.swift create mode 100644 Forji/Forji/Views/IssueDetailView.swift create mode 100644 Forji/Forji/Views/IssueEditView.swift create mode 100644 Forji/Forji/Views/IssueLabelView.swift create mode 100644 Forji/Forji/Views/IssueListView.swift create mode 100644 Forji/Forji/Views/IssuesOverviewView.swift create mode 100644 Forji/Forji/Views/ListHelpers.swift create mode 100644 Forji/Forji/Views/MarkdownComponents.swift create mode 100644 Forji/Forji/Views/MentionableEditorField.swift create mode 100644 Forji/Forji/Views/MermaidWebView.swift create mode 100644 Forji/Forji/Views/MetadataPickers.swift create mode 100644 Forji/Forji/Views/NotificationsOverviewView.swift create mode 100644 Forji/Forji/Views/PullRequestCreateView.swift create mode 100644 Forji/Forji/Views/PullRequestDetailView.swift create mode 100644 Forji/Forji/Views/PullRequestEditView.swift create mode 100644 Forji/Forji/Views/PullRequestListView.swift create mode 100644 Forji/Forji/Views/PullRequestMergeView.swift create mode 100644 Forji/Forji/Views/PullRequestReviewSheet.swift create mode 100644 Forji/Forji/Views/PullRequestsOverviewView.swift create mode 100644 Forji/Forji/Views/RepositoryDetailView.swift create mode 100644 Forji/Forji/Views/RepositoryListView.swift create mode 100644 Forji/Forji/Views/RepositoryPickerView.swift create mode 100644 Forji/Forji/Views/ReviewSummaryView.swift create mode 100644 Forji/Forji/Views/SearchableOverviewView.swift create mode 100644 Forji/Forji/Views/StateAccent.swift create mode 100644 Forji/ForjiTests/CommentSheetTests.swift create mode 100644 Forji/ForjiTests/ForjiTests.swift create mode 100644 Forji/ForjiTests/KeychainManagerTests.swift create mode 100644 Forji/ForjiTests/LanguageColorTests.swift create mode 100644 Forji/ForjiTests/MarkdownComponentsTests.swift create mode 100644 Forji/ForjiTests/MermaidParserTests.swift create mode 100644 Forji/ForjiTests/PaginationStateTests.swift create mode 100644 Forji/ForjiTests/StateTests.swift create mode 100644 Forji/ForjiUITests/CommitHistoryUITests.swift create mode 100644 Forji/ForjiUITests/ForgejoReadOnlyUITestBase.swift create mode 100644 Forji/ForjiUITests/ForgejoUITestBase.swift create mode 100644 Forji/ForjiUITests/ForjiUITestsLaunchTests.swift create mode 100644 Forji/ForjiUITests/HomeScreenUITests.swift create mode 100644 Forji/ForjiUITests/IssueMutatingUITests.swift create mode 100644 Forji/ForjiUITests/IssueUITests.swift create mode 100644 Forji/ForjiUITests/LoginUITests.swift create mode 100644 Forji/ForjiUITests/NotificationsUITests.swift create mode 100644 Forji/ForjiUITests/OverviewCreateMutatingUITests.swift create mode 100644 Forji/ForjiUITests/OverviewCreateUITests.swift create mode 100644 Forji/ForjiUITests/PaginationUITests.swift create mode 100644 Forji/ForjiUITests/PermissionUITests.swift create mode 100644 Forji/ForjiUITests/PullRequestMutatingUITests.swift create mode 100644 Forji/ForjiUITests/PullRequestUITests.swift create mode 100644 Forji/ForjiUITests/RepositoryMutatingUITests.swift create mode 100644 Forji/ForjiUITests/RepositoryUITests.swift create mode 100644 Forji/ForjiUITests/UITestNavigating.swift create mode 100644 Forji/Info.plist create mode 100644 LICENSE create mode 100644 README.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 integration/.forgejo-seed-hash create mode 100644 integration/.forgejo-seed-snapshot.tar.gz create mode 100644 integration/docker-compose.yml create mode 100755 integration/setup.sh create mode 100644 justfile create mode 100644 screenshots/01_repositories.png create mode 100644 screenshots/02_code_browser.png create mode 100644 screenshots/03_pull_request.png create mode 100644 screenshots/04_notifications.png create mode 100644 screenshots/app_icon.png diff --git a/Forji/Forji.xcodeproj/project.pbxproj b/Forji/Forji.xcodeproj/project.pbxproj new file mode 100644 index 0000000..efef86d --- /dev/null +++ b/Forji/Forji.xcodeproj/project.pbxproj @@ -0,0 +1,683 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + DE00000000000001000000AA /* ForgejoKit in Frameworks */ = {isa = PBXBuildFile; productRef = DE00000000000002000000AA /* ForgejoKit */; }; + DE00000000000003000000AA /* ForgejoKit in Frameworks */ = {isa = PBXBuildFile; productRef = DE00000000000004000000AA /* ForgejoKit */; }; + DE00000000000006000000BB /* AppIntents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE00000000000007000000BB /* AppIntents.framework */; }; + DE00000000000008000000BB /* AppIntents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE00000000000007000000BB /* AppIntents.framework */; }; + DE00000000000009000000BB /* AppIntents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE00000000000007000000BB /* AppIntents.framework */; }; + DEC49F6E2F3D023400E7DD54 /* Textual in Frameworks */ = {isa = PBXBuildFile; productRef = DEC49F6D2F3D023400E7DD54 /* Textual */; }; + DEC49F832F3D173F00E7DD54 /* HighlightSwift in Frameworks */ = {isa = PBXBuildFile; productRef = DEC49F822F3D173F00E7DD54 /* HighlightSwift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + DEC49F312F3CE05400E7DD54 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DEC49F192F3CE05200E7DD54 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DEC49F202F3CE05200E7DD54; + remoteInfo = Forji; + }; + DEC49F3B2F3CE05400E7DD54 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DEC49F192F3CE05200E7DD54 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DEC49F202F3CE05200E7DD54; + remoteInfo = Forji; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + DE00000000000007000000BB /* AppIntents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppIntents.framework; path = System/Library/Frameworks/AppIntents.framework; sourceTree = SDKROOT; }; + DEC49F212F3CE05200E7DD54 /* Forji.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Forji.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DEC49F302F3CE05400E7DD54 /* ForjiTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ForjiTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DEC49F3A2F3CE05400E7DD54 /* ForjiUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ForjiUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + DEC49F232F3CE05200E7DD54 /* Forji */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Forji; + sourceTree = ""; + }; + DEC49F332F3CE05400E7DD54 /* ForjiTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = ForjiTests; + sourceTree = ""; + }; + DEC49F3D2F3CE05400E7DD54 /* ForjiUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = ForjiUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + DEC49F1E2F3CE05200E7DD54 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DE00000000000001000000AA /* ForgejoKit in Frameworks */, + DE00000000000006000000BB /* AppIntents.framework in Frameworks */, + DEC49F6E2F3D023400E7DD54 /* Textual in Frameworks */, + DEC49F832F3D173F00E7DD54 /* HighlightSwift in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEC49F2D2F3CE05400E7DD54 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DE00000000000003000000AA /* ForgejoKit in Frameworks */, + DE00000000000008000000BB /* AppIntents.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEC49F372F3CE05400E7DD54 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DE00000000000009000000BB /* AppIntents.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + DEC49F182F3CE05200E7DD54 = { + isa = PBXGroup; + children = ( + DEC49F232F3CE05200E7DD54 /* Forji */, + DEC49F332F3CE05400E7DD54 /* ForjiTests */, + DEC49F3D2F3CE05400E7DD54 /* ForjiUITests */, + DEC49F6C2F3D023400E7DD54 /* Frameworks */, + DEC49F222F3CE05200E7DD54 /* Products */, + ); + sourceTree = ""; + }; + DEC49F222F3CE05200E7DD54 /* Products */ = { + isa = PBXGroup; + children = ( + DEC49F212F3CE05200E7DD54 /* Forji.app */, + DEC49F302F3CE05400E7DD54 /* ForjiTests.xctest */, + DEC49F3A2F3CE05400E7DD54 /* ForjiUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + DEC49F6C2F3D023400E7DD54 /* Frameworks */ = { + isa = PBXGroup; + children = ( + DE00000000000007000000BB /* AppIntents.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + DEC49F202F3CE05200E7DD54 /* Forji */ = { + isa = PBXNativeTarget; + buildConfigurationList = DEC49F442F3CE05400E7DD54 /* Build configuration list for PBXNativeTarget "Forji" */; + buildPhases = ( + DEC49F1D2F3CE05200E7DD54 /* Sources */, + DEC49F1E2F3CE05200E7DD54 /* Frameworks */, + DEC49F1F2F3CE05200E7DD54 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + DEC49F232F3CE05200E7DD54 /* Forji */, + ); + name = Forji; + packageProductDependencies = ( + DE00000000000002000000AA /* ForgejoKit */, + DEC49F6D2F3D023400E7DD54 /* Textual */, + DEC49F822F3D173F00E7DD54 /* HighlightSwift */, + ); + productName = Forji; + productReference = DEC49F212F3CE05200E7DD54 /* Forji.app */; + productType = "com.apple.product-type.application"; + }; + DEC49F2F2F3CE05400E7DD54 /* ForjiTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = DEC49F472F3CE05400E7DD54 /* Build configuration list for PBXNativeTarget "ForjiTests" */; + buildPhases = ( + DEC49F2C2F3CE05400E7DD54 /* Sources */, + DEC49F2D2F3CE05400E7DD54 /* Frameworks */, + DEC49F2E2F3CE05400E7DD54 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + DEC49F322F3CE05400E7DD54 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + DEC49F332F3CE05400E7DD54 /* ForjiTests */, + ); + name = ForjiTests; + packageProductDependencies = ( + DE00000000000004000000AA /* ForgejoKit */, + ); + productName = ForjiTests; + productReference = DEC49F302F3CE05400E7DD54 /* ForjiTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + DEC49F392F3CE05400E7DD54 /* ForjiUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = DEC49F4A2F3CE05400E7DD54 /* Build configuration list for PBXNativeTarget "ForjiUITests" */; + buildPhases = ( + DEC49F362F3CE05400E7DD54 /* Sources */, + DEC49F372F3CE05400E7DD54 /* Frameworks */, + DEC49F382F3CE05400E7DD54 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + DEC49F3C2F3CE05400E7DD54 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + DEC49F3D2F3CE05400E7DD54 /* ForjiUITests */, + ); + name = ForjiUITests; + packageProductDependencies = ( + ); + productName = ForjiUITests; + productReference = DEC49F3A2F3CE05400E7DD54 /* ForjiUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + DEC49F192F3CE05200E7DD54 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2620; + LastUpgradeCheck = 2620; + TargetAttributes = { + DEC49F202F3CE05200E7DD54 = { + CreatedOnToolsVersion = 26.2; + }; + DEC49F2F2F3CE05400E7DD54 = { + CreatedOnToolsVersion = 26.2; + TestTargetID = DEC49F202F3CE05200E7DD54; + }; + DEC49F392F3CE05400E7DD54 = { + CreatedOnToolsVersion = 26.2; + TestTargetID = DEC49F202F3CE05200E7DD54; + }; + }; + }; + buildConfigurationList = DEC49F1C2F3CE05200E7DD54 /* Build configuration list for PBXProject "Forji" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = DEC49F182F3CE05200E7DD54; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + DE00000000000005000000AA /* XCRemoteSwiftPackageReference "ForgejoKit" */, + DEC49F6B2F3D00C700E7DD54 /* XCRemoteSwiftPackageReference "textual" */, + DEC49F812F3D173F00E7DD54 /* XCRemoteSwiftPackageReference "HighlightSwift" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = DEC49F222F3CE05200E7DD54 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + DEC49F202F3CE05200E7DD54 /* Forji */, + DEC49F2F2F3CE05400E7DD54 /* ForjiTests */, + DEC49F392F3CE05400E7DD54 /* ForjiUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + DEC49F1F2F3CE05200E7DD54 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEC49F2E2F3CE05400E7DD54 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEC49F382F3CE05400E7DD54 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + DEC49F1D2F3CE05200E7DD54 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEC49F2C2F3CE05400E7DD54 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DEC49F362F3CE05400E7DD54 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + DEC49F322F3CE05400E7DD54 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DEC49F202F3CE05200E7DD54 /* Forji */; + targetProxy = DEC49F312F3CE05400E7DD54 /* PBXContainerItemProxy */; + }; + DEC49F3C2F3CE05400E7DD54 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DEC49F202F3CE05200E7DD54 /* Forji */; + targetProxy = DEC49F3B2F3CE05400E7DD54 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + DEC49F422F3CE05400E7DD54 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = RVT2M7QTD4; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + DEC49F432F3CE05400E7DD54 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = RVT2M7QTD4; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + DEC49F452F3CE05400E7DD54 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = RVT2M7QTD4; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Forji; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = de.hausotte.Forji; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_CONST_VALUE_PROTOCOLS = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + DEC49F462F3CE05400E7DD54 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = RVT2M7QTD4; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Forji; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = de.hausotte.Forji; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_CONST_VALUE_PROTOCOLS = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + DEC49F482F3CE05400E7DD54 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = RVT2M7QTD4; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = de.hausotte.ForjiTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_CONST_VALUE_PROTOCOLS = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Forji.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Forji"; + }; + name = Debug; + }; + DEC49F492F3CE05400E7DD54 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = RVT2M7QTD4; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = de.hausotte.ForjiTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_CONST_VALUE_PROTOCOLS = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Forji.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Forji"; + }; + name = Release; + }; + DEC49F4B2F3CE05400E7DD54 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = RVT2M7QTD4; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = de.hausotte.ForjiUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_CONST_VALUE_PROTOCOLS = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Forji; + }; + name = Debug; + }; + DEC49F4C2F3CE05400E7DD54 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = RVT2M7QTD4; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = de.hausotte.ForjiUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_CONST_VALUE_PROTOCOLS = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Forji; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + DEC49F1C2F3CE05200E7DD54 /* Build configuration list for PBXProject "Forji" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DEC49F422F3CE05400E7DD54 /* Debug */, + DEC49F432F3CE05400E7DD54 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DEC49F442F3CE05400E7DD54 /* Build configuration list for PBXNativeTarget "Forji" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DEC49F452F3CE05400E7DD54 /* Debug */, + DEC49F462F3CE05400E7DD54 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DEC49F472F3CE05400E7DD54 /* Build configuration list for PBXNativeTarget "ForjiTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DEC49F482F3CE05400E7DD54 /* Debug */, + DEC49F492F3CE05400E7DD54 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DEC49F4A2F3CE05400E7DD54 /* Build configuration list for PBXNativeTarget "ForjiUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DEC49F4B2F3CE05400E7DD54 /* Debug */, + DEC49F4C2F3CE05400E7DD54 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + DE00000000000005000000AA /* XCRemoteSwiftPackageReference "ForgejoKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://codeberg.org/secana/ForgejoKit.git"; + requirement = { + kind = exactVersion; + version = 0.1.0; + }; + }; + DEC49F6B2F3D00C700E7DD54 /* XCRemoteSwiftPackageReference "textual" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/gonzalezreal/textual"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.3.1; + }; + }; + DEC49F812F3D173F00E7DD54 /* XCRemoteSwiftPackageReference "HighlightSwift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/appstefan/HighlightSwift.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.9; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + DE00000000000002000000AA /* ForgejoKit */ = { + isa = XCSwiftPackageProductDependency; + package = DE00000000000005000000AA /* XCRemoteSwiftPackageReference "ForgejoKit" */; + productName = ForgejoKit; + }; + DE00000000000004000000AA /* ForgejoKit */ = { + isa = XCSwiftPackageProductDependency; + package = DE00000000000005000000AA /* XCRemoteSwiftPackageReference "ForgejoKit" */; + productName = ForgejoKit; + }; + DEC49F6D2F3D023400E7DD54 /* Textual */ = { + isa = XCSwiftPackageProductDependency; + package = DEC49F6B2F3D00C700E7DD54 /* XCRemoteSwiftPackageReference "textual" */; + productName = Textual; + }; + DEC49F822F3D173F00E7DD54 /* HighlightSwift */ = { + isa = XCSwiftPackageProductDependency; + package = DEC49F812F3D173F00E7DD54 /* XCRemoteSwiftPackageReference "HighlightSwift" */; + productName = HighlightSwift; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = DEC49F192F3CE05200E7DD54 /* Project object */; +} diff --git a/Forji/Forji.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Forji/Forji.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Forji/Forji.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Forji/Forji.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Forji/Forji.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..297298f --- /dev/null +++ b/Forji/Forji.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,51 @@ +{ + "originHash" : "931ec0beeaf4e6a5eaa0afab6f815f97bc126bda7a2c9f001c10de58585e766f", + "pins" : [ + { + "identity" : "forgejokit", + "kind" : "remoteSourceControl", + "location" : "https://codeberg.org/secana/ForgejoKit.git", + "state" : { + "revision" : "897a8ebeddfc97444c27752df8e99eda589eeaba", + "version" : "0.1.0" + } + }, + { + "identity" : "highlightswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/appstefan/HighlightSwift.git", + "state" : { + "revision" : "784ca3ccfc8a2cf724fbb2f06cb6ec3329424da3", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "5a3825302b1a0d744183200915a47b508c828e6f", + "version" : "1.3.2" + } + }, + { + "identity" : "swiftui-math", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/swiftui-math", + "state" : { + "revision" : "0b5c2cfaaec8d6193db206f675048eeb5ce95f71", + "version" : "0.1.0" + } + }, + { + "identity" : "textual", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/textual", + "state" : { + "revision" : "5b06b811c0f5313b6b84bbef98c635a630638c38", + "version" : "0.3.1" + } + } + ], + "version" : 3 +} diff --git a/Forji/Forji.xcodeproj/project.xcworkspace/xcuserdata/hausi.xcuserdatad/UserInterfaceState.xcuserstate b/Forji/Forji.xcodeproj/project.xcworkspace/xcuserdata/hausi.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..abe45ef2b79db431b5334884281438f47b89fb3c GIT binary patch literal 157339 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

p!ieQ}@)~QjN0gS;qHLO!( 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Forji/Forji.xcodeproj/xcuserdata/hausi.xcuserdatad/xcschemes/xcschememanagement.plist b/Forji/Forji.xcodeproj/xcuserdata/hausi.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..b00ac61 --- /dev/null +++ b/Forji/Forji.xcodeproj/xcuserdata/hausi.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,22 @@ + + + + + SchemeUserState + + Forji.xcscheme_^#shared#^_ + + orderHint + 0 + + + SuppressBuildableAutocreation + + DEC49F202F3CE05200E7DD54 + + primary + + + + + diff --git a/Forji/Forji/App/ContentView.swift b/Forji/Forji/App/ContentView.swift new file mode 100644 index 0000000..2155f17 --- /dev/null +++ b/Forji/Forji/App/ContentView.swift @@ -0,0 +1,100 @@ +import ForgejoKit +import SwiftData +import SwiftUI + +enum AppAppearance: Int, CaseIterable { + case system = 0 + case light = 1 + case dark = 2 + + var colorScheme: ColorScheme? { + switch self { + case .system: nil + case .light: .light + case .dark: .dark + } + } +} + +struct ContentView: View { + @AppStorage("appearance") private var appearance: AppAppearance = .system + @State private var authService = AuthenticationService() + @Query(filter: #Predicate { $0.isDefault }) private var defaultInstances: [ForgejoInstance] + @State private var hasAttemptedAutoLogin = false + @Environment(\.modelContext) private var modelContext + + // Dev auto-login credentials, set via launch arguments for development and UI integration tests + #if DEBUG + @AppStorage("dev_serverURL") private var devServerURL = "" + @AppStorage("dev_username") private var devUsername = "" + @AppStorage("dev_password") private var devPassword = "" + @AppStorage("dev_skipAutoLogin") private var devSkipAutoLogin = false + #endif + + var body: some View { + Group { + if authService.isAuthenticated, authService.client != nil { + HomeView(authService: authService) + } else if !hasAttemptedAutoLogin { + ProgressView("Connecting...") + .task { await attemptAutoLogin() } + } else { + InstanceListView(authService: authService) + } + } + .preferredColorScheme(appearance.colorScheme) + } + + private func attemptAutoLogin() async { + #if DEBUG + if devSkipAutoLogin { + hasAttemptedAutoLogin = true + return + } + // Dev auto-login: launch arguments set dev_ UserDefaults + if !devServerURL.isEmpty, !devUsername.isEmpty, !devPassword.isEmpty { + do { + try await authService.login(serverURL: devServerURL, username: devUsername, password: devPassword) + let descriptor = FetchDescriptor(predicate: #Predicate { + $0.serverURL == devServerURL && $0.username == devUsername + }) + let existing = (try? modelContext.fetch(descriptor)) ?? [] + let instance: ForgejoInstance + if let found = existing.first { + instance = found + } else { + instance = ForgejoInstance(serverURL: devServerURL, username: devUsername, name: "Dev") + modelContext.insert(instance) + } + authService.currentInstance = instance + hasAttemptedAutoLogin = true + return + } catch { + // Fall through to normal flow + } + } + #endif + + guard let defaultInstance = defaultInstances.first else { + hasAttemptedAutoLogin = true + return + } + do { + try await authService.restoreSession(instance: defaultInstance) + defaultInstance.lastUsed = Date() + do { + try modelContext.save() + } catch { + assertionFailure("SwiftData save failed: \(error)") + } + } catch { + // Auto-login failed — fall through to instance list + } + hasAttemptedAutoLogin = true + } +} + +#Preview { + ContentView() + .modelContainer(for: ForgejoInstance.self, inMemory: true) +} diff --git a/Forji/Forji/App/ForjiApp.swift b/Forji/Forji/App/ForjiApp.swift new file mode 100644 index 0000000..10820b0 --- /dev/null +++ b/Forji/Forji/App/ForjiApp.swift @@ -0,0 +1,25 @@ +import SwiftData +import SwiftUI + +@main +struct ForjiApp: App { + var sharedModelContainer: ModelContainer = { + let schema = Schema([ + ForgejoInstance.self, + ]) + let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) + + do { + return try ModelContainer(for: schema, configurations: [modelConfiguration]) + } catch { + fatalError("Could not create ModelContainer: \(error)") + } + }() + + var body: some Scene { + WindowGroup { + ContentView() + } + .modelContainer(sharedModelContainer) + } +} diff --git a/Forji/Forji/Assets.xcassets/AccentColor.colorset/Contents.json b/Forji/Forji/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Forji/Forji/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Forji/Forji/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/Forji/Forji/Assets.xcassets/AppIcon.appiconset/AppIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..296ef3756edc8f8fe4be3674a52c9ced71e93124 GIT binary patch literal 57766 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

7K& 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 + let action: () async -> Void + + func body(content: Content) -> some View { + content + .onChange(of: searchText) { + searchTask?.cancel() + searchTask = Task { + do { + try await Task.sleep(for: .milliseconds(300)) + } catch { + return + } + await action() + } + } + } +} + +extension View { + func debouncedSearch( + text: Binding, + task: Binding?>, + action: @escaping () async -> Void, + ) -> some View { + modifier(DebouncedSearchModifier(searchText: text, searchTask: task, action: action)) + } +} diff --git a/Forji/Forji/Helpers/DiffParserSwiftUI.swift b/Forji/Forji/Helpers/DiffParserSwiftUI.swift new file mode 100644 index 0000000..da18064 --- /dev/null +++ b/Forji/Forji/Helpers/DiffParserSwiftUI.swift @@ -0,0 +1,22 @@ +import ForgejoKit +import SwiftUI + +extension DiffLineType { + var color: Color { + switch self { + case .header: .blue + case .addition: .green + case .deletion: .red + case .context: .primary + } + } + + var backgroundColor: Color { + switch self { + case .header: .blue.opacity(0.1) + case .addition: .green.opacity(0.1) + case .deletion: .red.opacity(0.1) + case .context: .clear + } + } +} diff --git a/Forji/Forji/Helpers/LanguageColor.swift b/Forji/Forji/Helpers/LanguageColor.swift new file mode 100644 index 0000000..13b7251 --- /dev/null +++ b/Forji/Forji/Helpers/LanguageColor.swift @@ -0,0 +1,10 @@ +import SwiftUI + +func colorForLanguage(_ language: String) -> Color { + var hash: UInt64 = 5381 + for byte in language.utf8 { + hash = ((hash &<< 5) &+ hash) &+ UInt64(byte) + } + let hue = Double(hash % 360) / 360.0 + return Color(hue: hue, saturation: 0.6, brightness: 0.75) +} diff --git a/Forji/Forji/Helpers/MermaidParser.swift b/Forji/Forji/Helpers/MermaidParser.swift new file mode 100644 index 0000000..ef799b7 --- /dev/null +++ b/Forji/Forji/Helpers/MermaidParser.swift @@ -0,0 +1,60 @@ +import Foundation + +enum MarkdownSegment: Equatable { + case text(String) + case mermaid(String) +} + +enum MermaidParser { + private static let regex: NSRegularExpression? = { + let pattern = #"(?m)^```mermaid[ \t]*\n([\s\S]*?)^```[ \t]*$"# + return try? NSRegularExpression(pattern: pattern, options: .anchorsMatchLines) + }() + + static func parse(_ markdown: String) -> [MarkdownSegment] { + guard let regex else { + return [.text(markdown)] + } + + let nsString = markdown as NSString + let fullRange = NSRange(location: 0, length: nsString.length) + let matches = regex.matches(in: markdown, range: fullRange) + + if matches.isEmpty { + return [.text(markdown)] + } + + var segments: [MarkdownSegment] = [] + var lastEnd = 0 + + for match in matches { + let matchRange = match.range + let codeRange = match.range(at: 1) + + if matchRange.location > lastEnd { + let textRange = NSRange(location: lastEnd, length: matchRange.location - lastEnd) + let text = nsString.substring(with: textRange) + if !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + segments.append(.text(text)) + } + } + + let code = nsString.substring(with: codeRange) + .trimmingCharacters(in: .whitespacesAndNewlines) + if !code.isEmpty { + segments.append(.mermaid(code)) + } + + lastEnd = matchRange.location + matchRange.length + } + + if lastEnd < nsString.length { + let text = nsString.substring(from: lastEnd) + if !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + segments.append(.text(text)) + } + } + + return segments + } +} diff --git a/Forji/Forji/Helpers/MetadataLoader.swift b/Forji/Forji/Helpers/MetadataLoader.swift new file mode 100644 index 0000000..e4ebff5 --- /dev/null +++ b/Forji/Forji/Helpers/MetadataLoader.swift @@ -0,0 +1,18 @@ +import ForgejoKit + +struct RepositoryMetadata { + var labels: [IssueLabel] = [] + var milestones: [IssueMilestone] = [] + var assignees: [User] = [] +} + +func loadRepositoryMetadata(service: RepositoryService, owner: String, repo: String) async -> RepositoryMetadata { + async let labels = try? service.fetchLabels(owner: owner, repo: repo) + async let milestones = try? service.fetchMilestones(owner: owner, repo: repo) + async let assignees = try? service.fetchAssignees(owner: owner, repo: repo) + return await RepositoryMetadata( + labels: labels ?? [], + milestones: milestones ?? [], + assignees: assignees ?? [], + ) +} diff --git a/Forji/Forji/Helpers/PRStatusStyle.swift b/Forji/Forji/Helpers/PRStatusStyle.swift new file mode 100644 index 0000000..40a14b1 --- /dev/null +++ b/Forji/Forji/Helpers/PRStatusStyle.swift @@ -0,0 +1,33 @@ +import SwiftUI + +struct PRStatusStyle { + let icon: String + let color: Color + let text: String + + init(state: PullRequestState, merged: Bool?, draft: Bool?) { + let isMerged = merged == true + let isDraft = draft == true + + if isMerged { + icon = "arrow.triangle.merge" + color = .purple + text = "Merged" + } else if isDraft, state == .open { + icon = "circle.dashed" + color = .green + text = "Draft" + } else { + switch state { + case .open: + icon = "arrow.triangle.pull" + color = .green + text = "Open" + default: + icon = "xmark.circle.fill" + color = .red + text = "Closed" + } + } + } +} diff --git a/Forji/Forji/Helpers/PaginationState.swift b/Forji/Forji/Helpers/PaginationState.swift new file mode 100644 index 0000000..4832abb --- /dev/null +++ b/Forji/Forji/Helpers/PaginationState.swift @@ -0,0 +1,92 @@ +import SwiftUI + +@MainActor +@Observable +final class PaginationState { + var items: [Item] = [] + private(set) var isLoading = false + private(set) var hasMore = true + var errorMessage: String? + var showError = false + + private var currentPage = 1 + private var loadTask: Task? + let pageSize: Int + + init(pageSize: Int = 20) { + self.pageSize = pageSize + } + + init(items: [Item], hasMore: Bool = false, pageSize: Int = 20) { + self.items = items + self.hasMore = hasMore + self.pageSize = pageSize + } + + /// Cancels any in-flight load, resets pagination, and starts a new fetch. + /// Returns the internal Task so callers can await completion (e.g. refreshable). + @discardableResult + func reload( + clearItems: Bool = false, + using fetch: @escaping (_ page: Int, _ limit: Int) async throws -> [Item], + ) -> Task { + loadTask?.cancel() + if clearItems { items = [] } + currentPage = 1 + hasMore = true + isLoading = true + showError = false + let pageSize = pageSize + let task = Task { + do { + let fetched = try await fetch(1, pageSize) + guard !Task.isCancelled else { return } + items = fetched + hasMore = fetched.count >= pageSize + currentPage = 2 + } catch is CancellationError { + // Ignore cancellation + } catch let urlError as URLError where urlError.code == .cancelled { + // Ignore URLSession cancellation from cancelled tasks + } catch { + guard !Task.isCancelled else { return } + errorMessage = error.localizedDescription + showError = true + } + if !Task.isCancelled { + isLoading = false + } + } + loadTask = task + return task + } + + func loadMore(using fetch: @escaping (_ page: Int, _ limit: Int) async throws -> [Item]) async { + guard hasMore, !isLoading else { return } + isLoading = true + let currentPage = currentPage + let pageSize = pageSize + let task = Task { + do { + let fetched = try await fetch(currentPage, pageSize) + guard !Task.isCancelled else { return } + items.append(contentsOf: fetched) + hasMore = fetched.count >= pageSize + self.currentPage = currentPage + 1 + } catch is CancellationError { + // Ignore cancellation + } catch let urlError as URLError where urlError.code == .cancelled { + // Ignore URLSession cancellation from cancelled tasks + } catch { + guard !Task.isCancelled else { return } + errorMessage = error.localizedDescription + showError = true + } + if !Task.isCancelled { + isLoading = false + } + } + loadTask = task + await task.value + } +} diff --git a/Forji/Forji/Helpers/PreviewData.swift b/Forji/Forji/Helpers/PreviewData.swift new file mode 100644 index 0000000..3415ad2 --- /dev/null +++ b/Forji/Forji/Helpers/PreviewData.swift @@ -0,0 +1,350 @@ +import ForgejoKit +import Foundation + +// Fake model instances for SwiftUI previews +#if DEBUG + extension Repository { + static let preview = Repository( + id: 1, + name: "test-repo", + fullName: "testuser/test-repo", + description: "A test repository", + empty: false, + private: false, + fork: false, + parent: nil, + mirror: false, + size: 1024, + language: "Swift", + languagesUrl: nil, + htmlUrl: "", + sshUrl: "", + cloneUrl: "", + website: nil, + starsCount: 42, + forksCount: 7, + watchersCount: 10, + openIssuesCount: 3, + openPrCounter: 1, + releaseCounter: 2, + defaultBranch: "main", + archived: false, + createdAt: Date(), + updatedAt: Date(), + permissions: nil, + hasIssues: true, + internalTracker: nil, + hasWiki: true, + hasPullRequests: true, + hasProjects: true, + hasReleases: true, + hasPackages: false, + hasActions: true, + template: false, + avatarUrl: nil, + ) + } + + extension User { + static let preview = User( + id: 1, + login: "testuser", + fullName: "Test User", + email: "test@example.com", + avatarUrl: nil, + isAdmin: false, + created: Date(), + ) + + static let previewBot = User( + id: 2, + login: "botuser", + fullName: "Bot User", + email: "bot@example.com", + avatarUrl: nil, + isAdmin: false, + created: Date(), + ) + } + + extension Commit { + static let preview = Commit( + sha: "abc123def456789", + htmlUrl: "https://forgejo.example.com/testuser/test-repo/commit/abc123def456789", + commit: CommitDetail( + message: "Add new feature", + author: CommitSignature(name: "Test User", email: "test@example.com", date: Date()), + committer: CommitSignature(name: "Test User", email: "test@example.com", date: Date()), + ), + author: .preview, + ) + } + + extension ForgejoInstance { + static let preview = ForgejoInstance( + serverURL: "https://forgejo.example.com", + username: "testuser", + ) + } + + extension IssueLabel { + static let preview = IssueLabel( + id: 1, + name: "bug", + color: "ee0701", + description: "Something isn't working", + ) + + static let previewList: [IssueLabel] = [ + .preview, + IssueLabel( + id: 2, + name: "enhancement", + color: "0075ca", + description: "New feature or request", + ), + ] + } + + extension IssueMilestone { + static let preview = IssueMilestone( + id: 1, + title: "v1.0", + description: "First stable release", + state: "open", + ) + } + + extension IssueComment { + static let preview = IssueComment( + id: 1, + body: "This looks good! I think we should also consider **edge cases** for empty input.", + user: .preview, + createdAt: Date(), + updatedAt: Date(), + ) + } + + extension Issue { + static let preview = Issue( + id: 1, + number: 1, + title: "Fix login crash on empty password", + body: """ + The app crashes when the user submits the login form with an empty password. + + ## Steps to reproduce + 1. Open the app + 2. Enter a username + 3. Leave password empty + 4. Tap **Login** + """, + state: "open", + user: .preview, + labels: IssueLabel.previewList, + milestone: .preview, + assignees: [.preview], + createdAt: Date().addingTimeInterval(-86400), + updatedAt: Date(), + comments: 2, + ) + + static let previewWithRepo = Issue( + id: 1, + number: 1, + title: "Fix login crash on empty password", + body: "The app crashes when the user submits the login form with an empty password.", + state: "open", + user: .preview, + labels: IssueLabel.previewList, + milestone: .preview, + assignees: [.preview], + createdAt: Date().addingTimeInterval(-86400), + updatedAt: Date(), + comments: 2, + repository: .preview, + ) + + static let previewPullRequest = Issue( + id: 10, + number: 4, + title: "Add dark mode support", + body: "This PR adds dark mode support.", + state: "open", + user: .preview, + labels: IssueLabel.previewList, + createdAt: Date().addingTimeInterval(-86400), + updatedAt: Date(), + pullRequest: IssuePullRequest(merged: false), + comments: 1, + repository: .preview, + ) + } + + extension PRBranchRef { + static let previewHead = PRBranchRef( + label: "testuser:feature-branch", + ref: "feature-branch", + sha: "abc123def456789", + repo: PRRepo(id: 1, name: "test-repo", fullName: "testuser/test-repo"), + ) + + static let previewBase = PRBranchRef( + label: "testuser:main", + ref: "main", + sha: "def456789abc123", + repo: PRRepo(id: 1, name: "test-repo", fullName: "testuser/test-repo"), + ) + } + + extension PullRequest { + static let preview = PullRequest( + id: 1, + number: 4, + title: "Add dark mode support", + body: "This PR adds dark mode support to the app.\n\n- Updated color scheme\n- Added toggle in settings", + state: "open", + user: .preview, + labels: IssueLabel.previewList, + assignees: [.preview, .previewBot], + head: .previewHead, + base: .previewBase, + mergeable: true, + merged: false, + requestedReviewers: [.previewBot], + draft: false, + comments: 1, + createdAt: Date().addingTimeInterval(-86400), + updatedAt: Date(), + ) + } + + extension NotificationSubject { + static let preview = NotificationSubject( + type: "Issue", + title: "Fix login crash on empty password", + state: "open", + ) + } + + extension NotificationThread { + static let preview = NotificationThread( + id: 1, + unread: true, + pinned: false, + updatedAt: Date(), + subject: .preview, + repository: .preview, + ) + + static let previewList: [NotificationThread] = [ + .preview, + NotificationThread( + id: 2, + unread: false, + pinned: false, + updatedAt: Date().addingTimeInterval(-3600), + subject: NotificationSubject( + type: "Pull", + title: "Add dark mode support", + state: "open", + ), + repository: .preview, + ), + ] + } + + extension PullRequestReview { + static let preview = PullRequestReview( + id: 1, + body: "Looks good overall, just a few minor suggestions.", + user: .previewBot, + state: "APPROVED", + commentsCount: 1, + submittedAt: Date(), + ) + } + + extension ReviewComment { + static let preview = ReviewComment( + id: 1, + body: "Consider using a guard statement here instead.", + user: .previewBot, + path: "README.md", + diffHunk: "@@ -1,3 +1,4 @@\n # Test Repo\n+Some new line", + createdAt: Date(), + updatedAt: Date(), + ) + } + + extension ParsedDiff { + static let preview = ParsedDiff( + files: [ + DiffFile( + oldName: "README.md", + newName: "README.md", + hunks: [ + DiffHunk( + header: "@@ -1,5 +1,6 @@", + lines: [ + DiffLine( + type: .context, content: " # Test Repo", + oldLineNumber: 1, newLineNumber: 1, diffPosition: 1, + ), + DiffLine( + type: .context, content: " ", + oldLineNumber: 2, newLineNumber: 2, diffPosition: 2, + ), + DiffLine( + type: .deletion, content: "-Old description", + oldLineNumber: 3, newLineNumber: nil, diffPosition: 3, + ), + DiffLine( + type: .addition, content: "+New and improved description", + oldLineNumber: nil, newLineNumber: 3, diffPosition: 4, + ), + DiffLine( + type: .context, content: " ", + oldLineNumber: 4, newLineNumber: 4, diffPosition: 5, + ), + DiffLine( + type: .addition, content: "+Added a new line", + oldLineNumber: nil, newLineNumber: 5, diffPosition: 6, + ), + ], + ), + ], + ), + ], + ) + } + + extension Branch { + static let preview = Branch( + name: "main", + commit: BranchCommit(id: "abc123", message: "Initial commit", url: ""), + protected: true, + ) + + static let previewList: [Branch] = [ + .preview, + Branch( + name: "feature-branch", + commit: BranchCommit(id: "def456", message: "Add feature", url: ""), + protected: false, + ), + Branch( + name: "develop", + commit: BranchCommit(id: "789abc", message: "Merge develop", url: ""), + protected: false, + ), + ] + } + + extension AuthenticationService { + static let previewDefault: AuthenticationService = .preview( + user: .preview, + instance: .preview, + ) + } +#endif diff --git a/Forji/Forji/Models/ForgejoInstance.swift b/Forji/Forji/Models/ForgejoInstance.swift new file mode 100644 index 0000000..d1e9d6a --- /dev/null +++ b/Forji/Forji/Models/ForgejoInstance.swift @@ -0,0 +1,24 @@ +import Foundation +import SwiftData + +@Model +final class ForgejoInstance { + var serverURL: String + var username: String + var name: String + var isDefault: Bool + var lastUsed: Date + var allowSelfSignedCertificates: Bool + + init( + serverURL: String, username: String, name: String = "", + isDefault: Bool = false, allowSelfSignedCertificates: Bool = false, + ) { + self.serverURL = serverURL + self.username = username + self.name = name + self.isDefault = isDefault + lastUsed = Date() + self.allowSelfSignedCertificates = allowSelfSignedCertificates + } +} diff --git a/Forji/Forji/Models/ReviewState.swift b/Forji/Forji/Models/ReviewState.swift new file mode 100644 index 0000000..4f7a15d --- /dev/null +++ b/Forji/Forji/Models/ReviewState.swift @@ -0,0 +1,53 @@ +import Foundation + +enum ReviewEvent: String, CaseIterable, Identifiable { + case comment = "COMMENT" + case approved = "APPROVED" + case requestChanges = "REQUEST_CHANGES" + + var id: String { + rawValue + } + + var title: String { + switch self { + case .comment: + "Comment" + case .approved: + "Approve" + case .requestChanges: + "Reject" + } + } + + var systemImage: String { + switch self { + case .comment: + "message" + case .approved: + "checkmark.circle" + case .requestChanges: + "xmark.circle" + } + } +} + +enum ReviewState: String { + case approved = "APPROVED" + case requestChanges = "REQUEST_CHANGES" + case comment = "COMMENT" + case pending = "PENDING" + + var label: String { + switch self { + case .approved: + "approved" + case .requestChanges: + "rejected" + case .comment: + "commented" + case .pending: + "pending" + } + } +} diff --git a/Forji/Forji/Models/State.swift b/Forji/Forji/Models/State.swift new file mode 100644 index 0000000..9542cca --- /dev/null +++ b/Forji/Forji/Models/State.swift @@ -0,0 +1,62 @@ +import ForgejoKit + +enum IssueState: String { + case open + case closed +} + +enum IssueFilterState: String { + case open + case closed + case all +} + +enum PullRequestState: String { + case open + case closed +} + +enum PullRequestFilterState: String { + case open + case closed + case all +} + +enum NotificationSubjectState: String { + case open + case closed + case merged +} + +extension Issue { + var stateValue: IssueState { + IssueState(rawValue: state) ?? .closed + } +} + +extension Issue { + var pullRequestStateValue: PullRequestState { + PullRequestState(rawValue: state) ?? .closed + } +} + +extension PullRequest { + var stateValue: PullRequestState { + PullRequestState(rawValue: state) ?? .closed + } +} + +enum InvolvementScope: String, CaseIterable { + case involved + case created + case assigned + case mentioned + case reviewRequested = "review_requested" +} + +extension NotificationSubject { + var stateValue: NotificationSubjectState? { + guard let state else { return nil } + return NotificationSubjectState(rawValue: state) + } +} diff --git a/Forji/Forji/Services/AuthenticationService.swift b/Forji/Forji/Services/AuthenticationService.swift new file mode 100644 index 0000000..505c01f --- /dev/null +++ b/Forji/Forji/Services/AuthenticationService.swift @@ -0,0 +1,131 @@ +import ForgejoKit +import Foundation +import SwiftData + +@Observable +class AuthenticationService { + var isAuthenticated = false + var currentUser: User? + var currentInstance: ForgejoInstance? + private(set) var client: ForgejoClient? + + func login(serverURL: String, username: String, password: String, allowSelfSigned: Bool = false) async throws { + let result = try await ForgejoClient.login( + serverURL: serverURL, + username: username, + password: password, + allowSelfSignedCertificates: allowSelfSigned, + ) + try await storeCredentials(result: result, password: password) + } + + func loginWithOTP( + serverURL: String, username: String, password: String, otp: String, + allowSelfSigned: Bool = false, + ) async throws { + let result = try await ForgejoClient.loginWithOTP( + serverURL: serverURL, + username: username, + password: password, + otp: otp, + allowSelfSignedCertificates: allowSelfSigned, + ) + try await storeCredentials(result: result, password: password) + } + + private func storeCredentials(result: LoginResult, password: String) async throws { + try await KeychainManager.shared.savePassword( + password, for: result.client.serverURL, username: result.client.username, + ) + try await KeychainManager.shared.saveToken( + result.token, for: result.client.serverURL, username: result.client.username, + ) + client = result.client + currentUser = result.user + isAuthenticated = true + } + + func disconnect() { + isAuthenticated = false + currentUser = nil + currentInstance = nil + client = nil + } + + func logout(modelContext: ModelContext? = nil) async { + if let instance = currentInstance { + let normalizedURL = ForgejoClient.normalizeServerURL(instance.serverURL) + do { + try await KeychainManager.shared.deletePassword( + for: normalizedURL, + username: instance.username, + ) + try await KeychainManager.shared.deleteToken( + for: normalizedURL, + username: instance.username, + ) + } catch { + // Keychain delete failed — log in debug builds + #if DEBUG + print("Keychain delete failed during logout: \(error)") + #endif + } + // Remove the instance from SwiftData and unset default + if let modelContext { + instance.isDefault = false + modelContext.delete(instance) + try? modelContext.save() + } + } + isAuthenticated = false + currentUser = nil + currentInstance = nil + client = nil + } + + func restoreSession(instance: ForgejoInstance) async throws { + let normalizedURL = ForgejoClient.normalizeServerURL(instance.serverURL) + + // Try restoring from stored API token first (avoids 2FA prompt) + if let token = try? await KeychainManager.shared.getToken(for: normalizedURL, username: instance.username) { + let tokenClient = ForgejoClient( + serverURL: normalizedURL, + username: instance.username, + token: token, + allowSelfSignedCertificates: instance.allowSelfSignedCertificates, + ) + // Validate the token still works + let user = try await tokenClient.fetchCurrentUser() + client = tokenClient + currentUser = user + isAuthenticated = true + currentInstance = instance + return + } + + // Fall back to password-based login (creates a new token) + let password = try await KeychainManager.shared.getPassword(for: normalizedURL, username: instance.username) + try await login( + serverURL: normalizedURL, username: instance.username, + password: password, + allowSelfSigned: instance.allowSelfSignedCertificates, + ) + currentInstance = instance + } + + // Stub factories for SwiftUI previews + #if DEBUG + static func preview(user: User, instance: ForgejoInstance? = nil) -> AuthenticationService { + let service = AuthenticationService() + service.isAuthenticated = true + service.currentUser = user + service.currentInstance = instance + service.client = ForgejoClient( + serverURL: instance?.serverURL ?? "https://forgejo.example.com", + username: user.login, + token: "preview-token", + ) + return service + } + #endif +} diff --git a/Forji/Forji/Services/KeychainManager.swift b/Forji/Forji/Services/KeychainManager.swift new file mode 100644 index 0000000..7d9dac1 --- /dev/null +++ b/Forji/Forji/Services/KeychainManager.swift @@ -0,0 +1,114 @@ +import Foundation +import Security + +actor KeychainManager { + static let shared = KeychainManager() + + private static let serviceName = "de.hausotte.Forji" + + private init() {} + + private func key(for server: String, username: String, suffix: String = "") -> String { + "\(server)_\(username)\(suffix)" + } + + // MARK: - Password + + func savePassword(_ password: String, for server: String, username: String) throws { + try saveItem(password, forKey: key(for: server, username: username)) + } + + func getPassword(for server: String, username: String) throws -> String { + try getItem(forKey: key(for: server, username: username)) + } + + func deletePassword(for server: String, username: String) throws { + try deleteItem(forKey: key(for: server, username: username)) + } + + // MARK: - Token + + func saveToken(_ token: String, for server: String, username: String) throws { + try saveItem(token, forKey: key(for: server, username: username, suffix: "_token")) + } + + func getToken(for server: String, username: String) throws -> String { + try getItem(forKey: key(for: server, username: username, suffix: "_token")) + } + + func deleteToken(for server: String, username: String) throws { + try deleteItem(forKey: key(for: server, username: username, suffix: "_token")) + } + + // MARK: - Private helpers + + private func saveItem(_ value: String, forKey key: String) throws { + guard let data = value.data(using: .utf8) else { + throw KeychainError.unableToSave + } + + let deleteQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: Self.serviceName, + kSecAttrAccount as String: key, + ] + + SecItemDelete(deleteQuery as CFDictionary) + + let addQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: Self.serviceName, + kSecAttrAccount as String: key, + kSecValueData as String: data, + kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + ] + + let status = SecItemAdd(addQuery as CFDictionary, nil) + + guard status == errSecSuccess else { + throw KeychainError.unableToSave + } + } + + private func getItem(forKey key: String) throws -> String { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: Self.serviceName, + kSecAttrAccount as String: key, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne, + ] + + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + guard status == errSecSuccess, + let data = result as? Data, + let value = String(data: data, encoding: .utf8) + else { + throw KeychainError.notFound + } + + return value + } + + private func deleteItem(forKey key: String) throws { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: Self.serviceName, + kSecAttrAccount as String: key, + ] + + let status = SecItemDelete(query as CFDictionary) + + guard status == errSecSuccess || status == errSecItemNotFound else { + throw KeychainError.unableToDelete + } + } +} + +enum KeychainError: Error, Sendable { + case unableToSave + case notFound + case unableToDelete +} diff --git a/Forji/Forji/Views/CommentSheet.swift b/Forji/Forji/Views/CommentSheet.swift new file mode 100644 index 0000000..d1f7ba3 --- /dev/null +++ b/Forji/Forji/Views/CommentSheet.swift @@ -0,0 +1,71 @@ +import ForgejoKit +import SwiftUI + +struct CommentSheet: View { + let users: [User] + var onSubmit: (String) async throws -> Void + + @State private var text = "" + @State private var selectedTab: EditPreviewTab = .edit + @State private var isSubmitting = false + @State private var errorMessage: String? + @State private var showError = false + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationStack { + Group { + if users.isEmpty { + MarkdownEditorField( + text: $text, + selectedTab: $selectedTab, + showToolbar: true, + ) + } else { + MentionableEditorField( + text: $text, + selectedTab: $selectedTab, + users: users, + showToolbar: true, + ) + } + } + .padding() + .navigationTitle("New Comment") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + if isSubmitting { + ProgressView() + .controlSize(.small) + } else { + Button("Submit") { + Task { await submit() } + } + .disabled(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isSubmitting) + } + } + } + .errorAlert(message: $errorMessage, isPresented: $showError) + } + } + + private func submit() async { + isSubmitting = true + do { + try await onSubmit(text.trimmingCharacters(in: .whitespacesAndNewlines)) + dismiss() + } catch { + errorMessage = error.localizedDescription + showError = true + } + isSubmitting = false + } +} + +#Preview { + CommentSheet(users: [.preview, .previewBot]) { _ in } +} diff --git a/Forji/Forji/Views/CommentView.swift b/Forji/Forji/Views/CommentView.swift new file mode 100644 index 0000000..ee0a96a --- /dev/null +++ b/Forji/Forji/Views/CommentView.swift @@ -0,0 +1,118 @@ +import ForgejoKit +import SwiftUI + +struct CommentView: View { + let comment: IssueComment + let currentUsername: String? + let onSave: ((String) async -> Bool)? + + @State private var isEditing = false + @State private var editedBody: String = "" + @State private var displayBody: String = "" + + private var isOwnComment: Bool { + currentUsername != nil && comment.user.login == currentUsername + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 6) { + Text(comment.user.login) + .font(.subheadline) + .fontWeight(.medium) + Text(formatRelativeDate(comment.createdAt)) + .font(.caption) + .foregroundStyle(.secondary) + + Spacer() + + if isOwnComment { + Button { + editedBody = displayBody + isEditing = true + } label: { + Image(systemName: "pencil") + .font(.caption) + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) + } + } + + MarkdownPreview(text: displayBody) + } + .padding(.vertical, 4) + .onAppear { + displayBody = comment.body + } + .onChange(of: comment.body) { _, newValue in + displayBody = newValue + } + .sheet(isPresented: $isEditing) { + CommentEditSheet(text: $editedBody) { saved in + displayBody = saved + } onSave: { body in + await onSave?(body) ?? false + } + } + } +} + +private struct CommentEditSheet: View { + @Binding var text: String + let onDismissWithSave: (String) -> Void + let onSave: (String) async -> Bool + @State private var selectedTab: EditPreviewTab = .edit + @State private var isSaving = false + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationStack { + MarkdownEditorField( + text: $text, + selectedTab: $selectedTab, + showToolbar: true, + ) + .frame(maxHeight: .infinity) + .padding() + .navigationTitle("Edit Comment") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + if isSaving { + ProgressView() + .controlSize(.small) + } else { + Button("Save") { + Task { await save() } + } + .disabled(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + } + } + } + } + } + + private func save() async { + isSaving = true + let success = await onSave(text) + if success { + onDismissWithSave(text) + dismiss() + } + isSaving = false + } +} + +#Preview { + List { + CommentView( + comment: .preview, + currentUsername: "testuser", + onSave: nil, + ) + } +} diff --git a/Forji/Forji/Views/CommitDetailView.swift b/Forji/Forji/Views/CommitDetailView.swift new file mode 100644 index 0000000..ae23b27 --- /dev/null +++ b/Forji/Forji/Views/CommitDetailView.swift @@ -0,0 +1,104 @@ +import ForgejoKit +import SwiftUI + +struct CommitDetailView: View { + let commit: Commit + let repository: Repository + @State private var authService: AuthenticationService + @State private var parsedDiff: ParsedDiff? + @State private var isLoadingDiff = false + @State private var errorMessage: String? + @State private var showError = false + + private let repositoryService: RepositoryService? + + init(commit: Commit, repository: Repository, authService: AuthenticationService) { + self.commit = commit + self.repository = repository + self.authService = authService + repositoryService = authService.client.map { RepositoryService(client: $0) } + } + + var body: some View { + List { + // Commit info + Section { + VStack(alignment: .leading, spacing: 8) { + Text(commit.commit.message) + .font(.body) + + HStack(spacing: 8) { + if let authorName = commit.author?.login ?? commit.commit.author?.name { + Text(authorName) + .fontWeight(.medium) + } + + Text(String(commit.sha.prefix(7))) + .monospaced() + + if let date = commit.commit.committer?.date ?? commit.commit.author?.date { + Text(formatRelativeDate(date)) + } + } + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(.vertical, 4) + } + + // Diff + Section("Changes") { + if isLoadingDiff { + HStack { + Spacer() + ProgressView() + Spacer() + } + .listRowBackground(Color.clear) + } else if let parsedDiff { + if parsedDiff.files.isEmpty { + Text("No file changes") + .foregroundStyle(.secondary) + } else { + DiffView(diff: parsedDiff) + .listRowInsets(EdgeInsets()) + } + } + } + } + .listStyle(.insetGrouped) + .navigationTitle(String(commit.sha.prefix(7))) + .navigationBarTitleDisplayMode(.inline) + .task { + await loadDiff() + } + .errorAlert(message: $errorMessage, isPresented: $showError) + } + + private func loadDiff() async { + guard let repositoryService else { return } + isLoadingDiff = true + do { + let rawDiff = try await repositoryService.fetchCommitDiff( + owner: repository.owner, + repo: repository.repoName, + sha: commit.sha, + ) + parsedDiff = DiffParser.parse(rawDiff) + } catch { + errorMessage = error.localizedDescription + showError = true + } + isLoadingDiff = false + } +} + +#Preview { + NavigationStack { + CommitDetailView( + commit: .preview, + repository: .preview, + authService: .previewDefault, + ) + } +} diff --git a/Forji/Forji/Views/CommitHistoryView.swift b/Forji/Forji/Views/CommitHistoryView.swift new file mode 100644 index 0000000..0feb3a9 --- /dev/null +++ b/Forji/Forji/Views/CommitHistoryView.swift @@ -0,0 +1,220 @@ +import ForgejoKit +import SwiftUI + +struct CommitHistoryView: View { + let repository: Repository + @Binding var branch: String + @State private var authService: AuthenticationService + @State private var pagination = PaginationState() + @State private var branches: [Branch] = [] + @State private var showBranchPicker = false + + private let repositoryService: RepositoryService? + + init(repository: Repository, branch: Binding, authService: AuthenticationService) { + self.repository = repository + _branch = branch + self.authService = authService + repositoryService = authService.client.map { RepositoryService(client: $0) } + } + + private var owner: String { + repository.owner + } + + private var repo: String { + repository.repoName + } + + var body: some View { + @Bindable var pagination = pagination + List { + if pagination.isLoading, pagination.items.isEmpty { + LoadingListSection() + } else if pagination.items.isEmpty { + ContentUnavailableView { + Label("No Commits", systemImage: "clock.arrow.circlepath") + .foregroundStyle(.secondary) + } description: { + Text("No commits found on \(branch).") + } + } else { + Section { + ForEach(pagination.items) { commit in + NavigationLink { + CommitDetailView( + commit: commit, + repository: repository, + authService: authService, + ) + } label: { + CommitRow(commit: commit) + } + } + + if pagination.hasMore { + ProgressView() + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + .task { + await loadMoreCommits() + } + } + } + } + } + .listStyle(.insetGrouped) + .refreshable { + await reloadCommits(clearItems: true).value + } + .task { + reloadCommits() + await loadBranches() + } + .onChange(of: branch) { + reloadCommits(clearItems: true) + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + showBranchPicker = true + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.triangle.branch") + Text(branch) + .lineLimit(1) + .frame(maxWidth: 140) + } + .font(.subheadline.weight(.medium)) + } + .disabled(branches.isEmpty) + .accessibilityIdentifier("branch-selector") + } + } + .sheet(isPresented: $showBranchPicker) { + NavigationStack { + List(branches) { branchItem in + Button { + branch = branchItem.name + showBranchPicker = false + } label: { + HStack { + Text(branchItem.name) + Spacer() + if branchItem.name == branch { + Image(systemName: "checkmark") + .foregroundStyle(.blue) + } + } + } + .accessibilityIdentifier("branch-option-\(branchItem.name)") + } + .navigationTitle("Switch Branch") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + showBranchPicker = false + } + } + } + } + .presentationDetents([.medium]) + } + .errorAlert(message: $pagination.errorMessage, isPresented: $pagination.showError) + } + + @discardableResult + private func reloadCommits(clearItems: Bool = false) -> Task { + guard let repositoryService else { return Task {} } + return pagination.reload(clearItems: clearItems) { [self] page, limit in + try await repositoryService.fetchCommits( + owner: owner, + repo: repo, + sha: branch, + page: page, + limit: limit, + ) + } + } + + private func loadBranches() async { + guard let repositoryService else { return } + do { + branches = try await repositoryService.fetchBranches( + owner: owner, + repo: repo, + ) + } catch { + // Non-critical — branch selector stays disabled + } + } + + private func loadMoreCommits() async { + guard let repositoryService else { return } + await pagination.loadMore { page, limit in + let fetched = try await repositoryService.fetchCommits( + owner: owner, + repo: repo, + sha: branch, + page: page, + limit: limit, + ) + try Task.checkCancellation() + return fetched + } + } +} + +struct CommitRow: View { + let commit: Commit + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text(commitTitle) + .font(.body) + .lineLimit(2) + + HStack(spacing: 8) { + if let authorName = displayName { + Text(authorName) + .fontWeight(.medium) + } + + Text(shortSHA) + .monospaced() + + if let date = commit.commit.committer?.date ?? commit.commit.author?.date { + Text(formatRelativeDate(date)) + } + } + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(.vertical, 4) + .accessibilityIdentifier("commit-row-\(commit.sha.prefix(7))") + } + + private var commitTitle: String { + let message = commit.commit.message + return message.components(separatedBy: "\n").first ?? message + } + + private var shortSHA: String { + String(commit.sha.prefix(7)) + } + + private var displayName: String? { + commit.author?.login ?? commit.commit.author?.name + } +} + +#Preview { + NavigationStack { + CommitHistoryView( + repository: .preview, + branch: .constant("main"), + authService: .previewDefault, + ) + } +} diff --git a/Forji/Forji/Views/DiffView.swift b/Forji/Forji/Views/DiffView.swift new file mode 100644 index 0000000..eee1f76 --- /dev/null +++ b/Forji/Forji/Views/DiffView.swift @@ -0,0 +1,151 @@ +import ForgejoKit +import SwiftUI + +struct DiffView: View { + let diff: ParsedDiff + var reviewComments: [ReviewComment] = [] + var onLineTap: ((DiffLine, String) -> Void)? + + private var commentIndex: [String: [Int: [ReviewComment]]] { + Self.buildCommentIndex(reviewComments) + } + + static func buildCommentIndex(_ comments: [ReviewComment]) -> [String: [Int: [ReviewComment]]] { + var index: [String: [Int: [ReviewComment]]] = [:] + for comment in comments { + let path = comment.path + let pos = comment.position ?? comment.originalPosition ?? 0 + guard pos > 0 else { continue } + index[path, default: [:]][pos, default: []].append(comment) + } + return index + } + + var body: some View { + ScrollView(.horizontal) { + LazyVStack(alignment: .leading, spacing: 0) { + ForEach(Array(diff.files.enumerated()), id: \.offset) { fileIndex, file in + let filePath = file.newName == "/dev/null" ? file.oldName : file.newName + + HStack(spacing: 4) { + Image(systemName: "doc.text") + .font(.caption2) + Text(filePath) + .font(.system(.caption, design: .monospaced)) + .fontWeight(.semibold) + .fixedSize(horizontal: true, vertical: false) + } + .padding(.horizontal, 10) + .padding(.vertical, 8) + .frame(maxWidth: .infinity, alignment: .leading) + .glassEffect(.regular) + + ForEach(Array(file.hunks.enumerated()), id: \.offset) { _, hunk in + ForEach(hunk.lines) { line in + diffLineRow(line: line, filePath: filePath) + + if !reviewComments.isEmpty { + ForEach(commentsForLine(line, path: filePath)) { comment in + InlineCommentBubble(comment: comment) + } + } + } + } + + if fileIndex < diff.files.count - 1 { + Spacer().frame(height: 8) + } + } + } + } + } + + private func diffLineRow(line: DiffLine, filePath: String) -> some View { + HStack(spacing: 0) { + HStack(spacing: 2) { + Text(line.oldLineNumber.map { "\($0)" } ?? "") + .font(.system(.caption2, design: .monospaced)) + .foregroundStyle(.tertiary) + .frame(minWidth: 20, alignment: .trailing) + + Text(line.newLineNumber.map { "\($0)" } ?? "") + .font(.system(.caption2, design: .monospaced)) + .foregroundStyle(.tertiary) + .frame(minWidth: 20, alignment: .trailing) + } + .padding(.trailing, 2) + + Rectangle() + .fill(.quaternary) + .frame(width: 1) + .padding(.vertical, 1) + + Text(line.content) + .font(.system(.caption2, design: .monospaced)) + .foregroundStyle(line.type.color) + .fixedSize(horizontal: true, vertical: false) + .padding(.leading, 4) + + Spacer(minLength: 0) + + if onLineTap != nil, line.type != .header, line.diffPosition != nil { + Image(systemName: "plus.bubble") + .font(.caption2) + .foregroundStyle(.tertiary) + .padding(.trailing, 4) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 2) + .padding(.vertical, 1) + .background(line.type.backgroundColor) + .contentShape(Rectangle()) + .onTapGesture { + if line.type != .header, line.diffPosition != nil { + onLineTap?(line, filePath) + } + } + } + + private func commentsForLine(_ line: DiffLine, path: String) -> [ReviewComment] { + guard let linePos = line.diffPosition else { return [] } + return commentIndex[path]?[linePos] ?? [] + } +} + +private struct InlineCommentBubble: View { + let comment: ReviewComment + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 4) { + Text(comment.user?.login ?? "Unknown") + .font(.caption2) + .fontWeight(.medium) + if let date = comment.createdAt { + Text(formatRelativeDate(date)) + .font(.caption2) + .foregroundStyle(.secondary) + } + } + MarkdownPreview(text: comment.body) + } + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .containerRelativeFrame(.horizontal, alignment: .leading) { width, _ in + max(0, width - 52) + } + .background(Color.blue.opacity(0.08)) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .padding(.leading, 48) + .padding(.trailing, 4) + .padding(.vertical, 2) + } +} + +#Preview { + List { + DiffView(diff: .preview) + .listRowInsets(EdgeInsets()) + } +} diff --git a/Forji/Forji/Views/DisplaySections.swift b/Forji/Forji/Views/DisplaySections.swift new file mode 100644 index 0000000..02970eb --- /dev/null +++ b/Forji/Forji/Views/DisplaySections.swift @@ -0,0 +1,58 @@ +import ForgejoKit +import SwiftUI + +struct MilestoneDisplaySection: View { + let milestone: IssueMilestone + + var body: some View { + Section("Milestone") { + HStack(spacing: 8) { + Image(systemName: "flag") + .foregroundStyle(.secondary) + VStack(alignment: .leading, spacing: 2) { + Text(milestone.title) + .font(.subheadline) + if let due = milestone.dueOn { + Text("Due \(due, style: .date)") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + } + } +} + +struct AssigneesDisplaySection: View { + let assignees: [User] + + var body: some View { + Section("Assignees") { + ForEach(assignees) { user in + HStack(spacing: 8) { + Image(systemName: "person") + .foregroundStyle(.secondary) + Text(user.fullName ?? user.login) + .font(.subheadline) + if user.fullName != nil { + Text("@\(user.login)") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + } + } +} + +#Preview("Milestone") { + List { + MilestoneDisplaySection(milestone: .preview) + } +} + +#Preview("Assignees") { + List { + AssigneesDisplaySection(assignees: [.preview, .previewBot]) + } +} diff --git a/Forji/Forji/Views/ErrorAlert.swift b/Forji/Forji/Views/ErrorAlert.swift new file mode 100644 index 0000000..d6f2381 --- /dev/null +++ b/Forji/Forji/Views/ErrorAlert.swift @@ -0,0 +1,22 @@ +import SwiftUI + +struct ErrorAlertModifier: ViewModifier { + @Binding var errorMessage: String? + @Binding var isPresented: Bool + var title: String = "Error" + + func body(content: Content) -> some View { + content + .alert(title, isPresented: $isPresented) { + Button("OK", role: .cancel) {} + } message: { + Text(errorMessage ?? "An unknown error occurred") + } + } +} + +extension View { + func errorAlert(_ title: String = "Error", message: Binding, isPresented: Binding) -> some View { + modifier(ErrorAlertModifier(errorMessage: message, isPresented: isPresented, title: title)) + } +} diff --git a/Forji/Forji/Views/FileViewerView.swift b/Forji/Forji/Views/FileViewerView.swift new file mode 100644 index 0000000..a0dae80 --- /dev/null +++ b/Forji/Forji/Views/FileViewerView.swift @@ -0,0 +1,337 @@ +import ForgejoKit +import HighlightSwift +import SwiftUI + +struct FileViewerView: View { + let repository: Repository + let filePath: String + let fileName: String + let ref: String? + @State private var authService: AuthenticationService + @State private var fileContent: String? + @State private var fileSha: String? + @State private var isLoading = false + @State private var errorMessage: String? + @State private var showError = false + @State private var isEditing = false + @State private var editedContent: String = "" + @State private var showCommitSheet = false + @State private var navigateToFilePath: String? + + private let repositoryService: RepositoryService? + + init( + repository: Repository, filePath: String, fileName: String, + authService: AuthenticationService, ref: String? = nil, + ) { + self.repository = repository + self.filePath = filePath + self.fileName = fileName + self.authService = authService + self.ref = ref + repositoryService = authService.client.map { RepositoryService(client: $0) } + } + + var body: some View { + Group { + if isLoading { + VStack { + ProgressView("Loading file...") + .padding() + } + } else if let content = fileContent { + if isEditing { + TextEditor(text: $editedContent) + .font(.system(.body, design: .monospaced)) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + } else { + if isMarkdownFile { + ScrollView { + MarkdownPreview( + text: content, + baseURL: markdownBaseURL, + onNavigateToFile: { path in + navigateToFilePath = path + }, + ) + .padding() + } + } else { + let lines = content.components(separatedBy: "\n") + let lineNumberText = (1 ... lines.count).map(String.init).joined(separator: "\n") + + ScrollView(.vertical) { + HStack(alignment: .top, spacing: 0) { + Text(lineNumberText) + .font(.system(.body, design: .monospaced)) + .foregroundStyle(.quaternary) + .multilineTextAlignment(.trailing) + .padding(.leading, 8) + .padding(.trailing, 8) + .padding(.vertical, 12) + .glassEffect(.regular) + + ScrollView(.horizontal, showsIndicators: false) { + CodeText(content) + .fixedSize(horizontal: true, vertical: false) + .textSelection(.enabled) + .padding(12) + } + } + } + } + } + } else { + ContentUnavailableView { + Label("Could not load file", systemImage: "doc.text.fill.badge.questionmark") + .foregroundStyle(.orange) + } description: { + Text("The file could not be loaded.") + } + } + } + .navigationTitle(fileName) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItemGroup(placement: .topBarTrailing) { + if isEditing { + Button("Cancel") { + isEditing = false + editedContent = fileContent ?? "" + } + .accessibilityIdentifier("file-edit-cancel") + Button("Commit") { + showCommitSheet = true + } + .disabled(editedContent == fileContent) + .accessibilityIdentifier("file-edit-commit") + } else if fileContent != nil, fileSha != nil { + Button { + editedContent = fileContent ?? "" + isEditing = true + } label: { + Image(systemName: "pencil") + } + .accessibilityIdentifier("file-edit-button") + } + } + } + .sheet(isPresented: $showCommitSheet) { + if let repositoryService, let currentSha = fileSha { + CommitSheetView( + fileName: fileName, + repositoryService: repositoryService, + repository: repository, + filePath: filePath, + editedContent: editedContent, + fileSha: currentSha, + branch: ref, + onCommit: { _ in + isEditing = false + await loadFile() + }, + ) + } + } + .task { + await loadFile() + } + .errorAlert(message: $errorMessage, isPresented: $showError) + .navigationDestination(item: $navigateToFilePath) { path in + let fileName = path.components(separatedBy: "/").last ?? path + FileViewerView( + repository: repository, + filePath: path, + fileName: fileName, + authService: authService, + ref: ref, + ) + } + } + + private var isMarkdownFile: Bool { + let lowercased = fileName.lowercased() + return lowercased.hasSuffix(".md") || lowercased.hasSuffix(".markdown") + } + + private var markdownBaseURL: URL? { + guard let serverURL = authService.client?.serverURL else { return nil } + let branch = ref ?? repository.defaultBranch ?? "main" + let encodedBranch = branch.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? branch + let directory = filePath.contains("/") + ? filePath.components(separatedBy: "/").dropLast() + .map { $0.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? $0 } + .joined(separator: "/") + "/" + : "" + return URL(string: "\(serverURL)/\(repository.fullName)/src/branch/\(encodedBranch)/\(directory)") + } + + #if DEBUG + init(preview _: Void, repository: Repository, filePath: String, fileName: String, + authService: AuthenticationService, ref: String? = nil, + fileContent: String? = nil, fileSha: String? = nil) + { + self.repository = repository + self.filePath = filePath + self.fileName = fileName + self.authService = authService + self.ref = ref + repositoryService = nil + _fileContent = State(initialValue: fileContent) + _fileSha = State(initialValue: fileSha) + } + #endif + + private func loadFile() async { + guard let repositoryService else { return } + isLoading = true + errorMessage = nil + + do { + let fileContentData = try await repositoryService.fetchFileContent( + owner: repository.owner, + repo: repository.repoName, + path: filePath, + ref: ref, + ) + + fileContent = fileContentData.decodedContent + fileSha = fileContentData.sha + + } catch { + errorMessage = error.localizedDescription + showError = true + } + + isLoading = false + } +} + +struct CommitSheetView: View { + let fileName: String + let repositoryService: RepositoryService + let repository: Repository + let filePath: String + let editedContent: String + let fileSha: String + let branch: String? + let onCommit: (FileContent) async -> Void + + @Environment(\.dismiss) private var dismiss + @State private var commitMessage: String = "" + @State private var isCommitting = false + @State private var errorMessage: String? + @State private var showError = false + + var body: some View { + NavigationStack { + Form { + Section("Commit Message") { + TextField("Update \(fileName)", text: $commitMessage) + .accessibilityIdentifier("commit-message-field") + } + } + .navigationTitle("Commit Changes") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + dismiss() + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Commit") { + Task { + await commit() + } + } + .disabled(isCommitting) + .accessibilityIdentifier("commit-submit") + } + } + .errorAlert(message: $errorMessage, isPresented: $showError) + } + } + + private func commit() async { + isCommitting = true + + do { + let message = commitMessage.isEmpty ? "Update \(fileName)" : commitMessage + + let updatedFile = try await repositoryService.updateFile( + owner: repository.owner, + repo: repository.repoName, + path: filePath, + content: editedContent, + sha: fileSha, + message: message, + branch: branch, + ) + + await onCommit(updatedFile) + dismiss() + return + } catch { + errorMessage = error.localizedDescription + showError = true + } + + isCommitting = false + } +} + +#Preview("Markdown") { + NavigationStack { + FileViewerView( + preview: (), + repository: .preview, + filePath: "README.md", + fileName: "README.md", + authService: .previewDefault, + fileContent: """ + # test-repo + + A sample repository for testing. + + ## Getting Started + + ```bash + git clone https://example.com/testadmin/test-repo.git + cd test-repo + ``` + + ## Features + + - Written in **Python** + - Includes `hello.py` and `src/main.py` + - Licensed under MIT + """, + fileSha: "abc123", + ) + } +} + +#Preview("Code") { + NavigationStack { + FileViewerView( + preview: (), + repository: .preview, + filePath: "hello.py", + fileName: "hello.py", + authService: .previewDefault, + fileContent: """ + #!/usr/bin/env python3 + + def greet(name: str) -> str: + \"\"\"Return a greeting message.\"\"\" + return f"Hello, {name}!" + + if __name__ == "__main__": + print(greet("World")) + """, + fileSha: "def456", + ) + } +} diff --git a/Forji/Forji/Views/FloatingButtons.swift b/Forji/Forji/Views/FloatingButtons.swift new file mode 100644 index 0000000..af60d98 --- /dev/null +++ b/Forji/Forji/Views/FloatingButtons.swift @@ -0,0 +1,45 @@ +import SwiftUI + +struct FloatingCreateButton: View { + let action: () -> Void + + var body: some View { + Button(action: action) { + Image(systemName: "plus") + .font(.title2.bold()) + .foregroundStyle(.white) + .frame(width: 56, height: 56) + } + .glassEffect(.regular.tint(.blue).interactive()) + .padding(.trailing, 20) + .padding(.bottom, 20) + } +} + +struct ExpandableActionMenu: View { + @Binding var isExpanded: Bool + @ViewBuilder let content: () -> Content + + var body: some View { + VStack(alignment: .trailing, spacing: 10) { + if isExpanded { + content() + } + + Button { + withAnimation(.snappy) { + isExpanded.toggle() + } + } label: { + Image(systemName: isExpanded ? "xmark" : "wrench.and.screwdriver") + .font(.body) + .frame(width: 40, height: 40) + } + .buttonStyle(.glass) + .accessibilityIdentifier("action-menu-toggle") + .accessibilityValue(isExpanded ? "expanded" : "collapsed") + } + .padding(.trailing, 20) + .padding(.bottom, 20) + } +} diff --git a/Forji/Forji/Views/FlowLayout.swift b/Forji/Forji/Views/FlowLayout.swift new file mode 100644 index 0000000..78861f4 --- /dev/null +++ b/Forji/Forji/Views/FlowLayout.swift @@ -0,0 +1,46 @@ +import SwiftUI + +struct FlowLayout: Layout { + var spacing: CGFloat = 4 + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache _: inout ()) -> CGSize { + let result = arrange(proposal: proposal, subviews: subviews) + return result.size + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache _: inout ()) { + let result = arrange(proposal: proposal, subviews: subviews) + for (index, position) in result.positions.enumerated() { + subviews[index].place( + at: CGPoint(x: bounds.minX + position.x, y: bounds.minY + position.y), + proposal: .unspecified, + ) + } + } + + private func arrange(proposal: ProposedViewSize, subviews: Subviews) -> (positions: [CGPoint], size: CGSize) { + let maxWidth = proposal.width ?? .infinity + var positions: [CGPoint] = [] + var xPos: CGFloat = 0 + var yPos: CGFloat = 0 + var rowHeight: CGFloat = 0 + var totalHeight: CGFloat = 0 + var totalWidth: CGFloat = 0 + + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + if xPos + size.width > maxWidth, xPos > 0 { + xPos = 0 + yPos += rowHeight + spacing + rowHeight = 0 + } + positions.append(CGPoint(x: xPos, y: yPos)) + rowHeight = max(rowHeight, size.height) + xPos += size.width + spacing + totalWidth = max(totalWidth, xPos - spacing) + totalHeight = yPos + rowHeight + } + + return (positions, CGSize(width: totalWidth, height: totalHeight)) + } +} diff --git a/Forji/Forji/Views/HomeView.swift b/Forji/Forji/Views/HomeView.swift new file mode 100644 index 0000000..2a69621 --- /dev/null +++ b/Forji/Forji/Views/HomeView.swift @@ -0,0 +1,180 @@ +import ForgejoKit +import SwiftUI + +struct HomeView: View { + @State private var authService: AuthenticationService + @State private var unreadCount = 0 + @State private var selectedTab = 0 + + private let notificationService: NotificationService? + + init(authService: AuthenticationService) { + self.authService = authService + notificationService = authService.client.map { NotificationService(client: $0) } + } + + var body: some View { + TabView(selection: $selectedTab) { + Tab("Repositories", systemImage: "folder.fill", value: 0) { + NavigationStack { + RepositoryListView(authService: authService) + } + } + + Tab("Issues", systemImage: "exclamationmark.circle.fill", value: 1) { + NavigationStack { + IssuesOverviewView(authService: authService) + } + } + + Tab("Pull Requests", systemImage: "arrow.triangle.pull", value: 2) { + NavigationStack { + PullRequestsOverviewView(authService: authService) + } + } + + Tab("Notifications", systemImage: "bell.fill", value: 3) { + NavigationStack { + NotificationsOverviewView(authService: authService) + } + } + .badge(unreadCount) + + Tab("Settings", systemImage: "gearshape", value: 4) { + NavigationStack { + SettingsTabView(authService: authService) + } + } + } + .tabBarMinimizeBehavior(.onScrollDown) + .task { + await loadUnreadCount() + } + .onChange(of: selectedTab) { + Task { await loadUnreadCount() } + } + } + + private func loadUnreadCount() async { + guard let notificationService else { return } + do { + unreadCount = try await notificationService.fetchUnreadCount() + } catch { + // Silently ignore — badge is non-critical + } + } +} + +// MARK: - Settings Tab + +struct SettingsTabView: View { + @AppStorage("appearance") private var appearance: AppAppearance = .system + @State private var authService: AuthenticationService + @Environment(\.modelContext) private var modelContext + + init(authService: AuthenticationService) { + self.authService = authService + } + + var body: some View { + List { + Section("Appearance") { + Picker("Theme", selection: $appearance) { + Text("System").tag(AppAppearance.system) + Text("Light").tag(AppAppearance.light) + Text("Dark").tag(AppAppearance.dark) + } + .pickerStyle(.inline) + .labelsHidden() + } + + if let user = authService.currentUser { + Section { + VStack(alignment: .leading, spacing: 8) { + Text(user.fullName ?? user.login) + .font(.headline) + Text("@\(user.login)") + .font(.subheadline) + .foregroundStyle(.secondary) + if let email = user.email { + Text(email) + .font(.caption) + .foregroundStyle(.secondary) + } + } + .padding(.vertical, 8) + } + } + + if let instance = authService.currentInstance { + Section("Instance") { + LabeledContent("Server", value: instance.serverURL) + LabeledContent("User", value: instance.username) + } + } + + Section { + Button { + authService.disconnect() + } label: { + Label("Switch Instance", systemImage: "arrow.triangle.swap") + } + .accessibilityIdentifier("home-switch-instance-button") + + Button(role: .destructive) { + Task { await authService.logout(modelContext: modelContext) } + } label: { + Label("Logout", systemImage: "rectangle.portrait.and.arrow.right") + } + .accessibilityIdentifier("home-logout-button") + } + + Section("About") { + VStack(alignment: .leading, spacing: 8) { + Text("License") + .font(.subheadline) + .foregroundStyle(.secondary) + // swiftlint:disable:next line_length + Text("Forji is free software licensed under the GNU General Public License v3.0 or later (GPLv3+). You are free to use, modify, and redistribute it under the terms of that license.") + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(.vertical, 4) + + LabeledContent("Copyright", value: "Stefan Hausotte") + + VStack(alignment: .leading, spacing: 8) { + Text("Logo") + .font(.subheadline) + .foregroundStyle(.secondary) + Text("The Forji logo is based on the Forgejo logo by Caesar Schinas, licensed under CC BY-SA 4.0.") + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(.vertical, 4) + + VStack(alignment: .leading, spacing: 8) { + Text("Libraries") + .font(.subheadline) + .foregroundStyle(.secondary) + VStack(alignment: .leading, spacing: 4) { + Text("ForgejoKit (MIT)") + Link("Textual (MIT)", destination: URL(string: "https://github.com/gonzalezreal/textual")!) + Link( + "HighlightSwift (MIT)", + destination: URL(string: "https://github.com/appstefan/HighlightSwift")!, + ) + } + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(.vertical, 4) + } + } + .navigationTitle("Settings") + } +} + +#Preview { + HomeView(authService: .previewDefault) +} diff --git a/Forji/Forji/Views/InlineCommentSheet.swift b/Forji/Forji/Views/InlineCommentSheet.swift new file mode 100644 index 0000000..4092a23 --- /dev/null +++ b/Forji/Forji/Views/InlineCommentSheet.swift @@ -0,0 +1,139 @@ +import ForgejoKit +import SwiftUI + +struct DiffLineHeader: View { + let path: String + let line: DiffLine + + var body: some View { + VStack(spacing: 8) { + HStack(spacing: 8) { + Image(systemName: "doc.text") + .font(.caption) + .foregroundStyle(.secondary) + Text(path) + .font(.system(.caption, design: .monospaced)) + Spacer() + if let oldLine = line.oldLineNumber { + Text("L\(oldLine)") + .font(.caption) + .foregroundStyle(.secondary) + } + if let newLine = line.newLineNumber { + Text("L\(newLine)") + .font(.caption) + .foregroundStyle(.secondary) + } + } + + Text(line.content) + .font(.system(.caption2, design: .monospaced)) + .foregroundStyle(line.type.color) + .padding(6) + .frame(maxWidth: .infinity, alignment: .leading) + .background(line.type.backgroundColor) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + .padding() + } +} + +struct InlineCommentSheet: View { + let context: InlineCommentContext + let prService: PullRequestService + let owner: String + let repo: String + let prNumber: Int + let isOwnPR: Bool + var onSubmitted: (() -> Void)? + + @State private var commentBody = "" + @State private var reviewEvent: ReviewEvent = .comment + @State private var isSubmitting = false + @State private var errorMessage: String? + @State private var showError = false + @Environment(\.dismiss) private var dismiss + + private var line: DiffLine { + context.line + } + + private var path: String { + context.path + } + + var body: some View { + NavigationStack { + VStack(spacing: 0) { + DiffLineHeader(path: path, line: line) + + if !isOwnPR { + Picker("Type", selection: $reviewEvent) { + Label(ReviewEvent.comment.title, systemImage: ReviewEvent.comment.systemImage) + .tag(ReviewEvent.comment) + Label(ReviewEvent.requestChanges.title, systemImage: ReviewEvent.requestChanges.systemImage) + .tag(ReviewEvent.requestChanges) + } + .pickerStyle(.segmented) + .padding(.horizontal) + } + + TextEditor(text: $commentBody) + .frame(maxHeight: .infinity) + .scrollContentBackground(.hidden) + .padding(8) + .background(Color(.tertiarySystemFill)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .padding(.horizontal) + .padding(.bottom) + } + .navigationTitle("Inline Comment") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button("Submit") { + Task { await submit() } + } + .disabled(commentBody.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isSubmitting) + } + } + .errorAlert(message: $errorMessage, isPresented: $showError) + } + } + + private func submit() async { + guard line.diffPosition != nil else { + errorMessage = "Cannot comment on this line (no diff position)" + showError = true + return + } + isSubmitting = true + do { + let oldPosition = line.type == .deletion ? line.diffPosition : nil + let newPosition = line.type == .deletion ? nil : line.diffPosition + let inlineComment = CreateReviewComment( + body: commentBody, + path: path, + oldPosition: oldPosition, + newPosition: newPosition, + ) + _ = try await prService.createReview( + owner: owner, + repo: repo, + index: prNumber, + body: "", + event: reviewEvent.rawValue, + comments: [inlineComment], + ) + onSubmitted?() + dismiss() + } catch { + errorMessage = error.localizedDescription + showError = true + } + isSubmitting = false + } +} diff --git a/Forji/Forji/Views/InstanceFormView.swift b/Forji/Forji/Views/InstanceFormView.swift new file mode 100644 index 0000000..ff2a4fe --- /dev/null +++ b/Forji/Forji/Views/InstanceFormView.swift @@ -0,0 +1,267 @@ +import ForgejoKit +import SwiftData +import SwiftUI + +struct InstanceFormView: View { + enum Mode: Identifiable { + case add + case edit(ForgejoInstance) + + var id: String { + switch self { + case .add: "add" + case let .edit(instance): instance.serverURL + instance.username + } + } + } + + @Environment(\.modelContext) private var modelContext + @Environment(\.dismiss) private var dismiss + @Query(sort: \ForgejoInstance.lastUsed, order: .reverse) private var instances: [ForgejoInstance] + @State private var authService: AuthenticationService + + let mode: Mode + + #if DEBUG + @AppStorage("dev_serverURL") private var devServerURL = "" + @AppStorage("dev_username") private var devUsername = "" + @AppStorage("dev_password") private var devPassword = "" + @AppStorage("dev_allowSelfSigned") private var devAllowSelfSigned = false + #endif + + @State private var name = "" + @State private var serverURL = "" + @State private var username = "" + @State private var password = "" + @State private var allowSelfSignedCertificates = false + @State private var isDefault = false + @State private var otpCode = "" + @State private var needsOTP = false + @State private var isLoading = false + @State private var errorMessage: String? + @State private var showError = false + + init(authService: AuthenticationService, mode: Mode) { + self.authService = authService + self.mode = mode + } + + var body: some View { + NavigationStack { + Form { + Section { + TextField("Name (optional)", text: $name) + .autocorrectionDisabled() + .accessibilityIdentifier("instance-name-field") + + TextField("Server URL", text: $serverURL) + .textContentType(.URL) + .autocapitalization(.none) + .autocorrectionDisabled() + .keyboardType(.URL) + .accessibilityIdentifier("login-serverURL-field") + + TextField("Username", text: $username) + .textContentType(.username) + .autocapitalization(.none) + .autocorrectionDisabled() + .accessibilityIdentifier("login-username-field") + + SecureField("Password", text: $password) + .textContentType(.password) + .accessibilityIdentifier("login-password-field") + + if needsOTP { + TextField("One-Time Code", text: $otpCode) + .keyboardType(.numberPad) + .textContentType(.oneTimeCode) + .accessibilityIdentifier("login-otp-field") + } + } header: { + Text("Forgejo Instance") + } footer: { + if needsOTP { + Text("Enter the 6-digit code from your authenticator app.") + } else { + switch mode { + case .add: + Text("Enter your Forgejo server URL (e.g., forgejo.example.com)") + case .edit: + Text("Leave password blank to keep the existing one.") + } + } + } + + Section { + Toggle("Accept Self-Signed Certificates", isOn: $allowSelfSignedCertificates) + } footer: { + Text("Enable this if your Forgejo instance uses a self-signed SSL certificate.") + .font(.caption) + } + + Section { + Toggle("Default Instance", isOn: $isDefault) + } footer: { + Text("The default instance will be connected automatically on launch.") + .font(.caption) + } + + Section { + Button(action: handleSave) { + HStack { + Spacer() + if isLoading { + ProgressView() + .progressViewStyle(.circular) + } else { + Text(isAddMode ? "Login" : "Save") + .fontWeight(.semibold) + } + Spacer() + } + } + .buttonStyle(.borderedProminent) + .disabled( + isLoading || serverURL.isEmpty || username.isEmpty + || (isAddMode && password.isEmpty) || (needsOTP && otpCode.isEmpty), + ) + .listRowBackground(Color.clear) + .accessibilityIdentifier("login-button") + } + } + .navigationTitle(isAddMode ? "Add Instance" : "Edit Instance") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + } + .errorAlert("Login Failed", message: $errorMessage, isPresented: $showError) + .onAppear { + populateFields() + } + } + } + + private var isAddMode: Bool { + if case .add = mode { return true } + return false + } + + private func populateFields() { + switch mode { + case .add: + #if DEBUG + if !devServerURL.isEmpty { serverURL = devServerURL } + if !devUsername.isEmpty { username = devUsername } + if !devPassword.isEmpty { password = devPassword } + allowSelfSignedCertificates = devAllowSelfSigned + #endif + case let .edit(instance): + name = instance.name + serverURL = instance.serverURL + username = instance.username + allowSelfSignedCertificates = instance.allowSelfSignedCertificates + isDefault = instance.isDefault + } + } + + private func performLogin() async throws { + if needsOTP, !otpCode.isEmpty { + try await authService.loginWithOTP( + serverURL: serverURL, + username: username, + password: password, + otp: otpCode, + allowSelfSigned: allowSelfSignedCertificates, + ) + } else { + try await authService.login( + serverURL: serverURL, + username: username, + password: password, + allowSelfSigned: allowSelfSignedCertificates, + ) + } + } + + // swiftlint:disable:next function_body_length cyclomatic_complexity + private func handleSave() { + isLoading = true + errorMessage = nil + + Task { + do { + switch mode { + case .add: + try await performLogin() + + if isDefault { + for inst in instances where inst.isDefault { + inst.isDefault = false + } + } + + let normalizedURL = ForgejoClient.normalizeServerURL(serverURL) + let instance = ForgejoInstance( + serverURL: normalizedURL, + username: username, + name: name, + isDefault: isDefault, + allowSelfSignedCertificates: allowSelfSignedCertificates, + ) + modelContext.insert(instance) + do { + try modelContext.save() + } catch { + assertionFailure("SwiftData save failed: \(error)") + } + authService.currentInstance = instance + + case let .edit(instance): + if !password.isEmpty { + try await performLogin() + } + + instance.name = name + instance.serverURL = ForgejoClient.normalizeServerURL(serverURL) + instance.username = username + instance.allowSelfSignedCertificates = allowSelfSignedCertificates + + if isDefault, !instance.isDefault { + for inst in instances where inst.isDefault { + inst.isDefault = false + } + } + instance.isDefault = isDefault + + do { + try modelContext.save() + } catch { + assertionFailure("SwiftData save failed: \(error)") + } + authService.currentInstance = instance + } + + dismiss() + } catch AuthenticationError.otpRequired { + needsOTP = true + } catch { + errorMessage = error.localizedDescription + showError = true + } + + isLoading = false + } + } +} + +#Preview("Add") { + InstanceFormView(authService: .previewDefault, mode: .add) + .modelContainer(for: ForgejoInstance.self, inMemory: true) +} + +#Preview("Edit") { + InstanceFormView(authService: .previewDefault, mode: .edit(.preview)) + .modelContainer(for: ForgejoInstance.self, inMemory: true) +} diff --git a/Forji/Forji/Views/InstanceListView.swift b/Forji/Forji/Views/InstanceListView.swift new file mode 100644 index 0000000..ed40d4d --- /dev/null +++ b/Forji/Forji/Views/InstanceListView.swift @@ -0,0 +1,181 @@ +import ForgejoKit +import SwiftData +import SwiftUI + +struct InstanceListView: View { + @Environment(\.modelContext) private var modelContext + @Query(sort: \ForgejoInstance.lastUsed, order: .reverse) private var instances: [ForgejoInstance] + @State private var authService: AuthenticationService + @State private var showAddSheet = false + @State private var editingInstance: ForgejoInstance? + @State private var connectingInstance: ForgejoInstance? + @State private var errorMessage: String? + @State private var showError = false + + init(authService: AuthenticationService) { + self.authService = authService + } + + var body: some View { + NavigationStack { + Group { + if instances.isEmpty { + ContentUnavailableView { + Label("No Instances", systemImage: "server.rack") + .foregroundStyle(.blue) + } description: { + Text("Add your first Forgejo instance to get started.") + } + } else { + List { + ForEach(instances) { instance in + instanceRow(instance) + .swipeActions(edge: .trailing, allowsFullSwipe: false) { + Button(role: .destructive) { + deleteInstance(instance) + } label: { + Label("Delete", systemImage: "trash") + } + } + .swipeActions(edge: .leading, allowsFullSwipe: true) { + Button { + toggleDefault(instance) + } label: { + Label( + instance.isDefault ? "Unset Default" : "Set Default", + systemImage: instance.isDefault ? "star.slash" : "star.fill", + ) + } + .tint(.yellow) + } + } + } + .accessibilityIdentifier("instance-list") + } + } + .navigationTitle("Instances") + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button { + showAddSheet = true + } label: { + Image(systemName: "plus") + } + .accessibilityIdentifier("instance-add-button") + } + } + .sheet(isPresented: $showAddSheet) { + InstanceFormView(authService: authService, mode: .add) + } + .sheet(item: $editingInstance) { instance in + InstanceFormView(authService: authService, mode: .edit(instance)) + } + .errorAlert("Connection Failed", message: $errorMessage, isPresented: $showError) + } + } + + private func instanceRow(_ instance: ForgejoInstance) -> some View { + Button { + connect(to: instance) + } label: { + HStack { + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 8) { + Text(instance.name.isEmpty ? instance.serverURL : instance.name) + .font(.headline) + if instance.isDefault { + Text("Default") + .font(.caption2) + .fontWeight(.medium) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .glassEffect(.regular.tint(.yellow)) + } + } + Text(instance.serverURL) + .font(.subheadline) + .foregroundStyle(.secondary) + Text("@\(instance.username)") + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer() + if connectingInstance?.id == instance.id { + ProgressView() + } else { + Button { + editingInstance = instance + } label: { + Image(systemName: "pencil.circle.fill") + .font(.title3) + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) + .accessibilityIdentifier("instance-edit-button") + } + } + } + .disabled(connectingInstance != nil) + .accessibilityIdentifier("instance-row") + } + + private func connect(to instance: ForgejoInstance) { + connectingInstance = instance + Task { + do { + try await authService.restoreSession(instance: instance) + instance.lastUsed = Date() + do { + try modelContext.save() + } catch { + assertionFailure("SwiftData save failed: \(error)") + } + } catch is KeychainError { + editingInstance = instance + } catch { + errorMessage = error.localizedDescription + showError = true + } + connectingInstance = nil + } + } + + private func deleteInstance(_ instance: ForgejoInstance) { + // Disconnect if deleting the currently active instance + if authService.currentInstance?.id == instance.id { + authService.disconnect() + } + let normalizedURL = ForgejoClient.normalizeServerURL(instance.serverURL) + let username = instance.username + modelContext.delete(instance) + do { + try modelContext.save() + } catch { + assertionFailure("SwiftData save failed: \(error)") + } + Task { + try? await KeychainManager.shared.deletePassword(for: normalizedURL, username: username) + } + } + + private func toggleDefault(_ instance: ForgejoInstance) { + if instance.isDefault { + instance.isDefault = false + } else { + for inst in instances where inst.isDefault { + inst.isDefault = false + } + instance.isDefault = true + } + do { + try modelContext.save() + } catch { + assertionFailure("SwiftData save failed: \(error)") + } + } +} + +#Preview { + InstanceListView(authService: .previewDefault) + .modelContainer(for: ForgejoInstance.self, inMemory: true) +} diff --git a/Forji/Forji/Views/IssueCreateView.swift b/Forji/Forji/Views/IssueCreateView.swift new file mode 100644 index 0000000..7f238f7 --- /dev/null +++ b/Forji/Forji/Views/IssueCreateView.swift @@ -0,0 +1,160 @@ +import ForgejoKit +import SwiftUI + +struct IssueCreateView: View { + let repository: Repository + @State private var authService: AuthenticationService + @State private var title = "" + @State private var bodyText = "" + @State private var isSubmitting = false + @State private var errorMessage: String? + @State private var showError = false + @State private var availableLabels: [IssueLabel] = [] + @State private var availableMilestones: [IssueMilestone] = [] + @State private var availableAssignees: [User] = [] + @State private var selectedLabelIDs: Set = [] + @State private var selectedMilestoneID: Int? + @State private var selectedAssigneeLogins: Set = [] + @Environment(\.dismiss) private var dismiss + + private let issueService: IssueService? + private let repositoryService: RepositoryService? + private let onCreated: () -> Void + private let embeddedInNavigation: Bool + + init( + repository: Repository, + authService: AuthenticationService, + embeddedInNavigation: Bool = false, + onCreated: @escaping () -> Void, + ) { + self.repository = repository + self.authService = authService + issueService = authService.client.map { IssueService(client: $0) } + repositoryService = authService.client.map { RepositoryService(client: $0) } + self.onCreated = onCreated + self.embeddedInNavigation = embeddedInNavigation + } + + private var owner: String { + repository.owner + } + + private var repo: String { + repository.repoName + } + + var body: some View { + if embeddedInNavigation { + formContent + } else { + NavigationStack { + formContent + } + } + } + + private var formContent: some View { + Form { + Section { + TextField("Title", text: $title) + .accessibilityIdentifier("issue-create-title-field") + } + + DescriptionEditorSection(text: $bodyText) + + LabelPickerSection( + availableLabels: availableLabels, + selectedLabelIDs: $selectedLabelIDs, + ) + MilestonePickerSection( + availableMilestones: availableMilestones, + selectedMilestoneID: $selectedMilestoneID, + ) + UserPickerSection( + title: "Assignees", + availableUsers: availableAssignees, + selectedLogins: $selectedAssigneeLogins, + ) + } + .navigationTitle("New Issue") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button("Create") { + Task { await createIssue() } + } + .disabled(title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isSubmitting) + .accessibilityIdentifier("issue-create-submit") + } + } + .errorAlert(message: $errorMessage, isPresented: $showError) + .task { + await loadMetadata() + } + } + + private func loadMetadata() async { + guard let repositoryService else { return } + let metadata = await loadRepositoryMetadata(service: repositoryService, owner: owner, repo: repo) + availableLabels = metadata.labels + availableMilestones = metadata.milestones + availableAssignees = metadata.assignees + } + + private func createIssue() async { + guard let issueService else { return } + isSubmitting = true + do { + _ = try await issueService.createIssue( + owner: owner, + repo: repo, + title: title.trimmingCharacters(in: .whitespacesAndNewlines), + body: bodyText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + ? nil : bodyText.trimmingCharacters(in: .whitespacesAndNewlines), + labels: selectedLabelIDs.isEmpty ? nil : Array(selectedLabelIDs), + milestone: selectedMilestoneID, + assignees: selectedAssigneeLogins.isEmpty ? nil : Array(selectedAssigneeLogins), + ) + onCreated() + dismiss() + } catch { + errorMessage = error.localizedDescription + showError = true + } + isSubmitting = false + } + + #if DEBUG + init(preview _: Void, repository: Repository, authService: AuthenticationService, + availableLabels: [IssueLabel] = [], availableMilestones: [IssueMilestone] = [], + availableAssignees: [User] = []) + { + self.repository = repository + self.authService = authService + issueService = nil + repositoryService = nil + onCreated = {} + embeddedInNavigation = false + _availableLabels = State(initialValue: availableLabels) + _availableMilestones = State(initialValue: availableMilestones) + _availableAssignees = State(initialValue: availableAssignees) + } + #endif +} + +#Preview { + NavigationStack { + IssueCreateView( + preview: (), + repository: .preview, + authService: .previewDefault, + availableLabels: IssueLabel.previewList, + availableMilestones: [.preview], + availableAssignees: [.preview, .previewBot], + ) + } +} diff --git a/Forji/Forji/Views/IssueDetailView.swift b/Forji/Forji/Views/IssueDetailView.swift new file mode 100644 index 0000000..5d5ef38 --- /dev/null +++ b/Forji/Forji/Views/IssueDetailView.swift @@ -0,0 +1,300 @@ +import ForgejoKit +import SwiftUI + +struct IssueDetailView: View { + let repository: Repository + let issueNumber: Int + @State private var authService: AuthenticationService + @State private var issue: Issue? + @State private var comments: [IssueComment] = [] + @State private var isLoading = true + @State private var errorMessage: String? + @State private var showError = false + @State private var showEditSheet = false + @State private var isTogglingState = false + @State private var showCommentSheet = false + @State private var showActionsExpanded = false + + private let issueService: IssueService? + + init(repository: Repository, issueNumber: Int, authService: AuthenticationService) { + self.repository = repository + self.issueNumber = issueNumber + self.authService = authService + issueService = authService.client.map { IssueService(client: $0) } + } + + private var owner: String { + repository.owner + } + + private var repo: String { + repository.repoName + } + + private var hasPushPermission: Bool { + repository.permissions?.push == true || repository.permissions?.admin == true + } + + private var isAuthor: Bool { + guard let currentUser = authService.currentUser?.login else { return false } + return issue?.user.login == currentUser + } + + private var canEditOrClose: Bool { + hasPushPermission || isAuthor + } + + var body: some View { + ZStack(alignment: .bottomTrailing) { + Group { + if isLoading, issue == nil { + ProgressView() + } else if let issue { + List { + // Header + Section { + VStack(alignment: .leading, spacing: 10) { + Text(issue.title) + .font(.title3) + .fontWeight(.bold) + .accessibilityIdentifier("issue-detail-title") + + HStack(spacing: 8) { + HStack(spacing: 4) { + Image( + systemName: issue.stateValue == .open + ? "circle.circle" : "checkmark.circle.fill", + ) + Text(issue.stateValue == .open ? "Open" : "Closed") + } + .font(.caption) + .fontWeight(.medium) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .foregroundStyle(.white) + .glassEffect(.regular.tint(issue.stateValue == .open ? .green : .purple)) + + Text("#\(issue.number)") + .font(.subheadline) + .foregroundStyle(.secondary) + } + + Text("\(issue.user.login) opened \(formatRelativeDate(issue.createdAt))") + .font(.subheadline) + .foregroundStyle(.secondary) + + if !issue.labels.isEmpty { + FlowLayout(spacing: 4) { + ForEach(issue.labels) { label in + IssueLabelView(label: label) + } + } + } + } + .padding(.vertical, 4) + } + .listRowBackground( + (issue.stateValue == .open ? Color.green : Color.purple).opacity(0.08), + ) + + if let milestone = issue.milestone { + MilestoneDisplaySection(milestone: milestone) + } + + if let assignees = issue.assignees, !assignees.isEmpty { + AssigneesDisplaySection(assignees: assignees) + } + + // Body + if let body = issue.body, !body.isEmpty { + Section("Description") { + MarkdownPreview(text: body) + .padding(.vertical, 4) + } + } + + // Comments + if !comments.isEmpty { + Section("Comments (\(comments.count))") { + ForEach(comments) { comment in + CommentView( + comment: comment, + currentUsername: authService.currentUser?.login, + ) { newBody in + await editComment(commentId: comment.id, body: newBody) + } + } + } + } + + // Spacer so content isn't hidden behind floating bar + Section {} footer: { + Spacer().frame(height: 60) + } + } + .listStyle(.insetGrouped) + } + } + + // Floating glass action cluster + if let currentIssue = issue { + ExpandableActionMenu(isExpanded: $showActionsExpanded) { + Button { showCommentSheet = true } label: { + Label("Comment", systemImage: "text.bubble") + } + .buttonStyle(.glassProminent) + .transition(.move(edge: .bottom).combined(with: .opacity)) + .accessibilityIdentifier("issue-comment-button") + + if canEditOrClose { + Button { showEditSheet = true } label: { + Label("Edit", systemImage: "pencil") + } + .buttonStyle(.glassProminent) + .transition(.move(edge: .bottom).combined(with: .opacity)) + .accessibilityIdentifier("issue-edit-button") + + Button { Task { await toggleIssueState() } } label: { + if isTogglingState { + ProgressView() + .controlSize(.small) + } else { + Label( + currentIssue.stateValue == .open ? "Close" : "Reopen", + systemImage: currentIssue.stateValue == .open + ? "xmark.circle" : "arrow.uturn.left.circle", + ) + } + } + .buttonStyle(.glassProminent) + .tint(currentIssue.stateValue == .open ? .purple : .green) + .disabled(isTogglingState) + .transition(.move(edge: .bottom).combined(with: .opacity)) + .accessibilityIdentifier("issue-toggle-state") + } + } + } + } + .navigationTitle("#\(issueNumber)") + .navigationBarTitleDisplayMode(.inline) + .sheet(isPresented: $showEditSheet) { + if let issue { + IssueEditView( + repository: repository, + issue: issue, + authService: authService, + ) { updatedIssue in + self.issue = updatedIssue + } + } + } + .sheet(isPresented: $showCommentSheet) { + CommentSheet(users: []) { body in + guard let issueService else { throw URLError(.userAuthenticationRequired) } + let comment = try await issueService.createComment( + owner: owner, + repo: repo, + index: issueNumber, + body: body, + ) + comments.append(comment) + } + } + .task { + await loadData() + } + .errorAlert(message: $errorMessage, isPresented: $showError) + } + + private func loadData() async { + guard let issueService else { return } + isLoading = true + do { + async let fetchedIssue = issueService.fetchIssue(owner: owner, repo: repo, index: issueNumber) + async let fetchedComments = issueService.fetchComments(owner: owner, repo: repo, index: issueNumber) + let loadedIssue = try await fetchedIssue + let loadedComments = try await fetchedComments + try Task.checkCancellation() + issue = loadedIssue + comments = loadedComments + } catch is CancellationError { + // Ignore cancellation + } catch { + errorMessage = error.localizedDescription + showError = true + } + isLoading = false + } + + private func toggleIssueState() async { + guard let issueService else { return } + guard let issue else { return } + isTogglingState = true + + do { + let newState = issue.stateValue == .open ? IssueState.closed.rawValue : IssueState.open.rawValue + let updated = try await issueService.editIssue( + owner: owner, + repo: repo, + index: issueNumber, + title: nil, + body: nil, + state: newState, + ) + self.issue = updated + } catch { + errorMessage = error.localizedDescription + showError = true + } + + isTogglingState = false + } + + private func editComment(commentId: Int, body: String) async -> Bool { + guard let issueService else { return false } + do { + let updated = try await issueService.editComment( + owner: owner, + repo: repo, + commentId: commentId, + body: body, + ) + if let index = comments.firstIndex(where: { $0.id == commentId }) { + comments[index] = updated + } + return true + } catch { + errorMessage = error.localizedDescription + showError = true + return false + } + } + + #if DEBUG + init(preview _: Void, repository: Repository, issueNumber: Int, authService: AuthenticationService, + issue: Issue, comments: [IssueComment] = []) + { + self.repository = repository + self.issueNumber = issueNumber + self.authService = authService + issueService = nil + _issue = State(initialValue: issue) + _comments = State(initialValue: comments) + _isLoading = State(initialValue: false) + } + #endif +} + +#Preview { + NavigationStack { + IssueDetailView( + preview: (), + repository: .preview, + issueNumber: 1, + authService: .previewDefault, + issue: .preview, + comments: [.preview], + ) + } +} diff --git a/Forji/Forji/Views/IssueEditView.swift b/Forji/Forji/Views/IssueEditView.swift new file mode 100644 index 0000000..e072202 --- /dev/null +++ b/Forji/Forji/Views/IssueEditView.swift @@ -0,0 +1,169 @@ +import ForgejoKit +import SwiftUI + +struct IssueEditView: View { + let repository: Repository + let issue: Issue + @State private var authService: AuthenticationService + @State private var title: String + @State private var bodyText: String + @State private var isOpen: Bool + @State private var isSubmitting = false + @State private var errorMessage: String? + @State private var showError = false + @State private var availableLabels: [IssueLabel] = [] + @State private var availableMilestones: [IssueMilestone] = [] + @State private var availableAssignees: [User] = [] + @State private var selectedLabelIDs: Set + @State private var selectedMilestoneID: Int? + @State private var selectedAssigneeLogins: Set + @Environment(\.dismiss) private var dismiss + + private let issueService: IssueService? + private let repositoryService: RepositoryService? + private let onSaved: (Issue) -> Void + + init(repository: Repository, issue: Issue, authService: AuthenticationService, onSaved: @escaping (Issue) -> Void) { + self.repository = repository + self.issue = issue + self.authService = authService + issueService = authService.client.map { IssueService(client: $0) } + repositoryService = authService.client.map { RepositoryService(client: $0) } + self.onSaved = onSaved + _title = State(initialValue: issue.title) + _bodyText = State(initialValue: issue.body ?? "") + _isOpen = State(initialValue: issue.stateValue == .open) + _selectedLabelIDs = State(initialValue: Set(issue.labels.map(\.id))) + _selectedMilestoneID = State(initialValue: issue.milestone?.id) + _selectedAssigneeLogins = State(initialValue: Set((issue.assignees ?? []).map(\.login))) + } + + private var owner: String { + repository.owner + } + + private var repo: String { + repository.repoName + } + + var body: some View { + NavigationStack { + Form { + Section { + TextField("Title", text: $title) + .accessibilityIdentifier("issue-edit-title-field") + } + + DescriptionEditorSection(text: $bodyText) + + LabelPickerSection( + availableLabels: availableLabels, + selectedLabelIDs: $selectedLabelIDs, + ) + MilestonePickerSection( + availableMilestones: availableMilestones, + selectedMilestoneID: $selectedMilestoneID, + ) + UserPickerSection( + title: "Assignees", + availableUsers: availableAssignees, + selectedLogins: $selectedAssigneeLogins, + ) + + Section { + Toggle("Open", isOn: $isOpen) + } + } + .navigationTitle("Edit Issue") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button("Save") { + Task { await saveIssue() } + } + .disabled(title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isSubmitting) + .accessibilityIdentifier("issue-edit-save") + } + } + .errorAlert(message: $errorMessage, isPresented: $showError) + .task { + await loadMetadata() + } + } + } + + private func loadMetadata() async { + guard let repositoryService else { return } + let metadata = await loadRepositoryMetadata(service: repositoryService, owner: owner, repo: repo) + availableLabels = metadata.labels + availableMilestones = metadata.milestones + availableAssignees = metadata.assignees + } + + private func saveIssue() async { + guard let issueService else { return } + isSubmitting = true + do { + try await issueService.replaceLabels( + owner: owner, + repo: repo, + index: issue.number, + labelIDs: Array(selectedLabelIDs), + ) + let updatedIssue = try await issueService.editIssue( + owner: owner, + repo: repo, + index: issue.number, + title: title.trimmingCharacters(in: .whitespacesAndNewlines), + body: bodyText.trimmingCharacters(in: .whitespacesAndNewlines), + state: isOpen ? IssueState.open.rawValue : IssueState.closed.rawValue, + milestone: selectedMilestoneID ?? 0, // 0 clears the milestone per Forgejo API + assignees: Array(selectedAssigneeLogins), + ) + onSaved(updatedIssue) + dismiss() + } catch { + errorMessage = error.localizedDescription + showError = true + } + isSubmitting = false + } + + #if DEBUG + init(preview _: Void, repository: Repository, issue: Issue, authService: AuthenticationService, + availableLabels: [IssueLabel] = [], availableMilestones: [IssueMilestone] = [], + availableAssignees: [User] = []) + { + self.repository = repository + self.issue = issue + self.authService = authService + issueService = nil + repositoryService = nil + onSaved = { _ in } + _title = State(initialValue: issue.title) + _bodyText = State(initialValue: issue.body ?? "") + _isOpen = State(initialValue: issue.stateValue == .open) + _selectedLabelIDs = State(initialValue: Set(issue.labels.map(\.id))) + _selectedMilestoneID = State(initialValue: issue.milestone?.id) + _selectedAssigneeLogins = State(initialValue: Set((issue.assignees ?? []).map(\.login))) + _availableLabels = State(initialValue: availableLabels) + _availableMilestones = State(initialValue: availableMilestones) + _availableAssignees = State(initialValue: availableAssignees) + } + #endif +} + +#Preview { + IssueEditView( + preview: (), + repository: .preview, + issue: .preview, + authService: .previewDefault, + availableLabels: IssueLabel.previewList, + availableMilestones: [.preview], + availableAssignees: [.preview, .previewBot], + ) +} diff --git a/Forji/Forji/Views/IssueLabelView.swift b/Forji/Forji/Views/IssueLabelView.swift new file mode 100644 index 0000000..72a2d33 --- /dev/null +++ b/Forji/Forji/Views/IssueLabelView.swift @@ -0,0 +1,59 @@ +import ForgejoKit +import SwiftUI + +struct IssueLabelView: View { + let label: IssueLabel + + var body: some View { + Text(label.name) + .font(.caption2) + .fontWeight(.medium) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .foregroundStyle(textColor) + .glassEffect(.regular.tint(backgroundColor)) + } + + private var backgroundColor: Color { + Color(hex: label.color) ?? .gray + } + + private var textColor: Color { + let hex = label.color.trimmingCharacters(in: CharacterSet(charactersIn: "#")) + guard hex.count == 6, + let rgb = UInt64(hex, radix: 16) + else { + return .white + } + let red = Double((rgb >> 16) & 0xFF) / 255.0 + let green = Double((rgb >> 8) & 0xFF) / 255.0 + let blue = Double(rgb & 0xFF) / 255.0 + let luminance = 0.299 * red + 0.587 * green + 0.114 * blue + return luminance > 0.5 ? .black : .white + } +} + +extension Color { + init?(hex: String) { + let hex = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#")) + guard hex.count == 6, + let rgb = UInt64(hex, radix: 16) + else { + return nil + } + self.init( + red: Double((rgb >> 16) & 0xFF) / 255.0, + green: Double((rgb >> 8) & 0xFF) / 255.0, + blue: Double(rgb & 0xFF) / 255.0, + ) + } +} + +#Preview { + HStack { + ForEach(IssueLabel.previewList) { label in + IssueLabelView(label: label) + } + } + .padding() +} diff --git a/Forji/Forji/Views/IssueListView.swift b/Forji/Forji/Views/IssueListView.swift new file mode 100644 index 0000000..623fffd --- /dev/null +++ b/Forji/Forji/Views/IssueListView.swift @@ -0,0 +1,199 @@ +import ForgejoKit +import SwiftUI + +struct IssueListView: View { + let repository: Repository + @State private var authService: AuthenticationService + @State private var pagination = PaginationState() + @State private var stateFilter: IssueState = .open + @State private var showCreateSheet = false + + private let issueService: IssueService? + + init(repository: Repository, authService: AuthenticationService) { + self.repository = repository + self.authService = authService + issueService = authService.client.map { IssueService(client: $0) } + } + + private var owner: String { + repository.owner + } + + private var repo: String { + repository.repoName + } + + var body: some View { + @Bindable var pagination = pagination + ZStack(alignment: .bottomTrailing) { + VStack(spacing: 0) { + SegmentedPickerSection( + title: "State", + selection: $stateFilter, + options: [("Open", .open), ("Closed", .closed)], + accessibilityIdentifier: "issue-state-picker", + ) + + List { + if pagination.isLoading, pagination.items.isEmpty { + LoadingListSection() + } else if pagination.items.isEmpty { + ContentUnavailableView { + Label( + "No Issues", + systemImage: stateFilter == .open + ? "checkmark.circle" : "exclamationmark.circle", + ) + .foregroundStyle(stateFilter == .open ? .green : .secondary) + } description: { + Text( + stateFilter == .open + ? "All clear — no open issues to review." + : "No closed issues found.", + ) + } + } else { + Section { + ForEach(pagination.items) { issue in + NavigationLink { + IssueDetailView( + repository: repository, + issueNumber: issue.number, + authService: authService, + ) + } label: { + IssueRow(issue: issue) + } + } + + if pagination.hasMore { + ProgressView() + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + .accessibilityIdentifier("load-more-indicator") + .task { + await loadMoreIssues() + } + } + } + } + } + .listStyle(.insetGrouped) + .refreshable { + await reloadIssues().value + } + } + + // Floating glass create button + FloatingCreateButton(action: { showCreateSheet = true }) + .accessibilityIdentifier("issue-create-button") + } + .sheet(isPresented: $showCreateSheet) { + IssueCreateView( + repository: repository, + authService: authService, + ) { + reloadIssues() + } + } + .task { + reloadIssues() + } + .onChange(of: stateFilter) { + reloadIssues(clearItems: true) + } + .errorAlert(message: $pagination.errorMessage, isPresented: $pagination.showError) + } + + @discardableResult + private func reloadIssues(clearItems: Bool = false) -> Task { + guard let issueService else { return Task {} } + return pagination.reload(clearItems: clearItems) { [self] page, limit in + try await issueService.fetchIssues( + owner: owner, + repo: repo, + state: stateFilter.rawValue, + page: page, + limit: limit, + ) + } + } + + private func loadMoreIssues() async { + guard let issueService else { return } + await pagination.loadMore { page, limit in + try await issueService.fetchIssues( + owner: owner, + repo: repo, + state: stateFilter.rawValue, + page: page, + limit: limit, + ) + } + } + + #if DEBUG + init(preview _: Void, repository: Repository, authService: AuthenticationService, issues: [Issue]) { + self.repository = repository + self.authService = authService + issueService = nil + _pagination = State(initialValue: PaginationState(items: issues)) + } + #endif +} + +#Preview { + NavigationStack { + IssueListView( + preview: (), + repository: .preview, + authService: .previewDefault, + issues: [.preview], + ) + } +} + +struct IssueRow: View { + let issue: Issue + + var body: some View { + HStack(alignment: .top, spacing: 10) { + Image(systemName: issue.stateValue == .open ? "circle.circle" : "checkmark.circle.fill") + .foregroundStyle(issue.stateValue == .open ? .green : .purple) + .font(.body) + .padding(.top, 2) + + VStack(alignment: .leading, spacing: 4) { + Text(issue.title) + .font(.body) + .lineLimit(2) + + if let repository = issue.repository { + Text(repository.fullName) + .font(.caption) + .foregroundStyle(.secondary) + } + + HStack(spacing: 6) { + Text("#\(issue.number)") + if issue.comments > 0 { + Label("\(issue.comments)", systemImage: "bubble.right") + } + } + .font(.caption) + .foregroundStyle(.secondary) + + if !issue.labels.isEmpty { + FlowLayout(spacing: 4) { + ForEach(issue.labels) { label in + IssueLabelView(label: label) + } + } + } + } + } + .padding(.vertical, 2) + .stateAccent(issue.stateValue == .open ? .green : .purple) + } +} diff --git a/Forji/Forji/Views/IssuesOverviewView.swift b/Forji/Forji/Views/IssuesOverviewView.swift new file mode 100644 index 0000000..26ac249 --- /dev/null +++ b/Forji/Forji/Views/IssuesOverviewView.swift @@ -0,0 +1,59 @@ +import ForgejoKit +import SwiftUI + +struct IssuesOverviewView: View { + let authService: AuthenticationService + + init(authService: AuthenticationService) { + self.authService = authService + } + + var body: some View { + SearchableOverviewView( + authService: authService, + issueType: "issues", + navigationTitle: "Issues", + searchPrompt: "Search issues", + emptyTitle: "No Issues", + emptyOpenIcon: "checkmark.circle", + emptyClosedIcon: "exclamationmark.circle", + itemNoun: "issues", + createButtonId: "issue-create-button", + showReviewRequested: false, + row: { issue in + IssueRow(issue: issue) + }, + detail: { repository, number, auth in + IssueDetailView( + repository: repository, + issueNumber: number, + authService: auth, + ) + }, + createView: { repo, auth, embedded, onCreated in + IssueCreateView( + repository: repo, + authService: auth, + embeddedInNavigation: embedded, + onCreated: onCreated, + ) + }, + ) + } + + #if DEBUG + init(preview _: Void, authService: AuthenticationService, issues _: [Issue]) { + self.authService = authService + } + #endif +} + +#Preview { + NavigationStack { + IssuesOverviewView( + preview: (), + authService: .previewDefault, + issues: [.previewWithRepo], + ) + } +} diff --git a/Forji/Forji/Views/ListHelpers.swift b/Forji/Forji/Views/ListHelpers.swift new file mode 100644 index 0000000..905eeb7 --- /dev/null +++ b/Forji/Forji/Views/ListHelpers.swift @@ -0,0 +1,47 @@ +import SwiftUI + +struct SegmentedPickerSection: View { + let title: String + @Binding var selection: Selection + let options: [(label: String, tag: Selection)] + let accessibilityIdentifier: String + + var body: some View { + HStack(spacing: 8) { + ForEach(options, id: \.tag) { option in + let isSelected = selection == option.tag + Button { + withAnimation(.snappy(duration: 0.2)) { + selection = option.tag + } + } label: { + Text(option.label) + .font(.subheadline) + .fontWeight(isSelected ? .semibold : .regular) + .padding(.horizontal, 14) + .padding(.vertical, 7) + .frame(maxWidth: .infinity) + .foregroundStyle(isSelected ? .primary : .secondary) + .glassEffect(.regular.tint(isSelected ? .blue : .clear), in: .capsule) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal) + .padding(.vertical, 6) + .accessibilityIdentifier(accessibilityIdentifier) + } +} + +struct LoadingListSection: View { + var body: some View { + Section { + HStack { + Spacer() + ProgressView() + Spacer() + } + .listRowBackground(Color.clear) + } + } +} diff --git a/Forji/Forji/Views/MarkdownComponents.swift b/Forji/Forji/Views/MarkdownComponents.swift new file mode 100644 index 0000000..1c27bee --- /dev/null +++ b/Forji/Forji/Views/MarkdownComponents.swift @@ -0,0 +1,218 @@ +import ForgejoKit +import SwiftUI +import Textual + +enum EditPreviewTab: String, CaseIterable { + case edit = "Edit" + case preview = "Preview" +} + +struct MarkdownEditorField: View { + @Binding var text: String + @Binding var selectedTab: EditPreviewTab + var minHeight: CGFloat = 150 + var showToolbar: Bool = false + + var body: some View { + VStack(spacing: 0) { + Picker("Mode", selection: $selectedTab) { + ForEach(EditPreviewTab.allCases, id: \.self) { tab in + Text(tab.rawValue).tag(tab) + } + } + .pickerStyle(.segmented) + .padding(.bottom, 10) + + if selectedTab == .edit { + VStack(spacing: 0) { + if showToolbar { + MarkdownToolbar(text: $text) + } + TextEditor(text: $text) + .frame(minHeight: minHeight, maxHeight: .infinity) + .scrollContentBackground(.hidden) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .padding(8) + .accessibilityIdentifier("markdown-text-editor") + } + .background(Color(.tertiarySystemFill)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } else { + Group { + if text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + Text("Nothing to preview") + .foregroundStyle(.tertiary) + .frame(maxWidth: .infinity, alignment: .leading) + } else { + MarkdownPreview(text: text) + } + } + .frame(minHeight: minHeight, maxHeight: .infinity, alignment: .topLeading) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(.tertiarySystemFill)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + } + } +} + +private struct MarkdownToolbar: View { + @Binding var text: String + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + toolbarButton("bold", icon: "bold") { wrap("**") } + toolbarButton("italic", icon: "italic") { wrap("_") } + toolbarButton("heading", icon: "number") { prefix("# ") } + toolbarButton("code", icon: "chevron.left.forwardslash.chevron.right") { wrap("`") } + toolbarButton("codeblock", icon: "text.page") { wrapBlock("```") } + toolbarButton("link", icon: "link") { insertLink() } + toolbarButton("list", icon: "list.bullet") { prefix("- ") } + toolbarButton("quote", icon: "text.quote") { prefix("> ") } + toolbarButton("task", icon: "checklist") { prefix("- [ ] ") } + } + .padding(.horizontal, 8) + .padding(.vertical, 6) + } + } + + private func toolbarButton(_ id: String, icon: String, action: @escaping () -> Void) -> some View { + Button(action: action) { + Image(systemName: icon) + .font(.subheadline) + .frame(width: 28, height: 28) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .foregroundStyle(.primary) + .accessibilityIdentifier("markdown-toolbar-\(id)") + } + + private func wrap(_ marker: String) { + text.append("\(marker)text\(marker)") + } + + private func prefix(_ marker: String) { + if text.isEmpty || text.hasSuffix("\n") { + text.append(marker) + } else { + text.append("\n\(marker)") + } + } + + private func wrapBlock(_ fence: String) { + if text.isEmpty || text.hasSuffix("\n") { + text.append("\(fence)\n\n\(fence)") + } else { + text.append("\n\(fence)\n\n\(fence)") + } + } + + private func insertLink() { + text.append("[title](url)") + } +} + +struct MarkdownPreview: View { + let text: String + var baseURL: URL? + var onNavigateToFile: ((String) -> Void)? + + var body: some View { + let segments = MermaidParser.parse(text) + let hasMermaid = segments.contains { if case .mermaid = $0 { true } else { false } } + + if hasMermaid { + VStack(alignment: .leading, spacing: 12) { + ForEach(Array(segments.enumerated()), id: \.offset) { _, segment in + switch segment { + case let .text(markdown): + StructuredText(markdown: markdown, baseURL: baseURL) + .textual.inlineStyle( + InlineStyle() + .code( + .monospaced, .fontScale(0.85), + .foregroundColor(.secondary), + .backgroundColor(.secondary.opacity(0.15)), + ), + ) + .textual.tableStyle(.overflow) + .textual.overflowMode(.scroll) + .textual.textSelection(.enabled) + case let .mermaid(code): + MermaidDiagramView(code: code) + } + } + } + .environment(\.openURL, openURLAction) + } else { + StructuredText(markdown: text, baseURL: baseURL) + .textual.inlineStyle( + InlineStyle() + .code( + .monospaced, .fontScale(0.85), + .foregroundColor(.secondary), + .backgroundColor(.secondary.opacity(0.15)), + ), + ) + .textual.tableStyle(.overflow) + .textual.overflowMode(.scroll) + .textual.textSelection(.enabled) + .environment(\.openURL, openURLAction) + } + } + + private var openURLAction: OpenURLAction { + OpenURLAction { url in + if let onNavigateToFile, let path = repoRelativePath(from: url) { + onNavigateToFile(path) + return .handled + } + return .systemAction + } + } +} + +/// Extracts a repo-relative file path from a Forgejo URL. +/// URL pattern: `{serverURL}/[subpath/]{owner}/{repo}/src/branch/{ref}/{filePath}` +/// Searches for the `src/branch` segment pair to handle servers running under a subpath. +/// Returns the `filePath` portion if the URL matches, `nil` otherwise. +func repoRelativePath(from url: URL) -> String? { + let parts = url.pathComponents.filter { $0 != "/" } + // Find the "src" segment followed by "branch" + guard let srcIndex = parts.indices.first(where: { + parts[$0] == "src" && $0 + 1 < parts.count && parts[$0 + 1] == "branch" + }), + srcIndex + 2 < parts.count // need at least ref after "branch" + else { return nil } + // After "src", "branch", ref comes the file path + let filePathStart = srcIndex + 3 + guard filePathStart < parts.count else { return nil } + return parts.dropFirst(filePathStart).joined(separator: "/") +} + +struct MermaidDiagramView: View { + let code: String + @State private var height: CGFloat = 200 + + var body: some View { + MermaidWebView(code: code, height: $height) + .frame(height: height) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } +} + +#Preview("Editor") { + @Previewable @State var text = "# Hello\n\nThis is **bold** and *italic* text.\n\n- Item 1\n- Item 2" + @Previewable @State var tab: EditPreviewTab = .edit + MarkdownEditorField(text: $text, selectedTab: $tab) + .padding() +} + +#Preview("Preview") { + MarkdownPreview(text: "# Hello\n\nThis is **bold** and *italic* text.\n\n```swift\nlet x = 42\n```") + .padding() +} diff --git a/Forji/Forji/Views/MentionableEditorField.swift b/Forji/Forji/Views/MentionableEditorField.swift new file mode 100644 index 0000000..76904ed --- /dev/null +++ b/Forji/Forji/Views/MentionableEditorField.swift @@ -0,0 +1,88 @@ +import ForgejoKit +import SwiftUI + +struct MentionableEditorField: View { + @Binding var text: String + @Binding var selectedTab: EditPreviewTab + var users: [User] + var minHeight: CGFloat = 150 + var showToolbar: Bool = false + + @State private var mentionQuery: String? + + private var matchingUsers: [User] { + guard let query = mentionQuery, !query.isEmpty else { + return [] + } + return users.filter { $0.login.localizedCaseInsensitiveContains(query) } + } + + var body: some View { + VStack(spacing: 0) { + MarkdownEditorField( + text: $text, + selectedTab: $selectedTab, + minHeight: minHeight, + showToolbar: showToolbar, + ) + + if selectedTab == .edit, mentionQuery != nil, !matchingUsers.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 6) { + ForEach(matchingUsers.prefix(10)) { user in + Button { + insertMention(user.login) + } label: { + Text("@\(user.login)") + .font(.caption) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .glassEffect(.regular) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 4) + .padding(.vertical, 6) + } + } + } + .onChange(of: text) { + mentionQuery = detectMentionQuery(in: text) + } + } + + private func detectMentionQuery(in text: String) -> String? { + // Find the last @ that isn't preceded by a word character + guard let atIndex = text.lastIndex(of: "@") else { return nil } + + // Check that there's no space between @ and cursor (end of string) + let afterAt = text[text.index(after: atIndex)...] + guard !afterAt.contains(" "), !afterAt.contains("\n") else { return nil } + + // Check that @ is at start or preceded by whitespace/newline + if atIndex != text.startIndex { + let before = text[text.index(before: atIndex)] + guard before == " " || before == "\n" || before == "\t" else { return nil } + } + + return String(afterAt) + } + + private func insertMention(_ username: String) { + guard let atIndex = text.lastIndex(of: "@") else { return } + text = String(text[.. Coordinator { + Coordinator(parent: self) + } + + func makeUIView(context: Context) -> WKWebView { + let config = WKWebViewConfiguration() + let controller = WKUserContentController() + controller.add(context.coordinator, name: "heightReporter") + config.userContentController = controller + + let webView = WKWebView(frame: .zero, configuration: config) + webView.isOpaque = false + webView.backgroundColor = .clear + webView.scrollView.backgroundColor = .clear + webView.scrollView.isScrollEnabled = false + webView.navigationDelegate = context.coordinator + return webView + } + + // swiftlint:disable:next function_body_length + func updateUIView(_ webView: WKWebView, context: Context) { + let theme = colorScheme == .dark ? "dark" : "default" + let key = "\(code)\n---\n\(theme)" + guard key != context.coordinator.lastRenderedKey else { return } + context.coordinator.lastRenderedKey = key + + let safeCode = code + .replacingOccurrences(of: "&", with: "&") + .replacingOccurrences(of: "<", with: "<") + .replacingOccurrences(of: ">", with: ">") + .replacingOccurrences(of: "\"", with: """) + .replacingOccurrences(of: "'", with: "'") + + // swiftlint:disable:next line_length + let csp = "default-src 'none'; script-src https://cdn.jsdelivr.net 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:;" + let html = """ + + + + + + + + +

+        \(safeCode)
+        
+ + + + """ + + webView.loadHTMLString(html, baseURL: nil) + } + + static func dismantleUIView(_ webView: WKWebView, coordinator _: Coordinator) { + webView.configuration.userContentController.removeScriptMessageHandler(forName: "heightReporter") + } + + class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate { + let parent: MermaidWebView + var lastRenderedKey: String? + + init(parent: MermaidWebView) { + self.parent = parent + } + + func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) { + if let height = message.body as? CGFloat, height > 0 { + DispatchQueue.main.async { [weak self] in + self?.parent.height = height + } + } + } + + // swiftlint:disable:next line_length + func webView(_: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if navigationAction.navigationType == .linkActivated { + decisionHandler(.cancel) + } else { + decisionHandler(.allow) + } + } + } +} diff --git a/Forji/Forji/Views/MetadataPickers.swift b/Forji/Forji/Views/MetadataPickers.swift new file mode 100644 index 0000000..5495b93 --- /dev/null +++ b/Forji/Forji/Views/MetadataPickers.swift @@ -0,0 +1,330 @@ +import ForgejoKit +import SwiftUI + +// MARK: - Description Editor + +struct DescriptionEditorSection: View { + @Binding var text: String + var title: String = "Description" + @State private var showingEditor = false + + var body: some View { + Section(title) { + Button { + showingEditor = true + } label: { + if text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + Text("None") + .foregroundStyle(.secondary) + } else { + MarkdownPreview(text: text) + .font(.subheadline) + .lineLimit(3) + } + } + .tint(.primary) + .sheet(isPresented: $showingEditor) { + DescriptionEditorSheet(text: $text, title: title) + } + } + } +} + +private struct DescriptionEditorSheet: View { + @Binding var text: String + let title: String + @State private var selectedTab: EditPreviewTab = .edit + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationStack { + MarkdownEditorField( + text: $text, + selectedTab: $selectedTab, + showToolbar: true, + ) + .frame(maxHeight: .infinity) + .padding() + .navigationTitle(title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { dismiss() } + } + } + } + } +} + +// MARK: - Picker Sheet (shared chrome) + +private struct PickerSheet: View { + let title: String + let items: [Item] + let isSelected: (Item) -> Bool + let onToggle: (Item) -> Void + @ViewBuilder let rowContent: (Item) -> RowContent + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationStack { + List { + ForEach(items) { item in + Button { + onToggle(item) + } label: { + HStack { + rowContent(item) + Spacer() + if isSelected(item) { + Image(systemName: "checkmark") + .foregroundStyle(.blue) + } + } + } + .tint(.primary) + } + } + .navigationTitle(title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { dismiss() } + } + } + } + } +} + +// MARK: - Label Picker + +struct LabelPickerSection: View { + let availableLabels: [IssueLabel] + @Binding var selectedLabelIDs: Set + @State private var showingPicker = false + + private var selectedLabels: [IssueLabel] { + availableLabels.filter { selectedLabelIDs.contains($0.id) } + } + + var body: some View { + Section("Labels") { + if availableLabels.isEmpty { + Text("No labels available") + .foregroundStyle(.secondary) + .font(.subheadline) + } else { + Button { + showingPicker = true + } label: { + if selectedLabels.isEmpty { + Text("None") + .foregroundStyle(.secondary) + } else { + FlowLayout(spacing: 4) { + ForEach(selectedLabels) { label in + IssueLabelView(label: label) + } + } + } + } + .tint(.primary) + .sheet(isPresented: $showingPicker) { + PickerSheet( + title: "Labels", + items: availableLabels, + isSelected: { selectedLabelIDs.contains($0.id) }, + onToggle: { toggleLabel($0.id) }, + rowContent: { IssueLabelView(label: $0) }, + ) + } + } + } + } + + private func toggleLabel(_ id: Int) { + if selectedLabelIDs.contains(id) { + selectedLabelIDs.remove(id) + } else { + selectedLabelIDs.insert(id) + } + } +} + +// MARK: - Milestone Picker + +struct MilestonePickerSection: View { + let availableMilestones: [IssueMilestone] + @Binding var selectedMilestoneID: Int? + @State private var showingPicker = false + + private var selectedMilestone: IssueMilestone? { + availableMilestones.first { $0.id == selectedMilestoneID } + } + + var body: some View { + Section("Milestone") { + if availableMilestones.isEmpty { + Text("No milestones available") + .foregroundStyle(.secondary) + .font(.subheadline) + } else { + Button { + showingPicker = true + } label: { + if let milestone = selectedMilestone { + VStack(alignment: .leading) { + Text(milestone.title) + .font(.subheadline) + if let due = milestone.dueOn { + Text("Due \(due, style: .date)") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } else { + Text("None") + .foregroundStyle(.secondary) + } + } + .tint(.primary) + .sheet(isPresented: $showingPicker) { + PickerSheet( + title: "Milestone", + items: availableMilestones, + isSelected: { selectedMilestoneID == $0.id }, + onToggle: { selectMilestone($0.id) }, + rowContent: { milestone in + VStack(alignment: .leading) { + Text(milestone.title) + .font(.subheadline) + if let due = milestone.dueOn { + Text("Due \(due, style: .date)") + .font(.caption) + .foregroundStyle(.secondary) + } + } + }, + ) + } + } + } + } + + private func selectMilestone(_ id: Int) { + if selectedMilestoneID == id { + selectedMilestoneID = nil + } else { + selectedMilestoneID = id + } + } +} + +// MARK: - User Picker (Assignees / Reviewers) + +struct UserPickerSection: View { + let title: String + let availableUsers: [User] + @Binding var selectedLogins: Set + @State private var showingPicker = false + + private var selectedUsers: [User] { + availableUsers.filter { selectedLogins.contains($0.login) } + } + + var body: some View { + Section(title) { + if availableUsers.isEmpty { + Text("No \(title.lowercased()) available") + .foregroundStyle(.secondary) + .font(.subheadline) + } else { + Button { + showingPicker = true + } label: { + if selectedUsers.isEmpty { + Text("None") + .foregroundStyle(.secondary) + } else { + Text(selectedUsers.map { $0.fullName ?? $0.login }.joined(separator: ", ")) + .font(.subheadline) + } + } + .tint(.primary) + .sheet(isPresented: $showingPicker) { + PickerSheet( + title: title, + items: availableUsers, + isSelected: { selectedLogins.contains($0.login) }, + onToggle: { toggleUser($0.login) }, + rowContent: { user in + VStack(alignment: .leading) { + Text(user.fullName ?? user.login) + .font(.subheadline) + if user.fullName != nil { + Text("@\(user.login)") + .font(.caption) + .foregroundStyle(.secondary) + } + } + }, + ) + } + } + } + } + + private func toggleUser(_ login: String) { + if selectedLogins.contains(login) { + selectedLogins.remove(login) + } else { + selectedLogins.insert(login) + } + } +} + +#Preview("Description") { + @Previewable @State var text = "Some **markdown** description" + NavigationStack { + List { + DescriptionEditorSection(text: $text) + } + } +} + +#Preview("Labels") { + @Previewable @State var selected: Set = [1] + NavigationStack { + List { + LabelPickerSection( + availableLabels: IssueLabel.previewList, + selectedLabelIDs: $selected, + ) + } + } +} + +#Preview("Milestones") { + @Previewable @State var selected: Int? + NavigationStack { + List { + MilestonePickerSection( + availableMilestones: [.preview], + selectedMilestoneID: $selected, + ) + } + } +} + +#Preview("Users") { + @Previewable @State var selected: Set = [] + NavigationStack { + List { + UserPickerSection( + title: "Assignees", + availableUsers: [.preview, .previewBot], + selectedLogins: $selected, + ) + } + } +} diff --git a/Forji/Forji/Views/NotificationsOverviewView.swift b/Forji/Forji/Views/NotificationsOverviewView.swift new file mode 100644 index 0000000..49aac15 --- /dev/null +++ b/Forji/Forji/Views/NotificationsOverviewView.swift @@ -0,0 +1,272 @@ +import ForgejoKit +import SwiftUI + +struct NotificationsOverviewView: View { + @State private var authService: AuthenticationService + @State private var pagination = PaginationState() + @State private var statusFilter: String = "unread" + + private let notificationService: NotificationService? + + init(authService: AuthenticationService) { + self.authService = authService + notificationService = authService.client.map { NotificationService(client: $0) } + } + + private var statusTypes: [String] { + switch statusFilter { + case "unread": ["unread"] + case "read": ["read"] + default: ["unread", "read"] + } + } + + private var emptyStateDescription: String { + if statusFilter == "all" { + return "There are no notifications" + } + return "There are no \(statusFilter) notifications" + } + + var body: some View { + @Bindable var pagination = pagination + VStack(spacing: 0) { + SegmentedPickerSection( + title: "Status", + selection: $statusFilter, + options: [("Unread", "unread"), ("Read", "read"), ("All", "all")], + accessibilityIdentifier: "notification-status-picker", + ) + + List { + if pagination.isLoading, pagination.items.isEmpty { + LoadingListSection() + } else if pagination.items.isEmpty { + ContentUnavailableView { + Label("No Notifications", systemImage: "bell.slash") + .foregroundStyle(statusFilter == "unread" ? .green : .secondary) + } description: { + Text(statusFilter == "unread" ? "You're all caught up!" : emptyStateDescription) + } + } else { + Section { + ForEach(pagination.items) { notification in + notificationRow(notification) + .swipeActions(edge: .leading) { + if notification.unread { + Button { + Task { await markAsRead(notification) } + } label: { + Label("Mark Read", systemImage: "envelope.open") + } + .tint(.blue) + } + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button { + Task { await dismissNotification(notification) } + } label: { + Label("Dismiss", systemImage: "xmark") + } + .tint(.gray) + } + } + + if pagination.hasMore { + ProgressView() + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + .accessibilityIdentifier("load-more-indicator") + .task { + await loadMoreNotifications() + } + } + } + } + } + .listStyle(.insetGrouped) + } + .navigationTitle("Notifications") + .refreshable { + await reloadNotifications().value + } + .task { + reloadNotifications() + } + .onChange(of: statusFilter) { + reloadNotifications(clearItems: true) + } + .errorAlert(message: $pagination.errorMessage, isPresented: $pagination.showError) + } + + @ViewBuilder + private func notificationRow(_ notification: NotificationThread) -> some View { + let destination = navigationDestination(for: notification) + if let destination { + NavigationLink { destination } label: { + NotificationRow(notification: notification) + } + } else { + NotificationRow(notification: notification) + } + } + + @ViewBuilder + private func navigationDestination(for notification: NotificationThread) -> (some View)? { + switch notification.subject.type { + case "Issue": + if let number = subjectNumber(from: notification) { + IssueDetailView( + repository: notification.repository, + issueNumber: number, + authService: authService, + ) + } + case "Pull": + if let number = subjectNumber(from: notification) { + PullRequestDetailView( + repository: notification.repository, + prNumber: number, + authService: authService, + ) + } + default: + nil as EmptyView? + } + } + + private func subjectNumber(from notification: NotificationThread) -> Int? { + notificationSubjectNumber(from: notification.subject.url) + ?? notificationSubjectNumber(from: notification.subject.htmlUrl) + } + + @discardableResult + private func reloadNotifications(clearItems: Bool = false) -> Task { + guard let notificationService else { return Task {} } + return pagination.reload(clearItems: clearItems) { [self] page, limit in + try await notificationService.fetchNotifications( + statusTypes: statusTypes, + page: page, + limit: limit, + ) + } + } + + private func loadMoreNotifications() async { + guard let notificationService else { return } + await pagination.loadMore { page, limit in + try await notificationService.fetchNotifications( + statusTypes: statusTypes, + page: page, + limit: limit, + ) + } + } + + private func markAsRead(_ notification: NotificationThread) async { + await setNotificationRead(notification) + } + + private func dismissNotification(_ notification: NotificationThread) async { + await setNotificationRead(notification) + } + + private func setNotificationRead(_ notification: NotificationThread) async { + guard let notificationService else { return } + do { + try await notificationService.markAsRead(id: notification.id) + withAnimation { + if statusFilter == "unread" { + pagination.items.removeAll { $0.id == notification.id } + } else if let index = pagination.items.firstIndex(where: { $0.id == notification.id }) { + pagination.items[index] = NotificationThread( + id: notification.id, + unread: false, + pinned: notification.pinned, + updatedAt: notification.updatedAt, + url: notification.url, + subject: notification.subject, + repository: notification.repository, + ) + } + } + } catch { + pagination.errorMessage = error.localizedDescription + pagination.showError = true + } + } + + #if DEBUG + init(preview _: Void, authService: AuthenticationService, notifications: [NotificationThread]) { + self.authService = authService + notificationService = nil + _pagination = State(initialValue: PaginationState(items: notifications)) + } + #endif +} + +private struct NotificationRow: View { + let notification: NotificationThread + + private var typeIcon: String { + switch notification.subject.type { + case "Issue": "exclamationmark.circle" + case "Pull": "arrow.triangle.pull" + case "Commit": "circle.dotted.circle" + case "Repository": "folder" + default: "bell" + } + } + + private var stateColor: Color { + switch notification.subject.stateValue { + case .open: .green + case .closed, .merged: .purple + default: .secondary + } + } + + var body: some View { + HStack(alignment: .top, spacing: 10) { + Image(systemName: typeIcon) + .foregroundStyle(stateColor) + .font(.body) + .padding(.top, 2) + + VStack(alignment: .leading, spacing: 4) { + Text(notification.subject.title) + .font(.body) + .lineLimit(2) + + Text(notification.repository.fullName) + .font(.caption) + .foregroundStyle(.secondary) + + Text(formatRelativeDate(notification.updatedAt)) + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + if notification.unread { + Circle() + .fill(.blue) + .frame(width: 10, height: 10) + .glassEffect(.regular.tint(.blue)) + .padding(.top, 6) + } + } + .padding(.vertical, 2) + } +} + +#Preview { + NavigationStack { + NotificationsOverviewView( + preview: (), + authService: .previewDefault, + notifications: NotificationThread.previewList, + ) + } +} diff --git a/Forji/Forji/Views/PullRequestCreateView.swift b/Forji/Forji/Views/PullRequestCreateView.swift new file mode 100644 index 0000000..22d14e6 --- /dev/null +++ b/Forji/Forji/Views/PullRequestCreateView.swift @@ -0,0 +1,250 @@ +import ForgejoKit +import SwiftUI + +struct PullRequestCreateView: View { + let repository: Repository + @State private var authService: AuthenticationService + @State private var title = "" + @State private var bodyText = "" + @State private var headBranch = "" + @State private var baseBranch = "" + @State private var branches: [Branch] = [] + @State private var isLoadingBranches = true + @State private var isSubmitting = false + @State private var errorMessage: String? + @State private var showError = false + @State private var availableLabels: [IssueLabel] = [] + @State private var availableMilestones: [IssueMilestone] = [] + @State private var availableAssignees: [User] = [] + @State private var selectedLabelIDs: Set = [] + @State private var selectedMilestoneID: Int? + @State private var selectedAssigneeLogins: Set = [] + @State private var selectedReviewerLogins: Set = [] + @Environment(\.dismiss) private var dismiss + + private let prService: PullRequestService? + private let repositoryService: RepositoryService? + private let onCreated: () -> Void + private let embeddedInNavigation: Bool + + init( + repository: Repository, + authService: AuthenticationService, + embeddedInNavigation: Bool = false, + onCreated: @escaping () -> Void, + ) { + self.repository = repository + self.authService = authService + prService = authService.client.map { PullRequestService(client: $0) } + repositoryService = authService.client.map { RepositoryService(client: $0) } + self.onCreated = onCreated + self.embeddedInNavigation = embeddedInNavigation + _baseBranch = State(initialValue: repository.defaultBranch ?? "main") + } + + private var owner: String { + repository.owner + } + + private var repo: String { + repository.repoName + } + + private var canCreate: Bool { + !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + && !headBranch.isEmpty + && !baseBranch.isEmpty + && headBranch != baseBranch + && !isSubmitting + } + + var body: some View { + if embeddedInNavigation { + formContent + } else { + NavigationStack { + formContent + } + } + } + + private var formContent: some View { + Form { + Section("Branches") { + if isLoadingBranches { + HStack { + Spacer() + ProgressView() + Spacer() + } + } else { + Picker("Head", selection: $headBranch) { + Text("Select branch").tag("") + ForEach(branches) { branch in + Text(branch.name).tag(branch.name) + } + } + + Picker("Base", selection: $baseBranch) { + ForEach(branches) { branch in + Text(branch.name).tag(branch.name) + } + } + + if !headBranch.isEmpty, headBranch == baseBranch { + Label("Head and base branches must be different", systemImage: "exclamationmark.triangle") + .font(.caption) + .foregroundStyle(.orange) + } + } + } + + Section { + TextField("Title", text: $title) + .accessibilityIdentifier("pr-create-title-field") + } + + DescriptionEditorSection(text: $bodyText) + + LabelPickerSection( + availableLabels: availableLabels, + selectedLabelIDs: $selectedLabelIDs, + ) + MilestonePickerSection( + availableMilestones: availableMilestones, + selectedMilestoneID: $selectedMilestoneID, + ) + UserPickerSection( + title: "Assignees", + availableUsers: availableAssignees, + selectedLogins: $selectedAssigneeLogins, + ) + UserPickerSection( + title: "Reviewers", + availableUsers: availableAssignees, + selectedLogins: $selectedReviewerLogins, + ) + } + .navigationTitle("New Pull Request") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button("Create") { + Task { await createPullRequest() } + } + .disabled(!canCreate) + .accessibilityIdentifier("pr-create-submit") + } + } + .errorAlert(message: $errorMessage, isPresented: $showError) + .task { + await loadInitialData() + } + } + + private func loadInitialData() async { + async let branchResult: () = loadBranches() + async let metadataResult: () = loadMetadata() + _ = await (branchResult, metadataResult) + } + + private func loadBranches() async { + guard let repositoryService else { return } + isLoadingBranches = true + do { + branches = try await repositoryService.fetchBranches(owner: owner, repo: repo) + } catch { + errorMessage = error.localizedDescription + showError = true + } + isLoadingBranches = false + } + + private func loadMetadata() async { + guard let repositoryService else { return } + let metadata = await loadRepositoryMetadata(service: repositoryService, owner: owner, repo: repo) + availableLabels = metadata.labels + availableMilestones = metadata.milestones + availableAssignees = metadata.assignees + } + + private func createPullRequest() async { + guard let prService else { return } + isSubmitting = true + do { + let trimmedBody = bodyText.trimmingCharacters(in: .whitespacesAndNewlines) + let createdPR = try await prService.createPullRequest( + owner: owner, + repo: repo, + title: title.trimmingCharacters(in: .whitespacesAndNewlines), + head: headBranch, + base: baseBranch, + body: trimmedBody.isEmpty ? nil : trimmedBody, + labels: selectedLabelIDs.isEmpty ? nil : Array(selectedLabelIDs), + milestone: selectedMilestoneID, + assignees: selectedAssigneeLogins.isEmpty ? nil : Array(selectedAssigneeLogins), + ) + if !selectedReviewerLogins.isEmpty { + do { + try await prService.requestReviewers( + owner: owner, + repo: repo, + index: createdPR.number, + reviewers: Array(selectedReviewerLogins), + ) + } catch { + errorMessage = "PR created, but requesting reviewers failed: \(error.localizedDescription)" + showError = true + onCreated() + isSubmitting = false + return + } + } + onCreated() + dismiss() + } catch { + errorMessage = error.localizedDescription + showError = true + } + isSubmitting = false + } + + #if DEBUG + init(preview _: Void, repository: Repository, authService: AuthenticationService, + branches: [Branch] = [], baseBranch: String = "main", + availableLabels: [IssueLabel] = [], availableMilestones: [IssueMilestone] = [], + availableAssignees: [User] = []) + { + self.repository = repository + self.authService = authService + prService = nil + repositoryService = nil + onCreated = {} + embeddedInNavigation = false + _branches = State(initialValue: branches) + _baseBranch = State(initialValue: baseBranch) + _isLoadingBranches = State(initialValue: false) + _availableLabels = State(initialValue: availableLabels) + _availableMilestones = State(initialValue: availableMilestones) + _availableAssignees = State(initialValue: availableAssignees) + } + #endif +} + +#Preview { + NavigationStack { + PullRequestCreateView( + preview: (), + repository: .preview, + authService: .previewDefault, + branches: Branch.previewList, + baseBranch: "main", + availableLabels: IssueLabel.previewList, + availableMilestones: [.preview], + availableAssignees: [.preview, .previewBot], + ) + } +} diff --git a/Forji/Forji/Views/PullRequestDetailView.swift b/Forji/Forji/Views/PullRequestDetailView.swift new file mode 100644 index 0000000..74837c4 --- /dev/null +++ b/Forji/Forji/Views/PullRequestDetailView.swift @@ -0,0 +1,605 @@ +import ForgejoKit + +// swiftlint:disable file_length +import SwiftUI + +// swiftlint:disable:next type_body_length +struct PullRequestDetailView: View { + let repository: Repository + let prNumber: Int + @State private var authService: AuthenticationService + @State private var pullRequest: PullRequest? + @State private var comments: [IssueComment] = [] + @State private var isLoading = true + @State private var errorMessage: String? + @State private var showError = false + @State private var showEditSheet = false + @State private var showMergeSheet = false + @State private var isTogglingState = false + @State private var showCommentSheet = false + @State private var diffText: String? + @State private var isDiffExpanded = false + @State private var isLoadingDiff = false + + // Reviews + @State private var reviews: [PullRequestReview] = [] + @State private var reviewComments: [Int: [ReviewComment]] = [:] + @State private var parsedDiff: ParsedDiff? + @State private var assignees: [User] = [] + @State private var showSubmitReviewSheet = false + @State private var inlineCommentContext: InlineCommentContext? + @State private var showActionsExpanded = false + + private let prService: PullRequestService? + private let repositoryService: RepositoryService? + + init(repository: Repository, prNumber: Int, authService: AuthenticationService) { + self.repository = repository + self.prNumber = prNumber + self.authService = authService + prService = authService.client.map { PullRequestService(client: $0) } + repositoryService = authService.client.map { RepositoryService(client: $0) } + } + + private var owner: String { + repository.owner + } + + private var repo: String { + repository.repoName + } + + private var isMerged: Bool { + pullRequest?.merged == true + } + + private var hasPushPermission: Bool { + repository.permissions?.push == true || repository.permissions?.admin == true + } + + private var isOwnPR: Bool { + guard let currentUser = authService.currentUser?.login, + let prAuthor = pullRequest?.user.login + else { + return false + } + return currentUser == prAuthor + } + + private var canEditOrClose: Bool { + hasPushPermission || isOwnPR + } + + private var canMerge: Bool { + hasPushPermission + } + + private var status: PRStatusStyle { + PRStatusStyle( + state: pullRequest?.stateValue ?? .closed, + merged: pullRequest?.merged, + draft: pullRequest?.draft, + ) + } + + var body: some View { + ZStack(alignment: .bottomTrailing) { + Group { + if isLoading, pullRequest == nil { + ProgressView() + } else if let activePR = pullRequest { + List { + headerSection(activePR) + + if let milestone = activePR.milestone { + MilestoneDisplaySection(milestone: milestone) + } + + if let prAssignees = activePR.assignees, !prAssignees.isEmpty { + AssigneesDisplaySection(assignees: prAssignees) + } + + requestedReviewersSection(activePR) + + if let body = activePR.body, !body.isEmpty { + Section("Description") { + MarkdownPreview(text: body) + .padding(.vertical, 4) + } + } + + reviewsSection + changesSection + conflictSection(activePR) + commentsSection + + Section {} footer: { + Spacer().frame(height: 60) + } + } + .listStyle(.insetGrouped) + } + } + + if let activePR = pullRequest { + actionMenu(activePR) + } + } + .navigationTitle("#\(prNumber)") + .navigationBarTitleDisplayMode(.inline) + .sheet(isPresented: $showEditSheet) { + if let editingPR = pullRequest { + PullRequestEditView( + repository: repository, + pullRequest: editingPR, + authService: authService, + ) { _ in + Task { await loadData() } + } + } + } + .sheet(isPresented: $showMergeSheet) { + if let mergingPR = pullRequest { + PullRequestMergeView( + repository: repository, + pullRequest: mergingPR, + authService: authService, + ) { + Task { await loadData() } + } + } + } + .sheet(isPresented: $showSubmitReviewSheet) { + PullRequestReviewSheet( + repository: repository, + prNumber: prNumber, + authService: authService, + isOwnPR: isOwnPR, + ) { + Task { await loadData() } + } + } + .sheet(item: $inlineCommentContext) { context in + if let prService { + InlineCommentSheet( + context: context, + prService: prService, + owner: owner, + repo: repo, + prNumber: prNumber, + isOwnPR: isOwnPR, + ) { + Task { await loadReviews() } + } + } + } + .sheet(isPresented: $showCommentSheet) { + CommentSheet(users: assignees) { body in + guard let prService else { throw URLError(.userAuthenticationRequired) } + let comment = try await prService.createComment( + owner: owner, + repo: repo, + index: prNumber, + body: body, + ) + comments.append(comment) + } + } + .task { + await loadData() + } + .errorAlert(message: $errorMessage, isPresented: $showError) + } + + // MARK: - View Sections + + // swiftlint:disable:next function_body_length + private func headerSection(_ pullReq: PullRequest) -> some View { + Section { + VStack(alignment: .leading, spacing: 10) { + Text(pullReq.title) + .font(.title3) + .fontWeight(.bold) + .accessibilityIdentifier("pr-detail-title") + + HStack(spacing: 8) { + HStack(spacing: 4) { + Image(systemName: status.icon) + Text(status.text) + } + .font(.caption) + .fontWeight(.medium) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .foregroundStyle(.white) + .glassEffect(.regular.tint(status.color)) + + Text("#\(pullReq.number)") + .font(.subheadline) + .foregroundStyle(.secondary) + } + + Text("\(pullReq.user.login) opened \(formatRelativeDate(pullReq.createdAt))") + .font(.subheadline) + .foregroundStyle(.secondary) + + HStack(spacing: 4) { + Label(pullReq.head.ref, systemImage: "arrow.branch") + .font(.caption) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .glassEffect(.regular) + + Image(systemName: "arrow.right") + .font(.caption2) + .foregroundStyle(.secondary) + + Text(pullReq.base.ref) + .font(.caption) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .glassEffect(.regular) + + Spacer() + + if !isMerged { + mergeableIndicator(pullReq) + } + } + + if !pullReq.labels.isEmpty { + FlowLayout(spacing: 4) { + ForEach(pullReq.labels) { label in + IssueLabelView(label: label) + } + } + } + } + .padding(.vertical, 4) + } + .listRowBackground(status.color.opacity(0.08)) + } + + @ViewBuilder + private func mergeableIndicator(_ pullReq: PullRequest) -> some View { + if pullReq.mergeable == true { + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(.green) + .font(.caption) + } else if pullReq.mergeable == false { + Image(systemName: "xmark.circle.fill") + .foregroundStyle(.red) + .font(.caption) + } else { + Image(systemName: "questionmark.circle") + .foregroundStyle(.secondary) + .font(.caption) + } + } + + @ViewBuilder + private func requestedReviewersSection(_ pullReq: PullRequest) -> some View { + if let reviewers = pullReq.requestedReviewers, !reviewers.isEmpty { + Section("Requested Reviewers") { + ForEach(reviewers) { user in + HStack(spacing: 8) { + Image(systemName: "eye") + .foregroundStyle(.orange) + Text(user.fullName ?? user.login) + .font(.subheadline) + if user.fullName != nil { + Text("@\(user.login)") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + } + } + } + + @ViewBuilder + private var reviewsSection: some View { + if !reviews.isEmpty { + Section("Reviews (\(reviews.count))") { + ForEach(reviews) { review in + ReviewSummaryView( + review: review, + comments: reviewComments[review.id] ?? [], + ) + } + } + } + } + + private var changesSection: some View { + Section { + DisclosureGroup("Changes", isExpanded: $isDiffExpanded) { + if isLoadingDiff { + HStack { + Spacer() + ProgressView() + Spacer() + } + } else if let parsed = parsedDiff { + DiffView( + diff: parsed, + reviewComments: allReviewComments, + ) { line, path in + inlineCommentContext = InlineCommentContext(line: line, path: path) + } + .listRowInsets(EdgeInsets()) + } + } + .onChange(of: isDiffExpanded) { + if isDiffExpanded, diffText == nil, !isLoadingDiff { + Task { await loadDiff() } + } + } + } + } + + @ViewBuilder + private func conflictSection(_ pullReq: PullRequest) -> some View { + if pullReq.stateValue == .open, pullReq.mergeable == false, !isMerged { + Section { + HStack(spacing: 8) { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundStyle(.orange) + Text("This pull request has conflicts that must be resolved via git or the web interface.") + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + } + } + + @ViewBuilder + private var commentsSection: some View { + if !comments.isEmpty { + Section("Comments (\(comments.count))") { + ForEach(comments) { comment in + CommentView( + comment: comment, + currentUsername: authService.currentUser?.login, + ) { newBody in + await editComment(commentId: comment.id, body: newBody) + } + } + } + } + } + + // swiftlint:disable:next function_body_length + private func actionMenu(_ pullReq: PullRequest) -> some View { + ExpandableActionMenu(isExpanded: $showActionsExpanded) { + Button { showCommentSheet = true } label: { + Label("Comment", systemImage: "text.bubble") + } + .buttonStyle(.glassProminent) + .transition(.move(edge: .bottom).combined(with: .opacity)) + .accessibilityIdentifier("pr-comment-button") + + if canEditOrClose { + Button { showEditSheet = true } label: { + Label("Edit", systemImage: "pencil") + } + .buttonStyle(.glassProminent) + .transition(.move(edge: .bottom).combined(with: .opacity)) + .accessibilityIdentifier("pr-edit-button") + } + + if pullReq.stateValue == .open { + Button { showSubmitReviewSheet = true } label: { + Label("Review", systemImage: "checkmark.message.fill") + } + .buttonStyle(.glassProminent) + .tint(.blue) + .transition(.move(edge: .bottom).combined(with: .opacity)) + .accessibilityIdentifier("pr-submit-review") + } + + if canMerge, pullReq.stateValue == .open, pullReq.draft != true, pullReq.mergeable != false { + Button { showMergeSheet = true } label: { + Label("Merge", systemImage: "arrow.triangle.merge") + } + .buttonStyle(.glassProminent) + .tint(.purple) + .transition(.move(edge: .bottom).combined(with: .opacity)) + .accessibilityIdentifier("pr-merge-button") + } + + if !isMerged, canEditOrClose { + Button { Task { await togglePRState() } } label: { + if isTogglingState { + ProgressView() + .controlSize(.small) + } else { + Label( + pullReq.stateValue == .open ? "Close" : "Reopen", + systemImage: pullReq.stateValue == .open ? "xmark.circle" : "arrow.uturn.left.circle", + ) + } + } + .buttonStyle(.glassProminent) + .tint(pullReq.stateValue == .open ? .red : .green) + .disabled(isTogglingState) + .transition(.move(edge: .bottom).combined(with: .opacity)) + .accessibilityIdentifier("pr-toggle-state") + } + } + } + + // MARK: - Data Loading + + private var allReviewComments: [ReviewComment] { + reviewComments.values.flatMap(\.self) + } + + private func loadData() async { + guard let prService, let repositoryService else { return } + isLoading = true + do { + async let fetchedPR = prService.fetchPullRequest(owner: owner, repo: repo, index: prNumber) + async let fetchedComments = prService.fetchComments(owner: owner, repo: repo, index: prNumber) + async let fetchedReviews = prService.fetchReviews(owner: owner, repo: repo, index: prNumber) + async let fetchedAssignees = repositoryService.fetchAssignees(owner: owner, repo: repo) + + let loadedPR = try await fetchedPR + let loadedComments = try await fetchedComments + let loadedReviews = try await fetchedReviews + let loadedAssignees = await (try? fetchedAssignees) ?? [] + + try Task.checkCancellation() + pullRequest = loadedPR + comments = loadedComments + reviews = loadedReviews + assignees = loadedAssignees + + reviewComments = await fetchAllReviewComments(for: reviews) + + if !allReviewComments.isEmpty { + await loadDiff() + isDiffExpanded = true + } + } catch is CancellationError { + // Ignore cancellation + } catch { + errorMessage = error.localizedDescription + showError = true + } + isLoading = false + } + + private func loadReviews() async { + guard let prService else { return } + do { + reviews = try await prService.fetchReviews(owner: owner, repo: repo, index: prNumber) + reviewComments = await fetchAllReviewComments(for: reviews) + + if !allReviewComments.isEmpty { + if diffText == nil { + await loadDiff() + } + isDiffExpanded = true + } + } catch { + errorMessage = error.localizedDescription + showError = true + } + } + + private func fetchAllReviewComments(for reviews: [PullRequestReview]) async -> [Int: [ReviewComment]] { + guard let prService else { return [:] } + return await withTaskGroup(of: (Int, [ReviewComment])?.self) { group in + for review in reviews { + group.addTask { + guard let comments = try? await prService.fetchReviewComments( + owner: owner, repo: repo, index: prNumber, reviewId: review.id, + ), !comments.isEmpty else { + return nil + } + return (review.id, comments) + } + } + var commentsMap: [Int: [ReviewComment]] = [:] + for await result in group { + if let (reviewId, comments) = result { + commentsMap[reviewId] = comments + } + } + return commentsMap + } + } + + private func loadDiff() async { + guard let prService else { return } + isLoadingDiff = true + do { + let raw = try await prService.fetchDiff(owner: owner, repo: repo, index: prNumber) + try Task.checkCancellation() + diffText = raw + parsedDiff = DiffParser.parse(raw) + } catch is CancellationError { + // Ignore cancellation + } catch { + errorMessage = error.localizedDescription + showError = true + } + isLoadingDiff = false + } + + private func togglePRState() async { + guard let prService, let currentPR = pullRequest else { return } + isTogglingState = true + + do { + let newState = currentPR.stateValue == .open + ? PullRequestState.closed.rawValue : PullRequestState.open.rawValue + let updated = try await prService.editPullRequest( + owner: owner, + repo: repo, + index: prNumber, + title: nil, + body: nil, + state: newState, + ) + pullRequest = updated + } catch { + errorMessage = error.localizedDescription + showError = true + } + + isTogglingState = false + } + + private func editComment(commentId: Int, body: String) async -> Bool { + guard let prService else { return false } + do { + let updated = try await prService.editComment( + owner: owner, + repo: repo, + commentId: commentId, + body: body, + ) + if let index = comments.firstIndex(where: { $0.id == commentId }) { + comments[index] = updated + } + return true + } catch { + errorMessage = error.localizedDescription + showError = true + return false + } + } + + #if DEBUG + init(preview _: Void, repository: Repository, prNumber: Int, authService: AuthenticationService, + pullRequest: PullRequest, comments: [IssueComment] = [], reviews: [PullRequestReview] = []) + { + self.repository = repository + self.prNumber = prNumber + self.authService = authService + prService = nil + repositoryService = nil + _pullRequest = State(initialValue: pullRequest) + _comments = State(initialValue: comments) + _reviews = State(initialValue: reviews) + _isLoading = State(initialValue: false) + } + #endif +} + +#Preview { + NavigationStack { + PullRequestDetailView( + preview: (), + repository: .preview, + prNumber: 4, + authService: .previewDefault, + pullRequest: .preview, + comments: [.preview], + reviews: [.preview], + ) + } +} diff --git a/Forji/Forji/Views/PullRequestEditView.swift b/Forji/Forji/Views/PullRequestEditView.swift new file mode 100644 index 0000000..d5a7ebe --- /dev/null +++ b/Forji/Forji/Views/PullRequestEditView.swift @@ -0,0 +1,216 @@ +import ForgejoKit +import SwiftUI + +struct PullRequestEditView: View { + let repository: Repository + let pullRequest: PullRequest + @State private var authService: AuthenticationService + @State private var title: String + @State private var bodyText: String + @State private var isOpen: Bool + @State private var isSubmitting = false + @State private var errorMessage: String? + @State private var showError = false + @State private var availableLabels: [IssueLabel] = [] + @State private var availableMilestones: [IssueMilestone] = [] + @State private var availableAssignees: [User] = [] + @State private var selectedLabelIDs: Set + @State private var selectedMilestoneID: Int? + @State private var selectedAssigneeLogins: Set + @State private var selectedReviewerLogins: Set + @Environment(\.dismiss) private var dismiss + + private let prService: PullRequestService? + private let repositoryService: RepositoryService? + private let onSaved: (PullRequest) -> Void + + private var isMerged: Bool { + pullRequest.merged == true + } + + init( + repository: Repository, pullRequest: PullRequest, + authService: AuthenticationService, + onSaved: @escaping (PullRequest) -> Void, + ) { + self.repository = repository + self.pullRequest = pullRequest + self.authService = authService + prService = authService.client.map { PullRequestService(client: $0) } + repositoryService = authService.client.map { RepositoryService(client: $0) } + self.onSaved = onSaved + _title = State(initialValue: pullRequest.title) + _bodyText = State(initialValue: pullRequest.body ?? "") + _isOpen = State(initialValue: pullRequest.stateValue == .open) + _selectedLabelIDs = State(initialValue: Set(pullRequest.labels.map(\.id))) + _selectedMilestoneID = State(initialValue: pullRequest.milestone?.id) + _selectedAssigneeLogins = State(initialValue: Set((pullRequest.assignees ?? []).map(\.login))) + _selectedReviewerLogins = State(initialValue: Set((pullRequest.requestedReviewers ?? []).map(\.login))) + } + + private var owner: String { + repository.owner + } + + private var repo: String { + repository.repoName + } + + var body: some View { + NavigationStack { + Form { + Section { + TextField("Title", text: $title) + .accessibilityIdentifier("pr-edit-title-field") + } + + DescriptionEditorSection(text: $bodyText) + + LabelPickerSection( + availableLabels: availableLabels, + selectedLabelIDs: $selectedLabelIDs, + ) + MilestonePickerSection( + availableMilestones: availableMilestones, + selectedMilestoneID: $selectedMilestoneID, + ) + UserPickerSection( + title: "Assignees", + availableUsers: availableAssignees, + selectedLogins: $selectedAssigneeLogins, + ) + UserPickerSection( + title: "Reviewers", + availableUsers: availableAssignees, + selectedLogins: $selectedReviewerLogins, + ) + + if !isMerged { + Section { + Toggle("Open", isOn: $isOpen) + } + } + } + .navigationTitle("Edit Pull Request") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button("Save") { + Task { await savePullRequest() } + } + .disabled(title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isSubmitting) + .accessibilityIdentifier("pr-edit-save") + } + } + .errorAlert(message: $errorMessage, isPresented: $showError) + .task { + await loadMetadata() + } + } + } + + private func loadMetadata() async { + guard let repositoryService else { return } + let metadata = await loadRepositoryMetadata(service: repositoryService, owner: owner, repo: repo) + availableLabels = metadata.labels + availableMilestones = metadata.milestones + availableAssignees = metadata.assignees + } + + // swiftlint:disable:next function_body_length + private func savePullRequest() async { + guard let prService, let client = authService.client else { return } + isSubmitting = true + do { + let issueService = IssueService(client: client) + try await issueService.replaceLabels( + owner: owner, + repo: repo, + index: pullRequest.number, + labelIDs: Array(selectedLabelIDs), + ) + _ = try await issueService.editIssue( + owner: owner, + repo: repo, + index: pullRequest.number, + title: nil, + body: nil, + state: nil, + milestone: selectedMilestoneID ?? 0, // 0 clears the milestone per Forgejo API + ) + let updatedPR = try await prService.editPullRequest( + owner: owner, + repo: repo, + index: pullRequest.number, + title: title.trimmingCharacters(in: .whitespacesAndNewlines), + body: bodyText.trimmingCharacters(in: .whitespacesAndNewlines), + state: isMerged ? nil : (isOpen ? PullRequestState.open.rawValue : PullRequestState.closed.rawValue), + assignees: Array(selectedAssigneeLogins), + ) + let initialReviewerLogins = Set((pullRequest.requestedReviewers ?? []).map(\.login)) + let addedReviewers = selectedReviewerLogins.subtracting(initialReviewerLogins) + let removedReviewers = initialReviewerLogins.subtracting(selectedReviewerLogins) + if !addedReviewers.isEmpty { + try await prService.requestReviewers( + owner: owner, + repo: repo, + index: pullRequest.number, + reviewers: Array(addedReviewers), + ) + } + if !removedReviewers.isEmpty { + try await prService.removeReviewers( + owner: owner, + repo: repo, + index: pullRequest.number, + reviewers: Array(removedReviewers), + ) + } + onSaved(updatedPR) + dismiss() + } catch { + errorMessage = error.localizedDescription + showError = true + } + isSubmitting = false + } + + #if DEBUG + init(preview _: Void, repository: Repository, pullRequest: PullRequest, authService: AuthenticationService, + availableLabels: [IssueLabel] = [], availableMilestones: [IssueMilestone] = [], + availableAssignees: [User] = []) + { + self.repository = repository + self.pullRequest = pullRequest + self.authService = authService + prService = nil + repositoryService = nil + onSaved = { _ in } + _title = State(initialValue: pullRequest.title) + _bodyText = State(initialValue: pullRequest.body ?? "") + _isOpen = State(initialValue: pullRequest.stateValue == .open) + _selectedLabelIDs = State(initialValue: Set(pullRequest.labels.map(\.id))) + _selectedMilestoneID = State(initialValue: pullRequest.milestone?.id) + _selectedAssigneeLogins = State(initialValue: Set((pullRequest.assignees ?? []).map(\.login))) + _selectedReviewerLogins = State(initialValue: Set((pullRequest.requestedReviewers ?? []).map(\.login))) + _availableLabels = State(initialValue: availableLabels) + _availableMilestones = State(initialValue: availableMilestones) + _availableAssignees = State(initialValue: availableAssignees) + } + #endif +} + +#Preview { + PullRequestEditView( + preview: (), + repository: .preview, + pullRequest: .preview, + authService: .previewDefault, + availableLabels: IssueLabel.previewList, + availableMilestones: [.preview], + availableAssignees: [.preview, .previewBot], + ) +} diff --git a/Forji/Forji/Views/PullRequestListView.swift b/Forji/Forji/Views/PullRequestListView.swift new file mode 100644 index 0000000..aa65e72 --- /dev/null +++ b/Forji/Forji/Views/PullRequestListView.swift @@ -0,0 +1,206 @@ +import ForgejoKit +import SwiftUI + +struct PullRequestListView: View { + let repository: Repository + @State private var authService: AuthenticationService + @State private var pagination = PaginationState() + @State private var stateFilter: PullRequestState = .open + @State private var showCreateSheet = false + + private let prService: PullRequestService? + + init(repository: Repository, authService: AuthenticationService) { + self.repository = repository + self.authService = authService + prService = authService.client.map { PullRequestService(client: $0) } + } + + private var owner: String { + repository.owner + } + + private var repo: String { + repository.repoName + } + + var body: some View { + @Bindable var pagination = pagination + ZStack(alignment: .bottomTrailing) { + VStack(spacing: 0) { + SegmentedPickerSection( + title: "State", + selection: $stateFilter, + options: [("Open", .open), ("Closed", .closed)], + accessibilityIdentifier: "pr-state-picker", + ) + + List { + if pagination.isLoading, pagination.items.isEmpty { + LoadingListSection() + } else if pagination.items.isEmpty { + ContentUnavailableView { + Label( + "No Pull Requests", + systemImage: stateFilter == .open + ? "checkmark.circle" : "arrow.triangle.pull", + ) + .foregroundStyle(stateFilter == .open ? .green : .secondary) + } description: { + Text( + stateFilter == .open + ? "All clear — no open pull requests." + : "No closed pull requests found.", + ) + } + } else { + Section { + ForEach(pagination.items) { pullRequest in + NavigationLink { + PullRequestDetailView( + repository: repository, + prNumber: pullRequest.number, + authService: authService, + ) + } label: { + PullRequestRow(pullRequest: pullRequest) + } + } + + if pagination.hasMore { + ProgressView() + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + .accessibilityIdentifier("load-more-indicator") + .task { + await loadMorePullRequests() + } + } + } + } + } + .listStyle(.insetGrouped) + .refreshable { + await reloadPullRequests().value + } + } + + // Floating glass create button + FloatingCreateButton(action: { showCreateSheet = true }) + .accessibilityIdentifier("pr-create-button") + } + .sheet(isPresented: $showCreateSheet) { + PullRequestCreateView( + repository: repository, + authService: authService, + ) { + reloadPullRequests() + } + } + .task { + reloadPullRequests() + } + .onChange(of: stateFilter) { + reloadPullRequests(clearItems: true) + } + .errorAlert(message: $pagination.errorMessage, isPresented: $pagination.showError) + } + + @discardableResult + private func reloadPullRequests(clearItems: Bool = false) -> Task { + guard let prService else { return Task {} } + return pagination.reload(clearItems: clearItems) { [self] page, limit in + try await prService.fetchPullRequests( + owner: owner, + repo: repo, + state: stateFilter.rawValue, + page: page, + limit: limit, + ) + } + } + + private func loadMorePullRequests() async { + guard let prService else { return } + await pagination.loadMore { page, limit in + try await prService.fetchPullRequests( + owner: owner, + repo: repo, + state: stateFilter.rawValue, + page: page, + limit: limit, + ) + } + } + + #if DEBUG + init(preview _: Void, repository: Repository, authService: AuthenticationService, pullRequests: [PullRequest]) { + self.repository = repository + self.authService = authService + prService = nil + _pagination = State(initialValue: PaginationState(items: pullRequests)) + } + #endif +} + +#Preview { + NavigationStack { + PullRequestListView( + preview: (), + repository: .preview, + authService: .previewDefault, + pullRequests: [.preview], + ) + } +} + +struct PullRequestRow: View { + let pullRequest: PullRequest + + private var status: PRStatusStyle { + PRStatusStyle(state: pullRequest.stateValue, merged: pullRequest.merged, draft: pullRequest.draft) + } + + var body: some View { + HStack(alignment: .top, spacing: 10) { + Image(systemName: status.icon) + .foregroundStyle(status.color) + .font(.body) + .padding(.top, 2) + + VStack(alignment: .leading, spacing: 4) { + Text(pullRequest.title) + .font(.body) + .lineLimit(2) + + HStack(spacing: 6) { + Text("#\(pullRequest.number)") + Text("\(pullRequest.head.ref) \u{2192} \(pullRequest.base.ref)") + .lineLimit(1) + if pullRequest.comments > 0 { + Label("\(pullRequest.comments)", systemImage: "bubble.right") + } + } + .font(.caption) + .foregroundStyle(.secondary) + + FlowLayout(spacing: 4) { + if pullRequest.draft == true { + Text("Draft") + .font(.caption2) + .fontWeight(.medium) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .glassEffect(.regular) + } + + ForEach(pullRequest.labels) { label in + IssueLabelView(label: label) + } + } + } + } + .padding(.vertical, 2) + .stateAccent(status.color) + } +} diff --git a/Forji/Forji/Views/PullRequestMergeView.swift b/Forji/Forji/Views/PullRequestMergeView.swift new file mode 100644 index 0000000..930f93f --- /dev/null +++ b/Forji/Forji/Views/PullRequestMergeView.swift @@ -0,0 +1,111 @@ +import ForgejoKit +import SwiftUI + +struct PullRequestMergeView: View { + let repository: Repository + let pullRequest: PullRequest + @State private var authService: AuthenticationService + @State private var mergeMethod = "merge" + @State private var commitMessage = "" + @State private var deleteBranch = false + @State private var isMerging = false + @State private var errorMessage: String? + @State private var showError = false + @Environment(\.dismiss) private var dismiss + + private let prService: PullRequestService? + private let onMerged: () -> Void + + init( + repository: Repository, pullRequest: PullRequest, + authService: AuthenticationService, + onMerged: @escaping () -> Void, + ) { + self.repository = repository + self.pullRequest = pullRequest + self.authService = authService + prService = authService.client.map { PullRequestService(client: $0) } + self.onMerged = onMerged + } + + private var owner: String { + repository.owner + } + + private var repo: String { + repository.repoName + } + + var body: some View { + NavigationStack { + Form { + Section("Merge Method") { + Picker("Method", selection: $mergeMethod) { + Text("Merge Commit").tag("merge") + Text("Rebase").tag("rebase") + Text("Squash").tag("squash") + } + .pickerStyle(.segmented) + .accessibilityIdentifier("merge-method-picker") + } + + if mergeMethod != "rebase" { + Section("Commit Message") { + TextEditor(text: $commitMessage) + .frame(minHeight: 80) + .scrollContentBackground(.hidden) + } + } + + Section { + Toggle("Delete branch after merge", isOn: $deleteBranch) + .accessibilityIdentifier("merge-delete-branch") + } + } + .navigationTitle("Merge Pull Request") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button("Merge") { + Task { await merge() } + } + .disabled(isMerging) + .accessibilityIdentifier("merge-confirm") + } + } + .errorAlert(message: $errorMessage, isPresented: $showError) + } + } + + private func merge() async { + guard let prService else { return } + isMerging = true + do { + try await prService.mergePullRequest( + owner: owner, + repo: repo, + index: pullRequest.number, + method: mergeMethod, + message: mergeMethod != "rebase" ? commitMessage : nil, + deleteBranch: deleteBranch, + ) + onMerged() + dismiss() + } catch { + errorMessage = error.localizedDescription + showError = true + } + isMerging = false + } +} + +#Preview { + PullRequestMergeView( + repository: .preview, + pullRequest: .preview, + authService: .previewDefault, + ) {} +} diff --git a/Forji/Forji/Views/PullRequestReviewSheet.swift b/Forji/Forji/Views/PullRequestReviewSheet.swift new file mode 100644 index 0000000..9b054f7 --- /dev/null +++ b/Forji/Forji/Views/PullRequestReviewSheet.swift @@ -0,0 +1,246 @@ +import ForgejoKit +import SwiftUI + +struct PendingInlineComment: Identifiable { + let id = UUID() + let body: String + let path: String + let line: DiffLine + + var reviewComment: CreateReviewComment? { + guard let position = line.diffPosition else { return nil } + return CreateReviewComment( + body: body, + path: path, + oldPosition: line.type == .deletion ? position : nil, + newPosition: line.type == .deletion ? nil : position, + ) + } +} + +struct PullRequestReviewSheet: View { + let repository: Repository + let prNumber: Int + let isOwnPR: Bool + @State private var authService: AuthenticationService + + @State private var reviewEvent: ReviewEvent = .comment + @State private var reviewBody = "" + @State private var isSubmitting = false + @State private var errorMessage: String? + @State private var showError = false + @State private var parsedDiff: ParsedDiff? + @State private var isLoadingDiff = false + @State private var inlineCommentContext: InlineCommentContext? + @State private var pendingComments: [PendingInlineComment] = [] + @Environment(\.dismiss) private var dismiss + + private let prService: PullRequestService? + private let onSubmitted: () -> Void + + init( + repository: Repository, + prNumber: Int, + authService: AuthenticationService, + isOwnPR: Bool = false, + onSubmitted: @escaping () -> Void, + ) { + self.repository = repository + self.prNumber = prNumber + self.isOwnPR = isOwnPR + self.authService = authService + prService = authService.client.map { PullRequestService(client: $0) } + self.onSubmitted = onSubmitted + } + + private var owner: String { + repository.owner + } + + private var repo: String { + repository.repoName + } + + var body: some View { + NavigationStack { + List { + Section("Review Type") { + Picker("Type", selection: $reviewEvent) { + Label(ReviewEvent.comment.title, systemImage: ReviewEvent.comment.systemImage) + .tag(ReviewEvent.comment) + if !isOwnPR { + Label(ReviewEvent.approved.title, systemImage: ReviewEvent.approved.systemImage) + .tag(ReviewEvent.approved) + Label(ReviewEvent.requestChanges.title, systemImage: ReviewEvent.requestChanges.systemImage) + .tag(ReviewEvent.requestChanges) + } + } + .pickerStyle(.inline) + .labelsHidden() + .accessibilityIdentifier("review-type-picker") + } + + Section("Changes") { + if isLoadingDiff { + HStack { + Spacer() + ProgressView() + Spacer() + } + } else if let diff = parsedDiff { + DiffView(diff: diff) { line, path in + inlineCommentContext = InlineCommentContext(line: line, path: path) + } + .listRowInsets(EdgeInsets()) + } + } + + if !pendingComments.isEmpty { + Section("Inline Comments (\(pendingComments.count))") { + ForEach(pendingComments) { comment in + VStack(alignment: .leading, spacing: 4) { + Text(comment.path) + .font(.system(.caption, design: .monospaced)) + .foregroundStyle(.secondary) + Text(comment.body) + .font(.subheadline) + } + .padding(.vertical, 2) + } + .onDelete { offsets in + pendingComments.remove(atOffsets: offsets) + } + } + } + + DescriptionEditorSection(text: $reviewBody, title: "Review Body") + } + .listStyle(.insetGrouped) + .navigationTitle("Submit Review") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button("Submit") { + Task { await submitReview() } + } + .disabled(isSubmitting) + .accessibilityIdentifier("review-submit") + } + } + .errorAlert(message: $errorMessage, isPresented: $showError) + .sheet(item: $inlineCommentContext) { context in + AddInlineCommentSheet(context: context) { comment in + pendingComments.append(comment) + } + } + .task { + await loadDiff() + } + } + } + + private func loadDiff() async { + guard let prService else { return } + isLoadingDiff = true + do { + let raw = try await prService.fetchDiff(owner: owner, repo: repo, index: prNumber) + parsedDiff = DiffParser.parse(raw) + } catch { + errorMessage = error.localizedDescription + showError = true + } + isLoadingDiff = false + } + + private func submitReview() async { + guard let prService else { return } + isSubmitting = true + do { + _ = try await prService.createReview( + owner: owner, + repo: repo, + index: prNumber, + body: reviewBody, + event: reviewEvent.rawValue, + comments: pendingComments.compactMap(\.reviewComment), + ) + onSubmitted() + dismiss() + } catch { + errorMessage = error.localizedDescription + showError = true + } + isSubmitting = false + } + + #if DEBUG + init(preview _: Void, repository: Repository, prNumber: Int, authService: AuthenticationService, + isOwnPR: Bool = false, parsedDiff: ParsedDiff? = nil) + { + self.repository = repository + self.prNumber = prNumber + self.isOwnPR = isOwnPR + self.authService = authService + prService = nil + onSubmitted = {} + _parsedDiff = State(initialValue: parsedDiff) + _isLoadingDiff = State(initialValue: false) + } + #endif +} + +private struct AddInlineCommentSheet: View { + let context: InlineCommentContext + let onAdd: (PendingInlineComment) -> Void + + @State private var commentBody = "" + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationStack { + VStack(spacing: 0) { + DiffLineHeader(path: context.path, line: context.line) + + TextEditor(text: $commentBody) + .frame(maxHeight: .infinity) + .scrollContentBackground(.hidden) + .padding(8) + .background(Color(.tertiarySystemFill)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .padding(.horizontal) + .padding(.bottom) + } + .navigationTitle("Add Comment") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button("Add") { + onAdd(PendingInlineComment( + body: commentBody, + path: context.path, + line: context.line, + )) + dismiss() + } + .disabled(commentBody.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + } + } + } + } +} + +#Preview { + PullRequestReviewSheet( + preview: (), + repository: .preview, + prNumber: 4, + authService: .previewDefault, + parsedDiff: .preview, + ) +} diff --git a/Forji/Forji/Views/PullRequestsOverviewView.swift b/Forji/Forji/Views/PullRequestsOverviewView.swift new file mode 100644 index 0000000..31ed068 --- /dev/null +++ b/Forji/Forji/Views/PullRequestsOverviewView.swift @@ -0,0 +1,107 @@ +import ForgejoKit +import SwiftUI + +struct PullRequestsOverviewView: View { + let authService: AuthenticationService + + init(authService: AuthenticationService) { + self.authService = authService + } + + var body: some View { + SearchableOverviewView( + authService: authService, + issueType: "pulls", + navigationTitle: "Pull Requests", + searchPrompt: "Search pull requests", + emptyTitle: "No Pull Requests", + emptyOpenIcon: "arrow.triangle.pull", + emptyClosedIcon: "arrow.triangle.pull", + itemNoun: "pull requests", + createButtonId: "pr-create-button", + showReviewRequested: true, + row: { issue in + PullRequestOverviewRow(issue: issue) + }, + detail: { repository, number, auth in + PullRequestDetailView( + repository: repository, + prNumber: number, + authService: auth, + ) + }, + createView: { repo, auth, embedded, onCreated in + PullRequestCreateView( + repository: repo, + authService: auth, + embeddedInNavigation: embedded, + onCreated: onCreated, + ) + }, + ) + } + + #if DEBUG + init(preview _: Void, authService: AuthenticationService, pullRequests _: [Issue]) { + self.authService = authService + } + #endif +} + +struct PullRequestOverviewRow: View { + let issue: Issue + + private var status: PRStatusStyle { + PRStatusStyle(state: issue.pullRequestStateValue, merged: issue.pullRequest?.merged, draft: nil) + } + + var body: some View { + HStack(alignment: .top, spacing: 10) { + Image(systemName: status.icon) + .foregroundStyle(status.color) + .font(.body) + .padding(.top, 2) + + VStack(alignment: .leading, spacing: 4) { + Text(issue.title) + .font(.body) + .lineLimit(2) + + if let repository = issue.repository { + Text(repository.fullName) + .font(.caption) + .foregroundStyle(.secondary) + } + + HStack(spacing: 6) { + Text("#\(issue.number)") + if issue.comments > 0 { + Label("\(issue.comments)", systemImage: "bubble.right") + } + } + .font(.caption) + .foregroundStyle(.secondary) + + if !issue.labels.isEmpty { + FlowLayout(spacing: 4) { + ForEach(issue.labels) { label in + IssueLabelView(label: label) + } + } + } + } + } + .padding(.vertical, 2) + .stateAccent(status.color) + } +} + +#Preview { + NavigationStack { + PullRequestsOverviewView( + preview: (), + authService: .previewDefault, + pullRequests: [.previewPullRequest], + ) + } +} diff --git a/Forji/Forji/Views/RepositoryDetailView.swift b/Forji/Forji/Views/RepositoryDetailView.swift new file mode 100644 index 0000000..ada353c --- /dev/null +++ b/Forji/Forji/Views/RepositoryDetailView.swift @@ -0,0 +1,566 @@ +import ForgejoKit + +// swiftlint:disable file_length +import SwiftUI + +struct FileNavigation: Hashable { + let path: String + let branch: String +} + +struct RepositoryDetailView: View { + let repository: Repository + @State private var authService: AuthenticationService + @State private var selectedTab: DetailTab = .code + @State private var fileNavigation: FileNavigation? + @State private var selectedBranch: String + @State private var showCommitHistory = false + @State private var branches: [Branch] = [] + @State private var showBranchPicker = false + + private let repositoryService: RepositoryService? + + init(repository: Repository, authService: AuthenticationService) { + self.repository = repository + self.authService = authService + repositoryService = authService.client.map { RepositoryService(client: $0) } + _selectedBranch = State(initialValue: repository.defaultBranch ?? "main") + } + + enum DetailTab: String, CaseIterable { + case code = "Code" + case issues = "Issues" + case pulls = "Pull Requests" + + var icon: String { + switch self { + case .code: "doc.text" + case .issues: "exclamationmark.circle" + case .pulls: "arrow.triangle.pull" + } + } + } + + var body: some View { + VStack(spacing: 0) { + // Repository header + VStack(alignment: .leading, spacing: 12) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(repository.name) + .font(.title2) + .fontWeight(.bold) + + if let description = repository.description { + Text(description) + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + + Spacer() + + if repository.private ?? false { + Label("Private", systemImage: "lock.fill") + .font(.caption2) + .fontWeight(.medium) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .glassEffect(.regular) + } + } + + // Stats + HStack(spacing: 20) { + Label("\(repository.starsCount ?? 0)", systemImage: "star.fill") + .font(.subheadline) + + Label("\(repository.forksCount ?? 0)", systemImage: "tuningfork") + .font(.subheadline) + + if let language = repository.language { + HStack(spacing: 4) { + Circle() + .fill(colorForLanguage(language)) + .frame(width: 12, height: 12) + Text(language) + .font(.subheadline) + } + } + } + .foregroundStyle(.secondary) + } + .padding() + + Divider() + + // Tab selector + Picker("View", selection: $selectedTab) { + ForEach(DetailTab.allCases, id: \.self) { tab in + Label(tab.rawValue, systemImage: tab.icon) + .tag(tab) + } + } + .pickerStyle(.segmented) + .padding() + .accessibilityIdentifier("repo-detail-tab-picker") + + // Tab content + TabView(selection: $selectedTab) { + RepositoryCodeView( + repository: repository, + authService: authService, + selectedBranch: $selectedBranch, + onFileNavigation: { path, branch in + fileNavigation = FileNavigation(path: path, branch: branch) + }, + ) + .tag(DetailTab.code) + + IssueListView( + repository: repository, + authService: authService, + ) + .tag(DetailTab.issues) + + PullRequestListView( + repository: repository, + authService: authService, + ) + .tag(DetailTab.pulls) + } + .tabViewStyle(.page(indexDisplayMode: .never)) + } + .navigationTitle(repository.name) + .navigationBarTitleDisplayMode(.inline) + .navigationDestination(item: $fileNavigation) { nav in + let fileName = nav.path.components(separatedBy: "/").last ?? nav.path + FileViewerView( + repository: repository, + filePath: nav.path, + fileName: fileName, + authService: authService, + ref: nav.branch, + ) + } + .navigationDestination(isPresented: $showCommitHistory) { + CommitHistoryView( + repository: repository, + branch: $selectedBranch, + authService: authService, + ) + .navigationTitle("Commits") + .navigationBarTitleDisplayMode(.inline) + } + .task { + guard let repositoryService else { return } + do { + branches = try await repositoryService.fetchBranches( + owner: repository.owner, + repo: repository.repoName, + ) + } catch { + // Non-critical — branch selector stays disabled + } + } + .toolbar { + if selectedTab == .code { + ToolbarItem(placement: .topBarTrailing) { + Button { + showBranchPicker = true + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.triangle.branch") + Text(selectedBranch) + .lineLimit(1) + .frame(maxWidth: 140) + } + .font(.subheadline.weight(.medium)) + } + .disabled(branches.isEmpty) + .accessibilityIdentifier("branch-selector") + } + ToolbarItem(placement: .topBarTrailing) { + Button { + showCommitHistory = true + } label: { + Image(systemName: "clock.arrow.circlepath") + } + .accessibilityIdentifier("commits-button") + } + } + } + .sheet(isPresented: $showBranchPicker) { + NavigationStack { + List(branches) { branch in + Button { + selectedBranch = branch.name + showBranchPicker = false + } label: { + HStack { + Text(branch.name) + Spacer() + if branch.name == selectedBranch { + Image(systemName: "checkmark") + .foregroundStyle(.blue) + } + } + } + .accessibilityIdentifier("branch-option-\(branch.name)") + } + .navigationTitle("Switch Branch") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + showBranchPicker = false + } + } + } + } + .presentationDetents([.medium]) + } + } +} + +// swiftlint:disable:next type_body_length +struct RepositoryCodeView: View { + let repository: Repository + @State private var authService: AuthenticationService + @State private var contents: [RepositoryContent] = [] + @State private var currentPath: String = "" + @State private var pathComponents: [String] = [] + @State private var isLoading = false + @State private var errorMessage: String? + @State private var showError = false + @State private var readmeContent: String? + @State private var scrollToFiles = false + @State private var scrollToReadme = false + @State private var isScrolledToFiles = false + @Binding var selectedBranch: String + @State private var branchContentTask: Task? + @State private var contentLoadTask: Task? + + private let repositoryService: RepositoryService? + var onFileNavigation: ((String, String) -> Void)? + + init( + repository: Repository, + authService: AuthenticationService, + selectedBranch: Binding, + onFileNavigation: ((String, String) -> Void)? = nil, + ) { + self.repository = repository + self.authService = authService + repositoryService = authService.client.map { RepositoryService(client: $0) } + self.onFileNavigation = onFileNavigation + _selectedBranch = selectedBranch + } + + private var readmeBaseURL: URL? { + guard let serverURL = authService.client?.serverURL else { return nil } + let encodedBranch = selectedBranch.addingPercentEncoding( + withAllowedCharacters: .urlPathAllowed, + ) ?? selectedBranch + let pathSuffix: String = if currentPath.isEmpty { + "" + } else { + currentPath.components(separatedBy: "/") + .map { $0.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? $0 } + .joined(separator: "/") + "/" + } + return URL(string: "\(serverURL)/\(repository.fullName)/src/branch/\(encodedBranch)/\(pathSuffix)") + } + + var body: some View { + ZStack(alignment: .bottomTrailing) { + VStack(spacing: 0) { + // Breadcrumb navigation + if !pathComponents.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 4) { + Button { + navigateToRoot() + } label: { + Text(repository.name) + .font(.subheadline) + } + + ForEach(Array(pathComponents.enumerated()), id: \.offset) { index, component in + Text("/") + .foregroundStyle(.secondary) + .font(.subheadline) + + Button { + navigateToPath(upTo: index) + } label: { + Text(component) + .font(.subheadline) + } + } + } + .padding(.horizontal) + .padding(.vertical, 8) + } + } + + // Content list + ScrollViewReader { proxy in + List { + if let readme = readmeContent { + Section("README") { + MarkdownPreview( + text: readme, + baseURL: readmeBaseURL, + onNavigateToFile: { path in + onFileNavigation?(path, selectedBranch) + }, + ) + .padding() + } + .id("readme") + } + + // Files and folders + Section("Files") { + if isLoading { + HStack { + Spacer() + ProgressView() + Spacer() + } + } else { + ForEach(contents) { content in + if content.type == .dir { + Button { + handleContentTap(content) + } label: { + ContentRow(content: content) + } + .buttonStyle(.plain) + .accessibilityIdentifier("directory-\(content.name)") + } else { + NavigationLink { + FileViewerView( + repository: repository, + filePath: currentPath.isEmpty + ? content.name : "\(currentPath)/\(content.name)", + fileName: content.name, + authService: authService, + ref: selectedBranch, + ) + } label: { + ContentRow(content: content) + } + } + } + } + } + .id("files") + } + .onChange(of: scrollToFiles) { + if scrollToFiles { + withAnimation { + proxy.scrollTo("files", anchor: .top) + } + scrollToFiles = false + isScrolledToFiles = true + } + } + .onChange(of: scrollToReadme) { + if scrollToReadme { + withAnimation { + proxy.scrollTo("readme", anchor: .top) + } + scrollToReadme = false + isScrolledToFiles = false + } + } + } + } + .task { + await loadContents() + } + .onChange(of: selectedBranch) { + pathComponents = [] + currentPath = "" + readmeContent = nil + branchContentTask?.cancel() + branchContentTask = Task { + await loadContents() + } + } + .errorAlert(message: $errorMessage, isPresented: $showError) + + // Floating button to jump between README and Files + if readmeContent != nil { + Button { + if isScrolledToFiles { + scrollToReadme = true + } else { + scrollToFiles = true + } + } label: { + Label( + isScrolledToFiles ? "README" : "Files", + systemImage: isScrolledToFiles ? "arrow.up.doc.fill" : "arrow.down.doc.fill", + ) + .font(.headline) + .foregroundStyle(.white) + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + .glassEffect(.regular.tint(.blue).interactive()) + .padding(.trailing, 20) + .padding(.bottom, 20) + .animation(.easeInOut, value: isScrolledToFiles) + .accessibilityIdentifier("code-scroll-toggle") + } + } + } + + private func loadContents() async { + guard let repositoryService else { return } + isLoading = true + errorMessage = nil + + do { + contents = try await repositoryService.fetchContents( + owner: repository.owner, + repo: repository.repoName, + path: currentPath, + ref: selectedBranch, + ) + + // Sort: directories first, then files + contents.sort { lhs, rhs in + if lhs.type == .dir, rhs.type != .dir { + true + } else if lhs.type != .dir, rhs.type == .dir { + false + } else { + lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending + } + } + + readmeContent = nil + await loadReadme(owner: repository.owner, repo: repository.repoName) + + } catch is CancellationError { + // Ignore cancellation + return + } catch { + errorMessage = error.localizedDescription + showError = true + } + + isLoading = false + } + + private func loadReadme(owner: String, repo: String) async { + guard let repositoryService else { return } + let readmeFile = contents.first { + $0.type == .file && $0.name.lowercased().hasPrefix("readme") + } + guard let readmeFile else { return } + + do { + let readmePath = currentPath.isEmpty ? readmeFile.name : "\(currentPath)/\(readmeFile.name)" + let fileContent = try await repositoryService.fetchFileContent( + owner: owner, + repo: repo, + path: readmePath, + ref: selectedBranch, + ) + readmeContent = fileContent.decodedContent + } catch { + // README exists in listing but couldn't be fetched — ignore + } + } + + private func handleContentTap(_ content: RepositoryContent) { + if content.type == .dir { + pathComponents.append(content.name) + currentPath = pathComponents.joined(separator: "/") + contentLoadTask?.cancel() + contentLoadTask = Task { + await loadContents() + } + } + } + + private func navigateToRoot() { + pathComponents = [] + currentPath = "" + contentLoadTask?.cancel() + contentLoadTask = Task { + await loadContents() + } + } + + private func navigateToPath(upTo index: Int) { + pathComponents = Array(pathComponents.prefix(index + 1)) + currentPath = pathComponents.joined(separator: "/") + contentLoadTask?.cancel() + contentLoadTask = Task { + await loadContents() + } + } +} + +struct ContentRow: View { + let content: RepositoryContent + + var body: some View { + HStack(spacing: 12) { + Image(systemName: iconForType(content.type)) + .foregroundStyle(content.type == .dir ? .blue : .secondary) + .frame(width: 24) + + VStack(alignment: .leading, spacing: 2) { + Text(content.name) + .foregroundStyle(.primary) + + if content.type == .file { + Text(formatFileSize(content.size)) + .font(.caption) + .foregroundStyle(.secondary) + } + } + + Spacer() + + if content.type == .dir { + Image(systemName: "chevron.right") + .foregroundStyle(.tertiary) + .font(.caption) + } + } + .padding(.vertical, 4) + } + + private func iconForType(_ type: RepositoryContent.ContentType) -> String { + switch type { + case .dir: + "folder.fill" + case .file: + "doc.text" + case .symlink: + "link" + case .submodule: + "folder.badge.gearshape" + } + } + + private func formatFileSize(_ bytes: Int) -> String { + let formatter = ByteCountFormatter() + formatter.countStyle = .file + return formatter.string(fromByteCount: Int64(bytes)) + } +} + +#Preview { + NavigationStack { + RepositoryDetailView(repository: .preview, authService: .previewDefault) + } +} diff --git a/Forji/Forji/Views/RepositoryListView.swift b/Forji/Forji/Views/RepositoryListView.swift new file mode 100644 index 0000000..f19488b --- /dev/null +++ b/Forji/Forji/Views/RepositoryListView.swift @@ -0,0 +1,334 @@ +import ForgejoKit +import SwiftUI + +struct RepositoryListView: View { + @State private var authService: AuthenticationService + @State private var repositories: [Repository] = [] + @State private var starredRepoIds: Set = [] + @State private var isLoading = false + @State private var errorMessage: String? + @State private var showError = false + @State private var selectedFilter: RepositoryFilter = .all + @State private var searchText = "" + @State private var searchTask: Task? + @State private var hasMore = true + @State private var currentPage = 1 + @State private var starringInFlight: Set = [] + + private let repositoryService: RepositoryService? + private let pageSize = 20 + + init(authService: AuthenticationService) { + self.authService = authService + repositoryService = authService.client.map { RepositoryService(client: $0) } + } + + enum RepositoryFilter: String, CaseIterable { + case all = "All" + case starred = "Starred" + + var icon: String { + switch self { + case .all: "folder.fill" + case .starred: "star.fill" + } + } + } + + var body: some View { + VStack(spacing: 0) { + SegmentedPickerSection( + title: "Filter", + selection: $selectedFilter, + options: RepositoryFilter.allCases.map { ($0.rawValue, $0) }, + accessibilityIdentifier: "repo-filter-picker", + ) + + List { + if isLoading, repositories.isEmpty { + Section { + HStack { + Spacer() + ProgressView() + Spacer() + } + .listRowBackground(Color.clear) + } + } else if repositories.isEmpty { + ContentUnavailableView { + Label("No Repositories", systemImage: "folder.badge.questionmark") + .foregroundStyle(.secondary) + } description: { + Text( + searchText.isEmpty + ? "No repositories yet." + : "No repositories matching your search.", + ) + } + } else { + Section { + ForEach(repositories) { repo in + NavigationLink { + RepositoryDetailView(repository: repo, authService: authService) + } label: { + RepositoryRow( + repository: repo, + isStarred: starredRepoIds.contains(repo.id), + isStarring: starringInFlight.contains(repo.id), + ) { + await toggleStar(for: repo) + } + } + } + + if hasMore { + ProgressView() + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + .accessibilityIdentifier("load-more-indicator") + .task { + await loadMoreRepositories() + } + } + } + } + } + .listStyle(.insetGrouped) + .accessibilityIdentifier("repo-list") + .refreshable { + currentPage = 1 + hasMore = true + await loadRepositories() + } + } + .navigationTitle("Repositories") + .searchable(text: $searchText, prompt: "Search repositories") + .task { + await loadRepositories() + } + .onChange(of: selectedFilter) { _, _ in + Task { + currentPage = 1 + hasMore = true + repositories = [] + await loadRepositories() + } + } + .debouncedSearch(text: $searchText, task: $searchTask) { + currentPage = 1 + hasMore = true + await loadRepositories() + } + .errorAlert(message: $errorMessage, isPresented: $showError) + } + + private func loadRepositories() async { + guard let repositoryService else { return } + isLoading = true + errorMessage = nil + + do { + async let allStarredIds = repositoryService.fetchAllStarredRepoIds() + + if searchText.isEmpty { + switch selectedFilter { + case .all: + let fetched = try await repositoryService.fetchUserRepositories(page: 1, limit: pageSize) + repositories = fetched + hasMore = fetched.count >= pageSize + case .starred: + let fetched = try await repositoryService.fetchStarredRepositories(page: 1, limit: pageSize) + repositories = fetched + hasMore = fetched.count >= pageSize + } + } else { + let results = try await repositoryService.searchRepositories( + query: searchText, page: 1, limit: pageSize, + ) + switch selectedFilter { + case .all: + repositories = results + hasMore = results.count >= pageSize + case .starred: + let ids = try await allStarredIds + let filtered = results.filter { ids.contains($0.id) } + repositories = filtered + hasMore = results.count >= pageSize + } + } + + starredRepoIds = try await allStarredIds + currentPage = 2 + } catch is CancellationError { + // Ignore cancellation + } catch { + errorMessage = error.localizedDescription + showError = true + } + + isLoading = false + } + + private func loadMoreRepositories() async { + guard let repositoryService, hasMore, !isLoading else { return } + isLoading = true + + do { + if searchText.isEmpty { + switch selectedFilter { + case .all: + let fetched = try await repositoryService.fetchUserRepositories(page: currentPage, limit: pageSize) + repositories.append(contentsOf: fetched) + hasMore = fetched.count >= pageSize + case .starred: + let fetched = try await repositoryService.fetchStarredRepositories( + page: currentPage, limit: pageSize, + ) + repositories.append(contentsOf: fetched) + hasMore = fetched.count >= pageSize + } + } else { + let results = try await repositoryService.searchRepositories( + query: searchText, page: currentPage, limit: pageSize, + ) + switch selectedFilter { + case .all: + repositories.append(contentsOf: results) + hasMore = results.count >= pageSize + case .starred: + let filtered = results.filter { starredRepoIds.contains($0.id) } + repositories.append(contentsOf: filtered) + hasMore = results.count >= pageSize + } + } + currentPage += 1 + } catch is CancellationError { + // Ignore cancellation + } catch { + errorMessage = error.localizedDescription + showError = true + } + + isLoading = false + } + + private func toggleStar(for repo: Repository) async { + guard let repositoryService else { return } + guard !starringInFlight.contains(repo.id) else { return } + let parts = repo.fullName.split(separator: "/") + guard parts.count == 2 else { return } + let owner = String(parts[0]) + let repoName = String(parts[1]) + + starringInFlight.insert(repo.id) + let isCurrentlyStarred = starredRepoIds.contains(repo.id) + + // Optimistic update + if isCurrentlyStarred { + starredRepoIds.remove(repo.id) + } else { + starredRepoIds.insert(repo.id) + } + + do { + if isCurrentlyStarred { + try await repositoryService.unstarRepository(owner: owner, repo: repoName) + } else { + try await repositoryService.starRepository(owner: owner, repo: repoName) + } + if selectedFilter == .starred { + currentPage = 1 + hasMore = true + await loadRepositories() + } + } catch { + // Revert optimistic update + if isCurrentlyStarred { + starredRepoIds.insert(repo.id) + } else { + starredRepoIds.remove(repo.id) + } + errorMessage = error.localizedDescription + showError = true + } + starringInFlight.remove(repo.id) + } +} + +struct RepositoryRow: View { + let repository: Repository + let isStarred: Bool + var isStarring: Bool = false + let onToggleStar: () async -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(repository.name) + .font(.headline) + + if let description = repository.description { + Text(description) + .font(.subheadline) + .foregroundStyle(.secondary) + .lineLimit(2) + } + } + + Spacer() + + if repository.private ?? false { + Label("Private", systemImage: "lock.fill") + .font(.caption2) + .fontWeight(.medium) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .glassEffect(.regular) + } + + Button { + Task { await onToggleStar() } + } label: { + Image(systemName: isStarred ? "star.fill" : "star") + .foregroundStyle(isStarred ? .yellow : .secondary) + .font(.body) + } + .buttonStyle(.borderless) + .disabled(isStarring) + .accessibilityIdentifier("star-button") + } + + HStack(spacing: 12) { + if let language = repository.language { + Text(language) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .glassEffect(.regular.tint(colorForLanguage(language))) + } + + Label("\(repository.starsCount ?? 0)", systemImage: "star.fill") + + Label("\(repository.forksCount ?? 0)", systemImage: "tuningfork") + + if (repository.openIssuesCount ?? 0) > 0 { + Label("\(repository.openIssuesCount ?? 0)", systemImage: "exclamationmark.circle") + } + } + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + + if let updatedAt = repository.updatedAt { + Text(formatRelativeDate(updatedAt)) + .font(.caption2) + .foregroundStyle(.tertiary) + } + } + .padding(.vertical, 4) + } +} + +#Preview { + RepositoryListView(authService: .previewDefault) +} diff --git a/Forji/Forji/Views/RepositoryPickerView.swift b/Forji/Forji/Views/RepositoryPickerView.swift new file mode 100644 index 0000000..a43d6ee --- /dev/null +++ b/Forji/Forji/Views/RepositoryPickerView.swift @@ -0,0 +1,183 @@ +import ForgejoKit +import SwiftUI + +struct RepositoryPickerView: View { + @State private var authService: AuthenticationService + @State private var repositories: [Repository] = [] + @State private var isLoading = false + @State private var errorMessage: String? + @State private var showError = false + @State private var searchText = "" + @State private var searchTask: Task? + @State private var selectedRepository: Repository? + @State private var hasMore = true + @State private var currentPage = 1 + @Environment(\.dismiss) private var dismiss + + private let repositoryService: RepositoryService? + private let pageSize = 20 + let destination: (Repository) -> Destination + + init(authService: AuthenticationService, @ViewBuilder destination: @escaping (Repository) -> Destination) { + self.authService = authService + repositoryService = authService.client.map { RepositoryService(client: $0) } + self.destination = destination + } + + var body: some View { + NavigationStack { + List { + if isLoading, repositories.isEmpty { + LoadingListSection() + } else if repositories.isEmpty { + ContentUnavailableView { + Label("No Repositories", systemImage: "folder.badge.questionmark") + .foregroundStyle(.secondary) + } description: { + Text( + searchText.isEmpty + ? "No repositories yet." + : "No repositories matching your search.", + ) + } + } else { + Section { + ForEach(repositories) { repo in + Button { + selectedRepository = repo + } label: { + RepositoryPickerRow(repository: repo) + } + .accessibilityIdentifier("repo-picker-\(repo.fullName)") + } + + if hasMore { + ProgressView() + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + .accessibilityIdentifier("load-more-indicator") + .task { + await loadMoreRepositories() + } + } + } + } + } + .listStyle(.insetGrouped) + .navigationTitle("Select Repository") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + } + .searchable(text: $searchText, prompt: "Search repositories") + .task { + await loadRepositories() + } + .debouncedSearch(text: $searchText, task: $searchTask) { + currentPage = 1 + hasMore = true + await loadRepositories() + } + .errorAlert(message: $errorMessage, isPresented: $showError) + .navigationDestination(item: $selectedRepository) { repo in + destination(repo) + } + } + } + + private func loadRepositories() async { + guard let repositoryService else { return } + isLoading = true + defer { isLoading = false } + do { + let fetched: [Repository] = if searchText.isEmpty { + try await repositoryService.fetchUserRepositories(page: 1, limit: pageSize) + } else { + try await repositoryService.searchRepositories(query: searchText, page: 1, limit: pageSize) + } + repositories = fetched + hasMore = fetched.count >= pageSize + currentPage = 2 + } catch is CancellationError { + // Ignore cancellation + } catch { + errorMessage = error.localizedDescription + showError = true + } + } + + private func loadMoreRepositories() async { + guard let repositoryService, hasMore, !isLoading else { return } + isLoading = true + defer { isLoading = false } + do { + let fetched: [Repository] = if searchText.isEmpty { + try await repositoryService.fetchUserRepositories(page: currentPage, limit: pageSize) + } else { + try await repositoryService.searchRepositories( + query: searchText, page: currentPage, limit: pageSize, + ) + } + repositories.append(contentsOf: fetched) + hasMore = fetched.count >= pageSize + currentPage += 1 + } catch is CancellationError { + // Ignore cancellation + } catch { + errorMessage = error.localizedDescription + showError = true + } + } + + #if DEBUG + init(preview _: Void, authService: AuthenticationService, repositories: [Repository], + @ViewBuilder destination: @escaping (Repository) -> Destination) + { + self.authService = authService + repositoryService = nil + self.destination = destination + _repositories = State(initialValue: repositories) + _hasMore = State(initialValue: false) + } + #endif +} + +private struct RepositoryPickerRow: View { + let repository: Repository + + var body: some View { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(repository.fullName) + .font(.body) + + if let description = repository.description, !description.isEmpty { + Text(description) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(2) + } + } + + Spacer() + + if repository.private ?? false { + Label("Private", systemImage: "lock.fill") + .font(.caption2) + .fontWeight(.medium) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .glassEffect(.regular) + } + } + .padding(.vertical, 2) + } +} + +#Preview { + RepositoryPickerView(preview: (), authService: .previewDefault, repositories: [.preview]) { repo in + Text(repo.fullName) + } +} diff --git a/Forji/Forji/Views/ReviewSummaryView.swift b/Forji/Forji/Views/ReviewSummaryView.swift new file mode 100644 index 0000000..1929168 --- /dev/null +++ b/Forji/Forji/Views/ReviewSummaryView.swift @@ -0,0 +1,85 @@ +import ForgejoKit +import SwiftUI + +struct ReviewSummaryView: View { + let review: PullRequestReview + let comments: [ReviewComment] + + private var stateIcon: String { + switch ReviewState(rawValue: review.state) { + case .approved: "checkmark.circle.fill" + case .requestChanges: "xmark.circle.fill" + default: "message.circle.fill" + } + } + + private var stateColor: Color { + switch ReviewState(rawValue: review.state) { + case .approved: .green + case .requestChanges: .red + default: .blue + } + } + + private var stateLabel: String { + ReviewState(rawValue: review.state)?.label ?? review.state.lowercased() + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 6) { + Image(systemName: stateIcon) + .foregroundStyle(stateColor) + .font(.subheadline) + Text(review.user?.login ?? "Unknown") + .font(.subheadline) + .fontWeight(.medium) + Text(stateLabel) + .font(.subheadline) + .foregroundStyle(.secondary) + Spacer() + if let date = review.submittedAt { + Text(formatRelativeDate(date)) + .font(.caption) + .foregroundStyle(.secondary) + } + } + + if let body = review.body, !body.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + MarkdownPreview(text: body) + } + + if !comments.isEmpty { + DisclosureGroup("Inline comments (\(comments.count))") { + ForEach(comments) { comment in + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 4) { + Text(comment.path) + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.blue) + if let line = comment.position ?? comment.originalPosition { + Text("Pos \(line)") + .font(.caption2) + .foregroundStyle(.secondary) + } + } + MarkdownPreview(text: comment.body) + } + .padding(.vertical, 2) + } + } + } + } + .padding(.vertical, 4) + } +} + +#Preview { + List { + ReviewSummaryView( + review: .preview, + comments: [.preview], + ) + } +} diff --git a/Forji/Forji/Views/SearchableOverviewView.swift b/Forji/Forji/Views/SearchableOverviewView.swift new file mode 100644 index 0000000..20556f3 --- /dev/null +++ b/Forji/Forji/Views/SearchableOverviewView.swift @@ -0,0 +1,316 @@ +import ForgejoKit +import SwiftUI + +// swiftlint:disable:next type_body_length +struct SearchableOverviewView: View { + @State private var authService: AuthenticationService + @State private var pagination = PaginationState() + @State private var stateFilter: IssueFilterState = .open + @State private var searchText = "" + @State private var searchTask: Task? + @State private var showCreateFlow = false + @State private var involvementScope: InvolvementScope = .created + + private let issueService: IssueService? + private let issueType: String + private let navigationTitle: String + private let searchPrompt: String + private let emptyTitle: String + private let emptyOpenIcon: String + private let emptyClosedIcon: String + private let itemNoun: String + private let createButtonId: String + private let showReviewRequested: Bool + private let rowContent: (Issue) -> Row + private let detailContent: (Repository, Int, AuthenticationService) -> Detail + private let createContent: (Repository, AuthenticationService, Bool, @escaping () -> Void) -> CreateView + + init( + authService: AuthenticationService, + issueType: String, + navigationTitle: String, + searchPrompt: String, + emptyTitle: String, + emptyOpenIcon: String, + emptyClosedIcon: String, + itemNoun: String, + createButtonId: String, + showReviewRequested: Bool, + @ViewBuilder row: @escaping (Issue) -> Row, + @ViewBuilder detail: @escaping (Repository, Int, AuthenticationService) -> Detail, + @ViewBuilder createView: @escaping ( + Repository, AuthenticationService, Bool, @escaping () -> Void, + ) -> CreateView, + ) { + self.authService = authService + self.issueType = issueType + self.navigationTitle = navigationTitle + self.searchPrompt = searchPrompt + self.emptyTitle = emptyTitle + self.emptyOpenIcon = emptyOpenIcon + self.emptyClosedIcon = emptyClosedIcon + self.itemNoun = itemNoun + self.createButtonId = createButtonId + self.showReviewRequested = showReviewRequested + rowContent = row + detailContent = detail + createContent = createView + issueService = authService.client.map { IssueService(client: $0) } + } + + private var emptyDescription: String { + if !searchText.isEmpty { + return "No \(itemNoun) matching your search." + } + if stateFilter == .open { + return "All clear — no open \(itemNoun) to review." + } + let prefix = stateFilter == .all ? "" : stateFilter.rawValue + " " + return "No \(prefix)\(itemNoun) found." + } + + private var hasNonDefaultFilters: Bool { + stateFilter != .open || involvementScope != .created + } + + private var filterSummaryText: String { + let stateLabel = switch stateFilter { + case .open: "Open" + case .closed: "Closed" + case .all: "All" + } + let scopeLabel = scopeDisplayLabel(involvementScope) + if stateFilter == .open, involvementScope == .involved { + return stateLabel + } + return "\(stateLabel) · \(scopeLabel)" + } + + private func scopeDisplayLabel(_ scope: InvolvementScope) -> String { + switch scope { + case .involved: "All" + case .created: "Created by you" + case .assigned: "Assigned to you" + case .mentioned: "Mentioned" + case .reviewRequested: "Review requested" + } + } + + var body: some View { + @Bindable var pagination = pagination + ZStack(alignment: .bottomTrailing) { + VStack(spacing: 0) { + Text(filterSummaryText) + .font(.caption) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity) + .padding(.horizontal) + .padding(.vertical, 6) + .accessibilityIdentifier("filter-summary") + + List { + if pagination.isLoading, pagination.items.isEmpty { + LoadingListSection() + } else if pagination.items.isEmpty { + ContentUnavailableView { + Label(emptyTitle, systemImage: stateFilter == .open ? emptyOpenIcon : emptyClosedIcon) + .foregroundStyle(stateFilter == .open ? .green : .secondary) + } description: { + Text(emptyDescription) + } + } else { + Section { + ForEach(pagination.items) { issue in + if let repository = issue.repository { + NavigationLink { + detailContent(repository, issue.number, authService) + } label: { + rowContent(issue) + } + } else { + rowContent(issue) + } + } + + if pagination.hasMore { + ProgressView() + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + .accessibilityIdentifier("load-more-indicator") + .task { + await loadMore() + } + } + } + } + } + .listStyle(.insetGrouped) + } + + FloatingCreateButton(action: { showCreateFlow = true }) + .accessibilityIdentifier(createButtonId) + } + .navigationTitle(navigationTitle) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Menu { + Section("State") { + filterButton("Open", isSelected: stateFilter == .open) { + stateFilter = .open + } + filterButton("Closed", isSelected: stateFilter == .closed) { + stateFilter = .closed + } + filterButton("All", isSelected: stateFilter == .all) { + stateFilter = .all + } + } + Section("Scope") { + filterButton("All", isSelected: involvementScope == .involved) { + involvementScope = .involved + } + filterButton("Created by you", isSelected: involvementScope == .created) { + involvementScope = .created + } + filterButton("Assigned to you", isSelected: involvementScope == .assigned) { + involvementScope = .assigned + } + filterButton("Mentioned", isSelected: involvementScope == .mentioned) { + involvementScope = .mentioned + } + if showReviewRequested { + filterButton( + "Review requested", + isSelected: involvementScope == .reviewRequested, + ) { + involvementScope = .reviewRequested + } + } + } + } label: { + Image( + systemName: hasNonDefaultFilters + ? "line.3.horizontal.decrease.circle.fill" + : "line.3.horizontal.decrease.circle", + ) + } + .accessibilityIdentifier("filter-menu-button") + } + } + .searchable(text: $searchText, prompt: searchPrompt) + .refreshable { + await reloadItems().value + } + .task { + reloadItems() + } + .onChange(of: stateFilter) { + reloadItems(clearItems: true) + } + .onChange(of: involvementScope) { + reloadItems(clearItems: true) + } + .debouncedSearch(text: $searchText, task: $searchTask) { + await reloadItems().value + } + .sheet(isPresented: $showCreateFlow) { + RepositoryPickerView(authService: authService) { repo in + createContent(repo, authService, true) { + showCreateFlow = false + reloadItems() + } + } + } + .errorAlert(message: $pagination.errorMessage, isPresented: $pagination.showError) + } + + private func filterButton( + _ label: String, + isSelected: Bool, + action: @escaping () -> Void, + ) -> some View { + Button(action: action) { + if isSelected { + Label(label, systemImage: "checkmark") + } else { + Text(label) + } + } + } + + private func searchIssues(service: IssueService, page: Int, limit: Int) async throws -> [Issue] { + switch involvementScope { + case .involved: + // No involvement flags — the API uses AND logic when multiple are set, + // so we omit all flags to show everything visible to the user. + try await service.searchIssues( + type: issueType, state: stateFilter.rawValue, query: searchText, + page: page, limit: limit, + ) + case .created: + try await service.searchIssues( + type: issueType, state: stateFilter.rawValue, query: searchText, + page: page, limit: limit, created: true, + ) + case .assigned: + try await service.searchIssues( + type: issueType, state: stateFilter.rawValue, query: searchText, + page: page, limit: limit, assigned: true, + ) + case .mentioned: + try await service.searchIssues( + type: issueType, state: stateFilter.rawValue, query: searchText, + page: page, limit: limit, mentioned: true, + ) + case .reviewRequested: + try await service.searchIssues( + type: issueType, state: stateFilter.rawValue, query: searchText, + page: page, limit: limit, reviewRequested: true, + ) + } + } + + @discardableResult + private func reloadItems(clearItems: Bool = false) -> Task { + guard let issueService else { return Task {} } + return pagination.reload(clearItems: clearItems) { [self] page, limit in + try await searchIssues(service: issueService, page: page, limit: limit) + } + } + + private func loadMore() async { + guard let issueService else { return } + await pagination.loadMore { [self] page, limit in + try await searchIssues(service: issueService, page: page, limit: limit) + } + } + + #if DEBUG + init( + preview _: Void, + authService: AuthenticationService, + items: [Issue], + @ViewBuilder row: @escaping (Issue) -> Row, + @ViewBuilder detail: @escaping (Repository, Int, AuthenticationService) -> Detail, + @ViewBuilder createView: @escaping ( + Repository, AuthenticationService, Bool, @escaping () -> Void, + ) -> CreateView, + ) { + self.authService = authService + issueService = nil + issueType = "issues" + navigationTitle = "" + searchPrompt = "" + emptyTitle = "" + emptyOpenIcon = "" + emptyClosedIcon = "" + itemNoun = "" + createButtonId = "" + showReviewRequested = false + rowContent = row + detailContent = detail + createContent = createView + _pagination = State(initialValue: PaginationState(items: items)) + } + #endif +} diff --git a/Forji/Forji/Views/StateAccent.swift b/Forji/Forji/Views/StateAccent.swift new file mode 100644 index 0000000..16aaa77 --- /dev/null +++ b/Forji/Forji/Views/StateAccent.swift @@ -0,0 +1,22 @@ +import SwiftUI + +struct StateAccentModifier: ViewModifier { + let color: Color + + func body(content: Content) -> some View { + content + .overlay(alignment: .leading) { + RoundedRectangle(cornerRadius: 1.5) + .fill(color) + .frame(width: 3) + .padding(.vertical, 4) + .offset(x: -12) + } + } +} + +extension View { + func stateAccent(_ color: Color) -> some View { + modifier(StateAccentModifier(color: color)) + } +} diff --git a/Forji/ForjiTests/CommentSheetTests.swift b/Forji/ForjiTests/CommentSheetTests.swift new file mode 100644 index 0000000..6fdb8da --- /dev/null +++ b/Forji/ForjiTests/CommentSheetTests.swift @@ -0,0 +1,36 @@ +import Foundation +import Testing +import ForgejoKit +@testable import Forji + +@MainActor +struct CommentSheetTests { + + @Test func onSubmitClosureIsInvoked() async throws { + var submittedBody: String? + let sheet = CommentSheet(users: []) { body in + submittedBody = body + } + #expect(sheet.users.isEmpty) + try await sheet.onSubmit(" hello world ") + #expect(submittedBody == " hello world ") + } + + @Test func usersPassedThrough() { + let user = User( + id: 1, + login: "testuser", + fullName: "Test User", + email: "test@example.com", + avatarUrl: "https://example.com/avatar.png" + ) + let sheet = CommentSheet(users: [user]) { _ in } + #expect(sheet.users.count == 1) + #expect(sheet.users.first?.login == "testuser") + } + + @Test func emptyUsersForIssues() { + let sheet = CommentSheet(users: []) { _ in } + #expect(sheet.users.isEmpty) + } +} diff --git a/Forji/ForjiTests/ForjiTests.swift b/Forji/ForjiTests/ForjiTests.swift new file mode 100644 index 0000000..fd1156c --- /dev/null +++ b/Forji/ForjiTests/ForjiTests.swift @@ -0,0 +1,26 @@ +import Foundation +import Testing +import ForgejoKit +@testable import Forji + +struct ForjiTests { + + @Test func formatRelativeDateReturnsNonEmptyString() { + let fixedDate = Date(timeIntervalSince1970: 0) + let result = formatRelativeDate(fixedDate) + #expect(!result.isEmpty) + } + + @Test func formatRelativeDateHandlesRecentDate() { + let fixedDate = Date(timeIntervalSince1970: 1000) + let result = formatRelativeDate(fixedDate) + #expect(!result.isEmpty) + } + + @Test func formatRelativeDateReturnsSameValueOnRepeatedCalls() { + let date = Date().addingTimeInterval(-3600) + let first = formatRelativeDate(date) + let second = formatRelativeDate(date) + #expect(first == second) + } +} diff --git a/Forji/ForjiTests/KeychainManagerTests.swift b/Forji/ForjiTests/KeychainManagerTests.swift new file mode 100644 index 0000000..db8dd1d --- /dev/null +++ b/Forji/ForjiTests/KeychainManagerTests.swift @@ -0,0 +1,136 @@ +import Foundation +import Testing +@testable import Forji + +struct KeychainManagerTests { + + // MARK: - Key format consistency + + @Test func keychainKeyFormatMatchesBetweenSaveAndGet() async throws { + let server = "https://forgejo.example.com" + let username = "testuser" + let password = "secret123" + + // Save and retrieve to verify key format consistency + try await KeychainManager.shared.savePassword(password, for: server, username: username) + let retrieved = try await KeychainManager.shared.getPassword(for: server, username: username) + #expect(retrieved == password) + + // Clean up + try await KeychainManager.shared.deletePassword(for: server, username: username) + } + + @Test func keychainDeleteNonExistentDoesNotThrow() async throws { + // Deleting a key that doesn't exist should not throw + // (errSecItemNotFound is accepted) + try await KeychainManager.shared.deletePassword( + for: "https://nonexistent.example.com", + username: "nobody" + ) + } + + @Test func keychainGetNonExistentThrowsNotFound() async { + do { + _ = try await KeychainManager.shared.getPassword( + for: "https://nonexistent.example.com", + username: "nobody" + ) + Issue.record("Expected KeychainError.notFound") + } catch is KeychainError { + // Expected + } catch { + Issue.record("Unexpected error: \(error)") + } + } + + @Test func keychainSaveOverwritesExistingEntry() async throws { + let server = "https://overwrite-test.example.com" + let username = "user" + + try await KeychainManager.shared.savePassword("first", for: server, username: username) + try await KeychainManager.shared.savePassword("second", for: server, username: username) + + let retrieved = try await KeychainManager.shared.getPassword(for: server, username: username) + #expect(retrieved == "second") + + // Clean up + try await KeychainManager.shared.deletePassword(for: server, username: username) + } + + @Test func keychainDifferentUsernamesSameServer() async throws { + let server = "https://multi-user.example.com" + + try await KeychainManager.shared.savePassword("pass1", for: server, username: "user1") + try await KeychainManager.shared.savePassword("pass2", for: server, username: "user2") + + let p1 = try await KeychainManager.shared.getPassword(for: server, username: "user1") + let p2 = try await KeychainManager.shared.getPassword(for: server, username: "user2") + #expect(p1 == "pass1") + #expect(p2 == "pass2") + + // Clean up + try await KeychainManager.shared.deletePassword(for: server, username: "user1") + try await KeychainManager.shared.deletePassword(for: server, username: "user2") + } + + @Test func keychainTokenSaveAndRetrieve() async throws { + let server = "https://token-test.example.com" + let username = "tokenuser" + let token = "abc123tokenvalue" + + try await KeychainManager.shared.saveToken(token, for: server, username: username) + let retrieved = try await KeychainManager.shared.getToken(for: server, username: username) + #expect(retrieved == token) + + // Clean up + try await KeychainManager.shared.deleteToken(for: server, username: username) + + // Verify deletion + do { + _ = try await KeychainManager.shared.getToken(for: server, username: username) + Issue.record("Expected KeychainError.notFound after delete") + } catch is KeychainError { + // Expected + } + } + + @Test func keychainTokenAndPasswordAreSeparateKeys() async throws { + let server = "https://separate-keys.example.com" + let username = "user" + let password = "mypassword" + let token = "mytoken" + + try await KeychainManager.shared.savePassword(password, for: server, username: username) + try await KeychainManager.shared.saveToken(token, for: server, username: username) + + let retrievedPassword = try await KeychainManager.shared.getPassword(for: server, username: username) + let retrievedToken = try await KeychainManager.shared.getToken(for: server, username: username) + + #expect(retrievedPassword == password) + #expect(retrievedToken == token) + + // Clean up + try await KeychainManager.shared.deletePassword(for: server, username: username) + try await KeychainManager.shared.deleteToken(for: server, username: username) + } + + @Test func keychainNormalizedAndRawURLsAreDifferentKeys() async throws { + // This tests the exact issue: if you save with normalized URL but delete with raw URL, + // the deletion will miss. + let raw = "https://forgejo.example.com/" + let normalized = "https://forgejo.example.com" + + try await KeychainManager.shared.savePassword("pass", for: normalized, username: "user") + + // Getting with the raw URL (with trailing slash) should fail because keys differ + do { + _ = try await KeychainManager.shared.getPassword(for: raw, username: "user") + Issue.record("Should not find password with non-normalized key") + } catch is KeychainError { + // Expected — keys differ + } + + // Clean up + try await KeychainManager.shared.deletePassword(for: normalized, username: "user") + } +} diff --git a/Forji/ForjiTests/LanguageColorTests.swift b/Forji/ForjiTests/LanguageColorTests.swift new file mode 100644 index 0000000..140d12a --- /dev/null +++ b/Forji/ForjiTests/LanguageColorTests.swift @@ -0,0 +1,48 @@ +import SwiftUI +import Testing +@testable import Forji + +struct LanguageColorTests { + + // MARK: - Determinism + + @Test func sameLanguageReturnsSameColor() { + let color1 = colorForLanguage("Swift") + let color2 = colorForLanguage("Swift") + #expect(color1 == color2) + } + + @Test func differentLanguagesReturnDifferentColors() { + let swift = colorForLanguage("Swift") + let python = colorForLanguage("Python") + let rust = colorForLanguage("Rust") + // At least some should differ (extremely unlikely all 3 hash to same hue) + let allSame = (swift == python) && (python == rust) + #expect(!allSame) + } + + // MARK: - Edge cases + + @Test func emptyStringDoesNotCrash() { + let color = colorForLanguage("") + // Should return a valid color, not crash + _ = color + } + + @Test func caseMatters() { + let lower = colorForLanguage("swift") + let upper = colorForLanguage("Swift") + #expect(lower != upper) + } + + @Test func nonAsciiLanguageNames() { + // Should handle non-ASCII without crashing + let color = colorForLanguage("Ren\u{2019}Py") + _ = color + } + + @Test func longLanguageName() { + let color = colorForLanguage(String(repeating: "x", count: 10000)) + _ = color + } +} diff --git a/Forji/ForjiTests/MarkdownComponentsTests.swift b/Forji/ForjiTests/MarkdownComponentsTests.swift new file mode 100644 index 0000000..46fedc2 --- /dev/null +++ b/Forji/ForjiTests/MarkdownComponentsTests.swift @@ -0,0 +1,32 @@ +import Foundation +import Testing +@testable import Forji + +struct MarkdownComponentsTests { + + @Test func repoRelativePathExtractsFilePath() { + let url = URL(string: "https://forgejo.example.com/owner/repo/src/branch/main/path/to/file.swift")! + #expect(repoRelativePath(from: url) == "path/to/file.swift") + } + + @Test func repoRelativePathReturnsNilForBranchOnly() { + // URL ends right after the ref — no file path follows + let url = URL(string: "https://forgejo.example.com/owner/repo/src/branch/main")! + #expect(repoRelativePath(from: url) == nil) + } + + @Test func repoRelativePathReturnsNilForUnrelatedURL() { + let url = URL(string: "https://example.com/some/other/page")! + #expect(repoRelativePath(from: url) == nil) + } + + @Test func repoRelativePathHandlesSubpath() { + let url = URL(string: "https://example.com/forgejo/owner/repo/src/branch/main/README.md")! + #expect(repoRelativePath(from: url) == "README.md") + } + + @Test func repoRelativePathHandlesNestedFilePath() { + let url = URL(string: "https://forgejo.example.com/owner/repo/src/branch/feature/a/b/c.txt")! + #expect(repoRelativePath(from: url) == "a/b/c.txt") + } +} diff --git a/Forji/ForjiTests/MermaidParserTests.swift b/Forji/ForjiTests/MermaidParserTests.swift new file mode 100644 index 0000000..69e53f0 --- /dev/null +++ b/Forji/ForjiTests/MermaidParserTests.swift @@ -0,0 +1,188 @@ +import Foundation +import Testing +@testable import Forji + +@MainActor +struct MermaidParserTests { + + // MARK: - No mermaid blocks + + @Test func noMermaidBlocks() { + let markdown = "# Hello\n\nSome regular markdown text." + let segments = MermaidParser.parse(markdown) + #expect(segments == [.text(markdown)]) + } + + @Test func emptyString() { + let segments = MermaidParser.parse("") + #expect(segments == [.text("")]) + } + + // MARK: - Single mermaid block + + @Test func singleMermaidBlockWithSurroundingText() { + let markdown = """ + # Title + + Some text before. + + ```mermaid + graph TD + A --> B + ``` + + Some text after. + """ + let segments = MermaidParser.parse(markdown) + #expect(segments.count == 3) + + if case .text(let before) = segments[0] { + #expect(before.contains("Title")) + #expect(before.contains("Some text before.")) + } else { + Issue.record("Expected .text segment") + } + + if case .mermaid(let code) = segments[1] { + #expect(code.contains("graph TD")) + #expect(code.contains("A --> B")) + } else { + Issue.record("Expected .mermaid segment") + } + + if case .text(let after) = segments[2] { + #expect(after.contains("Some text after.")) + } else { + Issue.record("Expected .text segment") + } + } + + // MARK: - Multiple mermaid blocks + + @Test func multipleMermaidBlocks() { + let markdown = """ + Text before first. + + ```mermaid + graph LR + A --> B + ``` + + Text between. + + ```mermaid + sequenceDiagram + Alice->>Bob: Hello + ``` + + Text after second. + """ + let segments = MermaidParser.parse(markdown) + #expect(segments.count == 5) + + if case .text = segments[0] {} else { Issue.record("Expected .text") } + if case .mermaid(let code1) = segments[1] { + #expect(code1.contains("graph LR")) + } else { Issue.record("Expected .mermaid") } + if case .text = segments[2] {} else { Issue.record("Expected .text") } + if case .mermaid(let code2) = segments[3] { + #expect(code2.contains("sequenceDiagram")) + } else { Issue.record("Expected .mermaid") } + if case .text = segments[4] {} else { Issue.record("Expected .text") } + } + + // MARK: - Edge cases + + @Test func mermaidBlockAtStart() { + let markdown = """ + ```mermaid + graph TD + A --> B + ``` + + Text after. + """ + let segments = MermaidParser.parse(markdown) + #expect(segments.count == 2) + if case .mermaid = segments[0] {} else { Issue.record("Expected .mermaid") } + if case .text = segments[1] {} else { Issue.record("Expected .text") } + } + + @Test func mermaidBlockAtEnd() { + let markdown = """ + Text before. + + ```mermaid + graph TD + A --> B + ``` + """ + let segments = MermaidParser.parse(markdown) + #expect(segments.count == 2) + if case .text = segments[0] {} else { Issue.record("Expected .text") } + if case .mermaid = segments[1] {} else { Issue.record("Expected .mermaid") } + } + + @Test func emptyMermaidBlock() { + let markdown = """ + Text before. + + ```mermaid + ``` + + Text after. + """ + let segments = MermaidParser.parse(markdown) + for segment in segments { + if case .mermaid = segment { + Issue.record("Empty mermaid block should be skipped") + } + } + } + + // MARK: - Non-mermaid code blocks + + @Test func nonMermaidCodeBlocksLeftAsText() { + let markdown = """ + # Code example + + ```python + print("hello") + ``` + + ```javascript + console.log("hi") + ``` + """ + let segments = MermaidParser.parse(markdown) + #expect(segments == [.text(markdown)]) + } + + @Test func mixedMermaidAndNonMermaidBlocks() { + let markdown = """ + ```python + print("hello") + ``` + + ```mermaid + graph TD + A --> B + ``` + + ```javascript + console.log("hi") + ``` + """ + let segments = MermaidParser.parse(markdown) + #expect(segments.count == 3) + if case .text(let before) = segments[0] { + #expect(before.contains("python")) + } else { Issue.record("Expected .text") } + if case .mermaid(let code) = segments[1] { + #expect(code.contains("graph TD")) + } else { Issue.record("Expected .mermaid") } + if case .text(let after) = segments[2] { + #expect(after.contains("javascript")) + } else { Issue.record("Expected .text") } + } +} diff --git a/Forji/ForjiTests/PaginationStateTests.swift b/Forji/ForjiTests/PaginationStateTests.swift new file mode 100644 index 0000000..edaa0b0 --- /dev/null +++ b/Forji/ForjiTests/PaginationStateTests.swift @@ -0,0 +1,387 @@ +import Foundation +import Testing +@testable import Forji + +// MARK: - Test helpers + +/// Waits on MainActor until a condition is true, yielding between checks. +@MainActor +private func yieldUntil(_ condition: @MainActor () -> Bool) async { + for _ in 0 ..< 500 { + if condition() { return } + await Task.yield() + } +} + +/// A fetch whose completion the test controls via continuations. +@MainActor +private final class ControllableFetch { + private(set) var callCount = 0 + private var continuation: CheckedContinuation<[String], any Error>? + var isPending: Bool { continuation != nil } + + func fetch(page: Int, limit: Int) async throws -> [String] { + callCount += 1 + return try await withCheckedThrowingContinuation { cont in + self.continuation = cont + } + } + + func complete(returning items: [String]) { + let cont = continuation + continuation = nil + cont?.resume(returning: items) + } + + func complete(throwing error: any Error) { + let cont = continuation + continuation = nil + cont?.resume(throwing: error) + } +} + +// MARK: - Basic reload + +struct PaginationStateBasicTests { + + @Test @MainActor func reloadSetsItems() async { + let pagination = PaginationState(pageSize: 5) + await pagination.reload { _, _ in ["a", "b", "c"] }.value + #expect(pagination.items == ["a", "b", "c"]) + #expect(!pagination.isLoading) + #expect(!pagination.hasMore) // 3 < pageSize 5 + } + + @Test @MainActor func reloadSetsHasMoreWhenFull() async { + let pagination = PaginationState(pageSize: 3) + await pagination.reload { _, _ in ["a", "b", "c"] }.value + #expect(pagination.hasMore) // 3 >= pageSize 3 + } + + @Test @MainActor func reloadReplacesExistingItems() async { + let pagination = PaginationState(pageSize: 20) + await pagination.reload { _, _ in ["old"] }.value + await pagination.reload { _, _ in ["new"] }.value + #expect(pagination.items == ["new"]) + } + + @Test @MainActor func reloadError() async { + let pagination = PaginationState(pageSize: 20) + await pagination.reload { _, _ in throw URLError(.badServerResponse) }.value + #expect(pagination.items.isEmpty) + #expect(pagination.showError) + #expect(pagination.errorMessage != nil) + #expect(!pagination.isLoading) + } + + @Test @MainActor func reloadClearsErrorState() async { + let pagination = PaginationState(pageSize: 20) + await pagination.reload { _, _ in throw URLError(.badServerResponse) }.value + #expect(pagination.showError) + + await pagination.reload { _, _ in ["ok"] }.value + #expect(!pagination.showError) + #expect(pagination.items == ["ok"]) + } + + @Test @MainActor func clearItemsEmptiesImmediately() async { + let pagination = PaginationState(pageSize: 20) + await pagination.reload { _, _ in ["a", "b"] }.value + + let fetcher = ControllableFetch() + let task = pagination.reload(clearItems: true) { page, limit in + try await fetcher.fetch(page: page, limit: limit) + } + // Items should be cleared synchronously before the fetch starts + #expect(pagination.items.isEmpty) + #expect(pagination.isLoading) + + await yieldUntil { fetcher.isPending } + fetcher.complete(returning: ["c"]) + await task.value + #expect(pagination.items == ["c"]) + #expect(!pagination.isLoading) + } +} + +// MARK: - Cancellation + +struct PaginationStateCancellationTests { + + @Test @MainActor func cancellationErrorIgnored() async { + let pagination = PaginationState(pageSize: 20) + await pagination.reload { _, _ in ["initial"] }.value + + await pagination.reload { _, _ in throw CancellationError() }.value + + // Items from new reload are empty since it threw before returning items + // But cancellation should not surface an error + #expect(!pagination.showError) + #expect(!pagination.isLoading) + } + + @Test @MainActor func urlErrorCancelledIgnored() async { + let pagination = PaginationState(pageSize: 20) + + await pagination.reload { _, _ in throw URLError(.cancelled) }.value + + #expect(pagination.items.isEmpty) + #expect(!pagination.showError) + #expect(!pagination.isLoading) + } +} + +// MARK: - Concurrent reloads (internal task cancellation) + +struct PaginationStateConcurrentTests { + + @Test @MainActor func secondReloadCancelsFirst() async { + let pagination = PaginationState(pageSize: 20) + let fetcherA = ControllableFetch() + let fetcherB = ControllableFetch() + + // Start reload A + pagination.reload { page, limit in + try await fetcherA.fetch(page: page, limit: limit) + } + await yieldUntil { fetcherA.isPending } + + // Start reload B — cancels A's internal task + let taskB = pagination.reload { page, limit in + try await fetcherB.fetch(page: page, limit: limit) + } + await yieldUntil { fetcherB.isPending } + + // Complete both — only B's results should be applied + fetcherA.complete(returning: ["stale"]) + fetcherB.complete(returning: ["fresh"]) + await taskB.value + + #expect(pagination.items == ["fresh"]) + #expect(!pagination.isLoading) + } + + @Test @MainActor func staleLoadCompletingFirstIsDiscarded() async { + let pagination = PaginationState(pageSize: 20) + let fetcherA = ControllableFetch() + let fetcherB = ControllableFetch() + + pagination.reload { page, limit in + try await fetcherA.fetch(page: page, limit: limit) + } + await yieldUntil { fetcherA.isPending } + + let taskB = pagination.reload { page, limit in + try await fetcherB.fetch(page: page, limit: limit) + } + await yieldUntil { fetcherB.isPending } + + // Complete A first (stale, its task was cancelled) — must be discarded + fetcherA.complete(returning: ["stale"]) + await yieldUntil { fetcherA.callCount == 1 } + + #expect(pagination.items.isEmpty) // B hasn't completed yet + #expect(pagination.isLoading) // B is still loading + + // Complete B (fresh) + fetcherB.complete(returning: ["fresh"]) + await taskB.value + + #expect(pagination.items == ["fresh"]) + #expect(!pagination.isLoading) + } + + @Test @MainActor func threeOverlappingReloadsOnlyLatestApplied() async { + let pagination = PaginationState(pageSize: 20) + let fetcherA = ControllableFetch() + let fetcherB = ControllableFetch() + let fetcherC = ControllableFetch() + + pagination.reload { page, limit in + try await fetcherA.fetch(page: page, limit: limit) + } + await yieldUntil { fetcherA.isPending } + + pagination.reload { page, limit in + try await fetcherB.fetch(page: page, limit: limit) + } + await yieldUntil { fetcherB.isPending } + + let taskC = pagination.reload { page, limit in + try await fetcherC.fetch(page: page, limit: limit) + } + await yieldUntil { fetcherC.isPending } + + // Complete in order A, B, C — only C should be applied + fetcherA.complete(returning: ["a"]) + fetcherB.complete(returning: ["b"]) + fetcherC.complete(returning: ["c"]) + await taskC.value + + #expect(pagination.items == ["c"]) + #expect(!pagination.isLoading) + } + + @Test @MainActor func staleErrorDoesNotSurface() async { + let pagination = PaginationState(pageSize: 20) + let fetcherA = ControllableFetch() + let fetcherB = ControllableFetch() + + pagination.reload { page, limit in + try await fetcherA.fetch(page: page, limit: limit) + } + await yieldUntil { fetcherA.isPending } + + let taskB = pagination.reload { page, limit in + try await fetcherB.fetch(page: page, limit: limit) + } + await yieldUntil { fetcherB.isPending } + + // A fails with error — but its task was cancelled, so no alert + fetcherA.complete(throwing: URLError(.badServerResponse)) + fetcherB.complete(returning: ["ok"]) + await taskB.value + + #expect(pagination.items == ["ok"]) + #expect(!pagination.showError) + } + + @Test @MainActor func isLoadingStaysTrueWhileLatestIsInFlight() async { + let pagination = PaginationState(pageSize: 20) + let fetcherA = ControllableFetch() + let fetcherB = ControllableFetch() + + pagination.reload { page, limit in + try await fetcherA.fetch(page: page, limit: limit) + } + await yieldUntil { fetcherA.isPending } + #expect(pagination.isLoading) + + let taskB = pagination.reload { page, limit in + try await fetcherB.fetch(page: page, limit: limit) + } + await yieldUntil { fetcherB.isPending } + #expect(pagination.isLoading) + + // Complete stale A — isLoading must REMAIN true (B is still in flight) + fetcherA.complete(returning: ["stale"]) + await yieldUntil { fetcherA.callCount == 1 } + #expect(pagination.isLoading, "isLoading must stay true while latest reload (B) is pending") + + fetcherB.complete(returning: ["fresh"]) + await taskB.value + #expect(!pagination.isLoading) + } + + /// Simulates the exact pattern from SearchableOverviewView: + /// .task fires reload → user changes filter → onChange fires reload (cancels previous) + @Test @MainActor func simulatedViewFilterChange() async { + let pagination = PaginationState(pageSize: 20) + let initialFetcher = ControllableFetch() + let filterFetcher = ControllableFetch() + + // Simulate .task { pagination.reload { ... } } + pagination.reload { page, limit in + try await initialFetcher.fetch(page: page, limit: limit) + } + await yieldUntil { initialFetcher.isPending } + + // Simulate onChange: user changed filter — just call reload again + let filterTask = pagination.reload(clearItems: true) { page, limit in + try await filterFetcher.fetch(page: page, limit: limit) + } + await yieldUntil { filterFetcher.isPending } + + // Initial load completes (stale, its task was cancelled) + initialFetcher.complete(returning: ["stale-created-results"]) + // Filter load completes with correct results + filterFetcher.complete(returning: ["fresh-involved-results"]) + + await filterTask.value + + #expect(pagination.items == ["fresh-involved-results"]) + #expect(!pagination.isLoading) + #expect(!pagination.showError) + } +} + +// MARK: - loadMore + +struct PaginationStateLoadMoreTests { + + @Test @MainActor func loadMoreAppendsItems() async { + let pagination = PaginationState(pageSize: 2) + await pagination.reload { _, _ in ["a", "b"] }.value + #expect(pagination.hasMore) + + await pagination.loadMore { page, limit in + #expect(page == 2) + return ["c", "d"] + } + #expect(pagination.items == ["a", "b", "c", "d"]) + } + + @Test @MainActor func loadMoreStopsWhenNoMore() async { + let pagination = PaginationState(pageSize: 5) + await pagination.reload { _, _ in ["a", "b", "c", "d", "e"] }.value + + await pagination.loadMore { _, _ in ["f"] } // 1 < pageSize -> no more + #expect(!pagination.hasMore) + #expect(pagination.items == ["a", "b", "c", "d", "e", "f"]) + } + + @Test @MainActor func loadMoreSkipsWhenAlreadyLoading() async { + let pagination = PaginationState(pageSize: 2) + await pagination.reload { _, _ in ["a", "b"] }.value + + let fetcher = ControllableFetch() + let firstMore = Task { + await pagination.loadMore { page, limit in + try await fetcher.fetch(page: page, limit: limit) + } + } + await yieldUntil { fetcher.isPending } + + // Second loadMore should bail because isLoading is true + var secondCalled = false + await pagination.loadMore { _, _ in + secondCalled = true + return ["should-not-run"] + } + #expect(!secondCalled) + + fetcher.complete(returning: ["c"]) + await firstMore.value + } + + @Test @MainActor func loadMoreDiscardedWhenReloadSupersedes() async { + let pagination = PaginationState(pageSize: 2) + await pagination.reload { _, _ in ["a", "b"] }.value + + let moreFetcher = ControllableFetch() + let moreTask = Task { + await pagination.loadMore { page, limit in + try await moreFetcher.fetch(page: page, limit: limit) + } + } + await yieldUntil { moreFetcher.isPending } + + // A reload supersedes the in-flight loadMore (cancels its task) + let freshFetcher = ControllableFetch() + let reloadTask = pagination.reload { page, limit in + try await freshFetcher.fetch(page: page, limit: limit) + } + await yieldUntil { freshFetcher.isPending } + + // loadMore completes — should be discarded (task was cancelled) + moreFetcher.complete(returning: ["stale-more"]) + await moreTask.value + + // reload completes — should be applied + freshFetcher.complete(returning: ["fresh"]) + await reloadTask.value + + #expect(pagination.items == ["fresh"]) + #expect(!pagination.isLoading) + } +} diff --git a/Forji/ForjiTests/StateTests.swift b/Forji/ForjiTests/StateTests.swift new file mode 100644 index 0000000..eaac1ae --- /dev/null +++ b/Forji/ForjiTests/StateTests.swift @@ -0,0 +1,136 @@ +import Foundation +import Testing +import ForgejoKit +@testable import Forji + +struct StateTests { + + // MARK: - IssueState + + @Test func issueStateOpenRawValue() { + let state = IssueState(rawValue: "open") + #expect(state == .open) + } + + @Test func issueStateClosedRawValue() { + let state = IssueState(rawValue: "closed") + #expect(state == .closed) + } + + @Test func issueStateUnknownReturnsNil() { + let state = IssueState(rawValue: "unknown") + #expect(state == nil) + } + + @Test func issueStateEmptyReturnsNil() { + let state = IssueState(rawValue: "") + #expect(state == nil) + } + + // MARK: - PullRequestState + + @Test func pullRequestStateOpenRawValue() { + let state = PullRequestState(rawValue: "open") + #expect(state == .open) + } + + @Test func pullRequestStateClosedRawValue() { + let state = PullRequestState(rawValue: "closed") + #expect(state == .closed) + } + + @Test func pullRequestStateUnknownReturnsNil() { + let state = PullRequestState(rawValue: "unknown") + #expect(state == nil) + } + + // MARK: - Issue stateValue extension defaults to closed for unknown + + @Test func issueStateValueDefaultsToClosedForUnknown() { + let issue = Issue( + id: 1, + number: 1, + title: "Test", + state: "unknown", + user: User(id: 1, login: "test", avatarUrl: nil), + createdAt: Date(), + updatedAt: Date() + ) + #expect(issue.stateValue == .closed) + } + + @Test func issueStateValueReturnsOpenForOpen() { + let issue = Issue( + id: 1, + number: 1, + title: "Test", + state: "open", + user: User(id: 1, login: "test", avatarUrl: nil), + createdAt: Date(), + updatedAt: Date() + ) + #expect(issue.stateValue == .open) + } + + // MARK: - NotificationSubjectState + + @Test func notificationSubjectStateMerged() { + let state = NotificationSubjectState(rawValue: "merged") + #expect(state == .merged) + } + + @Test func notificationSubjectStateUnknownReturnsNil() { + let state = NotificationSubjectState(rawValue: "pending") + #expect(state == nil) + } + + // MARK: - IssueFilterState + + @Test func issueFilterStateAllRawValue() { + let state = IssueFilterState(rawValue: "all") + #expect(state == .all) + } + + // MARK: - PullRequestFilterState + + @Test func pullRequestFilterStateAllRawValue() { + let state = PullRequestFilterState(rawValue: "all") + #expect(state == .all) + } + + // MARK: - InvolvementScope + + @Test func involvementScopeInvolvedRawValue() { + let scope = InvolvementScope(rawValue: "involved") + #expect(scope == .involved) + } + + @Test func involvementScopeCreatedRawValue() { + let scope = InvolvementScope(rawValue: "created") + #expect(scope == .created) + } + + @Test func involvementScopeAssignedRawValue() { + let scope = InvolvementScope(rawValue: "assigned") + #expect(scope == .assigned) + } + + @Test func involvementScopeMentionedRawValue() { + let scope = InvolvementScope(rawValue: "mentioned") + #expect(scope == .mentioned) + } + + @Test func involvementScopeReviewRequestedRawValue() { + let scope = InvolvementScope(rawValue: "review_requested") + #expect(scope == .reviewRequested) + } + + @Test func involvementScopeUnknownReturnsNil() { + let scope = InvolvementScope(rawValue: "unknown") + #expect(scope == nil) + } + + @Test func involvementScopeAllCases() { + #expect(InvolvementScope.allCases.count == 5) + } +} diff --git a/Forji/ForjiUITests/CommitHistoryUITests.swift b/Forji/ForjiUITests/CommitHistoryUITests.swift new file mode 100644 index 0000000..717f519 --- /dev/null +++ b/Forji/ForjiUITests/CommitHistoryUITests.swift @@ -0,0 +1,47 @@ +import XCTest + +final class CommitHistoryUITests: ForgejoReadOnlyUITestBase { + + override func tearDown() { + navigateBackToHome() + super.tearDown() + } + + // MARK: - Helpers + + @MainActor + private func navigateToCommitHistory() { + navigateToRepoDetail() + let commitsButton = app.buttons["commits-button"] + XCTAssertTrue(commitsButton.waitForExistence(timeout: 10), "Commits button not found") + commitsButton.tap() + } + + // MARK: - Commit History + + @MainActor + func testCommitHistoryNavigation() throws { + navigateToCommitHistory() + + // Commits list should appear with rows from seed data + let commitList = app.collectionViews.firstMatch + XCTAssertTrue(commitList.waitForExistence(timeout: 10), "Commit list not found") + + let firstCommitCell = commitList.cells.firstMatch + XCTAssertTrue(firstCommitCell.waitForExistence(timeout: 10), "No commit rows found") + + // The test repo has multiple commits from setup (auto_init + file creates) + XCTAssertTrue(commitList.cells.count >= 2, "Should have at least 2 commits from seed data") + + // Verify cells contain text (commit messages rendered) + let cellTexts = firstCommitCell.staticTexts + XCTAssertTrue(cellTexts.count >= 1, "Commit cells should contain text content") + + // Tap a commit to view its detail with diff + firstCommitCell.tap() + + // The Changes section should appear with diff content + let changesHeader = app.staticTexts["Changes"] + XCTAssertTrue(changesHeader.waitForExistence(timeout: 10), "Changes section not found in commit detail") + } +} diff --git a/Forji/ForjiUITests/ForgejoReadOnlyUITestBase.swift b/Forji/ForjiUITests/ForgejoReadOnlyUITestBase.swift new file mode 100644 index 0000000..ec8f555 --- /dev/null +++ b/Forji/ForjiUITests/ForgejoReadOnlyUITestBase.swift @@ -0,0 +1,123 @@ +import XCTest + +/// Base class for read-only UI tests that share a single app launch per class. +/// The app is launched once in `class setUp()` and reused across all test methods, +/// avoiding the overhead of repeated app launches for tests that don't mutate data. +class ForgejoReadOnlyUITestBase: XCTestCase, UITestNavigating { + + static var sharedApp: XCUIApplication! + static var sharedServerURL: String! + + override class func setUp() { + super.setUp() + + guard let url = ForgejoUITestBase.resolveTestServerURL(), + !url.isEmpty + else { + // Can't XCTSkip from class setUp — individual tests will skip + return + } + sharedServerURL = url + + let app = XCUIApplication() + app.launchArguments += [ + "-dev_serverURL", url, + "-dev_username", "testadmin", + "-dev_password", "admin1234", + ] + app.launch() + + let reposTab = app.tabBars.buttons["Repositories"] + guard reposTab.waitForExistence(timeout: 15) else { + XCTFail("HomeView did not appear after auto-login in class setUp") + return + } + sharedApp = app + } + + override class func tearDown() { + sharedApp = nil + sharedServerURL = nil + super.tearDown() + } + + // MARK: - Instance setup + + /// Non-optional convenience accessor (instance-level). + var app: XCUIApplication! { Self.sharedApp } + var serverURL: String! { Self.sharedServerURL } + + override func setUpWithError() throws { + continueAfterFailure = true + + guard Self.sharedServerURL != nil, Self.sharedApp != nil else { + throw XCTSkip("Integration server not running") + } + + // Force portrait orientation + XCUIDevice.shared.orientation = .portrait + + // Ensure we're at the home screen (Repositories tab as anchor) + let reposTab = app.tabBars.buttons["Repositories"] + if !reposTab.waitForExistence(timeout: 5) { + // Recovery: app may have crashed or navigated away — relaunch + let freshApp = XCUIApplication() + freshApp.launchArguments += [ + "-dev_serverURL", Self.sharedServerURL, + "-dev_username", "testadmin", + "-dev_password", "admin1234", + ] + freshApp.launch() + guard freshApp.tabBars.buttons["Repositories"].waitForExistence(timeout: 15) else { + XCTFail("Could not recover app to home screen") + return + } + Self.sharedApp = freshApp + } + } + + // MARK: - Helpers + + func navigateBackToHome() { + guard app != nil else { return } + + // Switch to Repositories tab first + let reposTab = app.tabBars.buttons["Repositories"] + if reposTab.exists && reposTab.isHittable { + reposTab.tap() + } + + // Pop any navigation stack on the Repositories tab back to root + let repoList = app.collectionViews["repo-list"] + for _ in 0..<10 { + if repoList.waitForExistence(timeout: 1) { break } + let navBarButtons = app.navigationBars.buttons + guard navBarButtons.count > 0 else { break } + let backButton = navBarButtons.element(boundBy: 0) + guard backButton.exists && backButton.isHittable else { break } + backButton.tap() + } + } + + func navigateToRepoDetail(_ repoName: String = "test-repo") { + app.tabBars.buttons["Repositories"].tap() + + // Ensure we're at the repo list root, not deep in navigation + let repoList = app.collectionViews["repo-list"] + if !repoList.waitForExistence(timeout: 3) { + for _ in 0..<5 { + let navBarButtons = app.navigationBars.buttons + guard navBarButtons.count > 0 else { break } + let backButton = navBarButtons.element(boundBy: 0) + guard backButton.exists && backButton.isHittable else { break } + backButton.tap() + if repoList.waitForExistence(timeout: 1) { break } + } + } + + let repoCell = app.staticTexts[repoName].firstMatch + XCTAssertTrue(repoCell.waitForExistence(timeout: 10)) + repoCell.tap() + } + +} diff --git a/Forji/ForjiUITests/ForgejoUITestBase.swift b/Forji/ForjiUITests/ForgejoUITestBase.swift new file mode 100644 index 0000000..2adbe69 --- /dev/null +++ b/Forji/ForjiUITests/ForgejoUITestBase.swift @@ -0,0 +1,62 @@ +import XCTest + +class ForgejoUITestBase: XCTestCase, UITestNavigating { + + var app: XCUIApplication! + var serverURL: String! + + override func setUpWithError() throws { + continueAfterFailure = true + + guard let url = Self.resolveTestServerURL(), + !url.isEmpty + else { + throw XCTSkip("Integration server not running") + } + serverURL = url + + // Force portrait orientation for consistent vertical space across test runs + XCUIDevice.shared.orientation = .portrait + + app = XCUIApplication() + app.launchArguments += [ + "-dev_serverURL", serverURL, + "-dev_username", "testadmin", + "-dev_password", "admin1234", + ] + } + + // MARK: - Server URL Resolution + + /// Resolves the Forgejo test server URL from simulator-specific temp files. + /// Each simulator gets its own file (keyed by device name) so parallel test + /// groups on different simulators can target different Forgejo instances. + static func resolveTestServerURL() -> String? { + let deviceName = (ProcessInfo.processInfo.environment["SIMULATOR_DEVICE_NAME"] ?? "") + .replacingOccurrences(of: " ", with: "_") + let specificPath = "/tmp/forgejo_test_url_\(deviceName).txt" + let genericPath = "/tmp/forgejo_test_url.txt" + + return ((try? String(contentsOfFile: specificPath, encoding: .utf8)) + ?? (try? String(contentsOfFile: genericPath, encoding: .utf8))) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + } + + // MARK: - Helpers + + /// Launches the app and waits for HomeView to appear via auto-login. + func loginAndWaitForHome() { + app.launch() + let reposTab = app.tabBars.buttons["Repositories"] + XCTAssertTrue(reposTab.waitForExistence(timeout: 15), "HomeView did not appear after auto-login") + } + + func navigateToRepoDetail(_ repoName: String = "test-repo") { + app.tabBars.buttons["Repositories"].tap() + + let repoCell = app.staticTexts[repoName].firstMatch + XCTAssertTrue(repoCell.waitForExistence(timeout: 10)) + repoCell.tap() + } + +} diff --git a/Forji/ForjiUITests/ForjiUITestsLaunchTests.swift b/Forji/ForjiUITests/ForjiUITestsLaunchTests.swift new file mode 100644 index 0000000..bdb3d7a --- /dev/null +++ b/Forji/ForjiUITests/ForjiUITestsLaunchTests.swift @@ -0,0 +1,31 @@ +import XCTest + +final class ForjiUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + + // Skip during integration test runs — these tests need manual credentials + if FileManager.default.fileExists(atPath: "/tmp/forgejo_test_url.txt") { + throw XCTSkip("Skipping launch tests during integration test run") + } + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/Forji/ForjiUITests/HomeScreenUITests.swift b/Forji/ForjiUITests/HomeScreenUITests.swift new file mode 100644 index 0000000..2a96752 --- /dev/null +++ b/Forji/ForjiUITests/HomeScreenUITests.swift @@ -0,0 +1,34 @@ +import XCTest + +final class HomeScreenUITests: ForgejoUITestBase { + + // MARK: - Home Screen (login, nav links, logout) + + @MainActor + func testHomeScreen() throws { + loginAndWaitForHome() + + // Verify home screen content + XCTAssertTrue(app.tabBars.buttons["Repositories"].exists) + + // All nav links present + XCTAssertTrue(app.tabBars.buttons["Issues"].exists) + XCTAssertTrue(app.tabBars.buttons["Pull Requests"].exists) + XCTAssertTrue(app.tabBars.buttons["Notifications"].exists) + + // Navigate to Settings tab and verify user info + app.tabBars.buttons["Settings"].tap() + XCTAssertTrue(app.staticTexts["@testadmin"].waitForExistence(timeout: 5)) + + // Scroll down to reveal logout button (may be below the fold) + app.swipeUp() + + // Logout returns to instance list + let logoutButton = app.buttons["home-logout-button"] + XCTAssertTrue(logoutButton.waitForExistence(timeout: 5)) + logoutButton.tap() + + let addButton = app.buttons["instance-add-button"] + XCTAssertTrue(addButton.waitForExistence(timeout: 5)) + } +} diff --git a/Forji/ForjiUITests/IssueMutatingUITests.swift b/Forji/ForjiUITests/IssueMutatingUITests.swift new file mode 100644 index 0000000..8c37279 --- /dev/null +++ b/Forji/ForjiUITests/IssueMutatingUITests.swift @@ -0,0 +1,88 @@ +import XCTest + +final class IssueMutatingUITests: ForgejoUITestBase { + + // MARK: - Issue Tab (list, filter, detail, label, create, edit, close/reopen) + + @MainActor + func testIssueTab() throws { + loginAndWaitForHome() + navigateToIssueTab() + + // Open issues visible + XCTAssertTrue(app.staticTexts["Test issue 1 from integration tests"].waitForExistence(timeout: 10)) + let issueList = app.collectionViews.firstMatch + XCTAssertTrue(issueList.cells.count >= 2, "Should have at least 2 open issues") + + // Filter by Closed + let closedButton = app.buttons["Closed"] + XCTAssertTrue(closedButton.waitForExistence(timeout: 10)) + closedButton.tap() + XCTAssertTrue(app.staticTexts["Test issue 3 from integration tests"].waitForExistence(timeout: 10)) + + // Back to Open + let openButton = app.buttons["Open"] + XCTAssertTrue(openButton.waitForExistence(timeout: 5)) + openButton.tap() + XCTAssertTrue(app.staticTexts["Test issue 1 from integration tests"].waitForExistence(timeout: 10)) + + // Issue detail — title, comments, label, milestone, assignee + let issueCell = app.staticTexts["Test issue 1 from integration tests"].firstMatch + issueCell.tap() + + let titleText = app.staticTexts["issue-detail-title"] + XCTAssertTrue(titleText.waitForExistence(timeout: 10)) + XCTAssertTrue(app.staticTexts["bug"].waitForExistence(timeout: 10)) + + // Milestone and assignee (may need scroll to see assignee section) + XCTAssertTrue(app.staticTexts["v1.0"].waitForExistence(timeout: 5), "Milestone should be visible") + app.swipeUp() + app.swipeUp() + XCTAssertTrue(app.staticTexts["@testadmin"].waitForExistence(timeout: 5), "Assignee should be visible") + XCTAssertTrue(app.staticTexts["testbot"].waitForExistence(timeout: 5)) + app.swipeDown() + app.swipeDown() + + // Back to issue list + app.navigationBars.buttons.firstMatch.tap() + XCTAssertTrue(app.staticTexts["Test issue 1 from integration tests"].waitForExistence(timeout: 10)) + + // Note: Create button is inside a ZStack within a paging TabView, + // which clips accessibility elements. Create is tested via overview. + + // Edit issue #2 title + let issue2Cell = app.staticTexts["Test issue 2 from integration tests"].firstMatch + XCTAssertTrue(issue2Cell.waitForExistence(timeout: 10)) + issue2Cell.tap() + + XCTAssertTrue(app.staticTexts["issue-detail-title"].waitForExistence(timeout: 10)) + + expandActionMenu() + let issueEditButton = app.buttons["issue-edit-button"] + XCTAssertTrue(issueEditButton.waitForExistence(timeout: 5)) + issueEditButton.tap() + + let editTitleField = app.textFields["issue-edit-title-field"] + XCTAssertTrue(editTitleField.waitForExistence(timeout: 5)) + editTitleField.tap(withNumberOfTaps: 3, numberOfTouches: 1) + editTitleField.typeText("Edited issue title") + + let saveButton = app.buttons["issue-edit-save"] + XCTAssertTrue(saveButton.waitForExistence(timeout: 5)) + saveButton.tap() + XCTAssertTrue(app.staticTexts["Edited issue title"].waitForExistence(timeout: 10)) + + // Close and reopen + expandActionMenu() + let toggleButton = app.buttons["issue-toggle-state"] + XCTAssertTrue(toggleButton.waitForExistence(timeout: 10)) + toggleButton.tap() + XCTAssertTrue(app.staticTexts["Closed"].waitForExistence(timeout: 10)) + + expandActionMenu() + let reopenButton = app.buttons["issue-toggle-state"] + XCTAssertTrue(reopenButton.waitForExistence(timeout: 5)) + reopenButton.tap() + XCTAssertTrue(app.staticTexts["Open"].waitForExistence(timeout: 10)) + } +} diff --git a/Forji/ForjiUITests/IssueUITests.swift b/Forji/ForjiUITests/IssueUITests.swift new file mode 100644 index 0000000..ee2ccd3 --- /dev/null +++ b/Forji/ForjiUITests/IssueUITests.swift @@ -0,0 +1,123 @@ +import XCTest + +final class IssueUITests: ForgejoReadOnlyUITestBase { + + override func tearDown() { + navigateBackToHome() + super.tearDown() + } + + // MARK: - Issues Overview (all issues, filter, search) + + @MainActor + func testIssuesOverview() throws { + app.tabBars.buttons["Issues"].tap() + + // Issues list loads (may show pagination test issues first on page 1) + XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 10), "Issues should load") + + // Filter All via toolbar menu + let filterMenuButton = app.buttons["filter-menu-button"] + XCTAssertTrue(filterMenuButton.waitForExistence(timeout: 5)) + filterMenuButton.tap() + app.buttons["All"].firstMatch.tap() + XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 10), "Issues should load after filter change") + + // Search narrows results to specific issue (unique keyword avoids pagination test issues) + let searchField = app.searchFields.firstMatch + XCTAssertTrue(searchField.waitForExistence(timeout: 5)) + searchField.tap() + searchField.typeText("integration") + + XCTAssertTrue(app.staticTexts["Test issue 1 from integration tests"].waitForExistence(timeout: 10)) + } + + // MARK: - Issues Overview Involvement Filter + + @MainActor + func testIssuesOverviewInvolvementFilter() throws { + app.tabBars.buttons["Issues"].tap() + + // Reset filters to defaults (previous test may have changed them) + resetOverviewFilters() + + // Wait for list to load + XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 10), "Issues should load") + + // Default filter summary should show "Open · Created by you" + let filterSummary = app.staticTexts["filter-summary"] + XCTAssertTrue(filterSummary.waitForExistence(timeout: 5)) + XCTAssertEqual(filterSummary.label, "Open · Created by you") + + let filterMenuButton = app.buttons["filter-menu-button"] + + // Tap "Assigned to you" via filter menu + filterMenuButton.tap() + app.buttons["Assigned to you"].tap() + sleep(2) + XCTAssertTrue(filterSummary.label.contains("Assigned to you")) + + // Tap "Mentioned" via filter menu + filterMenuButton.tap() + app.buttons["Mentioned"].tap() + sleep(2) + XCTAssertTrue(filterSummary.label.contains("Mentioned")) + + // Tap back to "All" scope via filter menu + filterMenuButton.tap() + // The scope "All" button — use firstMatch since "All" also appears in State section + app.buttons["All"].firstMatch.tap() + sleep(2) + + // "Review requested" should NOT be present for issues + filterMenuButton.tap() + XCTAssertFalse(app.buttons["Review requested"].exists, "Review filter should not appear for issues") + // Dismiss the menu by tapping elsewhere + app.tap() + } + + // MARK: - All State + All Scope loads items + + @MainActor + func testAllStateAllScopeLoadsIssues() throws { + app.tabBars.buttons["Issues"].tap() + + let filterSummary = app.staticTexts["filter-summary"] + let filterMenuButton = app.buttons["filter-menu-button"] + XCTAssertTrue(filterMenuButton.waitForExistence(timeout: 10)) + + // Reset to known state: Open + Created by you + filterMenuButton.tap() + app.buttons["Open"].tap() + sleep(1) + filterMenuButton.tap() + app.buttons["Created by you"].tap() + + XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 10), + "Issues should load with default Open + Created filters") + XCTAssertEqual(filterSummary.label, "Open · Created by you") + + // Step 1: Change state to All (first "All" in menu is from State section) + filterMenuButton.tap() + app.buttons["All"].firstMatch.tap() + + XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 10), + "Issues should load after state changed to All") + XCTAssertEqual(filterSummary.label, "All · Created by you") + + // Step 2: Change scope to All (second "All" in menu is from Scope section) + filterMenuButton.tap() + let allButtons = app.buttons.matching(identifier: "All") + XCTAssertGreaterThanOrEqual(allButtons.count, 2, + "Menu should have at least 2 'All' buttons (State + Scope)") + allButtons.element(boundBy: 1).tap() + + XCTAssertTrue(filterSummary.waitForExistence(timeout: 5)) + XCTAssertEqual(filterSummary.label, "All · All", + "Filter summary should show All state and All scope") + + // The key assertion: items must actually load + XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 10), + "Issues MUST load with All state + All scope (involved)") + } +} diff --git a/Forji/ForjiUITests/LoginUITests.swift b/Forji/ForjiUITests/LoginUITests.swift new file mode 100644 index 0000000..3d8b4f4 --- /dev/null +++ b/Forji/ForjiUITests/LoginUITests.swift @@ -0,0 +1,33 @@ +import XCTest + +final class LoginUITests: ForgejoUITestBase { + + // MARK: - Login Bad Credentials + + @MainActor + func testLoginBadCredentials() throws { + // Skip auto-login entirely so the instance list appears immediately, + // regardless of stored tokens/sessions from previous test classes. + app.launchArguments = [ + "-dev_serverURL", serverURL!, + "-dev_username", "testadmin", + "-dev_password", "wrongpassword", + "-dev_skipAutoLogin", "true", + ] + app.launch() + + let addButton = app.buttons["instance-add-button"] + XCTAssertTrue(addButton.waitForExistence(timeout: 15), "Instance list did not appear") + addButton.tap() + + // Scroll form sheet to reveal login button (may be below the fold) + app.swipeUp() + + let loginButton = app.buttons["login-button"] + XCTAssertTrue(loginButton.waitForExistence(timeout: 5)) + loginButton.tap() + + let alert = app.alerts["Login Failed"] + XCTAssertTrue(alert.waitForExistence(timeout: 10)) + } +} diff --git a/Forji/ForjiUITests/NotificationsUITests.swift b/Forji/ForjiUITests/NotificationsUITests.swift new file mode 100644 index 0000000..3ef71a3 --- /dev/null +++ b/Forji/ForjiUITests/NotificationsUITests.swift @@ -0,0 +1,52 @@ +import XCTest + +final class NotificationsUITests: ForgejoUITestBase { + + // MARK: - Notifications (unread, filter, swipe mark-read, swipe dismiss) + + @MainActor + func testNotifications() throws { + loginAndWaitForHome() + app.tabBars.buttons["Notifications"].tap() + + let notificationsList = app.collectionViews.firstMatch + XCTAssertTrue(notificationsList.waitForExistence(timeout: 10)) + XCTAssertTrue(app.staticTexts["testadmin/test-repo"].waitForExistence(timeout: 5)) + + // Filter by Read + let readButton = app.buttons["Read"] + XCTAssertTrue(readButton.waitForExistence(timeout: 5)) + readButton.tap() + + // Switch to All + let allButton = app.buttons["All"] + XCTAssertTrue(allButton.waitForExistence(timeout: 5)) + allButton.tap() + + // Switch back to Unread for swipe tests + let unreadButton = app.buttons["Unread"] + XCTAssertTrue(unreadButton.waitForExistence(timeout: 5)) + unreadButton.tap() + XCTAssertTrue(notificationsList.waitForExistence(timeout: 10)) + + // Swipe right to mark as read + let firstCell = notificationsList.cells.firstMatch + XCTAssertTrue(firstCell.waitForExistence(timeout: 5)) + firstCell.swipeRight() + + let markReadButton = app.buttons["Mark Read"] + if markReadButton.waitForExistence(timeout: 3) { + markReadButton.tap() + } + + // Swipe left to dismiss + let nextCell = notificationsList.cells.firstMatch + if nextCell.waitForExistence(timeout: 5) { + nextCell.swipeLeft() + let dismissButton = app.buttons["Dismiss"] + if dismissButton.waitForExistence(timeout: 3) { + dismissButton.tap() + } + } + } +} diff --git a/Forji/ForjiUITests/OverviewCreateMutatingUITests.swift b/Forji/ForjiUITests/OverviewCreateMutatingUITests.swift new file mode 100644 index 0000000..a5200e1 --- /dev/null +++ b/Forji/ForjiUITests/OverviewCreateMutatingUITests.swift @@ -0,0 +1,49 @@ +import XCTest + +final class OverviewCreateMutatingUITests: ForgejoUITestBase { + + // MARK: - Create Issue from Issues Overview (mutates — creates an issue) + + @MainActor + func testCreateIssueFromOverview() throws { + loginAndWaitForHome() + app.tabBars.buttons["Issues"].tap() + + // Wait for the issues list to load + XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 10), "Issues should load") + + // Tap floating create button + let createButton = app.buttons["issue-create-button"] + XCTAssertTrue(createButton.waitForExistence(timeout: 10), "Floating create button should exist on Issues overview") + createButton.tap() + + // Repository picker sheet should appear + XCTAssertTrue(app.staticTexts["Select Repository"].waitForExistence(timeout: 10), "Repository picker should appear") + + // Select test-repo via accessibility identifier on the button + let repoButton = app.buttons["repo-picker-testadmin/test-repo"] + XCTAssertTrue(repoButton.waitForExistence(timeout: 10), "test-repo should appear in the picker") + repoButton.tap() + + // Issue create form should appear after selecting a repo + XCTAssertTrue(app.staticTexts["New Issue"].waitForExistence(timeout: 15), "Issue create form should appear after selecting a repo") + + // Fill in the title + let titleField = app.textFields["issue-create-title-field"] + XCTAssertTrue(titleField.waitForExistence(timeout: 5)) + titleField.tap() + titleField.typeText("Issue created from overview") + + // Submit + let submitButton = app.buttons["issue-create-submit"] + XCTAssertTrue(submitButton.waitForExistence(timeout: 5)) + submitButton.tap() + + // After creation, the sheet dismisses and the issues list should refresh + // showing the newly created issue + XCTAssertTrue( + app.staticTexts["Issue created from overview"].waitForExistence(timeout: 15), + "Newly created issue should appear in the overview list" + ) + } +} diff --git a/Forji/ForjiUITests/OverviewCreateUITests.swift b/Forji/ForjiUITests/OverviewCreateUITests.swift new file mode 100644 index 0000000..828774b --- /dev/null +++ b/Forji/ForjiUITests/OverviewCreateUITests.swift @@ -0,0 +1,75 @@ +import XCTest + +final class OverviewCreateUITests: ForgejoReadOnlyUITestBase { + + override func tearDown() { + navigateBackToHome() + super.tearDown() + } + + // MARK: - Create PR from Pull Requests Overview + + @MainActor + func testCreatePRFromOverview() throws { + app.tabBars.buttons["Pull Requests"].tap() + + // Test PRs were created by testbot — switch scope to "All" to see them + setOverviewScopeAll() + + // Wait for the PR list to load + XCTAssertTrue(app.staticTexts["Add feature file"].waitForExistence(timeout: 10)) + + // Tap floating create button + let createButton = app.buttons["pr-create-button"] + XCTAssertTrue(createButton.waitForExistence(timeout: 10), "Floating create button should exist on Pull Requests overview") + createButton.tap() + + // Repository picker sheet should appear + XCTAssertTrue(app.staticTexts["Select Repository"].waitForExistence(timeout: 10), "Repository picker should appear") + + // Select test-repo via accessibility identifier on the button + let repoButton = app.buttons["repo-picker-testadmin/test-repo"] + XCTAssertTrue(repoButton.waitForExistence(timeout: 10), "test-repo should appear in the picker") + repoButton.tap() + + // PR create form should appear with branch pickers after selecting a repo + XCTAssertTrue(app.staticTexts["New Pull Request"].waitForExistence(timeout: 15), "PR create form should appear after selecting a repo") + XCTAssertTrue(app.staticTexts["Branches"].waitForExistence(timeout: 10), "Branch picker section should be visible") + + // Cancel and verify we return to the overview + let cancelButton = app.buttons["Cancel"] + XCTAssertTrue(cancelButton.waitForExistence(timeout: 5)) + cancelButton.tap() + + XCTAssertTrue( + app.staticTexts["Add feature file"].waitForExistence(timeout: 10), + "Should return to PR overview after cancelling" + ) + } + + // MARK: - Cancel Repository Picker + + @MainActor + func testCancelRepoPicker() throws { + app.tabBars.buttons["Issues"].tap() + + // Wait for the floating create button (confirms the overview rendered) + let createButton = app.buttons["issue-create-button"] + XCTAssertTrue(createButton.waitForExistence(timeout: 15), "Floating create button should exist on Issues overview") + createButton.tap() + + // Repository picker sheet should appear + XCTAssertTrue(app.staticTexts["Select Repository"].waitForExistence(timeout: 10)) + + // Cancel + let cancelButton = app.buttons["Cancel"] + XCTAssertTrue(cancelButton.waitForExistence(timeout: 5)) + cancelButton.tap() + + // Should return to the issues overview without opening the create form + XCTAssertTrue( + createButton.waitForExistence(timeout: 10), + "Should return to issues overview after cancelling repo picker" + ) + } +} diff --git a/Forji/ForjiUITests/PaginationUITests.swift b/Forji/ForjiUITests/PaginationUITests.swift new file mode 100644 index 0000000..9f4bae5 --- /dev/null +++ b/Forji/ForjiUITests/PaginationUITests.swift @@ -0,0 +1,95 @@ +import XCTest + +final class PaginationUITests: ForgejoReadOnlyUITestBase { + + override func tearDown() { + navigateBackToHome() + super.tearDown() + } + + // MARK: - Issue List Pagination + + @MainActor + func testIssueListPaginationLoadsAllItems() throws { + navigateToRepoDetail("test-repo-2") + app.buttons["Issues"].firstMatch.tap() + + // Wait for page 1 issues to load (25 issues in test-repo-2, page size 20) + XCTAssertTrue(app.staticTexts["Pagination test issue 25"].waitForExistence(timeout: 10), + "Should show recent issues on page 1") + + // Scroll to bottom to trigger page 2 auto-load + for _ in 0..<10 { + app.swipeUp() + } + + // Verify older issues from page 2 have loaded + let olderIssue = app.staticTexts["Pagination test issue 1"] + var found = false + for _ in 0..<5 { + if olderIssue.exists { + found = true + break + } + app.swipeUp() + // Wait for element to appear instead of fixed sleep + if olderIssue.waitForExistence(timeout: 2) { + found = true + break + } + } + XCTAssertTrue(found, "Page 2 issues should have loaded after scrolling to bottom") + } + + @MainActor + func testIssueListFilterResetsPagination() throws { + navigateToIssueTab() + + // Wait for open issues to load (test-repo has only 2 open issues) + XCTAssertTrue(app.staticTexts["Test issue 1 from integration tests"].waitForExistence(timeout: 10)) + + // Switch to Closed filter + let closedButton = app.buttons["Closed"] + XCTAssertTrue(closedButton.waitForExistence(timeout: 5)) + closedButton.tap() + + // Only 1 closed issue (#3) — should not show load-more + XCTAssertTrue(app.staticTexts["Test issue 3 from integration tests"].waitForExistence(timeout: 10)) + + let loadMore = app.activityIndicators["load-more-indicator"] + // Wait briefly for UI to settle, then check absence + _ = loadMore.waitForExistence(timeout: 2) + XCTAssertFalse(loadMore.exists, "load-more should not exist with <20 closed issues") + } + + // MARK: - Pull Request List (few items) + + @MainActor + func testPullRequestListNoLoadMoreWhenFewItems() throws { + navigateToPRTab() + + // Only 2 PRs exist + XCTAssertTrue(app.staticTexts["Add feature file"].waitForExistence(timeout: 10)) + XCTAssertTrue(app.staticTexts["Merge test PR"].waitForExistence(timeout: 5)) + + let loadMore = app.activityIndicators["load-more-indicator"] + _ = loadMore.waitForExistence(timeout: 2) + XCTAssertFalse(loadMore.exists, "load-more should not exist when there are only 2 PRs") + } + + // MARK: - Notifications (few items) + + @MainActor + func testNotificationsLoadWithoutLoadMore() throws { + app.tabBars.buttons["Notifications"].tap() + + // Wait for notifications to load + let firstNotification = app.staticTexts["Test issue 1 from integration tests"] + XCTAssertTrue(firstNotification.waitForExistence(timeout: 15), + "Should show at least one notification") + + let loadMore = app.activityIndicators["load-more-indicator"] + _ = loadMore.waitForExistence(timeout: 2) + XCTAssertFalse(loadMore.exists, "load-more should not exist with few notifications") + } +} diff --git a/Forji/ForjiUITests/PermissionUITests.swift b/Forji/ForjiUITests/PermissionUITests.swift new file mode 100644 index 0000000..63c753e --- /dev/null +++ b/Forji/ForjiUITests/PermissionUITests.swift @@ -0,0 +1,80 @@ +import XCTest + +final class PermissionUITests: ForgejoUITestBase { + + override func setUpWithError() throws { + try super.setUpWithError() + + // Override credentials to use the read-only user + app = XCUIApplication() + app.launchArguments += [ + "-dev_serverURL", serverURL, + "-dev_username", "readonlyuser", + "-dev_password", "readonly1234", + ] + } + + // MARK: - Issue Detail — read-only user + + @MainActor + func testIssueDetailHidesActionsForReadOnlyUser() throws { + loginAndWaitForHome() + + navigateToIssueTab() + + let issueCell = app.staticTexts["Test issue 1 from integration tests"].firstMatch + XCTAssertTrue(issueCell.waitForExistence(timeout: 10)) + issueCell.tap() + + let title = app.staticTexts["issue-detail-title"] + XCTAssertTrue(title.waitForExistence(timeout: 10)) + + expandActionMenu() + + // Comment button should always be visible + let commentButton = app.buttons["issue-comment-button"] + XCTAssertTrue(commentButton.waitForExistence(timeout: 5), "Comment button should be visible for read-only user") + + // Edit and Close/Reopen should be hidden + let editButton = app.buttons["issue-edit-button"] + XCTAssertFalse(editButton.exists, "Edit button should be hidden for read-only user") + + let toggleStateButton = app.buttons["issue-toggle-state"] + XCTAssertFalse(toggleStateButton.exists, "Close/Reopen button should be hidden for read-only user") + } + + // MARK: - PR Detail — read-only user + + @MainActor + func testPRDetailHidesActionsForReadOnlyUser() throws { + loginAndWaitForHome() + + navigateToPRTab() + + let prCell = app.staticTexts["Add feature file"].firstMatch + XCTAssertTrue(prCell.waitForExistence(timeout: 10)) + prCell.tap() + + let title = app.staticTexts["pr-detail-title"] + XCTAssertTrue(title.waitForExistence(timeout: 10)) + + expandActionMenu() + + // Comment and Review should always be visible + let commentButton = app.buttons["pr-comment-button"] + XCTAssertTrue(commentButton.waitForExistence(timeout: 5), "Comment button should be visible for read-only user") + + let reviewButton = app.buttons["pr-submit-review"] + XCTAssertTrue(reviewButton.exists, "Review button should be visible for read-only user") + + // Edit, Merge, Close/Reopen should be hidden + let editButton = app.buttons["pr-edit-button"] + XCTAssertFalse(editButton.exists, "Edit button should be hidden for read-only user") + + let mergeButton = app.buttons["pr-merge-button"] + XCTAssertFalse(mergeButton.exists, "Merge button should be hidden for read-only user") + + let toggleStateButton = app.buttons["pr-toggle-state"] + XCTAssertFalse(toggleStateButton.exists, "Close/Reopen button should be hidden for read-only user") + } +} diff --git a/Forji/ForjiUITests/PullRequestMutatingUITests.swift b/Forji/ForjiUITests/PullRequestMutatingUITests.swift new file mode 100644 index 0000000..c2f97ea --- /dev/null +++ b/Forji/ForjiUITests/PullRequestMutatingUITests.swift @@ -0,0 +1,139 @@ +import XCTest + +final class PullRequestMutatingUITests: ForgejoUITestBase { + + // MARK: - PR Tab Actions (add comment, edit sheet, submit review, close/reopen) + + @MainActor + func testPRTabActions() throws { + loginAndWaitForHome() + navigateToPRTab() + + let prCell = app.staticTexts["Add feature file"].firstMatch + XCTAssertTrue(prCell.waitForExistence(timeout: 10)) + prCell.tap() + + XCTAssertTrue(app.staticTexts["pr-detail-title"].waitForExistence(timeout: 10)) + + // Add comment via sheet + expandActionMenu() + let commentButton = app.buttons["pr-comment-button"] + XCTAssertTrue(commentButton.waitForExistence(timeout: 5)) + commentButton.tap() + + XCTAssertTrue(app.staticTexts["New Comment"].waitForExistence(timeout: 5)) + + let commentEditor = app.textViews["markdown-text-editor"].firstMatch + XCTAssertTrue(commentEditor.waitForExistence(timeout: 5)) + commentEditor.tap() + commentEditor.typeText("UI test comment on PR") + + let submitButton = app.buttons["Submit"] + XCTAssertTrue(submitButton.waitForExistence(timeout: 5)) + submitButton.tap() + + // Wait for sheet to dismiss and scroll to bottom to find new comment + XCTAssertTrue(app.staticTexts["pr-detail-title"].waitForExistence(timeout: 10)) + app.swipeUp() + app.swipeUp() + XCTAssertTrue(app.staticTexts["UI test comment on PR"].waitForExistence(timeout: 10)) + + // Scroll back up + app.swipeDown() + app.swipeDown() + + // Edit sheet + expandActionMenu() + let editButton = app.buttons["pr-edit-button"] + XCTAssertTrue(editButton.waitForExistence(timeout: 5)) + editButton.tap() + + let titleField = app.textFields["pr-edit-title-field"] + XCTAssertTrue(titleField.waitForExistence(timeout: 5)) + app.buttons["Cancel"].tap() + XCTAssertTrue(app.staticTexts["pr-detail-title"].waitForExistence(timeout: 10)) + + // Submit review sheet + app.swipeUp() + + expandActionMenu() + let reviewButton = app.buttons["pr-submit-review"] + XCTAssertTrue(reviewButton.waitForExistence(timeout: 10)) + reviewButton.tap() + + XCTAssertTrue(app.staticTexts["Submit Review"].waitForExistence(timeout: 5)) + XCTAssertTrue(app.staticTexts["Approve"].waitForExistence(timeout: 5)) + app.buttons["Cancel"].tap() + + // Close and reopen + expandActionMenu() + let closeButton = app.buttons["pr-toggle-state"] + XCTAssertTrue(closeButton.waitForExistence(timeout: 10)) + closeButton.tap() + + app.swipeDown() + app.swipeDown() + XCTAssertTrue(app.staticTexts["Closed"].waitForExistence(timeout: 10)) + + app.swipeUp() + expandActionMenu() + let reopenButton = app.buttons["pr-toggle-state"] + XCTAssertTrue(reopenButton.waitForExistence(timeout: 5)) + reopenButton.tap() + + app.swipeDown() + app.swipeDown() + XCTAssertTrue(app.staticTexts["Open"].waitForExistence(timeout: 10)) + } + + // MARK: - PR Merge (separate — mutates PR state) + + @MainActor + func testPRMerge() throws { + loginAndWaitForHome() + navigateToPRTab() + + let prCell = app.staticTexts["Merge test PR"].firstMatch + XCTAssertTrue(prCell.waitForExistence(timeout: 10)) + prCell.tap() + + XCTAssertTrue(app.staticTexts["pr-detail-title"].waitForExistence(timeout: 10)) + + app.swipeUp() + + expandActionMenu() + let mergeButton = app.buttons["pr-merge-button"] + XCTAssertTrue(mergeButton.waitForExistence(timeout: 10)) + mergeButton.tap() + + XCTAssertTrue(app.staticTexts["Merge Pull Request"].waitForExistence(timeout: 5)) + + let methodPicker = app.segmentedControls["merge-method-picker"] + XCTAssertTrue(methodPicker.waitForExistence(timeout: 5)) + + let deleteBranch = app.switches["merge-delete-branch"] + XCTAssertTrue(deleteBranch.waitForExistence(timeout: 5)) + + let mergeConfirm = app.buttons["merge-confirm"] + XCTAssertTrue(mergeConfirm.waitForExistence(timeout: 5)) + mergeConfirm.tap() + + // Check if the merge sheet was dismissed (success) or still showing (error) + let mergeSheetTitle = app.staticTexts["Merge Pull Request"] + if mergeSheetTitle.waitForExistence(timeout: 5) { + let errorAlert = app.alerts.firstMatch + if errorAlert.exists { + let alertText = errorAlert.staticTexts.allElementsBoundByIndex.map { $0.label }.joined(separator: " | ") + XCTFail("Merge failed with error alert: \(alertText)") + } else { + XCTFail("Merge sheet still visible but no error alert — merge may be in progress") + } + return + } + + XCTAssertTrue( + app.staticTexts["pr-detail-title"].waitForExistence(timeout: 10), + "Should return to PR detail after successful merge" + ) + } +} diff --git a/Forji/ForjiUITests/PullRequestUITests.swift b/Forji/ForjiUITests/PullRequestUITests.swift new file mode 100644 index 0000000..622b030 --- /dev/null +++ b/Forji/ForjiUITests/PullRequestUITests.swift @@ -0,0 +1,153 @@ +import XCTest + +final class PullRequestUITests: ForgejoReadOnlyUITestBase { + + override func tearDown() { + navigateBackToHome() + super.tearDown() + } + + // MARK: - PR Tab Content (list, filter, detail, review, diff, comment) + + @MainActor + func testPRTabContent() throws { + navigateToPRTab() + + // PR list shows open PRs + XCTAssertTrue(app.staticTexts["Add feature file"].waitForExistence(timeout: 10)) + XCTAssertTrue(app.staticTexts["Merge test PR"].waitForExistence(timeout: 5)) + + // Filter by Closed + let closedButton = app.buttons["Closed"] + XCTAssertTrue(closedButton.waitForExistence(timeout: 10)) + closedButton.tap() + XCTAssertTrue(app.staticTexts["No Pull Requests"].waitForExistence(timeout: 10)) + + // Back to Open + let openButton = app.buttons["Open"] + XCTAssertTrue(openButton.waitForExistence(timeout: 5)) + openButton.tap() + XCTAssertTrue(app.staticTexts["Add feature file"].waitForExistence(timeout: 10)) + + // PR detail — title, branches + let prCell = app.staticTexts["Add feature file"].firstMatch + prCell.tap() + + let titleText = app.staticTexts["pr-detail-title"] + XCTAssertTrue(titleText.waitForExistence(timeout: 10)) + XCTAssertTrue(app.staticTexts["feature-branch"].waitForExistence(timeout: 5)) + XCTAssertTrue(app.staticTexts["main"].waitForExistence(timeout: 5)) + + // Milestone and assignee (adjacent sections, both near top of list) + XCTAssertTrue(app.staticTexts["v1.0"].waitForExistence(timeout: 5), "PR milestone should be visible") + XCTAssertTrue(app.staticTexts["@testadmin"].waitForExistence(timeout: 10), "PR assignee should be visible") + + // Review (may need scroll to reach review section) + app.swipeUp() + XCTAssertTrue(app.staticTexts["commented"].waitForExistence(timeout: 15)) + + // Diff shows feature.txt (scroll down past description and reviews) + app.swipeUp() + XCTAssertTrue(app.staticTexts["feature.txt"].waitForExistence(timeout: 10)) + + // Scroll down to see comment from testbot + app.swipeUp() + XCTAssertTrue(app.staticTexts["testbot"].waitForExistence(timeout: 10)) + } + + // MARK: - Diff View — context line has no comment button + + @MainActor + func testDiffViewContextLineHasNoCommentButton() throws { + navigateToPRTab() + + let prCell = app.staticTexts["Add feature file"].firstMatch + XCTAssertTrue(prCell.waitForExistence(timeout: 10)) + prCell.tap() + + XCTAssertTrue(app.staticTexts["pr-detail-title"].waitForExistence(timeout: 10)) + + // Open review sheet to see the diff with inline comment support + app.swipeUp() + + expandActionMenu() + let reviewButton = app.buttons["pr-submit-review"] + XCTAssertTrue(reviewButton.waitForExistence(timeout: 10)) + reviewButton.tap() + + XCTAssertTrue(app.staticTexts["Submit Review"].waitForExistence(timeout: 5)) + + // The diff should show feature.txt + XCTAssertTrue(app.staticTexts["feature.txt"].waitForExistence(timeout: 10)) + + // Dismiss + app.buttons["Cancel"].tap() + } + + // MARK: - Pull Requests Overview (all PRs, filter, search) + + @MainActor + func testPullRequestsOverview() throws { + app.tabBars.buttons["Pull Requests"].tap() + + // Test PRs were created by testbot — switch scope to "All" to see them + setOverviewScopeAll() + + XCTAssertTrue(app.staticTexts["Add feature file"].waitForExistence(timeout: 10)) + + // Filter state to All via toolbar menu + let filterMenuButton = app.buttons["filter-menu-button"] + XCTAssertTrue(filterMenuButton.waitForExistence(timeout: 5)) + filterMenuButton.tap() + app.buttons["All"].firstMatch.tap() + + // Search + let searchField = app.searchFields.firstMatch + XCTAssertTrue(searchField.waitForExistence(timeout: 5)) + searchField.tap() + searchField.typeText("feature") + + XCTAssertTrue(app.staticTexts["Add feature file"].waitForExistence(timeout: 10)) + } + + // MARK: - Pull Requests Overview Involvement Filter + + @MainActor + func testPullRequestsOverviewInvolvementFilter() throws { + app.tabBars.buttons["Pull Requests"].tap() + + // Reset filters to defaults (previous test may have changed them) + resetOverviewFilters() + + // Default filter summary should show "Open · Created by you" + let filterSummary = app.staticTexts["filter-summary"] + XCTAssertTrue(filterSummary.waitForExistence(timeout: 5)) + XCTAssertEqual(filterSummary.label, "Open · Created by you") + + let filterMenuButton = app.buttons["filter-menu-button"] + + // Tap "Assigned to you" via filter menu + filterMenuButton.tap() + app.buttons["Assigned to you"].tap() + sleep(2) + XCTAssertTrue(filterSummary.label.contains("Assigned to you")) + + // Tap "Mentioned" via filter menu + filterMenuButton.tap() + app.buttons["Mentioned"].tap() + sleep(2) + XCTAssertTrue(filterSummary.label.contains("Mentioned")) + + // Tap back to "All" scope via filter menu + filterMenuButton.tap() + app.buttons["All"].firstMatch.tap() + sleep(2) + + // "Review requested" SHOULD be present for PRs + filterMenuButton.tap() + let reviewButton = app.buttons["Review requested"] + XCTAssertTrue(reviewButton.exists, "Review filter should appear for pull requests") + // Dismiss the menu by tapping elsewhere + app.tap() + } +} diff --git a/Forji/ForjiUITests/RepositoryMutatingUITests.swift b/Forji/ForjiUITests/RepositoryMutatingUITests.swift new file mode 100644 index 0000000..cf0d462 --- /dev/null +++ b/Forji/ForjiUITests/RepositoryMutatingUITests.swift @@ -0,0 +1,73 @@ +import XCTest + +final class RepositoryMutatingUITests: ForgejoUITestBase { + + // MARK: - Code Browser & File Viewer (mutates repo via file edit + commit) + + @MainActor + func testCodeBrowserAndFileViewer() throws { + loginAndWaitForHome() + navigateToRepoDetail() + + // Code tab shows files + XCTAssertTrue(app.staticTexts["README.md"].waitForExistence(timeout: 10)) + XCTAssertTrue(app.staticTexts["hello.py"].waitForExistence(timeout: 5)) + + // Navigate into src directory + let srcFolder = app.staticTexts["src"].firstMatch + XCTAssertTrue(srcFolder.waitForExistence(timeout: 10)) + srcFolder.tap() + + XCTAssertTrue(app.staticTexts["main.py"].waitForExistence(timeout: 10)) + XCTAssertTrue(app.buttons["test-repo"].waitForExistence(timeout: 5)) + + // Breadcrumb back to root + let rootBreadcrumb = app.buttons["test-repo"] + rootBreadcrumb.tap() + XCTAssertTrue(app.staticTexts["hello.py"].waitForExistence(timeout: 10)) + XCTAssertTrue(app.staticTexts["README.md"].waitForExistence(timeout: 5)) + + // Scroll toggle button + let toggleButton = app.buttons["code-scroll-toggle"] + XCTAssertTrue(toggleButton.waitForExistence(timeout: 5)) + toggleButton.tap() + toggleButton.tap() + + // File viewer — tap hello.py + let fileCell = app.staticTexts["hello.py"].firstMatch + XCTAssertTrue(fileCell.waitForExistence(timeout: 10)) + fileCell.tap() + + let editButton = app.buttons["file-edit-button"] + XCTAssertTrue(editButton.waitForExistence(timeout: 10)) + + // Enter edit mode + editButton.tap() + + let cancelButton = app.buttons["file-edit-cancel"] + XCTAssertTrue(cancelButton.waitForExistence(timeout: 5)) + cancelButton.tap() + XCTAssertTrue(editButton.waitForExistence(timeout: 5)) + + // Edit and commit flow + editButton.tap() + + let textEditor = app.textViews.firstMatch + XCTAssertTrue(textEditor.waitForExistence(timeout: 5)) + textEditor.tap() + textEditor.typeText("\n# UI test edit") + + let commitButton = app.buttons["file-edit-commit"] + XCTAssertTrue(commitButton.waitForExistence(timeout: 5)) + commitButton.tap() + + let commitMessageField = app.textFields["commit-message-field"] + XCTAssertTrue(commitMessageField.waitForExistence(timeout: 5)) + + let commitSubmit = app.buttons["commit-submit"] + XCTAssertTrue(commitSubmit.waitForExistence(timeout: 5)) + commitSubmit.tap() + + XCTAssertTrue(editButton.waitForExistence(timeout: 10)) + } +} diff --git a/Forji/ForjiUITests/RepositoryUITests.swift b/Forji/ForjiUITests/RepositoryUITests.swift new file mode 100644 index 0000000..455bb98 --- /dev/null +++ b/Forji/ForjiUITests/RepositoryUITests.swift @@ -0,0 +1,86 @@ +import XCTest + +final class RepositoryUITests: ForgejoReadOnlyUITestBase { + + // MARK: - Repository List (repos, star, filter, search) + + @MainActor + func testRepositoryList() throws { + app.tabBars.buttons["Repositories"].tap() + + let repoList = app.collectionViews["repo-list"] + XCTAssertTrue(repoList.waitForExistence(timeout: 10)) + + // Both repos visible + XCTAssertTrue(app.staticTexts["test-repo"].waitForExistence(timeout: 5)) + XCTAssertTrue(app.staticTexts["test-repo-2"].waitForExistence(timeout: 5)) + + // Filter by Starred + let starredButton = app.buttons["Starred"] + XCTAssertTrue(starredButton.waitForExistence(timeout: 5)) + starredButton.tap() + + // Switch back to All + let allButton = app.buttons["All"] + XCTAssertTrue(allButton.waitForExistence(timeout: 5)) + allButton.tap() + XCTAssertTrue(app.staticTexts["test-repo"].waitForExistence(timeout: 10)) + + // Search + let searchField = app.searchFields.firstMatch + XCTAssertTrue(searchField.waitForExistence(timeout: 5)) + searchField.tap() + searchField.typeText("test-repo-2") + + XCTAssertTrue(app.staticTexts["test-repo-2"].waitForExistence(timeout: 10)) + + // Clear search to restore full list + searchField.buttons["Clear text"].tap() + XCTAssertTrue(app.staticTexts["test-repo"].waitForExistence(timeout: 10)) + + // Star toggle (last — tap triggers NavigationLink, only tests tappability) + let starButton = repoList.buttons["star-button"].firstMatch + XCTAssertTrue(starButton.waitForExistence(timeout: 5), "No star button found in repo list") + starButton.tap() + } + + override func tearDown() { + navigateBackToHome() + super.tearDown() + } + + // MARK: - Branch Selector + + @MainActor + func testBranchSelector() throws { + navigateToRepoDetail() + + // Branch selector should exist and show "main" by default + let branchSelector = app.buttons["branch-selector"] + XCTAssertTrue(branchSelector.waitForExistence(timeout: 10), "Branch selector not found") + XCTAssertTrue(branchSelector.label.contains("main"), "Branch selector should show 'main' initially") + + // Verify main branch files are visible + XCTAssertTrue(app.staticTexts["hello.py"].waitForExistence(timeout: 10)) + + // Switch to feature-branch via branch picker sheet + branchSelector.tap() + let featureBranchOption = app.buttons["branch-option-feature-branch"] + XCTAssertTrue(featureBranchOption.waitForExistence(timeout: 5), "feature-branch option not found") + featureBranchOption.tap() + + // feature.txt should appear (only exists on feature-branch) + XCTAssertTrue(app.staticTexts["feature.txt"].waitForExistence(timeout: 10), "feature.txt should be visible on feature-branch") + + // Switch back to main + let updatedSelector = app.buttons["branch-selector"] + XCTAssertTrue(updatedSelector.waitForExistence(timeout: 5)) + updatedSelector.tap() + let mainBranchOption = app.buttons["branch-option-main"] + XCTAssertTrue(mainBranchOption.waitForExistence(timeout: 5), "main option not found") + mainBranchOption.tap() + + // feature.txt should no longer be visible, hello.py should be back + XCTAssertTrue(app.staticTexts["hello.py"].waitForExistence(timeout: 10), "hello.py should be visible on main") + } +} diff --git a/Forji/ForjiUITests/UITestNavigating.swift b/Forji/ForjiUITests/UITestNavigating.swift new file mode 100644 index 0000000..b159d1d --- /dev/null +++ b/Forji/ForjiUITests/UITestNavigating.swift @@ -0,0 +1,66 @@ +import XCTest + +/// Shared navigation helpers for UI test base classes. +/// Each conforming class provides its own `app` and `navigateToRepoDetail` +/// implementation; the protocol extension supplies the rest. +protocol UITestNavigating { + var app: XCUIApplication! { get } + func navigateToRepoDetail(_ repoName: String) +} + +extension UITestNavigating { + + func navigateToRepoDetail() { + navigateToRepoDetail("test-repo") + } + + func navigateToIssueTab() { + navigateToRepoDetail() + let picker = app.segmentedControls["repo-detail-tab-picker"] + XCTAssertTrue(picker.waitForExistence(timeout: 10)) + picker.buttons["Issues"].tap() + } + + func navigateToPRTab() { + navigateToRepoDetail() + let picker = app.segmentedControls["repo-detail-tab-picker"] + XCTAssertTrue(picker.waitForExistence(timeout: 10)) + picker.buttons["Pull Requests"].tap() + } + + /// Resets the overview filter to Open + Created by you (the app defaults). + func resetOverviewFilters() { + let filterMenuButton = app.buttons["filter-menu-button"] + guard filterMenuButton.waitForExistence(timeout: 5) else { return } + filterMenuButton.tap() + app.buttons["Open"].tap() + sleep(1) + filterMenuButton.tap() + app.buttons["Created by you"].tap() + sleep(1) + } + + /// Switches the overview scope to "All" to show items from all users. + /// The Scope "All" is the second "All" button in the filter menu + /// (the first is from the State section). + func setOverviewScopeAll() { + let filterMenuButton = app.buttons["filter-menu-button"] + guard filterMenuButton.waitForExistence(timeout: 5) else { return } + filterMenuButton.tap() + let allButtons = app.buttons.matching(identifier: "All") + if allButtons.count >= 2 { + allButtons.element(boundBy: 1).tap() + } else { + allButtons.firstMatch.tap() + } + sleep(1) + } + + func expandActionMenu() { + let toggle = app.buttons["action-menu-toggle"] + XCTAssertTrue(toggle.waitForExistence(timeout: 10), "Action menu toggle not found") + if toggle.value as? String != "expanded" { + toggle.tap() + } + } +} diff --git a/Forji/Info.plist b/Forji/Info.plist new file mode 100644 index 0000000..6a6654d --- /dev/null +++ b/Forji/Info.plist @@ -0,0 +1,11 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..59bd55d --- /dev/null +++ b/LICENSE @@ -0,0 +1,716 @@ +Forji — A native iOS/macOS client for Forgejo +Copyright (C) 2026 Stefan Hausotte + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + + +## App Store Exception + +As a special exception to the terms of the GNU General Public License +version 3, you are granted additional permission to convey the compiled +application through Apple's App Store, under Apple's standard App Store +terms of service, provided that: + +1. The complete Corresponding Source of the application remains freely + available under the terms of the GNU General Public License version 3 + (or later) at the project's public repository. +2. This exception applies solely to distribution through Apple's App + Store and does not modify the terms of the GNU General Public License + in any other respect. +3. All other rights and obligations under the GNU General Public License + version 3 remain in full effect. + +This exception is granted under Section 7 of the GNU General Public +License version 3, which permits additional permissions to be added to +covered works. + +By contributing to this project, you agree that your contributions may +be distributed through Apple's App Store under this exception. + + +# GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +## Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom +to share and change all versions of a program--to make sure it remains +free software for all its users. We, the Free Software Foundation, use +the GNU General Public License for most of our software; it applies +also to any other work released this way by its authors. You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you +have certain responsibilities if you distribute copies of the +software, or if you modify it: responsibilities to respect the freedom +of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the +manufacturer can do so. This is fundamentally incompatible with the +aim of protecting users' freedom to change the software. The +systematic pattern of such abuse occurs in the area of products for +individuals to use, which is precisely where it is most unacceptable. +Therefore, we have designed this version of the GPL to prohibit the +practice for those products. If such problems arise substantially in +other domains, we stand ready to extend this provision to those +domains in future versions of the GPL, as needed to protect the +freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish +to avoid the special danger that patents applied to a free program +could make it effectively proprietary. To prevent this, the GPL +assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + +## TERMS AND CONDITIONS + +### 0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds +of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of +an exact copy. The resulting work is called a "modified version" of +the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user +through a computer network, with no transfer of a copy, is not +conveying. + +An interactive user interface displays "Appropriate Legal Notices" to +the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code. + +The "source code" for a work means the preferred form of the work for +making modifications to it. "Object code" means any non-source form of +a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can +regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same +work. + +### 2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, +without conditions so long as your license otherwise remains in force. +You may convey covered works to others for the sole purpose of having +them make modifications exclusively for you, or provide you with +facilities for running those works, provided that you comply with the +terms of this License in conveying all material for which you do not +control copyright. Those thus making or running the covered works for +you must do so exclusively on your behalf, under your direction and +control, on terms that prohibit them from making any copies of your +copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes +it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such +circumvention is effected by exercising rights under this License with +respect to the covered work, and you disclaim any intention to limit +operation or modification of the work as a means of enforcing, against +the work's users, your or third parties' legal rights to forbid +circumvention of technological measures. + +### 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +### 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these +conditions: + +- a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. +- b) The work must carry prominent notices stating that it is + released under this License and any conditions added under + section 7. This requirement modifies the requirement in section 4 + to "keep intact all notices". +- c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. +- d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +### 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of +sections 4 and 5, provided that you also convey the machine-readable +Corresponding Source under the terms of this License, in one of these +ways: + +- a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. +- b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the Corresponding + Source from a network server at no charge. +- c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. +- d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. +- e) Convey the object code using peer-to-peer transmission, + provided you inform other peers where the object code and + Corresponding Source of the work are being offered to the general + public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, +family, or household purposes, or (2) anything designed or sold for +incorporation into a dwelling. In determining whether a product is a +consumer product, doubtful cases shall be resolved in favor of +coverage. For a particular product received by a particular user, +"normally used" refers to a typical or common use of that class of +product, regardless of the status of the particular user or of the way +in which the particular user actually uses, or expects or is expected +to use, the product. A product is a consumer product regardless of +whether the product has substantial commercial, industrial or +non-consumer uses, unless such uses represent the only significant +mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to +install and execute modified versions of a covered work in that User +Product from a modified version of its Corresponding Source. The +information must suffice to ensure that the continued functioning of +the modified object code is in no case prevented or interfered with +solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or +updates for a work that has been modified or installed by the +recipient, or for the User Product in which it has been modified or +installed. Access to a network may be denied when the modification +itself materially and adversely affects the operation of the network +or violates the rules and protocols for communication across the +network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +### 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders +of that material) supplement the terms of this License with terms: + +- a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or +- b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or +- c) Prohibiting misrepresentation of the origin of that material, + or requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or +- d) Limiting the use for publicity purposes of names of licensors + or authors of the material; or +- e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or +- f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions + of it) with contractual assumptions of liability to the recipient, + for any liability that these contractual assumptions directly + impose on those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; the +above requirements apply either way. + +### 8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally +terminates your license, and (b) permanently, if the copyright holder +fails to notify you of the violation by some reasonable means prior to +60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +### 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run +a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +### 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +### 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned +or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on +the non-exercise of one or more of the rights that are specifically +granted under this License. You may not convey a covered work if you +are a party to an arrangement with a third party that is in the +business of distributing software, under which you make payment to the +third party based on the extent of your activity of conveying the +work, and under which the third party grants, to any of the parties +who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by +you (or copies made from those copies), or (b) primarily for and in +connection with specific products or compilations that contain the +covered work, unless you entered into that arrangement, or that patent +license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +### 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under +this License and any other pertinent obligations, then as a +consequence you may not convey it at all. For example, if you agree to +terms that obligate you to collect a royalty for further conveying +from those to whom you convey the Program, the only way you could +satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +### 13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +### 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions +of the GNU General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in +detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies that a certain numbered version of the GNU General Public +License "or any later version" applies to it, you have the option of +following the terms and conditions either of that numbered version or +of any later version published by the Free Software Foundation. If the +Program does not specify a version number of the GNU General Public +License, you may choose any version ever published by the Free +Software Foundation. + +If the Program specifies that a proxy can decide which future versions +of the GNU General Public License can be used, that proxy's public +statement of acceptance of a version permanently authorizes you to +choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +### 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT +WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +### 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR +CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT +NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR +LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM +TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +### 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +## How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + +To do so, attach the following notices to the program. It is safest to +attach them to the start of each source file to most effectively state +the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper +mail. + +If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands \`show w' and \`show c' should show the +appropriate parts of the General Public License. Of course, your +program's commands might be different; for a GUI interface, you would +use an "about box". + +You should also get your employer (if you work as a programmer) or +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. For more information on this, and how to apply and follow +the GNU GPL, see . + +The GNU General Public License does not permit incorporating your +program into proprietary programs. If your program is a subroutine +library, you may consider it more useful to permit linking proprietary +applications with the library. If this is what you want to do, use the +GNU Lesser General Public License instead of this License. But first, +please read . diff --git a/README.md b/README.md new file mode 100644 index 0000000..747ee73 --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# Forji + +A native iOS client for [Forgejo](https://forgejo.org) + +--- + +Forji is a native iOS app for managing your Forgejo repositories, issues, pull requests, and notifications from your phone. Built with SwiftUI and Swift concurrency. + +## Screenshots + +![Repositories](screenshots/01_repositories.png) +![Code browser](screenshots/02_code_browser.png) +![Pull request detail](screenshots/03_pull_request.png) +![Notifications](screenshots/04_notifications.png) + +## Features + +### Repositories +- Browse and search your repositories +- Filter by All or Starred +- Star/unstar repositories +- View repository stats (stars, forks, language, open issues) + +### Code Browser +- Navigate file trees +- View files with syntax highlighting +- Render Markdown previews +- Edit files and commit changes directly +- Browse commit history with diffs +- Switch branches + +### Issues +- View issues across all repositories or per-repo +- Filter by open/closed state and search +- Create, edit, and close/reopen issues +- Manage labels, milestones, and assignees +- Comment with Markdown support + +### Pull Requests +- View PRs across all repositories or per-repo +- Create PRs with branch selection, labels, milestones, assignees, and reviewers +- Review diffs with inline comments +- Submit reviews (comment, approve, request changes) +- Merge with merge commit, rebase, or squash +- Close, reopen, and edit PRs + +### Notifications +- View unread, read, and all notifications +- Swipe to mark as read or dismiss +- Unread badge on the tab bar + +### Other +- Connect to any Forgejo instance (including self-hosted) +- Self-signed certificate support +- Multiple instance management +- Light, dark, and system theme +- Mermaid diagram rendering + +## Development + +Requires Xcode 26. The project uses [ForgejoKit](https://codeberg.org/secana/ForgejoKit) as a remote Swift package dependency. + +### Dev Environment + +A [Nix flake](flake.nix) provides all CLI tooling (`just`, `xcbeautify`, `swiftlint`, `swiftformat`): + +```bash +nix develop # Enter dev shell with all tools available +``` + +### Building & Testing + +A [justfile](https://github.com/casey/just) is provided for common tasks: + +```bash +just build # Build the app +just test # Run app unit tests +just run # Build, install, and launch in the simulator +just lint # Lint Swift code with SwiftLint +just format # Format Swift code with SwiftFormat +just test-ui # Run UI integration tests (requires Docker) +just test-one Class # Run a single integration test +just test-list # List all available integration tests +``` + +Integration tests spin up Forgejo instances in Docker, seed test data, and run UI tests against them. See [integration/](integration/) for details. + +## Architecture + +| Layer | Location | Purpose | +|-------|----------|---------| +| **[ForgejoKit](https://codeberg.org/secana/ForgejoKit)** | Remote SPM package | Platform-agnostic Swift package with all Forgejo API logic, models, and services | +| **Forji** | `Forji/` | SwiftUI iOS app with views, authentication, and persistence | + +ForgejoKit has no SwiftUI or platform dependencies, making it reusable for other clients (macOS, CLI, etc.). It is published as a separate package at [codeberg.org/secana/ForgejoKit](https://codeberg.org/secana/ForgejoKit). + +## Acknowledgments + +The Forji logo is based on the [Forgejo logo](https://codeberg.org/forgejo/forgejo) by Caesar Schinas and is licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). + +### Libraries + +- [ForgejoKit](https://codeberg.org/secana/ForgejoKit) — Forgejo API client (MIT) +- [Textual](https://github.com/gonzalezreal/textual) — Markdown rendering (MIT) +- [HighlightSwift](https://github.com/appstefan/HighlightSwift) — Code syntax highlighting (MIT) + +## Contributing + +Contributions are welcome! By submitting a pull request, you agree that your contributions are licensed under the [GNU General Public License v3.0](LICENSE) and may be distributed through Apple's App Store under the [App Store Exception](LICENSE#app-store-exception) included in the license. + +## License + +Forji is licensed under the [GNU General Public License v3.0](LICENSE) with an [App Store Exception](LICENSE#app-store-exception) that permits distribution of the compiled application through Apple's App Store. + +ForgejoKit is licensed under the [MIT License](https://codeberg.org/secana/ForgejoKit/src/branch/main/LICENSE). diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..21cf7ce --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1771207753, + "narHash": "sha256-b9uG8yN50DRQ6A7JdZBfzq718ryYrlmGgqkRm9OOwCE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d1c15b7d5806069da59e819999d70e1cec0760bf", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..20edbe7 --- /dev/null +++ b/flake.nix @@ -0,0 +1,24 @@ +{ + description = "Forji — native iOS/macOS client for Forgejo"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { nixpkgs, flake-utils, ... }: + flake-utils.lib.eachSystem [ "aarch64-darwin" "x86_64-darwin" ] (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + devShells.default = pkgs.mkShellNoCC { + packages = [ + pkgs.just + pkgs.xcbeautify + pkgs.swiftlint + pkgs.swiftformat + ]; + }; + }); +} diff --git a/integration/.forgejo-seed-hash b/integration/.forgejo-seed-hash new file mode 100644 index 0000000..d49875f --- /dev/null +++ b/integration/.forgejo-seed-hash @@ -0,0 +1 @@ +11b89bd63fe2a77529f2f8124efafaadcf4deccb435ae43957501aab0795fffd diff --git a/integration/.forgejo-seed-snapshot.tar.gz b/integration/.forgejo-seed-snapshot.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..2d67bbfeeeb44b10dca4340cd65bbdc4fa84133e GIT binary patch literal 259960 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

)PQ8o4 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 literal 0 HcmV?d00001 diff --git a/integration/docker-compose.yml b/integration/docker-compose.yml new file mode 100644 index 0000000..53d60de --- /dev/null +++ b/integration/docker-compose.yml @@ -0,0 +1,38 @@ +services: + forgejo-1: + image: codeberg.org/forgejo/forgejo:14 + ports: + - "13001:3000" + environment: + - FORGEJO__security__INSTALL_LOCK=true + - FORGEJO__service__DISABLE_REGISTRATION=true + - FORGEJO__server__ROOT_URL=http://localhost:13001 + volumes: + - forgejo-1-data:/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/v1/version"] + interval: 5s + timeout: 3s + retries: 12 + start_period: 10s + + forgejo-2: + image: codeberg.org/forgejo/forgejo:14 + ports: + - "13002:3000" + environment: + - FORGEJO__security__INSTALL_LOCK=true + - FORGEJO__service__DISABLE_REGISTRATION=true + - FORGEJO__server__ROOT_URL=http://localhost:13002 + volumes: + - forgejo-2-data:/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/v1/version"] + interval: 5s + timeout: 3s + retries: 12 + start_period: 10s + +volumes: + forgejo-1-data: + forgejo-2-data: diff --git a/integration/setup.sh b/integration/setup.sh new file mode 100755 index 0000000..0659fac --- /dev/null +++ b/integration/setup.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash +set -eo pipefail + +SCRIPT_DIR="$(dirname "$0")" +COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml" +BASE_URL="${1:-http://localhost:13001}" +SERVICE_NAME="${2:-forgejo-1}" +ADMIN_USER="testadmin" +ADMIN_PASS="admin1234" +ADMIN_AUTH="$ADMIN_USER:$ADMIN_PASS" +TESTBOT_AUTH="testbot:testbot1234" + +# --- Phase 1: Create users (sequential — prerequisite for everything) --- +echo "Creating admin user..." +docker compose -f "$COMPOSE_FILE" exec -T -u git "$SERVICE_NAME" \ + forgejo admin user create --admin \ + --username "$ADMIN_USER" \ + --password "$ADMIN_PASS" \ + --email testadmin@test.local \ + --must-change-password=false + +echo "Creating testbot user..." +docker compose -f "$COMPOSE_FILE" exec -T -u git "$SERVICE_NAME" \ + forgejo admin user create \ + --username testbot \ + --password testbot1234 \ + --email testbot@test.local \ + --must-change-password=false + +echo "Creating readonlyuser..." +docker compose -f "$COMPOSE_FILE" exec -T -u git "$SERVICE_NAME" \ + forgejo admin user create \ + --username readonlyuser \ + --password readonly1234 \ + --email readonlyuser@test.local \ + --must-change-password=false + +# --- Phase 2: Create both repos (parallel) --- +echo "Creating repositories..." +curl -sf -X POST "$BASE_URL/api/v1/user/repos" \ + -u "$ADMIN_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"name": "test-repo", "private": false, "auto_init": true}' & + +curl -sf -X POST "$BASE_URL/api/v1/user/repos" \ + -u "$ADMIN_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"name": "test-repo-2", "description": "A second test repository", "private": false, "auto_init": true}' & + +wait +echo "Repositories created." + +# --- Phase 3: Create issues + code files + pagination issues (parallel) --- + +# Create 25 pagination issues sequentially in a background subshell +# (sequential for deterministic issue numbering, background to overlap with other work) +echo "Creating pagination test issues..." +(for i in $(seq 1 25); do + curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo-2/issues" \ + -u "$ADMIN_AUTH" \ + -H "Content-Type: application/json" \ + -d "{\"title\": \"Pagination test issue $i\"}" +done) & + +# Create issues 1-3 as testbot (generates notifications for testadmin) +echo "Creating issues as testbot..." +for i in 1 2 3; do + curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues" \ + -u "$TESTBOT_AUTH" \ + -H "Content-Type: application/json" \ + -d "{\"title\": \"Test issue $i from integration tests\"}" +done + +# Phase 3b: Independent operations on test-repo (parallel) +echo "Setting up test-repo content..." + +# Comment on issue #1 +curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues/1/comments" \ + -u "$TESTBOT_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"body": "This is a test comment from testbot"}' & + +# Add body text + close issue #3 in one PATCH +curl -sf -X PATCH "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues/3" \ + -u "$ADMIN_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"body": "This is the **markdown** body for issue 3.\n\nIt has multiple paragraphs.", "state": "closed"}' & + +# Body for issue #1 +curl -sf -X PATCH "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues/1" \ + -u "$ADMIN_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"body": "This is the **markdown** body for issue 1.\n\nIt has multiple paragraphs."}' & + +# Body for issue #2 +curl -sf -X PATCH "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues/2" \ + -u "$ADMIN_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"body": "This is the **markdown** body for issue 2.\n\nIt has multiple paragraphs."}' & + +# Code files (sequential — both commit to same branch) +curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/contents/hello.py" \ + -u "$ADMIN_AUTH" \ + -H "Content-Type: application/json" \ + -d "{\"content\": \"$(echo -n 'print(\"Hello from Forgejo!\")' | base64)\", \"message\": \"Add hello.py\"}" + +curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/contents/src/main.py" \ + -u "$ADMIN_AUTH" \ + -H "Content-Type: application/json" \ + -d "{\"content\": \"$(echo -n 'def main():\n print(\"Main module\")' | base64)\", \"message\": \"Add src/main.py\"}" + +# Add testbot as collaborator +curl -sf -X PUT "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/collaborators/testbot" \ + -u "$ADMIN_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"permission": "write"}' & + +# Add readonlyuser as read-only collaborator +curl -sf -X PUT "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/collaborators/readonlyuser" \ + -u "$ADMIN_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"permission": "read"}' & + +# Create label +curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/labels" \ + -u "$ADMIN_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"name": "bug", "color": "#ee0701"}' & + +# Create milestone +curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/milestones" \ + -u "$ADMIN_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"title": "v1.0", "description": "First release milestone", "due_on": "2026-03-01T00:00:00Z"}' & + +wait +echo "Phase 3 done." + +# --- Phase 3c: Metadata that depends on label/milestone existing --- +# Label ID 1 and milestone ID 1 are deterministic because they're the first +# created on a fresh Forgejo instance (auto-increment starts at 1). +echo "Setting issue metadata..." + +# Add label to issue #1 (label ID 1 = "bug", created in Phase 3b) +curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues/1/labels" \ + -u "$ADMIN_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"labels": [1]}' & + +# Set milestone + assignee on issue #1 (milestone ID 1 = "v1.0", created in Phase 3b) +curl -sf -X PATCH "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues/1" \ + -u "$ADMIN_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"milestone": 1, "assignees": ["testadmin"]}' & + +wait + +# --- Phase 4: Create branches + PRs (sequential — numbers must be deterministic #4, #5) --- +echo "Creating feature branch..." +curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/contents/feature.txt" \ + -u "$TESTBOT_AUTH" \ + -H "Content-Type: application/json" \ + -d "{\"content\": \"$(echo -n 'This file was added in a feature branch' | base64)\", \"message\": \"Add feature.txt\", \"new_branch\": \"feature-branch\"}" + +echo "Creating pull request #4..." +curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/pulls" \ + -u "$TESTBOT_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"title": "Add feature file", "head": "feature-branch", "base": "main", "body": "This PR adds a new feature file."}' + +echo "Creating merge branch..." +curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/contents/merge-test.txt" \ + -u "$TESTBOT_AUTH" \ + -H "Content-Type: application/json" \ + -d "{\"content\": \"$(echo -n 'This file is for merge testing' | base64)\", \"message\": \"Add merge-test.txt\", \"new_branch\": \"merge-branch\"}" + +echo "Creating pull request #5..." +curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/pulls" \ + -u "$TESTBOT_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"title": "Merge test PR", "head": "merge-branch", "base": "main", "body": "This PR is for testing the merge flow."}' + +# --- Phase 5: Post-PR metadata (parallel) --- +echo "Setting PR metadata..." + +# Comment on PR #4 +curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues/4/comments" \ + -u "$TESTBOT_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"body": "Please review this PR when you get a chance."}' & + +# Review on PR #4 +curl -sf -X POST "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/pulls/4/reviews" \ + -u "$ADMIN_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"event": "COMMENT", "body": "Looks good so far, just a few comments.", "comments": [{"path": "feature.txt", "body": "Consider a more descriptive filename.", "new_position": 1}]}' & + +# Milestone + assignee on PR #4 +curl -sf -X PATCH "$BASE_URL/api/v1/repos/$ADMIN_USER/test-repo/issues/4" \ + -u "$ADMIN_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"milestone": 1, "assignees": ["testadmin"]}' & + +wait + +echo "" +echo "Seed data created successfully." diff --git a/justfile b/justfile new file mode 100644 index 0000000..aaaf138 --- /dev/null +++ b/justfile @@ -0,0 +1,257 @@ +set shell := ["bash", "-eo", "pipefail", "-c"] + +default_destination := "platform=iOS Simulator,name=iPhone 17 Pro" +default_destination_b := "platform=iOS Simulator,name=iPhone Air" +compose_file := "integration/docker-compose.yml" +base_url_1 := "http://localhost:13001" +base_url_2 := "http://localhost:13002" +readonly_classes := "LoginUITests CommitHistoryUITests PaginationUITests RepositoryUITests IssueUITests PullRequestUITests OverviewCreateUITests" +mutating_classes := "RepositoryMutatingUITests IssueMutatingUITests PullRequestMutatingUITests OverviewCreateMutatingUITests HomeScreenUITests NotificationsUITests PermissionUITests" + +# List all available tasks +default: + @just --list + +# Build the app +build destination=default_destination: + xcodebuild -project Forji/Forji.xcodeproj -scheme Forji -destination '{{destination}}' build 2>&1 | xcbeautify + +# Run app unit tests +test destination=default_destination: + xcodebuild -project Forji/Forji.xcodeproj -scheme Forji -destination '{{destination}}' test -only-testing:ForjiTests 2>&1 | xcbeautify + +# Lint Swift code +lint: + swiftlint lint Forji/Forji + +# Format Swift code +format: + swiftformat Forji/Forji + +# Clean build artifacts +clean: + xcodebuild -project Forji/Forji.xcodeproj -scheme Forji clean 2>&1 | xcbeautify + +# Build, install, and launch in simulator +run destination=default_destination: + #!/usr/bin/env bash + set -eo pipefail + SIM_NAME="$(echo '{{destination}}' | sed -n 's/.*name=\([^,]*\).*/\1/p')" + xcrun simctl boot "$SIM_NAME" 2>/dev/null || true + open -a Simulator + xcodebuild -project Forji/Forji.xcodeproj -scheme Forji -destination '{{destination}}' build 2>&1 | xcbeautify + BUILT_APP="$(xcodebuild -project Forji/Forji.xcodeproj -scheme Forji -destination '{{destination}}' -showBuildSettings 2>/dev/null | grep ' BUILT_PRODUCTS_DIR' | awk '{print $3}')/Forji.app" + xcrun simctl install "$SIM_NAME" "$BUILT_APP" + xcrun simctl launch "$SIM_NAME" "$(defaults read "$BUILT_APP/Info.plist" CFBundleIdentifier)" + +# List all UI integration tests +test-list: + @grep -rh 'func test.*()' Forji/ForjiUITests/*.swift \ + | grep -v 'override\|private\|ForgejoUITestBase' \ + | sed 's/.*func //' | sed 's/().*//' \ + | while read -r method; do \ + file=$(grep -rl "func $method()" Forji/ForjiUITests/*.swift | head -1); \ + class=$(grep 'class.*:' "$file" | head -1 | sed 's/.*class //' | sed 's/[: ].*//' ); \ + printf " %s/%s\n" "$class" "$method"; \ + done | sort + +# Start Forgejo Docker containers +docker-up: + docker compose -f {{compose_file}} up -d --wait --wait-timeout 120 + +# Stop Forgejo Docker containers and clean up +docker-down: + docker compose -f {{compose_file}} down -v 2>/dev/null || true + rm -f /tmp/forgejo_test_url.txt /tmp/forgejo_test_url_*.txt + +# Seed test data into Forgejo instances (with snapshot caching) +seed: + #!/usr/bin/env bash + set -eo pipefail + COMPOSE_FILE="{{compose_file}}" + BASE_URL_1="{{base_url_1}}" + BASE_URL_2="{{base_url_2}}" + SNAPSHOT_FILE="integration/.forgejo-seed-snapshot.tar.gz" + HASH_FILE="integration/.forgejo-seed-hash" + SETUP_HASH=$(shasum -a 256 integration/setup.sh | awk '{print $1}') + wait_for_instance() { + local url="$1" elapsed=0 + until curl -sf "$url/api/v1/version" > /dev/null 2>&1; do + if [ "$elapsed" -ge 60 ]; then + echo "Timed out waiting for $url after 60s" + exit 1 + fi + sleep 1 + elapsed=$((elapsed + 1)) + done + echo " $url is ready (${elapsed}s)" + } + if [ -f "$SNAPSHOT_FILE" ] && [ -f "$HASH_FILE" ] && [ "$(cat "$HASH_FILE")" = "$SETUP_HASH" ]; then + echo "Restoring from seed snapshot..." + CONTAINER_1=$(docker compose -f "$COMPOSE_FILE" ps -q forgejo-1) + CONTAINER_2=$(docker compose -f "$COMPOSE_FILE" ps -q forgejo-2) + docker cp "$SNAPSHOT_FILE" "$CONTAINER_1:/tmp/snapshot.tar.gz" & + docker cp "$SNAPSHOT_FILE" "$CONTAINER_2:/tmp/snapshot.tar.gz" & + wait + docker exec "$CONTAINER_1" sh -c "cd / && tar xzf /tmp/snapshot.tar.gz" & + docker exec "$CONTAINER_2" sh -c "cd / && tar xzf /tmp/snapshot.tar.gz" & + wait + docker compose -f "$COMPOSE_FILE" restart forgejo-1 forgejo-2 + wait_for_instance "$BASE_URL_1" & + wait_for_instance "$BASE_URL_2" & + wait + echo "Snapshot restored to both instances." + else + echo "Seeding test data..." + bash integration/setup.sh "$BASE_URL_1" forgejo-1 + echo "Snapshotting seed data..." + CONTAINER_1=$(docker compose -f "$COMPOSE_FILE" ps -q forgejo-1) + docker exec "$CONTAINER_1" sh -c "cd / && tar czf /tmp/snapshot.tar.gz data" + docker cp "$CONTAINER_1:/tmp/snapshot.tar.gz" "$SNAPSHOT_FILE" + echo "$SETUP_HASH" > "$HASH_FILE" + echo "Restoring snapshot to instance 2..." + CONTAINER_2=$(docker compose -f "$COMPOSE_FILE" ps -q forgejo-2) + docker cp "$SNAPSHOT_FILE" "$CONTAINER_2:/tmp/snapshot.tar.gz" + docker exec "$CONTAINER_2" sh -c "cd / && tar xzf /tmp/snapshot.tar.gz" + docker compose -f "$COMPOSE_FILE" restart forgejo-2 + wait_for_instance "$BASE_URL_2" + echo "Instance 2 seeded from snapshot." + fi + +# Run read-only UI tests (assumes build + seed + URL files are set up) +test-readonly destination=default_destination: + #!/usr/bin/env bash + set -eo pipefail + ARGS="" + for cls in {{readonly_classes}}; do + ARGS="$ARGS -only-testing:ForjiUITests/$cls" + done + xcodebuild test-without-building \ + -project Forji/Forji.xcodeproj \ + -scheme Forji \ + -destination '{{destination}}' \ + -parallel-testing-enabled NO \ + $ARGS 2>&1 | xcbeautify + +# Run mutating UI tests (assumes build + seed + URL files are set up) +test-mutating destination=default_destination_b: + #!/usr/bin/env bash + set -eo pipefail + ARGS="" + for cls in {{mutating_classes}}; do + ARGS="$ARGS -only-testing:ForjiUITests/$cls" + done + xcodebuild test-without-building \ + -project Forji/Forji.xcodeproj \ + -scheme Forji \ + -destination '{{destination}}' \ + -parallel-testing-enabled NO \ + $ARGS 2>&1 | xcbeautify + +# Run full UI integration test suite (requires Docker) +test-ui destination_a=default_destination destination_b=default_destination_b: + #!/usr/bin/env bash + set -eo pipefail + cleanup() { + just docker-down + } + trap cleanup EXIT + just sim-boot '{{destination_a}}' '{{destination_b}}' & + just docker-up & + just build-for-testing '{{destination_a}}' '{{destination_b}}' & + wait + just seed + just _write-url-files '{{destination_a}}' '{{destination_b}}' + LOG_A="/tmp/forji_test_group_a.log" + LOG_B="/tmp/forji_test_group_b.log" + EXIT_A=0 + EXIT_B=0 + echo "Starting read-only tests..." + just test-readonly '{{destination_a}}' > "$LOG_A" 2>&1 & + PID_A=$! + echo "Starting mutating tests..." + just test-mutating '{{destination_b}}' > "$LOG_B" 2>&1 & + PID_B=$! + wait $PID_A || EXIT_A=$? + wait $PID_B || EXIT_B=$? + echo "=== Read-only tests output ===" + cat "$LOG_A" + echo "" + echo "=== Mutating tests output ===" + cat "$LOG_B" + rm -f "$LOG_A" "$LOG_B" + if [ "$EXIT_A" -ne 0 ]; then + echo "Read-only tests failed (exit code $EXIT_A)." + fi + if [ "$EXIT_B" -ne 0 ]; then + echo "Mutating tests failed (exit code $EXIT_B)." + fi + if [ "$EXIT_A" -ne 0 ] || [ "$EXIT_B" -ne 0 ]; then + echo "Integration tests FAILED." + exit 1 + fi + echo "" + echo "All integration tests passed." + +# Run a single UI test (requires Docker). Use `just test-list` to see available tests. +test-one filter="" destination=default_destination: + #!/usr/bin/env bash + set -eo pipefail + FILTER="{{filter}}" + if [ -z "$FILTER" ]; then + echo "Usage: just test-one " + echo "" + echo "Run 'just test-list' to see available tests." + exit 1 + fi + cleanup() { + just docker-down + } + trap cleanup EXIT + SIM_NAME="$(echo '{{destination}}' | sed -n 's/.*name=\([^,]*\).*/\1/p')" + xcrun simctl boot "$SIM_NAME" 2>/dev/null || true + just docker-up & + xcodebuild build-for-testing \ + -project Forji/Forji.xcodeproj \ + -scheme Forji \ + -destination '{{destination}}' 2>&1 | xcbeautify & + wait + just seed + SIM_SAFE="${SIM_NAME// /_}" + echo "{{base_url_1}}" > "/tmp/forgejo_test_url_${SIM_SAFE}.txt" + echo "{{base_url_1}}" > /tmp/forgejo_test_url.txt + xcodebuild test-without-building \ + -project Forji/Forji.xcodeproj \ + -scheme Forji \ + -destination '{{destination}}' \ + -parallel-testing-enabled NO \ + -only-testing:"ForjiUITests/$FILTER" 2>&1 | xcbeautify + +[private] +sim-boot destination_a=default_destination destination_b=default_destination_b: + #!/usr/bin/env bash + set -eo pipefail + SIM_A="$(echo '{{destination_a}}' | sed -n 's/.*name=\([^,]*\).*/\1/p')" + SIM_B="$(echo '{{destination_b}}' | sed -n 's/.*name=\([^,]*\).*/\1/p')" + xcrun simctl boot "$SIM_A" 2>/dev/null || true + xcrun simctl boot "$SIM_B" 2>/dev/null || true + +[private] +build-for-testing destination_a=default_destination destination_b=default_destination_b: + xcodebuild build-for-testing \ + -project Forji/Forji.xcodeproj \ + -scheme Forji \ + -destination '{{destination_a}}' \ + -destination '{{destination_b}}' 2>&1 | xcbeautify + +[private] +_write-url-files destination_a=default_destination destination_b=default_destination_b: + #!/usr/bin/env bash + set -eo pipefail + SIM_A="$(echo '{{destination_a}}' | sed -n 's/.*name=\([^,]*\).*/\1/p')" + SIM_B="$(echo '{{destination_b}}' | sed -n 's/.*name=\([^,]*\).*/\1/p')" + SIM_A_SAFE="${SIM_A// /_}" + SIM_B_SAFE="${SIM_B// /_}" + echo "{{base_url_1}}" > "/tmp/forgejo_test_url_${SIM_A_SAFE}.txt" + echo "{{base_url_2}}" > "/tmp/forgejo_test_url_${SIM_B_SAFE}.txt" + echo "{{base_url_1}}" > /tmp/forgejo_test_url.txt diff --git a/screenshots/01_repositories.png b/screenshots/01_repositories.png new file mode 100644 index 0000000000000000000000000000000000000000..adb08a057fba6830a64dbfe79dadefba70090896 GIT binary patch literal 46073 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

r!MojMw99bO@NdM&UX!D6FX~nXIQ_p1!%RKiZw( zlq+!m#=&xi_A;zHgoOlMP&{4_OuGi--7h`xN=c*H&|< zRC6Ifv^lMj6HD;;$2-DhuYO*1fdA_cM&X2d=y<4|IwbO)b^rWjvhHI+=Z^D&pt>4jwlVSq@995y zQCBY!DQWyuJTs4SWIdjg#S`esh^Y7aQL0^~<`CW*Ra-;Qrtw(R4DEf|U~0a0DEz)> zP3PfN$9Y55AY;c7To@kd%x)!2pM!qym76ScQxh*l?WC)GTf#tRji`;t(45U{Oz{0y zdpiWj-$*3b-)~%+X`a6Aj_txszig_-2Ai+ypjV<-*bz@~>GE29UFZ%Ygp%12x@S_{ z2*J+3-h0`VW7AFQ8?m!Hich^5UhYs2@IFRlyJdW3vQnphUIFyP&ClOmSIDe1oUL&_ zK;p^|2Tp%0L-8MP@cAm=k8{$J!P-jS9wQky#6Q7AdAyWm%M)fTU|M%~hxZm?_y3lL zZ!Z>vEN?9>GqABTpeth|9a9#Vf**yQ74(Y5cgsEZzZtABuMyK*+DBzyf;^gr>sGcB z)@+)YtTVtc+g@TljPl2g7YQn<{VwJrJ(p8$S0{Fn2dp(KE#%g>ygwME4TP)(fv)61 zqUu5X4PR~;hpCkktwB`y(@Fm9?&;UbTm}WU4{#ps1U)5x?sc|3Bp{H$=*XHS+4<>}z|KH}Hi1t^|7Pyv;!F3`>HEv1}9JYDg7(Tivb_ z#$%>;=C-0EfS7INjCcAkN#NxX?Q z^#&D*bk_hY#LMHvL!8hc7hY%Rw8#=+%WY*g5b?{3iZ%nQa<|Ah#AZIbQ6l96Kh=B; ztw44p()8>$#jeZMOBaPkJDXYYOY}+>q&mMqk_|a$L z7M-V?#$KOywJ%_WcE$6;4A-L`VWJ70^51o2&+NE0VA!87u5u0g7lmsIF*j?_+b_Pm zJ=4jduUk7^-h^&REfK%vGyalYNeSPs|N2LLH8Xr<$W+xqncTn>{H)|cMW8hy&h7@WuZ3xcWe=X6BFyq=1q|&fgs3 zhoN(5aSIN96{yi)$w~Y#S%$E;K6%_~;SAj)qCBdMTLX`jtmJ=lv1oJ&=%+ zO_R5ZORiTf&5!37f52;miLF<4WOs7jP3ru8*KL7U0rebaGc!JNP~J$@l?H-Qusz<) z6jyW)Ow&=kS_9l;hv1&;y_2zMLD1MZz?!=bQJbZ}y86?I5tAr2&9(kW>{zD^PYom{ zT79Li%_H*M#XY@-enGE2;gaf$CZX!tSgeKmF%EtW9*?a=j-$_VkEo{C=Af_+97Xf| z$Uvb^=p?(-%}X;nc%5>R2fXousNXyL%ZqoQ{VZa-Q=S4~mWI4n7yc@kl z4?Tluz$yC6NqY~yonPEzvPqf?ss8d`q@rG}bO9Egv_6M>zv)a}%_h4T1aHc&J?Hvi zH(*?P^3H8fTi!U;2mEc2-$&9TN3>FvF-4@Wigm`2>!2teK2+q$_ZJc7-7fgCx?GBam;)anOyEGUYCHJxQ(fHzIC*FJST$f_#wDw(+UN|%>d>br{0D27M z_>eqzFG7U_!kUh`U!;&iCwug(>lF#2?^7r-Z2Yn^#e~)qH7{p;FWrud*7(D-CF`7NjFucfE11NJ#G(%>uB=1ZpbXyNCvSD6;`=eqD@~h-1)cno zTa(w)jeTRWk8ekQ*0HHYHtYHtpv?@pQi)APPc-ZcQ6~xec7eSx{l?g4te`=W^%OV` zTKHE#JOUl#?_@;`_r??b$l8kPW+upS?6VCEeEtC}P3t{p^QnD~#}(mC&Cr}ed6O_T z@K;NG@v)$u4Kd9<^MpynpacALZJ0|#m@?|89Xw3(9-YjRdzE%*!P7=gnKDo@!K(ane zDAq*aC`*8A3dOdNfMhbif5uzI&QdDucIzLTM$DSAZTF~|S!rA8+4b7f*dNt606P%$ z5=@Sf`#r&m-O=N3u%_AOn#Q{Zy06yVPzeEYmit9dgLW$#qc1@cn2}_$LDFLy0t8m1 zo0jH!B5JlMl2834$i|JfJl;Hwhs#cMJ{rXWn-;%nVUgE-$=oAjFw zoeVk))ejDIg#e~>z|%Rug&JbMtIoE>;Chj9TOa#Z-!04*xtS)}b zpK=#k11!t=S5}x{+;U~cRTStN08Q|)XjZHMBFI&Z{mOw2o&e4y7BU)KCL>HSB8Gk^p6U_%;}Qgf+t8$HPYnQ)6&PUg;9Wkwmh#=`Zx|Ve-hZ5M`gPxtVOh$yh`Erq5-sejh}_fUCunV#{Lv8 zR;TgvF?8X^CnfvIO`1TraWSUtE`G6ixXhyk(3X!Oj9$~&%*Dt%LP=|tzXZ1}VnvF1 zb90fH0a+6(HSv@d^~C|9M4TY|0|3+0vGRt7m){TP1smsmH3pz*_K4+##W_0>W0lB= zMW>w03{*x`^+5JnK}{`A^zMY29NXx)JHm(|mvkB$jF@V_FuLSTdLFLtbC6<~CsT;w zf%uV2S{p)#{cNgE%kZb5q2Xjs0o5J10;PMqh98{C?_84`(OaFH8cV1$#VpUy#cGcA zyMoEL=K2-KuC_|L}C99oww(> znD(viaDsC3ko_R_8)-|u^`biZQ@eCRnmyi*&ri`R4kZnN>X=ZfQF08<9od)!%WtbH7cJAj&@ii=;o~S085jhvtP~2FGnMFY|Uj z7D*}N*Gto?Tz>BHv@7bb`ckJ@*>e3&cqK(44jxV4Bh5dAvkMB?|3h!)Z|1b#SVV2jM}_bB8aUcEB37K zmDbQ$+}abA#8qT9(9Ac&jcP0WmQX<=$|^O|l^>j!Mj@$iw=tXe5UIfJuv)C`lP5s>q1MPw8Y~;-TZBx$}FirAISN>l1o{p;hnA1z`fTHjj6)cYGKqXl1&*5qF6y=-`!{R)Y<7n$gTGBJUN zIVoKG&G>CG-yZGw=~paB5v)tiK)G*S!l(1x(%y&VN-QD)vb`*<<9B|S4l7|={}O1{ zShcp=I>>Zar@aJ~qzbW0g!&Ou%Q@#Bc%TQCj{e-L5gW=XV8g*H7OGb9rRoSwPRCC zKu-YcS@`RB^B;BK=GAS8TlATkxONz<`xP zI5u=l=NT?ts+ta3f2?H}d5PV?dx0#*O@R?P^7B1}4ajZSNztDV5e8reNpOYxIJ(UW z6=;{9xmFQdW`{!t8ObT>6MYUQ{GvTCy+so7dld0wMzV=@I^y#C2qzuAg|A}7wpXj# z9o}pcM5azt@{6!~+dkmfGO<+}z^Aq%{^f@Z(XHH8R?IW`D(0RdEkx*R!Mb(;xH|q`pdlxTzHar%NcKj=v3c2$srx=kw*e>IogK zSDJ~BKulK2dG{gR_XV!HStoHG{LV0>~JOkP4-Vo=0HcdDpnXe#z(l?vk@QFT}oBN2u2fw0&HMEC1W;&Q{u-OfBd%32%&~2FM~D$+$0lnWfV4x@SKWG- zHA$*5r~EoA)_nlG9}1Eo>z-Zl(h8DdWFh&<5Rm98j+#MtYF1lhm3f-%i-&?eaf%L@ zVH3(Cxwd&QYgnN2V)tOV*pb5&gz3a9v15P&LIfOEPG60YH&D!IUlR&=b1} zYn4NNo189QEibg47s3(zT{65=ft54Rtz`u4ps8v`Ov26_W8b_vaHdqE;hG)%vcQN6 z@3&$`%T+!N2br%HIGPniq9rH4jWysK4=2dKU6eCAMThO} z1jOFJPp4=iSq&(Vy1%(|=FDj92xwK~BVLq^$e@;+`n=wZ@sJtPkcj>?dmRtp7cfV8 z<|TunJ!8q$>x48#Qa$g(hWQxC;pFCK7Dtq5Z!HK3*Vbi&4+-=xC9%zB^ytCE8?UiR zF84fID8@&NwZ~&ty3RG}we2(Ney60im3;7V1kdbxcw0D6Z7tb*S79|}o)49z$Dih{ zA^|9~unGLIlo-eDa9_^>9D*n?ne!SY)X@JRk}-@-^Z;RzBE+b27t0t*Eo3>+ZAOVM zd+G7$$V)2+ajiZaOO~nAHy>NlSS8*j7jD%ZfAfWLwHx-_u?b9Sr_|1k748%eL23U1cGB&~y+J-j|vxy?#z^vC3fqfTDN0Jc7#kY

=zLJ-0gDaFY$PQYHU4V!H66{wN#~II9 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