fix more animations
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s

This commit is contained in:
wt committed 2026-09-18 14:42:57 +07:00
1 parent 3e990e7af9
commit 28e8c2e2a8
3 files changed
+14 -165

No files matched your search

+12
View File
@@ -7,6 +7,10 @@
objects = {
/* Begin PBXBuildFile section */
AB12CD34EF56010203040503 /* ChatMessageEntrancePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB12CD34EF56010203040501 /* ChatMessageEntrancePolicy.swift */; };
AB12CD34EF56010203040504 /* ChatMessageEntrancePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB12CD34EF56010203040501 /* ChatMessageEntrancePolicy.swift */; };
AB12CD34EF56010203040505 /* MessageBubbleEntrance.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB12CD34EF56010203040502 /* MessageBubbleEntrance.swift */; };
AB12CD34EF56010203040506 /* MessageBubbleEntrance.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB12CD34EF56010203040502 /* MessageBubbleEntrance.swift */; };
00FCAB7210D005D2C30747CB /* AccountConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2006464EAC88C6E4E2B08AC8 /* AccountConfiguration.swift */; };
01039E013AB84FA338B2A5CF /* MessageCorrectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 096748EF4DC92889BCAB3260 /* MessageCorrectionTests.swift */; };
01E4131ED4DBED0BEAD465A2 /* ServerInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBA20B768A9C675FABCDC821 /* ServerInfoView.swift */; };
@@ -332,6 +336,8 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
AB12CD34EF56010203040501 /* ChatMessageEntrancePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageEntrancePolicy.swift; sourceTree = "<group>"; };
AB12CD34EF56010203040502 /* MessageBubbleEntrance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBubbleEntrance.swift; sourceTree = "<group>"; };
0142FF4A1B05C373DAB4CDBE /* VideoNoteCaptureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoNoteCaptureView.swift; sourceTree = "<group>"; };
0386C2687C1C60EC734FDBB0 /* BubbleShapes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleShapes.swift; sourceTree = "<group>"; };
03AC6BC7FC7CFFD69A20DA17 /* MessageBubble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBubble.swift; sourceTree = "<group>"; };
@@ -569,6 +575,7 @@
C04D749B7CA93CB7C59127D6 /* AttachmentDraft.swift */,
6079CE75BAB995515A8D5460 /* CallSnapshot.swift */,
12C6A6B83DA076867481CCF3 /* ChatMessage.swift */,
AB12CD34EF56010203040501 /* ChatMessageEntrancePolicy.swift */,
C37AAE886AA3B8782B89E1A0 /* ChatScrollPositionPolicy.swift */,
13E35F7F0CB0243311FCDD61 /* ChatTimelineEntry.swift */,
1C3E51BB890F05FD02C71F6E /* ChatTypingPolicy.swift */,
@@ -816,6 +823,7 @@
B96E7970930AB7F09CB5DA9C /* LocationMessagePreview.swift */,
4380219D3AF1D75EDF4D3167 /* MediaViewer.swift */,
03AC6BC7FC7CFFD69A20DA17 /* MessageBubble.swift */,
AB12CD34EF56010203040502 /* MessageBubbleEntrance.swift */,
8D2294F0FE44B2268A0620BB /* PhotoAttachmentPreview.swift */,
19C556A41E640592AF6376B6 /* ReplyThreadOverlay.swift */,
5003B4F9DF154543555E9825 /* RTCVideoRendererView.swift */,
@@ -1054,6 +1062,7 @@
97A3A0635ADA8A8227C94216 /* ChatBubbleShape.swift in Sources */,
7AC93485DA12BC90717739E9 /* ChatMediaImageCache.swift in Sources */,
66489D4D15366683D3095B3D /* ChatMessage.swift in Sources */,
AB12CD34EF56010203040503 /* ChatMessageEntrancePolicy.swift in Sources */,
D5E6B1859101ABBD33D90C3C /* ChatScrollPositionPolicy.swift in Sources */,
1E239B9338CEEA7EA704626C /* ChatTimelineEntry.swift in Sources */,
433A7B65E635FB59021F7D87 /* ChatTypingPolicy.swift in Sources */,
@@ -1100,6 +1109,7 @@
B33870CD89289E44882750C3 /* MediaViewer.swift in Sources */,
EBAF4861DD93FBEAA4B37D1C /* MediaViewerItem.swift in Sources */,
230E0DAA5396EA507D6F6D47 /* MessageBubble.swift in Sources */,
AB12CD34EF56010203040505 /* MessageBubbleEntrance.swift in Sources */,
FD9F8640841236BB78563573 /* MessageReaction.swift in Sources */,
30BE19F459FDDAD640F66C68 /* MessageReplyFallback.swift in Sources */,
94FB3597E01FC508F872861E /* MessageReplySwipePolicy.swift in Sources */,
@@ -1180,6 +1190,7 @@
2A7611650AB52EDABB978362 /* ChatBubbleShape.swift in Sources */,
6FCF6A4EFF8AE1FBFC2D81E1 /* ChatMediaImageCache.swift in Sources */,
8714A5A67956D6A78D4D3E4B /* ChatMessage.swift in Sources */,
AB12CD34EF56010203040504 /* ChatMessageEntrancePolicy.swift in Sources */,
8B98B818AC4379F03907B0E0 /* ChatScrollPositionPolicy.swift in Sources */,
7F7BA6805C1D1EBCEAFDD3AD /* ChatTimelineEntry.swift in Sources */,
B4AA654DF3A53AAAAC23915D /* ChatTypingPolicy.swift in Sources */,
@@ -1226,6 +1237,7 @@
CC5297F90A3DBD45C5E41A3A /* MediaViewer.swift in Sources */,
2C5BAAEF4EC7AAE5FE0FEEDB /* MediaViewerItem.swift in Sources */,
A90142509385DE6E83818953 /* MessageBubble.swift in Sources */,
AB12CD34EF56010203040506 /* MessageBubbleEntrance.swift in Sources */,
ECCD95DD4E16E0B393C707A9 /* MessageReaction.swift in Sources */,
C2CC9BABAE68FA9258694766 /* MessageReplyFallback.swift in Sources */,
D4E1CF1F7D948B2E72C063BE /* MessageReplySwipePolicy.swift in Sources */,
+2 -5
View File
@@ -155,7 +155,6 @@ struct ChatView: View {
)
_knownEntranceIDs = State<Set<String>>(initialValue: [])
_hasPlayedOpeningEntrance = State(initialValue: false)
_entranceConversationID = State<String?>(initialValue: nil)
_entranceReleaseToken = State(initialValue: UUID())
}
@@ -494,8 +493,7 @@ struct ChatView: View {
}
}
private func rebuildTimelineEntries(from newMessages: [ChatMessage]? = nil)
{
private func rebuildTimelineEntries(from newMessages: [ChatMessage]? = nil) {
let source = newMessages ?? messages
timelineEntries = ChatTimelineEntry.make(from: source)
updateEntranceModes(for: source)
@@ -1568,8 +1566,7 @@ struct ChatView: View {
}
}
private func sendRecordedVideoNote(_ recording: VideoNoteRecorder.Recording)
{
private func sendRecordedVideoNote(_ recording: VideoNoteRecorder.Recording) {
videoNoteIsSending = true
Task { @MainActor in
defer {
-160
View File
@@ -1,160 +0,0 @@
Sources/Shared/UI/ChatView.swift
────────────────────────────────────────────────────────────────────────────────────────────
────────────────────────────┐
75: 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.
─────────────────────────────┐
150: 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 {
─────────────────────────────┐
390: struct ChatView: View { │
─────────────────────────────┘
 historyTopTriggerVisible = false
 historyAutoContinueCount = 0
 emptyHistoryRetryCount = 0
 entranceModes = [:]
 knownEntranceIDs = []
 hasPlayedOpeningEntrance = false
 entranceReleaseToken = UUID()
 }
 .onDisappear {
 model.endConversationViewing(jid: conversation.jid)
─────────────────────────────┐
496: 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 {
─────────────────────────────┐
704: struct ChatView: View { │
─────────────────────────────┘
 )
 }
 )
 .modifier(
 MessageBubbleEntrance(
 mode: entranceModes[
 message.clientID
 ] ?? .settled
 )
 )
 .id(message.clientID)
 }
 }