Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"Bash(xcodebuild:*)",
"Bash(find:*)"
],
"deny": [],
"ask": []
}
}
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Select XCode Version
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "16.1"
xcode-version: "latest-stable"
- name: Build
run: xcodebuild clean build analyze -scheme "PReek" -project "PReek.xcodeproj" -destination "generic/platform=${{ matrix.platform }}" CODE_SIGNING_ALLOWED=NO | xcpretty && exit ${PIPESTATUS[0]}

Expand All @@ -36,6 +36,6 @@ jobs:
- name: Select XCode Version
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "16.1"
xcode-version: "latest-stable"
- name: Test
run: xcodebuild test -scheme "PReek" -project "PReek.xcodeproj" -destination "platform=macOS,arch=arm64" CODE_SIGNING_ALLOWED=NO | xcpretty && exit ${PIPESTATUS[0]}
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon

# Thumbnails
._*

Expand Down Expand Up @@ -52,3 +49,4 @@ xcuserdata/

# End of https://www.toptal.com/developers/gitignore/api/macos,xcode

build/
131 changes: 131 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

PReek is a macOS/iOS SwiftUI application that brings GitHub Pull Request notifications directly into the macOS MenuBar. It fetches pull requests based on GitHub notifications and displays them in a clean, organized interface with vim-like keyboard shortcuts.

## Development Commands

### Building
```bash
# Build for macOS
xcodebuild clean build -scheme "PReek" -project "PReek.xcodeproj" -destination "generic/platform=macOS" CODE_SIGNING_ALLOWED=NO

# Build for iOS
xcodebuild clean build -scheme "PReek" -project "PReek.xcodeproj" -destination "generic/platform=iOS" CODE_SIGNING_ALLOWED=NO

# Build with analysis
xcodebuild clean build analyze -scheme "PReek" -project "PReek.xcodeproj" -destination "generic/platform=macOS" CODE_SIGNING_ALLOWED=NO
```

### Testing
```bash
# Run tests
xcodebuild test -scheme "PReek" -project "PReek.xcodeproj" -destination "platform=macOS,arch=arm64" CODE_SIGNING_ALLOWED=NO

# Run tests with pretty output (requires xcpretty)
xcodebuild test -scheme "PReek" -project "PReek.xcodeproj" -destination "platform=macOS,arch=arm64" CODE_SIGNING_ALLOWED=NO | xcpretty
```

### Opening in Xcode
```bash
open PReek.xcodeproj
```

## Architecture Overview

### Core Components

**App Structure:**
- `PReekApp.swift` - Main app entry point, handles MenuBarExtra setup and lifecycle
- `ContentView.swift` - Root content view with navigation logic, handles macOS/iOS platform differences

**Data Flow:**
- `PullRequestsViewModel.swift` - Central view model managing pull request state, GitHub API calls, and UI updates
- `ConfigViewModel.swift` - Manages app configuration and GitHub authentication
- `GitHubService.swift` - API service layer for GitHub REST and GraphQL APIs

**Models:**
- `PullRequest.swift` - Core data model with status tracking and unread calculation
- `Event.swift`, `Comment.swift`, `User.swift`, `Repository.swift` - Supporting GitHub data models
- `Notification.swift` - GitHub notification model

**Services:**
- GraphQL queries in `PullRequestsQuery.swift` and `ViewerQuery.swift`
- DTO mapping layer in `Services/DtoModelMapper/` for converting API responses to domain models
- `ConfigService.swift` - Configuration management with UserDefaults

### Key Architectural Patterns

**Cross-Platform Support:**
- Platform-specific UI code using `#if os(macOS)` / `#else` blocks
- macOS uses MenuBarExtra with NavigationStack, iOS uses TabView
- Shared view models and business logic across platforms

**State Management:**
- SwiftUI `@ObservedObject` and `@StateObject` for reactive UI updates
- Combine publishers for filtering and data transformation
- `@AppStorage` for persistent user preferences

**API Integration:**
- Dual GitHub API support (REST for notifications, GraphQL for pull request details)
- Enterprise GitHub support via configurable base URLs
- JWT-style token authentication

**Unread State Calculation:**
- Complex logic in `PullRequest.calculateUnread()` comparing notification timestamps with event timestamps
- Tracks oldest unread events for each pull request

### View Architecture

**Main Screens:**
- `MainScreen.swift` - Primary pull request list interface
- `WelcomeScreen.swift` - Initial setup and authentication
- `SettingsScreen.swift` - Configuration management

**Pull Request Views:**
- Two main list styles: `PullRequestsList.swift` (iOS-style) and `PullRequestsDisclosureGroupList.swift` (macOS-style)
- `PullRequestDetailView.swift` - Detailed view with comments, commits, and events
- Event display in `EventView.swift` and `CommentsView.swift`

**Utility Components:**
- `ResourceIcon.swift` - GitHub status icons (open, closed, merged, draft)
- `TimeSensitiveText.swift` - Relative time display
- Keyboard navigation handler for vim-like shortcuts (j/k/g/G/space)

## Testing

Tests are located in `PReekTests/` and focus on:
- Data transformation logic (`ReviewThreadsCommentsToEventsTests.swift`)
- Unread state calculation (`PullRequestUnreadTests.swift`)

## Dependencies

Key external dependencies managed via Swift Package Manager:
- `MenuBarExtraAccess` - Enhanced MenuBar control
- `KeychainAccess` - Secure token storage
- `MarkdownUI` - Markdown rendering for comments
- `LinkHeaderParser` - GitHub API pagination support

## Notable Implementation Details

**GitHub API Integration:**
- Uses GitHub's notification API to determine which PRs to show (notifications-based approach)
- Requires `notifications` scope for basic functionality, `repo` scope for private repositories
- GraphQL queries fetch comprehensive PR data including timeline events and review comments

**MenuBar Behavior:**
- Dynamic icon changes based on unread status (`MenuBarIcon` vs `MenuBarIconUnread`)
- Auto-focus and keyboard shortcut handling when menu opens
- 5-second delay before resetting navigation state when menu closes

**Keyboard Navigation:**
- Vim-like navigation: j (down), k (up), g (top), G (bottom), space (toggle)
- Focus management across different UI states and screens

**Release Process:**
- Automated DMG creation with signing and notarization in Xcode scheme post-actions
- Uses `create-dmg` npm package for DMG generation
- Full notarization workflow integrated into build process
2 changes: 1 addition & 1 deletion Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -1234,5 +1234,5 @@
}
}
},
"version" : "1.0"
"version" : "1.1"
}
30 changes: 23 additions & 7 deletions PReek.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
6C1CB6FD2C92344200305288 /* RevealSecureField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1CB6FC2C92342F00305288 /* RevealSecureField.swift */; };
6C1F49B82CB5BB9B00C30677 /* SafeArrayIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1F49B72CB5BB9300C30677 /* SafeArrayIndex.swift */; };
6C1F49B92CB5BB9B00C30677 /* SafeArrayIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1F49B72CB5BB9300C30677 /* SafeArrayIndex.swift */; };
E300212E25D94E51B33AB453 /* PullRequestUnreadCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E300212F25D94E51B33AB454 /* PullRequestUnreadCalculator.swift */; };
AB76BF5A49F34BF6B75F6415 /* PullRequestUnreadCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E300212F25D94E51B33AB454 /* PullRequestUnreadCalculator.swift */; };
6C232DE72C08C51D00C003B1 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 6C232DE62C08C51D00C003B1 /* KeychainAccess */; };
6C232DEB2C08C95E00C003B1 /* ConfigViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C232DEA2C08C95E00C003B1 /* ConfigViewModel.swift */; };
6C232DED2C08CB9600C003B1 /* KeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C232DEC2C08CB9600C003B1 /* KeychainStorage.swift */; };
Expand Down Expand Up @@ -143,6 +145,7 @@
6C1CB6F62C921CCF00305288 /* IfView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IfView.swift; sourceTree = "<group>"; };
6C1CB6FC2C92342F00305288 /* RevealSecureField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevealSecureField.swift; sourceTree = "<group>"; };
6C1F49B72CB5BB9300C30677 /* SafeArrayIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeArrayIndex.swift; sourceTree = "<group>"; };
E300212F25D94E51B33AB454 /* PullRequestUnreadCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullRequestUnreadCalculator.swift; sourceTree = "<group>"; };
6C232DEA2C08C95E00C003B1 /* ConfigViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigViewModel.swift; sourceTree = "<group>"; };
6C232DEC2C08CB9600C003B1 /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = "<group>"; };
6C232DEE2C08F6C600C003B1 /* ConfigService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigService.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -372,6 +375,7 @@
6C3DA9F02D82334D0075AFBC /* systemColors.swift */,
6C84D9A92D1EF2F700355C11 /* DisableAutocapitalization.swift */,
6C1F49B72CB5BB9300C30677 /* SafeArrayIndex.swift */,
E300212F25D94E51B33AB454 /* PullRequestUnreadCalculator.swift */,
6C7C4CA22C087A9900B9B4DA /* IfStringInterpolation.swift */,
6C1CB6F62C921CCF00305288 /* IfView.swift */,
6C1CB6F32C921CAF00305288 /* CodableAppStorage.swift */,
Expand Down Expand Up @@ -503,7 +507,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1600;
LastUpgradeCheck = 1540;
LastUpgradeCheck = 2600;
TargetAttributes = {
6C1CB6CD2C920E2E00305288 = {
CreatedOnToolsVersion = 16.0;
Expand Down Expand Up @@ -578,6 +582,7 @@
6C9322062D1317E200D6C057 /* ReadData.swift in Sources */,
6C3DA9F22D8233510075AFBC /* systemColors.swift in Sources */,
6C1F49B92CB5BB9B00C30677 /* SafeArrayIndex.swift in Sources */,
E300212E25D94E51B33AB453 /* PullRequestUnreadCalculator.swift in Sources */,
6C1CB6F52C921CB700305288 /* CodableAppStorage.swift in Sources */,
6C1CB6EC2C921B2100305288 /* mergeArray.swift in Sources */,
6C1CB6EB2C921AE500305288 /* PullRequestDto.swift in Sources */,
Expand Down Expand Up @@ -628,6 +633,7 @@
6C1CB6F42C921CB700305288 /* CodableAppStorage.swift in Sources */,
6CE563FB2C00862E008E9A4B /* DividedView.swift in Sources */,
6C1F49B82CB5BB9B00C30677 /* SafeArrayIndex.swift in Sources */,
AB76BF5A49F34BF6B75F6415 /* PullRequestUnreadCalculator.swift in Sources */,
6C232DEB2C08C95E00C003B1 /* ConfigViewModel.swift in Sources */,
6C1CB6F22C921C9900305288 /* CaseIterableDefaultsLast.swift in Sources */,
6C3DA9F12D8233510075AFBC /* systemColors.swift in Sources */,
Expand Down Expand Up @@ -696,7 +702,6 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 68JNFYH5JF;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.0;
Expand All @@ -712,7 +717,6 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 68JNFYH5JF;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.0;
Expand Down Expand Up @@ -760,6 +764,7 @@
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 68JNFYH5JF;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
Expand All @@ -783,6 +788,7 @@
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
Expand Down Expand Up @@ -826,6 +832,7 @@
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 68JNFYH5JF;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
Expand All @@ -842,6 +849,7 @@
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_VERSION = 5.0;
};
Expand All @@ -854,13 +862,15 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = PReek/PReek.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 68JNFYH5JF;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PReek/Info.plist;
Expand All @@ -869,10 +879,12 @@
INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSCameraUsageDescription = "Scan QR codes to import app configuration";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 0.5.4;
PRODUCT_BUNDLE_IDENTIFIER = "de.max-heidinger.PReek";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -882,7 +894,7 @@
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
Expand All @@ -893,13 +905,15 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = PReek/PReek.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 68JNFYH5JF;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PReek/Info.plist;
Expand All @@ -908,10 +922,12 @@
INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSCameraUsageDescription = "Scan QR codes to import app configuration";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 0.5.4;
PRODUCT_BUNDLE_IDENTIFIER = "de.max-heidinger.PReek";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -921,7 +937,7 @@
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
Expand Down
Loading