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:
Stefan Ceriu
2023-01-25 19:45:01 +02:00
committed by GitHub
parent 3a74f13097
commit bd4c0b72cb
58 changed files with 875 additions and 830 deletions
@@ -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?
}
@@ -27,7 +27,6 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
var sender: TimelineItemSender
let source: MediaSourceProxy?
var image: UIImage?
var cachedFileURL: URL?
var width: CGFloat?
@@ -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?
@@ -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)
}
}