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); }