mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Loading images and user avatars.
This commit is contained in:
parent
30378ae6a9
commit
120bcaf012
@ -27,9 +27,12 @@
|
||||
18C5745227E1D88600D70937 /* ImageRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18C5745127E1D88600D70937 /* ImageRoomMessage.swift */; };
|
||||
18C5745427E1D88E00D70937 /* TextRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18C5745327E1D88E00D70937 /* TextRoomMessage.swift */; };
|
||||
18C5745627E1DCA800D70937 /* RoomMessageFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18C5745527E1DCA800D70937 /* RoomMessageFactory.swift */; };
|
||||
18C5745827E1EB6E00D70937 /* TimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18C5745727E1EB6E00D70937 /* TimelineItemFactory.swift */; };
|
||||
18DF7C2A27E23E3A00291672 /* TimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18DF7C2927E23E3A00291672 /* TimelineItemProtocol.swift */; };
|
||||
18DF7C2C27E23EC000291672 /* TimelineViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18DF7C2B27E23EC000291672 /* TimelineViewFactory.swift */; };
|
||||
18C5745827E1EB6E00D70937 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18C5745727E1EB6E00D70937 /* RoomTimelineItemFactory.swift */; };
|
||||
18DF7C2A27E23E3A00291672 /* RoomTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18DF7C2927E23E3A00291672 /* RoomTimelineItemProtocol.swift */; };
|
||||
18DF7C2C27E23EC000291672 /* RoomTimelineViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18DF7C2B27E23EC000291672 /* RoomTimelineViewFactory.swift */; };
|
||||
18DF7C2F27E264FC00291672 /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18DF7C2E27E264FC00291672 /* MediaProvider.swift */; };
|
||||
18DF7C3127E3608100291672 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18DF7C3027E3608100291672 /* MediaProviderProtocol.swift */; };
|
||||
18DF7C3327E3608800291672 /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18DF7C3227E3608800291672 /* MockMediaProvider.swift */; };
|
||||
18F2BADA27D25B4000DD1988 /* RoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BA7727D25B4000DD1988 /* RoomTimelineProvider.swift */; };
|
||||
18F2BADB27D25B4000DD1988 /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BA7927D25B4000DD1988 /* AuthenticationCoordinator.swift */; };
|
||||
18F2BADC27D25B4000DD1988 /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BA7A27D25B4000DD1988 /* UserSession.swift */; };
|
||||
@ -133,9 +136,12 @@
|
||||
18C5745127E1D88600D70937 /* ImageRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomMessage.swift; sourceTree = "<group>"; };
|
||||
18C5745327E1D88E00D70937 /* TextRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomMessage.swift; sourceTree = "<group>"; };
|
||||
18C5745527E1DCA800D70937 /* RoomMessageFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageFactory.swift; sourceTree = "<group>"; };
|
||||
18C5745727E1EB6E00D70937 /* TimelineItemFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemFactory.swift; sourceTree = "<group>"; };
|
||||
18DF7C2927E23E3A00291672 /* TimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemProtocol.swift; sourceTree = "<group>"; };
|
||||
18DF7C2B27E23EC000291672 /* TimelineViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewFactory.swift; sourceTree = "<group>"; };
|
||||
18C5745727E1EB6E00D70937 /* RoomTimelineItemFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFactory.swift; sourceTree = "<group>"; };
|
||||
18DF7C2927E23E3A00291672 /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = "<group>"; };
|
||||
18DF7C2B27E23EC000291672 /* RoomTimelineViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactory.swift; sourceTree = "<group>"; };
|
||||
18DF7C2E27E264FC00291672 /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = "<group>"; };
|
||||
18DF7C3027E3608100291672 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = "<group>"; };
|
||||
18DF7C3227E3608800291672 /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = "<group>"; };
|
||||
18F2BA7727D25B4000DD1988 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = "<group>"; };
|
||||
18F2BA7927D25B4000DD1988 /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = "<group>"; };
|
||||
18F2BA7A27D25B4000DD1988 /* UserSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = "<group>"; };
|
||||
@ -306,9 +312,9 @@
|
||||
18A318D927DA42C9000867CD /* TimelineItems */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
18C5745727E1EB6E00D70937 /* TimelineItemFactory.swift */,
|
||||
18DF7C2927E23E3A00291672 /* TimelineItemProtocol.swift */,
|
||||
18DF7C2B27E23EC000291672 /* TimelineViewFactory.swift */,
|
||||
18DF7C2927E23E3A00291672 /* RoomTimelineItemProtocol.swift */,
|
||||
18C5745727E1EB6E00D70937 /* RoomTimelineItemFactory.swift */,
|
||||
18DF7C2B27E23EC000291672 /* RoomTimelineViewFactory.swift */,
|
||||
18A318DB27DA42C9000867CD /* RoomTimelineViewProvider.swift */,
|
||||
18F9889727DB7473002F48B4 /* ImageRoomTimelineItem.swift */,
|
||||
18F9889D27DB752B002F48B4 /* ImageRoomTimelineView.swift */,
|
||||
@ -349,12 +355,23 @@
|
||||
path = Messages;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
18DF7C2D27E264EE00291672 /* Media */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
18DF7C3027E3608100291672 /* MediaProviderProtocol.swift */,
|
||||
18DF7C2E27E264FC00291672 /* MediaProvider.swift */,
|
||||
18DF7C3227E3608800291672 /* MockMediaProvider.swift */,
|
||||
);
|
||||
path = Media;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
18F2BA7227D25B4000DD1988 /* Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
18F2BA7827D25B4000DD1988 /* Authentication */,
|
||||
18C5744727E1D84000D70937 /* Room */,
|
||||
18F2BA7627D25B4000DD1988 /* Timeline */,
|
||||
18DF7C2D27E264EE00291672 /* Media */,
|
||||
);
|
||||
path = Services;
|
||||
sourceTree = "<group>";
|
||||
@ -804,7 +821,7 @@
|
||||
18F2BB1527D25B4000DD1988 /* LoginScreenViewModelProtocol.swift in Sources */,
|
||||
18F2BAEB27D25B4000DD1988 /* LabelledActivityIndicatorView.swift in Sources */,
|
||||
18F2BAE427D25B4000DD1988 /* Presentable.swift in Sources */,
|
||||
18DF7C2A27E23E3A00291672 /* TimelineItemProtocol.swift in Sources */,
|
||||
18DF7C2A27E23E3A00291672 /* RoomTimelineItemProtocol.swift in Sources */,
|
||||
18F2BAF927D25B4000DD1988 /* SplashViewController.swift in Sources */,
|
||||
18F2BAE327D25B4000DD1988 /* RootRouter.swift in Sources */,
|
||||
18F2BAE527D25B4000DD1988 /* NavigationModule.swift in Sources */,
|
||||
@ -834,14 +851,17 @@
|
||||
18F2BAF527D25B4000DD1988 /* WeakKeyDictionary.swift in Sources */,
|
||||
18F2BADF27D25B4000DD1988 /* NavigationRouterStore.swift in Sources */,
|
||||
18F2BAFE27D25B4000DD1988 /* HomeScreenViewModelProtocol.swift in Sources */,
|
||||
18DF7C3327E3608800291672 /* MockMediaProvider.swift in Sources */,
|
||||
18F2BAE827D25B4000DD1988 /* RectangleToastView.swift in Sources */,
|
||||
18F2BB1627D25B4000DD1988 /* LoginScreenModels.swift in Sources */,
|
||||
18F9889E27DB752B002F48B4 /* ImageRoomTimelineView.swift in Sources */,
|
||||
18F2BADA27D25B4000DD1988 /* RoomTimelineProvider.swift in Sources */,
|
||||
18DF7C2F27E264FC00291672 /* MediaProvider.swift in Sources */,
|
||||
18C5744D27E1D84000D70937 /* RoomProxy.swift in Sources */,
|
||||
18F9889827DB7473002F48B4 /* ImageRoomTimelineItem.swift in Sources */,
|
||||
18F2BB0027D25B4000DD1988 /* HomeScreen.swift in Sources */,
|
||||
18F2BB2827D2647A00DD1988 /* MockRoomTimelineController.swift in Sources */,
|
||||
18DF7C3127E3608100291672 /* MediaProviderProtocol.swift in Sources */,
|
||||
18F2BB0127D25B4000DD1988 /* HomeScreenViewModel.swift in Sources */,
|
||||
18F2BAF027D25B4000DD1988 /* ActivityDismissal.swift in Sources */,
|
||||
18F2BADD27D25B4000DD1988 /* KeychainController.swift in Sources */,
|
||||
@ -860,8 +880,8 @@
|
||||
18C5744E27E1D84000D70937 /* MockRoomProxy.swift in Sources */,
|
||||
18F2BADC27D25B4000DD1988 /* UserSession.swift in Sources */,
|
||||
18F2BAEF27D25B4000DD1988 /* ActivityRequest.swift in Sources */,
|
||||
18DF7C2C27E23EC000291672 /* TimelineViewFactory.swift in Sources */,
|
||||
18C5745827E1EB6E00D70937 /* TimelineItemFactory.swift in Sources */,
|
||||
18DF7C2C27E23EC000291672 /* RoomTimelineViewFactory.swift in Sources */,
|
||||
18C5745827E1EB6E00D70937 /* RoomTimelineItemFactory.swift in Sources */,
|
||||
18F2BAEE27D25B4000DD1988 /* Activity.swift in Sources */,
|
||||
18F2BAEC27D25B4000DD1988 /* ToastActivityPresenter.swift in Sources */,
|
||||
);
|
||||
|
@ -6,7 +6,6 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Kingfisher
|
||||
|
||||
class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
private let window: UIWindow
|
||||
@ -81,8 +80,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
fatalError("User session should be already setup at this point")
|
||||
}
|
||||
|
||||
let parameters = HomeScreenCoordinatorParameters(userSession: userSession)
|
||||
let coordinator = HomeScreenCoordinator(parameters: parameters, imageCache: ImageCache.default)
|
||||
let parameters = HomeScreenCoordinatorParameters(userSession: userSession, mediaProvider: userSession.mediaProvider)
|
||||
let coordinator = HomeScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.completion = { [weak self] result in
|
||||
switch result {
|
||||
@ -114,7 +113,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
return
|
||||
}
|
||||
|
||||
let parameters = RoomScreenCoordinatorParameters(roomProxy: roomProxy)
|
||||
let parameters = RoomScreenCoordinatorParameters(roomProxy: roomProxy, mediaProvider: userSession.mediaProvider)
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
self.add(childCoordinator: coordinator)
|
||||
|
@ -16,10 +16,10 @@
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import Kingfisher
|
||||
|
||||
struct HomeScreenCoordinatorParameters {
|
||||
let userSession: UserSession
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
}
|
||||
|
||||
enum HomeScreenCoordinatorResult {
|
||||
@ -47,11 +47,11 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: HomeScreenCoordinatorParameters, imageCache: Kingfisher.ImageCache) {
|
||||
init(parameters: HomeScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let userDisplayName = self.parameters.userSession.userDisplayName ?? self.parameters.userSession.userIdentifier
|
||||
viewModel = HomeScreenViewModel(userDisplayName: userDisplayName, imageCache: imageCache)
|
||||
viewModel = HomeScreenViewModel(userDisplayName: userDisplayName, mediaProvider: self.parameters.mediaProvider)
|
||||
|
||||
let view = HomeScreen(context: viewModel.context)
|
||||
hostingController = UIHostingController(rootView: view)
|
||||
@ -63,7 +63,7 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
|
||||
case .logout:
|
||||
self.completion?(.logout)
|
||||
case .loadUserAvatar:
|
||||
self.parameters.userSession.loadUserAvatar({ result in
|
||||
self.parameters.mediaProvider.loadCurrentUserAvatar({ result in
|
||||
switch result {
|
||||
case .success(let avatar):
|
||||
self.viewModel.updateWithUserAvatar(avatar)
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import Kingfisher
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias HomeScreenViewModelType = StateStoreViewModel<HomeScreenViewState,
|
||||
@ -25,21 +24,21 @@ typealias HomeScreenViewModelType = StateStoreViewModel<HomeScreenViewState,
|
||||
@available(iOS 14, *)
|
||||
class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol {
|
||||
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
|
||||
private var roomUpdateListeners = Set<AnyCancellable>()
|
||||
private var roomList: [RoomProxyProtocol]? {
|
||||
didSet {
|
||||
self.state.isLoadingRooms = (roomList?.count ?? 0 == 0)
|
||||
}
|
||||
}
|
||||
|
||||
private let imageCache: ImageCache
|
||||
|
||||
var completion: ((HomeScreenViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(userDisplayName: String, imageCache: Kingfisher.ImageCache) {
|
||||
self.imageCache = imageCache
|
||||
init(userDisplayName: String, mediaProvider: MediaProviderProtocol) {
|
||||
self.mediaProvider = mediaProvider
|
||||
super.init(initialViewState: HomeScreenViewState(userDisplayName: userDisplayName, isLoadingRooms: true))
|
||||
}
|
||||
|
||||
@ -94,37 +93,14 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
|
||||
private func loadAvatarForRoomWithIdentifier(_ roomIdentifier: String) {
|
||||
guard let room = roomList?.filter({ $0.id == roomIdentifier }).first,
|
||||
let cacheKey = room.avatarURL?.path else {
|
||||
let avatarURLString = room.avatarURL else {
|
||||
return
|
||||
}
|
||||
|
||||
if imageCache.isCached(forKey: cacheKey) {
|
||||
imageCache.retrieveImage(forKey: cacheKey) { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case .success(let value):
|
||||
self.updateAvatar(value.image, forRoomWithIdentifier: roomIdentifier)
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed retrieving avatar from cache with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
room.loadAvatar { [weak self] result in
|
||||
mediaProvider.loadImageFromURL(avatarURLString) { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
case .success(let avatar):
|
||||
guard let avatar = avatar else {
|
||||
return
|
||||
}
|
||||
|
||||
self.imageCache.store(avatar, forKey: cacheKey)
|
||||
self.updateAvatar(avatar, forRoomWithIdentifier: roomIdentifier)
|
||||
default:
|
||||
break
|
||||
if case let .success(image) = result {
|
||||
self.updateAvatar(image, forRoomWithIdentifier: roomIdentifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,6 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct HomeScreen: View {
|
||||
|
||||
@ -154,7 +153,8 @@ struct RoomCell: View {
|
||||
|
||||
struct HomeScreen_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = HomeScreenViewModel(userDisplayName: "Johnny Appleseed", imageCache: ImageCache.default)
|
||||
let viewModel = HomeScreenViewModel(userDisplayName: "Johnny Appleseed",
|
||||
mediaProvider: MockMediaProvider())
|
||||
|
||||
let rooms = [MockRoomProxy(displayName: "Alfa"),
|
||||
MockRoomProxy(displayName: "Beta"),
|
||||
|
@ -18,6 +18,7 @@ import SwiftUI
|
||||
|
||||
struct RoomScreenCoordinatorParameters {
|
||||
let roomProxy: RoomProxyProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
}
|
||||
|
||||
final class RoomScreenCoordinator: Coordinator, Presentable {
|
||||
@ -43,10 +44,12 @@ final class RoomScreenCoordinator: Coordinator, Presentable {
|
||||
|
||||
let timelineProvider = RoomTimelineProvider(roomProxy: parameters.roomProxy)
|
||||
let timelineController = RoomTimelineController(timelineProvider: timelineProvider,
|
||||
timelineItemFactory: TimelineItemFactory(),
|
||||
timelineViewFactory: TimelineViewFactory())
|
||||
timelineItemFactory: RoomTimelineItemFactory(mediaProvider: parameters.mediaProvider),
|
||||
mediaProvider: parameters.mediaProvider)
|
||||
|
||||
let viewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy, timelineController: timelineController)
|
||||
let viewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy,
|
||||
timelineController: timelineController,
|
||||
timelineViewFactory: RoomTimelineViewFactory())
|
||||
let view = RoomScreen(context: viewModel.context)
|
||||
roomScreenViewModel = viewModel
|
||||
roomScreenHostingController = UIHostingController(rootView: view)
|
||||
|
@ -28,6 +28,6 @@ enum RoomScreenViewAction {
|
||||
|
||||
struct RoomScreenViewState: BindableState {
|
||||
var roomTitle: String = ""
|
||||
var timelineItems: [RoomTimelineViewProvider] = []
|
||||
var items: [RoomTimelineViewProvider] = []
|
||||
var isBackPaginating = false
|
||||
}
|
||||
|
@ -29,24 +29,35 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private let timelineController: RoomTimelineControllerProtocol
|
||||
private let timelineViewFactory: RoomTimelineViewFactory
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(roomProxy: RoomProxyProtocol, timelineController: RoomTimelineControllerProtocol) {
|
||||
init(roomProxy: RoomProxyProtocol,
|
||||
timelineController: RoomTimelineControllerProtocol,
|
||||
timelineViewFactory: RoomTimelineViewFactory) {
|
||||
self.roomProxy = roomProxy
|
||||
self.timelineController = timelineController
|
||||
self.timelineViewFactory = timelineViewFactory
|
||||
|
||||
super.init(initialViewState: RoomScreenViewState())
|
||||
|
||||
state.roomTitle = roomProxy.name ?? ""
|
||||
state.timelineItems = timelineController.timelineItems
|
||||
buildTimelineViews()
|
||||
|
||||
timelineController.callbacks.sink { [weak self] callback in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch callback {
|
||||
case .updatedTimelineItems:
|
||||
self.state.timelineItems = timelineController.timelineItems
|
||||
self.buildTimelineViews()
|
||||
case .updatedTimelineItem(let itemId):
|
||||
guard let timelineItem = self.timelineController.timelineItems.first(where: { $0.id == itemId }),
|
||||
let viewIndex = self.state.items.firstIndex(where: { $0.id == itemId }) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.state.items[viewIndex] = timelineViewFactory.buildTimelineViewFor(timelineItem)
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
@ -60,10 +71,18 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
timelineController.paginateBackwards(Constants.backPaginationPageSize) { [weak self] _ in
|
||||
self?.state.isBackPaginating = false
|
||||
}
|
||||
case .itemAppeared:
|
||||
break
|
||||
case .itemDisappeared:
|
||||
break
|
||||
case .itemAppeared(let id):
|
||||
timelineController.processItemAppearance(id)
|
||||
case .itemDisappeared(let id):
|
||||
timelineController.processItemDisappearance(id)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func buildTimelineViews() {
|
||||
state.items = timelineController.timelineItems.map { item in
|
||||
timelineViewFactory.buildTimelineViewFor(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,12 +40,9 @@ struct RoomScreen: View {
|
||||
}
|
||||
|
||||
// No idea why previews don't work otherwise
|
||||
ForEach(isPreview ? context.viewState.timelineItems : timelineItems) { timelineItem in
|
||||
ForEach(isPreview ? context.viewState.items : timelineItems) { timelineItem in
|
||||
timelineItem
|
||||
.listRowSeparator(.hidden)
|
||||
.task {
|
||||
|
||||
}
|
||||
.onAppear {
|
||||
context.send(viewAction: .itemAppeared(id: timelineItem.id))
|
||||
}
|
||||
@ -75,7 +72,7 @@ struct RoomScreen: View {
|
||||
|
||||
attemptBackPagination()
|
||||
})
|
||||
.onChange(of: context.viewState.timelineItems) { _ in
|
||||
.onChange(of: context.viewState.items) { _ in
|
||||
// Don't update the list while moving
|
||||
if tableViewObserver.isDecelerating || tableViewObserver.isTracking {
|
||||
hasPendingChanges = true
|
||||
@ -83,7 +80,7 @@ struct RoomScreen: View {
|
||||
}
|
||||
|
||||
tableViewObserver.saveCurrentOffset()
|
||||
timelineItems = context.viewState.timelineItems
|
||||
timelineItems = context.viewState.items
|
||||
}
|
||||
.onReceive(tableViewObserver.scrollViewDidRest, perform: {
|
||||
if hasPendingChanges == false {
|
||||
@ -91,7 +88,7 @@ struct RoomScreen: View {
|
||||
}
|
||||
|
||||
tableViewObserver.saveCurrentOffset()
|
||||
timelineItems = context.viewState.timelineItems
|
||||
timelineItems = context.viewState.items
|
||||
hasPendingChanges = false
|
||||
})
|
||||
.onChange(of: timelineItems, perform: { _ in
|
||||
@ -244,7 +241,8 @@ private class TableViewObserver: NSObject, UITableViewDelegate {
|
||||
struct RoomScreen_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = RoomScreenViewModel(roomProxy: MockRoomProxy(displayName: "Test"),
|
||||
timelineController: MockRoomTimelineController())
|
||||
timelineController: MockRoomTimelineController(),
|
||||
timelineViewFactory: RoomTimelineViewFactory())
|
||||
|
||||
RoomScreen(context: viewModel.context)
|
||||
}
|
||||
|
@ -9,15 +9,12 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
import Combine
|
||||
import UIKit
|
||||
import Kingfisher
|
||||
|
||||
enum UserSessionCallback {
|
||||
case updatedRoomsList
|
||||
}
|
||||
|
||||
enum UserSessionError: Error {
|
||||
case failedRetrievingAvatar
|
||||
}
|
||||
|
||||
private class WeakUserSessionWrapper: ClientDelegate {
|
||||
private weak var userSession: UserSession?
|
||||
|
||||
@ -41,6 +38,8 @@ class UserSession: ClientDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
|
||||
deinit {
|
||||
client.setDelegate(delegate: nil)
|
||||
}
|
||||
@ -49,6 +48,7 @@ class UserSession: ClientDelegate {
|
||||
|
||||
init(client: Client) {
|
||||
self.client = client
|
||||
self.mediaProvider = MediaProvider(client: client, imageCache: ImageCache.default)
|
||||
|
||||
client.setDelegate(delegate: WeakUserSessionWrapper(userSession: self))
|
||||
client.startSync()
|
||||
@ -72,22 +72,6 @@ class UserSession: ClientDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func loadUserAvatar(_ completion: @escaping (Result<UIImage?, Error>) -> Void) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
do {
|
||||
let avatarData = try self.client.avatar()
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(UIImage(data: Data(bytes: avatarData, count: avatarData.count))))
|
||||
}
|
||||
} catch {
|
||||
MXLog.error("Failed retrieving room name with error: \(error)")
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(UserSessionError.failedRetrievingAvatar))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getRoomList(_ completion: @escaping ([RoomProxyProtocol]) -> Void) {
|
||||
fetchRoomList(completion)
|
||||
}
|
||||
|
75
ElementX/Sources/Services/Media/MediaProvider.swift
Normal file
75
ElementX/Sources/Services/Media/MediaProvider.swift
Normal file
@ -0,0 +1,75 @@
|
||||
//
|
||||
// MediaProvider.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 16/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MatrixRustSDK
|
||||
import Kingfisher
|
||||
|
||||
struct MediaProvider: MediaProviderProtocol {
|
||||
private let client: Client
|
||||
private let imageCache: Kingfisher.ImageCache
|
||||
|
||||
init(client: Client, imageCache: Kingfisher.ImageCache) {
|
||||
self.client = client
|
||||
self.imageCache = imageCache
|
||||
}
|
||||
|
||||
func loadCurrentUserAvatar(_ completion: @escaping (Result<UIImage?, MediaProviderError>) -> Void) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
do {
|
||||
let imageData = try self.client.avatar()
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(UIImage(data: Data(bytes: imageData, count: imageData.count))))
|
||||
}
|
||||
} catch {
|
||||
MXLog.error("Failed retrieving image with error: \(error)")
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(.failedRetrievingImage))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hasImageCachedForURL(_ url: String) -> Bool {
|
||||
self.imageCache.imageCachedType(forKey: url) == .memory
|
||||
}
|
||||
|
||||
func loadImageFromURL(_ url: String, _ completion: @escaping (Result<UIImage, MediaProviderError>) -> Void) {
|
||||
self.imageCache.retrieveImage(forKey: url) { result in
|
||||
if case let .success(cacheResult) = result,
|
||||
let image = cacheResult.image {
|
||||
completion(.success(image))
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
do {
|
||||
let imageData = try self.client.loadImage(url: url)
|
||||
|
||||
guard let image = UIImage(data: Data(bytes: imageData, count: imageData.count)) else {
|
||||
MXLog.error("Invalid image data")
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(.invalidImageData))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.imageCache.store(image, forKey: url)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(image))
|
||||
}
|
||||
} catch {
|
||||
MXLog.error("Failed retrieving image with error: \(error)")
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(.failedRetrievingImage))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
ElementX/Sources/Services/Media/MediaProviderProtocol.swift
Normal file
21
ElementX/Sources/Services/Media/MediaProviderProtocol.swift
Normal file
@ -0,0 +1,21 @@
|
||||
//
|
||||
// MediaProviderProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 17/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
enum MediaProviderError: Error {
|
||||
case failedRetrievingImage
|
||||
case invalidImageData
|
||||
}
|
||||
|
||||
protocol MediaProviderProtocol {
|
||||
func loadCurrentUserAvatar(_ completion: @escaping (Result<UIImage?, MediaProviderError>) -> Void)
|
||||
func hasImageCachedForURL(_ url: String) -> Bool
|
||||
func loadImageFromURL(_ url: String, _ completion: @escaping (Result<UIImage, MediaProviderError>) -> Void)
|
||||
}
|
25
ElementX/Sources/Services/Media/MockMediaProvider.swift
Normal file
25
ElementX/Sources/Services/Media/MockMediaProvider.swift
Normal file
@ -0,0 +1,25 @@
|
||||
//
|
||||
// MockMediaProvider.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 17/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct MockMediaProvider: MediaProviderProtocol {
|
||||
|
||||
func loadCurrentUserAvatar(_ completion: @escaping (Result<UIImage?, MediaProviderError>) -> Void) {
|
||||
|
||||
}
|
||||
|
||||
func hasImageCachedForURL(_ url: String) -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
func loadImageFromURL(_ url: String, _ completion: @escaping (Result<UIImage, MediaProviderError>) -> Void) {
|
||||
|
||||
}
|
||||
}
|
@ -31,4 +31,8 @@ struct ImageRoomMessage: RoomMessageProtocol {
|
||||
var originServerTs: Date {
|
||||
Date(timeIntervalSince1970: TimeInterval(message.baseMessage().originServerTs()))
|
||||
}
|
||||
|
||||
var url: String? {
|
||||
message.url()
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
|
||||
struct MockRoomProxy: RoomProxyProtocol {
|
||||
let id = UUID().uuidString
|
||||
@ -18,7 +17,7 @@ struct MockRoomProxy: RoomProxyProtocol {
|
||||
let topic: String? = nil
|
||||
let lastMessage: String? = "Last message"
|
||||
|
||||
let avatarURL: URL? = nil
|
||||
let avatarURL: String? = nil
|
||||
|
||||
let isDirect = Bool.random()
|
||||
let isSpace = Bool.random()
|
||||
@ -27,19 +26,19 @@ struct MockRoomProxy: RoomProxyProtocol {
|
||||
|
||||
var callbacks = PassthroughSubject<RoomProxyCallback, Never>()
|
||||
|
||||
func loadDisplayName(_ completion: @escaping (Result<String, Error>) -> Void) {
|
||||
func loadDisplayName(_ completion: @escaping (Result<String, RoomProxyError>) -> Void) {
|
||||
completion(.success(displayName))
|
||||
}
|
||||
|
||||
func loadAvatar(_ completion: (Result<UIImage?, Error>) -> Void) {
|
||||
completion(.success(UIImage(systemName: "wand.and.stars")))
|
||||
}
|
||||
|
||||
func startLiveEventListener() {
|
||||
|
||||
}
|
||||
|
||||
func paginateBackwards(count: UInt, callback: ((Result<[RoomMessageProtocol], Error>) -> Void)?) {
|
||||
func paginateBackwards(count: UInt, callback: ((Result<[RoomMessageProtocol], RoomProxyError>) -> Void)?) {
|
||||
|
||||
}
|
||||
|
||||
func avatarURLForUserId(_ userId: String, completion: @escaping (Result<String?, RoomProxyError>) -> Void) {
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -11,12 +11,6 @@ import Combine
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
enum RoomProxyError: Error {
|
||||
case failedRetrievingDisplayName
|
||||
case failedRetrievingAvatar
|
||||
case backwardStreamNotAvailable
|
||||
}
|
||||
|
||||
private class WeakRoomProxyWrapper: RoomDelegate {
|
||||
private weak var roomProxy: RoomProxy?
|
||||
|
||||
@ -92,21 +86,17 @@ class RoomProxy: RoomProxyProtocol, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
var avatarURL: URL? {
|
||||
guard let urlString = room.avatarUrl() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return URL(string: urlString)
|
||||
var avatarURL: String? {
|
||||
room.avatarUrl()
|
||||
}
|
||||
|
||||
func loadDisplayName(_ completion: @escaping (Result<String, Error>) -> Void) {
|
||||
func avatarURLForUserId(_ userId: String, completion: @escaping (Result<String?, RoomProxyError>) -> Void) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
do {
|
||||
let displayName = try self.room.displayName()
|
||||
let avatarURL = try self.room.memberAvatarUrl(userId: userId)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(displayName))
|
||||
completion(.success(avatarURL))
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
@ -116,28 +106,28 @@ class RoomProxy: RoomProxyProtocol, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
func loadAvatar(_ completion: @escaping (Result<UIImage?, Error>) -> Void) {
|
||||
func loadDisplayName(_ completion: @escaping (Result<String, RoomProxyError>) -> Void) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
do {
|
||||
let avatarData = try self.room.avatar()
|
||||
let displayName = try self.room.displayName()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(UIImage(data: Data(bytes: avatarData, count: avatarData.count))))
|
||||
completion(.success(displayName))
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(RoomProxyError.failedRetrievingAvatar))
|
||||
completion(.failure(.failedRetrievingDisplayName))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func paginateBackwards(count: UInt, callback: ((Result<[RoomMessageProtocol], Error>) -> Void)?) {
|
||||
|
||||
func paginateBackwards(count: UInt, callback: ((Result<[RoomMessageProtocol], RoomProxyError>) -> Void)?) {
|
||||
MXLog.debug("Started backpaginating")
|
||||
processingQueue.async {
|
||||
guard let backwardStream = self.backwardStream else {
|
||||
DispatchQueue.main.async {
|
||||
callback?(.failure(RoomProxyError.backwardStreamNotAvailable))
|
||||
callback?(.failure(.backwardStreamNotAvailable))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -7,7 +7,12 @@
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
|
||||
enum RoomProxyError: Error {
|
||||
case failedRetrievingDisplayName
|
||||
case failedRetrievingAvatar
|
||||
case backwardStreamNotAvailable
|
||||
}
|
||||
|
||||
enum RoomProxyCallback {
|
||||
case addedMessage(RoomMessageProtocol)
|
||||
@ -26,12 +31,13 @@ protocol RoomProxyProtocol {
|
||||
var topic: String? { get }
|
||||
var lastMessage: String? { get }
|
||||
|
||||
var avatarURL: URL? { get }
|
||||
var avatarURL: String? { get }
|
||||
|
||||
func loadDisplayName(_ completion: @escaping (Result<String, Error>) -> Void)
|
||||
func loadAvatar(_ completion: @escaping (Result<UIImage?, Error>) -> Void)
|
||||
func avatarURLForUserId(_ userId: String, completion: @escaping (Result<String?, RoomProxyError>) -> Void)
|
||||
|
||||
func paginateBackwards(count: UInt, callback: ((Result<[RoomMessageProtocol], Error>) -> Void)?)
|
||||
func loadDisplayName(_ completion: @escaping (Result<String, RoomProxyError>) -> Void)
|
||||
|
||||
func paginateBackwards(count: UInt, callback: ((Result<[RoomMessageProtocol], RoomProxyError>) -> Void)?)
|
||||
|
||||
var callbacks: PassthroughSubject<RoomProxyCallback, Never> { get }
|
||||
}
|
||||
|
@ -13,13 +13,21 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
||||
|
||||
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
|
||||
|
||||
var timelineItems: [RoomTimelineViewProvider] = [RoomTimelineViewProvider.separator(.init(id: UUID().uuidString, text: "Yesterday")),
|
||||
RoomTimelineViewProvider.text(.init(id: UUID().uuidString, senderDisplayName: "Alice", text: "You rock!", timestamp: "10:10 AM", shouldShowSenderDetails: true)),
|
||||
RoomTimelineViewProvider.text(.init(id: UUID().uuidString, senderDisplayName: "Alice", text: "You also rule!", timestamp: "10:11 AM", shouldShowSenderDetails: false)),
|
||||
RoomTimelineViewProvider.separator(.init(id: UUID().uuidString, text: "Today")),
|
||||
RoomTimelineViewProvider.text(.init(id: UUID().uuidString, senderDisplayName: "Bob", text: "You too!", timestamp: "5 PM", shouldShowSenderDetails: true))]
|
||||
var timelineItems: [RoomTimelineItemProtocol] = [SeparatorRoomTimelineItem(id: UUID().uuidString, text: "Yesterday"),
|
||||
TextRoomTimelineItem(id: UUID().uuidString, text: "You rock!", timestamp: "10:10 AM", shouldShowSenderDetails: true, sender: "Alice"),
|
||||
TextRoomTimelineItem(id: UUID().uuidString, text: "You also rule!", timestamp: "10:11 AM", shouldShowSenderDetails: false, sender: "Alice"),
|
||||
SeparatorRoomTimelineItem(id: UUID().uuidString, text: "Today"),
|
||||
TextRoomTimelineItem(id: UUID().uuidString, text: "You too!", timestamp: "5 PM", shouldShowSenderDetails: true, sender: "Bob")]
|
||||
|
||||
func paginateBackwards(_ count: UInt, callback: ((Result<Void, RoomTimelineControllerError>) -> Void)) {
|
||||
callbacks.send(.updatedTimelineItems)
|
||||
}
|
||||
|
||||
func processItemAppearance(_ itemId: String) {
|
||||
|
||||
}
|
||||
|
||||
func processItemDisappearance(_ itemId: String) {
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -8,31 +8,31 @@
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
|
||||
class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
private let timelineProvider: RoomTimelineProvider
|
||||
private let timelineItemFactory: TimelineItemFactory
|
||||
private let timelineViewFactory: TimelineViewFactory
|
||||
private let timelineItemFactory: RoomTimelineItemFactory
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
|
||||
|
||||
private(set) var timelineItems = [RoomTimelineViewProvider]()
|
||||
private(set) var timelineItems = [RoomTimelineItemProtocol]()
|
||||
|
||||
init(timelineProvider: RoomTimelineProvider,
|
||||
timelineItemFactory: TimelineItemFactory,
|
||||
timelineViewFactory: TimelineViewFactory) {
|
||||
timelineItemFactory: RoomTimelineItemFactory,
|
||||
mediaProvider: MediaProviderProtocol) {
|
||||
self.timelineProvider = timelineProvider
|
||||
self.timelineItemFactory = timelineItemFactory
|
||||
self.timelineViewFactory = timelineViewFactory
|
||||
self.mediaProvider = mediaProvider
|
||||
|
||||
self.timelineProvider.callbacks.sink { [weak self] callback in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch callback {
|
||||
case .addedMessage:
|
||||
self.rebuildTimeline()
|
||||
self.updateTimelineItems()
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
@ -42,17 +42,58 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
switch result {
|
||||
case .success:
|
||||
callback(.success(()))
|
||||
self?.rebuildTimeline()
|
||||
self?.updateTimelineItems()
|
||||
case .failure:
|
||||
callback(.failure(.generic))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func processItemAppearance(_ itemId: String) {
|
||||
guard let timelineItem = self.timelineItems.filter({ $0.id == itemId}).first else {
|
||||
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 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))
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func processItemDisappearance(_ itemId: String) {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func rebuildTimeline() {
|
||||
var newTimelineItems = [RoomTimelineViewProvider]()
|
||||
private func updateTimelineItems() {
|
||||
var newTimelineItems = [RoomTimelineItemProtocol]()
|
||||
|
||||
var previousMessage: RoomMessageProtocol?
|
||||
for message in self.timelineProvider.messages {
|
||||
@ -60,17 +101,14 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
let shouldAddSectionHeader = !areMessagesFromTheSameDay
|
||||
|
||||
if shouldAddSectionHeader {
|
||||
let item = SeparatorRoomTimelineItem(id: message.originServerTs.ISO8601Format(),
|
||||
text: message.originServerTs.formatted(date: .long, time: .omitted))
|
||||
|
||||
newTimelineItems.append(RoomTimelineViewProvider.separator(item))
|
||||
newTimelineItems.append(SeparatorRoomTimelineItem(id: message.originServerTs.ISO8601Format(),
|
||||
text: message.originServerTs.formatted(date: .long, time: .omitted)))
|
||||
}
|
||||
|
||||
let areMessagesFromTheSameSender = (previousMessage?.sender == message.sender)
|
||||
let shouldShowSenderDetails = !areMessagesFromTheSameSender || !areMessagesFromTheSameDay
|
||||
|
||||
let timelineItem = timelineItemFactory.buildTimelineItemFor(message, showSenderDetails: shouldShowSenderDetails)
|
||||
newTimelineItems.append(timelineViewFactory.buildTimelineViewFor(timelineItem))
|
||||
newTimelineItems.append(timelineItemFactory.buildTimelineItemFor(message, showSenderDetails: shouldShowSenderDetails))
|
||||
|
||||
previousMessage = message
|
||||
}
|
||||
@ -87,4 +125,43 @@ 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
|
||||
}
|
||||
|
||||
timelineProvider.avatarURLForUserId(item.sender) { [weak self] result in
|
||||
guard let self = self 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))
|
||||
}
|
||||
}
|
||||
case .failure:
|
||||
MXLog.error("Failed retrieving user avatar")
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import Combine
|
||||
|
||||
enum RoomTimelineControllerCallback {
|
||||
case updatedTimelineItems
|
||||
case updatedTimelineItem(_ itemId: String)
|
||||
}
|
||||
|
||||
enum RoomTimelineControllerError: Error {
|
||||
@ -18,8 +19,12 @@ enum RoomTimelineControllerError: Error {
|
||||
}
|
||||
|
||||
protocol RoomTimelineControllerProtocol {
|
||||
var timelineItems: [RoomTimelineViewProvider] { get }
|
||||
var timelineItems: [RoomTimelineItemProtocol] { get }
|
||||
var callbacks: PassthroughSubject<RoomTimelineControllerCallback, Never> { get }
|
||||
|
||||
|
||||
func paginateBackwards(_ count: UInt, callback: @escaping ((Result<Void, RoomTimelineControllerError>) -> Void))
|
||||
|
||||
func processItemAppearance(_ itemId: String)
|
||||
|
||||
func processItemDisappearance(_ itemId: String)
|
||||
}
|
||||
|
@ -8,7 +8,6 @@
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
|
||||
enum RoomTimelineCallback {
|
||||
case addedMessage
|
||||
@ -52,4 +51,16 @@ class RoomTimelineProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is probably not the right place for this method. We need a RoomMemberProvider or something
|
||||
func avatarURLForUserId(_ userId: String, completion: @escaping (Result<String?, RoomTimelineError>) -> Void) {
|
||||
self.roomProxy.avatarURLForUserId(userId) { result in
|
||||
switch result {
|
||||
case .success(let avatarURL):
|
||||
completion(.success(avatarURL))
|
||||
case .failure:
|
||||
completion(.failure(.generic))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,11 +7,17 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct ImageRoomTimelineItem: TimelineItemProtocol, Identifiable, Equatable {
|
||||
struct ImageRoomTimelineItem: BaseRoomTimelineItemProtocol, Identifiable, Equatable {
|
||||
let id: String
|
||||
let senderDisplayName: String
|
||||
let text: String
|
||||
let timestamp: String
|
||||
let shouldShowSenderDetails: Bool
|
||||
|
||||
let sender: String
|
||||
var senderAvatar: UIImage?
|
||||
|
||||
let url: String?
|
||||
var image: UIImage?
|
||||
}
|
||||
|
@ -11,16 +11,50 @@ import SwiftUI
|
||||
|
||||
struct ImageRoomTimelineView: View {
|
||||
let timelineItem: ImageRoomTimelineItem
|
||||
var loadedImage: UIImage?
|
||||
|
||||
var body: some View {
|
||||
if let loadedImage = loadedImage {
|
||||
Image(uiImage: loadedImage)
|
||||
} else {
|
||||
if let image = timelineItem.image {
|
||||
VStack {
|
||||
Image(systemName: "photo")
|
||||
ProgressView()
|
||||
HStack {
|
||||
Text(timelineItem.text)
|
||||
Spacer()
|
||||
}
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .center) {
|
||||
HStack {
|
||||
Text(timelineItem.text)
|
||||
Spacer()
|
||||
}
|
||||
ProgressView("Loading")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
let timelineItem = ImageRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Some image",
|
||||
timestamp: "Now",
|
||||
shouldShowSenderDetails: false,
|
||||
sender: "Bob",
|
||||
url: nil,
|
||||
image: UIImage(systemName: "photo"))
|
||||
ImageRoomTimelineView(timelineItem: timelineItem)
|
||||
|
||||
let timelineItem = ImageRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Some other image",
|
||||
timestamp: "Now",
|
||||
shouldShowSenderDetails: false,
|
||||
sender: "Bob",
|
||||
url: nil,
|
||||
image: nil)
|
||||
ImageRoomTimelineView(timelineItem: timelineItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,22 +7,43 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct TimelineItemFactory {
|
||||
func buildTimelineItemFor(_ roomMessage: RoomMessageProtocol, showSenderDetails: Bool) -> TimelineItemProtocol {
|
||||
struct RoomTimelineItemFactory {
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
|
||||
init(mediaProvider: MediaProviderProtocol) {
|
||||
self.mediaProvider = mediaProvider
|
||||
}
|
||||
|
||||
func buildTimelineItemFor(_ roomMessage: RoomMessageProtocol, showSenderDetails: Bool) -> RoomTimelineItemProtocol {
|
||||
switch roomMessage {
|
||||
case let message as TextRoomMessage:
|
||||
return TextRoomTimelineItem(id: message.id,
|
||||
senderDisplayName: message.sender,
|
||||
text: message.content,
|
||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||
shouldShowSenderDetails: showSenderDetails)
|
||||
shouldShowSenderDetails: showSenderDetails,
|
||||
sender: message.sender)
|
||||
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,
|
||||
senderDisplayName: message.sender,
|
||||
text: message.content,
|
||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||
shouldShowSenderDetails: showSenderDetails)
|
||||
shouldShowSenderDetails: showSenderDetails,
|
||||
sender: message.sender,
|
||||
url: message.url,
|
||||
image: image)
|
||||
default:
|
||||
fatalError("Unknown room message.")
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
//
|
||||
// RoomTimelineItemProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 16/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
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,24 @@
|
||||
//
|
||||
// TimelineViewFactory.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 16/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct RoomTimelineViewFactory {
|
||||
func buildTimelineViewFor(_ timelineItem: RoomTimelineItemProtocol) -> RoomTimelineViewProvider {
|
||||
switch timelineItem {
|
||||
case let item as TextRoomTimelineItem:
|
||||
return .text(item)
|
||||
case let item as ImageRoomTimelineItem:
|
||||
return .image(item)
|
||||
case let item as SeparatorRoomTimelineItem:
|
||||
return .separator(item)
|
||||
default:
|
||||
fatalError("Unknown timeline item")
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct SeparatorRoomTimelineItem: TimelineItemProtocol, Identifiable, Equatable {
|
||||
struct SeparatorRoomTimelineItem: RoomTimelineItemProtocol, Identifiable, Equatable {
|
||||
let id: String
|
||||
let text: String
|
||||
}
|
||||
|
@ -7,11 +7,14 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct TextRoomTimelineItem: TimelineItemProtocol, Identifiable, Equatable {
|
||||
struct TextRoomTimelineItem: BaseRoomTimelineItemProtocol, Identifiable, Equatable {
|
||||
let id: String
|
||||
let senderDisplayName: String
|
||||
let text: String
|
||||
let timestamp: String
|
||||
let shouldShowSenderDetails: Bool
|
||||
|
||||
let sender: String
|
||||
var senderAvatar: UIImage?
|
||||
}
|
||||
|
@ -16,7 +16,8 @@ struct TextRoomTimelineView: View {
|
||||
VStack(alignment: .leading) {
|
||||
if timelineItem.shouldShowSenderDetails {
|
||||
HStack {
|
||||
Text(timelineItem.senderDisplayName)
|
||||
avatar
|
||||
Text(timelineItem.sender)
|
||||
.font(.footnote)
|
||||
.bold()
|
||||
Spacer()
|
||||
@ -24,10 +25,48 @@ struct TextRoomTimelineView: View {
|
||||
.font(.footnote)
|
||||
}
|
||||
Divider()
|
||||
Spacer()
|
||||
}
|
||||
Text(timelineItem.text)
|
||||
}
|
||||
.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 {
|
||||
static var previews: some View {
|
||||
VStack(spacing: 20.0) {
|
||||
let timelineItem = TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Short loin ground round tongue hamburger, fatback salami shoulder. Beef turkey sausage kielbasa strip steak. Alcatra capicola pig tail pancetta chislic.",
|
||||
timestamp: "Now",
|
||||
shouldShowSenderDetails: true,
|
||||
sender: "Bob")
|
||||
TextRoomTimelineView(timelineItem: timelineItem)
|
||||
|
||||
let timelineItem = TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Some other text",
|
||||
timestamp: "Later",
|
||||
shouldShowSenderDetails: true,
|
||||
sender: "Anne")
|
||||
TextRoomTimelineView(timelineItem: timelineItem)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
//
|
||||
// TimelineItemProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 16/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol TimelineItemProtocol {
|
||||
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
//
|
||||
// TimelineViewFactory.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 16/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TimelineViewFactory {
|
||||
func buildTimelineViewFor(_ timelineItem: TimelineItemProtocol) -> RoomTimelineViewProvider {
|
||||
switch timelineItem {
|
||||
case let textItem as TextRoomTimelineItem:
|
||||
return .text(textItem)
|
||||
case let imageItem as ImageRoomTimelineItem:
|
||||
return .image(imageItem)
|
||||
default:
|
||||
fatalError("Unknown timeline item")
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user