diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart
index 521413647..b2f70cdd5 100644
--- a/flutter/lib/desktop/pages/server_page.dart
+++ b/flutter/lib/desktop/pages/server_page.dart
@@ -521,6 +521,38 @@ class _CmControlPanel extends StatelessWidget {
     return Column(
       mainAxisAlignment: MainAxisAlignment.end,
       children: [
+        Offstage(
+          offstage: !client.inVoiceCall,
+          child: buildButton(context,
+              color: Colors.purple,
+              onClick: () => closeVoiceCall(),
+              icon: Icon(Icons.reply, color: Colors.white),
+              text: "Stop voice call",
+              textColor: Colors.white),
+        ),
+        Offstage(
+          offstage: !client.incomingVoiceCall,
+          child: Row(
+            children: [
+              Expanded(
+                child: buildButton(context,
+                    color: MyTheme.accent,
+                    onClick: () => handleVoiceCall(true),
+                    icon: Icon(Icons.phone, color: Colors.white),
+                    text: "Accept",
+                    textColor: Colors.white),
+              ),
+              Expanded(
+                child: buildButton(context,
+                    color: Colors.red,
+                    onClick: () => handleVoiceCall(false),
+                    icon: Icon(Icons.phone, color: Colors.white),
+                    text: "Deny",
+                    textColor: Colors.white),
+              )
+            ],
+          ),
+        ),
         Offstage(
           offstage: !client.fromSwitch,
           child: buildButton(context,
@@ -626,7 +658,7 @@ class _CmControlPanel extends StatelessWidget {
         .marginSymmetric(horizontal: showElevation ? 0 : bigMargin);
   }
 
-  buildButton(
+  Widget buildButton(
     BuildContext context, {
     required Color? color,
     required Function() onClick,
@@ -692,6 +724,14 @@ class _CmControlPanel extends StatelessWidget {
   void handleSwitchBack(BuildContext context) {
     bind.cmSwitchBack(connId: client.id);
   }
+
+  void handleVoiceCall(bool accept) {
+    bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept);
+  }
+
+  void closeVoiceCall() {
+    bind.cmCloseVoiceCall(id: client.id);
+  }
 }
 
 void checkClickTime(int id, Function() callback) async {
diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart
index d06be52fa..653ff37b1 100644
--- a/flutter/lib/desktop/widgets/remote_menubar.dart
+++ b/flutter/lib/desktop/widgets/remote_menubar.dart
@@ -713,19 +713,27 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
       () {
         switch (widget.ffi.chatModel.voiceCallStatus.value) {
           case VoiceCallStatus.waitingForResponse:
-            return SvgPicture.asset(
-              "assets/voice_call_waiting.svg",
-              color: _MenubarTheme.commonColor,
-              width: Theme.of(context).iconTheme.size ?? 24.0,
-              height: Theme.of(context).iconTheme.size ?? 24.0,
-            );
-            break;
+            return IconButton(
+                onPressed: () {
+                  widget.ffi.chatModel.closeVoiceCall(widget.id);
+                },
+                icon: SvgPicture.asset(
+                  "assets/voice_call_waiting.svg",
+                  color: Colors.red,
+                  width: Theme.of(context).iconTheme.size ?? 24.0,
+                  height: Theme.of(context).iconTheme.size ?? 24.0,
+                ));
           case VoiceCallStatus.connected:
-            return SvgPicture.asset(
-              "assets/voice_call.svg",
-              color: Colors.red,
-              width: Theme.of(context).iconTheme.size ?? 24.0,
-              height: Theme.of(context).iconTheme.size ?? 24.0,
+            return IconButton(
+              onPressed: () {
+                widget.ffi.chatModel.closeVoiceCall(widget.id);
+              },
+              icon: SvgPicture.asset(
+                "assets/voice_call.svg",
+                color: Colors.red,
+                width: Theme.of(context).iconTheme.size ?? 24.0,
+                height: Theme.of(context).iconTheme.size ?? 24.0,
+              ),
             );
           default:
             return const Offstage();
diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart
index 61602c5b4..14af96570 100644
--- a/flutter/lib/models/chat_model.dart
+++ b/flutter/lib/models/chat_model.dart
@@ -316,6 +316,10 @@ class ChatModel with ChangeNotifier {
       _voiceCallStatus.value = VoiceCallStatus.incoming;
     }
   }
+
+  void closeVoiceCall(String id) {
+    bind.sessionCloseVoiceCall(id: id);
+  }
 }
 
 enum VoiceCallStatus {
diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart
index 2a4c68839..a2fe205af 100644
--- a/flutter/lib/models/model.dart
+++ b/flutter/lib/models/model.dart
@@ -216,6 +216,8 @@ class FfiModel with ChangeNotifier {
       } else if (name == "on_voice_call_incoming") {
         // Voice call is requested by the peer.
         parent.target?.chatModel.onVoiceCallIncoming();
+      } else if (name == "update_voice_call_state") {
+        parent.target?.serverModel.updateVoiceCallState(evt);
       } else {
         debugPrint("Unknown event name: $name");
       }
diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart
index 56dca4cdf..6cd905c37 100644
--- a/flutter/lib/models/server_model.dart
+++ b/flutter/lib/models/server_model.dart
@@ -579,6 +579,20 @@ class ServerModel with ChangeNotifier {
       notifyListeners();
     }
   }
+
+  void updateVoiceCallState(Map<String, dynamic> evt) {
+    try {
+      final client = Client.fromJson(jsonDecode(evt["client"]));
+      final index = _clients.indexWhere((element) => element.id == client.id);
+      if (index != -1) {
+        _clients[index].inVoiceCall = evt['in_voice_call'];
+        _clients[index].incomingVoiceCall = evt['incoming_voice_call'];
+        notifyListeners();
+      }
+    } catch (e) {
+      debugPrint("updateVoiceCallState failed: $e");
+    }
+  }
 }
 
 enum ClientType {
@@ -602,6 +616,8 @@ class Client {
   bool recording = false;
   bool disconnected = false;
   bool fromSwitch = false;
+  bool inVoiceCall = false;
+  bool incomingVoiceCall = false;
 
   RxBool hasUnreadChatMessage = false.obs;
 
@@ -623,6 +639,8 @@ class Client {
     recording = json['recording'];
     disconnected = json['disconnected'];
     fromSwitch = json['from_switch'];
+    inVoiceCall = json['in_voice_call'];
+    incomingVoiceCall = json['incoming_voice_call'];
   }
 
   Map<String, dynamic> toJson() {
diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs
index d49227864..aa51df378 100644
--- a/src/client/io_loop.rs
+++ b/src/client/io_loop.rs
@@ -754,6 +754,7 @@ impl<T: InvokeUiSession> Remote<T> {
             Data::CloseVoiceCall => {
                 self.stop_voice_call();
                 let msg = new_voice_call_request(false);
+                self.handler.on_voice_call_closed("Closed manually by the peer");
                 allow_err!(peer.send(&msg).await);
             }
             _ => {}
diff --git a/src/flutter.rs b/src/flutter.rs
index 4249e4d94..a27a9d4e1 100644
--- a/src/flutter.rs
+++ b/src/flutter.rs
@@ -538,16 +538,9 @@ pub mod connection_manager {
             self.push_event("show_elevation", vec![("show", &show.to_string())]);
         }
 
-        fn voice_call_started(&self, id: i32) {
-            self.push_event("voice_call_started", vec![("show", &id.to_string())]);
-        }
-
-        fn voice_call_incoming(&self, id: i32) {
-            self.push_event("voice_call_incoming", vec![("id", &id.to_string())]);
-        }
-
-        fn voice_call_closed(&self, id: i32, reason: &str) {
-            self.push_event("voice_call_closed", vec![("id", &id.to_string()), ("reason", &reason.to_string())]);
+        fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) {
+            let client_json = serde_json::to_string(&client).unwrap_or("".into());
+            self.push_event("update_voice_call_state", vec![("client", &client_json)]);
         }
     }
 
diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs
index 588733c37..cfca0e082 100644
--- a/src/flutter_ffi.rs
+++ b/src/flutter_ffi.rs
@@ -838,6 +838,14 @@ pub fn session_close_voice_call(id: String) {
     }
 }
 
+pub fn cm_handle_incoming_voice_call(id: i32, accept: bool) {
+    crate::ui_cm_interface::handle_incoming_voice_call(id, accept);
+}
+
+pub fn cm_close_voice_call(id: i32) {
+    crate::ui_cm_interface::close_voice_call(id);
+}
+
 pub fn main_get_last_remote_id() -> String {
     LocalConfig::get_remote_id()
 }
diff --git a/src/server/connection.rs b/src/server/connection.rs
index da0126213..1e88b9b05 100644
--- a/src/server/connection.rs
+++ b/src/server/connection.rs
@@ -1636,7 +1636,7 @@ impl Connection {
         if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) {
             set_sound_input(sound_input);
             // Notify the connection manager.
-            self.send_to_cm(Data::CloseVoiceCall("Closed manually by the peer".to_owned()));
+            self.send_to_cm(Data::CloseVoiceCall("".to_owned()));
         }
     }
 
diff --git a/src/ui/cm.rs b/src/ui/cm.rs
index dc941c3d0..cce553154 100644
--- a/src/ui/cm.rs
+++ b/src/ui/cm.rs
@@ -56,16 +56,15 @@ impl InvokeUiCM for SciterHandler {
         self.call("showElevation", &make_args!(show));
     }
 
-    fn voice_call_started(&self, id: i32) {
-        self.call("voice_call_started", &make_args!(id));
-    }
-
-    fn voice_call_incoming(&self, id: i32) {
-        self.call("voice_call_incoming", &make_args!(id));
-    }
-
-    fn voice_call_closed(&self, id: i32, reason: &str) {
-        self.call("voice_call_incoming", &make_args!(id, reason));
+    fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) {
+        self.call(
+            "updateVoiceCallState",
+            &make_args!(
+                client.id,
+                client.in_voice_call,
+                client.incoming_voice_call
+            ),
+        );
     }
 }
 
diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs
index 1120a1731..ccddab0ee 100644
--- a/src/ui_cm_interface.rs
+++ b/src/ui_cm_interface.rs
@@ -49,6 +49,8 @@ pub struct Client {
     pub restart: bool,
     pub recording: bool,
     pub from_switch: bool,
+    pub in_voice_call: bool,
+    pub incoming_voice_call: bool,
     #[serde(skip)]
     tx: UnboundedSender<Data>,
 }
@@ -89,11 +91,7 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized {
 
     fn show_elevation(&self, show: bool);
 
-    fn voice_call_started(&self, id: i32);
-
-    fn voice_call_incoming(&self, id: i32);
-
-    fn voice_call_closed(&self, id: i32, reason: &str);
+    fn update_voice_call_state(&self, client: &Client);
 }
 
 impl<T: InvokeUiCM> Deref for ConnectionManager<T> {
@@ -144,6 +142,8 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
             recording,
             from_switch,
             tx,
+            in_voice_call: false,
+            incoming_voice_call: false
         };
         CLIENTS
             .write()
@@ -188,15 +188,27 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
     }
 
     fn voice_call_started(&self, id: i32) {
-        self.ui_handler.voice_call_started(id);
+        if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) {
+            client.incoming_voice_call = false;
+            client.in_voice_call = true;
+            self.ui_handler.update_voice_call_state(client);
+        }
     }
 
     fn voice_call_incoming(&self, id: i32) {
-        self.ui_handler.voice_call_incoming(id);
+        if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) {
+            client.incoming_voice_call = true;
+            client.in_voice_call = false;
+            self.ui_handler.update_voice_call_state(client);
+        }
     }
 
-    fn voice_call_closed(&self, id: i32, reason: &str) {
-        self.ui_handler.voice_call_closed(id, reason);
+    fn voice_call_closed(&self, id: i32, _reason: &str) {
+        if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) {
+            client.incoming_voice_call = false;
+            client.in_voice_call = false;
+            self.ui_handler.update_voice_call_state(client);
+        }
     }
 }
 
@@ -832,3 +844,17 @@ pub fn elevate_portable(_id: i32) {
         }
     }
 }
+
+#[inline]
+pub fn handle_incoming_voice_call(id: i32, accept: bool) {
+    if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) {
+        allow_err!(client.tx.send(Data::VoiceCallResponse(accept)));
+    };
+}
+
+#[inline]
+pub fn close_voice_call(id: i32) {
+    if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) {
+        allow_err!(client.tx.send(Data::CloseVoiceCall("".to_owned())));
+    };
+}
\ No newline at end of file
diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs
index f63bbd081..cd0bdcde2 100644
--- a/src/ui_session_interface.rs
+++ b/src/ui_session_interface.rs
@@ -661,7 +661,7 @@ impl<T: InvokeUiSession> Session<T> {
     pub fn request_voice_call(&self) {
         self.send(Data::NewVoiceCall);
     }
-
+    
     pub fn close_voice_call(&self) {
         self.send(Data::CloseVoiceCall);
     }