diff --git a/Flipcash/Core/Screens/Main/Username/UsernameLookupScreen.swift b/Flipcash/Core/Screens/Main/Username/UsernameLookupScreen.swift index 3a0ef491c..b4eada42f 100644 --- a/Flipcash/Core/Screens/Main/Username/UsernameLookupScreen.swift +++ b/Flipcash/Core/Screens/Main/Username/UsernameLookupScreen.swift @@ -24,11 +24,12 @@ private let logger = Logger(label: "flipcash.username-lookup") /// verification screens use (`PhoneVerificationViewModel.swift:320-329`), and so /// is the push. /// -/// This screen then drops out from under the chat, since a chat's Back belongs -/// on the chat list. It goes while the push is still animating: the removal is a -/// whole-path rewrite, which remounts the chat, and a freshly-mounted chat -/// settles its layout a beat after it appears. Under the transition that settle -/// is hidden — the same way it is on every other push into a chat. +/// The route in then drops out from under the chat — this screen and the New +/// Chat picker above the list — since a chat's Back belongs on the chat list. It +/// goes while the push is still animating: the removal is a whole-path rewrite, +/// which remounts the chat, and a freshly-mounted chat settles its layout a beat +/// after it appears. Under the transition that settle is hidden — the same way +/// it is on every other push into a chat. struct UsernameLookupScreen: View { @Environment(Container.self) private var container @@ -158,6 +159,12 @@ struct UsernameLookupScreen: View { lookupTask = nil let chat = AppRouter.Destination.tipConversationForUser(userID) + + // Measured rather than assumed: this screen is reached through + // the New Chat picker today and was reached straight off the + // chat list before it, so a fixed depth silently stops matching + // the moment a screen is added to or removed from the route. + let depthWithChat = router[.tips].count + 1 router.push(chat) // Well inside the push transition, so the remounted chat settles @@ -169,7 +176,7 @@ struct UsernameLookupScreen: View { // Unless they went back while it landed: the lookup left with // them, and rewriting the path would drag the chat back. - guard router[.tips].count == 2 else { return } + guard router[.tips].count == depthWithChat else { return } // Unanimated, because nothing is arriving — only the entry // underneath is leaving. Animated, SwiftUI stages the remount as diff --git a/FlipcashTests/UsernameLookupRoutingTests.swift b/FlipcashTests/UsernameLookupRoutingTests.swift index a102c268f..bef528285 100644 --- a/FlipcashTests/UsernameLookupRoutingTests.swift +++ b/FlipcashTests/UsernameLookupRoutingTests.swift @@ -79,6 +79,40 @@ struct UsernameLookupRoutingTests { #expect(ConversationContext.existing(chatID).resolvedContact(in: [contact]) != nil) } + // MARK: - Back stack - + + @Test("The chat list is not a fixed number of screens below the lookup") + func backStack_routeInHasNoFixedDepth() { + // The lookup was pushed straight off the chat list until the New Chat + // picker went in between (#790). A screen that assumed the depth it had + // then is a screen that stops unwinding the moment the route changes. + let router = AppRouter() + router.activeTabStack = .tips + + router.push(.newChat) + router.push(.usernameLookup) + + #expect(router[.tips].count == 2) + } + + @Test("Back from a chat opened by handle lands on the chat list") + func backStack_rewriteLeavesOnlyTheChat() { + let router = AppRouter() + router.activeTabStack = .tips + router.push(.newChat) + router.push(.usernameLookup) + + let chat = AppRouter.Destination.tipConversationForUser(UUID()) + let depthWithChat = router[.tips].count + 1 + router.push(chat) + #expect(router[.tips].count == depthWithChat) + + // What the screen does once the push has started: neither the picker + // nor the lookup is somewhere Back belongs, and the list is the root. + router.setPath([chat], on: .tips) + #expect(router[.tips].count == 1) + } + // MARK: - Counterpart - @Test("The fetched profile supplies the name and handle the chat shows")