mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
Introduced MemberDetailsProvider, caching user avatar urls and images. Various other tweaks and improvements.
This commit is contained in:
@@ -13,6 +13,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
private let timelineProvider: RoomTimelineProvider
|
||||
private let timelineItemFactory: RoomTimelineItemFactory
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let memberDetailsProvider: MemberDetailsProvider
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
@@ -22,10 +23,12 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
|
||||
init(timelineProvider: RoomTimelineProvider,
|
||||
timelineItemFactory: RoomTimelineItemFactory,
|
||||
mediaProvider: MediaProviderProtocol) {
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
memberDetailsProvider: MemberDetailsProvider) {
|
||||
self.timelineProvider = timelineProvider
|
||||
self.timelineItemFactory = timelineItemFactory
|
||||
self.mediaProvider = mediaProvider
|
||||
self.memberDetailsProvider = memberDetailsProvider
|
||||
|
||||
self.timelineProvider.callbacks.sink { [weak self] callback in
|
||||
guard let self = self else { return }
|
||||
@@ -54,33 +57,13 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
loadAvatarIfNeededForTimelineItem(timelineItem)
|
||||
|
||||
switch timelineItem {
|
||||
case var item as ImageRoomTimelineItem:
|
||||
if item.image != nil {
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = item.url else {
|
||||
return
|
||||
}
|
||||
|
||||
mediaProvider.loadImageFromURL(url) { [weak self] result in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
if let item = timelineItem as? EventBasedTimelineItemProtocol {
|
||||
loadAvatarForTimelineItem(item)
|
||||
}
|
||||
|
||||
if case let .success(image) = result {
|
||||
guard let index = self.timelineItems.firstIndex(where: { $0.id == itemId }) else {
|
||||
return
|
||||
}
|
||||
|
||||
item.image = image
|
||||
self.timelineItems[index] = item
|
||||
self.callbacks.send(.updatedTimelineItem(itemId))
|
||||
}
|
||||
}
|
||||
switch timelineItem {
|
||||
case let item as ImageRoomTimelineItem:
|
||||
loadImageForTimelineItem(item)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -126,42 +109,56 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
return Calendar.current.isDate(lhs.originServerTs, inSameDayAs: rhs.originServerTs)
|
||||
}
|
||||
|
||||
private func loadAvatarIfNeededForTimelineItem(_ timelineItem: RoomTimelineItemProtocol) {
|
||||
switch timelineItem {
|
||||
case var item as BaseRoomTimelineItemProtocol:
|
||||
if item.shouldShowSenderDetails == false {
|
||||
break
|
||||
private func loadImageForTimelineItem(_ timelineItem: ImageRoomTimelineItem) {
|
||||
var item = timelineItem
|
||||
|
||||
if item.image != nil {
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = item.url else {
|
||||
return
|
||||
}
|
||||
|
||||
mediaProvider.loadImageFromURL(url) { [weak self] result in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
timelineProvider.avatarURLForUserId(item.sender) { [weak self] result in
|
||||
guard let self = self else {
|
||||
if case let .success(image) = result {
|
||||
guard let index = self.timelineItems.firstIndex(where: { $0.id == timelineItem.id }) else {
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let userAvatarURL):
|
||||
guard let avatarURL = userAvatarURL else {
|
||||
return
|
||||
}
|
||||
|
||||
self.mediaProvider.loadImageFromURL(avatarURL) { result in
|
||||
if case let .success(image) = result {
|
||||
guard let index = self.timelineItems.firstIndex(where: { $0.id == timelineItem.id }) else {
|
||||
return
|
||||
}
|
||||
|
||||
item.senderAvatar = image
|
||||
self.timelineItems[index] = item
|
||||
self.callbacks.send(.updatedTimelineItem(timelineItem.id))
|
||||
item.image = image
|
||||
self.timelineItems[index] = item
|
||||
self.callbacks.send(.updatedTimelineItem(timelineItem.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadAvatarForTimelineItem(_ timelineItem: EventBasedTimelineItemProtocol) {
|
||||
var item = timelineItem
|
||||
|
||||
if item.shouldShowSenderDetails == false {
|
||||
return
|
||||
}
|
||||
|
||||
memberDetailsProvider.avatarURLForUserId(timelineItem.sender) { result in
|
||||
if case let .success(avatarURL) = result,
|
||||
let avatarURL = avatarURL {
|
||||
self.mediaProvider.loadImageFromURL(avatarURL) { result in
|
||||
if case let .success(image) = result {
|
||||
guard let index = self.timelineItems.firstIndex(where: { $0.id == timelineItem.id }) else {
|
||||
return
|
||||
}
|
||||
|
||||
item.senderAvatar = image
|
||||
self.timelineItems[index] = item
|
||||
self.callbacks.send(.updatedTimelineItem(timelineItem.id))
|
||||
}
|
||||
case .failure:
|
||||
MXLog.error("Failed retrieving user avatar")
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// DecorationTimelineItemProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 18/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol DecorationTimelineItemProtocol: RoomTimelineItemProtocol {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// EventBasedTimelineItemProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 18/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol {
|
||||
var text: String { get }
|
||||
var timestamp: String { get }
|
||||
var shouldShowSenderDetails: Bool { get }
|
||||
|
||||
var sender: String { get }
|
||||
var senderAvatar: UIImage? { get set }
|
||||
}
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct ImageRoomTimelineItem: BaseRoomTimelineItemProtocol, Identifiable, Equatable {
|
||||
struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equatable {
|
||||
let id: String
|
||||
let text: String
|
||||
let timestamp: String
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct SeparatorRoomTimelineItem: RoomTimelineItemProtocol, Identifiable, Equatable {
|
||||
struct SeparatorRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Equatable {
|
||||
let id: String
|
||||
let text: String
|
||||
}
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct TextRoomTimelineItem: BaseRoomTimelineItemProtocol, Identifiable, Equatable {
|
||||
struct TextRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equatable {
|
||||
let id: String
|
||||
let text: String
|
||||
let timestamp: String
|
||||
@@ -11,39 +11,36 @@ import UIKit
|
||||
|
||||
struct RoomTimelineItemFactory {
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let memberDetailsProvider: MemberDetailsProvider
|
||||
|
||||
init(mediaProvider: MediaProviderProtocol) {
|
||||
init(mediaProvider: MediaProviderProtocol,
|
||||
memberDetailsProvider: MemberDetailsProvider) {
|
||||
self.mediaProvider = mediaProvider
|
||||
self.memberDetailsProvider = memberDetailsProvider
|
||||
}
|
||||
|
||||
func buildTimelineItemFor(_ roomMessage: RoomMessageProtocol, showSenderDetails: Bool) -> RoomTimelineItemProtocol {
|
||||
|
||||
let avatarURL = memberDetailsProvider.avatarURLForUserId(roomMessage.sender)
|
||||
let avatarImage = mediaProvider.imageForURL(avatarURL)
|
||||
|
||||
switch roomMessage {
|
||||
case let message as TextRoomMessage:
|
||||
return TextRoomTimelineItem(id: message.id,
|
||||
text: message.content,
|
||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||
shouldShowSenderDetails: showSenderDetails,
|
||||
sender: message.sender)
|
||||
sender: message.sender,
|
||||
senderAvatar: avatarImage)
|
||||
case let message as ImageRoomMessage:
|
||||
var image: UIImage?
|
||||
|
||||
if let url = message.url {
|
||||
if mediaProvider.hasImageCachedForURL(url) {
|
||||
mediaProvider.loadImageFromURL(url, { result in
|
||||
if case let .success(cachedImage) = result {
|
||||
image = cachedImage
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return ImageRoomTimelineItem(id: message.id,
|
||||
text: message.content,
|
||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||
shouldShowSenderDetails: showSenderDetails,
|
||||
sender: message.sender,
|
||||
senderAvatar: avatarImage,
|
||||
url: message.url,
|
||||
image: image)
|
||||
image: mediaProvider.imageForURL(message.url))
|
||||
default:
|
||||
fatalError("Unknown room message.")
|
||||
}
|
||||
|
||||
@@ -12,12 +12,3 @@ import UIKit
|
||||
protocol RoomTimelineItemProtocol {
|
||||
var id: String { get }
|
||||
}
|
||||
|
||||
protocol BaseRoomTimelineItemProtocol: RoomTimelineItemProtocol {
|
||||
var text: String { get }
|
||||
var timestamp: String { get }
|
||||
var shouldShowSenderDetails: Bool { get }
|
||||
|
||||
var sender: String { get }
|
||||
var senderAvatar: UIImage? { get set }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// EventBasedTimelineView.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 18/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct EventBasedTimelineView: View {
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
|
||||
var body: some View {
|
||||
if timelineItem.shouldShowSenderDetails {
|
||||
HStack {
|
||||
avatar
|
||||
Text(timelineItem.sender)
|
||||
.font(.footnote)
|
||||
.bold()
|
||||
Spacer()
|
||||
Text(timelineItem.timestamp)
|
||||
.font(.footnote)
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private var avatar: some View {
|
||||
ZStack(alignment: .center) {
|
||||
if let avatar = timelineItem.senderAvatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.overlay(Circle().stroke(Color(.sRGB, red: 0.05, green: 0.74, blue: 0.55, opacity: 1.0)))
|
||||
} else {
|
||||
PlaceholderAvatarImage(firstCharacter: String(timelineItem.sender.prefix(2).suffix(1)).uppercased())
|
||||
}
|
||||
}
|
||||
.clipShape(Circle())
|
||||
.frame(width: 44.0, height: 44.0)
|
||||
}
|
||||
}
|
||||
+3
-5
@@ -14,11 +14,9 @@ struct ImageRoomTimelineView: View {
|
||||
|
||||
var body: some View {
|
||||
if let image = timelineItem.image {
|
||||
VStack {
|
||||
HStack {
|
||||
Text(timelineItem.text)
|
||||
Spacer()
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
EventBasedTimelineView(timelineItem: timelineItem)
|
||||
Text(timelineItem.text)
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
+3
-30
@@ -14,40 +14,13 @@ struct TextRoomTimelineView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if timelineItem.shouldShowSenderDetails {
|
||||
HStack {
|
||||
avatar
|
||||
Text(timelineItem.sender)
|
||||
.font(.footnote)
|
||||
.bold()
|
||||
Spacer()
|
||||
Text(timelineItem.timestamp)
|
||||
.font(.footnote)
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
EventBasedTimelineView(timelineItem: timelineItem)
|
||||
Text(timelineItem.text)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.id(timelineItem.id)
|
||||
}
|
||||
|
||||
@ViewBuilder var avatar: some View {
|
||||
ZStack(alignment: .center) {
|
||||
Circle()
|
||||
.fill(Color(.sRGB, red: 0.05, green: 0.74, blue: 0.55, opacity: 1.0))
|
||||
if let avatar = timelineItem.senderAvatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
Text(timelineItem.sender.prefix(2).suffix(1))
|
||||
.foregroundColor(.white)
|
||||
.font(.title)
|
||||
.bold()
|
||||
}
|
||||
}
|
||||
.frame(width: 44.0, height: 44.0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct TextRoomTimelineView_Previews: PreviewProvider {
|
||||
Reference in New Issue
Block a user