Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify User Service #23818

Draft
wants to merge 1 commit into
base: trunk
Choose a base branch
from
Draft
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
4 changes: 2 additions & 2 deletions Modules/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ let package = Package(
.package(url: "https://github.com/wordpress-mobile/WordPressKit-iOS", branch: "task/reader-discover"),
.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.
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-swift-20240813"),
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20241116"),
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", revision: "6cc307e7fc24910697be5f71b7d70f465a9c0f63"),
.package(url: "https://github.com/Automattic/color-studio", branch: "trunk"),
],
Expand Down Expand Up @@ -161,7 +161,7 @@ enum XcodeSupport {
.product(name: "ZendeskSupportSDK", package: "support_sdk_ios"),
.product(name: "ZIPFoundation", package: "ZIPFoundation"),
.product(name: "WordPressAPI", package: "wordpress-rs"),
.product(name: "ColorStudio", package: "color-studio"),
.product(name: "ColorStudio", package: "color-studio")
]),
.xcodeTarget("XcodeTarget_WordPressTests", dependencies: testDependencies + [
"WordPressShared",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ struct UserListItem: View {
}
}
}

#Preview {
UserListItem(user: DisplayUser.MockUser, userService: MockUserProvider())
}
//
//#Preview {
// UserListItem(user: DisplayUser.MockUser, userService: MockUserProvider())
//}
109 changes: 49 additions & 60 deletions Modules/Sources/WordPressUI/Views/Users/UserProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,70 +2,59 @@ import Foundation
import Combine

public protocol UserServiceProtocol: Actor {
var users: [DisplayUser]? { get }
nonisolated var usersUpdates: AsyncStream<[DisplayUser]> { get }

func fetchUsers() async throws -> [DisplayUser]
func fetchPaginatedUsers() -> AsyncThrowingStream<[DisplayUser], Error>

func isCurrentUserCapableOf(_ capability: String) async throws -> Bool

func setNewPassword(id: Int32, newPassword: String) async throws

func deleteUser(id: Int32, reassigningPostsTo newUserId: Int32) async throws
}

package actor MockUserProvider: UserServiceProtocol {

enum Scenario {
case infinitLoading
case dummyData
case error
}

var scenario: Scenario

package nonisolated let usersUpdates: AsyncStream<[DisplayUser]>
private let usersUpdatesContinuation: AsyncStream<[DisplayUser]>.Continuation

package private(set) var users: [DisplayUser]? {
didSet {
if let users {
usersUpdatesContinuation.yield(users)
}
}
}

init(scenario: Scenario = .dummyData) {
self.scenario = scenario
(usersUpdates, usersUpdatesContinuation) = AsyncStream<[DisplayUser]>.makeStream()
}

package func fetchUsers() async throws -> [DisplayUser] {
switch scenario {
case .infinitLoading:
// Do nothing
try await Task.sleep(for: .seconds(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)
let users = try JSONDecoder().decode([DisplayUser].self, from: response.0)
self.users = users
return users
case .error:
throw URLError(.timedOut)
}
}

package func isCurrentUserCapableOf(_ capability: String) async throws -> Bool {
true
}

package func setNewPassword(id: Int32, newPassword: String) async throws {
// Not used in Preview
}

package func deleteUser(id: Int32, reassigningPostsTo newUserId: Int32) async throws {
// Not used in Preview
}
}
//
//package actor MockUserProvider: UserServiceProtocol {
//
// enum Scenario {
// case infinitLoading
// case dummyData
// case error
// }
//
// var scenario: Scenario
//
// init(scenario: Scenario = .dummyData) {
// self.scenario = scenario
// }
//
// public func fetchUsers() async throws -> any AsyncSequence {
// [DisplayUser]().async
// }
//
//
// package func fetchUsers() async throws -> [DisplayUser] {
// switch scenario {
// case .infinitLoading:
// // Do nothing
// try await Task.sleep(for: .seconds(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)
// let users = try JSONDecoder().decode([DisplayUser].self, from: response.0)
// return users
// case .error:
// throw URLError(.timedOut)
// }
// }
//
// package func isCurrentUserCapableOf(_ capability: String) async throws -> Bool {
// true
// }
//
// package func setNewPassword(id: Int32, newPassword: String) async throws {
// // Not used in Preview
// }
//
// package func deleteUser(id: Int32, reassigningPostsTo newUserId: Int32) async throws {
// // Not used in Preview
// }
//}
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ public class UserDeleteViewModel: ObservableObject {
}

do {
let users = try await userService.fetchUsers()
self.otherUsers = users
let users = await userService.fetchPaginatedUsers()
self.otherUsers = try await users
.reduce(into: [DisplayUser](), { partialResult, users in
partialResult.append(contentsOf: users)
})
.filter { $0.id != self.user.id } // Don't allow re-assigning to yourself
.sorted(using: KeyPathComparator(\.username))
} catch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ class UserListViewModel: ObservableObject {
}

/// The initial set of users fetched by `fetchItems`
private var users: [DisplayUser] = [] {
private var users: [Int32: DisplayUser] = [:] {
didSet {
sortedUsers = self.sortUsers(users)
sortedUsers = self.sortUsers(Array(users.values))
}
}
private var updateUsersTask: Task<Void, Never>?
Expand All @@ -36,7 +36,7 @@ class UserListViewModel: ObservableObject {
if searchTerm.trimmingCharacters(in: .whitespacesAndNewlines) == "" {
setSearchResults(sortUsers(users))
} else {
let searchResults = users.search(searchTerm, using: \.searchString)
let searchResults = users.values.search(searchTerm, using: \.searchString)
setSearchResults([Section(role: "Search Results", users: searchResults)])
}
}
Expand All @@ -51,41 +51,40 @@ class UserListViewModel: ObservableObject {
}

func onAppear() async {
if updateUsersTask == nil {
updateUsersTask = Task { @MainActor [weak self, usersUpdates = userService.usersUpdates] in
for await users in usersUpdates {
guard let self else { break }

self.users = users
}
}
}

if !initialLoad {
initialLoad = true
await fetchItems()
}
}

private func fetchItems() async {
isLoadingItems = true
defer { isLoadingItems = false }

_ = try? await userService.fetchUsers()
do {
for try await page in await userService.fetchPaginatedUsers() {
for user in page {
self.users[user.id] = user
}

// Show results after the first page has loaded
isLoadingItems = false
}
} catch {
self.error = error
}
}

@Sendable
func refreshItems() async {
_ = try? await userService.fetchUsers()
await fetchItems()
}

func setUsers(_ newValue: [DisplayUser]) {
withAnimation {
self.users = newValue
self.sortedUsers = sortUsers(newValue)
isLoadingItems = false
}
}
// func setUsers(_ newValue: [DisplayUser]) {
// withAnimation {
// self.users = newValue
// self.sortedUsers = sortUsers(newValue)
// isLoadingItems = false
// }
// }

func setSearchResults(_ newValue: [Section]) {
withAnimation {
Expand All @@ -98,4 +97,10 @@ class UserListViewModel: ObservableObject {
.map { Section(role: $0.key, users: $0.value.sorted(by: { $0.username < $1.username })) }
.sorted { $0.role < $1.role }
}

private func sortUsers(_ users: [Int32: DisplayUser]) -> [Section] {
Dictionary(grouping: users.values, by: { $0.role })
.map { Section(role: $0.key, users: $0.value.sorted(by: { $0.username < $1.username })) }
.sorted { $0.role < $1.role }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -380,8 +380,8 @@ private extension String {
}
}

#Preview {
NavigationStack {
UserDetailsView(user: DisplayUser.MockUser, userService: MockUserProvider())
}
}
//#Preview {
// NavigationStack {
// UserDetailsView(user: DisplayUser.MockUser, userService: MockUserProvider())
// }
//}
36 changes: 18 additions & 18 deletions Modules/Sources/WordPressUI/Views/Users/Views/UserListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,21 @@ public struct UserListView: View {
)
}
}

#Preview("Loading") {
NavigationView {
UserListView(userService: MockUserProvider())
}
}

#Preview("Error") {
NavigationView {
UserListView(userService: MockUserProvider(scenario: .error))
}
}

#Preview("List") {
NavigationView {
UserListView(userService: MockUserProvider(scenario: .dummyData))
}
}
//
//#Preview("Loading") {
// NavigationView {
// UserListView(userService: MockUserProvider())
// }
//}
//
//#Preview("Error") {
// NavigationView {
// UserListView(userService: MockUserProvider(scenario: .error))
// }
//}
//
//#Preview("List") {
// NavigationView {
// UserListView(userService: MockUserProvider(scenario: .dummyData))
// }
//}
4 changes: 2 additions & 2 deletions WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Automattic/wordpress-rs",
"state" : {
"branch" : "alpha-swift-20240813",
"revision" : "b51c560d83a61917eb3361441e65779b71fb96ce"
"branch" : "alpha-20241116",
"revision" : "1249ae77fcea2e836b7878a1b1cffdf6c1080256"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion WordPress/Classes/Networking/WordPressClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ actor WordPressClient {
try await self.api.plugins.create(params: PluginCreateParams(
slug: "InstallJetpack",
status: .active
))
)).data
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import WordPressAPI
}

private func fetchTokens(forUserId userId: Int32) async throws -> [ApplicationPasswordWithEditContext] {
try await apiClient.api.applicationPasswords.listWithEditContext(userId: userId)
try await apiClient.api.applicationPasswords.listWithEditContext(userId: userId).data
}
}

Expand Down
Loading