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
51 changes: 44 additions & 7 deletions Flipcash/Core/Screens/Conversation/ConversationScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ struct ConversationScreen: View {
@Environment(Container.self) private var container
@Environment(SessionContainer.self) private var sessionContainer
@Environment(\.scenePhase) private var scenePhase
@Environment(\.accessibilityReduceMotion) private var reduceMotion

@State private var didInitialRead = false
@State private var didOpenRequest = false
Expand Down Expand Up @@ -110,6 +111,8 @@ struct ConversationScreen: View {
@State private var paymentRequestDeck = PaymentRequestDeckState()
@State private var deckWindow = PaymentRequestDeckWindow()
@State private var requestDeckPresence = PaymentRequestDeckPresence()
/// The instant the request deck is worded at; it advances each minute and at every deadline it holds.
@State private var requestDeckClock = Date.now
/// Paying a share from the request stack's Pay button.
@State private var requestPayFlow = PaymentRequestPayFlow()
/// The request stack's measured height, reserved at the transcript's top; 0 while hidden.
Expand Down Expand Up @@ -682,7 +685,15 @@ struct ConversationScreen: View {
Task { await sessionContainer.hostedLobbies.refresh(hostedConversationID) }
}
.onChange(of: requestDeckItems, initial: true) { _, items in
requestDeckPresence.update(items)
requestDeckPresence.update(items, completed: completedDeckItem, animates: !reduceMotion)
}
// Wakes at the deck's next deadline, so a request leaves at its `ends_at`, and at least once a
// minute so the time left stays current.
.task(id: requestDeckWake) {
let wake = requestDeckWake
try? await Task.sleep(for: .seconds(max(0, wake.timeIntervalSinceNow)) + .milliseconds(50))
guard !Task.isCancelled else { return }
requestDeckClock = max(.now, wake)
}
.onChange(of: conversationController.reactions.error) { _, _ in
scheduleReactionErrorDismissal()
Expand Down Expand Up @@ -891,7 +902,7 @@ struct ConversationScreen: View {
.onGeometryChange(for: CGRect.self) { $0.frame(in: .global) } action: { deckWindow.listFrame = $0 }
.onChange(of: paymentRequestDeck.phase != .collapsed) { _, isUp in
if isUp {
deckWindow.items = requestDeckItems
deckWindow.items = requestDeckPresence.listItems(requestDeckItems)
deckWindow.present(state: paymentRequestDeck) { id in
PaymentRequestDetailsScreen(requestID: id)
.injectingEnvironment(from: sessionContainer)
Expand All @@ -901,7 +912,14 @@ struct ConversationScreen: View {
deckWindow.dismiss()
}
}
.onChange(of: requestDeckItems.map(\.id)) { deckWindow.items = requestDeckItems }
.onChange(of: requestDeckPresence.listItems(requestDeckItems)) { _, items in
deckWindow.items = items
// An expanded list that loses its last card collapses as the stack fades out. Details
// opened from it stay up; closing them puts the deck back at rest.
if items.isEmpty, paymentRequestDeck.isOpen, deckWindow.details == nil {
paymentRequestDeck.collapse()
}
}
.onChange(of: scenePhase, initial: true) { _, phase in deckWindow.scenePhase = phase }
.onDisappear { deckWindow.dismiss() }
// Each refetch puts the deck back in its default order.
Expand All @@ -910,15 +928,34 @@ struct ConversationScreen: View {
}
}

/// The private group's open and recently ended requests, worded for the viewer, in deck order.
/// The private group's open requests, worded for the viewer, in deck order.
private var requestDeckItems: [PaymentRequestDeckItem] {
guard let group = groupConversation, group.isPrivate else { return [] }
return sessionContainer.paymentRequests.deckItems(
for: group.id,
viewer: session.userID,
order: paymentRequestDeck.order,
copy: PaymentRequestCopy(session.displayCurrency),
now: .now
now: requestDeckClock
)
}

/// When the deck's clock next advances: the nearest deadline it holds, or a minute on.
private var requestDeckWake: Date {
let minute = requestDeckClock.addingTimeInterval(60)
guard let expiry = GroupPaymentRequest.nextExpiry(of: requestDeckItems.map(\.request), after: requestDeckClock) else {
return minute
}
return min(expiry, minute)
}

/// A request that just left the deck, as its card reads now that it completed.
private func completedDeckItem(_ id: PaymentRequestID) -> PaymentRequestDeckItem? {
sessionContainer.paymentRequests.completedDeckItem(
id,
viewer: session.userID,
copy: PaymentRequestCopy(session.displayCurrency),
now: requestDeckClock
)
}

Expand Down Expand Up @@ -1087,12 +1124,12 @@ struct ConversationScreen: View {
return PaymentRequestCopy.owedAccessibilityLabel(
share: share,
title: front.card.title,
deadline: front.request.deadline(at: .now),
deadline: front.request.deadline(at: requestDeckClock),
count: requestDeckItems.count
)
}
return PaymentRequestCopy(session.displayCurrency)
.deckAccessibilityLabel(front: front.request, title: front.card.title, viewerStatus: front.viewerStatus, count: requestDeckItems.count, now: .now)
.deckAccessibilityLabel(front: front.request, title: front.card.title, viewerStatus: front.viewerStatus, count: requestDeckItems.count, now: requestDeckClock)
}

private func payRequest(_ id: PaymentRequestID) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ final class PaymentRequestCreateViewModel {
}

var enteredAmount: String = ""
var title: String
/// The request's title, the weekday default until the creator saves one on the Name screen.
var name = PaymentRequestName()
/// The title the request is sent with.
var title: String { name.title }
var deadline: PaymentRequestDeadline = .oneWeek
private(set) var people: PaymentRequestPeople?
/// Set when checking a member would make a 101st share, until the sheet is dismissed.
Expand Down Expand Up @@ -59,13 +62,12 @@ final class PaymentRequestCreateViewModel {
self.conversationController = sessionContainer.conversationController
self.requests = sessionContainer.paymentRequests
self.conversationID = conversationID
self.title = defaultPaymentRequestTitle()
}

/// Clears the draft for a fresh New Request: amount, default title and deadline, everyone selected.
func startNewRequest() {
enteredAmount = ""
title = defaultPaymentRequestTitle()
name = PaymentRequestName()
deadline = .oneWeek
people = nil
idempotencyKey = nil
Expand Down Expand Up @@ -129,10 +131,7 @@ final class PaymentRequestCreateViewModel {
return people.shares(perPerson: amount)
}

var sendButtonTitle: String {
guard let amount else { return "Request" }
return people?.buttonTitle(perPerson: amount) ?? "Request"
}
var sendButtonTitle: String { paymentRequestSendButtonTitle }

// MARK: - Send -

Expand Down
Loading
Loading