Skip to content

Commit 6cc69a5

Browse files
isaacclaude
andcommitted
Profile: the music strip arrives with the transition
The header showed the profile's saved-music list only, and the list context publishes an empty loading state at once, so the full user's track (which the chat has usually fetched already) was ignored until the list answered. On a first open that is the network: the strip was missing from the first layout, and when it landed during the chat-to-profile expansion the held layout was applied without animation as the transition ended, snapping the strip and the 24 pt header growth in one frame. - Until the list answers, the strip shows the full user's track (peerInfoProfileMusic, tested); a list that answers empty shows none. - The strip arriving or leaving counts as an info change in updateData, so a late arrival animates. - A strip change while the views are lent to an expansion is applied with the same spring when they come back. - Tapping the strip before the list answers waits for it, since the player plays the list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent a918a3c commit 6cc69a5

4 files changed

Lines changed: 80 additions & 13 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Foundation
2+
3+
/// The track a profile's music strip shows: the first of the profile's saved-music list (`list`, nil where the screen
4+
/// has none). While that list has not answered (loading with nothing in it, before its cache or the server replies:
5+
/// a profile opened for the first time), the full user's track (`CachedUserData.savedMusic`), which the chat has usually
6+
/// fetched already, so the strip is in the profile's first layout and arrives with the transition instead of after it.
7+
/// A list that answered empty shows none, whatever the full user still says.
8+
public func peerInfoProfileMusic<File>(list: (files: [File], isLoading: Bool)?, fullUserMusic: File?) -> File? {
9+
guard let list else {
10+
return fullUserMusic
11+
}
12+
if let first = list.files.first {
13+
return first
14+
}
15+
return list.isLoading ? fullUserMusic : nil
16+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import XCTest
2+
@testable import PeerInfoPaneNode
3+
4+
final class PeerInfoProfileMusicTests: XCTestCase {
5+
// The list's first track once it has one, loading or not.
6+
func test_listTrack() {
7+
XCTAssertEqual(peerInfoProfileMusic(list: (files: [1, 2], isLoading: false), fullUserMusic: 3), 1)
8+
XCTAssertEqual(peerInfoProfileMusic(list: (files: [1, 2], isLoading: true), fullUserMusic: 3), 1)
9+
}
10+
11+
// A list that has not answered yet: the full user's track, so the strip is there from the first layout.
12+
func test_loadingListFallsBackToFullUser() {
13+
XCTAssertEqual(peerInfoProfileMusic(list: (files: [Int](), isLoading: true), fullUserMusic: 3), 3)
14+
XCTAssertNil(peerInfoProfileMusic(list: (files: [Int](), isLoading: true), fullUserMusic: nil))
15+
}
16+
17+
// A list that answered empty: none, even while the full user still carries a track.
18+
func test_answeredEmptyListShowsNone() {
19+
XCTAssertNil(peerInfoProfileMusic(list: (files: [Int](), isLoading: false), fullUserMusic: 3))
20+
}
21+
22+
// No list on the screen: the full user's track.
23+
func test_noList() {
24+
XCTAssertEqual(peerInfoProfileMusic(list: nil, fullUserMusic: 3), 3)
25+
XCTAssertNil(peerInfoProfileMusic(list: nil as (files: [Int], isLoading: Bool)?, fullUserMusic: nil))
26+
}
27+
}

‎submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,18 @@ final class PeerInfoHeaderNode: ASDisplayNode {
196196
var hasBottomStrip: Bool {
197197
return self.music != nil
198198
}
199+
200+
/// The track the music strip shows for `peer` and `screenData` (`peerInfoProfileMusic`): none on Settings and on the
201+
/// account's own peer opened as a profile.
202+
func profileMusic(peer: EnginePeer?, screenData: PeerInfoScreenData?) -> TelegramMediaFile? {
203+
guard let peer, peer.id != self.context.account.peerId || self.isMyProfile, let screenData else {
204+
return nil
205+
}
206+
let list = screenData.savedMusicState.map { state -> (files: [TelegramMediaFile], isLoading: Bool) in
207+
return (state.files, state.dataState == .loading)
208+
}
209+
return peerInfoProfileMusic(list: list, fullUserMusic: (screenData.cachedData as? CachedUserData)?.savedMusic)
210+
}
199211

200212
var performButtonAction: ((PeerInfoHeaderButtonKey, PeerInfoHeaderButtonNode?, ContextGesture?) -> Void)?
201213
var requestAvatarExpansion: ((Bool, [AvatarGalleryEntry], AvatarGalleryEntry?, (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?) -> Void)?
@@ -622,14 +634,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
622634
backgroundCoverSubject = nil
623635
}
624636

625-
var currentSavedMusic: TelegramMediaFile?
626-
if let peer, peer.id != self.context.account.peerId || self.isMyProfile, let screenData {
627-
if let savedMusicState = screenData.savedMusicState {
628-
currentSavedMusic = savedMusicState.files.first
629-
} else if let cachedUserData = screenData.cachedData as? CachedUserData {
630-
currentSavedMusic = cachedUserData.savedMusic
631-
}
632-
}
637+
let currentSavedMusic = self.profileMusic(peer: peer, screenData: screenData)
633638
let currentWalletBalance: Int64?
634639
if !self.isSettings, let balance = (screenData?.cachedData as? CachedUserData)?.walletCard?.balance, balance >= 100_000_000 {
635640
currentWalletBalance = balance

‎submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
273273
var resolvePeerByNameDisposable: MetaDisposable?
274274
let navigationActionDisposable = MetaDisposable()
275275
let enqueueMediaMessageDisposable = MetaDisposable()
276+
private let openSavedMusicDisposable = MetaDisposable()
276277

277278
private(set) var validLayout: (ContainerViewLayout, CGFloat)?
278279
/// While an expansion has the profile's views they are held still: layouts wait, and the last one is applied when
@@ -2701,6 +2702,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
27012702
self.resolvePeerByNameDisposable?.dispose()
27022703
self.navigationActionDisposable.dispose()
27032704
self.enqueueMediaMessageDisposable.dispose()
2705+
self.openSavedMusicDisposable.dispose()
27042706
self.supportPeerDisposable.dispose()
27052707
self.tipsPeerDisposable.dispose()
27062708
self.shareStatusDisposable?.dispose()
@@ -2920,6 +2922,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
29202922
if (previousBusinessLocation == nil) != (currentBusinessLocation != nil) {
29212923
infoUpdated = true
29222924
}
2925+
// The music strip arriving or leaving changes the header's height, and the page moves with it.
2926+
if self.hasProfileMusic(data: previousData) != self.hasProfileMusic(data: data) {
2927+
infoUpdated = true
2928+
}
29232929
}
29242930

29252931
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: self.didSetReady && (membersUpdated || infoUpdated) ? .animated(duration: 0.3, curve: .spring) : .immediate)
@@ -3088,6 +3094,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
30883094
}
30893095
}
30903096

3097+
private func hasProfileMusic(data: PeerInfoScreenData?) -> Bool {
3098+
return self.headerNode.profileMusic(peer: data?.savedMessagesPeer ?? data?.peer, screenData: data) != nil
3099+
}
3100+
30913101
var previousSavedMusicTimestamp: Double?
30923102
private func displaySavedMusic() {
30933103
guard let savedMusicContext = self.data?.savedMusicContext else {
@@ -3100,15 +3110,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
31003110
}
31013111
self.previousSavedMusicTimestamp = currentTimestamp
31023112

3103-
let _ = (self.context.sharedContext.mediaManager.globalMediaPlayerState
3113+
// Until the list answers, the strip shows the full user's track (`peerInfoProfileMusic`); the player plays the
3114+
// list, so it waits for it.
3115+
let answeredList = savedMusicContext.state
3116+
|> filter { state in
3117+
return !state.files.isEmpty || state.dataState != .loading
3118+
}
31043119
|> take(1)
3105-
|> deliverOnMainQueue).start(next: { [weak self] accountStateAndType in
3120+
self.openSavedMusicDisposable.set((combineLatest(answeredList, self.context.sharedContext.mediaManager.globalMediaPlayerState |> take(1))
3121+
|> deliverOnMainQueue).start(next: { [weak self] listState, accountStateAndType in
31063122
guard let self else {
31073123
return
31083124
}
31093125
let peerId = self.peerId
31103126
var initialId: Int32
3111-
if let initialFileId = self.data?.savedMusicState?.files.first?.fileId {
3127+
if let initialFileId = listState.files.first?.fileId {
31123128
initialId = Int32(clamping: initialFileId.id % Int64(Int32.max))
31133129
} else {
31143130
initialId = 0
@@ -3139,7 +3155,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
31393155
musicController.navigationPresentation = .flatModal
31403156
self.controller?.push(musicController)
31413157
}
3142-
})
3158+
}))
31433159
}
31443160

31453161
func openAutoremove(currentValue: Int32?) {
@@ -5568,7 +5584,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
55685584
self.contentOffsetWhenLent = nil
55695585
if let held = self.layoutHeldDuringExpansion ?? self.validLayout.flatMap({ (layout: $0.0, navigationHeight: $0.1) }) {
55705586
self.layoutHeldDuringExpansion = nil
5571-
self.containerLayoutUpdated(layout: held.layout, navigationHeight: held.navigationHeight, transition: .immediate)
5587+
// The music strip arrived or left while the views were lent (the header still has what it had when they were
5588+
// lent): it moves in as it does outside an expansion, instead of appearing in one frame.
5589+
let stripChanged = self.headerNode.hasBottomStrip != self.hasProfileMusic(data: self.data)
5590+
self.containerLayoutUpdated(layout: held.layout, navigationHeight: held.navigationHeight, transition: stripChanged ? .animated(duration: 0.3, curve: .spring) : .immediate)
55725591
}
55735592
let actions = self.actionsAfterExpansion
55745593
self.actionsAfterExpansion = []

0 commit comments

Comments
 (0)