Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions Flipcash/Core/Navigation/AppRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ final class AppRouter {
/// The destination `push` last appended, and onto which stack. Any other change
/// to `paths` clears it, so it names the top of that stack whenever it is set.
@ObservationIgnored private var lastPush: (stack: Stack, destination: Destination)?
@ObservationIgnored private var pendingCollapse: (stack: Stack, destination: Destination, count: Int)?

/// The stack owned by the currently-selected tab. A tab is the active
/// surface without being a *sheet*, so `presentedSheet` is nil while a tab
Expand Down Expand Up @@ -336,6 +337,38 @@ final class AppRouter {
logger.info("Set path", metadata: navigationMetadata(stack: stack, destination: destinations.last))
}

/// Pushes `destination` over the current top as a normal forward push and remembers the
/// resulting depth, so `collapseTopmostPath(toTop:)` can later drop everything beneath it.
/// Replacing the path outright would animate as a pop whenever the new path is shorter.
func pushForCollapse(_ destination: Destination) {
guard let stack = topmostStack else {
logger.warning("Push for collapse attempted with no topmost stack", metadata: [
"destination": "\(destination)",
])
return
}
paths[stack, default: NavigationPath()].append(destination)
pendingCollapse = (stack, destination, paths[stack, default: NavigationPath()].count)
lastPush = (stack, destination)
logger.info("Push", metadata: navigationMetadata(stack: stack, destination: destination))
}

/// Once the push from `pushForCollapse(_:)` has settled, removes everything beneath
/// `destination` so the stack becomes `[destination]`. Removing views under the visible top
/// is not animated. Does nothing if the stack changed in the meantime (the user already
/// popped or something else was pushed), so it never discards a screen the user is on.
func collapseTopmostPath(toTop destination: Destination) {
guard let pending = pendingCollapse, pending.destination == destination else { return }
pendingCollapse = nil
guard paths[pending.stack, default: NavigationPath()].count == pending.count else { return }
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
setPath([destination], on: pending.stack)
}
lastPush = (pending.stack, destination)
}

/// SwiftUI binding setter — fires when the NavigationStack writes a new
/// path back through the binding (system swipe-back, NavigationLink
/// activation, programmatic `dismiss()`). Distinguished from explicit
Expand Down
2 changes: 1 addition & 1 deletion Flipcash/Core/Screens/Conversation/GroupReadyCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ private struct GroupReadyInvitePresenter: ViewModifier {
open()
}
.dialog(item: $dialog)
.fullScreenCover(item: $share) { share in
.sheet(item: $share) { share in
switch share {
case .invite(let links):
ShareToChatsSheet(subject: .inviteLink(conversationID, links), isPresented: isSharing, onInvited: invited)
Expand Down
8 changes: 2 additions & 6 deletions Flipcash/Core/Screens/Invite Links/InviteLinkCopy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import FlipcashCore
enum InviteLinkCopy {

static let creating = "Creating link…"
static let footnote = "Anyone with this link can join and see who’s in the group. Share it only with people you want in the group"
/// Under the link pill on a group's Invite People sheet, for both kinds of link.
static let groupSheetLine = "Send this link to the people you want to join your group"
static let cantShare = "Can’t share from this device"

/// Under an invite link's group card QR.
Expand Down Expand Up @@ -59,11 +60,6 @@ enum InviteLinkCopy {
isOn ? "You approve each person" : "Anyone with the link joins right away"
}

/// The sheet's meta line under the current link: "Expires Nov 3 · 0 joined".
static func sheetMeta(_ invite: Invite) -> String {
"\(expiry(invite)) · \(joined(invite))"
}

/// A Manage Invites row's second line.
static func rowDetail(_ entry: GroupInviteLinks.Entry) -> String {
let invite = entry.invite
Expand Down
2 changes: 1 addition & 1 deletion Flipcash/Core/Screens/Invite Links/InviteLinksScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ struct InviteLinksScreen: View {
)
}
// Share Link opens the invite share sheet (1f) on that row's link.
.fullScreenCover(item: $sharing) { shared in
.sheet(item: $sharing) { shared in
if let links {
ShareToChatsSheet(
subject: .inviteLink(conversationID, links, entry: shared.id),
Expand Down
4 changes: 2 additions & 2 deletions Flipcash/Core/Screens/Invite Links/InvitePeopleSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -362,10 +362,10 @@ private struct InvitePeoplePresenter: ViewModifier {
.task { try? await links.load() }
}
}
.fullScreenCover(isPresented: canInvite ? .constant(false) : $isPresented) {
.sheet(isPresented: canInvite ? .constant(false) : $isPresented) {
ShareToChatsSheet(subject: .group(conversationID), isPresented: $isPresented, onInvited: onInvited)
}
.fullScreenCover(item: $share) { share in
.sheet(item: $share) { share in
switch share {
case .invite(let links):
ShareToChatsSheet(subject: .inviteLink(conversationID, links), isPresented: isSharing, onInvited: invited)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,14 +305,17 @@ struct NewPublicGroupScreen: View {

guard !Task.isCancelled else { return }

// The whole create flow is replaced rather than stacked under the chat. Back from
// a group that now exists belongs at the chat list holding it, not at a filled
// form offering to create it twice or at the picker that opened the form. The
// chat list is this stack's root, so unwinding to it is the same as popping both.
router.popToRoot()
// Both privacies land in the chat. A private group's invite card is there, so the
// host's first job (inviting people) starts from it rather than from the profile.
router.push(.tipConversation(conversation.id))
// The chat is pushed over the form so it slides in from the right; replacing the
// path with a shorter one would animate as a pop. NavigationStack has no
// push-finished callback, so the form and picker are dropped from beneath it after
// a delay sized to the push transition. Back from a group that now exists belongs
// at the chat list, not at a filled form offering to create it twice. Both
// privacies land in the chat, where a private group's invite card starts the
// host's first invite.
let chat = AppRouter.Destination.tipConversation(conversation.id)
router.pushForCollapse(chat)
try? await Task.delay(milliseconds: 600)
router.collapseTopmostPath(toTop: chat)

} catch {
buttonState = .normal
Expand Down
Loading
Loading