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
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": []
}
}
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ WordPress-iOS uses a modular architecture with the main app and separate Swift p
- Use strict access control modifiers where possible
- Use four spaces (not tabs)

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

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

2 changes: 1 addition & 1 deletion Modules/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,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: "30dadcab01a980eb16976340c1e9e8a9527ddc05" // 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
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ public struct DataViewPaginatedForEach<Response: DataViewPaginatedResponseProtoc
DataViewPagingFooterView(.loading)
} else if response.error != nil {
DataViewPagingFooterView(.failure)
.onRetry {
response.loadMore()
}
.onRetry { response.loadMore() }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public protocol DataViewPaginatedResponseProtocol: ObservableObject {
/// 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 = 0
@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
Expand All @@ -26,11 +26,11 @@ public final class DataViewPaginatedResponse<Element: Identifiable, PageIndex>:
/// Result of a paginated load operation.
public struct Page {
public let items: [Element]
public let total: Int
public let total: Int?
public let hasMore: Bool
public let nextPage: PageIndex?

public init(items: [Element], total: Int, hasMore: Bool, nextPage: PageIndex?) {
public init(items: [Element], total: Int? = nil, hasMore: Bool, nextPage: PageIndex?) {
self.items = items
self.total = total
self.hasMore = hasMore
Expand Down Expand Up @@ -115,6 +115,8 @@ public final class DataViewPaginatedResponse<Element: Identifiable, PageIndex>:
return
}
items.remove(at: index)
total -= 1
if let total {
self.total = total - 1
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import SwiftUI

/// A generic search view that works with DataViewPaginatedResponse.
/// Provides search functionality with debouncing, loading states, and error handling.
public struct DataViewSearchView<Response: DataViewPaginatedResponseProtocol, Content: View>: View {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Extracted from SubscribersView.

/// The search text to monitor for changes
let searchText: String

/// The async function to perform the search
let search: () async throws -> Response

/// Content builder for the paginated list
let content: (Response) -> Content

/// Delay in milliseconds before executing search (default: 500ms)
let debounceDelay: UInt64

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nitpick: debounceDelayInMilliseconds (or use a type like Duration) to be extra clear about the time unit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I switched to the Duration type – much clearer.


@State private var response: Response?
@State private var error: Error?

public init(
searchText: String,
debounceDelay: UInt64 = 500,
search: @escaping () async throws -> Response,
@ViewBuilder content: @escaping (Response) -> Content
) {
self.searchText = searchText
self.debounceDelay = debounceDelay
self.search = search
self.content = content
}

public var body: some View {
List {
if let response {
content(response)
} else if error == nil {
DataViewPagingFooterView(.loading)
}
}
.listStyle(.plain)
.overlay {
if let response, response.items.isEmpty {
EmptyStateView.search()
} else if let error {
EmptyStateView.failure(error: error)
}
}
.task(id: searchText) {
error = nil
do {
try await Task.sleep(for: .milliseconds(debounceDelay))
let response = try await search()
guard !Task.isCancelled else { return }
self.response = response
} catch {
guard !Task.isCancelled else { return }
self.response = nil
self.error = error
}
}
}
}
25 changes: 25 additions & 0 deletions Modules/Sources/WordPressUI/Views/EmptyStateView.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import SwiftUI
import WordPressShared

public struct EmptyStateView<Label: View, Description: View, Actions: View>: View {
@ViewBuilder let label: () -> Label
Expand Down Expand Up @@ -73,6 +74,30 @@ private struct EmptyStateViewLabelStyle: LabelStyle {
}
}

extension EmptyStateView where Label == SwiftUI.Label<Text, Image>, Description == Text?, Actions == EmptyView {
public static func search() -> Self {
EmptyStateView(
AppLocalizedString("emptyStateView.noSearchResult.title", value: "No Results", comment: "Shared empty state view"),
systemImage: "magnifyingglass",
description: AppLocalizedString("emptyStateView.noSearchResult.description", value: "Try a new search", comment: "Shared empty state view")
)
}
}

extension EmptyStateView where Label == SwiftUI.Label<Text, Image>, Description == Text?, Actions == Button<Text>? {
public static func failure(error: Error, onRetry: (() -> Void)? = nil) -> Self {
EmptyStateView {
Label(AppLocalizedString("shared.error.generic", value: "Something went wrong", comment: "A generic error message"), systemImage: "exclamationmark.circle")
} description: {
Text(error.localizedDescription)
} actions: {
if let onRetry {
Button(AppLocalizedString("shared.button.retry", value: "Retry", comment: "A shared button title used in different contexts"), action: onRetry)
}
}
}
}

#Preview("Standard") {
EmptyStateView("You don't have any tags", systemImage: "magnifyingglass", description: "Tags created here can be easily added to new posts")
}
Expand Down
27 changes: 0 additions & 27 deletions WordPress/Classes/Extensions/EmptyStateView+Extensions.swift

This file was deleted.

3 changes: 2 additions & 1 deletion WordPress/Classes/Utility/SharedStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ enum SharedStrings {
static let copyLink = NSLocalizedString("shared.button.copyLink", value: "Copy Link", comment: "A shared button title used in different contexts")
static let `continue` = NSLocalizedString("shared.button.continue", value: "Continue", comment: "A shared button title used in different contexts")
static let undo = NSLocalizedString("shared.button.undo", value: "Undo", comment: "A shared button title used in different contexts")
static let clear = NSLocalizedString("shared.button.clear", value: "Clear", comment: "A shared button title used in different contexts")
}

enum Misc {
Expand All @@ -36,7 +37,7 @@ enum SharedStrings {
}

enum Error {
static let generic = NSLocalizedString("shared.error.geneirc", value: "Something went wrong", comment: "A generic error message")
static let generic = NSLocalizedString("shared.error.generic", value: "Something went wrong", comment: "A generic error message")
static let refreshFailed = NSLocalizedString("shared.error.failiedToReloadData", value: "Failed to update data", comment: "A generic error title indicating that a screen failed to fetch the latest data")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ class JetpackActivityLogViewController: BaseActivityListViewController {
guard let siteRef = JetpackSiteRef(blog: blog) else {
return nil
}

let isFreeWPCom = blog.isHostedAtWPcom && !blog.hasPaidPlan
self.init(site: siteRef, store: StoreContainer.shared.activity, isFreeWPCom: isFreeWPCom)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import SwiftUI
import WordPressUI

struct ActivityLogRowView: View {
let viewModel: ActivityLogRowViewModel

var body: some View {
HStack(alignment: .center, spacing: 12) {
icon

VStack(alignment: .leading, spacing: 4) {
HStack {
Text(viewModel.subtitle)
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
Spacer()
Text(viewModel.time)
.font(.caption2)
.foregroundColor(.secondary)
}

Text(viewModel.title)
.font(.subheadline)
.lineLimit(2)

if let actor = viewModel.actor {
HStack(spacing: 6) {
avatar
HStack(spacing: 4) {
Text(actor)
.font(.footnote)
.foregroundColor(.secondary)
if let role = viewModel.actorRole {
Text("·")
.font(.footnote)
.foregroundColor(.secondary)
Text(role)
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
.padding(.top, 4)
}
}
}
}

private var avatar: some View {
Group {
if let avatarURL = viewModel.actorAvatarURL {
AvatarView(style: .single(avatarURL), diameter: 16)
} else if viewModel.actor?.lowercased() == "jetpack" {
Image("icon-jetpack")
.resizable()
} else {
Circle()
.fill(Color(.secondarySystemBackground))
.overlay(
Text((viewModel.actor ?? "").prefix(1).uppercased())
.font(.system(size: 9, weight: .medium))
.foregroundColor(.secondary)
)
}
}
.frame(width: 16, height: 16)
}

private var icon: some View {
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(viewModel.tintColor.opacity(0.15))
.frame(width: 36, height: 36)

if let iconImage = viewModel.icon {
Image(uiImage: iconImage)
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
.foregroundColor(viewModel.tintColor)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Foundation
import SwiftUI
import UIKit
import WordPressKit
import WordPressUI
import FormattableContentKit

struct ActivityLogRowViewModel: Identifiable {
let id: String
let actorAvatarURL: URL?
var actor: String?
var actorRole: String?
let title: String
let subtitle: String
let date: Date
let time: String
let icon: UIImage?
let tintColor: Color
let activity: Activity

init(activity: Activity) {
self.activity = activity
self.id = activity.activityID
self.actorAvatarURL = activity.actor.flatMap { URL(string: $0.avatarURL) }
if let actor = activity.actor {
self.actor = actor.displayName
if !actor.role.isEmpty {
self.actorRole = actor.role.localizedCapitalized
}
}
self.date = activity.published
self.time = activity.published.formatted(date: .omitted, time: .shortened)
self.title = activity.text
self.subtitle = activity.summary.localizedCapitalized

self.icon = WPStyleGuide.ActivityStyleGuide.getIconForActivity(activity)
self.tintColor = Color(WPStyleGuide.ActivityStyleGuide.getColorByActivityStatus(activity))
}
}
Loading