Pull out popup view and replace with Transmission snackbars

PopupView and Transimission can interfere with each other and
cause an issue with the root screen becoming black and the app
getting stuck.
This commit is contained in:
Jackson Harper
2024-02-03 19:06:49 +08:00
parent b905ff126b
commit 03766734e5
22 changed files with 70 additions and 186 deletions

View File

@ -144,15 +144,6 @@
"version" : "2.30908.0"
}
},
{
"identity" : "popupview",
"kind" : "remoteSourceControl",
"location" : "https://github.com/exyte/PopupView.git",
"state" : {
"revision" : "68349a0ae704b9a7041f756f3f4f460ddbf7ba8d",
"version" : "2.6.0"
}
},
{
"identity" : "posthog-ios",
"kind" : "remoteSourceControl",

View File

@ -27,7 +27,6 @@ let package = Package(
"Models",
.product(name: "Introspect", package: "SwiftUI-Introspect"),
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
.productItem(name: "PopupView", package: "PopupView"),
.product(name: "Transmission", package: "Transmission")
],
resources: [.process("Resources")]
@ -71,7 +70,6 @@ var dependencies: [Package.Dependency] {
.package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"),
.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/exyte/PopupView.git", from: "2.6.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")

View File

@ -17,16 +17,5 @@ struct MiniShareExtensionView: View {
var body: some View {
ProgressView()
.popup(isPresented: $showToast) {
Text("Saving to Omnivore")
.padding(20)
} customize: {
$0
.type(.toast)
.position(.bottom)
.animation(.spring())
.closeOnTapOutside(true)
.backgroundColor(.black.opacity(0.5))
}
}
}

View File

@ -332,7 +332,7 @@ import Utils
if let customHighlight = annotation.customData?["omnivoreHighlight"] as? [String: String] {
if customHighlight["id"]?.lowercased() == highlightId {
if !document.remove(annotations: [annotation]) {
viewModel.snackbar(message: "Error removing highlight")
Snackbar.show(message: "Error removing highlight", dismissAfter: 2000)
}
}
}

View File

@ -8,9 +8,6 @@ final class PDFViewerViewModel: ObservableObject {
@Published var errorMessage: String?
@Published var readerView: Bool = false
@Published var showSnackbar: Bool = false
var snackbarMessage: String?
let pdfItem: PDFItem
var highlights: [Highlight]
@ -19,11 +16,6 @@ final class PDFViewerViewModel: ObservableObject {
self.highlights = pdfItem.highlights
}
func snackbar(message: String) {
snackbarMessage = message
showSnackbar = true
}
func findHighlight(dataService: DataService, highlightID: String) -> Highlight? {
let libraryItem = LibraryItem.lookup(byID: pdfItem.itemID, inContext: dataService.viewContext)
return libraryItem?.highlights.asArray(of: Highlight.self).first { $0.id == highlightID }

View File

@ -3,7 +3,7 @@ import Services
import Views
extension Snackbar {
static func showInLibrary(message: String, undoAction: (() -> Void)? = nil) {
NSNotification.librarySnackBar(message: message, undoAction: undoAction)
static func show(message: String, undoAction: (() -> Void)? = nil, dismissAfter: Int?) {
NSNotification.snackBar(message: message, undoAction: undoAction, dismissAfter: dismissAfter)
}
}

View File

@ -35,7 +35,7 @@
pasteBoard.writeObjects([highlightParams.quote as NSString])
#endif
// Snackbar.show(message: "Highlight copied")
Snackbar.show(message: "Highlight copied", dismissAfter: 2000)
},
label: { Label("Copy", systemImage: "doc.on.doc") }
)

View File

@ -222,7 +222,7 @@ struct AnimatingCellHeight: AnimatableModifier {
}
var body: some View {
ZStack {
ZStack {
HomeFeedView(
listTitle: $listTitle,
isListScrolled: $isListScrolled,
@ -506,29 +506,6 @@ struct AnimatingCellHeight: AnimatableModifier {
viewModel.negatedLabels = $1
}
}
.popup(isPresented: $viewModel.showSnackbar) {
if let operation = viewModel.snackbarOperation {
Snackbar(isShowing: $viewModel.showSnackbar, operation: operation)
} else {
EmptyView()
}
} customize: {
$0
.type(.toast)
.autohideIn(2)
.position(.bottom)
.animation(.spring())
.isOpaque(false)
}
.onReceive(NSNotification.librarySnackBarPublisher) { notification in
if !viewModel.showSnackbar {
if let message = notification.userInfo?["message"] as? String {
viewModel.snackbarOperation = SnackbarOperation(message: message,
undoAction: notification.userInfo?["undoAction"] as? SnackbarUndoAction)
viewModel.showSnackbar = true
}
}
}
}
}

View File

@ -28,10 +28,9 @@ enum LoadingBarStyle {
@Published var linkIsActive = false
@Published var showLabelsSheet = false
@Published var showSnackbar = false
@Published var showAddFeedView = false
@Published var showHideFollowingAlert = false
@Published var snackbarOperation: SnackbarOperation?
@Published var filters = [InternalFilter]()
@ -235,8 +234,7 @@ enum LoadingBarStyle {
}
func snackbar(_ message: String, undoAction: SnackbarUndoAction? = nil) {
snackbarOperation = SnackbarOperation(message: message, undoAction: undoAction)
showSnackbar = true
Snackbar.show(message: message, undoAction: undoAction, dismissAfter: 2000)
}
func setLinkArchived(dataService: DataService, objectID: NSManagedObjectID, archived: Bool) {

View File

@ -3,6 +3,7 @@ import Models
import Services
import SwiftUI
import Utils
import Views
@MainActor
public class LibraryAddFeedViewModel: NSObject, ObservableObject {
@ -101,9 +102,9 @@ public class LibraryAddFeedViewModel: NSObject, ObservableObject {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(4000)) {
if failureCount > 0 {
showInLibrarySnackbar("Failed to add \(failureCount) feeds")
Snackbar.show(message: "Failed to add \(failureCount) feeds", dismissAfter: 3000)
} else {
showInLibrarySnackbar("Added \(successCount) feed\(successCount == 0 ? "" : "s")")
Snackbar.show(message: "Added \(successCount) feed\(successCount == 0 ? "" : "s")", dismissAfter: 3000)
}
}
}
@ -211,7 +212,7 @@ public struct LibraryScanFeedView: View {
if viewModel.selected.count > 0 {
Button(action: {
dismiss()
showInLibrarySnackbar("Adding feeds...")
Snackbar.show(message: "Adding feeds...", dismissAfter: 2000)
Task {
await viewModel.addFeeds()
}

View File

@ -7,7 +7,6 @@
import Foundation
import Models
import PopupView
import Services
import SwiftUI
import Transmission
@ -71,8 +70,18 @@ struct LibraryTabView: View {
}
}
@State var showOperationToast = false
@State var operationStatus: OperationStatus = .none
@State var operationMessage: String?
var body: some View {
VStack(spacing: 0) {
WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $showOperationToast) {
OperationToast(operationMessage: $operationMessage, showOperationToast: $showOperationToast, operationStatus: $operationStatus)
} label: {
EmptyView()
}.buttonStyle(.plain)
TabView(selection: $selectedTab) {
if !hideFollowingTab {
NavigationView {
@ -128,6 +137,18 @@ struct LibraryTabView: View {
)
}
.navigationBarHidden(true)
.onReceive(NSNotification.snackBarPublisher) { notification in
if let message = notification.userInfo?["message"] as? String {
showOperationToast = true
operationMessage = message
operationStatus = .isPerforming
if let dismissAfter = notification.userInfo?["dismissAfter"] as? Int {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(dismissAfter)) {
showOperationToast = false
}
}
}
}
.onReceive(NSNotification.performSyncPublisher) { _ in
Task {
await syncManager.syncUpdates(dataService: dataService)

View File

@ -1,5 +1,4 @@
import Models
import PopupView
import Services
import SwiftUI
import Transmission
@ -7,7 +6,6 @@ import Views
@MainActor final class NewsletterEmailsViewModel: ObservableObject {
@Published var isLoading = false
@Published var showAddressCopied = false
@Published var emails = [NewsletterEmail]()
@Published var showOperationToast = false
@ -78,12 +76,6 @@ struct NewsletterEmailsView: View {
EmptyView()
}.buttonStyle(.plain)
WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $viewModel.showAddressCopied) {
MessageToast()
} label: {
EmptyView()
}.buttonStyle(.plain)
#if os(iOS)
Form {
innerBody
@ -162,10 +154,7 @@ struct NewsletterEmailRow: View {
pasteBoard.writeObjects([newsletterEmail.unwrappedEmail as NSString])
#endif
viewModel.showAddressCopied = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(2000)) {
viewModel.showAddressCopied = false
}
Snackbar.show(message: "Address copied", undoAction: nil, dismissAfter: 2000)
},
label: {
Text("Copy")
@ -191,22 +180,3 @@ struct NewsletterEmailRow: View {
}
}
struct MessageToast: View {
var body: some View {
VStack {
HStack {
Text("Address copied")
Spacer()
}
.padding(10)
.frame(minHeight: 50)
.frame(maxWidth: 380)
.background(Color(hex: "2A2A2A"))
.cornerRadius(4.0)
.tint(Color.green)
}
.padding(.bottom, 70)
.padding(.horizontal, 10)
.ignoresSafeArea(.all, edges: .bottom)
}
}

View File

@ -29,7 +29,7 @@
do {
try await dataService.leaveGroup(groupID: recommendationGroup.id)
// Snackbar.show(message: "You have left the club.")
Snackbar.show(message: "You have left the club.", dismissAfter: 2000)
} catch {
return false
}
@ -182,7 +182,7 @@
pasteBoard.writeObjects([highlightParams.quote as NSString])
#endif
// Snackbar.show(message: "Invite link copied")
Snackbar.show(message: "Invite link copied", dismissAfter: 2000)
}, label: {
Text("[\(viewModel.recommendationGroup.inviteUrl)](\(viewModel.recommendationGroup.inviteUrl))")
.font(.appCaption)

View File

@ -33,7 +33,7 @@ func removeLibraryItemAction(dataService: DataService, objectID: NSManagedObject
print("checking if task is canceled: ", Task.isCancelled)
}
Snackbar.showInLibrary(message: "Item removed", undoAction: {
Snackbar.show(message: "Item removed", undoAction: {
print("canceling task", syncTask)
syncTask.cancel()
dataService.viewContext.performAndWait {
@ -42,5 +42,5 @@ func removeLibraryItemAction(dataService: DataService, objectID: NSManagedObject
try? dataService.viewContext.save()
}
}
})
}, dismissAfter: 2000)
}

View File

@ -95,7 +95,7 @@
return AnyView(Button(action: {
Task {
if await viewModel.recommend(dataService: dataService) {
// Snackbar.show(message: "Recommendation sent")
Snackbar.show(message: "Recommendation sent", dismissAfter: 2000)
dismiss()
}
}

View File

@ -103,7 +103,7 @@ struct WebReader: PlatformViewRepresentable {
do {
try (webView as? OmnivoreWebView)?.dispatchEvent(.saveAnnotation(annotation: annotation))
} catch {
showInLibrarySnackbar("Error saving note.")
Snackbar.show(message: "Error saving note.", dismissAfter: 2000)
}
}

View File

@ -1,6 +1,5 @@
import AVFoundation
import Models
import PopupView
import Services
import SwiftUI
import Transmission
@ -355,12 +354,6 @@ struct WebReaderContainerView: View {
var body: some View {
ZStack {
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 let articleContent = viewModel.articleContent {
WebReader(
item: item,
@ -421,7 +414,7 @@ struct WebReaderContainerView: View {
#else
// Pasteboard.general.string = item.unwrappedPageURLString TODO: fix for mac
#endif
showInLibrarySnackbar("Link Copied")
Snackbar.show(message: "Link copied", dismissAfter: 2000)
}, label: { Text(LocalText.readerCopyLink) })
Button(action: {
if let linkToOpen = linkToOpen {
@ -533,7 +526,7 @@ struct WebReaderContainerView: View {
self.isRecovering = true
Task {
if !(await dataService.recoverItem(itemID: item.unwrappedID)) {
viewModel.snackbar(message: "Error recovering item")
Snackbar.show(message: "Error recoviering item", dismissAfter: 2000)
} else {
await viewModel.loadContent(
dataService: dataService,
@ -625,28 +618,7 @@ struct WebReaderContainerView: View {
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("PopToRoot"))) { _ in
pop()
}
.popup(isPresented: $viewModel.showSnackbar) {
if let operation = viewModel.snackbarOperation {
Snackbar(isShowing: $viewModel.showSnackbar, operation: operation)
} else {
EmptyView()
}
} customize: {
$0
.type(.toast)
.autohideIn(2)
.position(.bottom)
.animation(.spring())
.isOpaque(false)
}
.ignoresSafeArea(.all, edges: .bottom)
.onReceive(NSNotification.readerSnackBarPublisher) { notification in
if let message = notification.userInfo?["message"] as? String {
viewModel.snackbarOperation = SnackbarOperation(message: message,
undoAction: notification.userInfo?["undoAction"] as? SnackbarUndoAction)
viewModel.showSnackbar = true
}
}
}
func moveToInbox() {
@ -673,6 +645,7 @@ struct WebReaderContainerView: View {
dataService.archiveLink(objectID: item.objectID, archived: !isArchived)
#if os(iOS)
pop()
Snackbar.show(message: isArchived ? "Unarchived" : "Archived", dismissAfter: 2000)
#endif
}
@ -693,9 +666,9 @@ struct WebReaderContainerView: View {
pasteBoard.clearContents()
pasteBoard.writeObjects([deepLink.absoluteString as NSString])
#endif
showInLibrarySnackbar("Deeplink Copied")
Snackbar.show(message: "Deeplink Copied", dismissAfter: 2000)
} else {
showInLibrarySnackbar("Error copying deeplink")
Snackbar.show(message: "Error copying deeplink", dismissAfter: 2000)
}
}

View File

@ -20,14 +20,6 @@ struct SafariWebLink: Identifiable {
@Published var showOperationToast: Bool = false
@Published var operationStatus: OperationStatus = .none
@Published var showSnackbar: Bool = false
var snackbarOperation: SnackbarOperation?
func snackbar(message: String) {
snackbarOperation = SnackbarOperation(message: message, undoAction: nil)
showSnackbar = true
}
func hasOriginalUrl(_ item: Models.LibraryItem) -> Bool {
if let pageURLString = item.pageURLString, let host = URL(string: pageURLString)?.host {
if host == "omnivore.app" {
@ -39,7 +31,7 @@ struct SafariWebLink: Identifiable {
}
func downloadAudio(audioController: AudioController, item: Models.LibraryItem) {
snackbar(message: "Downloading Offline Audio")
Snackbar.show(message: "Downloading Offline Audio", dismissAfter: 2000)
isDownloadingAudio = true
if let audioDownloadTask = audioDownloadTask {
@ -53,7 +45,7 @@ struct SafariWebLink: Identifiable {
DispatchQueue.main.async {
self.isDownloadingAudio = false
if !canceled {
self.snackbar(message: downloaded ? "Audio file downloaded" : "Error downloading audio")
Snackbar.show(message: downloaded ? "Audio file downloaded" : "Error downloading audio", dismissAfter: 2000)
}
}
}
@ -214,12 +206,11 @@ struct SafariWebLink: Identifiable {
func saveLink(dataService: DataService, url: URL) {
Task {
do {
snackbar(message: "Saving link")
print("SAVING: ", url.absoluteString)
Snackbar.show(message: "Saving link", dismissAfter: 5000)
_ = try await dataService.createPageFromUrl(id: UUID().uuidString, url: url.absoluteString)
snackbar(message: "Link saved")
Snackbar.show(message: "Link saved", dismissAfter: 2000)
} catch {
snackbar(message: "Error saving link")
Snackbar.show(message: "Error saving link", dismissAfter: 2000)
}
}
}
@ -227,14 +218,14 @@ struct SafariWebLink: Identifiable {
func saveLinkAndFetch(dataService: DataService, username: String, url: URL) {
Task {
do {
snackbar(message: "Saving link")
Snackbar.show(message: "Saving link", dismissAfter: 5000)
let requestId = UUID().uuidString
_ = try await dataService.createPageFromUrl(id: requestId, url: url.absoluteString)
snackbar(message: "Link saved")
Snackbar.show(message: "Link saved", dismissAfter: 2000)
await loadContent(dataService: dataService, username: username, itemID: requestId, retryCount: 0)
} catch {
snackbar(message: "Error saving link")
Snackbar.show(message: "Error saving link", dismissAfter: 2000)
}
}
}

View File

@ -14,11 +14,6 @@ extension DataService {
// Send update to server
self.syncLinkArchiveStatus(itemID: linkedItem.unwrappedID, archived: archived)
let message = archived ? "Link archived" : "Link unarchived"
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) {
showInLibrarySnackbar(message)
}
}
}

View File

@ -5,8 +5,7 @@ import Models
public extension NSNotification {
static let PushJSONArticle = Notification.Name("PushJSONArticle")
static let PushReaderItem = Notification.Name("PushReaderItem")
static let LibrarySnackBar = Notification.Name("LibrarySnackBar")
static let ReaderSnackBar = Notification.Name("ReaderSnackBar")
static let SnackBar = Notification.Name("SnackBar")
static let OperationFailure = Notification.Name("OperationFailure")
static let ReaderSettingsChanged = Notification.Name("ReaderSettingsChanged")
static let SpeakingReaderItem = Notification.Name("SpeakingReaderItem")
@ -27,12 +26,8 @@ public extension NSNotification {
NotificationCenter.default.publisher(for: PushReaderItem)
}
static var readerSnackBarPublisher: NotificationCenter.Publisher {
NotificationCenter.default.publisher(for: ReaderSnackBar)
}
static var librarySnackBarPublisher: NotificationCenter.Publisher {
NotificationCenter.default.publisher(for: LibrarySnackBar)
static var snackBarPublisher: NotificationCenter.Publisher {
NotificationCenter.default.publisher(for: SnackBar)
}
static var operationFailedPublisher: NotificationCenter.Publisher {
@ -82,10 +77,12 @@ public extension NSNotification {
)
}
static func librarySnackBar(message: String, undoAction: (() -> Void)?) {
NotificationCenter.default.post(name: NSNotification.LibrarySnackBar,
static func snackBar(message: String, undoAction: (() -> Void)?, dismissAfter: Int?) {
NotificationCenter.default.post(name: NSNotification.SnackBar,
object: nil,
userInfo: ["message": message, "undoAction": undoAction as Any])
userInfo: ["message": message,
"undoAction": undoAction as Any,
"dismissAfter": dismissAfter as Any])
}
static func operationFailed(message: String) {

View File

@ -1,18 +0,0 @@
//
// ShowInSnackbar.swift
//
//
// Created by Jackson Harper on 11/1/22.
//
import Foundation
public func showInLibrarySnackbar(_ message: String) {
let nname = Notification.Name("LibrarySnackBar")
NotificationCenter.default.post(name: nname, object: nil, userInfo: ["message": message])
}
public func showInReaderSnackbar(_ message: String) {
let nname = Notification.Name("ReaderSnackBar")
NotificationCenter.default.post(name: nname, object: nil, userInfo: ["message": message])
}

View File

@ -1,6 +1,7 @@
import Models
import Utils
import WebKit
// swiftlint:disable file_length
/// Describes actions that can be sent from the WebView back to native views.
@ -191,6 +192,14 @@ public final class OmnivoreWebView: WKWebView {
}
}
#endif
// Because all the snackbar stuff lives in app we just use notifications here
func showInReaderSnackbar(_ message: String) {
NotificationCenter.default.post(name: Notification.Name("SnackBar"),
object: nil,
userInfo: ["message": message,
"dismissAfter": 2000 as Any])
}
}
#if os(iOS)