diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart
index ef53b2c41..1a0d59e16 100644
--- a/flutter/lib/common.dart
+++ b/flutter/lib/common.dart
@@ -194,14 +194,20 @@ void msgBox(String type, String title, String text, {bool? hasCancel}) {
               style: TextStyle(color: MyTheme.accent))));
 
   SmartDialog.dismiss();
-  final buttons = [
-    wrap(Translator.call('OK'), () {
-      SmartDialog.dismiss();
-      backToHome();
-    })
-  ];
+  List<Widget> buttons = [];
+  if (type != "connecting" && type != "success" && type.indexOf("nook") < 0) {
+    buttons.insert(
+        0,
+        wrap(Translator.call('OK'), () {
+          SmartDialog.dismiss();
+          backToHome();
+        }));
+  }
   if (hasCancel == null) {
-    hasCancel = type != 'error';
+    // hasCancel = type != 'error';
+    hasCancel = type.indexOf("error") < 0 &&
+        type.indexOf("nocancel") < 0 &&
+        type != "restarting";
   }
   if (hasCancel) {
     buttons.insert(
@@ -210,6 +216,14 @@ void msgBox(String type, String title, String text, {bool? hasCancel}) {
           SmartDialog.dismiss();
         }));
   }
+  // TODO: test this button
+  if (type.indexOf("hasclose") >= 0) {
+    buttons.insert(
+        0,
+        wrap(Translator.call('Close'), () {
+          SmartDialog.dismiss();
+        }));
+  }
   DialogManager.show((setState, close) => CustomAlertDialog(
       title: Text(translate(title), style: TextStyle(fontSize: 21)),
       content: Text(Translator.call(text), style: TextStyle(fontSize: 15)),
diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart
index da5ad1455..da7a317a8 100644
--- a/flutter/lib/desktop/pages/remote_page.dart
+++ b/flutter/lib/desktop/pages/remote_page.dart
@@ -604,8 +604,12 @@ class _RemotePageState extends State<RemotePage>
           await bind.getSessionToggleOption(id: id, arg: 'privacy-mode') !=
               true) {
         more.add(PopupMenuItem<String>(
-            child: Text(translate((_ffi.ffiModel.inputBlocked ? 'Unb' : 'B') +
-                'lock user input')),
+            child: Consumer<FfiModel>(
+                builder: (_context, ffiModel, _child) => () {
+                      return Text(translate(
+                          (ffiModel.inputBlocked ? 'Unb' : 'B') +
+                              'lock user input'));
+                    }()),
             value: 'block-input'));
       }
     }
@@ -951,7 +955,11 @@ void showOptions(String id) async {
       more.add(getToggle(
           id, setState, 'lock-after-session-end', 'Lock after session end'));
       if (pi.platform == 'Windows') {
-        more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode'));
+        more.add(Consumer<FfiModel>(
+            builder: (_context, _ffiModel, _child) => () {
+                  return getToggle(
+                      id, setState, 'privacy-mode', 'Privacy mode');
+                }()));
       }
     }
     var setQuality = (String? value) {
diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart
index c7295f57e..4f295e377 100644
--- a/flutter/lib/models/model.dart
+++ b/flutter/lib/models/model.dart
@@ -173,6 +173,10 @@ class FfiModel with ChangeNotifier {
         parent.target?.serverModel.onClientRemove(evt);
       } else if (name == 'update_quality_status') {
         parent.target?.qualityMonitorModel.updateQualityStatus(evt);
+      } else if (name == 'update_block_input_state') {
+        updateBlockInputState(evt);
+      } else if (name == 'update_privacy_mode') {
+        updatePrivacyMode(evt);
       }
     };
   }
@@ -228,6 +232,10 @@ class FfiModel with ChangeNotifier {
         parent.target?.serverModel.onClientRemove(evt);
       } else if (name == 'update_quality_status') {
         parent.target?.qualityMonitorModel.updateQualityStatus(evt);
+      } else if (name == 'update_block_input_state') {
+        updateBlockInputState(evt);
+      } else if (name == 'update_privacy_mode') {
+        updatePrivacyMode(evt);
       }
     };
     platformFFI.setEventCallback(cb);
@@ -331,6 +339,15 @@ class FfiModel with ChangeNotifier {
     }
     notifyListeners();
   }
+
+  updateBlockInputState(Map<String, dynamic> evt) {
+    _inputBlocked = evt['input_state'] == 'on';
+    notifyListeners();
+  }
+
+  updatePrivacyMode(Map<String, dynamic> evt) {
+    notifyListeners();
+  }
 }
 
 class ImageModel with ChangeNotifier {
diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart
index bea110dab..4da0dca7f 100644
--- a/flutter/lib/utils/multi_window_manager.dart
+++ b/flutter/lib/utils/multi_window_manager.dart
@@ -1,4 +1,5 @@
 import 'dart:convert';
+import 'dart:ui';
 
 import 'package:desktop_multi_window/desktop_multi_window.dart';
 import 'package:flutter/services.dart';
diff --git a/src/client.rs b/src/client.rs
index 89d66c6ca..3c1e5c3c3 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -1004,6 +1004,13 @@ impl LoginConfigHandler {
         Some(msg_out)
     }
 
+    /// Get [`PeerConfig`] of the current [`LoginConfigHandler`].
+    ///
+    /// # Arguments
+    pub fn get_config(&mut self) -> &mut PeerConfig {
+        &mut self.config
+    }
+
     /// Get [`OptionMessage`] of the current [`LoginConfigHandler`].
     /// Return `None` if there's no option, for example, when the session is only for file transfer.
     ///
diff --git a/src/common.rs b/src/common.rs
index 605435956..5c387c07e 100644
--- a/src/common.rs
+++ b/src/common.rs
@@ -104,6 +104,19 @@ pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc<Mutex<String>>>)
     }
 }
 
+pub async fn send_opts_after_login(
+    config: &crate::client::LoginConfigHandler,
+    peer: &mut hbb_common::tcp::FramedStream,
+) {
+    if let Some(opts) = config.get_option_message_after_login() {
+        let mut misc = Misc::new();
+        misc.set_option(opts);
+        let mut msg_out = Message::new();
+        msg_out.set_misc(misc);
+        allow_err!(peer.send(&msg_out).await);
+    }
+}
+
 #[cfg(feature = "use_rubato")]
 pub fn resample_channels(
     data: &[f32],
diff --git a/src/flutter.rs b/src/flutter.rs
index d83e37b4e..bb8881c58 100644
--- a/src/flutter.rs
+++ b/src/flutter.rs
@@ -76,7 +76,7 @@ impl Session {
         // TODO close
         // Self::close();
         let events2ui = Arc::new(RwLock::new(events2ui));
-        let mut session = Session {
+        let session = Session {
             id: session_id.clone(),
             sender: Default::default(),
             lc: Default::default(),
@@ -663,6 +663,8 @@ struct Connection {
 }
 
 impl Connection {
+    // TODO: Similar to remote::start_clipboard
+    // merge the code
     fn start_clipboard(
         tx_protobuf: mpsc::UnboundedSender<Data>,
         lc: Arc<RwLock<LoginConfigHandler>>,
@@ -842,6 +844,7 @@ impl Connection {
                 Some(message::Union::VideoFrame(vf)) => {
                     if !self.first_frame {
                         self.first_frame = true;
+                        common::send_opts_after_login(&self.session.lc.read().unwrap(), peer).await;
                     }
                     let incomming_format = CodecFormat::from(&vf);
                     if self.video_format != incomming_format {
@@ -1083,6 +1086,11 @@ impl Connection {
                         self.session.msgbox("error", "Connection Error", &c);
                         return false;
                     }
+                    Some(misc::Union::BackNotification(notification)) => {
+                        if !self.handle_back_notification(notification).await {
+                            return false;
+                        }
+                    }
                     _ => {}
                 },
                 Some(message::Union::TestDelay(t)) => {
@@ -1107,6 +1115,130 @@ impl Connection {
         true
     }
 
+    async fn handle_back_notification(&mut self, notification: BackNotification) -> bool {
+        match notification.union {
+            Some(back_notification::Union::BlockInputState(state)) => {
+                self.handle_back_msg_block_input(
+                    state.enum_value_or(back_notification::BlockInputState::BlkStateUnknown),
+                )
+                .await;
+            }
+            Some(back_notification::Union::PrivacyModeState(state)) => {
+                if !self
+                    .handle_back_msg_privacy_mode(
+                        state.enum_value_or(back_notification::PrivacyModeState::PrvStateUnknown),
+                    )
+                    .await
+                {
+                    return false;
+                }
+            }
+            _ => {}
+        }
+        true
+    }
+
+    #[inline(always)]
+    fn update_block_input_state(&mut self, on: bool) {
+        self.session.push_event(
+            "update_block_input_state",
+            [("input_state", if on { "on" } else { "off" })].into(),
+        );
+    }
+
+    async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) {
+        match state {
+            back_notification::BlockInputState::BlkOnSucceeded => {
+                self.update_block_input_state(true);
+            }
+            back_notification::BlockInputState::BlkOnFailed => {
+                self.session
+                    .msgbox("custom-error", "Block user input", "Failed");
+                self.update_block_input_state(false);
+            }
+            back_notification::BlockInputState::BlkOffSucceeded => {
+                self.update_block_input_state(false);
+            }
+            back_notification::BlockInputState::BlkOffFailed => {
+                self.session
+                    .msgbox("custom-error", "Unblock user input", "Failed");
+            }
+            _ => {}
+        }
+    }
+
+    #[inline(always)]
+    fn update_privacy_mode(&mut self, on: bool) {
+        let mut config = self.session.load_config();
+        config.privacy_mode = on;
+        self.session.save_config(&config);
+        self.session.lc.write().unwrap().get_config().privacy_mode = on;
+        self.session.push_event("update_privacy_mode", [].into());
+    }
+
+    async fn handle_back_msg_privacy_mode(
+        &mut self,
+        state: back_notification::PrivacyModeState,
+    ) -> bool {
+        match state {
+            back_notification::PrivacyModeState::PrvOnByOther => {
+                self.session.msgbox(
+                    "error",
+                    "Connecting...",
+                    "Someone turns on privacy mode, exit",
+                );
+                return false;
+            }
+            back_notification::PrivacyModeState::PrvNotSupported => {
+                self.session
+                    .msgbox("custom-error", "Privacy mode", "Unsupported");
+                self.update_privacy_mode(false);
+            }
+            back_notification::PrivacyModeState::PrvOnSucceeded => {
+                self.session
+                    .msgbox("custom-nocancel", "Privacy mode", "In privacy mode");
+                self.update_privacy_mode(true);
+            }
+            back_notification::PrivacyModeState::PrvOnFailedDenied => {
+                self.session
+                    .msgbox("custom-error", "Privacy mode", "Peer denied");
+                self.update_privacy_mode(false);
+            }
+            back_notification::PrivacyModeState::PrvOnFailedPlugin => {
+                self.session
+                    .msgbox("custom-error", "Privacy mode", "Please install plugins");
+                self.update_privacy_mode(false);
+            }
+            back_notification::PrivacyModeState::PrvOnFailed => {
+                self.session
+                    .msgbox("custom-error", "Privacy mode", "Failed");
+                self.update_privacy_mode(false);
+            }
+            back_notification::PrivacyModeState::PrvOffSucceeded => {
+                self.session
+                    .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode");
+                self.update_privacy_mode(false);
+            }
+            back_notification::PrivacyModeState::PrvOffByPeer => {
+                self.session
+                    .msgbox("custom-error", "Privacy mode", "Peer exit");
+                self.update_privacy_mode(false);
+            }
+            back_notification::PrivacyModeState::PrvOffFailed => {
+                self.session
+                    .msgbox("custom-error", "Privacy mode", "Failed to turn off");
+            }
+            back_notification::PrivacyModeState::PrvOffUnknown => {
+                self.session
+                    .msgbox("custom-error", "Privacy mode", "Turned off");
+                // log::error!("Privacy mode is turned off with unknown reason");
+                self.update_privacy_mode(false);
+            }
+            _ => {}
+        }
+        true
+    }
+
     async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool {
         match data {
             Data::Close => {
diff --git a/src/ui/remote.rs b/src/ui/remote.rs
index 5d036dee2..060aa59db 100644
--- a/src/ui/remote.rs
+++ b/src/ui/remote.rs
@@ -25,8 +25,12 @@ use clipboard::{
 use enigo::{self, Enigo, KeyboardControllable};
 use hbb_common::{
     allow_err,
-    config::{Config, LocalConfig, PeerConfig},
-    fs, log,
+    config::{Config, LocalConfig, PeerConfig, TransferSerde},
+    fs::{
+        self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm,
+        DigestCheckResult, RemoveJobMeta, TransferJobMeta,
+    },
+    get_version_number, log,
     message_proto::{permission_info::Permission, *},
     protobuf::Message as _,
     rendezvous_proto::ConnType,
@@ -38,14 +42,6 @@ use hbb_common::{
     },
     Stream,
 };
-use hbb_common::{config::TransferSerde, fs::TransferJobMeta};
-use hbb_common::{
-    fs::{
-        can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult,
-        RemoveJobMeta,
-    },
-    get_version_number,
-};
 
 #[cfg(windows)]
 use crate::clipboard_file::*;
@@ -2071,22 +2067,6 @@ impl Remote {
         true
     }
 
-    async fn send_opts_after_login(&self, peer: &mut Stream) {
-        if let Some(opts) = self
-        .handler
-        .lc
-        .read()
-        .unwrap()
-        .get_option_message_after_login()
-    {
-        let mut misc = Misc::new();
-        misc.set_option(opts);
-        let mut msg_out = Message::new();
-        msg_out.set_misc(misc);
-        allow_err!(peer.send(&msg_out).await);
-    }
-    }
-
     async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool {
         if let Ok(msg_in) = Message::parse_from_bytes(&data) {
             match msg_in.union {
@@ -2095,7 +2075,7 @@ impl Remote {
                         self.first_frame = true;
                         self.handler.call2("closeSuccess", &make_args!());
                         self.handler.call("adaptSize", &make_args!());
-                        self.send_opts_after_login(peer).await;
+                        common::send_opts_after_login(&self.handler.lc.read().unwrap(), peer).await;
                     }
                     let incomming_format = CodecFormat::from(&vf);
                     if self.video_format != incomming_format {