1use super::*;
2
3#[uniffi::export]
7pub fn optimistic_push_screen_stack(
8 has_account: bool,
9 mut screen_stack: Vec<Screen>,
10 screen: Screen,
11) -> Option<Vec<Screen>> {
12 if !has_account {
13 return match screen {
14 Screen::Welcome => Some(Vec::new()),
15 Screen::CreateAccount
16 | Screen::RestoreAccount
17 | Screen::RemoteSigner
18 | Screen::AddDevice => Some(vec![screen]),
19 _ => None,
20 };
21 }
22
23 match screen {
24 Screen::ChatList => Some(Vec::new()),
25 Screen::NewChat
26 | Screen::NewGroup
27 | Screen::CreateInvite
28 | Screen::JoinInvite
29 | Screen::Settings
30 | Screen::DeviceRoster => Some(vec![screen]),
31 Screen::Chat { chat_id } => {
32 let chat_id = chat_id.trim();
33 (!chat_id.is_empty()).then(|| {
34 vec![Screen::Chat {
35 chat_id: chat_id.to_string(),
36 }]
37 })
38 }
39 Screen::DirectChatInfo { chat_id } => {
40 let chat_id = chat_id.trim();
41 if chat_id.is_empty() {
42 return None;
43 }
44 let info = Screen::DirectChatInfo {
45 chat_id: chat_id.to_string(),
46 };
47 if screen_stack.last() != Some(&info) {
48 screen_stack.push(info);
49 }
50 Some(screen_stack)
51 }
52 Screen::GroupDetails { group_id } => {
53 let group_id = group_id.trim();
54 if group_id.is_empty() {
55 return None;
56 }
57 let group_chat_id = group_chat_id(group_id);
58 if active_chat_in_stack(&screen_stack) != Some(group_chat_id.as_str()) {
59 screen_stack = vec![Screen::Chat {
60 chat_id: group_chat_id,
61 }];
62 }
63 let details = Screen::GroupDetails {
64 group_id: group_id.to_string(),
65 };
66 if screen_stack.last() != Some(&details) {
67 screen_stack.push(details);
68 }
69 Some(screen_stack)
70 }
71 Screen::CreateAccount
72 | Screen::RestoreAccount
73 | Screen::RemoteSigner
74 | Screen::AddDevice
75 | Screen::DeviceRevoked
76 | Screen::Welcome => None,
77 }
78}
79
80#[uniffi::export]
83pub fn pending_navigation_settled(
84 has_account: bool,
85 pending_stack: Vec<Screen>,
86 core_stack: Vec<Screen>,
87 expired: bool,
88) -> bool {
89 !has_account || expired || pending_stack == core_stack
90}
91
92fn active_chat_in_stack(screen_stack: &[Screen]) -> Option<&str> {
93 screen_stack.iter().rev().find_map(|screen| match screen {
94 Screen::Chat { chat_id } => Some(chat_id.as_str()),
95 _ => None,
96 })
97}
98
99impl AppCore {
100 pub(super) fn push_screen(&mut self, screen: Screen) {
101 if self.state.account.is_none() {
102 if !matches!(screen, Screen::RemoteSigner) {
103 self.cancel_remote_signer_login();
104 }
105 match screen {
106 Screen::RemoteSigner => {
107 self.start_remote_signer_login(None);
108 return;
109 }
110 Screen::Welcome => {
111 self.stop_pending_linked_device();
112 self.screen_stack.clear();
113 self.active_chat_id = None;
114 }
115 Screen::CreateAccount | Screen::RestoreAccount | Screen::AddDevice => {
116 self.screen_stack = vec![screen];
117 self.active_chat_id = None;
118 if matches!(self.screen_stack.last(), Some(Screen::AddDevice))
119 && self.pending_linked_device.is_none()
120 {
121 self.start_linked_device("");
122 return;
123 }
124 }
125 _ => return,
126 }
127
128 self.rebuild_persist_and_emit_state();
129 return;
130 }
131
132 match screen {
133 Screen::CreateAccount
134 | Screen::RestoreAccount
135 | Screen::AddDevice
136 | Screen::RemoteSigner => return,
137 Screen::ChatList => {
138 self.screen_stack.clear();
139 self.active_chat_id = None;
140 }
141 Screen::NewChat => {
142 if !self.can_use_chats() {
143 self.state.toast =
144 Some(chat_unavailable_message(self.logged_in.as_ref()).to_string());
145 self.emit_state();
146 return;
147 }
148 self.screen_stack = vec![Screen::NewChat];
149 self.active_chat_id = None;
150 }
151 Screen::NewGroup => {
152 if !self.can_use_chats() {
153 self.state.toast =
154 Some(chat_unavailable_message(self.logged_in.as_ref()).to_string());
155 self.emit_state();
156 return;
157 }
158 self.screen_stack = vec![Screen::NewGroup];
159 self.active_chat_id = None;
160 }
161 Screen::CreateInvite => {
162 if !self.can_use_chats() {
163 self.state.toast =
164 Some(chat_unavailable_message(self.logged_in.as_ref()).to_string());
165 self.emit_state();
166 return;
167 }
168 self.screen_stack = vec![Screen::CreateInvite];
169 self.active_chat_id = None;
170 self.create_public_invite();
171 return;
172 }
173 Screen::JoinInvite => {
174 if !self.can_use_chats() {
175 self.state.toast =
176 Some(chat_unavailable_message(self.logged_in.as_ref()).to_string());
177 self.emit_state();
178 return;
179 }
180 self.screen_stack = vec![Screen::JoinInvite];
181 self.active_chat_id = None;
182 }
183 Screen::Settings => {
184 self.screen_stack = vec![Screen::Settings];
185 self.active_chat_id = None;
186 }
187 Screen::Chat { chat_id } => {
188 self.open_chat(&chat_id);
189 return;
190 }
191 Screen::DirectChatInfo { chat_id } => {
192 let Some(chat_id) = self.normalize_chat_id(&chat_id) else {
193 return;
194 };
195 if !matches!(chat_kind_for_id(&chat_id), ChatKind::Direct) {
196 return;
197 }
198 self.ensure_thread_record(&chat_id, unix_now().get());
199 if !matches!(
200 self.screen_stack.last(),
201 Some(Screen::DirectChatInfo { chat_id: current }) if current == &chat_id
202 ) {
203 self.screen_stack.push(Screen::DirectChatInfo { chat_id });
204 }
205 }
206 Screen::GroupDetails { group_id } => {
207 let Some(group_id) = normalize_group_id(&group_id) else {
208 return;
209 };
210 let group_chat_id = group_chat_id(&group_id);
211 if self.active_chat_id.as_deref() != Some(group_chat_id.as_str()) {
212 self.open_chat(&group_chat_id);
213 }
214 if !matches!(
215 self.screen_stack.last(),
216 Some(Screen::GroupDetails { group_id: current }) if current == &group_id
217 ) {
218 self.screen_stack.push(Screen::GroupDetails { group_id });
219 }
220 }
221 Screen::DeviceRoster => {
222 self.screen_stack = vec![Screen::DeviceRoster];
223 self.active_chat_id = None;
224 self.fetch_pending_device_invites_for_local_owner();
225 }
226 Screen::DeviceRevoked | Screen::Welcome => return,
227 }
228
229 self.rebuild_persist_and_emit_state();
230 }
231
232 pub(super) fn navigate_back(&mut self) {
233 self.reset_pending_invite_acceptance();
234 if matches!(self.screen_stack.last(), Some(Screen::RemoteSigner)) {
235 self.cancel_remote_signer_login();
236 }
237 if self.screen_stack.pop().is_none() {
238 return;
239 }
240 self.sync_active_chat_from_router();
241 self.rebuild_persist_and_emit_state();
242 }
243
244 pub(super) fn update_screen_stack(&mut self, stack: Vec<Screen>) {
245 if self.state.account.is_none() {
246 self.screen_stack = stack
247 .into_iter()
248 .filter(|screen| {
249 matches!(
250 screen,
251 Screen::CreateAccount
252 | Screen::RestoreAccount
253 | Screen::AddDevice
254 | Screen::RemoteSigner
255 )
256 })
257 .collect();
258 if !self
259 .screen_stack
260 .iter()
261 .any(|screen| matches!(screen, Screen::AddDevice))
262 {
263 self.stop_pending_linked_device();
264 }
265 if !self
266 .screen_stack
267 .iter()
268 .any(|screen| matches!(screen, Screen::RemoteSigner))
269 {
270 self.cancel_remote_signer_login();
271 }
272 self.active_chat_id = None;
273 self.rebuild_persist_and_emit_state();
274 return;
275 }
276
277 let mut normalized_stack = Vec::new();
278 for screen in stack {
279 match screen {
280 Screen::Welcome
281 | Screen::CreateAccount
282 | Screen::RestoreAccount
283 | Screen::RemoteSigner
284 | Screen::AddDevice
285 | Screen::ChatList
286 | Screen::DeviceRevoked => {}
287 Screen::Settings => normalized_stack.push(Screen::Settings),
288 Screen::CreateInvite => {
289 if self.can_use_chats() {
290 normalized_stack.push(Screen::CreateInvite);
291 }
292 }
293 Screen::JoinInvite => {
294 if self.can_use_chats() {
295 normalized_stack.push(Screen::JoinInvite);
296 }
297 }
298 Screen::NewChat => {
299 if self.can_use_chats() {
300 normalized_stack.push(Screen::NewChat);
301 }
302 }
303 Screen::NewGroup => {
304 if self.can_use_chats() {
305 normalized_stack.push(Screen::NewGroup);
306 }
307 }
308 Screen::DeviceRoster => normalized_stack.push(Screen::DeviceRoster),
309 Screen::Chat { chat_id } => {
310 if self.can_use_chats() {
311 if let Some(chat_id) = self.normalize_chat_id(&chat_id) {
312 normalized_stack.push(Screen::Chat { chat_id });
313 }
314 }
315 }
316 Screen::DirectChatInfo { chat_id } => {
317 if self.can_use_chats() {
318 if let Some(chat_id) = self.normalize_chat_id(&chat_id) {
319 if matches!(chat_kind_for_id(&chat_id), ChatKind::Direct) {
320 self.ensure_thread_record(&chat_id, unix_now().get());
321 normalized_stack.push(Screen::DirectChatInfo { chat_id });
322 }
323 }
324 }
325 }
326 Screen::GroupDetails { group_id } => {
327 if self.can_use_chats() {
328 if let Some(group_id) = normalize_group_id(&group_id) {
329 normalized_stack.push(Screen::GroupDetails { group_id });
330 }
331 }
332 }
333 }
334 }
335
336 self.screen_stack = normalized_stack;
337 self.sync_active_chat_from_router();
338 self.rebuild_persist_and_emit_state();
339 }
340
341 pub(super) fn is_device_roster_open(&self) -> bool {
342 matches!(self.screen_stack.last(), Some(Screen::DeviceRoster))
343 }
344
345 pub(super) fn is_chat_visible(&self, chat_id: &str) -> bool {
346 self.app_in_foreground
347 && matches!(
348 self.screen_stack.last(),
349 Some(Screen::Chat { chat_id: current }) if current == chat_id
350 )
351 }
352
353 pub(super) fn sync_active_chat_from_router(&mut self) {
354 match active_chat_in_stack(&self.screen_stack).map(str::to_string) {
355 Some(chat_id) => {
356 self.active_chat_id = Some(chat_id.clone());
357 if let Some(thread) = self.threads.get_mut(&chat_id) {
358 thread.unread_count = 0;
359 }
360 }
361 _ => {
362 self.active_chat_id = None;
363 }
364 }
365 }
366}
367
368#[cfg(test)]
369mod optimistic_navigation_tests {
370 use super::*;
371
372 fn chat(chat_id: &str) -> Screen {
373 Screen::Chat {
374 chat_id: chat_id.to_string(),
375 }
376 }
377
378 fn group_details(group_id: &str) -> Screen {
379 Screen::GroupDetails {
380 group_id: group_id.to_string(),
381 }
382 }
383
384 fn direct_info(chat_id: &str) -> Screen {
385 Screen::DirectChatInfo {
386 chat_id: chat_id.to_string(),
387 }
388 }
389
390 #[test]
391 fn logged_out_pushes_only_predict_onboarding_screens() {
392 let push = |screen| optimistic_push_screen_stack(false, vec![Screen::AddDevice], screen);
393 assert_eq!(push(Screen::Welcome), Some(vec![]));
394 assert_eq!(
395 push(Screen::RestoreAccount),
396 Some(vec![Screen::RestoreAccount])
397 );
398 assert_eq!(push(Screen::RemoteSigner), Some(vec![Screen::RemoteSigner]));
399 assert_eq!(push(Screen::Settings), None);
400 assert_eq!(push(chat("a")), None);
401 }
402
403 #[test]
404 fn logged_in_root_screens_replace_the_stack() {
405 let push = |screen| optimistic_push_screen_stack(true, vec![chat("a")], screen);
406 assert_eq!(push(Screen::ChatList), Some(vec![]));
407 assert_eq!(push(Screen::Settings), Some(vec![Screen::Settings]));
408 assert_eq!(push(chat(" b ")), Some(vec![chat("b")]));
409 assert_eq!(push(chat(" ")), None);
410 assert_eq!(push(Screen::Welcome), None);
411 assert_eq!(push(Screen::AddDevice), None);
412 assert_eq!(push(Screen::DeviceRevoked), None);
413 }
414
415 #[test]
416 fn direct_chat_info_is_pushed_once() {
417 let stack = vec![chat("a")];
418 let pushed = optimistic_push_screen_stack(true, stack, direct_info(" a ")).unwrap();
419 assert_eq!(pushed, vec![chat("a"), direct_info("a")]);
420 assert_eq!(
421 optimistic_push_screen_stack(true, pushed.clone(), direct_info("a")),
422 Some(pushed)
423 );
424 assert_eq!(
425 optimistic_push_screen_stack(true, vec![], direct_info(" ")),
426 None
427 );
428 }
429
430 #[test]
431 fn group_details_opens_the_group_chat_unless_it_is_active() {
432 let opened = vec![chat("group:g"), group_details("g")];
433 assert_eq!(
434 optimistic_push_screen_stack(true, vec![chat("a")], group_details(" g ")),
435 Some(opened.clone())
436 );
437 assert_eq!(
438 optimistic_push_screen_stack(true, vec![chat("group:g")], group_details("g")),
439 Some(opened.clone())
440 );
441 assert_eq!(
442 optimistic_push_screen_stack(true, opened.clone(), group_details("g")),
443 Some(opened)
444 );
445 assert_eq!(
446 optimistic_push_screen_stack(true, vec![], group_details("")),
447 None
448 );
449 }
450
451 #[test]
452 fn pending_navigation_settles_on_logout_match_or_expiry() {
453 let pending = || vec![chat("a")];
454 assert!(!pending_navigation_settled(true, pending(), vec![], false));
455 assert!(pending_navigation_settled(
456 true,
457 pending(),
458 pending(),
459 false
460 ));
461 assert!(pending_navigation_settled(true, pending(), vec![], true));
462 assert!(pending_navigation_settled(false, pending(), vec![], false));
463 }
464}