Skip to content

Commit bcb3e07

Browse files
authored
Feature: Jetpack Activity Logs (#24597)
* Add PaginatedResponse * Refactor PaginatedResponse to DataViewPaginatedResponse - Rename PaginatedResponse to DataViewPaginatedResponse for better naming consistency - Move files from Pagination to DataView directory - Update documentation to focus on UI usage with PaginatedForEach - Add comprehensive unit tests for DataViewPaginatedResponse * Fix deleteItem to update total count - Update deleteItem method to properly decrement total when removing items - Update tests to verify total count is correctly maintained after deletion * Rename LoadMoreFooterView * Add DataViewPaginatedForEach component * Add DataViewPaginatedResponseProtocol * Refactor subscribers to use DataViewPaginatedResponse Replace custom SubscribersPaginatedResponse with generic DataViewPaginatedResponse system. Add notification-based subscriber deletion updates and improve pagination handling with DataViewPaginatedForEach component. * Refactor subscribers service creation and move deleteSubscriber extension - Rename getSubscribersService() to maketSubscribersService() to better reflect factory pattern - Move deleteSubscriber extension from SubsriberDetailsViewModel to dedicated SubscribersServiceRemote+Extensions file - Update all callers to use the renamed method * Fix typo in Subscriber * Fix formatting * Fix typos and address comments * Implement new Jetpack Activity Logs screen * Fix SwiftLint warnings * Add Miniature app * Add XcodeTarget_App as a dependency * Configure xcconfig (just use Reader) * Add initial ActivityLogDetailsView * Add ActivityLogDetailsView to Miniature target - Implement modern SwiftUI version of Activity Detail screen - Use card-based design system matching SubscriberDetailsView - Add proper localization with NSLocalizedString and reverse-DNS keys - Use WPStyleGuide for consistent icon and color handling - Include activity stats visualization for backup events - Add restore and download backup actions with confirmation dialogs - Integrate with existing Activity model and RewindStatus - Update ContentView with navigation links for testing - Configure MiniatureApp with proper navigation and theming * Extract Activity icon methods from WPStyleGuide to Activity extension - Create Activity+Icon.swift with gridiconType, icon, and statusColor properties - Update all references to use new Activity extension methods - Deprecate old WPStyleGuide methods with proper deprecation messages - Remove stringToGridiconTypeMapping from WPStyleGuide - Fix linter warnings for trailing whitespace and shorthand optional binding * Remove rewindStatus parameter from ActivityLogDetailsView - Remove rewindStatus parameter from ActivityLogDetailsView initializer - Remove rewindStatus checks in ActivityActionsCard - Update preview providers to not pass rewindStatus - Update ContentView to not create mock RewindStatus instances - Remove warning card for multisite installations - Simplify restore button enable/disable logic * Dev * Add formattedContent * Move the code to the main target * Extract reusable ActivityActorAvatarView * Remove unused code and integrate ActivityLogDetailsView * Fix SwiftLint warnings * Add initial restore/download backup code * Remove JetpackSiteRef usages * Add multisite handling * Remove the new screens and use the existing flows * Add analytics * Cleanup * Add missing analytics * Cleanup * Fix an issue with not all restorable acitivies shown in backups * Fix an issue with restore/download flow layout * Show restore checkpoint in section * Extract reusable CardView and InfoRow components from SubscribersDetailsVIew * Cleanup * Add rewindable * Move restore buttons higher * Add date filtering back * Add Backup tracking * Fix clear background in restore flows * Rework how we do polling * Rework how we manage backup statuss * Revert hasBackup change * Update tests * Update release notes * Remove unused code * Cleanup * Remove Application from list * Remove BackupListViewController and use ActivityLogsViewController instead - Delete BackupListViewController.swift and its extension - Update showBackup() to use ActivityLogsViewController with isBackupMode - Remove BackupListViewController reference from ActivityDetailViewController - Implement displayBackupWithSiteID using ActivityLogsViewController - Add WPAnalytics tracking to ContentCoordinator backup display * Remove JetpackActivityLogViewController and use ActivityLogsViewController - Delete JetpackActivityLogViewController.swift - Update showActivity() methods to use ActivityLogsViewController directly - Update ActivityDetailViewController to check for ActivityLogsViewController - Simplify DashboardActivityLogCardCell to use ActivityLogsViewController * Remove dataViews feature flag - Remove dataViews case from FeatureFlag enum - Remove dataViews from enabled property switch statement - Remove dataViews description The feature flag was already removed from usage in previous commits where we removed JetpackActivityLogViewController and BackupListViewController. * Create separate BackupsViewController and remove isBackupMode from ActivityLogsViewController - Create new BackupsViewController and BackupsView that reuses ActivityLogsView - Remove isBackupMode parameter from ActivityLogsViewController - Update all usages to use BackupsViewController for backups - Update ActivityDetailViewController to recognize BackupsViewController presenter - Keep ActivityLogsView and ActivityLogsViewModel unchanged as implementation details * Remove ActivityListViewModelTests * Remove ActivityTypeSelectorViewController * Remove ActivityListViewModel * Remove BaseActivityListViewController * Fix compilation * Remove RewindStatusRow * Remove RewindStatusTableViewCell * Remove ActivityListRow * Remove ActivityTableViewCell * Remove CalendarViewController * Remove JTAppleCalendar dependency * Remove FilterBarView * Remove FilterChipButton * Remove ActivityDetailViewController * Remove ActivityStore usages * Remove ActivityStoreTests * Fix how isAwaitingCredentials works * Remove ActivityStore * Integrate ActivityContentRouter and FormattableActivity * Update filter icon * Update filters icon * Remove WPStyleGuide+Activity and fix an issue with ActivityFormattableContentView layout * Fix layout of ActivityFormattableContentView * Add placeholders for empty fields * Remove build instructions from CLAUDE.md * Add more instructions for CLAUDE * Handle a scenario where logs are from existing user * Remove obsolete tests * Update UI tests * Fix release build * Use Duration * Simplify CardView * Add TODO for iOS 17 * Move state change to onAppear
1 parent 906aa9b commit bcb3e07

File tree

91 files changed

+3673
-4980
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+3673
-4980
lines changed

.claude/settings.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(cat:*)",
5+
"Bash(ls:*)",
6+
"Bash(rg:*)",
7+
"Bash(find:*)",
8+
"Bash(grep:*)",
9+
"Bash(head:*)",
10+
"Bash(tail:*)",
11+
"Bash(wc:*)",
12+
"Bash(tree:*)",
13+
"Bash(git:log,status,diff,branch)",
14+
],
15+
"deny": []
16+
}
17+
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ DerivedData
2525
*.hmap
2626
*.xcscmblueprint
2727

28+
# Claude
29+
.claude/settings.local.json
30+
2831
# Windows
2932
Thumbs.db
3033
ehthumbs.db

CLAUDE.md

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,6 @@ WordPress for iOS is the official mobile app for WordPress that lets users creat
88

99
Minimum requires iOS version is iOS 16.
1010

11-
## Common Development Commands
12-
13-
### Build & Dependencies
14-
- `rake build` - Build the app
15-
- `xcodebuild -scheme <target> -destination 'platform=iOS Simulator,name=iPhone 16' | bundle exec xcpretty` build targets from `Modules/`.
16-
17-
### Testing
18-
- `rake test` - Run all tests
19-
20-
### Code Quality
21-
- `rake lint` - Check for SwiftLint errors
22-
2311
## High-Level Architecture
2412

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

58-
### Development Workflow
49+
## Development Workflow
5950
- Branch from `trunk` (main branch)
6051
- PR target should be `trunk`
6152
- When writing commit messages, never include references to Claude

Modules/Package.resolved

Lines changed: 2 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/Package.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ let package = Package(
3737
.package(url: "https://github.com/erikdoe/ocmock", revision: "2c0bfd373289f4a7716db5d6db471640f91a6507"),
3838
.package(url: "https://github.com/johnxnguyen/Down", branch: "master"),
3939
.package(url: "https://github.com/kaishin/Gifu", from: "3.4.1"),
40-
.package(url: "https://github.com/patchthecode/JTAppleCalendar", from: "8.0.5"),
4140
.package(url: "https://github.com/Quick/Nimble", from: "10.0.0"),
4241
.package(url: "https://github.com/scinfu/SwiftSoup", exact: "2.7.5"),
4342
.package(url: "https://github.com/squarefrog/UIDeviceIdentifier", from: "2.3.0"),
@@ -50,7 +49,7 @@ let package = Package(
5049
.package(url: "https://github.com/wordpress-mobile/NSURL-IDN", revision: "b34794c9a3f32312e1593d4a3d120572afa0d010"),
5150
.package(
5251
url: "https://github.com/wordpress-mobile/WordPressKit-iOS",
53-
revision: "cc7fd8a7ea609fc139e7b9d9f53b12c51002ddf4" // see wpios-edition branch
52+
revision: "ae3961ce89ac0c43a90e88d4963a04aa92008443" // see wpios-edition branch
5453
),
5554
.package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"),
5655
// We can't use wordpress-rs branches nor commits here. Only tags work.
@@ -300,7 +299,6 @@ enum XcodeSupport {
300299
.product(name: "GravatarUI", package: "Gravatar-SDK-iOS"),
301300
.product(name: "Gridicons", package: "Gridicons-iOS"),
302301
.product(name: "GutenbergKit", package: "GutenbergKit"),
303-
.product(name: "JTAppleCalendar", package: "JTAppleCalendar"),
304302
.product(name: "Lottie", package: "lottie-ios"),
305303
.product(name: "MediaEditor", package: "MediaEditor-iOS"),
306304
.product(name: "NSObject-SafeExpectations", package: "NSObject-SafeExpectations"),

Modules/Sources/BuildSettingsKit/BuildSettingsEnvironment.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public enum BuildSettingsEnvironment: Sendable {
2121

2222
private extension ProcessInfo {
2323
var isXcodePreview: Bool {
24-
environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
24+
environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" || environment["WP_USE_PREVIEW_ENVIRONMENT"] == "1"
2525
}
2626

2727
var isTesting: Bool {

Modules/Sources/UITestsFoundation/Screens/ActivityLogScreen.swift

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,16 @@ import ScreenObject
22
import XCTest
33

44
public class ActivityLogScreen: ScreenObject {
5-
private let dateRangeButtonGetter: (XCUIApplication) -> XCUIElement = {
6-
$0.buttons["Date Range"].firstMatch
7-
}
8-
9-
private let activityTypeButtonGetter: (XCUIApplication) -> XCUIElement = {
10-
$0.buttons["Activity Type"].firstMatch
11-
}
12-
13-
var activityTypeButton: XCUIElement { activityTypeButtonGetter(app) }
14-
var dateRangeButton: XCUIElement { dateRangeButtonGetter(app) }
15-
16-
// Timeout duration to overwrite value defined in XCUITestHelpers
17-
var duration: TimeInterval = 10.0
18-
195
public init(app: XCUIApplication = XCUIApplication()) throws {
20-
try super.init(
21-
expectedElementGetters: [ dateRangeButtonGetter, activityTypeButtonGetter ],
22-
app: app
23-
)
24-
}
25-
26-
public static func isLoaded() -> Bool {
27-
(try? ActivityLogScreen().isLoaded) ?? false
6+
try super.init {
7+
$0.collectionViews["activity_logs_list"].firstMatch
8+
}
289
}
2910

3011
@discardableResult
3112
public func verifyActivityLogScreen(hasActivityPartial activityTitle: String) -> Self {
3213
XCTAssertTrue(
33-
app.staticTexts.matching(NSPredicate(format: "label CONTAINS[c] %@", activityTitle)).firstMatch.waitForIsHittable(timeout: duration),
14+
app.staticTexts.matching(NSPredicate(format: "label CONTAINS[c] %@", activityTitle)).firstMatch.waitForIsHittable(timeout: 10),
3415
"Activity Log Screen: \"\(activityTitle)\" activity not displayed.")
3516
return self
3617
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import SwiftUI
2+
3+
/// A reusable card view component that provides a consistent container style
4+
/// with optional title and customizable content.
5+
public struct CardView<Content: View>: View {
6+
let title: String?
7+
@ViewBuilder let content: () -> Content
8+
9+
public init(_ title: String? = nil, @ViewBuilder content: @escaping () -> Content) {
10+
self.title = title
11+
self.content = content
12+
}
13+
14+
public var body: some View {
15+
VStack(alignment: .leading, spacing: 16) {
16+
if let title {
17+
Text(title.uppercased())
18+
.font(.caption)
19+
.foregroundStyle(.secondary)
20+
}
21+
content()
22+
}
23+
.frame(maxWidth: .infinity, alignment: .leading)
24+
.padding()
25+
.clipShape(RoundedRectangle(cornerRadius: 8))
26+
.overlay(
27+
RoundedRectangle(cornerRadius: 8)
28+
.stroke(Color(.separator), lineWidth: 0.5)
29+
)
30+
}
31+
}
32+
33+
#Preview("With Title") {
34+
CardView("Section Title") {
35+
VStack(alignment: .leading, spacing: 12) {
36+
Text("Card Content")
37+
Text("More content here")
38+
.foregroundStyle(.secondary)
39+
}
40+
}
41+
.padding()
42+
}
43+
44+
#Preview("Without Title") {
45+
CardView {
46+
HStack {
47+
Image(systemName: "star.fill")
48+
.foregroundStyle(.yellow)
49+
Text("Featured Item")
50+
Spacer()
51+
}
52+
}
53+
.padding()
54+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import SwiftUI
2+
3+
/// A view that displays paginated data using ForEach with automatic loading triggers.
4+
public struct DataViewPaginatedForEach<Response: DataViewPaginatedResponseProtocol, Content: View>: View {
5+
@ObservedObject private var response: Response
6+
private let content: (Response.Element) -> Content
7+
8+
/// Creates a paginated ForEach view.
9+
///
10+
/// - Parameters:
11+
/// - response: The paginated response handler that manages the data.
12+
/// - content: A view builder that creates the content for each item.
13+
public init(
14+
response: Response,
15+
@ViewBuilder content: @escaping (Response.Element) -> Content
16+
) {
17+
self.response = response
18+
self.content = content
19+
}
20+
21+
public var body: some View {
22+
ForEach(response.items) { item in
23+
content(item)
24+
.onAppear {
25+
response.onRowAppeared(item)
26+
}
27+
}
28+
if response.isLoading {
29+
DataViewPagingFooterView(.loading)
30+
} else if response.error != nil {
31+
DataViewPagingFooterView(.failure)
32+
.onRetry { response.loadMore() }
33+
}
34+
}
35+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import Foundation
2+
import SwiftUI
3+
4+
@MainActor
5+
public protocol DataViewPaginatedResponseProtocol: ObservableObject {
6+
associatedtype Element: Identifiable
7+
8+
var items: [Element] { get }
9+
var isLoading: Bool { get }
10+
var error: Error? { get }
11+
12+
func onRowAppeared(_ item: Element)
13+
@discardableResult func loadMore() -> Task<Void, Error>?
14+
}
15+
16+
/// A generic paginated response handler that manages loading items with flexible pagination.
17+
/// This class is designed to be used in the UI in conjunction with `PaginatedForEach`.
18+
@MainActor
19+
public final class DataViewPaginatedResponse<Element: Identifiable, PageIndex>: DataViewPaginatedResponseProtocol {
20+
@Published public private(set) var total: Int?
21+
@Published public private(set) var items: [Element] = []
22+
@Published public private(set) var hasMore = true
23+
@Published public private(set) var isLoading = false
24+
@Published public private(set) var error: Error?
25+
26+
/// Result of a paginated load operation.
27+
public struct Page {
28+
public let items: [Element]
29+
public let total: Int?
30+
public let hasMore: Bool
31+
public let nextPage: PageIndex?
32+
33+
public init(items: [Element], total: Int? = nil, hasMore: Bool, nextPage: PageIndex?) {
34+
self.items = items
35+
self.total = total
36+
self.hasMore = hasMore
37+
self.nextPage = nextPage
38+
}
39+
}
40+
41+
public var isEmpty: Bool { items.isEmpty }
42+
43+
private var nextPage: PageIndex?
44+
private let loadPage: (PageIndex?) async throws -> Page
45+
46+
/// Creates a new paginated response handler.
47+
///
48+
/// - Parameter loadPage: A closure that loads items using pagination.
49+
/// - Parameter pageIndex: The page index to load (nil for initial load).
50+
/// - Returns: A PaginatedResult containing the items, total count, whether more pages exist, and next page index.
51+
/// - Throws: Any error from the initial page load.
52+
public init(loadPage: @escaping (PageIndex?) async throws -> Page) async throws {
53+
self.loadPage = loadPage
54+
55+
let response = try await loadPage(nil)
56+
didLoad(response)
57+
}
58+
59+
/// Loads the next page of items.
60+
///
61+
/// This method will do nothing if:
62+
/// - There are no more pages to load
63+
/// - A page is currently being loaded
64+
@discardableResult
65+
public func loadMore() -> Task<Void, Error>? {
66+
guard hasMore && !isLoading else {
67+
return nil
68+
}
69+
error = nil
70+
isLoading = true
71+
return Task {
72+
defer { isLoading = false }
73+
do {
74+
let response = try await loadPage(nextPage)
75+
didLoad(response)
76+
} catch {
77+
self.error = error
78+
throw error
79+
}
80+
}
81+
}
82+
83+
private func didLoad(_ response: Page) {
84+
total = response.total
85+
nextPage = response.nextPage
86+
hasMore = response.hasMore
87+
88+
let existingIDs = Set(items.map(\.id))
89+
let newItems = response.items.filter {
90+
!existingIDs.contains($0.id)
91+
}
92+
items += newItems
93+
}
94+
95+
/// Triggers loading more items when a row appears.
96+
///
97+
/// Call this method when a row becomes visible. If the row is within the last 10 items
98+
/// and there's no current error, it will trigger loading the next page.
99+
///
100+
/// - Parameter row: The row that appeared.
101+
public func onRowAppeared(_ row: Element) {
102+
guard items.suffix(10).contains(where: { $0.id == row.id }) else {
103+
return
104+
}
105+
if error == nil {
106+
loadMore()
107+
}
108+
}
109+
110+
/// Removes an item with the specified ID from the loaded items.
111+
///
112+
/// - Parameter id: The ID of the item to remove.
113+
public func deleteItem(withID id: Element.ID) {
114+
guard let index = items.firstIndex(where: { $0.id == id }) else {
115+
return
116+
}
117+
items.remove(at: index)
118+
if let total {
119+
self.total = total - 1
120+
}
121+
}
122+
}

0 commit comments

Comments
 (0)