Add UI for digest
This commit is contained in:
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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<MutationResult, Unions.SetUserPersonalizationResult> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user