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
15 changes: 8 additions & 7 deletions Modules/Sources/WordPressShared/Utility/StringRankedSearch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,14 @@ extension StringRankedSearch {
}
}

/// Objects conforming to `StringRankedSearchable` can be searched by calling `search(query:)` on a collection of them
public protocol StringRankedSearchable {
var searchString: String { get }
}
public extension Sequence {

func search(_ query: String, minScore: Double = 0.7, using transformer: (Element) -> String) -> [Element] {
StringRankedSearch(searchTerm: query).search(in: self, minScore: minScore, input: transformer)
}

public extension Collection where Iterator.Element: StringRankedSearchable {
func search(query: String, minScore: Double = 0.7) -> [Iterator.Element] {
StringRankedSearch(searchTerm: query).search(in: self, minScore: minScore) { $0.searchString }
func search(_ query: String, minScore: Double = 0.7) -> [Element] where Element == String {
search(query, minScore: minScore, using: \.self)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ struct UserListItem: View {
@ScaledMetric(relativeTo: .headline)
var height: CGFloat = 48

@Environment(\.sizeCategory)
var sizeCategory
@Environment(\.dynamicTypeSize)
var dynamicTypeSize

private let user: DisplayUser
private let userProvider: UserDataProvider
Expand All @@ -20,13 +20,11 @@ struct UserListItem: View {

var body: some View {
NavigationLink {
UserDetailView(user: user, userProvider: userProvider, actionDispatcher: actionDispatcher)
UserDetailsView(user: user, userProvider: userProvider, actionDispatcher: actionDispatcher)
} label: {
HStack(alignment: .top) {
if !sizeCategory.isAccessibilityCategory {
if let profilePhotoUrl = user.profilePhotoUrl {
UserProfileImage(size: CGSize(width: height, height: height), url: profilePhotoUrl)
}
if !dynamicTypeSize.isAccessibilitySize {
UserProfileImage(size: height, url: user.profilePhotoUrl)
}
VStack(alignment: .leading) {
Text(user.displayName).font(.headline)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,42 @@ import SwiftUI

struct UserProfileImage: View {

private let size: CGSize
private let size: CGFloat

private let url: URL
private let url: URL?

init(size: CGSize, email: String) {
self.size = size
self.url = URL(string: "https://gravatar.com/avatar/58fc51586c9a1f9895ac70e3ca60886e?size=256")!
}

init(size: CGSize, url: URL) {
init(size: CGFloat, url: URL?) {
self.size = size
self.url = url
}

var body: some View {
AsyncImage(
url: self.url,
content: { image in
image.resizable()
.frame(width: size.height, height: size.height)
.aspectRatio(contentMode: .fit)
.clipShape(.rect(cornerRadius: 4.0))
},
placeholder: {
ProgressView().frame(width: size.height, height: size.height)
}
)
if let url {
AsyncImage(
Copy link
Contributor

Choose a reason for hiding this comment

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

Please replace this with AvatarView. AsyncImage should basically never be used.

AvatarView or AsyncImageView are currently unavailable in WordPressUI as they are part of the app target. But UserProfile* don't belong to WordPressUI as it's designed for reusable components. It should be small and should compile fast as, in the long term, more modules will depend on it.

I suggest the following changes:

  • Create a WordPressMedia framework (or similar name) with ImageDownloader – other media-related stuff could go there later too.
  • Move AvatarView and AsyncImageView to either WordPressUI or WordPressMedia
  • (If you want to keep the previews) Create a separate module for user management.

If we keep adding everything to WordPressUI, it will eventually stop working well for Xcode Previews as these tend to become unusable with 10K+ lines in a module. The only long-term option if we want to facilitate Xcode Previews is to create feature-based modules (aka app micro modules). Since we are now fully in the SPM world, it should be feasible, so it might be a good time to start.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I forgot to reply to your original comment about AsyncImage.

Shall I just move the new user management code to the app target for now? So that the AvatarView and ImageDownloader stuff (I don't think we need it for this particular feature) will be available. /cc @jkmassel

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's address it separately. I'm going to add the approval as is.

url: url,
content: { image in
image.resizable()
.frame(width: size, height: size)
.aspectRatio(contentMode: .fit)
.clipShape(.circle)
},
placeholder: {
ProgressView().frame(width: size, height: size)
}
)
} else {
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: size, height: size)
.clipShape(.circle)
}
}
}

#Preview("Default") {
UserProfileImage(size: 64, url: nil)
}

#Preview {
UserProfileImage(size: CGSize(width: 64, height: 64), email: "test@example.com")
UserProfileImage(size: 64, url: URL(string: "https://gravatar.com/avatar/58fc51586c9a1f9895ac70e3ca60886e?size=256")!)
}
25 changes: 22 additions & 3 deletions Modules/Sources/WordPressUI/Views/Users/UserProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,30 @@ open class UserManagementActionDispatcher: ObservableObject {

package struct MockUserProvider: UserDataProvider {

let dummyDataUrl = URL(string: "https://my.api.mockaroo.com/users.json?key=067c9730")!
enum Scenario {
case infinitLoading
case dummyData
case error
}

var scenario: Scenario

init(scenario: Scenario = .dummyData) {
self.scenario = scenario
}

package func fetchUsers(cachedResults: CachedUserListCallback? = nil) async throws -> [WordPressUI.DisplayUser] {
let response = try await URLSession.shared.data(from: dummyDataUrl)
return try JSONDecoder().decode([DisplayUser].self, from: response.0)
switch scenario {
case .infinitLoading:
try await Task.sleep(for: .seconds(1 * 24 * 60 * 60))
return []
case .dummyData:
let dummyDataUrl = URL(string: "https://my.api.mockaroo.com/users.json?key=067c9730")!
let response = try await URLSession.shared.data(from: dummyDataUrl)
return try JSONDecoder().decode([DisplayUser].self, from: response.0)
case .error:
throw URLError(.timedOut)
}
}

package func fetchCurrentUserCan(_ capability: String) async throws -> Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,17 @@ public struct DisplayUser: Identifiable, Codable {
profilePhotoUrl: URL(string: "https://gravatar.com/avatar/58fc51586c9a1f9895ac70e3ca60886e?size=256"),
role: "administrator",
emailAddress: "[email protected]",
websiteUrl: "",
biography: ""
websiteUrl: "https://example.com",
biography: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
)
}

extension DisplayUser: StringRankedSearchable {
public var searchString: String {

extension DisplayUser {
var searchString: String {
// These are in ranked order – the higher something is in the list, the more heavily it's weighted
[
handle,
username,
firstName,
lastName,
emailAddress,
displayName,
username,
emailAddress,
]
.compactMap { $0 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ public class UserChangePasswordViewModel: ObservableObject {
var password: String = ""

@Published
var isChangingPassword: Bool = false
private(set) var isChangingPassword: Bool = false

@Published
var error: Error? = nil
private(set) var error: Error? = nil

@Environment(\.dismiss)
var dismissAction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@ import SwiftUI
public class UserDeleteViewModel: ObservableObject {

@Published
var isFetchingOtherUsers: Bool = false
private(set) var isFetchingOtherUsers: Bool = false

@Published
var isDeletingUser: Bool = false
private(set) var isDeletingUser: Bool = false

@Published
var error: Error? = nil
private(set) var error: Error? = nil

@Published
var otherUserId: Int32 = 0

@Published
var otherUsers: [DisplayUser] = []
private(set) var otherUsers: [DisplayUser] = []

@Published
var deleteButtonIsDisabled: Bool = true
private(set) var deleteButtonIsDisabled: Bool = true

private let userProvider: UserDataProvider
private let actionDispatcher: UserManagementActionDispatcher
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ class UserDetailViewModel: ObservableObject {
private let userProvider: UserDataProvider

@Published
var currentUserCanModifyUsers: Bool = false
private(set) var currentUserCanModifyUsers: Bool = false

@Published
var isLoadingCurrentUser: Bool = false
private(set) var isLoadingCurrentUser: Bool = false

@Published
var error: Error? = nil
private(set) var error: Error? = nil

init(userProvider: UserDataProvider) {
self.userProvider = userProvider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,24 @@ class UserListViewModel: ObservableObject {
private var users: [DisplayUser] = []
private let userProvider: UserDataProvider

private var initialLoad = false

@Published
var sortedUsers: [Section] = []
private(set) var sortedUsers: [Section] = []

@Published
var error: Error? = nil
private(set) var error: Error? = nil

@Published
var isLoadingItems: Bool = true
private(set) var isLoadingItems: Bool = true

@Published
var searchTerm: String = "" {
didSet {
if searchTerm.trimmingCharacters(in: .whitespacesAndNewlines) == "" {
setSearchResults(sortUsers(users))
} else {
let searchResults = users.search(query: searchTerm)
let searchResults = users.search(searchTerm, using: \.searchString)
setSearchResults([Section(role: "Search Results", users: searchResults)])
}
}
Expand All @@ -39,6 +41,13 @@ class UserListViewModel: ObservableObject {
self.userProvider = userProvider
}

func onAppear() async {
if !initialLoad {
initialLoad = true
await fetchItems()
}
}

func fetchItems() async {
withAnimation {
isLoadingItems = true
Expand Down
Loading