mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Media loading flow changes (#483)
* Use an imageProvider directly from the view in the home screen * Add support for media request coalescing * Rename MediaProxy to MediaLoader * Add new image loading mechanism to the room details screen avatar. * Use the `SettingsScreen` prefix for all settings screen related components * Add new image loading mechanism to the room header * Add new image loading mechanism to the room member details screen * Introduce a LoadableImage SwiftUI view that will automatically handle image loading * Adopt the new LoadableImage where possible * Fix LoadableImage not using/storing loaded images properly * Simplify media loader enqueueing * Made LodableImage load content after mediaSource updates. Adopt it on the home and settings screens * Introduce a LoadableAvatarImage component and reuse it throughout the app * Small logging tweaks, made some LoadableImage properties private * Fix redacted skeletons avatar background color * Fix placeholder avatars changing when backgrounding the app * PR comments. - Trim the @ sign off of mxid placeholders. - Only expose AvatarSize on the avatar image, use CGSize elsewhere. Co-authored-by: Doug <douglase@element.io>
This commit is contained in:
@@ -58,7 +58,7 @@ class UIKitBackgroundTask: BackgroundTaskProtocol {
|
||||
}
|
||||
|
||||
if identifier == .invalid {
|
||||
MXLog.info("Do not start background task: \(name), as OS declined")
|
||||
MXLog.error("Do not start background task: \(name), as OS declined")
|
||||
// call expiration handler immediately
|
||||
expirationHandler?(self)
|
||||
return nil
|
||||
@@ -69,7 +69,7 @@ class UIKitBackgroundTask: BackgroundTaskProtocol {
|
||||
reuse()
|
||||
}
|
||||
|
||||
MXLog.info("Start background task #\(identifier.rawValue) - \(name)")
|
||||
MXLog.verbose("Start background task #\(identifier.rawValue) - \(name)")
|
||||
}
|
||||
|
||||
func reuse() {
|
||||
@@ -92,7 +92,7 @@ class UIKitBackgroundTask: BackgroundTaskProtocol {
|
||||
|
||||
private func endTask() {
|
||||
if identifier != .invalid {
|
||||
MXLog.info("End background task #\(identifier.rawValue) - \(name) after \(readableElapsedTime)")
|
||||
MXLog.verbose("End background task #\(identifier.rawValue) - \(name) after \(readableElapsedTime)")
|
||||
|
||||
application.endBackgroundTask(identifier)
|
||||
identifier = .invalid
|
||||
|
||||
@@ -41,7 +41,7 @@ class UIKitBackgroundTaskService: BackgroundTaskServiceProtocol {
|
||||
}
|
||||
|
||||
if avoidStartingNewTasks(for: application) {
|
||||
MXLog.info("Do not start background task: \(name), as not enough time exists")
|
||||
MXLog.error("Do not start background task: \(name), as not enough time exists")
|
||||
// call expiration handler immediately
|
||||
expirationHandler?()
|
||||
return nil
|
||||
@@ -82,7 +82,7 @@ class UIKitBackgroundTaskService: BackgroundTaskServiceProtocol {
|
||||
let appState = application.applicationState
|
||||
let remainingTime = readableBackgroundTimeRemaining(application.backgroundTimeRemaining)
|
||||
|
||||
MXLog.info("Background task \(name) \(created ? "started" : "reused") with app state: \(appState) and estimated background time remaining: \(remainingTime)")
|
||||
MXLog.verbose("Background task \(name) \(created ? "started" : "reused") with app state: \(appState) and estimated background time remaining: \(remainingTime)")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
private let client: ClientProtocol
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
private var sessionVerificationControllerProxy: SessionVerificationControllerProxy?
|
||||
private let mediaProxy: MediaProxyProtocol
|
||||
private let mediaLoader: MediaLoaderProtocol
|
||||
private let clientQueue: DispatchQueue
|
||||
|
||||
private var slidingSyncObserverToken: StoppableSpawn?
|
||||
@@ -86,7 +86,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
clientQueue = .init(label: "ClientProxyQueue", attributes: .concurrent)
|
||||
|
||||
mediaProxy = MediaProxy(client: client, clientQueue: clientQueue)
|
||||
mediaLoader = MediaLoader(client: client, clientQueue: clientQueue)
|
||||
|
||||
client.setDelegate(delegate: WeakClientProxyWrapper(clientProxy: self))
|
||||
|
||||
@@ -401,16 +401,16 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
extension ClientProxy: MediaProxyProtocol {
|
||||
func mediaSourceForURL(_ url: URL) -> MediaSourceProxy {
|
||||
mediaProxy.mediaSourceForURL(url)
|
||||
extension ClientProxy: MediaLoaderProtocol {
|
||||
func mediaSourceForURL(_ url: URL) async -> MediaSourceProxy {
|
||||
await mediaLoader.mediaSourceForURL(url)
|
||||
}
|
||||
|
||||
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data {
|
||||
try await mediaProxy.loadMediaContentForSource(source)
|
||||
try await mediaLoader.loadMediaContentForSource(source)
|
||||
}
|
||||
|
||||
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data {
|
||||
try await mediaProxy.loadMediaThumbnailForSource(source, width: width, height: height)
|
||||
try await mediaLoader.loadMediaThumbnailForSource(source, width: width, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ enum PushFormat {
|
||||
// }
|
||||
}
|
||||
|
||||
protocol ClientProxyProtocol: AnyObject, MediaProxyProtocol {
|
||||
protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
var callbacks: PassthroughSubject<ClientProxyCallback, Never> { get }
|
||||
|
||||
var userID: String { get }
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// 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?
|
||||
|
||||
@discardableResult func loadImageFromSource(_ source: MediaSourceProxy, size: CGSize?) async -> Result<UIImage, MediaProviderError>
|
||||
|
||||
func imageFromURL(_ url: URL?, size: CGSize?) -> UIImage?
|
||||
|
||||
@discardableResult func loadImageFromURL(_ url: URL, size: CGSize?) async -> Result<UIImage, MediaProviderError>
|
||||
}
|
||||
|
||||
extension ImageProviderProtocol {
|
||||
func imageFromSource(_ source: MediaSourceProxy?) -> UIImage? {
|
||||
imageFromSource(source, size: nil)
|
||||
}
|
||||
|
||||
@discardableResult func loadImageFromSource(_ source: MediaSourceProxy) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromSource(source, size: nil)
|
||||
}
|
||||
|
||||
func imageFromURL(_ url: URL?) -> UIImage? {
|
||||
imageFromURL(url, size: nil)
|
||||
}
|
||||
|
||||
@discardableResult func loadImageFromURL(_ url: URL) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromURL(url, size: nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// Copyright 2022 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 Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import UIKit
|
||||
|
||||
private final class MediaRequest {
|
||||
var continuations: [CheckedContinuation<Data, Error>] = []
|
||||
}
|
||||
|
||||
actor MediaLoader: MediaLoaderProtocol {
|
||||
private let client: ClientProtocol
|
||||
private let clientQueue: DispatchQueue
|
||||
private var ongoingRequests = [MediaSourceProxy: MediaRequest]()
|
||||
|
||||
init(client: ClientProtocol,
|
||||
clientQueue: DispatchQueue = .global()) {
|
||||
self.client = client
|
||||
self.clientQueue = clientQueue
|
||||
}
|
||||
|
||||
func mediaSourceForURL(_ url: URL) -> MediaSourceProxy {
|
||||
.init(url: url)
|
||||
}
|
||||
|
||||
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data {
|
||||
try await enqueueLoadMediaRequest(forSource: source) {
|
||||
try self.client.getMediaContent(source: source.underlyingSource)
|
||||
}
|
||||
}
|
||||
|
||||
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data {
|
||||
try await enqueueLoadMediaRequest(forSource: source) {
|
||||
try self.client.getMediaThumbnail(source: source.underlyingSource, width: UInt64(width), height: UInt64(height))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func enqueueLoadMediaRequest(forSource source: MediaSourceProxy, operation: @escaping () throws -> [UInt8]) async throws -> Data {
|
||||
if let ongoingRequest = ongoingRequests[source] {
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
ongoingRequest.continuations.append(continuation)
|
||||
}
|
||||
}
|
||||
|
||||
let ongoingRequest = MediaRequest()
|
||||
ongoingRequests[source] = ongoingRequest
|
||||
|
||||
defer {
|
||||
ongoingRequests[source] = nil
|
||||
}
|
||||
|
||||
do {
|
||||
let result = try await Task.dispatch(on: clientQueue) {
|
||||
let bytes = try operation()
|
||||
return Data(bytes: bytes, count: bytes.count)
|
||||
}
|
||||
|
||||
ongoingRequest.continuations.forEach { $0.resume(returning: result) }
|
||||
|
||||
return result
|
||||
|
||||
} catch {
|
||||
ongoingRequest.continuations.forEach { $0.resume(throwing: error) }
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -16,8 +16,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol MediaProxyProtocol {
|
||||
func mediaSourceForURL(_ url: URL) -> MediaSourceProxy
|
||||
protocol MediaLoaderProtocol {
|
||||
func mediaSourceForURL(_ url: URL) async -> MediaSourceProxy
|
||||
|
||||
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data
|
||||
|
||||
@@ -18,53 +18,54 @@ import Kingfisher
|
||||
import UIKit
|
||||
|
||||
struct MediaProvider: MediaProviderProtocol {
|
||||
private let mediaProxy: MediaProxyProtocol
|
||||
private let mediaLoader: MediaLoaderProtocol
|
||||
private let imageCache: Kingfisher.ImageCache
|
||||
private let fileCache: FileCacheProtocol
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol?
|
||||
|
||||
init(mediaProxy: MediaProxyProtocol,
|
||||
init(mediaLoader: MediaLoaderProtocol,
|
||||
imageCache: Kingfisher.ImageCache,
|
||||
fileCache: FileCacheProtocol,
|
||||
backgroundTaskService: BackgroundTaskServiceProtocol?) {
|
||||
self.mediaProxy = mediaProxy
|
||||
self.mediaLoader = mediaLoader
|
||||
self.imageCache = imageCache
|
||||
self.fileCache = fileCache
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
}
|
||||
|
||||
func imageFromSource(_ source: MediaSourceProxy?, avatarSize: AvatarSize?) -> UIImage? {
|
||||
// MARK: Images
|
||||
|
||||
func imageFromSource(_ source: MediaSourceProxy?, size: CGSize?) -> UIImage? {
|
||||
guard let url = source?.url else {
|
||||
return nil
|
||||
}
|
||||
let cacheKey = cacheKeyForURL(url, avatarSize: avatarSize)
|
||||
let cacheKey = cacheKeyForURL(url, size: size)
|
||||
return imageCache.retrieveImageInMemoryCache(forKey: cacheKey, options: nil)
|
||||
}
|
||||
|
||||
func imageFromURL(_ url: URL?, avatarSize: AvatarSize?) -> UIImage? {
|
||||
func imageFromURL(_ url: URL?, size: CGSize?) -> UIImage? {
|
||||
guard let url else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return imageFromSource(.init(url: url), avatarSize: avatarSize)
|
||||
return imageFromSource(.init(url: url), size: size)
|
||||
}
|
||||
|
||||
func loadImageFromURL(_ url: URL, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromSource(.init(url: url), avatarSize: avatarSize)
|
||||
func loadImageFromURL(_ url: URL, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromSource(.init(url: url), size: size)
|
||||
}
|
||||
|
||||
func loadImageFromSource(_ source: MediaSourceProxy, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
if let image = imageFromSource(source, avatarSize: avatarSize) {
|
||||
func loadImageFromSource(_ source: MediaSourceProxy, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
if let image = imageFromSource(source, size: size) {
|
||||
return .success(image)
|
||||
}
|
||||
|
||||
#warning("Media loading should check for existing in flight operations and de-dupe requests.")
|
||||
let loadImageBgTask = await backgroundTaskService?.startBackgroundTask(withName: "LoadImage: \(source.url.hashValue)")
|
||||
defer {
|
||||
loadImageBgTask?.stop()
|
||||
}
|
||||
|
||||
let cacheKey = cacheKeyForURL(source.url, avatarSize: avatarSize)
|
||||
let cacheKey = cacheKeyForURL(source.url, size: size)
|
||||
|
||||
if case let .success(cacheResult) = await imageCache.retrieveImage(forKey: cacheKey),
|
||||
let image = cacheResult.image {
|
||||
@@ -73,10 +74,10 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
|
||||
do {
|
||||
let imageData: Data
|
||||
if let avatarSize {
|
||||
imageData = try await mediaProxy.loadMediaThumbnailForSource(source, width: UInt(avatarSize.scaledValue), height: UInt(avatarSize.scaledValue))
|
||||
if let size {
|
||||
imageData = try await mediaLoader.loadMediaThumbnailForSource(source, width: UInt(size.width), height: UInt(size.height))
|
||||
} else {
|
||||
imageData = try await mediaProxy.loadMediaContentForSource(source)
|
||||
imageData = try await mediaLoader.loadMediaContentForSource(source)
|
||||
}
|
||||
|
||||
guard let image = UIImage(data: imageData) else {
|
||||
@@ -92,6 +93,8 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
return .failure(.failedRetrievingImage)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Files
|
||||
|
||||
func fileFromSource(_ source: MediaSourceProxy?, fileExtension: String) -> URL? {
|
||||
guard let source else {
|
||||
@@ -100,6 +103,18 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
let cacheKey = fileCacheKeyForURL(source.url)
|
||||
return fileCache.file(forKey: cacheKey, fileExtension: fileExtension)
|
||||
}
|
||||
|
||||
func fileFromURL(_ url: URL?, fileExtension: String) -> URL? {
|
||||
guard let url else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fileFromSource(MediaSourceProxy(url: url), fileExtension: fileExtension)
|
||||
}
|
||||
|
||||
func loadFileFromURL(_ url: URL, fileExtension: String) async -> Result<URL, MediaProviderError> {
|
||||
await loadFileFromSource(MediaSourceProxy(url: url), fileExtension: fileExtension)
|
||||
}
|
||||
|
||||
@discardableResult func loadFileFromSource(_ source: MediaSourceProxy, fileExtension: String) async -> Result<URL, MediaProviderError> {
|
||||
if let url = fileFromSource(source, fileExtension: fileExtension) {
|
||||
@@ -114,7 +129,7 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
let cacheKey = fileCacheKeyForURL(source.url)
|
||||
|
||||
do {
|
||||
let data = try await mediaProxy.loadMediaContentForSource(source)
|
||||
let data = try await mediaLoader.loadMediaContentForSource(source)
|
||||
|
||||
let url = try fileCache.store(data, with: fileExtension, forKey: cacheKey)
|
||||
return .success(url)
|
||||
@@ -124,23 +139,11 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func fileFromURL(_ url: URL?, fileExtension: String) -> URL? {
|
||||
guard let url else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fileFromSource(MediaSourceProxy(url: url), fileExtension: fileExtension)
|
||||
}
|
||||
|
||||
func loadFileFromURL(_ url: URL, fileExtension: String) async -> Result<URL, MediaProviderError> {
|
||||
await loadFileFromSource(MediaSourceProxy(url: url), fileExtension: fileExtension)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func cacheKeyForURL(_ url: URL, avatarSize: AvatarSize?) -> String {
|
||||
if let avatarSize {
|
||||
return "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}"
|
||||
private func cacheKeyForURL(_ url: URL, size: CGSize?) -> String {
|
||||
if let size {
|
||||
return "\(url.absoluteString){\(size.width),\(size.height)}"
|
||||
} else {
|
||||
return url.absoluteString
|
||||
}
|
||||
|
||||
@@ -23,15 +23,7 @@ enum MediaProviderError: Error {
|
||||
case invalidImageData
|
||||
}
|
||||
|
||||
protocol MediaProviderProtocol {
|
||||
func imageFromSource(_ source: MediaSourceProxy?, avatarSize: AvatarSize?) -> UIImage?
|
||||
|
||||
@discardableResult func loadImageFromSource(_ source: MediaSourceProxy, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError>
|
||||
|
||||
func imageFromURL(_ url: URL?, avatarSize: AvatarSize?) -> UIImage?
|
||||
|
||||
@discardableResult func loadImageFromURL(_ url: URL, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError>
|
||||
|
||||
protocol MediaProviderProtocol: ImageProviderProtocol {
|
||||
func fileFromSource(_ source: MediaSourceProxy?, fileExtension: String) -> URL?
|
||||
|
||||
@discardableResult func loadFileFromSource(_ source: MediaSourceProxy, fileExtension: String) async -> Result<URL, MediaProviderError>
|
||||
@@ -40,21 +32,3 @@ protocol MediaProviderProtocol {
|
||||
|
||||
@discardableResult func loadFileFromURL(_ url: URL, fileExtension: String) async -> Result<URL, MediaProviderError>
|
||||
}
|
||||
|
||||
extension MediaProviderProtocol {
|
||||
func imageFromSource(_ source: MediaSourceProxy?) -> UIImage? {
|
||||
imageFromSource(source, avatarSize: nil)
|
||||
}
|
||||
|
||||
@discardableResult func loadImageFromSource(_ source: MediaSourceProxy) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromSource(source, avatarSize: nil)
|
||||
}
|
||||
|
||||
func imageFromURL(_ url: URL?) -> UIImage? {
|
||||
imageFromURL(url, avatarSize: nil)
|
||||
}
|
||||
|
||||
@discardableResult func loadImageFromURL(_ url: URL) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromURL(url, avatarSize: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 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 Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import UIKit
|
||||
|
||||
class MediaProxy: MediaProxyProtocol {
|
||||
private let client: ClientProtocol
|
||||
private let clientQueue: DispatchQueue
|
||||
|
||||
init(client: ClientProtocol,
|
||||
clientQueue: DispatchQueue = .global()) {
|
||||
self.client = client
|
||||
self.clientQueue = clientQueue
|
||||
}
|
||||
|
||||
func mediaSourceForURL(_ url: URL) -> MediaSourceProxy {
|
||||
.init(url: url)
|
||||
}
|
||||
|
||||
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data {
|
||||
try await Task.dispatch(on: clientQueue) {
|
||||
let bytes = try self.client.getMediaContent(source: source.underlyingSource)
|
||||
return Data(bytes: bytes, count: bytes.count)
|
||||
}
|
||||
}
|
||||
|
||||
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data {
|
||||
try await Task.dispatch(on: clientQueue) {
|
||||
let bytes = try self.client.getMediaThumbnail(source: source.underlyingSource, width: UInt64(width), height: UInt64(height))
|
||||
return Data(bytes: bytes, count: bytes.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,31 +18,31 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
struct MockMediaProvider: MediaProviderProtocol {
|
||||
func imageFromSource(_ source: MediaSourceProxy?, avatarSize: AvatarSize?) -> UIImage? {
|
||||
func imageFromSource(_ source: MediaSourceProxy?, size: CGSize?) -> UIImage? {
|
||||
nil
|
||||
}
|
||||
|
||||
func loadImageFromSource(_ source: MediaSourceProxy, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
func loadImageFromSource(_ source: MediaSourceProxy, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
.failure(.failedRetrievingImage)
|
||||
}
|
||||
|
||||
func imageFromURL(_ url: URL?, avatarSize: AvatarSize?) -> UIImage? {
|
||||
func imageFromURL(_ url: URL?, size: CGSize?) -> UIImage? {
|
||||
guard url != nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let avatarSize {
|
||||
switch avatarSize {
|
||||
case .room:
|
||||
#warning("Fix me. this is stupid!")
|
||||
if let size {
|
||||
if size == AvatarSize.room(on: .details).scaledSize
|
||||
|| size == AvatarSize.room(on: .home).scaledSize
|
||||
|| size == AvatarSize.room(on: .timeline).scaledSize {
|
||||
return Asset.Images.appLogo.image
|
||||
default:
|
||||
return UIImage(systemName: "photo")
|
||||
}
|
||||
}
|
||||
return UIImage(systemName: "photo")
|
||||
}
|
||||
|
||||
func loadImageFromURL(_ url: URL, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
func loadImageFromURL(_ url: URL, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
guard let image = UIImage(systemName: "photo") else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
@@ -103,20 +103,8 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
}
|
||||
|
||||
if let item = timelineItem as? EventBasedTimelineItemProtocol {
|
||||
await loadUserAvatarForTimelineItem(item)
|
||||
await loadUserDisplayNameForTimelineItem(item)
|
||||
}
|
||||
|
||||
switch timelineItem {
|
||||
case let item as ImageRoomTimelineItem:
|
||||
await loadThumbnailForImageTimelineItem(item)
|
||||
case let item as VideoRoomTimelineItem:
|
||||
await loadThumbnailForVideoTimelineItem(item)
|
||||
case let item as StickerRoomTimelineItem:
|
||||
await loadImageForStickerTimelineItem(item)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func processItemDisappearance(_ itemID: String) { }
|
||||
@@ -339,78 +327,6 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
return .middle
|
||||
}
|
||||
|
||||
private func loadThumbnailForImageTimelineItem(_ timelineItem: ImageRoomTimelineItem) async {
|
||||
if timelineItem.image != nil {
|
||||
return
|
||||
}
|
||||
|
||||
guard let source = timelineItem.source else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadImageFromSource(source) {
|
||||
case .success(let image):
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
var item = timelineItems[index] as? ImageRoomTimelineItem else {
|
||||
return
|
||||
}
|
||||
|
||||
item.image = image
|
||||
timelineItems[index] = item
|
||||
callbacks.send(.updatedTimelineItem(timelineItem.id))
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func loadThumbnailForVideoTimelineItem(_ timelineItem: VideoRoomTimelineItem) async {
|
||||
if timelineItem.image != nil {
|
||||
return
|
||||
}
|
||||
|
||||
guard let source = timelineItem.thumbnailSource else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadImageFromSource(source) {
|
||||
case .success(let image):
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
var item = timelineItems[index] as? VideoRoomTimelineItem else {
|
||||
return
|
||||
}
|
||||
|
||||
item.image = image
|
||||
timelineItems[index] = item
|
||||
callbacks.send(.updatedTimelineItem(timelineItem.id))
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func loadImageForStickerTimelineItem(_ timelineItem: StickerRoomTimelineItem) async {
|
||||
if timelineItem.image != nil {
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = timelineItem.imageURL else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadImageFromURL(url) {
|
||||
case .success(let image):
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
var item = timelineItems[index] as? StickerRoomTimelineItem else {
|
||||
return
|
||||
}
|
||||
|
||||
item.image = image
|
||||
timelineItems[index] = item
|
||||
callbacks.send(.updatedTimelineItem(timelineItem.id))
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func loadVideoForTimelineItem(_ timelineItem: VideoRoomTimelineItem) async {
|
||||
if timelineItem.cachedVideoURL != nil {
|
||||
// already cached
|
||||
@@ -511,29 +427,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func loadUserAvatarForTimelineItem(_ timelineItem: EventBasedTimelineItemProtocol) async {
|
||||
guard timelineItem.shouldShowSenderDetails,
|
||||
let avatarURL = timelineItem.sender.avatarURL,
|
||||
timelineItem.sender.avatar == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadImageFromURL(avatarURL, avatarSize: .user(on: .timeline)) {
|
||||
case .success(let avatar):
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
var item = timelineItems[index] as? EventBasedTimelineItemProtocol else {
|
||||
return
|
||||
}
|
||||
|
||||
item.sender.avatar = avatar
|
||||
timelineItems[index] = item
|
||||
callbacks.send(.updatedTimelineItem(timelineItem.id))
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
#warning("This is here because sender profiles aren't working properly. Remove it entirely later")
|
||||
private func loadUserDisplayNameForTimelineItem(_ timelineItem: EventBasedTimelineItemProtocol) async {
|
||||
if timelineItem.shouldShowSenderDetails == false || timelineItem.sender.displayName != nil {
|
||||
|
||||
@@ -23,5 +23,4 @@ struct TimelineItemSender: Identifiable, Hashable {
|
||||
// Lazy loaded properties, displayName and avatarURL will be come lets.
|
||||
var displayName: String?
|
||||
var avatarURL: URL?
|
||||
var avatar: UIImage?
|
||||
}
|
||||
|
||||
-1
@@ -27,7 +27,6 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
|
||||
var sender: TimelineItemSender
|
||||
|
||||
let source: MediaSourceProxy?
|
||||
var image: UIImage?
|
||||
var cachedFileURL: URL?
|
||||
|
||||
var width: CGFloat?
|
||||
|
||||
-1
@@ -29,7 +29,6 @@ struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
|
||||
let duration: UInt64
|
||||
let source: MediaSourceProxy?
|
||||
let thumbnailSource: MediaSourceProxy?
|
||||
var image: UIImage?
|
||||
var cachedVideoURL: URL?
|
||||
|
||||
var width: CGFloat?
|
||||
|
||||
+1
-2
@@ -26,8 +26,7 @@ struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Ha
|
||||
|
||||
var sender: TimelineItemSender
|
||||
|
||||
let imageURL: URL?
|
||||
var image: UIImage?
|
||||
let imageURL: URL
|
||||
|
||||
var width: CGFloat?
|
||||
var height: CGFloat?
|
||||
|
||||
@@ -38,64 +38,61 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
|
||||
groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol? {
|
||||
var sender = eventItemProxy.sender
|
||||
if let senderAvatarURL = eventItemProxy.sender.avatarURL,
|
||||
let image = mediaProvider.imageFromURL(senderAvatarURL, avatarSize: .user(on: .timeline)) {
|
||||
sender.avatar = image
|
||||
}
|
||||
|
||||
let isOutgoing = eventItemProxy.isOwn
|
||||
|
||||
switch eventItemProxy.content.kind() {
|
||||
case .unableToDecrypt(let encryptedMessage):
|
||||
return buildEncryptedTimelineItem(eventItemProxy, encryptedMessage, sender, isOutgoing, groupState)
|
||||
return buildEncryptedTimelineItem(eventItemProxy, encryptedMessage, isOutgoing, groupState)
|
||||
case .redactedMessage:
|
||||
return buildRedactedTimelineItem(eventItemProxy, sender, isOutgoing, groupState)
|
||||
case .sticker(let body, let imageInfo, let url):
|
||||
return buildStickerTimelineItem(eventItemProxy, body, imageInfo, url, sender, isOutgoing, groupState)
|
||||
return buildRedactedTimelineItem(eventItemProxy, isOutgoing, groupState)
|
||||
case .sticker(let body, let imageInfo, let urlString):
|
||||
guard let url = URL(string: urlString) else {
|
||||
MXLog.error("Invalid sticker url string: \(urlString)")
|
||||
return buildUnsupportedTimelineItem(eventItemProxy, "m.sticker", "Invalid Sticker URL", isOutgoing, groupState)
|
||||
}
|
||||
|
||||
return buildStickerTimelineItem(eventItemProxy, body, imageInfo, url, isOutgoing, groupState)
|
||||
case .failedToParseMessageLike(let eventType, let error):
|
||||
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, sender, isOutgoing, groupState)
|
||||
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing, groupState)
|
||||
case .failedToParseState(let eventType, _, let error):
|
||||
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, sender, isOutgoing, groupState)
|
||||
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing, groupState)
|
||||
case .message:
|
||||
guard let messageContent = eventItemProxy.content.asMessage() else { fatalError("Invalid message timeline item: \(eventItemProxy)") }
|
||||
|
||||
switch messageContent.msgtype() {
|
||||
case .text(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return buildTextTimelineItemFromMessage(eventItemProxy, message, sender, isOutgoing, groupState)
|
||||
return buildTextTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
|
||||
case .image(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return buildImageTimelineItemFromMessage(eventItemProxy, message, sender, isOutgoing, groupState)
|
||||
return buildImageTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
|
||||
case .video(let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return buildVideoTimelineItemFromMessage(eventItemProxy, message, sender, isOutgoing, groupState)
|
||||
return buildVideoTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
|
||||
case .file(let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return buildFileTimelineItemFromMessage(eventItemProxy, message, sender, isOutgoing, groupState)
|
||||
return buildFileTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
|
||||
case .notice(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return buildNoticeTimelineItemFromMessage(eventItemProxy, message, sender, isOutgoing, groupState)
|
||||
return buildNoticeTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
|
||||
case .emote(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return buildEmoteTimelineItemFromMessage(eventItemProxy, message, sender, isOutgoing, groupState)
|
||||
return buildEmoteTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
|
||||
case .none:
|
||||
return buildFallbackTimelineItem(eventItemProxy, sender, isOutgoing, groupState)
|
||||
return buildFallbackTimelineItem(eventItemProxy, isOutgoing, groupState)
|
||||
}
|
||||
case .state(let stateKey, let content):
|
||||
return buildStateTimelineItemFor(eventItemProxy: eventItemProxy, state: content, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing)
|
||||
return buildStateTimelineItemFor(eventItemProxy: eventItemProxy, state: content, stateKey: stateKey, isOutgoing: isOutgoing)
|
||||
case .roomMembership(userId: let userID, change: let change):
|
||||
return buildStateMembershipChangeTimelineItemFor(eventItemProxy: eventItemProxy, member: userID, membershipChange: change, sender: sender, isOutgoing: isOutgoing)
|
||||
return buildStateMembershipChangeTimelineItemFor(eventItemProxy: eventItemProxy, member: userID, membershipChange: change, isOutgoing: isOutgoing)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Message Events
|
||||
|
||||
// swiftlint:disable:next function_parameter_count
|
||||
private func buildUnsupportedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ eventType: String,
|
||||
_ error: String,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
UnsupportedRoomTimelineItem(id: eventItemProxy.id,
|
||||
@@ -106,7 +103,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
properties: RoomTimelineItemProperties())
|
||||
}
|
||||
|
||||
@@ -114,8 +111,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
private func buildStickerTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ body: String,
|
||||
_ imageInfo: ImageInfo,
|
||||
_ imageURLString: String,
|
||||
_ sender: TimelineItemSender,
|
||||
_ imageURL: URL,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
var aspectRatio: CGFloat?
|
||||
@@ -125,20 +121,14 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
aspectRatio = width / height
|
||||
}
|
||||
|
||||
var image: UIImage?
|
||||
if let url = URL(string: imageURLString) {
|
||||
image = mediaProvider.imageFromURL(url)
|
||||
}
|
||||
|
||||
return StickerRoomTimelineItem(id: eventItemProxy.id,
|
||||
text: body,
|
||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
imageURL: URL(string: imageURLString),
|
||||
image: image,
|
||||
sender: eventItemProxy.sender,
|
||||
imageURL: imageURL,
|
||||
width: width,
|
||||
height: height,
|
||||
aspectRatio: aspectRatio,
|
||||
@@ -149,7 +139,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ encryptedMessage: EncryptedMessage,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
var encryptionType = EncryptedRoomTimelineItem.EncryptionType.unknown
|
||||
@@ -169,12 +158,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
properties: RoomTimelineItemProperties())
|
||||
}
|
||||
|
||||
private func buildRedactedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
RedactedRoomTimelineItem(id: eventItemProxy.id,
|
||||
@@ -183,12 +171,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
properties: RoomTimelineItemProperties())
|
||||
}
|
||||
|
||||
private func buildFallbackTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
let attributedText = attributedStringBuilder.fromPlain(eventItemProxy.body)
|
||||
@@ -201,14 +188,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
properties: RoomTimelineItemProperties(isEdited: eventItemProxy.content.asMessage()?.isEdited() ?? false,
|
||||
reactions: aggregateReactions(eventItemProxy.reactions)))
|
||||
}
|
||||
|
||||
private func buildTextTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ message: MessageTimelineItem<TextMessageContent>,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
@@ -221,7 +207,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus))
|
||||
@@ -229,7 +215,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
private func buildImageTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ message: MessageTimelineItem<ImageMessageContent>,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
var aspectRatio: CGFloat?
|
||||
@@ -244,9 +229,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
source: message.source,
|
||||
image: mediaProvider.imageFromSource(message.source),
|
||||
width: message.width,
|
||||
height: message.height,
|
||||
aspectRatio: aspectRatio,
|
||||
@@ -258,7 +242,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
private func buildVideoTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ message: MessageTimelineItem<VideoMessageContent>,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
var aspectRatio: CGFloat?
|
||||
@@ -273,11 +256,10 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
duration: message.duration,
|
||||
source: message.source,
|
||||
thumbnailSource: message.thumbnailSource,
|
||||
image: mediaProvider.imageFromSource(message.thumbnailSource),
|
||||
width: message.width,
|
||||
height: message.height,
|
||||
aspectRatio: aspectRatio,
|
||||
@@ -289,7 +271,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
private func buildFileTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ message: MessageTimelineItem<FileMessageContent>,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
FileRoomTimelineItem(id: message.id,
|
||||
@@ -298,7 +279,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
source: message.source,
|
||||
thumbnailSource: message.thumbnailSource,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
@@ -308,7 +289,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
private func buildNoticeTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ message: MessageTimelineItem<NoticeMessageContent>,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
@@ -321,7 +301,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus))
|
||||
@@ -329,10 +309,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
private func buildEmoteTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ message: MessageTimelineItem<EmoteMessageContent>,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
let name = sender.displayName ?? sender.id
|
||||
let name = eventItemProxy.sender.displayName ?? eventItemProxy.sender.id
|
||||
|
||||
var attributedText: AttributedString?
|
||||
if let htmlBody = message.htmlBody {
|
||||
@@ -350,7 +329,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus))
|
||||
@@ -368,28 +347,26 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
private func buildStateTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
|
||||
state: OtherState,
|
||||
stateKey: String,
|
||||
sender: TimelineItemSender,
|
||||
isOutgoing: Bool) -> RoomTimelineItemProtocol? {
|
||||
guard let text = stateEventStringBuilder.buildString(for: state, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing) else { return nil }
|
||||
return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, sender: sender, isOutgoing: isOutgoing)
|
||||
guard let text = stateEventStringBuilder.buildString(for: state, stateKey: stateKey, sender: eventItemProxy.sender, isOutgoing: isOutgoing) else { return nil }
|
||||
return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, isOutgoing: isOutgoing)
|
||||
}
|
||||
|
||||
private func buildStateMembershipChangeTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
|
||||
member: String,
|
||||
membershipChange: MembershipChange,
|
||||
sender: TimelineItemSender,
|
||||
isOutgoing: Bool) -> RoomTimelineItemProtocol? {
|
||||
guard let text = stateEventStringBuilder.buildString(for: membershipChange, member: member, sender: eventItemProxy.sender, isOutgoing: isOutgoing) else { return nil }
|
||||
return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, sender: sender, isOutgoing: isOutgoing)
|
||||
return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, isOutgoing: isOutgoing)
|
||||
}
|
||||
|
||||
private func buildStateTimelineItem(eventItemProxy: EventTimelineItemProxy, text: String, sender: TimelineItemSender, isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||
private func buildStateTimelineItem(eventItemProxy: EventTimelineItemProxy, text: String, isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||
StateRoomTimelineItem(id: eventItemProxy.id,
|
||||
text: text,
|
||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||
groupState: .single,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: false,
|
||||
sender: sender)
|
||||
sender: eventItemProxy.sender)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,12 +190,12 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
|
||||
let userNotificationController = UserNotificationController(rootCoordinator: settingsNavigationStackCoordinator)
|
||||
|
||||
let parameters = SettingsCoordinatorParameters(navigationStackCoordinator: settingsNavigationStackCoordinator,
|
||||
userNotificationController: userNotificationController,
|
||||
userSession: userSession,
|
||||
bugReportService: bugReportService)
|
||||
let settingsCoordinator = SettingsCoordinator(parameters: parameters)
|
||||
settingsCoordinator.callback = { [weak self] action in
|
||||
let parameters = SettingsScreenCoordinatorParameters(navigationStackCoordinator: settingsNavigationStackCoordinator,
|
||||
userNotificationController: userNotificationController,
|
||||
userSession: userSession,
|
||||
bugReportService: bugReportService)
|
||||
let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: parameters)
|
||||
settingsScreenCoordinator.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .dismiss:
|
||||
@@ -206,7 +206,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
settingsNavigationStackCoordinator.setRootCoordinator(settingsCoordinator)
|
||||
settingsNavigationStackCoordinator.setRootCoordinator(settingsScreenCoordinator)
|
||||
|
||||
navigationSplitCoordinator.setSheetCoordinator(userNotificationController) { [weak self] in
|
||||
self?.stateMachine.processEvent(.dismissedSettingsScreen)
|
||||
|
||||
@@ -96,7 +96,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
imageCache.memoryStorage.config.keepWhenEnteringBackground = true
|
||||
|
||||
return UserSession(clientProxy: clientProxy,
|
||||
mediaProvider: MediaProvider(mediaProxy: clientProxy,
|
||||
mediaProvider: MediaProvider(mediaLoader: clientProxy,
|
||||
imageCache: imageCache,
|
||||
fileCache: FileCache.default,
|
||||
backgroundTaskService: backgroundTaskService))
|
||||
|
||||
Reference in New Issue
Block a user