Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
c44a4c4
Add PaginatedResponse
kean Jun 16, 2025
9ab1930
Refactor PaginatedResponse to DataViewPaginatedResponse
kean Jun 16, 2025
48d56ee
Fix deleteItem to update total count
kean Jun 16, 2025
96824f9
Rename LoadMoreFooterView
kean Jun 16, 2025
4bd0903
Add DataViewPaginatedForEach component
kean Jun 16, 2025
86903ee
Add DataViewPaginatedResponseProtocol
kean Jun 16, 2025
a2f5eba
Refactor subscribers to use DataViewPaginatedResponse
kean Jun 16, 2025
077a2de
Refactor subscribers service creation and move deleteSubscriber exten…
kean Jun 16, 2025
d882b55
Fix typo in Subscriber
kean Jun 16, 2025
9157192
Fix formatting
kean Jun 16, 2025
c07e5bc
Fix typos and address comments
kean Jun 17, 2025
f99f95c
Merge branch 'trunk' into task/activity-logs-swiftui
kean Jun 18, 2025
96f9cfd
Implement new Jetpack Activity Logs screen
kean Jun 18, 2025
e61dbf3
Extract DataViewPaginatedResponse (#24592)
kean Jun 19, 2025
28352d3
Implement Jetpack Activity Logs screen (list) (#24596)
kean Jun 19, 2025
79dd91c
Fix SwiftLint warnings
kean Jun 19, 2025
2b0dba8
Add Miniature app
kean Jun 19, 2025
3063455
Add XcodeTarget_App as a dependency
kean Jun 19, 2025
6f94f9e
Configure xcconfig (just use Reader)
kean Jun 19, 2025
bb8ce34
Add Miniature App (#24598)
kean Jun 19, 2025
e220263
Add initial ActivityLogDetailsView
kean Jun 19, 2025
4526022
Add ActivityLogDetailsView to Miniature target
kean Jun 19, 2025
6b1ed24
Extract Activity icon methods from WPStyleGuide to Activity extension
kean Jun 19, 2025
8d3e826
Remove rewindStatus parameter from ActivityLogDetailsView
kean Jun 19, 2025
b0be437
Dev
kean Jun 19, 2025
fe65050
Add formattedContent
kean Jun 19, 2025
4567c26
Move the code to the main target
kean Jun 19, 2025
149a2fe
Extract reusable ActivityActorAvatarView
kean Jun 19, 2025
a312022
Remove unused code and integrate ActivityLogDetailsView
kean Jun 19, 2025
c02e30c
Fix SwiftLint warnings
kean Jun 19, 2025
8f7cced
Add ActivityLogsDetailsView (#24599)
kean Jun 19, 2025
6decdf8
Add initial restore/download backup code
kean Jun 19, 2025
cfdd76d
Remove JetpackSiteRef usages
kean Jun 19, 2025
b2e3c1c
Add multisite handling
kean Jun 19, 2025
e57f31c
Remove the new screens and use the existing flows
kean Jun 19, 2025
f6624ce
Add analytics
kean Jun 19, 2025
cbf1b14
Cleanup
kean Jun 19, 2025
d3d4bb6
Add missing analytics
kean Jun 19, 2025
053cd42
Cleanup
kean Jun 19, 2025
16c4c6e
Fix an issue with not all restorable acitivies shown in backups
kean Jun 19, 2025
d7bdf75
Fix an issue with restore/download flow layout
kean Jun 19, 2025
2ece5e9
Show restore checkpoint in section
kean Jun 19, 2025
4af44a5
Extract reusable CardView and InfoRow components from SubscribersDeta…
kean Jun 19, 2025
ea903d7
Cleanup
kean Jun 19, 2025
0e67f85
Add Restore and Download Backup buttons to ActivityLogDetailsView (#2…
kean Jun 19, 2025
5e60653
Add rewindable
kean Jun 19, 2025
31f862d
Move restore buttons higher
kean Jun 19, 2025
d764d47
Add date filtering back
kean Jun 19, 2025
1398936
Use ActivityLogsView to show Backups (#24601)
kean Jun 19, 2025
54d3c39
Add Backup tracking
kean Jun 20, 2025
7df96bd
Fix clear background in restore flows
kean Jun 20, 2025
6b72d76
Rework how we do polling
kean Jun 20, 2025
76c354a
Rework how we manage backup statuss
kean Jun 20, 2025
64d1585
Revert hasBackup change
kean Jun 20, 2025
5d41e39
Update tests
kean Jun 20, 2025
36ea180
Add Downloadable Backup Section to Backups list (#24602)
kean Jun 20, 2025
90d741b
Update release notes
kean Jun 20, 2025
0e89ba3
Remove unused code
kean Jun 20, 2025
5384608
Cleanup
kean Jun 20, 2025
9325a27
Remove Application from list
kean Jun 20, 2025
44972b9
Update Dashboard to use new ActivityLogsView (#24603)
kean Jun 20, 2025
1c8dc6f
Remove BackupListViewController and use ActivityLogsViewController in…
kean Jun 20, 2025
10e878a
Remove JetpackActivityLogViewController and use ActivityLogsViewContr…
kean Jun 20, 2025
cedbc2c
Remove dataViews feature flag
kean Jun 20, 2025
cc1719c
Create separate BackupsViewController and remove isBackupMode from Ac…
kean Jun 20, 2025
33b40bf
Activity Logs: Code Removal – Part 1 (#24604)
kean Jun 20, 2025
e376f30
Remove ActivityListViewModelTests
kean Jun 20, 2025
8957b9f
Remove ActivityTypeSelectorViewController
kean Jun 20, 2025
07a1f60
Remove ActivityListViewModel
kean Jun 20, 2025
b8dd674
Remove BaseActivityListViewController
kean Jun 20, 2025
e8022ec
Fix compilation
kean Jun 20, 2025
6e17279
Activity Logs: Code Removal – Part 2 (#24605)
kean Jun 20, 2025
23aa07a
Remove RewindStatusRow
kean Jun 20, 2025
bb07b08
Remove RewindStatusTableViewCell
kean Jun 20, 2025
fe68785
Remove ActivityListRow
kean Jun 20, 2025
43d84b6
Remove ActivityTableViewCell
kean Jun 20, 2025
525b647
Activity Logs: Code Removal – Part 3 (#24606)
kean Jun 20, 2025
733f01a
Remove CalendarViewController
kean Jun 20, 2025
c0ac5bb
Remove JTAppleCalendar dependency
kean Jun 20, 2025
ffda5bf
Activity Logs: Code Removal – Part 4 (#24607)
kean Jun 20, 2025
0ceeaa1
Remove FilterBarView
kean Jun 20, 2025
94809f0
Remove FilterChipButton
kean Jun 20, 2025
4e976a9
Remove ActivityDetailViewController
kean Jun 20, 2025
f4fbc82
Activity Logs: Code Removal – Part 5 (#24608)
kean Jun 20, 2025
cbfbad3
Remove ActivityStore usages
kean Jun 20, 2025
8420f31
Remove ActivityStoreTests
kean Jun 20, 2025
d77e1d4
Fix how isAwaitingCredentials works
kean Jun 20, 2025
c4c37d2
Remove ActivityStore
kean Jun 20, 2025
55cfee4
Activity Logs: Code Removal – Part 6 (#24609)
kean Jun 20, 2025
450d149
Integrate ActivityContentRouter and FormattableActivity
kean Jun 20, 2025
9746eaf
Integrate ActivityContentRouter and FormattableActivity (#24610)
kean Jun 20, 2025
e45c028
Update filter icon
kean Jun 20, 2025
f491206
Update filters icon
kean Jun 20, 2025
b4ee7ea
Update icons for filters in DataViews (#24611)
kean Jun 20, 2025
c930476
Remove WPStyleGuide+Activity and fix an issue with ActivityFormattabl…
kean Jun 20, 2025
0c04776
Fix layout of ActivityFormattableContentView
kean Jun 20, 2025
4ab4e55
Add placeholders for empty fields
kean Jun 20, 2025
7b3ccf9
Remove build instructions from CLAUDE.md
kean Jun 20, 2025
348bfb4
Add more instructions for CLAUDE
kean Jun 20, 2025
4fa01ef
Handle a scenario where logs are from existing user
kean Jun 20, 2025
84ad407
Remove obsolete tests
kean Jun 20, 2025
156d2a9
Update UI tests
kean Jun 20, 2025
521ff21
Fix release build
kean Jun 20, 2025
e636424
Use Duration
kean Jun 23, 2025
7bf0c14
Merge branch 'trunk' into logs-merge-test
kean Jun 24, 2025
76ffd88
Simplify CardView
kean Jun 24, 2025
10dde0e
Add TODO for iOS 17
kean Jun 24, 2025
b4a18ae
Move state change to onAppear
kean Jun 24, 2025
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
17 changes: 17 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"permissions": {
"allow": [
"Bash(cat:*)",
"Bash(ls:*)",
"Bash(rg:*)",
"Bash(find:*)",
"Bash(grep:*)",
"Bash(head:*)",
"Bash(tail:*)",
"Bash(wc:*)",
"Bash(tree:*)",
"Bash(git:log,status,diff,branch)",
],
"deny": []
}
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ DerivedData
*.hmap
*.xcscmblueprint

# Claude
.claude/settings.local.json

# Windows
Thumbs.db
ehthumbs.db
Expand Down
17 changes: 4 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,6 @@ WordPress for iOS is the official mobile app for WordPress that lets users creat

Minimum requires iOS version is iOS 16.

## Common Development Commands

### Build & Dependencies
- `rake build` - Build the app
- `xcodebuild -scheme <target> -destination 'platform=iOS Simulator,name=iPhone 16' | bundle exec xcpretty` build targets from `Modules/`.

### Testing
- `rake test` - Run all tests

### Code Quality
- `rake lint` - Check for SwiftLint errors

## High-Level Architecture

### Project Structure
Expand Down Expand Up @@ -54,8 +42,11 @@ WordPress-iOS uses a modular architecture with the main app and separate Swift p
- Follow Swift API Design Guidelines
- Use strict access control modifiers where possible
- Use four spaces (not tabs)
- Follow the standard formatting practices enforced by SwiftLint
- Don't create `body` for `View` that are too long
- Use semantics text sizes like `.headline`

### Development Workflow
## Development Workflow
- Branch from `trunk` (main branch)
- PR target should be `trunk`
- When writing commit messages, never include references to Claude
13 changes: 2 additions & 11 deletions Modules/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions Modules/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ let package = Package(
.package(url: "https://github.com/erikdoe/ocmock", revision: "2c0bfd373289f4a7716db5d6db471640f91a6507"),
.package(url: "https://github.com/johnxnguyen/Down", branch: "master"),
.package(url: "https://github.com/kaishin/Gifu", from: "3.4.1"),
.package(url: "https://github.com/patchthecode/JTAppleCalendar", from: "8.0.5"),
.package(url: "https://github.com/Quick/Nimble", from: "10.0.0"),
.package(url: "https://github.com/scinfu/SwiftSoup", exact: "2.7.5"),
.package(url: "https://github.com/squarefrog/UIDeviceIdentifier", from: "2.3.0"),
Expand All @@ -50,7 +49,7 @@ let package = Package(
.package(url: "https://github.com/wordpress-mobile/NSURL-IDN", revision: "b34794c9a3f32312e1593d4a3d120572afa0d010"),
.package(
url: "https://github.com/wordpress-mobile/WordPressKit-iOS",
revision: "cc7fd8a7ea609fc139e7b9d9f53b12c51002ddf4" // see wpios-edition branch
revision: "ae3961ce89ac0c43a90e88d4963a04aa92008443" // see wpios-edition branch
),
.package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"),
// We can't use wordpress-rs branches nor commits here. Only tags work.
Expand Down Expand Up @@ -300,7 +299,6 @@ enum XcodeSupport {
.product(name: "GravatarUI", package: "Gravatar-SDK-iOS"),
.product(name: "Gridicons", package: "Gridicons-iOS"),
.product(name: "GutenbergKit", package: "GutenbergKit"),
.product(name: "JTAppleCalendar", package: "JTAppleCalendar"),
.product(name: "Lottie", package: "lottie-ios"),
.product(name: "MediaEditor", package: "MediaEditor-iOS"),
.product(name: "NSObject-SafeExpectations", package: "NSObject-SafeExpectations"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public enum BuildSettingsEnvironment: Sendable {

private extension ProcessInfo {
var isXcodePreview: Bool {
environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" || environment["WP_USE_PREVIEW_ENVIRONMENT"] == "1"
}

var isTesting: Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,16 @@ import ScreenObject
import XCTest

public class ActivityLogScreen: ScreenObject {
private let dateRangeButtonGetter: (XCUIApplication) -> XCUIElement = {
$0.buttons["Date Range"].firstMatch
}

private let activityTypeButtonGetter: (XCUIApplication) -> XCUIElement = {
$0.buttons["Activity Type"].firstMatch
}

var activityTypeButton: XCUIElement { activityTypeButtonGetter(app) }
var dateRangeButton: XCUIElement { dateRangeButtonGetter(app) }

// Timeout duration to overwrite value defined in XCUITestHelpers
var duration: TimeInterval = 10.0

public init(app: XCUIApplication = XCUIApplication()) throws {
try super.init(
expectedElementGetters: [ dateRangeButtonGetter, activityTypeButtonGetter ],
app: app
)
}

public static func isLoaded() -> Bool {
(try? ActivityLogScreen().isLoaded) ?? false
try super.init {
$0.collectionViews["activity_logs_list"].firstMatch
}
}

@discardableResult
public func verifyActivityLogScreen(hasActivityPartial activityTitle: String) -> Self {
XCTAssertTrue(
app.staticTexts.matching(NSPredicate(format: "label CONTAINS[c] %@", activityTitle)).firstMatch.waitForIsHittable(timeout: duration),
app.staticTexts.matching(NSPredicate(format: "label CONTAINS[c] %@", activityTitle)).firstMatch.waitForIsHittable(timeout: 10),
"Activity Log Screen: \"\(activityTitle)\" activity not displayed.")
return self
}
Expand Down
54 changes: 54 additions & 0 deletions Modules/Sources/WordPressUI/Views/CardView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import SwiftUI

/// A reusable card view component that provides a consistent container style
/// with optional title and customizable content.
public struct CardView<Content: View>: View {
let title: String?
@ViewBuilder let content: () -> Content

public init(_ title: String? = nil, @ViewBuilder content: @escaping () -> Content) {
self.title = title
self.content = content
}

public var body: some View {
VStack(alignment: .leading, spacing: 16) {
if let title {
Text(title.uppercased())
.font(.caption)
.foregroundStyle(.secondary)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this Group necessary? Does moving the .frame call to VStack and deleting this Group look as expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it works the same – updated.

content()
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color(.separator), lineWidth: 0.5)
)
}
}

#Preview("With Title") {
CardView("Section Title") {
VStack(alignment: .leading, spacing: 12) {
Text("Card Content")
Text("More content here")
.foregroundStyle(.secondary)
}
}
.padding()
}

#Preview("Without Title") {
CardView {
HStack {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
Text("Featured Item")
Spacer()
}
}
.padding()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import SwiftUI

/// A view that displays paginated data using ForEach with automatic loading triggers.
public struct DataViewPaginatedForEach<Response: DataViewPaginatedResponseProtocol, Content: View>: View {
@ObservedObject private var response: Response
private let content: (Response.Element) -> Content

/// Creates a paginated ForEach view.
///
/// - Parameters:
/// - response: The paginated response handler that manages the data.
/// - content: A view builder that creates the content for each item.
public init(
response: Response,
@ViewBuilder content: @escaping (Response.Element) -> Content
) {
self.response = response
self.content = content
}

public var body: some View {
ForEach(response.items) { item in
content(item)
.onAppear {
response.onRowAppeared(item)
}
}
if response.isLoading {
DataViewPagingFooterView(.loading)
} else if response.error != nil {
DataViewPagingFooterView(.failure)
.onRetry { response.loadMore() }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import Foundation
import SwiftUI

@MainActor
public protocol DataViewPaginatedResponseProtocol: ObservableObject {
associatedtype Element: Identifiable

var items: [Element] { get }
var isLoading: Bool { get }
var error: Error? { get }

func onRowAppeared(_ item: Element)
@discardableResult func loadMore() -> Task<Void, Error>?
}

/// A generic paginated response handler that manages loading items with flexible pagination.
/// This class is designed to be used in the UI in conjunction with `PaginatedForEach`.
@MainActor
public final class DataViewPaginatedResponse<Element: Identifiable, PageIndex>: DataViewPaginatedResponseProtocol {
@Published public private(set) var total: Int?
@Published public private(set) var items: [Element] = []
@Published public private(set) var hasMore = true
@Published public private(set) var isLoading = false
@Published public private(set) var error: Error?

/// Result of a paginated load operation.
public struct Page {
public let items: [Element]
public let total: Int?
public let hasMore: Bool
public let nextPage: PageIndex?

public init(items: [Element], total: Int? = nil, hasMore: Bool, nextPage: PageIndex?) {
self.items = items
self.total = total
self.hasMore = hasMore
self.nextPage = nextPage
}
}

public var isEmpty: Bool { items.isEmpty }

private var nextPage: PageIndex?
private let loadPage: (PageIndex?) async throws -> Page

/// Creates a new paginated response handler.
///
/// - Parameter loadPage: A closure that loads items using pagination.
/// - Parameter pageIndex: The page index to load (nil for initial load).
/// - Returns: A PaginatedResult containing the items, total count, whether more pages exist, and next page index.
/// - Throws: Any error from the initial page load.
public init(loadPage: @escaping (PageIndex?) async throws -> Page) async throws {
self.loadPage = loadPage

let response = try await loadPage(nil)
didLoad(response)
}

/// Loads the next page of items.
///
/// This method will do nothing if:
/// - There are no more pages to load
/// - A page is currently being loaded
@discardableResult
public func loadMore() -> Task<Void, Error>? {
guard hasMore && !isLoading else {
return nil
}
error = nil
isLoading = true
return Task {
defer { isLoading = false }
do {
let response = try await loadPage(nextPage)
didLoad(response)
} catch {
self.error = error
throw error
}
}
}

private func didLoad(_ response: Page) {
total = response.total
nextPage = response.nextPage
hasMore = response.hasMore

let existingIDs = Set(items.map(\.id))
let newItems = response.items.filter {
!existingIDs.contains($0.id)
}
items += newItems
}

/// Triggers loading more items when a row appears.
///
/// Call this method when a row becomes visible. If the row is within the last 10 items
/// and there's no current error, it will trigger loading the next page.
///
/// - Parameter row: The row that appeared.
public func onRowAppeared(_ row: Element) {
guard items.suffix(10).contains(where: { $0.id == row.id }) else {
return
}
if error == nil {
loadMore()
}
}

/// Removes an item with the specified ID from the loaded items.
///
/// - Parameter id: The ID of the item to remove.
public func deleteItem(withID id: Element.ID) {
guard let index = items.firstIndex(where: { $0.id == id }) else {
return
}
items.remove(at: index)
if let total {
self.total = total - 1
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import SwiftUI

public struct LoadMoreFooterView: View {
public struct DataViewPagingFooterView: View {
public enum State {
case loading
case failure
Expand All @@ -13,7 +13,7 @@ public struct LoadMoreFooterView: View {
self.state = state
}

public func onRetry(_ closure: (() -> Void)?) -> LoadMoreFooterView {
public func onRetry(_ closure: (() -> Void)?) -> DataViewPagingFooterView {
var copy = self
copy.onRetry = closure
return copy
Expand Down
Loading