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)
 | 
						|
    }
 | 
						|
}
 |