From 9c3b228d02e02aed822aab0e6f7e276c8e8c38d1 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 29 Oct 2025 22:21:11 +0800 Subject: [PATCH] :sparkles: Pagination real impl on watchOS --- ios/WatchRunner Watch App/Models/Models.swift | 6 +++ .../Services/NetworkService.swift | 15 ++++-- .../ViewModels/ActivityViewModel.swift | 35 ++++++++++--- .../Views/ActivityListView.swift | 50 +++++++++++++------ 4 files changed, 80 insertions(+), 26 deletions(-) diff --git a/ios/WatchRunner Watch App/Models/Models.swift b/ios/WatchRunner Watch App/Models/Models.swift index c091eaef..5d89c5b3 100644 --- a/ios/WatchRunner Watch App/Models/Models.swift +++ b/ios/WatchRunner Watch App/Models/Models.swift @@ -207,3 +207,9 @@ struct NotificationResponse { let total: Int let hasMore: Bool } + +struct ActivityResponse { + let activities: [SnActivity] + let hasMore: Bool + let nextCursor: String? +} diff --git a/ios/WatchRunner Watch App/Services/NetworkService.swift b/ios/WatchRunner Watch App/Services/NetworkService.swift index 286959d4..5c6a171a 100644 --- a/ios/WatchRunner Watch App/Services/NetworkService.swift +++ b/ios/WatchRunner Watch App/Services/NetworkService.swift @@ -12,7 +12,7 @@ import Foundation class NetworkService { 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 { throw URLError(.badURL) } @@ -29,17 +29,22 @@ class NetworkService { var request = URLRequest(url: components.url!) request.httpMethod = "GET" request.setValue("application/json", forHTTPHeaderField: "Accept") - + request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") let (data, _) = try await session.data(for: request) - + let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 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 { diff --git a/ios/WatchRunner Watch App/ViewModels/ActivityViewModel.swift b/ios/WatchRunner Watch App/ViewModels/ActivityViewModel.swift index 0505004f..783a708d 100644 --- a/ios/WatchRunner Watch App/ViewModels/ActivityViewModel.swift +++ b/ios/WatchRunner Watch App/ViewModels/ActivityViewModel.swift @@ -14,12 +14,15 @@ import Combine class ActivityViewModel: ObservableObject { @Published var activities: [SnActivity] = [] @Published var isLoading = false + @Published var isLoadingMore = false @Published var errorMessage: String? + @Published var hasMore = false private let networkService = NetworkService() let filter: String private var isMock = false - private var hasFetched = false // Add this + private var hasFetched = false + private var nextCursor: String? init(filter: String, mockActivities: [SnActivity]? = nil) { self.filter = filter @@ -30,21 +33,41 @@ class ActivityViewModel: ObservableObject { } func fetchActivities(token: String, serverUrl: String) async { - if isMock || hasFetched { return } // Check hasFetched + if isMock || hasFetched { return } guard !isLoading else { return } isLoading = true errorMessage = nil - hasFetched = true // Set hasFetched + hasFetched = true + nextCursor = nil do { - let fetchedActivities = try await networkService.fetchActivities(filter: filter, token: token, serverUrl: serverUrl) - self.activities = fetchedActivities + let response = try await networkService.fetchActivities(filter: filter, cursor: nil, token: token, serverUrl: serverUrl) + self.activities = response.activities + self.hasMore = response.hasMore + self.nextCursor = response.nextCursor } catch { self.errorMessage = error.localizedDescription print("[watchOS] fetchActivities failed with error: \(error)") - hasFetched = false // Reset on error + hasFetched = 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 + } } diff --git a/ios/WatchRunner Watch App/Views/ActivityListView.swift b/ios/WatchRunner Watch App/Views/ActivityListView.swift index 8a126e13..50299009 100644 --- a/ios/WatchRunner Watch App/Views/ActivityListView.swift +++ b/ios/WatchRunner Watch App/Views/ActivityListView.swift @@ -12,11 +12,11 @@ import SwiftUI struct ActivityListView: View { @StateObject private var viewModel: ActivityViewModel @EnvironmentObject var appState: AppState - + init(filter: String, mockActivities: [SnActivity]? = nil) { _viewModel = StateObject(wrappedValue: ActivityViewModel(filter: filter, mockActivities: mockActivities)) } - + var body: some View { Group { if viewModel.isLoading { @@ -33,22 +33,42 @@ struct ActivityListView: View { } else if viewModel.activities.isEmpty { Text("No activities found.") } else { - List(viewModel.activities) { activity in - switch activity.type { - case "posts.new", "posts.new.replies": - if case .post(let post) = activity.data { - NavigationLink( - destination: PostDetailView(post: post).environmentObject(appState) - ) { - PostRowView(post: post) + List { + ForEach(viewModel.activities) { activity in + switch activity.type { + case "posts.new", "posts.new.replies": + if case .post(let post) = activity.data { + NavigationLink( + destination: PostDetailView(post: post).environmentObject(appState) + ) { + 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 { - DiscoveryView(discoveryData: discoveryData) + } + if viewModel.hasMore { + 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)") } } }