diff --git a/apple/OmnivoreKit/Sources/App/Views/AI/DigestConfigView.swift b/apple/OmnivoreKit/Sources/App/Views/AI/DigestConfigView.swift index 7536b0073..b9acf8784 100644 --- a/apple/OmnivoreKit/Sources/App/Views/AI/DigestConfigView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/AI/DigestConfigView.swift @@ -18,6 +18,9 @@ public class DigestConfigViewModel: ObservableObject { @Published var presentedLibraryItem: String? @Published var presentWebContainer = false + @Published var notificationsEnabled = false + @Published var isTryingToEnableNotifications = false + @AppStorage(UserDefaultKey.lastVisitedDigestId.rawValue) var lastVisitedDigestId = "" func checkAlreadyOptedIn(dataService: DataService) async { @@ -46,6 +49,25 @@ public class DigestConfigViewModel: ObservableObject { } isLoading = false } + + func tryEnableNotifications(dataService: DataService) { + isTryingToEnableNotifications = true + UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { granted, _ in + DispatchQueue.main.async { + self.notificationsEnabled = granted + UserDefaults.standard.set(granted, forKey: UserDefaultKey.notificationsEnabled.rawValue) + + Task { + if let savedToken = UserDefaults.standard.string(forKey: UserDefaultKey.firebasePushToken.rawValue) { + _ = try? await dataService.syncDeviceToken( + deviceTokenOperation: DeviceTokenOperation.addToken(token: savedToken)) + } + NotificationCenter.default.post(name: Notification.Name("ReconfigurePushNotifications"), object: nil) + self.isTryingToEnableNotifications = false + } + } + } + } } @available(iOS 17.0, *) @@ -87,8 +109,26 @@ struct DigestConfigView: View { } .padding(.top, 50) } else if viewModel.digestEnabled { - Text("You've been added to the AI Digest demo. You first issue should be ready soon.") - .padding(15) + VStack(spacing: 15) { + Spacer() + Text(""" + You've been added to the AI Digest demo. Your first issue should be ready soon. + When a new digest is ready the icon in the library header will change color. + You can close this window now. + """) + if !viewModel.notificationsEnabled { + if viewModel.isTryingToEnableNotifications { + ProgressView() + } else { + Button(action: { + viewModel.tryEnableNotifications(dataService: dataService) + }, label: { Text("Notify me when its ready") }) + .buttonStyle(RoundedRectButtonStyle(color: Color.blue, textColor: Color.white)) + } + } + Spacer() + } + .padding(20) } else if viewModel.isIneligible { Text("To enable digest you need to have saved at least ten library items and have two active subscriptions.") .padding(15) diff --git a/apple/OmnivoreKit/Sources/App/Views/AI/FullScreenDigestView.swift b/apple/OmnivoreKit/Sources/App/Views/AI/FullScreenDigestView.swift index dbaf2bd2a..fc92a1db9 100644 --- a/apple/OmnivoreKit/Sources/App/Views/AI/FullScreenDigestView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/AI/FullScreenDigestView.swift @@ -180,7 +180,6 @@ struct FullScreenDigestView: View { } }, label: { Text("Try again") }) .buttonStyle(RoundedRectButtonStyle(color: Color.blue, textColor: Color.white)) - Spacer() } } else { @@ -252,7 +251,8 @@ struct FullScreenDigestView: View { ChapterView( startTime: chapterData.time, skipIndex: chapterData.start, - chapter: chapter + chapter: chapter, + isCurrentChapter: currentChapter ) .onTapGesture { audioController.seek(toIdx: chapterData.start) @@ -264,8 +264,9 @@ struct FullScreenDigestView: View { viewModel.presentedLibraryItem = chapter.id viewModel.presentWebContainer = true } + .contentShape(Rectangle()) .background( - currentChapter ? Color.themeLabelBackground.opacity(0.6) : Color.clear + currentChapter ? Color.blue.opacity(0.2) : Color.clear ) .cornerRadius(5) } @@ -285,7 +286,6 @@ struct FullScreenDigestView: View { } .padding(15) .background(Color.themeLabelBackground.opacity(0.6)) - .cornerRadius(5) } Spacer(minLength: 60) @@ -300,27 +300,6 @@ struct FullScreenDigestView: View { RatingWidget() Spacer(minLength: 60) } -// -// VStack(alignment: .leading, spacing: 20) { -// Text("If you didn't like today's digest or would like another one you can create another one. The process takes a few minutes") -// Button(action: { -// Task { -// await viewModel.refreshDigest(dataService: dataService) -// } -// }, label: { -// Text("Create new digest") -// .font(Font.system(size: 13, weight: .medium)) -// .padding(.horizontal, 8) -// .padding(.vertical, 5) -// .tint(Color.blue) -// .background(Color.themeLabelBackground) -// .cornerRadius(5) -// }) -// } -// .padding(15) -// .background(Color.themeLabelBackground.opacity(0.6)) -// .cornerRadius(5) -// }.contentMargins(10, for: .scrollContent) Spacer() @@ -340,41 +319,70 @@ struct ChapterView: View { let startTime: String let skipIndex: Int let chapter: DigestChapter + let isCurrentChapter: Bool var body: some View { - HStack(spacing: 15) { + HStack { + VStack(spacing: 5) { + HStack { + Text(startTime) + .padding(4) + .padding(.horizontal, 4) + .foregroundColor(.blue) + .font(Font.system(size: 13)) + .background(Color.themeLabelBackground.opacity(0.6)) + .cornerRadius(5) + + if let author = chapter.author { + Text(author) + .font(Font.system(size: 14)) + .foregroundColor(Color.themeLibraryItemSubtle) + .lineLimit(1) + .padding(.trailing, 10) + } + + Spacer() + } + Text(chapter.title) + .foregroundColor(isCurrentChapter ? .primary :Color.themeLibraryItemSubtle.opacity(0.60)) + .font(Font.system(size: 14)) + .lineLimit(4) + .frame(maxWidth: .infinity, alignment: .topLeading) + } + .padding(.leading, 10) + Spacer() if let thumbnail = chapter.thumbnail, let thumbnailURL = URL(string: thumbnail) { AsyncImage(url: thumbnailURL) { image in image .resizable() .aspectRatio(contentMode: .fill) - .frame(width: 90, height: 50) + .frame(width: 65, height: 65) + .cornerRadius(5) .clipped() - } placeholder: { - Rectangle() - .foregroundColor(.gray) - .frame(width: 90, height: 50) - } - .cornerRadius(8) - } else { - Rectangle() - .foregroundColor(.gray) - .frame(width: 90, height: 50) - .cornerRadius(8) - } - VStack(alignment: .leading) { - (Text(startTime) - .foregroundColor(.blue) - .font(.caption) + .padding(.trailing, 10) - + - Text(" - " + chapter.title) - .foregroundColor(.primary) - .font(.caption)) - .lineLimit(2) + } placeholder: { + Rectangle() + .foregroundColor(.clear) + .frame(width: 65, height: 65) + .cornerRadius(5) + .padding(.trailing, 10) + } + } else { + ZStack { + Rectangle() + .foregroundColor(.thLibrarySeparator) + .frame(width: 65, height: 65) + .cornerRadius(5) + Image(systemName: "photo") + .foregroundColor(.thBorderColor) + .frame(width: 65, height: 65) + .cornerRadius(5) + } + .padding(.trailing, 10) } - Spacer() } + .frame(maxWidth: .infinity, alignment: .topLeading) .padding(.leading, 4) .padding(.vertical, 15) } @@ -424,8 +432,6 @@ struct PreviewItemView: View { } .padding(.top, 10) Text(viewModel.item.title) - // .font(.body) - // .fontWeight(.semibold) .font(Font.system(size: 18, weight: .semibold)) .frame(maxWidth: .infinity, alignment: .topLeading) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/AI/AITasks.swift b/apple/OmnivoreKit/Sources/Services/DataService/AI/AITasks.swift index 91d877499..b529929f4 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/AI/AITasks.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/AI/AITasks.swift @@ -26,11 +26,13 @@ public struct DigestChapter: Codable { public let id: String public let url: String public let wordCount: Double + public let author: String? public let thumbnail: String? - public init(title: String, id: String, url: String, wordCount: Double, thumbnail: String?) { + public init(title: String, id: String, url: String, wordCount: Double, author: String?, thumbnail: String?) { self.title = title self.id = id self.url = url + self.author = author self.wordCount = wordCount self.thumbnail = thumbnail } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateUserPersonalization.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateUserPersonalization.swift new file mode 100644 index 000000000..4f6f98e4d --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateUserPersonalization.swift @@ -0,0 +1,70 @@ +import CoreData +import Foundation +import Models +import SwiftGraphQL + +struct DigestConfig { + let channels: [String] +} + +struct UserPersonalization { + let digestConfig: DigestConfig +} + +let digestConfigSelection = Selection.DigestConfig { + DigestConfig(channels: (try? $0.channels())?.compactMap { $0 } ?? []) +} + +let channelSelection = Selection.UserPersonalization { + try $0.digestConfig(selection: digestConfigSelection.nullable)?.channels ?? [] +} + + +public extension DataService { + func setupUserDigestConfig() async throws { + enum MutationResult { + case success(channels: [String]) + case error(errorMessage: String) + } + + let selection = Selection { + try $0.on( + setUserPersonalizationError: .init { + .error(errorMessage: try $0.errorCodes().first?.rawValue ?? "Unknown Error") + }, + setUserPersonalizationSuccess: .init { .success(channels: try $0.updatedUserPersonalization(selection: channelSelection)) } + ) + } + + let mutation = Selection.Mutation { + try $0.setUserPersonalization( + input: InputObjects.SetUserPersonalizationInput( + digestConfig: OptionalArgument( + InputObjects.DigestConfigInput(channels: OptionalArgument([OptionalArgument("email")])) + ) + ), + selection: selection + ) + } + + let path = appEnvironment.graphqlPath + let headers = networker.defaultHeaders + + return try await withCheckedThrowingContinuation { continuation in + send(mutation, to: path, headers: headers) { queryResult in + guard let payload = try? queryResult.get() else { + print("network error setting up user digest config") + continuation.resume(throwing: BasicError.message(messageText: "network error")) + return + } + switch payload.data { + case .success: + continuation.resume() + case let .error(errorMessage: errorMessage): + continuation.resume(throwing: BasicError.message(messageText: errorMessage)) + } + } + } + } +} +