Automatically try reloading failed images on network changes (#3170)

* Add the networkMonitor to the NSE and delete the old MockMediaLoader

* Generate a MediaLoaderMock through AutoMockable and use it in the MediaProviderTests

* Implement an ImageProvider mechanism that automatically tries reloading images when connectivity is established again and use it for LoadableImages

* Merge the ImageProvider protocol back into the MediaProvider

* Address PR review comments

* Provide a default sdk client request configuration

* Address tasks not automatically cancelling themselves when views get deallocated
This commit is contained in:
Stefan Ceriu
2024-08-15 18:20:19 +03:00
committed by GitHub
parent cf35056c78
commit f89c3e54b4
92 changed files with 618 additions and 332 deletions
@@ -1,31 +0,0 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import UIKit
protocol ImageProviderProtocol {
func imageFromSource(_ source: MediaSourceProxy?, size: CGSize?) -> UIImage?
func loadImageFromSource(_ source: MediaSourceProxy, size: CGSize?) async -> Result<UIImage, MediaProviderError>
func loadImageDataFromSource(_ source: MediaSourceProxy) async -> Result<Data, MediaProviderError>
}
extension ImageProviderProtocol {
func imageFromSource(_ source: MediaSourceProxy?) -> UIImage? {
imageFromSource(source, size: nil)
}
}
@@ -16,6 +16,7 @@
import Foundation
// sourcery: AutoMockable
protocol MediaLoaderProtocol {
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data
@@ -14,17 +14,21 @@
// limitations under the License.
//
import Combine
import Kingfisher
import UIKit
struct MediaProvider: MediaProviderProtocol {
private let mediaLoader: MediaLoaderProtocol
private let imageCache: Kingfisher.ImageCache
private let networkMonitor: NetworkMonitorProtocol?
init(mediaLoader: MediaLoaderProtocol,
imageCache: Kingfisher.ImageCache) {
imageCache: Kingfisher.ImageCache,
networkMonitor: NetworkMonitorProtocol?) {
self.mediaLoader = mediaLoader
self.imageCache = imageCache
self.networkMonitor = networkMonitor
}
// MARK: Images
@@ -71,6 +75,45 @@ struct MediaProvider: MediaProviderProtocol {
}
}
func loadImageRetryingOnReconnection(_ source: MediaSourceProxy, size: CGSize?) -> Task<UIImage, any Error> {
guard let networkMonitor else {
fatalError("This method shouldn't be invoked without a NetworkMonitor set.")
}
return Task {
if case let .success(image) = await loadImageFromSource(source, size: size) {
return image
}
guard !Task.isCancelled else {
throw MediaProviderError.cancelled
}
for await reachability in networkMonitor.reachabilityPublisher.values {
guard !Task.isCancelled else {
throw MediaProviderError.cancelled
}
guard reachability == .reachable else {
continue
}
switch await loadImageFromSource(source, size: size) {
case .success(let image):
return image
case .failure:
// If it fails after a retry with the network available
// then something else must be wrong. Bail out.
if reachability == .reachable {
throw MediaProviderError.cancelled
}
}
}
throw MediaProviderError.cancelled
}
}
func loadImageDataFromSource(_ source: MediaSourceProxy) async -> Result<Data, MediaProviderError> {
do {
let imageData = try await mediaLoader.loadMediaContentForSource(source)
@@ -22,14 +22,29 @@ enum MediaProviderError: Error {
case failedRetrievingFile
case invalidImageData
case failedRetrievingThumbnail
case cancelled
}
protocol MediaProviderProtocol: ImageProviderProtocol {
func loadFileFromSource(_ source: MediaSourceProxy, body: String?) async -> Result<MediaFileHandleProxy, MediaProviderError>
protocol MediaProviderProtocol {
func imageFromSource(_ source: MediaSourceProxy?, size: CGSize?) -> UIImage?
func loadImageFromSource(_ source: MediaSourceProxy, size: CGSize?) async -> Result<UIImage, MediaProviderError>
func loadImageDataFromSource(_ source: MediaSourceProxy) async -> Result<Data, MediaProviderError>
func loadImageRetryingOnReconnection(_ source: MediaSourceProxy, size: CGSize?) -> Task<UIImage, Error>
func loadThumbnailForSource(source: MediaSourceProxy, size: CGSize) async -> Result<Data, MediaProviderError>
func loadFileFromSource(_ source: MediaSourceProxy, body: String?) async -> Result<MediaFileHandleProxy, MediaProviderError>
}
extension MediaProviderProtocol {
func imageFromSource(_ source: MediaSourceProxy?) -> UIImage? {
imageFromSource(source, size: nil)
}
func loadImageRetryingOnReconnection(_ source: MediaSourceProxy) -> Task<UIImage, Error> {
loadImageRetryingOnReconnection(source, size: nil)
}
func loadFileFromSource(_ source: MediaSourceProxy) async -> Result<MediaFileHandleProxy, MediaProviderError> {
await loadFileFromSource(source, body: nil)
}
@@ -58,4 +58,14 @@ struct MockMediaProvider: MediaProviderProtocol {
}
return .failure(.failedRetrievingFile)
}
func loadImageRetryingOnReconnection(_ source: MediaSourceProxy, size: CGSize?) -> Task<UIImage, any Error> {
Task {
guard let image = UIImage(systemName: "photo") else {
fatalError()
}
return image
}
}
}
@@ -111,7 +111,8 @@ class UserSessionStore: UserSessionStoreProtocol {
private func buildUserSessionWithClient(_ clientProxy: ClientProxyProtocol) -> UserSessionProtocol {
let mediaProvider = MediaProvider(mediaLoader: clientProxy,
imageCache: .onlyInMemory)
imageCache: .onlyInMemory,
networkMonitor: networkMonitor)
let voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider)