Add deferred spinner for voice messages (#2018)

* Add  delayedLoaderPlaybackState

* Add more tests

* Fix Xcode 14 build error

* Refactor  delayedLoaderPlaybackState -> playerButtonPlaybackState

* Fix runtime issue
This commit is contained in:
Alfonso Grillo
2023-11-03 11:39:22 +01:00
committed by GitHub
parent 86621eb2d0
commit 00994dc3d7
6 changed files with 75 additions and 10 deletions

View File

@@ -88,13 +88,13 @@ private struct VoiceMessageButtonStyle: ButtonStyle {
}
extension VoiceMessageButton.State {
init(state: AudioPlayerPlaybackState) {
init(_ state: AudioPlayerPlaybackState) {
switch state {
case .loading:
self = .loading
case .playing:
self = .playing
default:
case .stopped, .error, .readyToPlay:
self = .paused
}
}

View File

@@ -43,7 +43,7 @@ struct VoiceMessagePreviewComposer: View {
var body: some View {
HStack {
HStack {
VoiceMessageButton(state: .init(state: playerState.playbackState),
VoiceMessageButton(state: .init(playerState.playerButtonPlaybackState),
size: .small,
action: onPlayPause)
Text(timeLabelContent)

View File

@@ -37,11 +37,15 @@ class AudioPlayerState: ObservableObject, Identifiable {
let duration: Double
let waveform: EstimatedWaveform
@Published private(set) var playbackState: AudioPlayerPlaybackState
/// It's similar to `playbackState`, with the a difference: `.loading`
/// updates are delayed by a fixed amount of time
@Published private(set) var playerButtonPlaybackState: AudioPlayerPlaybackState
@Published private(set) var progress: Double
@Published private(set) var showProgressIndicator: Bool
private weak var audioPlayer: AudioPlayerProtocol?
private var cancellables: Set<AnyCancellable> = []
private var audioPlayerSubscription: AnyCancellable?
private var playbackStateSubscription: AnyCancellable?
private var displayLink: CADisplayLink?
/// The file url that the last player attached to this object has loaded.
@@ -63,6 +67,8 @@ class AudioPlayerState: ObservableObject, Identifiable {
self.progress = progress
showProgressIndicator = false
playbackState = .stopped
playerButtonPlaybackState = .stopped
setupPlaybackStateSubscription()
}
deinit {
@@ -99,7 +105,7 @@ class AudioPlayerState: ObservableObject, Identifiable {
func detachAudioPlayer() {
audioPlayer?.stop()
stopPublishProgress()
cancellables = []
audioPlayerSubscription = nil
audioPlayer = nil
playbackState = .stopped
showProgressIndicator = false
@@ -112,7 +118,7 @@ class AudioPlayerState: ObservableObject, Identifiable {
// MARK: - Private
private func subscribeToAudioPlayer(audioPlayer: AudioPlayerProtocol) {
audioPlayer.actions
audioPlayerSubscription = audioPlayer.actions
.receive(on: DispatchQueue.main)
.sink { [weak self] action in
guard let self else {
@@ -122,7 +128,6 @@ class AudioPlayerState: ObservableObject, Identifiable {
await self.handleAudioPlayerAction(action)
}
}
.store(in: &cancellables)
}
private func handleAudioPlayerAction(_ action: AudioPlayerAction) async {
@@ -175,6 +180,24 @@ class AudioPlayerState: ObservableObject, Identifiable {
private func restoreAudioPlayerState(audioPlayer: AudioPlayerProtocol) async {
await audioPlayer.seek(to: progress)
}
private func setupPlaybackStateSubscription() {
playbackStateSubscription = $playbackState
.map { state in
switch state {
case .loading:
return Just(state)
.delay(for: .seconds(2), scheduler: RunLoop.main)
.eraseToAnyPublisher()
case .playing, .stopped, .error, .readyToPlay:
return Just(state)
.eraseToAnyPublisher()
}
}
.switchToLatest()
.removeDuplicates()
.weakAssign(to: \.playerButtonPlaybackState, on: self)
}
}
extension AudioPlayerState: Equatable {

View File

@@ -409,8 +409,10 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
guard let playerState = mediaPlayerProvider.playerState(for: .timelineItemIdentifier(timelineItem.id)) else {
continue
}
playerState.detachAudioPlayer()
mediaPlayerProvider.unregister(audioPlayerState: playerState)
Task { @MainActor in
playerState.detachAudioPlayer()
mediaPlayerProvider.unregister(audioPlayerState: playerState)
}
}
newTimelineItems.append(timelineItem)

View File

@@ -43,7 +43,7 @@ struct VoiceMessageRoomPlaybackView: View {
var body: some View {
HStack {
HStack {
VoiceMessageButton(state: .init(state: playerState.playbackState),
VoiceMessageButton(state: .init(playerState.playerButtonPlaybackState),
size: .medium,
action: onPlayPause)
Text(timeLabelContent)

View File

@@ -69,6 +69,46 @@ class AudioPlayerStateTests: XCTestCase {
XCTAssertFalse(audioPlayerState.showProgressIndicator)
}
func testDelayedState() async throws {
audioPlayerState.attachAudioPlayer(audioPlayerMock)
XCTAssert(audioPlayerState.isAttached)
XCTAssertEqual(audioPlayerState.playbackState, .loading)
XCTAssertEqual(audioPlayerState.playerButtonPlaybackState, .stopped)
let deferred = deferFulfillment(audioPlayerState.$playerButtonPlaybackState) { output in
switch output {
case .loading:
return true
default:
return false
}
}
try await deferred.fulfill()
XCTAssertEqual(audioPlayerState.playerButtonPlaybackState, .loading)
}
func testOtherActionsAreNotDelayed() async throws {
audioPlayerState.attachAudioPlayer(audioPlayerMock)
XCTAssertEqual(audioPlayerState.playbackState, .loading)
XCTAssertEqual(audioPlayerState.playerButtonPlaybackState, .stopped)
let deferred = deferFulfillment(audioPlayerState.$playerButtonPlaybackState) { output in
switch output {
case .playing:
return true
default:
return false
}
}
audioPlayerActionsSubject.send(.didStartPlaying)
try await deferred.fulfill()
XCTAssertEqual(audioPlayerState.playbackState, .playing)
XCTAssertEqual(audioPlayerState.playerButtonPlaybackState, .playing)
}
func testReportError() async throws {
XCTAssertEqual(audioPlayerState.playbackState, .stopped)
audioPlayerState.reportError(AudioPlayerError.genericError)