Skip to content

Commit 66b7bf0

Browse files
committed
feat: manage SMP and XFTP servers
1 parent 44d61de commit 66b7bf0

4 files changed

Lines changed: 484 additions & 23 deletions

File tree

src/app.rs

Lines changed: 173 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use std::{
99
use crate::event::{AppEvent, Event, EventHandler};
1010
use crate::{
1111
chat::{
12-
ChatDeletionSettings, ChatFeatures, ChatRef, ChatSummary, Message, Profile, SimplexEvent,
13-
User,
12+
ChatDeletionSettings, ChatFeatures, ChatRef, ChatSummary, Message, Profile, ServerEntry,
13+
ServerProtocol, SimplexEvent, User,
1414
},
1515
preferences::Preferences,
1616
simplex::SimplexApi,
@@ -41,6 +41,7 @@ pub enum InputMode {
4141
None,
4242
CreateProfile,
4343
ConfirmDeleteProfile,
44+
AddServer,
4445
}
4546

4647
#[derive(Clone, Debug)]
@@ -94,7 +95,9 @@ pub struct App {
9495
pub preferences: Preferences,
9596
pub auto_delete_seconds: i64,
9697
pub auto_delete_pending: Option<i64>,
97-
pub smp_servers: Vec<String>,
98+
pub servers: Vec<ServerEntry>,
99+
pub server_protocol: ServerProtocol,
100+
pub selected_server: usize,
98101
pub chat_features: ChatFeatures,
99102
pub invitation_link: Option<String>,
100103
pub invitation_loading: bool,
@@ -150,7 +153,9 @@ impl Default for App {
150153
preferences: Preferences::default(),
151154
auto_delete_seconds: 0,
152155
auto_delete_pending: None,
153-
smp_servers: Vec::new(),
156+
servers: Vec::new(),
157+
server_protocol: ServerProtocol::Smp,
158+
selected_server: 0,
154159
chat_features: ChatFeatures::default(),
155160
invitation_link: None,
156161
invitation_loading: false,
@@ -189,7 +194,7 @@ impl App {
189194
"Privacy & Security",
190195
"Appearance",
191196
"Chat Features",
192-
"SMP Servers",
197+
"Servers",
193198
"About",
194199
];
195200

@@ -287,6 +292,23 @@ impl App {
287292
}
288293
return Ok(());
289294
}
295+
if self.input_mode == InputMode::AddServer {
296+
match key.code {
297+
KeyCode::Esc => {
298+
self.input_mode = InputMode::None;
299+
self.input.clear();
300+
}
301+
KeyCode::Enter if !self.input.trim().is_empty() => self.add_server(),
302+
KeyCode::Backspace => {
303+
self.input.pop();
304+
}
305+
KeyCode::Char(character) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
306+
self.input.push(character)
307+
}
308+
_ => {}
309+
}
310+
return Ok(());
311+
}
290312
if self.chat_deletion_dialog.is_some() {
291313
match key.code {
292314
KeyCode::Esc => self.chat_deletion_dialog = None,
@@ -352,7 +374,39 @@ impl App {
352374
self.input_mode = InputMode::ConfirmDeleteProfile;
353375
}
354376
KeyCode::Enter | KeyCode::Char(' ') if self.section == Section::Settings => {
355-
self.activate_setting()
377+
if self.selected_setting == 5 {
378+
self.toggle_selected_server();
379+
} else {
380+
self.activate_setting()
381+
}
382+
}
383+
KeyCode::Char('a')
384+
if self.section == Section::Settings && self.selected_setting == 5 =>
385+
{
386+
self.input_mode = InputMode::AddServer;
387+
self.input.clear();
388+
}
389+
KeyCode::Char('p')
390+
if self.section == Section::Settings && self.selected_setting == 5 =>
391+
{
392+
self.server_protocol = match self.server_protocol {
393+
ServerProtocol::Smp => ServerProtocol::Xftp,
394+
ServerProtocol::Xftp => ServerProtocol::Smp,
395+
};
396+
self.selected_server = 0;
397+
}
398+
KeyCode::Char('k')
399+
if self.section == Section::Settings && self.selected_setting == 5 =>
400+
{
401+
self.selected_server = self.selected_server.saturating_sub(1);
402+
}
403+
KeyCode::Char('j')
404+
if self.section == Section::Settings && self.selected_setting == 5 =>
405+
{
406+
self.selected_server = self
407+
.selected_server
408+
.saturating_add(1)
409+
.min(self.visible_servers().len().saturating_sub(1));
356410
}
357411
KeyCode::Char('c')
358412
if self.section == Section::Settings && self.selected_setting == 3 =>
@@ -764,7 +818,15 @@ impl App {
764818
replace_message(messages, message);
765819
}
766820
}
767-
SimplexEvent::ServersLoaded(servers) => self.smp_servers = servers,
821+
SimplexEvent::ServersLoaded(servers) => {
822+
self.servers = servers;
823+
self.selected_server = self
824+
.selected_server
825+
.min(self.visible_servers().len().saturating_sub(1));
826+
}
827+
SimplexEvent::ServersUpdateFailed(error) => {
828+
self.notice = Some(format!("Could not update servers: {error}"));
829+
}
768830
SimplexEvent::ChatFeaturesLoaded(features) => self.chat_features = features,
769831
SimplexEvent::InvitationCreated(link) => {
770832
self.invitation_link = Some(link);
@@ -1042,6 +1104,52 @@ impl App {
10421104
});
10431105
}
10441106

1107+
pub fn visible_servers(&self) -> Vec<&ServerEntry> {
1108+
self.servers
1109+
.iter()
1110+
.filter(|server| server.protocol == self.server_protocol)
1111+
.collect()
1112+
}
1113+
1114+
fn toggle_selected_server(&mut self) {
1115+
let Some(user_id) = self.active_user().map(|user| user.id) else {
1116+
self.notice = Some("Create a profile first".into());
1117+
return;
1118+
};
1119+
let Some(server) = self.visible_servers().get(self.selected_server).copied() else {
1120+
self.notice = Some("Add a server first".into());
1121+
return;
1122+
};
1123+
let protocol = server.protocol;
1124+
let address = server.address.clone();
1125+
let enabled = !server.enabled;
1126+
self.notice = Some("Updating server configuration…".into());
1127+
let _ = self
1128+
.simplex_commands
1129+
.send(SimplexCommand::SetServerEnabled {
1130+
user_id,
1131+
protocol,
1132+
address,
1133+
enabled,
1134+
});
1135+
}
1136+
1137+
fn add_server(&mut self) {
1138+
let Some(user_id) = self.active_user().map(|user| user.id) else {
1139+
self.notice = Some("Create a profile first".into());
1140+
return;
1141+
};
1142+
let address = self.input.trim().to_owned();
1143+
self.input_mode = InputMode::None;
1144+
self.input.clear();
1145+
self.notice = Some(format!("Adding {} server…", self.server_protocol.label()));
1146+
let _ = self.simplex_commands.send(SimplexCommand::AddServer {
1147+
user_id,
1148+
protocol: self.server_protocol,
1149+
address,
1150+
});
1151+
}
1152+
10451153
fn activate_profile(&mut self) {
10461154
let Some(profile) = self.profiles.get(self.selected_profile) else {
10471155
self.input_mode = InputMode::CreateProfile;
@@ -1787,4 +1895,62 @@ mod tests {
17871895
};
17881896
assert_eq!(file_id, 41);
17891897
}
1898+
1899+
#[tokio::test]
1900+
async fn server_settings_toggle_and_add_both_protocols() {
1901+
let (commands, receiver) = mpsc::channel();
1902+
let mut app = App {
1903+
section: Section::Settings,
1904+
selected_setting: 5,
1905+
startup: StartupState::Ready(User {
1906+
id: 3,
1907+
display_name: "alice".into(),
1908+
notifications: true,
1909+
active: true,
1910+
}),
1911+
servers: vec![ServerEntry {
1912+
protocol: ServerProtocol::Smp,
1913+
address: "smp://key@smp.example".into(),
1914+
enabled: true,
1915+
preset: true,
1916+
}],
1917+
simplex_commands: commands,
1918+
..App::default()
1919+
};
1920+
1921+
app.handle_key_events(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
1922+
.unwrap();
1923+
let SimplexCommand::SetServerEnabled {
1924+
user_id,
1925+
protocol,
1926+
address,
1927+
enabled,
1928+
} = receiver.try_recv().unwrap()
1929+
else {
1930+
panic!("expected server-toggle command")
1931+
};
1932+
assert_eq!(user_id, 3);
1933+
assert_eq!(protocol, ServerProtocol::Smp);
1934+
assert_eq!(address, "smp://key@smp.example");
1935+
assert!(!enabled);
1936+
1937+
app.handle_key_events(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE))
1938+
.unwrap();
1939+
assert_eq!(app.server_protocol, ServerProtocol::Xftp);
1940+
app.handle_key_events(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE))
1941+
.unwrap();
1942+
app.input = "xftp://key@files.example".into();
1943+
app.handle_key_events(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
1944+
.unwrap();
1945+
let SimplexCommand::AddServer {
1946+
user_id,
1947+
protocol,
1948+
address,
1949+
} = receiver.try_recv().unwrap()
1950+
else {
1951+
panic!("expected add-server command")
1952+
};
1953+
assert_eq!((user_id, protocol), (3, ServerProtocol::Xftp));
1954+
assert_eq!(address, "xftp://key@files.example");
1955+
}
17901956
}

src/chat.rs

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,36 @@ pub struct User {
1010

1111
pub type Profile = User;
1212

13+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14+
pub enum ServerProtocol {
15+
Smp,
16+
Xftp,
17+
}
18+
19+
impl ServerProtocol {
20+
pub fn label(self) -> &'static str {
21+
match self {
22+
Self::Smp => "SMP",
23+
Self::Xftp => "XFTP",
24+
}
25+
}
26+
27+
pub fn json_key(self) -> &'static str {
28+
match self {
29+
Self::Smp => "smpServers",
30+
Self::Xftp => "xftpServers",
31+
}
32+
}
33+
}
34+
35+
#[derive(Clone, Debug, Eq, PartialEq)]
36+
pub struct ServerEntry {
37+
pub protocol: ServerProtocol,
38+
pub address: String,
39+
pub enabled: bool,
40+
pub preset: bool,
41+
}
42+
1343
#[derive(Clone, Debug, Eq, PartialEq)]
1444
pub struct ChatSummary {
1545
pub chat_ref: ChatRef,
@@ -121,7 +151,8 @@ pub enum SimplexEvent {
121151
chat_ref: ChatRef,
122152
message: Message,
123153
},
124-
ServersLoaded(Vec<String>),
154+
ServersLoaded(Vec<ServerEntry>),
155+
ServersUpdateFailed(String),
125156
ChatFeaturesLoaded(ChatFeatures),
126157
InvitationCreated(String),
127158
InvitationFailed(String),
@@ -195,6 +226,48 @@ pub fn smp_servers(value: &Value) -> Result<Vec<String>, String> {
195226
Ok(servers)
196227
}
197228

229+
pub fn server_entries(value: &Value) -> Result<Vec<ServerEntry>, String> {
230+
let groups = value
231+
.pointer("/result/userServers")
232+
.and_then(Value::as_array)
233+
.ok_or_else(|| response_error(value, "user servers"))?;
234+
let mut entries = Vec::new();
235+
for group in groups {
236+
for protocol in [ServerProtocol::Smp, ServerProtocol::Xftp] {
237+
for server in group
238+
.get(protocol.json_key())
239+
.and_then(Value::as_array)
240+
.into_iter()
241+
.flatten()
242+
{
243+
let Some(address) = server.get("server").and_then(Value::as_str) else {
244+
continue;
245+
};
246+
if server
247+
.get("deleted")
248+
.and_then(Value::as_bool)
249+
.unwrap_or(false)
250+
{
251+
continue;
252+
}
253+
entries.push(ServerEntry {
254+
protocol,
255+
address: address.to_owned(),
256+
enabled: server
257+
.get("enabled")
258+
.and_then(Value::as_bool)
259+
.unwrap_or(true),
260+
preset: server
261+
.get("preset")
262+
.and_then(Value::as_bool)
263+
.unwrap_or(false),
264+
});
265+
}
266+
}
267+
}
268+
Ok(entries)
269+
}
270+
198271
pub fn profile_and_features(value: &Value) -> Result<(Value, ChatFeatures), String> {
199272
let result = value
200273
.get("result")
@@ -726,6 +799,46 @@ mod tests {
726799
);
727800
}
728801

802+
#[test]
803+
fn parses_smp_and_xftp_server_configuration() {
804+
let servers = server_entries(&json!({"result": {
805+
"type": "userServers",
806+
"userServers": [{
807+
"operator": {"tradeName": "SimpleX Chat"},
808+
"smpServers": [{
809+
"server": "smp://key@smp.example",
810+
"enabled": true,
811+
"preset": true,
812+
"deleted": false
813+
}],
814+
"xftpServers": [{
815+
"server": "xftp://key@xftp.example",
816+
"enabled": false,
817+
"preset": true,
818+
"deleted": false
819+
}]
820+
}]
821+
}}))
822+
.unwrap();
823+
assert_eq!(
824+
servers,
825+
vec![
826+
ServerEntry {
827+
protocol: ServerProtocol::Smp,
828+
address: "smp://key@smp.example".into(),
829+
enabled: true,
830+
preset: true,
831+
},
832+
ServerEntry {
833+
protocol: ServerProtocol::Xftp,
834+
address: "xftp://key@xftp.example".into(),
835+
enabled: false,
836+
preset: true,
837+
},
838+
]
839+
);
840+
}
841+
729842
#[test]
730843
fn recognizes_a_connected_invitation_contact() {
731844
let connected = connected_contact(&json!({"result": {

0 commit comments

Comments
 (0)