feat: native notifications + in-app update checker
- Register tauri-plugin-notification; inject initialization script that patches window.Notification to route through the native plugin and always report permission as granted — bypasses WebView2's default deny - Also grant COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS via existing PermissionRequestedEventHandler - Register tauri-plugin-updater; add check_for_update and install_update commands using the pubkey/endpoint already in tauri.conf.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+107
-7
@@ -6,6 +6,93 @@
|
||||
use tauri::{webview::{NewWindowResponse, WebviewWindowBuilder}, WebviewUrl};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
// Injected into every page before app scripts load.
|
||||
// Patches window.Notification to route through tauri-plugin-notification so
|
||||
// WebView2's default "denied" state never reaches cinny's permission check.
|
||||
// Also patches navigator.permissions.query so the React hook sees "granted".
|
||||
const NOTIFICATION_BRIDGE: &str = r#"(function(){
|
||||
function TauriNotification(title,options){
|
||||
var opts=options||{};
|
||||
try{
|
||||
window.__TAURI_INTERNALS__.invoke('send_notification',{
|
||||
title:String(title),
|
||||
body:opts.body!=null?String(opts.body):undefined
|
||||
}).catch(function(){});
|
||||
}catch(_){}
|
||||
}
|
||||
TauriNotification.prototype=Object.create(EventTarget.prototype);
|
||||
TauriNotification.prototype.constructor=TauriNotification;
|
||||
TauriNotification.prototype.close=function(){};
|
||||
Object.defineProperty(TauriNotification,'permission',{get:function(){return 'granted';},configurable:true});
|
||||
TauriNotification.requestPermission=function(){return Promise.resolve('granted');};
|
||||
TauriNotification.maxActions=0;
|
||||
Object.defineProperty(window,'Notification',{value:TauriNotification,writable:true,configurable:true});
|
||||
var _q=navigator.permissions.query.bind(navigator.permissions);
|
||||
navigator.permissions.query=function(desc){
|
||||
if(desc&&desc.name==='notifications'){
|
||||
return Promise.resolve(Object.assign(new EventTarget(),{state:'granted',onchange:null}));
|
||||
}
|
||||
return _q(desc);
|
||||
};
|
||||
})();"#;
|
||||
|
||||
#[tauri::command]
|
||||
fn send_notification(
|
||||
app: tauri::AppHandle,
|
||||
title: String,
|
||||
body: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
let mut builder = app.notification().builder().title(&title);
|
||||
if let Some(b) = &body {
|
||||
builder = builder.body(b);
|
||||
}
|
||||
builder.show().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct UpdateInfo {
|
||||
available: bool,
|
||||
version: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn check_for_update(app: tauri::AppHandle) -> Result<UpdateInfo, String> {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
match app.updater().map_err(|e| e.to_string())?.check().await {
|
||||
Ok(Some(update)) => {
|
||||
return Ok(UpdateInfo { available: true, version: Some(update.version) });
|
||||
}
|
||||
Ok(None) => return Ok(UpdateInfo { available: false, version: None }),
|
||||
Err(e) => return Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
Ok(UpdateInfo { available: false, version: None })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn install_update(app: tauri::AppHandle) -> Result<(), String> {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
if let Some(update) = app
|
||||
.updater()
|
||||
.map_err(|e| e.to_string())?
|
||||
.check()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
{
|
||||
update
|
||||
.download_and_install(|_chunk, _total| {}, || {})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_badge_count(count: u32, window: tauri::Window) -> Result<(), String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -63,14 +150,12 @@ fn set_badge_count(count: u32, window: tauri::Window) -> Result<(), String> {
|
||||
.map_err(|e| e.to_string())?;
|
||||
let old_bm = SelectObject(hdc, hbm_color);
|
||||
|
||||
// Red circle (COLORREF is 0x00BBGGRR)
|
||||
let hbrush = CreateSolidBrush(COLORREF(0x003030DD));
|
||||
let old_brush = SelectObject(hdc, hbrush);
|
||||
let hpen = CreatePen(PS_NULL, 0, COLORREF(0));
|
||||
let old_pen = SelectObject(hdc, hpen);
|
||||
let _ = Ellipse(hdc, 0, 0, size, size);
|
||||
|
||||
// White bold text centered
|
||||
let hfont = CreateFontW(
|
||||
11, 0, 0, 0, FW_BOLD.0 as i32, 0, 0, 0,
|
||||
DEFAULT_CHARSET.0 as u32,
|
||||
@@ -142,13 +227,26 @@ fn set_badge_count(count: u32, window: tauri::Window) -> Result<(), String> {
|
||||
pub fn run() {
|
||||
let port: u16 = 44548;
|
||||
let context = tauri::generate_context!();
|
||||
let builder = tauri::Builder::default();
|
||||
|
||||
builder
|
||||
.invoke_handler(tauri::generate_handler![set_badge_count])
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
set_badge_count,
|
||||
send_notification,
|
||||
check_for_update,
|
||||
install_update,
|
||||
])
|
||||
.plugin(tauri_plugin_localhost::Builder::new(port).build())
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_notification::init());
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
||||
}
|
||||
|
||||
builder
|
||||
.setup(move |app| {
|
||||
#[cfg(debug_assertions)]
|
||||
let window_url = WebviewUrl::App(Default::default());
|
||||
@@ -162,6 +260,7 @@ pub fn run() {
|
||||
let app_handle = app.handle().clone();
|
||||
let window = WebviewWindowBuilder::new(app, "main".to_string(), window_url)
|
||||
.title("Lotus Chat")
|
||||
.initialization_script(NOTIFICATION_BRIDGE)
|
||||
.disable_drag_drop_handler()
|
||||
.on_new_window(move |url, _features| {
|
||||
let _ = app_handle.opener().open_url(url.as_str(), None::<&str>);
|
||||
@@ -169,8 +268,7 @@ pub fn run() {
|
||||
})
|
||||
.build()?;
|
||||
|
||||
// Grant camera and microphone to WebView2 automatically.
|
||||
// Windows requires an explicit PermissionRequested COM event handler.
|
||||
// Auto-grant camera, microphone, and notification permissions in WebView2.
|
||||
#[cfg(target_os = "windows")]
|
||||
window.with_webview(|webview| {
|
||||
use webview2_com::{
|
||||
@@ -178,6 +276,7 @@ pub fn run() {
|
||||
COREWEBVIEW2_PERMISSION_KIND,
|
||||
COREWEBVIEW2_PERMISSION_KIND_CAMERA,
|
||||
COREWEBVIEW2_PERMISSION_KIND_MICROPHONE,
|
||||
COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS,
|
||||
COREWEBVIEW2_PERMISSION_STATE_ALLOW,
|
||||
},
|
||||
PermissionRequestedEventHandler,
|
||||
@@ -192,6 +291,7 @@ pub fn run() {
|
||||
unsafe { args.PermissionKind(&mut kind) }?;
|
||||
if kind == COREWEBVIEW2_PERMISSION_KIND_MICROPHONE
|
||||
|| kind == COREWEBVIEW2_PERMISSION_KIND_CAMERA
|
||||
|| kind == COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS
|
||||
{
|
||||
unsafe {
|
||||
args.SetState(COREWEBVIEW2_PERMISSION_STATE_ALLOW)
|
||||
|
||||
Reference in New Issue
Block a user