added more anomations
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/// Timing and geometry for the Messages-style chat animations.
|
||||
///
|
||||
/// Two effects live here: the staggered rise of the first visible page when a
|
||||
/// conversation opens, and the transition a freshly appended row uses while the
|
||||
/// user already watches the timeline (an outgoing bubble launches from the
|
||||
/// composer's send control, an incoming one springs from its own edge).
|
||||
enum ChatMessageEntrancePolicy {
|
||||
/// A row waits this long behind every newer row below it, so the visible
|
||||
/// page unfolds bottom-up the way Messages does.
|
||||
static let entranceStagger: TimeInterval = 0.035
|
||||
|
||||
/// Rows further from the bottom than this are left alone: past a screenful
|
||||
/// the cascade is invisible and the row may only appear on a later scroll.
|
||||
static let entranceCascadeLimit = 12
|
||||
|
||||
static let entranceRise: CGFloat = 16
|
||||
static let entranceScale: CGFloat = 0.94
|
||||
|
||||
static let sendScale: CGFloat = 0.2
|
||||
/// Launch point sits next to the composer's send control so the bubble
|
||||
/// reads as thrown out of the field instead of fading in place.
|
||||
static let sendRise: CGFloat = 12
|
||||
|
||||
static let receiveScale: CGFloat = 0.4
|
||||
static let receiveRise: CGFloat = 10
|
||||
|
||||
/// Upper bound of the spring used by both effects. The owning view keeps a
|
||||
/// row's entrance state alive for this long so a recycled row that reappears
|
||||
/// while scrolling plays nothing.
|
||||
static let entranceAnimationDuration: TimeInterval = 0.75
|
||||
|
||||
/// A burst larger than this is a history page arriving, not a conversation:
|
||||
/// animating every row of a page would flash the whole timeline.
|
||||
static let appendAnimationLimit = 4
|
||||
|
||||
/// Total time the opening cascade can still be pending for the last row.
|
||||
static var entranceCascadeDuration: TimeInterval {
|
||||
TimeInterval(entranceCascadeLimit - 1) * entranceStagger
|
||||
+ entranceAnimationDuration
|
||||
}
|
||||
|
||||
static var appendAnimation: Animation {
|
||||
.spring(response: 0.44, dampingFraction: 0.68)
|
||||
}
|
||||
|
||||
/// Per-row delays keyed by client ID. The newest row leads the cascade and
|
||||
/// every older row follows one stagger step behind it; rows outside the
|
||||
/// cascade get no delay and therefore no entrance at all.
|
||||
static func entranceDelays(forIDs ids: [String]) -> [String: TimeInterval] {
|
||||
let last = ids.count - 1
|
||||
guard last >= 0 else { return [:] }
|
||||
let firstAnimated = max(0, ids.count - entranceCascadeLimit)
|
||||
var delays: [String: TimeInterval] = [:]
|
||||
delays.reserveCapacity(ids.count - firstAnimated)
|
||||
for index in firstAnimated...last {
|
||||
delays[ids[index]] = TimeInterval(last - index) * entranceStagger
|
||||
}
|
||||
return delays
|
||||
}
|
||||
|
||||
/// Rows a query refresh added below everything already on screen.
|
||||
///
|
||||
/// A MAM page paged in above the viewport is deliberately ignored: replaying
|
||||
/// the entrance mid-scroll would yank the timeline the user is reading. A
|
||||
/// burst of rows is treated as the same kind of archive delivery.
|
||||
static func appendedLaunchIDs(
|
||||
in ids: [String],
|
||||
after previous: Set<String>
|
||||
) -> [String] {
|
||||
guard !previous.isEmpty else { return [] }
|
||||
guard let lastKnownIndex = ids.lastIndex(where: previous.contains)
|
||||
else { return [] }
|
||||
let appended = ids.dropFirst(lastKnownIndex + 1).filter {
|
||||
!previous.contains($0)
|
||||
}
|
||||
guard appended.count <= appendAnimationLimit else { return [] }
|
||||
return Array(appended)
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,13 @@ struct ChatView: View {
|
||||
@State private var captureSuspendsArchiveSync = false
|
||||
@State private var pickerResetToken = UUID()
|
||||
@State private var timelineEntries: [ChatTimelineEntry] = []
|
||||
/// Messages-style entrance state: which rows currently animate, which IDs
|
||||
/// the timeline already showed, and whether the opening cascade of this
|
||||
/// conversation has played.
|
||||
@State private var entranceModes: [String: MessageBubbleEntrance.Mode] = [:]
|
||||
@State private var knownEntranceIDs: Set<String> = []
|
||||
@State private var hasPlayedOpeningEntrance = false
|
||||
@State private var entranceReleaseToken = UUID()
|
||||
|
||||
/// Messages of this conversation straight from the SwiftData store,
|
||||
/// ordered like `AppModel.selectedMessages`: timestamp, then clientID.
|
||||
@@ -143,6 +150,13 @@ struct ChatView: View {
|
||||
_captureSuspendsArchiveSync = State(initialValue: false)
|
||||
_pickerResetToken = State(initialValue: UUID())
|
||||
_timelineEntries = State<[ChatTimelineEntry]>(initialValue: [])
|
||||
_entranceModes = State<[String: MessageBubbleEntrance.Mode]>(
|
||||
initialValue: [:]
|
||||
)
|
||||
_knownEntranceIDs = State<Set<String>>(initialValue: [])
|
||||
_hasPlayedOpeningEntrance = State(initialValue: false)
|
||||
_entranceConversationID = State<String?>(initialValue: nil)
|
||||
_entranceReleaseToken = State(initialValue: UUID())
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -376,6 +390,10 @@ struct ChatView: View {
|
||||
historyTopTriggerVisible = false
|
||||
historyAutoContinueCount = 0
|
||||
emptyHistoryRetryCount = 0
|
||||
entranceModes = [:]
|
||||
knownEntranceIDs = []
|
||||
hasPlayedOpeningEntrance = false
|
||||
entranceReleaseToken = UUID()
|
||||
}
|
||||
.onDisappear {
|
||||
model.endConversationViewing(jid: conversation.jid)
|
||||
@@ -478,7 +496,87 @@ struct ChatView: View {
|
||||
|
||||
private func rebuildTimelineEntries(from newMessages: [ChatMessage]? = nil)
|
||||
{
|
||||
timelineEntries = ChatTimelineEntry.make(from: newMessages ?? messages)
|
||||
let source = newMessages ?? messages
|
||||
timelineEntries = ChatTimelineEntry.make(from: source)
|
||||
updateEntranceModes(for: source)
|
||||
}
|
||||
|
||||
/// Decides which rows play a Messages-style entrance after a rebuild.
|
||||
/// The first page of an opened conversation cascades bottom-up; rows that
|
||||
/// appear later (a send, an incoming message) launch from the composer's
|
||||
/// edge. Modes are released once the animation window closes so a row
|
||||
/// recycled by the lazy stack during scrolling reappears statically.
|
||||
private func updateEntranceModes(for newMessages: [ChatMessage]) {
|
||||
let ids = newMessages.map(\.clientID)
|
||||
let known = knownEntranceIDs
|
||||
knownEntranceIDs = Set(ids)
|
||||
|
||||
guard hasPlayedOpeningEntrance else {
|
||||
guard !ids.isEmpty else { return }
|
||||
hasPlayedOpeningEntrance = true
|
||||
// The first message ever in an empty chat should launch out of
|
||||
// the composer like any other send, not cascade in.
|
||||
if ids.count == 1, known.isEmpty,
|
||||
newMessages.first?.direction == .outgoing
|
||||
{
|
||||
applyLaunch(for: newMessages, ids: ids)
|
||||
return
|
||||
}
|
||||
let delays = ChatMessageEntrancePolicy.entranceDelays(
|
||||
forIDs: ids
|
||||
)
|
||||
for (id, delay) in delays {
|
||||
entranceModes[id] = .cascade(delay: delay)
|
||||
}
|
||||
scheduleEntranceRelease(
|
||||
ids: Set(delays.keys),
|
||||
after: ChatMessageEntrancePolicy.entranceCascadeDuration
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
applyLaunch(
|
||||
for: newMessages,
|
||||
ids: ChatMessageEntrancePolicy.appendedLaunchIDs(
|
||||
in: ids,
|
||||
after: known
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private func applyLaunch(for messages: [ChatMessage], ids: [String]) {
|
||||
guard !ids.isEmpty else { return }
|
||||
let launched = Set(ids)
|
||||
var applied: Set<String> = []
|
||||
for message in messages where launched.contains(message.clientID) {
|
||||
entranceModes[message.clientID] = .launch(
|
||||
outgoing: message.direction == .outgoing
|
||||
)
|
||||
applied.insert(message.clientID)
|
||||
}
|
||||
scheduleEntranceRelease(
|
||||
ids: applied,
|
||||
after: ChatMessageEntrancePolicy.entranceAnimationDuration
|
||||
)
|
||||
}
|
||||
|
||||
private func scheduleEntranceRelease(
|
||||
ids: Set<String>,
|
||||
after delay: TimeInterval
|
||||
) {
|
||||
guard !ids.isEmpty else { return }
|
||||
let token = entranceReleaseToken
|
||||
Task { @MainActor in
|
||||
try? await Task.sleep(
|
||||
nanoseconds: UInt64(delay * 1_000_000_000)
|
||||
)
|
||||
guard !Task.isCancelled, token == entranceReleaseToken else {
|
||||
return
|
||||
}
|
||||
for id in ids {
|
||||
entranceModes[id] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isSelectingMessages: Bool {
|
||||
@@ -606,6 +704,13 @@ struct ChatView: View {
|
||||
)
|
||||
}
|
||||
)
|
||||
.modifier(
|
||||
MessageBubbleEntrance(
|
||||
mode: entranceModes[
|
||||
message.clientID
|
||||
] ?? .settled
|
||||
)
|
||||
)
|
||||
.id(message.clientID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import SwiftUI
|
||||
|
||||
/// Messages-style entrance for chat rows.
|
||||
///
|
||||
/// Opening a conversation reveals its first page as a bottom-up cascade
|
||||
/// (`.cascade`), and a row appended while the timeline is already rendered
|
||||
/// springs out of the composer's send control when outgoing (`.launch`).
|
||||
/// Everything else stays `.settled`, which keeps history scrolling, reactions
|
||||
/// and selection updates free of animation.
|
||||
struct MessageBubbleEntrance: ViewModifier {
|
||||
enum Mode: Equatable {
|
||||
case settled
|
||||
case cascade(delay: TimeInterval)
|
||||
case launch(outgoing: Bool)
|
||||
|
||||
var isSettled: Bool {
|
||||
if case .settled = self { return true }
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
let mode: Mode
|
||||
|
||||
// A settled row must never pass through an invisible frame, so only the
|
||||
// rows that actually animate start hidden.
|
||||
@State private var isRevealed: Bool
|
||||
@State private var hasStarted = false
|
||||
|
||||
init(mode: Mode) {
|
||||
self.mode = mode
|
||||
_isRevealed = State(initialValue: mode.isSettled)
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.opacity(isRevealed ? 1 : 0)
|
||||
.scaleEffect(isRevealed ? 1 : displacedScale, anchor: scaleAnchor)
|
||||
.offset(y: isRevealed ? 0 : displacedRise)
|
||||
.onAppear(perform: reveal)
|
||||
}
|
||||
|
||||
/// Outgoing bubbles shrink toward the trailing bottom corner, where the
|
||||
/// composer's send control sits; incoming ones grow from their own edge.
|
||||
private var displacedScale: CGFloat {
|
||||
switch mode {
|
||||
case .settled, .cascade:
|
||||
return ChatMessageEntrancePolicy.entranceScale
|
||||
case .launch(let outgoing):
|
||||
return outgoing
|
||||
? ChatMessageEntrancePolicy.sendScale
|
||||
: ChatMessageEntrancePolicy.receiveScale
|
||||
}
|
||||
}
|
||||
|
||||
private var scaleAnchor: UnitPoint {
|
||||
if case .launch(let outgoing) = mode, outgoing { return .bottomTrailing }
|
||||
return .bottom
|
||||
}
|
||||
|
||||
private var displacedRise: CGFloat {
|
||||
switch mode {
|
||||
case .settled, .cascade:
|
||||
return ChatMessageEntrancePolicy.entranceRise
|
||||
case .launch(let outgoing):
|
||||
return outgoing
|
||||
? ChatMessageEntrancePolicy.sendRise
|
||||
: ChatMessageEntrancePolicy.receiveRise
|
||||
}
|
||||
}
|
||||
|
||||
private func reveal() {
|
||||
guard !hasStarted else { return }
|
||||
hasStarted = true
|
||||
guard !isRevealed else { return }
|
||||
let delay: TimeInterval
|
||||
if case .cascade(let cascadeDelay) = mode {
|
||||
delay = cascadeDelay
|
||||
} else {
|
||||
delay = 0
|
||||
}
|
||||
withAnimation(
|
||||
ChatMessageEntrancePolicy.appendAnimation.delay(delay)
|
||||
) {
|
||||
isRevealed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
|
||||
[34mSources/Shared/UI/ChatView.swift[0m
|
||||
[34m────────────────────────────────────────────────────────────────────────────────────────────[0m
|
||||
|
||||
[34m────────────────────────────[0m[34m┐[0m
|
||||
[34m75[0m:[38;2;248;248;242m [38;2;102;217;239mstruct[38;2;248;248;242m ChatView: View { [0m[34m│[0m
|
||||
[34m────────────────────────────[0m[34m┘[0m
|
||||
[38;2;248;248;242m [38;2;249;38;114m@State[38;2;248;248;242m [38;2;249;38;114mprivate[38;2;248;248;242m [38;2;102;217;239mvar[38;2;248;248;242m captureSuspendsArchiveSync [38;2;249;38;114m=[38;2;248;248;242m [38;2;249;38;114mfalse[0m
|
||||
[38;2;248;248;242m [38;2;249;38;114m@State[38;2;248;248;242m [38;2;249;38;114mprivate[38;2;248;248;242m [38;2;102;217;239mvar[38;2;248;248;242m pickerResetToken [38;2;249;38;114m=[38;2;248;248;242m UUID()[0m
|
||||
[38;2;248;248;242m [38;2;249;38;114m@State[38;2;248;248;242m [38;2;249;38;114mprivate[38;2;248;248;242m [38;2;102;217;239mvar[38;2;248;248;242m timelineEntries: [ChatTimelineEntry] [38;2;249;38;114m=[38;2;248;248;242m [][0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;117;113;94m/// Messages-style entrance state: which rows currently animate, which IDs[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;117;113;94m/// the timeline already showed, and whether the opening cascade of this[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;117;113;94m/// conversation has played.[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114m@State[38;2;248;248;242m [38;2;249;38;114mprivate[38;2;248;248;242m [38;2;102;217;239mvar[38;2;248;248;242m entranceModes: [[38;2;166;226;46mString[38;2;248;248;242m: MessageBubbleEntrance[38;2;249;38;114m.[38;2;248;248;242mMode] [38;2;249;38;114m=[38;2;248;248;242m [[38;2;249;38;114m:[38;2;248;248;242m][0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114m@State[38;2;248;248;242m [38;2;249;38;114mprivate[38;2;248;248;242m [38;2;102;217;239mvar[38;2;248;248;242m knownEntranceIDs: Set[38;2;249;38;114m<[38;2;166;226;46mString[38;2;249;38;114m>[38;2;248;248;242m [38;2;249;38;114m=[38;2;248;248;242m [][0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114m@State[38;2;248;248;242m [38;2;249;38;114mprivate[38;2;248;248;242m [38;2;102;217;239mvar[38;2;248;248;242m hasPlayedOpeningEntrance [38;2;249;38;114m=[38;2;248;248;242m [38;2;249;38;114mfalse[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114m@State[38;2;248;248;242m [38;2;249;38;114mprivate[38;2;248;248;242m [38;2;102;217;239mvar[38;2;248;248;242m entranceReleaseToken [38;2;249;38;114m=[38;2;248;248;242m UUID()[0m[48;2;0;40;0m[0K[0m
|
||||
|
||||
[38;2;248;248;242m [38;2;117;113;94m/// Messages of this conversation straight from the SwiftData store,[0m
|
||||
[38;2;248;248;242m [38;2;117;113;94m/// ordered like `AppModel.selectedMessages`: timestamp, then clientID.[0m
|
||||
|
||||
[34m─────────────────────────────[0m[34m┐[0m
|
||||
[34m150[0m:[38;2;248;248;242m [38;2;102;217;239mstruct[38;2;248;248;242m ChatView: View { [0m[34m│[0m
|
||||
[34m─────────────────────────────[0m[34m┘[0m
|
||||
[38;2;248;248;242m _captureSuspendsArchiveSync [38;2;249;38;114m=[38;2;248;248;242m State(initialValue: [38;2;249;38;114mfalse[38;2;248;248;242m)[0m
|
||||
[38;2;248;248;242m _pickerResetToken [38;2;249;38;114m=[38;2;248;248;242m State(initialValue: UUID())[0m
|
||||
[38;2;248;248;242m _timelineEntries [38;2;249;38;114m=[38;2;248;248;242m State[38;2;249;38;114m<[38;2;248;248;242m[ChatTimelineEntry][38;2;249;38;114m>[38;2;248;248;242m(initialValue: [])[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m _entranceModes [38;2;249;38;114m=[38;2;248;248;242m State[38;2;249;38;114m<[38;2;248;248;242m[[38;2;166;226;46mString[38;2;248;248;242m: MessageBubbleEntrance[38;2;249;38;114m.[38;2;248;248;242mMode][38;2;249;38;114m>[38;2;248;248;242m([0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m initialValue: [[38;2;249;38;114m:[38;2;248;248;242m][0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m )[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m _knownEntranceIDs [38;2;249;38;114m=[38;2;248;248;242m State[38;2;249;38;114m<[38;2;248;248;242mSet[38;2;249;38;114m<[38;2;166;226;46mString[38;2;249;38;114m>>[38;2;248;248;242m(initialValue: [])[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m _hasPlayedOpeningEntrance [38;2;249;38;114m=[38;2;248;248;242m State(initialValue: [38;2;249;38;114mfalse[38;2;248;248;242m)[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m _entranceConversationID [38;2;249;38;114m=[38;2;248;248;242m State[38;2;249;38;114m<[38;2;166;226;46mString[38;2;248;248;242m?[38;2;249;38;114m>[38;2;248;248;242m(initialValue: nil)[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m _entranceReleaseToken [38;2;249;38;114m=[38;2;248;248;242m State(initialValue: UUID())[0m[48;2;0;40;0m[0K[0m
|
||||
[38;2;248;248;242m }[0m
|
||||
|
||||
[38;2;248;248;242m [38;2;102;217;239mvar[38;2;248;248;242m body: some View {[0m
|
||||
|
||||
[34m─────────────────────────────[0m[34m┐[0m
|
||||
[34m390[0m:[38;2;248;248;242m [38;2;102;217;239mstruct[38;2;248;248;242m ChatView: View { [0m[34m│[0m
|
||||
[34m─────────────────────────────[0m[34m┘[0m
|
||||
[38;2;248;248;242m historyTopTriggerVisible [38;2;249;38;114m=[38;2;248;248;242m [38;2;249;38;114mfalse[0m
|
||||
[38;2;248;248;242m historyAutoContinueCount [38;2;249;38;114m=[38;2;248;248;242m [38;2;190;132;255m0[0m
|
||||
[38;2;248;248;242m emptyHistoryRetryCount [38;2;249;38;114m=[38;2;248;248;242m [38;2;190;132;255m0[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m entranceModes [38;2;249;38;114m=[38;2;248;248;242m [[38;2;249;38;114m:[38;2;248;248;242m][0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m knownEntranceIDs [38;2;249;38;114m=[38;2;248;248;242m [][0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m hasPlayedOpeningEntrance [38;2;249;38;114m=[38;2;248;248;242m [38;2;249;38;114mfalse[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m entranceReleaseToken [38;2;249;38;114m=[38;2;248;248;242m UUID()[0m[48;2;0;40;0m[0K[0m
|
||||
[38;2;248;248;242m }[0m
|
||||
[38;2;248;248;242m [38;2;249;38;114m.[38;2;248;248;242monDisappear {[0m
|
||||
[38;2;248;248;242m model[38;2;249;38;114m.[38;2;248;248;242mendConversationViewing(jid: conversation[38;2;249;38;114m.[38;2;248;248;242mjid)[0m
|
||||
|
||||
[34m─────────────────────────────[0m[34m┐[0m
|
||||
[34m496[0m:[38;2;248;248;242m [38;2;102;217;239mstruct[38;2;248;248;242m ChatView: View { [0m[34m│[0m
|
||||
[34m─────────────────────────────[0m[34m┘[0m
|
||||
|
||||
[38;2;248;248;242m [38;2;249;38;114mprivate[38;2;248;248;242m [38;2;102;217;239mfunc[38;2;248;248;242m [38;2;166;226;46mrebuildTimelineEntries[38;2;248;248;242m(from newMessages: [ChatMessage]? [38;2;249;38;114m=[38;2;248;248;242m nil)[0m
|
||||
[38;2;248;248;242m {[0m
|
||||
[48;2;63;0;1m [48;2;144;16;17mtimelineEntries[48;2;63;0;1m = [48;2;144;16;17mChatTimelineEntry.make(from: [48;2;63;0;1mnewMessages ?? messages[48;2;144;16;17m)[0m[48;2;63;0;1m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [48;2;0;96;0;38;2;102;217;239mlet[38;2;248;248;242m source[48;2;0;40;0m [38;2;249;38;114m=[38;2;248;248;242m newMessages ?? messages[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m timelineEntries [38;2;249;38;114m=[38;2;248;248;242m ChatTimelineEntry[38;2;249;38;114m.[38;2;248;248;242mmake(from: source)[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m updateEntranceModes([38;2;249;38;114mfor[38;2;248;248;242m: source)[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m }[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;117;113;94m/// Decides which rows play a Messages-style entrance after a rebuild.[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;117;113;94m/// The first page of an opened conversation cascades bottom-up; rows that[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;117;113;94m/// appear later (a send, an incoming message) launch from the composer's[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;117;113;94m/// edge. Modes are released once the animation window closes so a row[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;117;113;94m/// recycled by the lazy stack during scrolling reappears statically.[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114mprivate[38;2;248;248;242m [38;2;102;217;239mfunc[38;2;248;248;242m [38;2;166;226;46mupdateEntranceModes[38;2;248;248;242m([38;2;249;38;114mfor[38;2;248;248;242m newMessages: [ChatMessage]) {[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;102;217;239mlet[38;2;248;248;242m ids [38;2;249;38;114m=[38;2;248;248;242m newMessages[38;2;249;38;114m.[38;2;248;248;242mmap(\[38;2;249;38;114m.[38;2;248;248;242mclientID)[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;102;217;239mlet[38;2;248;248;242m known [38;2;249;38;114m=[38;2;248;248;242m knownEntranceIDs[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m knownEntranceIDs [38;2;249;38;114m=[38;2;248;248;242m Set(ids)[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114mguard[38;2;248;248;242m hasPlayedOpeningEntrance [38;2;249;38;114melse[38;2;248;248;242m {[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114mguard[38;2;248;248;242m [38;2;249;38;114m![38;2;248;248;242mids[38;2;249;38;114m.[38;2;248;248;242misEmpty [38;2;249;38;114melse[38;2;248;248;242m { [38;2;249;38;114mreturn[38;2;248;248;242m }[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m hasPlayedOpeningEntrance [38;2;249;38;114m=[38;2;248;248;242m [38;2;249;38;114mtrue[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;117;113;94m// The first message ever in an empty chat should launch out of[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;117;113;94m// the composer like any other send, not cascade in.[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114mif[38;2;248;248;242m ids[38;2;249;38;114m.[38;2;248;248;242mcount [38;2;249;38;114m==[38;2;248;248;242m [38;2;190;132;255m1[38;2;248;248;242m, known[38;2;249;38;114m.[38;2;248;248;242misEmpty,[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m newMessages[38;2;249;38;114m.[38;2;248;248;242mfirst?[38;2;249;38;114m.[38;2;248;248;242mdirection [38;2;249;38;114m==[38;2;248;248;242m [38;2;249;38;114m.[38;2;248;248;242moutgoing[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m {[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m applyLaunch([38;2;249;38;114mfor[38;2;248;248;242m: newMessages, ids: ids)[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114mreturn[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m }[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;102;217;239mlet[38;2;248;248;242m delays [38;2;249;38;114m=[38;2;248;248;242m ChatMessageEntrancePolicy[38;2;249;38;114m.[38;2;248;248;242mentranceDelays([0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m forIDs: ids[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m )[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114mfor[38;2;248;248;242m (id, delay) [38;2;249;38;114min[38;2;248;248;242m delays {[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m entranceModes[id] [38;2;249;38;114m=[38;2;248;248;242m [38;2;249;38;114m.[38;2;248;248;242mcascade(delay: delay)[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m }[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m scheduleEntranceRelease([0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m ids: Set(delays[38;2;249;38;114m.[38;2;248;248;242mkeys),[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m after: ChatMessageEntrancePolicy[38;2;249;38;114m.[38;2;248;248;242mentranceCascadeDuration[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m )[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114mreturn[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m }[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m applyLaunch([0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114mfor[38;2;248;248;242m: newMessages,[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m ids: ChatMessageEntrancePolicy[38;2;249;38;114m.[38;2;248;248;242mappendedLaunchIDs([0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114min[38;2;248;248;242m: ids,[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m after: known[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m )[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m )[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m }[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114mprivate[38;2;248;248;242m [38;2;102;217;239mfunc[38;2;248;248;242m [38;2;166;226;46mapplyLaunch[38;2;248;248;242m([38;2;249;38;114mfor[38;2;248;248;242m messages: [ChatMessage], ids: [[38;2;166;226;46mString[38;2;248;248;242m]) {[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114mguard[38;2;248;248;242m [38;2;249;38;114m![38;2;248;248;242mids[38;2;249;38;114m.[38;2;248;248;242misEmpty [38;2;249;38;114melse[38;2;248;248;242m { [38;2;249;38;114mreturn[38;2;248;248;242m }[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;102;217;239mlet[38;2;248;248;242m launched [38;2;249;38;114m=[38;2;248;248;242m Set(ids)[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;102;217;239mvar[38;2;248;248;242m applied: Set[38;2;249;38;114m<[38;2;166;226;46mString[38;2;249;38;114m>[38;2;248;248;242m [38;2;249;38;114m=[38;2;248;248;242m [][0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114mfor[38;2;248;248;242m message [38;2;249;38;114min[38;2;248;248;242m messages [38;2;249;38;114mwhere[38;2;248;248;242m launched[38;2;249;38;114m.[38;2;248;248;242mcontains(message[38;2;249;38;114m.[38;2;248;248;242mclientID) {[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m entranceModes[message[38;2;249;38;114m.[38;2;248;248;242mclientID] [38;2;249;38;114m=[38;2;248;248;242m [38;2;249;38;114m.[38;2;248;248;242mlaunch([0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m outgoing: message[38;2;249;38;114m.[38;2;248;248;242mdirection [38;2;249;38;114m==[38;2;248;248;242m [38;2;249;38;114m.[38;2;248;248;242moutgoing[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m )[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m applied[38;2;249;38;114m.[38;2;248;248;242minsert(message[38;2;249;38;114m.[38;2;248;248;242mclientID)[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m }[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m scheduleEntranceRelease([0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m ids: applied,[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m after: ChatMessageEntrancePolicy[38;2;249;38;114m.[38;2;248;248;242mentranceAnimationDuration[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m )[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m }[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114mprivate[38;2;248;248;242m [38;2;102;217;239mfunc[38;2;248;248;242m [38;2;166;226;46mscheduleEntranceRelease[38;2;248;248;242m([0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m ids: Set[38;2;249;38;114m<[38;2;166;226;46mString[38;2;249;38;114m>[38;2;248;248;242m,[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m after delay: TimeInterval[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m ) {[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114mguard[38;2;248;248;242m [38;2;249;38;114m![38;2;248;248;242mids[38;2;249;38;114m.[38;2;248;248;242misEmpty [38;2;249;38;114melse[38;2;248;248;242m { [38;2;249;38;114mreturn[38;2;248;248;242m }[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;102;217;239mlet[38;2;248;248;242m token [38;2;249;38;114m=[38;2;248;248;242m entranceReleaseToken[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m Task { [38;2;249;38;114m@MainActor[38;2;248;248;242m [38;2;249;38;114min[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114mtry[38;2;248;248;242m? await Task[38;2;249;38;114m.[38;2;248;248;242msleep([0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m nanoseconds: [38;2;166;226;46mUInt64[38;2;248;248;242m(delay [38;2;249;38;114m*[38;2;248;248;242m [38;2;190;132;255m1_000_000_000[38;2;248;248;242m)[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m )[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114mguard[38;2;248;248;242m [38;2;249;38;114m![38;2;248;248;242mTask[38;2;249;38;114m.[38;2;248;248;242misCancelled, token [38;2;249;38;114m==[38;2;248;248;242m entranceReleaseToken [38;2;249;38;114melse[38;2;248;248;242m {[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114mreturn[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m }[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114mfor[38;2;248;248;242m id [38;2;249;38;114min[38;2;248;248;242m ids {[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m entranceModes[id] [38;2;249;38;114m=[38;2;248;248;242m nil[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m }[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m }[0m[48;2;0;40;0m[0K[0m
|
||||
[38;2;248;248;242m }[0m
|
||||
|
||||
[38;2;248;248;242m [38;2;249;38;114mprivate[38;2;248;248;242m [38;2;102;217;239mvar[38;2;248;248;242m isSelectingMessages: [38;2;166;226;46mBool[38;2;248;248;242m {[0m
|
||||
|
||||
[34m─────────────────────────────[0m[34m┐[0m
|
||||
[34m704[0m:[38;2;248;248;242m [38;2;102;217;239mstruct[38;2;248;248;242m ChatView: View { [0m[34m│[0m
|
||||
[34m─────────────────────────────[0m[34m┘[0m
|
||||
[38;2;248;248;242m )[0m
|
||||
[38;2;248;248;242m }[0m
|
||||
[38;2;248;248;242m )[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m [38;2;249;38;114m.[38;2;248;248;242mmodifier([0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m MessageBubbleEntrance([0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m mode: entranceModes[[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m message[38;2;249;38;114m.[38;2;248;248;242mclientID[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m ] ?? [38;2;249;38;114m.[38;2;248;248;242msettled[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m )[0m[48;2;0;40;0m[0K[0m
|
||||
[48;2;0;40;0;38;2;248;248;242m )[0m[48;2;0;40;0m[0K[0m
|
||||
[38;2;248;248;242m [38;2;249;38;114m.[38;2;248;248;242mid(message[38;2;249;38;114m.[38;2;248;248;242mclientID)[0m
|
||||
[38;2;248;248;242m }[0m
|
||||
[38;2;248;248;242m }[0m
|
||||
Reference in New Issue
Block a user