✨ Pagination real impl on watchOS
This commit is contained in:
		| @@ -207,3 +207,9 @@ struct NotificationResponse { | |||||||
|     let total: Int |     let total: Int | ||||||
|     let hasMore: Bool |     let hasMore: Bool | ||||||
| } | } | ||||||
|  |  | ||||||
|  | struct ActivityResponse { | ||||||
|  |     let activities: [SnActivity] | ||||||
|  |     let hasMore: Bool | ||||||
|  |     let nextCursor: String? | ||||||
|  | } | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ import Foundation | |||||||
| class NetworkService { | class NetworkService { | ||||||
|     private let session = URLSession.shared |     private let session = URLSession.shared | ||||||
|  |  | ||||||
|     func fetchActivities(filter: String, cursor: String? = nil, token: String, serverUrl: String) async throws -> [SnActivity] { |     func fetchActivities(filter: String, cursor: String? = nil, token: String, serverUrl: String) async throws -> ActivityResponse { | ||||||
|         guard let baseURL = URL(string: serverUrl) else { |         guard let baseURL = URL(string: serverUrl) else { | ||||||
|             throw URLError(.badURL) |             throw URLError(.badURL) | ||||||
|         } |         } | ||||||
| @@ -39,7 +39,12 @@ class NetworkService { | |||||||
|         decoder.dateDecodingStrategy = .iso8601 |         decoder.dateDecodingStrategy = .iso8601 | ||||||
|         decoder.keyDecodingStrategy = .convertFromSnakeCase |         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||||
|  |  | ||||||
|         return try decoder.decode([SnActivity].self, from: data) |         let activities = try decoder.decode([SnActivity].self, from: data) | ||||||
|  |  | ||||||
|  |         let hasMore = (activities.first?.type ?? "empty") != "empty" | ||||||
|  |         let nextCursor = activities.isEmpty ? nil : activities.map { $0.createdAt }.min()?.ISO8601Format() | ||||||
|  |  | ||||||
|  |         return ActivityResponse(activities: activities, hasMore: hasMore, nextCursor: nextCursor) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     func createPost(title: String, content: String, token: String, serverUrl: String) async throws { |     func createPost(title: String, content: String, token: String, serverUrl: String) async throws { | ||||||
|   | |||||||
| @@ -14,12 +14,15 @@ import Combine | |||||||
| class ActivityViewModel: ObservableObject { | class ActivityViewModel: ObservableObject { | ||||||
|     @Published var activities: [SnActivity] = [] |     @Published var activities: [SnActivity] = [] | ||||||
|     @Published var isLoading = false |     @Published var isLoading = false | ||||||
|  |     @Published var isLoadingMore = false | ||||||
|     @Published var errorMessage: String? |     @Published var errorMessage: String? | ||||||
|  |     @Published var hasMore = false | ||||||
|  |  | ||||||
|     private let networkService = NetworkService() |     private let networkService = NetworkService() | ||||||
|     let filter: String |     let filter: String | ||||||
|     private var isMock = false |     private var isMock = false | ||||||
|     private var hasFetched = false // Add this |     private var hasFetched = false | ||||||
|  |     private var nextCursor: String? | ||||||
|  |  | ||||||
|     init(filter: String, mockActivities: [SnActivity]? = nil) { |     init(filter: String, mockActivities: [SnActivity]? = nil) { | ||||||
|         self.filter = filter |         self.filter = filter | ||||||
| @@ -30,21 +33,41 @@ class ActivityViewModel: ObservableObject { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     func fetchActivities(token: String, serverUrl: String) async { |     func fetchActivities(token: String, serverUrl: String) async { | ||||||
|         if isMock || hasFetched { return } // Check hasFetched |         if isMock || hasFetched { return } | ||||||
|         guard !isLoading else { return } |         guard !isLoading else { return } | ||||||
|         isLoading = true |         isLoading = true | ||||||
|         errorMessage = nil |         errorMessage = nil | ||||||
|         hasFetched = true // Set hasFetched |         hasFetched = true | ||||||
|  |         nextCursor = nil | ||||||
|  |  | ||||||
|         do { |         do { | ||||||
|             let fetchedActivities = try await networkService.fetchActivities(filter: filter, token: token, serverUrl: serverUrl) |             let response = try await networkService.fetchActivities(filter: filter, cursor: nil, token: token, serverUrl: serverUrl) | ||||||
|             self.activities = fetchedActivities |             self.activities = response.activities | ||||||
|  |             self.hasMore = response.hasMore | ||||||
|  |             self.nextCursor = response.nextCursor | ||||||
|         } catch { |         } catch { | ||||||
|             self.errorMessage = error.localizedDescription |             self.errorMessage = error.localizedDescription | ||||||
|             print("[watchOS] fetchActivities failed with error: \(error)") |             print("[watchOS] fetchActivities failed with error: \(error)") | ||||||
|             hasFetched = false // Reset on error |             hasFetched = false | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         isLoading = false |         isLoading = false | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     func loadMoreActivities(token: String, serverUrl: String) async { | ||||||
|  |         guard !isLoadingMore && hasMore && nextCursor != nil else { return } | ||||||
|  |         isLoadingMore = true | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             let response = try await networkService.fetchActivities(filter: filter, cursor: nextCursor, token: token, serverUrl: serverUrl) | ||||||
|  |             self.activities.append(contentsOf: response.activities) | ||||||
|  |             self.hasMore = response.hasMore | ||||||
|  |             self.nextCursor = response.nextCursor | ||||||
|  |         } catch { | ||||||
|  |             self.errorMessage = error.localizedDescription | ||||||
|  |             print("[watchOS] loadMoreActivities failed with error: \(error)") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         isLoadingMore = false | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -33,22 +33,42 @@ struct ActivityListView: View { | |||||||
|             } else if viewModel.activities.isEmpty { |             } else if viewModel.activities.isEmpty { | ||||||
|                 Text("No activities found.") |                 Text("No activities found.") | ||||||
|             } else { |             } else { | ||||||
|                 List(viewModel.activities) { activity in |                 List { | ||||||
|                     switch activity.type { |                     ForEach(viewModel.activities) { activity in | ||||||
|                     case "posts.new", "posts.new.replies": |                         switch activity.type { | ||||||
|                         if case .post(let post) = activity.data { |                         case "posts.new", "posts.new.replies": | ||||||
|                             NavigationLink( |                             if case .post(let post) = activity.data { | ||||||
|                                 destination: PostDetailView(post: post).environmentObject(appState) |                                 NavigationLink( | ||||||
|                             ) { |                                     destination: PostDetailView(post: post).environmentObject(appState) | ||||||
|                                 PostRowView(post: post) |                                 ) { | ||||||
|  |                                     PostRowView(post: post) | ||||||
|  |                                 } | ||||||
|                             } |                             } | ||||||
|  |                         case "discovery": | ||||||
|  |                             if case .discovery(let discoveryData) = activity.data { | ||||||
|  |                                 DiscoveryView(discoveryData: discoveryData) | ||||||
|  |                             } | ||||||
|  |                         default: | ||||||
|  |                             Text("Unknown activity type: \(activity.type)") | ||||||
|                         } |                         } | ||||||
|                     case "discovery": |                     } | ||||||
|                         if case .discovery(let discoveryData) = activity.data { |                     if viewModel.hasMore { | ||||||
|                             DiscoveryView(discoveryData: discoveryData) |                         if viewModel.isLoadingMore { | ||||||
|  |                             HStack { | ||||||
|  |                                 Spacer() | ||||||
|  |                                 ProgressView() | ||||||
|  |                                 Spacer() | ||||||
|  |                             } | ||||||
|  |                         } else { | ||||||
|  |                             Button("Load More") { | ||||||
|  |                                 Task { | ||||||
|  |                                     if let token = appState.token, let serverUrl = appState.serverUrl { | ||||||
|  |                                         await viewModel.loadMoreActivities(token: token, serverUrl: serverUrl) | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                             .frame(maxWidth: .infinity) | ||||||
|                         } |                         } | ||||||
|                     default: |  | ||||||
|                         Text("Unknown activity type: \(activity.type)") |  | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user