Add UI for digest

This commit is contained in:
Jackson Harper
2024-05-15 20:11:40 +08:00
parent 854ca08142
commit abe8593ed7
4 changed files with 172 additions and 54 deletions

View File

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

View File

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

View File

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

View File

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