Sfoglia il codice sorgente

Add top toast feedback for meeting creation outcomes.

Show a styled top toast with success/error icons and improved copy when creating a calendar meeting succeeds or fails.

Made-with: Cursor
huzaifahayat12 1 settimana fa
parent
commit
17fc3bdd7f
1 ha cambiato i file con 95 aggiunte e 1 eliminazioni
  1. 95 1
      meetings_app/ViewController.swift

+ 95 - 1
meetings_app/ViewController.swift

@@ -362,6 +362,8 @@ final class ViewController: NSViewController {
362 362
     private weak var calendarPageDaySummaryLabel: NSTextField?
363 363
     private var calendarPageActionPopover: NSPopover?
364 364
     private var calendarPageCreatePopover: NSPopover?
365
+    private weak var topToastView: NSVisualEffectView?
366
+    private var topToastHideWorkItem: DispatchWorkItem?
365 367
 
366 368
     /// In-app browser navigation: `.allowAll` or `.whitelist(hostSuffixes:)` (e.g. `["google.com"]` matches `meet.google.com`).
367 369
     private let inAppBrowserDefaultPolicy: InAppBrowserURLPolicy = .allowAll
@@ -867,6 +869,95 @@ private extension ViewController {
867 869
         alert.runModal()
868 870
     }
869 871
 
872
+    private func showTopToast(message: String, isError: Bool) {
873
+        topToastHideWorkItem?.cancel()
874
+        topToastHideWorkItem = nil
875
+        topToastView?.removeFromSuperview()
876
+        topToastView = nil
877
+
878
+        let toast = NSVisualEffectView()
879
+        toast.translatesAutoresizingMaskIntoConstraints = false
880
+        toast.material = darkModeEnabled ? .hudWindow : .popover
881
+        toast.blendingMode = .withinWindow
882
+        toast.state = .active
883
+        toast.wantsLayer = true
884
+        toast.layer?.cornerRadius = 10
885
+        toast.layer?.masksToBounds = true
886
+        toast.layer?.borderWidth = 1
887
+        toast.layer?.borderColor = NSColor.white.withAlphaComponent(darkModeEnabled ? 0.10 : 0.18).cgColor
888
+        toast.layer?.backgroundColor = NSColor.black.withAlphaComponent(darkModeEnabled ? 0.68 : 0.78).cgColor
889
+        toast.alphaValue = 0
890
+
891
+        let row = NSStackView()
892
+        row.translatesAutoresizingMaskIntoConstraints = false
893
+        row.orientation = .horizontal
894
+        row.alignment = .centerY
895
+        row.spacing = 8
896
+        row.distribution = .fill
897
+
898
+        let icon = NSImageView()
899
+        icon.translatesAutoresizingMaskIntoConstraints = false
900
+        icon.imageScaling = .scaleProportionallyDown
901
+        icon.image = NSImage(
902
+            systemSymbolName: isError ? "xmark.circle.fill" : "checkmark.circle.fill",
903
+            accessibilityDescription: isError ? "Error" : "Success"
904
+        )
905
+        icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
906
+        icon.contentTintColor = isError ? NSColor.systemRed : NSColor.systemGreen
907
+
908
+        let label = textLabel(
909
+            message,
910
+            font: NSFont.systemFont(ofSize: 13, weight: .semibold),
911
+            color: NSColor.white
912
+        )
913
+        label.alignment = .left
914
+        label.maximumNumberOfLines = 2
915
+        label.lineBreakMode = .byWordWrapping
916
+        row.addArrangedSubview(icon)
917
+        row.addArrangedSubview(label)
918
+        toast.addSubview(row)
919
+
920
+        view.addSubview(toast)
921
+        NSLayoutConstraint.activate([
922
+            toast.topAnchor.constraint(equalTo: view.topAnchor, constant: 14),
923
+            toast.centerXAnchor.constraint(equalTo: view.centerXAnchor),
924
+            toast.widthAnchor.constraint(lessThanOrEqualTo: view.widthAnchor, constant: -40),
925
+            toast.heightAnchor.constraint(greaterThanOrEqualToConstant: 40),
926
+
927
+            icon.widthAnchor.constraint(equalToConstant: 16),
928
+            icon.heightAnchor.constraint(equalToConstant: 16),
929
+
930
+            row.leadingAnchor.constraint(equalTo: toast.leadingAnchor, constant: 14),
931
+            row.trailingAnchor.constraint(equalTo: toast.trailingAnchor, constant: -14),
932
+            row.topAnchor.constraint(equalTo: toast.topAnchor, constant: 10),
933
+            row.bottomAnchor.constraint(equalTo: toast.bottomAnchor, constant: -10)
934
+        ])
935
+
936
+        topToastView = toast
937
+
938
+        NSAnimationContext.runAnimationGroup { context in
939
+            context.duration = 0.18
940
+            context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
941
+            toast.animator().alphaValue = 1
942
+        }
943
+
944
+        let hideWorkItem = DispatchWorkItem { [weak self, weak toast] in
945
+            guard let self, let toast else { return }
946
+            NSAnimationContext.runAnimationGroup({ context in
947
+                context.duration = 0.2
948
+                context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
949
+                toast.animator().alphaValue = 0
950
+            }, completionHandler: {
951
+                toast.removeFromSuperview()
952
+                if self.topToastView === toast {
953
+                    self.topToastView = nil
954
+                }
955
+            })
956
+        }
957
+        topToastHideWorkItem = hideWorkItem
958
+        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: hideWorkItem)
959
+    }
960
+
870 961
     private func confirmPremiumUpgrade() -> Bool {
871 962
         let alert = NSAlert()
872 963
         alert.messageText = "Already Premium"
@@ -4686,9 +4777,12 @@ private extension ViewController {
4686 4777
                     self.calendarPageMonthLabel?.stringValue = self.calendarMonthTitleText(for: self.calendarPageMonthAnchor)
4687 4778
                     self.renderCalendarMonthGrid()
4688 4779
                     self.renderCalendarSelectedDay()
4780
+                    self.showTopToast(message: "Meeting added successfully.", isError: false)
4689 4781
                 }
4690 4782
             } catch {
4691
-                self.showSimpleError("Couldn’t create meeting.", error: error)
4783
+                await MainActor.run {
4784
+                    self.showTopToast(message: "An issue occurred. Please try again.", isError: true)
4785
+                }
4692 4786
             }
4693 4787
         }
4694 4788
     }