添加 Solian for Apple Watch #8
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -29,17 +29,22 @@ class NetworkService {
|
|||||||
var request = URLRequest(url: components.url!)
|
var request = URLRequest(url: components.url!)
|
||||||
request.httpMethod = "GET"
|
request.httpMethod = "GET"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
|
||||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
let (data, _) = try await session.data(for: request)
|
let (data, _) = try await session.data(for: request)
|
||||||
|
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import SwiftUI
|
|||||||
struct ActivityListView: View {
|
struct ActivityListView: View {
|
||||||
@StateObject private var viewModel: ActivityViewModel
|
@StateObject private var viewModel: ActivityViewModel
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
|
|
||||||
init(filter: String, mockActivities: [SnActivity]? = nil) {
|
init(filter: String, mockActivities: [SnActivity]? = nil) {
|
||||||
_viewModel = StateObject(wrappedValue: ActivityViewModel(filter: filter, mockActivities: mockActivities))
|
_viewModel = StateObject(wrappedValue: ActivityViewModel(filter: filter, mockActivities: mockActivities))
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if viewModel.isLoading {
|
if viewModel.isLoading {
|
||||||
@@ -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