Просмотр исходного кода

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 3 месяцев назад
Родитель
Сommit
17fc3bdd7f
1 измененных файлов с 95 добавлено и 1 удалено
  1. 95 1
      meetings_app/ViewController.swift

+ 95 - 1
meetings_app/ViewController.swift

@@ -362,6 +362,8 @@ final class ViewController: NSViewController {
     private weak var calendarPageDaySummaryLabel: NSTextField?
     private weak var calendarPageDaySummaryLabel: NSTextField?
     private var calendarPageActionPopover: NSPopover?
     private var calendarPageActionPopover: NSPopover?
     private var calendarPageCreatePopover: NSPopover?
     private var calendarPageCreatePopover: NSPopover?
+    private weak var topToastView: NSVisualEffectView?
+    private var topToastHideWorkItem: DispatchWorkItem?
 
 
     /// In-app browser navigation: `.allowAll` or `.whitelist(hostSuffixes:)` (e.g. `["google.com"]` matches `meet.google.com`).
     /// In-app browser navigation: `.allowAll` or `.whitelist(hostSuffixes:)` (e.g. `["google.com"]` matches `meet.google.com`).
     private let inAppBrowserDefaultPolicy: InAppBrowserURLPolicy = .allowAll
     private let inAppBrowserDefaultPolicy: InAppBrowserURLPolicy = .allowAll
@@ -867,6 +869,95 @@ private extension ViewController {
         alert.runModal()
         alert.runModal()
     }
     }
 
 
+    private func showTopToast(message: String, isError: Bool) {
+        topToastHideWorkItem?.cancel()
+        topToastHideWorkItem = nil
+        topToastView?.removeFromSuperview()
+        topToastView = nil
+
+        let toast = NSVisualEffectView()
+        toast.translatesAutoresizingMaskIntoConstraints = false
+        toast.material = darkModeEnabled ? .hudWindow : .popover
+        toast.blendingMode = .withinWindow
+        toast.state = .active
+        toast.wantsLayer = true
+        toast.layer?.cornerRadius = 10
+        toast.layer?.masksToBounds = true
+        toast.layer?.borderWidth = 1
+        toast.layer?.borderColor = NSColor.white.withAlphaComponent(darkModeEnabled ? 0.10 : 0.18).cgColor
+        toast.layer?.backgroundColor = NSColor.black.withAlphaComponent(darkModeEnabled ? 0.68 : 0.78).cgColor
+        toast.alphaValue = 0
+
+        let row = NSStackView()
+        row.translatesAutoresizingMaskIntoConstraints = false
+        row.orientation = .horizontal
+        row.alignment = .centerY
+        row.spacing = 8
+        row.distribution = .fill
+
+        let icon = NSImageView()
+        icon.translatesAutoresizingMaskIntoConstraints = false
+        icon.imageScaling = .scaleProportionallyDown
+        icon.image = NSImage(
+            systemSymbolName: isError ? "xmark.circle.fill" : "checkmark.circle.fill",
+            accessibilityDescription: isError ? "Error" : "Success"
+        )
+        icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
+        icon.contentTintColor = isError ? NSColor.systemRed : NSColor.systemGreen
+
+        let label = textLabel(
+            message,
+            font: NSFont.systemFont(ofSize: 13, weight: .semibold),
+            color: NSColor.white
+        )
+        label.alignment = .left
+        label.maximumNumberOfLines = 2
+        label.lineBreakMode = .byWordWrapping
+        row.addArrangedSubview(icon)
+        row.addArrangedSubview(label)
+        toast.addSubview(row)
+
+        view.addSubview(toast)
+        NSLayoutConstraint.activate([
+            toast.topAnchor.constraint(equalTo: view.topAnchor, constant: 14),
+            toast.centerXAnchor.constraint(equalTo: view.centerXAnchor),
+            toast.widthAnchor.constraint(lessThanOrEqualTo: view.widthAnchor, constant: -40),
+            toast.heightAnchor.constraint(greaterThanOrEqualToConstant: 40),
+
+            icon.widthAnchor.constraint(equalToConstant: 16),
+            icon.heightAnchor.constraint(equalToConstant: 16),
+
+            row.leadingAnchor.constraint(equalTo: toast.leadingAnchor, constant: 14),
+            row.trailingAnchor.constraint(equalTo: toast.trailingAnchor, constant: -14),
+            row.topAnchor.constraint(equalTo: toast.topAnchor, constant: 10),
+            row.bottomAnchor.constraint(equalTo: toast.bottomAnchor, constant: -10)
+        ])
+
+        topToastView = toast
+
+        NSAnimationContext.runAnimationGroup { context in
+            context.duration = 0.18
+            context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
+            toast.animator().alphaValue = 1
+        }
+
+        let hideWorkItem = DispatchWorkItem { [weak self, weak toast] in
+            guard let self, let toast else { return }
+            NSAnimationContext.runAnimationGroup({ context in
+                context.duration = 0.2
+                context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
+                toast.animator().alphaValue = 0
+            }, completionHandler: {
+                toast.removeFromSuperview()
+                if self.topToastView === toast {
+                    self.topToastView = nil
+                }
+            })
+        }
+        topToastHideWorkItem = hideWorkItem
+        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: hideWorkItem)
+    }
+
     private func confirmPremiumUpgrade() -> Bool {
     private func confirmPremiumUpgrade() -> Bool {
         let alert = NSAlert()
         let alert = NSAlert()
         alert.messageText = "Already Premium"
         alert.messageText = "Already Premium"
@@ -4686,9 +4777,12 @@ private extension ViewController {
                     self.calendarPageMonthLabel?.stringValue = self.calendarMonthTitleText(for: self.calendarPageMonthAnchor)
                     self.calendarPageMonthLabel?.stringValue = self.calendarMonthTitleText(for: self.calendarPageMonthAnchor)
                     self.renderCalendarMonthGrid()
                     self.renderCalendarMonthGrid()
                     self.renderCalendarSelectedDay()
                     self.renderCalendarSelectedDay()
+                    self.showTopToast(message: "Meeting added successfully.", isError: false)
                 }
                 }
             } catch {
             } catch {
-                self.showSimpleError("Couldn’t create meeting.", error: error)
+                await MainActor.run {
+                    self.showTopToast(message: "An issue occurred. Please try again.", isError: true)
+                }
             }
             }
         }
         }
     }
     }