199 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			199 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
| 
 | |
| //
 | |
| //  NotificationView.swift
 | |
| //  WatchRunner Watch App
 | |
| //
 | |
| //  Created by LittleSheep on 2025/10/29.
 | |
| //
 | |
| 
 | |
| import SwiftUI
 | |
| import Combine
 | |
| 
 | |
| @MainActor
 | |
| class NotificationViewModel: ObservableObject {
 | |
|     @Published var notifications = [SnNotification]()
 | |
|     @Published var isLoading = false
 | |
|     @Published var isLoadingMore = false
 | |
|     @Published var errorMessage: String?
 | |
|     @Published var hasMore = false
 | |
| 
 | |
|     private let networkService = NetworkService()
 | |
|     private var hasFetched = false
 | |
|     private var offset = 0
 | |
|     private let pageSize = 20
 | |
| 
 | |
|     func fetchNotifications(token: String, serverUrl: String) async {
 | |
|         if hasFetched { return }
 | |
|         guard !isLoading else { return }
 | |
|         isLoading = true
 | |
|         errorMessage = nil
 | |
|         hasFetched = true
 | |
|         offset = 0
 | |
| 
 | |
|         do {
 | |
|             let response = try await networkService.fetchNotifications(offset: offset, take: pageSize, token: token, serverUrl: serverUrl)
 | |
|             self.notifications = response.notifications
 | |
|             self.hasMore = response.hasMore
 | |
|             offset += response.notifications.count
 | |
|         } catch {
 | |
|             self.errorMessage = error.localizedDescription
 | |
|             print("[watchOS] fetchNotifications failed with error: \(error)")
 | |
|             hasFetched = false
 | |
|         }
 | |
| 
 | |
|         isLoading = false
 | |
|     }
 | |
| 
 | |
|     func loadMoreNotifications(token: String, serverUrl: String) async {
 | |
|         guard !isLoadingMore && hasMore else { return }
 | |
|         isLoadingMore = true
 | |
| 
 | |
|         do {
 | |
|             let response = try await networkService.fetchNotifications(offset: offset, take: pageSize, token: token, serverUrl: serverUrl)
 | |
|             self.notifications.append(contentsOf: response.notifications)
 | |
|             self.hasMore = response.hasMore
 | |
|             offset += response.notifications.count
 | |
|         } catch {
 | |
|             self.errorMessage = error.localizedDescription
 | |
|             print("[watchOS] loadMoreNotifications failed with error: \(error)")
 | |
|         }
 | |
| 
 | |
|         isLoadingMore = false
 | |
|     }
 | |
| }
 | |
| 
 | |
| struct NotificationView: View {
 | |
|     @EnvironmentObject var appState: AppState
 | |
|     @StateObject private var viewModel = NotificationViewModel()
 | |
| 
 | |
|     var body: some View {
 | |
|         Group {
 | |
|             if viewModel.isLoading {
 | |
|                 ProgressView()
 | |
|             } else if let errorMessage = viewModel.errorMessage {
 | |
|                 VStack {
 | |
|                     Text("Error")
 | |
|                         .font(.headline)
 | |
|                     Text(errorMessage)
 | |
|                         .font(.caption)
 | |
|                     Button("Retry") {
 | |
|                         Task {
 | |
|                             if let token = appState.token, let serverUrl = appState.serverUrl {
 | |
|                                 await viewModel.fetchNotifications(token: token, serverUrl: serverUrl)
 | |
|                             }
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|                 .padding()
 | |
|             } else if viewModel.notifications.isEmpty {
 | |
|                 Text("No notifications")
 | |
|             } else {
 | |
|                 List {
 | |
|                     ForEach(viewModel.notifications) { notification in
 | |
|                         NavigationLink(destination: NotificationDetailView(notification: notification)) {
 | |
|                             VStack(alignment: .leading, spacing: 4) {
 | |
|                                 HStack {
 | |
|                                     Text(notification.title)
 | |
|                                         .font(.headline)
 | |
|                                     Spacer()
 | |
|                                     if notification.viewedAt == nil {
 | |
|                                         Circle()
 | |
|                                             .fill(Color.blue)
 | |
|                                             .frame(width: 8, height: 8)
 | |
|                                     }
 | |
|                                 }
 | |
|                                 if !notification.subtitle.isEmpty {
 | |
|                                     Text(notification.subtitle)
 | |
|                                         .font(.subheadline)
 | |
|                                         .foregroundColor(.secondary)
 | |
|                                 }
 | |
|                                 if notification.content.count > 100 {
 | |
|                                     Text(notification.content.prefix(100) + "...")
 | |
|                                         .font(.caption)
 | |
|                                         .foregroundColor(.gray)
 | |
|                                         .lineLimit(2)
 | |
|                                 } else {
 | |
|                                     Text(notification.content)
 | |
|                                         .font(.caption)
 | |
|                                         .foregroundColor(.gray)
 | |
|                                         .lineLimit(2)
 | |
|                                 }
 | |
|                                 Text(notification.createdAt, style: .relative)
 | |
|                                     .font(.caption2)
 | |
|                                     .foregroundColor(.gray)
 | |
|                             }
 | |
|                             .padding(.vertical, 8)
 | |
|                         }
 | |
|                     }
 | |
|                     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.loadMoreNotifications(token: token, serverUrl: serverUrl)
 | |
|                                     }
 | |
|                                 }
 | |
|                             }
 | |
|                             .frame(maxWidth: .infinity)
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         .onAppear {
 | |
|             if appState.isReady, let token = appState.token, let serverUrl = appState.serverUrl {
 | |
|                 Task.detached {
 | |
|                     await viewModel.fetchNotifications(token: token, serverUrl: serverUrl)
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         .navigationTitle("Notifications")
 | |
|         .navigationBarTitleDisplayMode(.inline)
 | |
|     }
 | |
| }
 | |
| 
 | |
| struct NotificationDetailView: View {
 | |
|     let notification: SnNotification
 | |
| 
 | |
|     var body: some View {
 | |
|         ScrollView {
 | |
|             VStack(alignment: .leading, spacing: 16) {
 | |
|                 Text(notification.title)
 | |
|                     .font(.headline)
 | |
|                 
 | |
|                 if !notification.subtitle.isEmpty {
 | |
|                     Text(notification.subtitle)
 | |
|                         .font(.subheadline)
 | |
|                         .foregroundColor(.secondary)
 | |
|                 }
 | |
|                 
 | |
|                 Text(notification.content)
 | |
|                     .font(.body)
 | |
|                 
 | |
|                 HStack {
 | |
|                     Text(notification.createdAt, style: .date)
 | |
|                     Text("·")
 | |
|                     Text(notification.createdAt, style: .time)
 | |
|                 }
 | |
|                 .font(.caption)
 | |
|                 .foregroundColor(.gray)
 | |
|                 
 | |
|                 if notification.viewedAt == nil {
 | |
|                     Text("Unread")
 | |
|                         .font(.caption)
 | |
|                         .foregroundColor(.blue)
 | |
|                 }
 | |
|             }
 | |
|             .padding()
 | |
|         }
 | |
|         .navigationTitle("Notification")
 | |
|         .navigationBarTitleDisplayMode(.inline)
 | |
|     }
 | |
| }
 |