Merge pull request #3653 from omnivore-app/fix/ios-reading-position
iOS read position and view hierarchy fixes
This commit is contained in:
@ -32,8 +32,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/nathantannar4/Engine",
|
||||
"state" : {
|
||||
"revision" : "31949c114698e4fd43fd76290913bca415fa87bc",
|
||||
"version" : "1.1.0"
|
||||
"revision" : "e9867eb6df013abc65c3437d295e594077469a13",
|
||||
"version" : "1.5.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -180,24 +180,6 @@
|
||||
"version" : "1.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-async-algorithms",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-async-algorithms",
|
||||
"state" : {
|
||||
"revision" : "da4e36f86544cdf733a40d59b3a2267e3a7bbf36",
|
||||
"version" : "1.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-collections",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-collections.git",
|
||||
"state" : {
|
||||
"revision" : "d029d9d39c87bed85b1c50adee7c41795261a192",
|
||||
"version" : "1.0.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-graphql",
|
||||
"kind" : "remoteSourceControl",
|
||||
@ -257,8 +239,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/nathantannar4/Transmission",
|
||||
"state" : {
|
||||
"revision" : "9517912f8f528c777f86f7896b5c35d7e43fa916",
|
||||
"version" : "1.0.1"
|
||||
"revision" : "3dac53ae4bddc7ab99e6374622a9c5eefbe50eed",
|
||||
"version" : "1.1.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -266,8 +248,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/nathantannar4/Turbocharger",
|
||||
"state" : {
|
||||
"revision" : "b4201ba0bc094facf6cabe3b36fd3763b51ccfc8",
|
||||
"version" : "1.0.1"
|
||||
"revision" : "095344c0cac57873e1552f30d3561ab1bec5ae35",
|
||||
"version" : "1.1.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -71,8 +71,9 @@ var dependencies: [Package.Dependency] {
|
||||
.package(url: "https://github.com/google/GoogleSignIn-iOS", from: "6.2.2"),
|
||||
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.0.0"),
|
||||
.package(url: "https://github.com/PostHog/posthog-ios.git", from: "2.0.0"),
|
||||
.package(url: "https://github.com/nathantannar4/Transmission", from: "1.0.1"),
|
||||
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0")
|
||||
.package(url: "https://github.com/nathantannar4/Engine", exact: "1.5.1"),
|
||||
.package(url: "https://github.com/nathantannar4/Turbocharger", exact: "1.1.4"),
|
||||
.package(url: "https://github.com/nathantannar4/Transmission", from: "1.1.4")
|
||||
]
|
||||
// Comment out following line for macOS build
|
||||
deps.append(.package(url: "https://github.com/PSPDFKit/PSPDFKit-SP", from: "13.1.0"))
|
||||
|
||||
@ -14,7 +14,7 @@ struct MacFeedCardNavigationLink: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
LibraryItemCard(item: LibraryItemData.make(from: item), viewer: dataService.currentViewer)
|
||||
LibraryItemCard(item: item, viewer: dataService.currentViewer)
|
||||
NavigationLink(destination: LinkItemDetailView(
|
||||
linkedItemObjectID: item.objectID,
|
||||
isPDF: item.isPDF
|
||||
@ -36,7 +36,7 @@ struct LibraryItemListNavigationLink: View {
|
||||
Button(action: {
|
||||
viewModel.presentItem(item: item)
|
||||
}, label: {
|
||||
LibraryItemCard(item: LibraryItemData.make(from: item), viewer: dataService.currentViewer)
|
||||
LibraryItemCard(item: item, viewer: dataService.currentViewer)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -54,7 +54,7 @@ struct LibraryItemGridCardNavigationLink: View {
|
||||
Button(action: {
|
||||
viewModel.presentItem(item: item)
|
||||
}, label: {
|
||||
GridCard(item: LibraryItemData.make(from: item))
|
||||
GridCard(item: item)
|
||||
})
|
||||
.buttonStyle(.plain)
|
||||
.aspectRatio(1.0, contentMode: .fill)
|
||||
|
||||
@ -150,13 +150,15 @@ struct EmptyState: View {
|
||||
return AnyView(Group {
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
Text("No results found for this query")
|
||||
.font(Font.system(size: 18, weight: .bold))
|
||||
if viewModel.showLoadingBar == .none {
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
Text("No results found for this query")
|
||||
.font(Font.system(size: 18, weight: .bold))
|
||||
}
|
||||
.frame(minHeight: 400)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
}
|
||||
.frame(minHeight: 400)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
})
|
||||
@ -493,7 +495,8 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
options: PresentationLinkTransition.SlideTransitionOptions(edge: .trailing,
|
||||
options:
|
||||
PresentationLinkTransition.Options(
|
||||
modalPresentationCapturesStatusBarAppearance: true
|
||||
modalPresentationCapturesStatusBarAppearance: true,
|
||||
preferredPresentationBackgroundColor: ThemeManager.currentBgColor
|
||||
))),
|
||||
isPresented: $viewModel.presentWebContainer,
|
||||
destination: {
|
||||
@ -715,15 +718,6 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
}
|
||||
}
|
||||
|
||||
var redactedItems: some View {
|
||||
ForEach(Array(fakeLibraryItems(dataService: dataService).enumerated()), id: \.1.id) { _, item in
|
||||
let horizontalInset = CGFloat(UIDevice.isIPad ? 20 : 10)
|
||||
LibraryItemCard(item: item, viewer: dataService.currentViewer)
|
||||
.listRowSeparatorTint(Color.thBorderColor)
|
||||
.listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 10, trailing: horizontalInset))
|
||||
}.redacted(reason: .placeholder)
|
||||
}
|
||||
|
||||
var listItems: some View {
|
||||
ForEach(Array(viewModel.fetcher.items.enumerated()), id: \.1.unwrappedID) { idx, item in
|
||||
let horizontalInset = CGFloat(UIDevice.isIPad ? 20 : 10)
|
||||
@ -814,9 +808,7 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.showLoadingBar == .redacted {
|
||||
redactedItems
|
||||
} else if viewModel.showLoadingBar == .simple {
|
||||
if viewModel.showLoadingBar == .redacted || viewModel.showLoadingBar == .simple {
|
||||
VStack {
|
||||
ProgressView()
|
||||
}
|
||||
@ -843,7 +835,9 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
}, header: {
|
||||
filtersHeader
|
||||
})
|
||||
BottomView(viewModel: viewModel)
|
||||
if viewModel.showLoadingBar == .none {
|
||||
BottomView(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
.padding(0)
|
||||
.listStyle(.plain)
|
||||
@ -1001,14 +995,7 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
|
||||
ScrollView {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 325, maximum: 400), spacing: 16)], alignment: .center, spacing: 30) {
|
||||
if viewModel.showLoadingBar == .redacted {
|
||||
ForEach(fakeLibraryItems(dataService: dataService), id: \.id) { item in
|
||||
GridCard(item: item)
|
||||
.aspectRatio(1.0, contentMode: .fill)
|
||||
.background(Color.systemBackground)
|
||||
.cornerRadius(6)
|
||||
}.redacted(reason: .placeholder)
|
||||
} else if viewModel.showLoadingBar == .simple {
|
||||
if viewModel.showLoadingBar == .redacted || viewModel.showLoadingBar == .simple {
|
||||
VStack {
|
||||
ProgressView()
|
||||
}
|
||||
@ -1056,7 +1043,7 @@ struct AnimatingCellHeight: AnimatableModifier {
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.fetcher.items.isEmpty {
|
||||
if viewModel.fetcher.items.isEmpty || viewModel.showLoadingBar == .redacted || viewModel.showLoadingBar == .simple {
|
||||
EmptyState(viewModel: viewModel)
|
||||
} else {
|
||||
HStack {
|
||||
@ -1125,31 +1112,6 @@ struct LinkDestination: View {
|
||||
}
|
||||
}
|
||||
|
||||
func fakeLibraryItems(dataService _: DataService) -> [LibraryItemData] {
|
||||
Array(
|
||||
repeatElement(0, count: 20)
|
||||
.map { _ in
|
||||
LibraryItemData(
|
||||
id: UUID().uuidString,
|
||||
title: "fake title that is kind of long so it looks better",
|
||||
pageURLString: "",
|
||||
isArchived: false,
|
||||
author: "fake author",
|
||||
deepLink: nil,
|
||||
hasLabels: false,
|
||||
noteText: nil,
|
||||
readingProgress: 10,
|
||||
wordsCount: 10,
|
||||
isPDF: false,
|
||||
highlights: nil,
|
||||
sortedLabels: [],
|
||||
imageURL: nil,
|
||||
publisherDisplayName: "fake publisher",
|
||||
descriptionText: "This is a fake description"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
struct BottomView: View {
|
||||
@ObservedObject var viewModel: HomeFeedViewModel
|
||||
@EnvironmentObject var dataService: DataService
|
||||
|
||||
@ -73,7 +73,12 @@ enum LoadingBarStyle {
|
||||
self.linkIsActive = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func pushLinkedRequest(request: LinkRequest) {
|
||||
self.linkRequest = request
|
||||
self.presentWebContainer = true
|
||||
}
|
||||
|
||||
private var filterState: FetcherFilterState? {
|
||||
if let appliedFilter = appliedFilter {
|
||||
return FetcherFilterState(
|
||||
|
||||
@ -101,7 +101,8 @@
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
}.onTapGesture {
|
||||
viewModel.linkRequest = LinkRequest(id: UUID(), serverID: item.id)
|
||||
homeFeedViewModel.pushLinkedRequest(request: LinkRequest(id: UUID(), serverID: item.id))
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,47 +4,120 @@
|
||||
import SwiftUI
|
||||
import Utils
|
||||
import Views
|
||||
import Transmission
|
||||
|
||||
@MainActor final class PushNotificationSettingsViewModel: ObservableObject {
|
||||
@Published var isLoading = false
|
||||
@Published var emails = [NewsletterEmail]()
|
||||
@Published var desiredNotificationsEnabled = false
|
||||
@AppStorage(UserDefaultKey.notificationsEnabled.rawValue) var notificationsEnabled = false
|
||||
@MainActor final class PushNotificationSettingsViewModel: ObservableObject {
|
||||
@Published var isLoading = false
|
||||
@Published var isLoadingRules = true
|
||||
|
||||
func checkPushNotificationsStatus() {
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
DispatchQueue.main.async {
|
||||
self.desiredNotificationsEnabled = settings.alertSetting == UNNotificationSetting.enabled
|
||||
}
|
||||
@Published var emails = [NewsletterEmail]()
|
||||
@Published var desiredNotificationsEnabled = false
|
||||
@Published var allSubscriptionsNotificationRule: Rule?
|
||||
@Published var hasSubscriptionsNotifyRule = false
|
||||
@Published var showOperationToast = false
|
||||
@Published var operationStatus: OperationStatus = .none
|
||||
@Published var operationMessage: String?
|
||||
|
||||
let subscriptionRuleName = "system.autoNotify.subscriptions"
|
||||
|
||||
@AppStorage(UserDefaultKey.notificationsEnabled.rawValue) var notificationsEnabled = false
|
||||
|
||||
func checkPushNotificationsStatus() {
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
DispatchQueue.main.async {
|
||||
self.desiredNotificationsEnabled = settings.alertSetting == UNNotificationSetting.enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tryUpdateToDesired(dataService: DataService) {
|
||||
UserDefaults.standard.set(desiredNotificationsEnabled, forKey: UserDefaultKey.notificationsEnabled.rawValue)
|
||||
func tryUpdateToDesired(dataService: DataService) {
|
||||
UserDefaults.standard.set(desiredNotificationsEnabled, forKey: UserDefaultKey.notificationsEnabled.rawValue)
|
||||
|
||||
if desiredNotificationsEnabled {
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { granted, _ in
|
||||
DispatchQueue.main.async {
|
||||
self.desiredNotificationsEnabled = granted
|
||||
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)
|
||||
if desiredNotificationsEnabled {
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { granted, _ in
|
||||
DispatchQueue.main.async {
|
||||
self.desiredNotificationsEnabled = granted
|
||||
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)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let tokenID = UserDefaults.standard.string(forKey: UserDefaultKey.deviceTokenID.rawValue) {
|
||||
Task {
|
||||
try? await Services().dataService.syncDeviceToken(deviceTokenOperation: .deleteToken(tokenID: tokenID))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let tokenID = UserDefaults.standard.string(forKey: UserDefaultKey.deviceTokenID.rawValue) {
|
||||
Task {
|
||||
try? await Services().dataService.syncDeviceToken(deviceTokenOperation: .deleteToken(tokenID: tokenID))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadRule(dataService: DataService) async {
|
||||
do {
|
||||
let rule = try await dataService.rules().filter { $0.name == subscriptionRuleName }.first
|
||||
setAllSubscriptionRule(rule: rule)
|
||||
} catch {
|
||||
print("error fetching", error)
|
||||
setAllSubscriptionRule(rule: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func setAllSubscriptionRule(rule: Rule?) {
|
||||
allSubscriptionsNotificationRule = rule
|
||||
hasSubscriptionsNotifyRule = allSubscriptionsNotificationRule != nil
|
||||
isLoadingRules = false
|
||||
}
|
||||
|
||||
func createSubscriptionNotificationRule(dataService: DataService) {
|
||||
if allSubscriptionsNotificationRule != nil {
|
||||
return
|
||||
}
|
||||
|
||||
operationMessage = "Creating notification rule..."
|
||||
operationStatus = .isPerforming
|
||||
showOperationToast = true
|
||||
|
||||
Task {
|
||||
do {
|
||||
let rule = try await dataService.createNotificationRule(
|
||||
name: subscriptionRuleName,
|
||||
filter: "in:all has:subscription"
|
||||
)
|
||||
setAllSubscriptionRule(rule: rule)
|
||||
operationMessage = "Rule created"
|
||||
operationStatus = .success
|
||||
} catch {
|
||||
print("error creating notification rule: ", error)
|
||||
operationMessage = "Failed to create notification rule"
|
||||
operationStatus = .failure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteSubscriptionNotificationRule(dataService: DataService) {
|
||||
operationMessage = "Creating label rule..."
|
||||
operationStatus = .isPerforming
|
||||
showOperationToast = true
|
||||
|
||||
Task {
|
||||
do {
|
||||
if let allSubscriptionsNotificationRule = allSubscriptionsNotificationRule {
|
||||
_ = try await dataService.deleteRule(ruleID: allSubscriptionsNotificationRule.id)
|
||||
setAllSubscriptionRule(rule: nil)
|
||||
operationMessage = "Notification rule deleted"
|
||||
operationStatus = .success
|
||||
}
|
||||
} catch {
|
||||
operationMessage = "Failed to create label rule"
|
||||
operationStatus = .failure
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PushNotificationSettingsView: View {
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@ -54,6 +127,12 @@
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $viewModel.showOperationToast) {
|
||||
OperationToast(operationMessage: $viewModel.operationMessage, showOperationToast: $viewModel.showOperationToast, operationStatus: $viewModel.operationStatus)
|
||||
} label: {
|
||||
EmptyView()
|
||||
}.buttonStyle(.plain)
|
||||
|
||||
#if os(iOS)
|
||||
Form {
|
||||
innerBody
|
||||
@ -68,7 +147,10 @@
|
||||
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("ScrollToTop"))) { _ in
|
||||
dismiss()
|
||||
}
|
||||
.task { viewModel.checkPushNotificationsStatus() }
|
||||
.task {
|
||||
viewModel.checkPushNotificationsStatus()
|
||||
await viewModel.loadRule(dataService: dataService)
|
||||
}
|
||||
}
|
||||
|
||||
private var notificationsText: some View {
|
||||
@ -84,6 +166,14 @@
|
||||
.accentColor(.blue)
|
||||
}
|
||||
|
||||
private var rulesSection: some View {
|
||||
if viewModel.isLoadingRules {
|
||||
AnyView(EmptyView())
|
||||
} else {
|
||||
AnyView(Toggle("Notify me when new items arrive from my subscriptions", isOn: $viewModel.hasSubscriptionsNotifyRule))
|
||||
}
|
||||
}
|
||||
|
||||
private var innerBody: some View {
|
||||
Group {
|
||||
Section {
|
||||
@ -96,12 +186,35 @@
|
||||
notificationsText
|
||||
}
|
||||
|
||||
Section {
|
||||
rulesSection
|
||||
}
|
||||
|
||||
Section {
|
||||
NavigationLink("Devices") {
|
||||
PushNotificationDevicesView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: viewModel.hasSubscriptionsNotifyRule) { newValue in
|
||||
print("has notification rule: \(newValue)")
|
||||
if viewModel.isLoadingRules {
|
||||
return
|
||||
}
|
||||
|
||||
if newValue {
|
||||
viewModel.createSubscriptionNotificationRule(dataService: dataService)
|
||||
} else {
|
||||
viewModel.deleteSubscriptionNotificationRule(dataService: dataService)
|
||||
}
|
||||
}
|
||||
.onChange(of: viewModel.operationStatus) { newValue in
|
||||
if newValue == .success || newValue == .failure {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1000)) {
|
||||
viewModel.showOperationToast = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(LocalText.pushNotificationsGeneric)
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,7 +273,7 @@ struct SubscriptionsView: View {
|
||||
await viewModel.cancelSubscription(dataService: dataService, subscription: subscription)
|
||||
}
|
||||
}
|
||||
)
|
||||
).background(Color.systemBackground)
|
||||
} label: {
|
||||
SubscriptionCell(subscription: subscription)
|
||||
}
|
||||
@ -526,6 +526,25 @@ struct SubscriptionSettingsView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
// var notificationRuleRow: some View {
|
||||
// HStack {
|
||||
// Text("Add Labels")
|
||||
// Spacer()
|
||||
// if isLoadingRule || viewModel.rules != nil {
|
||||
// Button(action: { showLabelsSelector = true }, label: {
|
||||
// if let ruleLabels = ruleLabels {
|
||||
// let labelNames = ruleLabels.map(\.unwrappedName)
|
||||
// Text("[\(labelNames.joined(separator: ","))]")
|
||||
// } else {
|
||||
// Text("Create Rule")
|
||||
// }
|
||||
// }).tint(Color.blue)
|
||||
// } else {
|
||||
// ProgressView()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
|
||||
@ -10,6 +10,11 @@ import Views
|
||||
@Published var errorMessage: String?
|
||||
|
||||
func loadItem(dataService: DataService, username: String, requestID: String) async {
|
||||
if let cached = Models.LibraryItem.lookup(byID: requestID, inContext: dataService.viewContext) {
|
||||
item = cached
|
||||
return
|
||||
}
|
||||
|
||||
guard let objectID = try? await dataService.loadItemContentUsingRequestID(username: username,
|
||||
requestID: requestID)
|
||||
else {
|
||||
@ -57,8 +62,6 @@ public struct WebReaderLoadingContainer: View {
|
||||
PDFWrapperView(pdfURL: pdfURL)
|
||||
}
|
||||
#endif
|
||||
} else if item.state == "CONTENT_NOT_FETCHED" {
|
||||
ProgressView()
|
||||
} else {
|
||||
WebReaderContainerView(item: item)
|
||||
#if os(iOS)
|
||||
@ -72,11 +75,7 @@ public struct WebReaderLoadingContainer: View {
|
||||
} else {
|
||||
ProgressView()
|
||||
.task {
|
||||
if let username = dataService.currentViewer?.username {
|
||||
await viewModel.loadItem(dataService: dataService, username: username, requestID: requestID)
|
||||
} else {
|
||||
viewModel.errorMessage = "You are not logged in."
|
||||
}
|
||||
await viewModel.loadItem(dataService: dataService, username: "me", requestID: requestID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,4 +89,51 @@ public extension DataService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createNotificationRule(name: String, filter: String) async throws -> Rule {
|
||||
enum MutationResult {
|
||||
case result(rule: Rule)
|
||||
case error(errorMessage: String)
|
||||
}
|
||||
|
||||
let selection = Selection<MutationResult, Unions.SetRuleResult> {
|
||||
try $0.on(
|
||||
setRuleError: .init { .error(errorMessage: try $0.errorCodes().first?.rawValue ?? "Unknown Error") },
|
||||
setRuleSuccess: .init { .result(rule: try $0.rule(selection: ruleSelection)) }
|
||||
)
|
||||
}
|
||||
|
||||
let mutation = Selection.Mutation {
|
||||
try $0.setRule(
|
||||
input: InputObjects.SetRuleInput(
|
||||
actions: [InputObjects.RuleActionInput(params: [], type: .sendNotification)],
|
||||
enabled: true,
|
||||
eventTypes: [.pageCreated],
|
||||
filter: filter,
|
||||
id: OptionalArgument(nil),
|
||||
name: name
|
||||
),
|
||||
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 {
|
||||
continuation.resume(throwing: BasicError.message(messageText: "network error"))
|
||||
return
|
||||
}
|
||||
|
||||
switch payload.data {
|
||||
case let .result(rule: rule):
|
||||
continuation.resume(returning: rule)
|
||||
case let .error(errorMessage: errorMessage):
|
||||
continuation.resume(throwing: BasicError.message(messageText: errorMessage))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,10 +11,10 @@ public enum GridCardAction {
|
||||
}
|
||||
|
||||
public struct GridCard: View {
|
||||
let item: LibraryItemData
|
||||
@ObservedObject var item: Models.LibraryItem
|
||||
|
||||
public init(
|
||||
item: LibraryItemData
|
||||
item: Models.LibraryItem
|
||||
) {
|
||||
self.item = item
|
||||
}
|
||||
@ -67,7 +67,7 @@ public struct GridCard: View {
|
||||
var fallbackImage: some View {
|
||||
GeometryReader { geo in
|
||||
HStack {
|
||||
Text(item.title)
|
||||
Text(item.title ?? "")
|
||||
.font(fallbackFont)
|
||||
.frame(alignment: .center)
|
||||
.multilineTextAlignment(.leading)
|
||||
@ -232,7 +232,7 @@ public struct GridCard: View {
|
||||
.dynamicTypeSize(.xSmall ... .medium)
|
||||
.padding(.horizontal, 15)
|
||||
|
||||
Text(item.title)
|
||||
Text(item.title ?? "")
|
||||
.lineLimit(2)
|
||||
.font(.appHeadline)
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
@ -246,7 +246,7 @@ public struct GridCard: View {
|
||||
|
||||
// Link description and image
|
||||
HStack(alignment: .top) {
|
||||
Text(item.descriptionText ?? item.title)
|
||||
Text(item.descriptionText ?? item.title ?? "")
|
||||
.font(.appSubheadline)
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
.lineLimit(2)
|
||||
|
||||
@ -32,11 +32,11 @@ enum FlairLabels: String {
|
||||
}
|
||||
|
||||
public extension View {
|
||||
func draggableItem(item: LibraryItemData) -> some View {
|
||||
func draggableItem(item: Models.LibraryItem) -> some View {
|
||||
#if os(iOS)
|
||||
if #available(iOS 16.0, *), let url = item.deepLink {
|
||||
return AnyView(self.draggable(url) {
|
||||
Label(item.title, systemImage: "link")
|
||||
Label(item.title ?? "", systemImage: "link")
|
||||
})
|
||||
}
|
||||
#endif
|
||||
@ -44,75 +44,12 @@ public extension View {
|
||||
}
|
||||
}
|
||||
|
||||
public struct LibraryItemData {
|
||||
public var id: String
|
||||
public let title: String
|
||||
public let pageURLString: String
|
||||
public var isArchived: Bool
|
||||
public let author: String?
|
||||
public let deepLink: URL?
|
||||
public let hasLabels: Bool
|
||||
public let noteText: String?
|
||||
public let readingProgress: Double
|
||||
public let wordsCount: Int64
|
||||
public let isPDF: Bool
|
||||
public let highlights: NSSet?
|
||||
public let sortedLabels: [LinkedItemLabel]
|
||||
public let imageURL: URL?
|
||||
public let publisherDisplayName: String?
|
||||
public let descriptionText: String?
|
||||
|
||||
public init(id: String, title: String, pageURLString: String, isArchived: Bool, author: String?,
|
||||
deepLink: URL?, hasLabels: Bool, noteText: String?,
|
||||
readingProgress: Double, wordsCount: Int64, isPDF: Bool, highlights: NSSet?,
|
||||
sortedLabels: [LinkedItemLabel], imageURL: URL?, publisherDisplayName: String?, descriptionText: String?)
|
||||
{
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.pageURLString = pageURLString
|
||||
self.isArchived = isArchived
|
||||
self.author = author
|
||||
self.deepLink = deepLink
|
||||
self.hasLabels = hasLabels
|
||||
self.noteText = noteText
|
||||
self.readingProgress = readingProgress
|
||||
self.wordsCount = wordsCount
|
||||
self.isPDF = isPDF
|
||||
self.highlights = highlights
|
||||
self.sortedLabels = sortedLabels
|
||||
self.imageURL = imageURL
|
||||
self.publisherDisplayName = publisherDisplayName
|
||||
self.descriptionText = descriptionText
|
||||
}
|
||||
|
||||
public static func make(from item: Models.LibraryItem) -> LibraryItemData {
|
||||
LibraryItemData(
|
||||
id: item.unwrappedID,
|
||||
title: item.unwrappedTitle,
|
||||
pageURLString: item.unwrappedPageURLString,
|
||||
isArchived: item.isArchived,
|
||||
author: item.author,
|
||||
deepLink: item.deepLink,
|
||||
hasLabels: item.hasLabels,
|
||||
noteText: item.noteText,
|
||||
readingProgress: item.readingProgress,
|
||||
wordsCount: item.wordsCount,
|
||||
isPDF: item.isPDF,
|
||||
highlights: item.highlights,
|
||||
sortedLabels: item.sortedLabels,
|
||||
imageURL: item.imageURL,
|
||||
publisherDisplayName: item.publisherDisplayName,
|
||||
descriptionText: item.descriptionText
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public struct LibraryItemCard: View {
|
||||
let viewer: Viewer?
|
||||
var item: LibraryItemData
|
||||
@ObservedObject var item: Models.LibraryItem
|
||||
@State var noteLineLimit: Int? = 3
|
||||
|
||||
public init(item: LibraryItemData, viewer: Viewer?) {
|
||||
public init(item: Models.LibraryItem, viewer: Viewer?) {
|
||||
self.item = item
|
||||
self.viewer = viewer
|
||||
}
|
||||
@ -362,7 +299,7 @@ public struct LibraryItemCard: View {
|
||||
readInfo
|
||||
.dynamicTypeSize(.xSmall ... .medium)
|
||||
|
||||
Text(item.title)
|
||||
Text(item.title ?? "")
|
||||
.font(.body).fontWeight(.semibold)
|
||||
.lineSpacing(1.25)
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
|
||||
@ -55,7 +55,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
|
||||
|
||||
let userInfo = notification.request.content.userInfo
|
||||
UIApplication.shared.applicationIconBadgeNumber = 0
|
||||
print(userInfo) // extract data sent along with PN
|
||||
print("push data", userInfo) // extract data sent along with PN
|
||||
completionHandler([[.banner, .sound]])
|
||||
}
|
||||
|
||||
|
||||
@ -70,9 +70,13 @@ const sendNotification = async (obj: RuleActionObj) => {
|
||||
const message = {
|
||||
title: item.author || item.siteName || 'Omnivore',
|
||||
body: item.title,
|
||||
image: item.thumbnail,
|
||||
}
|
||||
const data = {
|
||||
libraryItemId: item.id,
|
||||
}
|
||||
|
||||
return sendPushNotifications(obj.userId, message, 'rule')
|
||||
return sendPushNotifications(obj.userId, message, 'rule', data)
|
||||
}
|
||||
|
||||
const getRuleAction = (actionType: RuleActionType): RuleActionFunc => {
|
||||
|
||||
@ -19,8 +19,18 @@ type ConfirmationModalProps = {
|
||||
}
|
||||
|
||||
export function ConfirmationModal(props: ConfirmationModalProps): JSX.Element {
|
||||
const safeOnOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
props.onOpenChange(open)
|
||||
setTimeout(() => {
|
||||
document.body.style.removeProperty('pointer-events')
|
||||
}, 200)
|
||||
},
|
||||
[props]
|
||||
)
|
||||
|
||||
return (
|
||||
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
|
||||
<ModalRoot defaultOpen onOpenChange={safeOnOpenChange}>
|
||||
<ModalOverlay />
|
||||
<ModalContent css={{ bg: '$grayBg', maxWidth: '20em', zIndex: '20' }}>
|
||||
<VStack alignment="center" distribution="center" css={{ p: '15px' }}>
|
||||
@ -46,11 +56,15 @@ export function ConfirmationModal(props: ConfirmationModalProps): JSX.Element {
|
||||
</Button>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
onClick={props.onAccept}
|
||||
onClick={() => {
|
||||
props.onAccept()
|
||||
document.body.style.removeProperty('pointer-events')
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
props.onAccept()
|
||||
document.body.style.removeProperty('pointer-events')
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user