Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/desktop/src-tauri/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ impl<R: tauri::Runtime, T: tauri::Manager<R>> AppExt<R> for T {
self.start_worker(&user_id).await?;
}

{
use tauri_plugin_notification::NotificationPluginExt;
self.start_notification_analytics(user_id).unwrap();
}

Ok(())
}

Expand Down
23 changes: 21 additions & 2 deletions crates/audio/src/mixed/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ impl MixedInput {
&cf::Uuid::new().to_cf_string(),
&cf::ArrayOf::from_slice(&[input_sub_device.as_ref(), output_sub_device.as_ref()]),
&cf::ArrayOf::from_slice(&[sub_tap.as_ref()]),
&input_uid, // Use input device as clock source for consistency
&input_uid,
],
);

Expand Down Expand Up @@ -155,7 +155,6 @@ impl MixedInput {
}
}

// Average the mixed sample
mixed_sample /= channel_count as f32;
mixed_buffer.push(mixed_sample);
}
Expand Down Expand Up @@ -316,5 +315,25 @@ mod tests {

handle.join().unwrap();
assert!(buffer.iter().any(|x| *x != 0.0));

{
let sample_rate = stream.sample_rate();

let mut writer = hound::WavWriter::create(
"./out.wav",
hound::WavSpec {
channels: 1,
sample_rate,
bits_per_sample: 32,
sample_format: hound::SampleFormat::Float,
},
)
.unwrap();

for sample in buffer {
writer.write_sample(sample).unwrap();
}
writer.finalize().unwrap();
}
}
}
7 changes: 7 additions & 0 deletions crates/notification-macos/examples/test_notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ fn main() {
std::thread::spawn(|| {
std::thread::sleep(Duration::from_millis(200));

setup_notification_confirm_handler(|id| {
println!("confirm: {}", id);
});
setup_notification_dismiss_handler(|id| {
println!("dismiss: {}", id);
});

let notification = Notification::builder()
.key("test_notification")
.title("Test Notification")
Expand Down
56 changes: 52 additions & 4 deletions crates/notification-macos/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,59 @@
pub use hypr_notification_interface::*;
use std::ffi::CStr;
use std::os::raw::c_char;
use std::sync::Mutex;

#[cfg(target_os = "macos")]
use swift_rs::{swift, Bool, SRString};

#[cfg(target_os = "macos")]
pub use hypr_notification_interface::*;

swift!(fn _show_notification(
title: &SRString,
message: &SRString,
url: &SRString,
timeout_seconds: f64
) -> Bool);

#[cfg(target_os = "macos")]
swift!(fn _dismiss_all_notifications() -> Bool);

static CONFIRM_CB: Mutex<Option<Box<dyn Fn(String) + Send + Sync>>> = Mutex::new(None);
static DISMISS_CB: Mutex<Option<Box<dyn Fn(String) + Send + Sync>>> = Mutex::new(None);

pub fn setup_notification_dismiss_handler<F>(f: F)
where
F: Fn(String) + Send + Sync + 'static,
{
*DISMISS_CB.lock().unwrap() = Some(Box::new(f));
}

pub fn setup_notification_confirm_handler<F>(f: F)
where
F: Fn(String) + Send + Sync + 'static,
{
*CONFIRM_CB.lock().unwrap() = Some(Box::new(f));
}

#[no_mangle]
pub extern "C" fn rust_on_notification_confirm(id_ptr: *const c_char) {
if let Some(cb) = CONFIRM_CB.lock().unwrap().as_ref() {
let id = unsafe { CStr::from_ptr(id_ptr) }
.to_str()
.unwrap()
.to_string();
cb(id);
}
}

#[no_mangle]
pub extern "C" fn rust_on_notification_dismiss(id_ptr: *const c_char) {
if let Some(cb) = DISMISS_CB.lock().unwrap().as_ref() {
let id = unsafe { CStr::from_ptr(id_ptr) }
.to_str()
.unwrap()
.to_string();
cb(id);
}
}

pub fn show(notification: &hypr_notification_interface::Notification) {
unsafe {
let title = SRString::from(notification.title.as_str());
Expand All @@ -27,6 +69,12 @@ pub fn show(notification: &hypr_notification_interface::Notification) {
}
}

pub fn dismiss_all() {
unsafe {
_dismiss_all_notifications();
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
66 changes: 47 additions & 19 deletions crates/notification-macos/swift-lib/src/lib.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ class NotificationInstance {
}
}

func dismissWithUserAction() {
self.id.uuidString.withCString { idPtr in
rustOnNotificationDismiss(idPtr)
}
dismiss()
}

deinit {
dismissTimer?.cancel()
}
Expand Down Expand Up @@ -128,10 +135,15 @@ class ClickableView: NSView {
override func mouseDown(with event: NSEvent) {
alphaValue = 0.95
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.alphaValue = 1.0 }
if let urlString = notification?.url, let url = URL(string: urlString) {
NSWorkspace.shared.open(url)
if let notification = notification {
notification.id.uuidString.withCString { idPtr in
rustOnNotificationConfirm(idPtr)
}
if let urlString = notification.url, let url = URL(string: urlString) {
NSWorkspace.shared.open(url)
}
notification.dismissWithUserAction()
}
notification?.dismiss()
}

override func viewDidMoveToWindow() {
Expand All @@ -144,8 +156,8 @@ class CloseButton: NSButton {
weak var notification: NotificationInstance?
var trackingArea: NSTrackingArea?

static let buttonSize: CGFloat = 13
static let symbolPointSize: CGFloat = 8
static let buttonSize: CGFloat = 15
static let symbolPointSize: CGFloat = 10

override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
Expand Down Expand Up @@ -204,7 +216,7 @@ class CloseButton: NSButton {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) {
self.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.5).cgColor
}
notification?.dismiss()
notification?.dismissWithUserAction()
}

override func mouseEntered(with event: NSEvent) {
Expand Down Expand Up @@ -279,7 +291,7 @@ class NotificationManager {

private struct Config {
static let notificationWidth: CGFloat = 360
static let notificationHeight: CGFloat = 82
static let notificationHeight: CGFloat = 64
static let rightMargin: CGFloat = 15
static let topMargin: CGFloat = 15
static let slideInOffset: CGFloat = 10
Expand Down Expand Up @@ -457,7 +469,7 @@ class NotificationManager {
screen: targetScreen
)

panel.level = .statusBar
panel.level = NSWindow.Level(rawValue: Int(Int32.max))
panel.isFloatingPanel = true
panel.hidesOnDeactivate = false
panel.isOpaque = false
Expand Down Expand Up @@ -556,35 +568,35 @@ class NotificationManager {
container.orientation = .horizontal
container.alignment = .centerY
container.distribution = .fill
container.spacing = 10
container.spacing = 8

let iconContainer = NSView()
iconContainer.wantsLayer = true
iconContainer.layer?.cornerRadius = 9
iconContainer.layer?.cornerRadius = 6
iconContainer.translatesAutoresizingMaskIntoConstraints = false
iconContainer.widthAnchor.constraint(equalToConstant: 42).isActive = true
iconContainer.heightAnchor.constraint(equalToConstant: 42).isActive = true
iconContainer.widthAnchor.constraint(equalToConstant: 32).isActive = true
iconContainer.heightAnchor.constraint(equalToConstant: 32).isActive = true

let iconImageView = createAppIconView()
iconContainer.addSubview(iconImageView)
NSLayoutConstraint.activate([
iconImageView.centerXAnchor.constraint(equalTo: iconContainer.centerXAnchor),
iconImageView.centerYAnchor.constraint(equalTo: iconContainer.centerYAnchor),
iconImageView.widthAnchor.constraint(equalToConstant: 32),
iconImageView.heightAnchor.constraint(equalToConstant: 32),
iconImageView.widthAnchor.constraint(equalToConstant: 24),
iconImageView.heightAnchor.constraint(equalToConstant: 24),
])

let textStack = NSStackView()
textStack.orientation = .vertical
textStack.spacing = 4
textStack.spacing = 2
textStack.alignment = .leading
textStack.distribution = .fill

textStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
textStack.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)

let titleLabel = NSTextField(labelWithString: title)
titleLabel.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
titleLabel.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
titleLabel.textColor = NSColor.labelColor
titleLabel.lineBreakMode = .byTruncatingTail
titleLabel.maximumNumberOfLines = 1
Expand All @@ -595,7 +607,7 @@ class NotificationManager {
titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)

let bodyLabel = NSTextField(labelWithString: body)
bodyLabel.font = NSFont.systemFont(ofSize: 12, weight: .light)
bodyLabel.font = NSFont.systemFont(ofSize: 11, weight: .regular)
bodyLabel.textColor = NSColor.secondaryLabelColor
bodyLabel.lineBreakMode = .byTruncatingTail
bodyLabel.maximumNumberOfLines = 1
Expand Down Expand Up @@ -634,10 +646,13 @@ class NotificationManager {

@objc private func handleActionButtonPress(_ sender: NSButton) {
guard let btn = sender as? ActionButton, let notification = btn.notification else { return }
notification.id.uuidString.withCString { idPtr in
rustOnNotificationConfirm(idPtr)
}
if let urlString = notification.url, let url = URL(string: urlString) {
NSWorkspace.shared.open(url)
}
notification.dismiss()
notification.dismissWithUserAction()
}

private func createAppIconView() -> NSImageView {
Expand Down Expand Up @@ -710,7 +725,8 @@ class NotificationManager {
display: false
)

notification.panel.orderFront(nil)
notification.panel.orderFrontRegardless()
notification.panel.makeKeyAndOrderFront(nil)

NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.3
Expand Down Expand Up @@ -780,6 +796,12 @@ class NotificationManager {
}
}

@_silgen_name("rust_on_notification_confirm")
func rustOnNotificationConfirm(_ idPtr: UnsafePointer<CChar>)

@_silgen_name("rust_on_notification_dismiss")
func rustOnNotificationDismiss(_ idPtr: UnsafePointer<CChar>)

@_cdecl("_show_notification")
public func _showNotification(
title: SRString,
Expand All @@ -802,3 +824,9 @@ public func _showNotification(
Thread.sleep(forTimeInterval: 0.1)
return true
}

@_cdecl("_dismiss_all_notifications")
public func _dismissAllNotifications() -> Bool {
NotificationManager.shared.dismissAll()
return true
}
26 changes: 26 additions & 0 deletions crates/notification/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ static RECENT_NOTIFICATIONS: OnceLock<Mutex<HashMap<String, Instant>>> = OnceLoc

const DEDUPE_WINDOW: Duration = Duration::from_secs(60 * 5);

pub enum NotificationMutation {
Confirm,
Dismiss,
}

#[cfg(target_os = "macos")]
pub fn show(notification: &hypr_notification_interface::Notification) {
let Some(key) = &notification.key else {
Expand Down Expand Up @@ -43,6 +48,27 @@ pub fn show(notification: &hypr_notification_interface::Notification) {
#[cfg(not(target_os = "macos"))]
pub fn show(notification: &hypr_notification_interface::Notification) {}

pub fn clear() {
#[cfg(target_os = "macos")]
hypr_notification_macos::dismiss_all();
}

pub fn setup_notification_dismiss_handler<F>(f: F)
where
F: Fn(String) + Send + Sync + 'static,
{
#[cfg(target_os = "macos")]
hypr_notification_macos::setup_notification_dismiss_handler(f);
}

pub fn setup_notification_confirm_handler<F>(f: F)
where
F: Fn(String) + Send + Sync + 'static,
{
#[cfg(target_os = "macos")]
hypr_notification_macos::setup_notification_confirm_handler(f);
}

#[cfg(target_os = "macos")]
pub fn is_do_not_disturb() -> bool {
match Command::new("defaults")
Expand Down
Loading
Loading