From 464cba93a056a34b57a6e8342954e63dc45bbaf5 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 11 Mar 2022 14:47:11 +0200 Subject: [PATCH] Introduced the RoomTimelineViewProvider with different timeline items/views. Added timeline date separators (currently breaks back pagination) --- ElementX.xcodeproj/project.pbxproj | 32 ++++- ElementX/Sources/AppCoordinator.swift | 8 +- .../Screens/HomeScreen/View/HomeScreen.swift | 117 +++++++++--------- .../Screens/RoomScreen/RoomScreenModels.swift | 4 +- .../RoomScreen/RoomScreenViewModel.swift | 12 +- .../Screens/RoomScreen/View/RoomScreen.swift | 23 ++-- .../Timeline/MockRoomTimelineController.swift | 12 +- .../Timeline/RoomTimelineController.swift | 43 +++---- .../RoomTimelineControllerProtocol.swift | 2 +- .../TimelineItems/ImageRoomTimelineItem.swift | 14 +++ .../TimelineItems/ImageRoomTimelineView.swift | 23 ++++ .../TimelineItems/RoomTimelineItem.swift | 84 ------------- .../RoomTimelineViewProvider.swift | 40 ++++++ .../SeparatorRoomTimelineItem.swift | 14 +++ .../SeparatorRoomTimelineView.swift | 43 +++++++ .../TimelineItems/TextRoomTimelineItem.swift | 17 +++ .../TimelineItems/TextRoomTimelineView.swift | 33 +++++ 17 files changed, 329 insertions(+), 192 deletions(-) create mode 100644 ElementX/Sources/Services/Timeline/TimelineItems/ImageRoomTimelineItem.swift create mode 100644 ElementX/Sources/Services/Timeline/TimelineItems/ImageRoomTimelineView.swift delete mode 100644 ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItem.swift create mode 100644 ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift create mode 100644 ElementX/Sources/Services/Timeline/TimelineItems/SeparatorRoomTimelineItem.swift create mode 100644 ElementX/Sources/Services/Timeline/TimelineItems/SeparatorRoomTimelineView.swift create mode 100644 ElementX/Sources/Services/Timeline/TimelineItems/TextRoomTimelineItem.swift create mode 100644 ElementX/Sources/Services/Timeline/TimelineItems/TextRoomTimelineView.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 4aaec9f79..ed9fca40d 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -19,7 +19,7 @@ 1859CF5527D7A6FF00E86E4E /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 1859CF5427D7A6FF00E86E4E /* MatrixRustSDK */; }; 1863A3FC27BA5A9100B52E4D /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 1863A3FB27BA5A9100B52E4D /* KeychainAccess */; }; 1863A40627BA6DFC00B52E4D /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = 1863A40527BA6DFC00B52E4D /* SwiftyBeaver */; }; - 18A318DD27DA42C9000867CD /* RoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A318DB27DA42C9000867CD /* RoomTimelineItem.swift */; }; + 18A318DD27DA42C9000867CD /* RoomTimelineViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A318DB27DA42C9000867CD /* RoomTimelineViewProvider.swift */; }; 18F2BAD727D25B4000DD1988 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BA7327D25B4000DD1988 /* RoomProxyProtocol.swift */; }; 18F2BAD827D25B4000DD1988 /* RoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BA7427D25B4000DD1988 /* RoomProxy.swift */; }; 18F2BAD927D25B4000DD1988 /* MockRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BA7527D25B4000DD1988 /* MockRoomProxy.swift */; }; @@ -80,6 +80,12 @@ 18F2BB2227D25D4600DD1988 /* LoginScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BACF27D25B4000DD1988 /* LoginScreenUITests.swift */; }; 18F2BB2827D2647A00DD1988 /* MockRoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BB2727D2647A00DD1988 /* MockRoomTimelineController.swift */; }; 18F2BB2A27D2648900DD1988 /* RoomTimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BB2927D2648900DD1988 /* RoomTimelineControllerProtocol.swift */; }; + 18F9889827DB7473002F48B4 /* ImageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F9889727DB7473002F48B4 /* ImageRoomTimelineItem.swift */; }; + 18F9889A27DB747B002F48B4 /* TextRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F9889927DB747B002F48B4 /* TextRoomTimelineItem.swift */; }; + 18F9889C27DB7491002F48B4 /* SeparatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F9889B27DB7491002F48B4 /* SeparatorRoomTimelineItem.swift */; }; + 18F9889E27DB752B002F48B4 /* ImageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F9889D27DB752B002F48B4 /* ImageRoomTimelineView.swift */; }; + 18F988A027DB7532002F48B4 /* TextRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F9889F27DB7532002F48B4 /* TextRoomTimelineView.swift */; }; + 18F988A227DB753B002F48B4 /* SeparatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F988A127DB753B002F48B4 /* SeparatorRoomTimelineView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -111,7 +117,7 @@ 1850256727B6A135002E6B18 /* ElementX.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = ElementX.entitlements; sourceTree = ""; }; 1850256827B6A135002E6B18 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 1850256A27B6A135002E6B18 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 18A318DB27DA42C9000867CD /* RoomTimelineItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomTimelineItem.swift; sourceTree = ""; }; + 18A318DB27DA42C9000867CD /* RoomTimelineViewProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewProvider.swift; sourceTree = ""; }; 18F2BA7327D25B4000DD1988 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = ""; }; 18F2BA7427D25B4000DD1988 /* RoomProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = ""; }; 18F2BA7527D25B4000DD1988 /* MockRoomProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockRoomProxy.swift; sourceTree = ""; }; @@ -172,6 +178,12 @@ 18F2BB1927D25BE800DD1988 /* RoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineController.swift; sourceTree = ""; }; 18F2BB2727D2647A00DD1988 /* MockRoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineController.swift; sourceTree = ""; }; 18F2BB2927D2648900DD1988 /* RoomTimelineControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerProtocol.swift; sourceTree = ""; }; + 18F9889727DB7473002F48B4 /* ImageRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineItem.swift; sourceTree = ""; }; + 18F9889927DB747B002F48B4 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = ""; }; + 18F9889B27DB7491002F48B4 /* SeparatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineItem.swift; sourceTree = ""; }; + 18F9889D27DB752B002F48B4 /* ImageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineView.swift; sourceTree = ""; }; + 18F9889F27DB7532002F48B4 /* TextRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineView.swift; sourceTree = ""; }; + 18F988A127DB753B002F48B4 /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = ""; }; 18FE279627C7B85300016375 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ @@ -277,7 +289,13 @@ 18A318D927DA42C9000867CD /* TimelineItems */ = { isa = PBXGroup; children = ( - 18A318DB27DA42C9000867CD /* RoomTimelineItem.swift */, + 18A318DB27DA42C9000867CD /* RoomTimelineViewProvider.swift */, + 18F9889727DB7473002F48B4 /* ImageRoomTimelineItem.swift */, + 18F9889D27DB752B002F48B4 /* ImageRoomTimelineView.swift */, + 18F9889927DB747B002F48B4 /* TextRoomTimelineItem.swift */, + 18F9889F27DB7532002F48B4 /* TextRoomTimelineView.swift */, + 18F9889B27DB7491002F48B4 /* SeparatorRoomTimelineItem.swift */, + 18F988A127DB753B002F48B4 /* SeparatorRoomTimelineView.swift */, ); path = TimelineItems; sourceTree = ""; @@ -735,6 +753,7 @@ 18F2BAED27D25B4000DD1988 /* FullscreenLoadingActivityPresenter.swift in Sources */, 18F2BB0F27D25B4000DD1988 /* RoomScreen.swift in Sources */, 18F2BAFF27D25B4000DD1988 /* HomeScreenModels.swift in Sources */, + 18F9889C27DB7491002F48B4 /* SeparatorRoomTimelineItem.swift in Sources */, 18F2BB1527D25B4000DD1988 /* LoginScreenViewModelProtocol.swift in Sources */, 18F2BAEB27D25B4000DD1988 /* LabelledActivityIndicatorView.swift in Sources */, 18F2BAE427D25B4000DD1988 /* Presentable.swift in Sources */, @@ -745,6 +764,7 @@ 18F2BAD927D25B4000DD1988 /* MockRoomProxy.swift in Sources */, 18F2BAE727D25B4000DD1988 /* RoundedToastView.swift in Sources */, 18F2BAF227D25B4000DD1988 /* WeakDictionaryKeyReference.swift in Sources */, + 18F988A027DB7532002F48B4 /* TextRoomTimelineView.swift in Sources */, 18F2BAE027D25B4000DD1988 /* NavigationRouter.swift in Sources */, 18F2BAF627D25B4000DD1988 /* Coordinator.swift in Sources */, 18F2BAEA27D25B4000DD1988 /* ActivityCenter.swift in Sources */, @@ -770,19 +790,23 @@ 18F2BAFE27D25B4000DD1988 /* HomeScreenViewModelProtocol.swift in Sources */, 18F2BAE827D25B4000DD1988 /* RectangleToastView.swift in Sources */, 18F2BB1627D25B4000DD1988 /* LoginScreenModels.swift in Sources */, + 18F9889E27DB752B002F48B4 /* ImageRoomTimelineView.swift in Sources */, 18F2BADA27D25B4000DD1988 /* RoomTimelineProvider.swift in Sources */, + 18F9889827DB7473002F48B4 /* ImageRoomTimelineItem.swift in Sources */, 18F2BB0027D25B4000DD1988 /* HomeScreen.swift in Sources */, 18F2BB2827D2647A00DD1988 /* MockRoomTimelineController.swift in Sources */, 18F2BB0127D25B4000DD1988 /* HomeScreenViewModel.swift in Sources */, 18F2BAF027D25B4000DD1988 /* ActivityDismissal.swift in Sources */, 18F2BADD27D25B4000DD1988 /* KeychainController.swift in Sources */, - 18A318DD27DA42C9000867CD /* RoomTimelineItem.swift in Sources */, + 18A318DD27DA42C9000867CD /* RoomTimelineViewProvider.swift in Sources */, 18F2BAFB27D25B4000DD1988 /* HomeScreenCoordinator.swift in Sources */, 18F2BB0C27D25B4000DD1988 /* RoomScreenCoordinator.swift in Sources */, 18F2BB0E27D25B4000DD1988 /* RoomScreenViewModelProtocol.swift in Sources */, 18F2BB0D27D25B4000DD1988 /* RoomScreenViewModel.swift in Sources */, + 18F988A227DB753B002F48B4 /* SeparatorRoomTimelineView.swift in Sources */, 18F2BAE127D25B4000DD1988 /* RootRouterType.swift in Sources */, 1850256C27B6A135002E6B18 /* AppCoordinator.swift in Sources */, + 18F9889A27DB747B002F48B4 /* TextRoomTimelineItem.swift in Sources */, 18F2BADC27D25B4000DD1988 /* UserSession.swift in Sources */, 18F2BAEF27D25B4000DD1988 /* ActivityRequest.swift in Sources */, 18F2BAEE27D25B4000DD1988 /* Activity.swift in Sources */, diff --git a/ElementX/Sources/AppCoordinator.swift b/ElementX/Sources/AppCoordinator.swift index 1733a6e54..7bfa888c2 100644 --- a/ElementX/Sources/AppCoordinator.swift +++ b/ElementX/Sources/AppCoordinator.swift @@ -117,12 +117,16 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { let parameters = RoomScreenCoordinatorParameters(roomProxy: roomProxy) let coordinator = RoomScreenCoordinator(parameters: parameters) - coordinator.completion = { [weak self] result in + coordinator.completion = { _ in } self.add(childCoordinator: coordinator) - self.navigationRouter.push(coordinator) + self.navigationRouter.push(coordinator) { [weak self] in + guard let self = self else { return } + + self.remove(childCoordinator: coordinator) + } } } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index b1feda51c..3ad46552e 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -24,79 +24,76 @@ struct HomeScreen: View { // MARK: Views var body: some View { - NavigationView { - VStack(spacing: 16.0) { - if context.viewState.isLoadingRooms { - VStack { - Text("Loading rooms") - ProgressView() - } - } else { - List { - Section("Rooms") { - ForEach(context.viewState.unencryptedRooms) { room in - RoomCell(room: room, context: context) - } - - let other = context.viewState.encryptedRooms - - if other.count > 0 { - DisclosureGroup("Encrypted") { - ForEach(other) { room in - RoomCell(room: room, context: context) - } - } - } + VStack(spacing: 16.0) { + if context.viewState.isLoadingRooms { + VStack { + Text("Loading rooms") + ProgressView() + } + } else { + List { + Section("Rooms") { + ForEach(context.viewState.unencryptedRooms) { room in + RoomCell(room: room, context: context) } - Section("People") { - ForEach(context.viewState.unencryptedDMs) { room in - RoomCell(room: room, context: context) - } - - let other = context.viewState.encryptedDMs - - if other.count > 0 { - DisclosureGroup("Encrypted") { - ForEach(other) { room in - RoomCell(room: room, context: context) - } + let other = context.viewState.encryptedRooms + + if other.count > 0 { + DisclosureGroup("Encrypted") { + ForEach(other) { room in + RoomCell(room: room, context: context) } } } } - .listStyle(.plain) - } - - Spacer() - } - .ignoresSafeArea(.all, edges: .bottom) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - HStack { - if let avatar = context.viewState.userAvatar { - Image(uiImage: avatar) - .resizable() - .scaledToFill() - .frame(width: 40, height: 40, alignment: .center) - .mask(Circle()) - } else { - let _ = context.send(viewAction: .loadUserAvatar) + + Section("People") { + ForEach(context.viewState.unencryptedDMs) { room in + RoomCell(room: room, context: context) + } + + let other = context.viewState.encryptedDMs + + if other.count > 0 { + DisclosureGroup("Encrypted") { + ForEach(other) { room in + RoomCell(room: room, context: context) + } + } } - Text("Hello, \(context.viewState.userDisplayName)!") - .font(.subheadline) - .fontWeight(.bold) } } - ToolbarItem(placement: .navigationBarTrailing) { - Button("Logout") { - context.send(viewAction: .logout) + .listStyle(.plain) + } + + Spacer() + } + .ignoresSafeArea(.all, edges: .bottom) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + HStack { + if let avatar = context.viewState.userAvatar { + Image(uiImage: avatar) + .resizable() + .scaledToFill() + .frame(width: 40, height: 40, alignment: .center) + .mask(Circle()) + } else { + let _ = context.send(viewAction: .loadUserAvatar) } + Text("Hello, \(context.viewState.userDisplayName)!") + .font(.subheadline) + .fontWeight(.bold) + } + } + ToolbarItem(placement: .navigationBarTrailing) { + Button("Logout") { + context.send(viewAction: .logout) } } } - .navigationViewStyle(StackNavigationViewStyle()) } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 1621ea5bc..69e346ec7 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -22,9 +22,11 @@ enum RoomScreenViewModelResult { enum RoomScreenViewAction { case loadPreviousPage + case itemAppeared(id: String) + case itemDisappeared(id: String) } struct RoomScreenViewState: BindableState { var roomTitle: String = "" - var messages: [RoomTimelineItem] = [] + var timelineItems: [RoomTimelineViewProvider] = [] } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index e4beb1c2c..c65a4d982 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -41,24 +41,28 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol super.init(initialViewState: RoomScreenViewState()) state.roomTitle = roomProxy.name ?? "" - state.messages = timelineController.timelineItems + state.timelineItems = timelineController.timelineItems timelineController.callbacks.sink { [weak self] callback in guard let self = self else { return } switch callback { case .updatedTimelineItems: - self.state.messages = timelineController.timelineItems + self.state.timelineItems = timelineController.timelineItems } }.store(in: &cancellables) } - + // MARK: - Public - + override func process(viewAction: RoomScreenViewAction) { switch viewAction { case .loadPreviousPage: timelineController.paginateBackwards(Constants.backPaginationPageSize) + case .itemAppeared: + break + case .itemDisappeared: + break } } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index afb86db70..4e3611bec 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -21,7 +21,7 @@ import Combine struct RoomScreen: View { @State private var scrollViewObserver: ScrollViewObserver = ScrollViewObserver() - @State private var messages: [RoomTimelineItem] = [] + @State private var timelineItems: [RoomTimelineViewProvider] = [] @State private var didRequestBackPagination = false @State private var hasPendingMessages = false @@ -58,8 +58,15 @@ struct RoomScreen: View { } } - ForEach(messages) { message in - message.body + ForEach(timelineItems) { timelineItem in + timelineItem + .listRowSeparator(.hidden) + .onAppear { + context.send(viewAction: .itemAppeared(id: timelineItem.id)) + } + .onDisappear { + context.send(viewAction: .itemDisappeared(id: timelineItem.id)) + } } Color.clear @@ -85,8 +92,8 @@ struct RoomScreen: View { // When the view state changes check whether the user is interacting with the scroll view. // Updating in that case causes undesired scrolling. Delay until the scroll view stops scrolling. // Also store previous top most message identifier to have something to scroll to after the update. - .onChange(of: context.viewState.messages) { newValue in - previousTopMostMessageIdentifier = messages.first?.id + .onChange(of: context.viewState.timelineItems) { newValue in + previousTopMostMessageIdentifier = timelineItems.first?.id wasBottomVisible = scrollViewObserver.isBottomVisible if scrollViewObserver.isTracking == true { @@ -94,17 +101,17 @@ struct RoomScreen: View { return } - messages = newValue + timelineItems = newValue } // Check if we have pending messages to apply and apply them when the scroll finishes scrolling .onReceive(scrollViewObserver.didEndScrolling, perform: { if hasPendingMessages { - messages = context.viewState.messages + timelineItems = context.viewState.timelineItems hasPendingMessages = false } }) // Process timeline updates - .onChange(of: messages, perform: { _ in + .onChange(of: timelineItems, perform: { _ in if didRequestBackPagination && wasBottomVisible { scrollViewProxy.scrollTo(timelineBottomAnchor, anchor: .bottom) } else if didRequestBackPagination == false { diff --git a/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift index 6a454640f..0da6b81a9 100644 --- a/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift @@ -10,12 +10,16 @@ import Foundation import Combine class MockRoomTimelineController: RoomTimelineControllerProtocol { - let timelineItems: [RoomTimelineItem] = [RoomTimelineItem.text(id: UUID().uuidString, senderDisplayName: "Anne", text: "You rock!", originServerTs: .now, shouldShowSenderDetails: true), - RoomTimelineItem.text(id: UUID().uuidString, senderDisplayName: "Anne", text: "Some other message from Anne", originServerTs: .now, shouldShowSenderDetails: false), - RoomTimelineItem.sectionTitle(id: UUID().uuidString, text: "The next day"), - RoomTimelineItem.text(id: UUID().uuidString, senderDisplayName: "Bob", text: "You rule!", originServerTs: .now, shouldShowSenderDetails: true)] + let callbacks = PassthroughSubject() + let 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))] + func paginateBackwards(_ count: UInt) { } diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift index f8f789079..f03c20b3e 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift @@ -14,20 +14,13 @@ enum RoomTimelineControllerCallback { case updatedTimelineItems } -private var sectionTitleDateFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .long - dateFormatter.timeStyle = .none - return dateFormatter -}() - class RoomTimelineController: RoomTimelineControllerProtocol { private let timelineProvider: RoomTimelineProvider private var cancellables = Set() let callbacks = PassthroughSubject() - private(set) var timelineItems = [RoomTimelineItem]() + private(set) var timelineItems = [RoomTimelineViewProvider]() init(timelineProvider: RoomTimelineProvider) { self.timelineProvider = timelineProvider @@ -37,32 +30,34 @@ class RoomTimelineController: RoomTimelineControllerProtocol { switch callback { case .updatedMessages: - var newTimelineItems = [RoomTimelineItem]() + var newTimelineItems = [RoomTimelineViewProvider]() var previousMessage: Message? - var previousSender: String? for message in self.timelineProvider.messages { let timestamp = Date(timeIntervalSince1970: TimeInterval(message.originServerTs())) let areMessagesFromTheSameDay = self.haveSameDay(lhs: previousMessage, rhs: message) -// let shouldAddSectionHeader = !areMessagesFromTheSameDay -// -// if shouldAddSectionHeader { -// newTimelineItems.append(RoomTimelineItem.sectionTitle(id: message.id(), -// text: sectionTitleDateFormatter.string(from: timestamp))) -// } + let shouldAddSectionHeader = !areMessagesFromTheSameDay - let areMessagesFromTheSameSender = previousSender == message.sender() + if shouldAddSectionHeader { + let item = SeparatorRoomTimelineItem(id: timestamp.ISO8601Format(), + text: timestamp.formatted(date: .long, time: .omitted)) + + newTimelineItems.append(RoomTimelineViewProvider.separator(item)) + } + + let areMessagesFromTheSameSender = (previousMessage?.sender() == message.sender()) let shouldShowSenderDetails = !areMessagesFromTheSameSender || !areMessagesFromTheSameDay - - newTimelineItems.append(RoomTimelineItem.text(id: message.id(), - senderDisplayName: message.sender(), - text: message.content(), - originServerTs: timestamp, - shouldShowSenderDetails: shouldShowSenderDetails)) + + let item = TextRoomTimelineItem(id: message.id(), + senderDisplayName: message.sender(), + text: message.content(), + timestamp: timestamp.formatted(date: .omitted, time: .shortened), + shouldShowSenderDetails: shouldShowSenderDetails) + + newTimelineItems.append(RoomTimelineViewProvider.text(item)) previousMessage = message - previousSender = message.sender() } self.timelineItems = newTimelineItems diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift index fd733340f..a71ef4a4d 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift @@ -10,7 +10,7 @@ import Foundation import Combine protocol RoomTimelineControllerProtocol { - var timelineItems: [RoomTimelineItem] { get } + var timelineItems: [RoomTimelineViewProvider] { get } var callbacks: PassthroughSubject { get } func paginateBackwards(_ count: UInt) diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/ImageRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/ImageRoomTimelineItem.swift new file mode 100644 index 000000000..1727fbbac --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItems/ImageRoomTimelineItem.swift @@ -0,0 +1,14 @@ +// +// ImageRoomTimelineItem.swift +// ElementX +// +// Created by Stefan Ceriu on 11/03/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation + +struct ImageRoomTimelineItem: Identifiable, Equatable { + let id: String + let text: String +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/ImageRoomTimelineView.swift b/ElementX/Sources/Services/Timeline/TimelineItems/ImageRoomTimelineView.swift new file mode 100644 index 000000000..0b858278e --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItems/ImageRoomTimelineView.swift @@ -0,0 +1,23 @@ +// +// ImageRoomTimelineView.swift +// ElementX +// +// Created by Stefan Ceriu on 11/03/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation +import SwiftUI + +struct ImageRoomTimelineView: View { + let timelineItem: ImageRoomTimelineItem + var loadedImage: UIImage? + + var body: some View { + if let loadedImage = loadedImage { + Image(uiImage: loadedImage) + } else { + ProgressView() + } + } +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItem.swift deleted file mode 100644 index 0c4552237..000000000 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItem.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// TextRoomTimelineItem.swift -// ElementX -// -// Created by Stefan Ceriu on 04.03.2022. -// Copyright © 2022 Element. All rights reserved. -// - -import Foundation -import SwiftUI - -private var dateFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .none - dateFormatter.timeStyle = .short - return dateFormatter -}() - -enum RoomTimelineItem: Identifiable, Equatable { - case text(id: String, senderDisplayName: String, text: String, originServerTs: Date, shouldShowSenderDetails: Bool) - case sectionTitle(id: String, text: String) - - var id: String { - switch self { - case .text(let id, _, _, _, _): - return id - case .sectionTitle(let id, _): - return id - } - } -} - -extension RoomTimelineItem: View { - var body: some View { - switch self { - case .text(let id, let senderDisplayName, let text, let originServerTs, let shouldShowSenderDetails): - VStack(alignment: .leading) { - if shouldShowSenderDetails { - HStack { - Text(senderDisplayName) - .font(.footnote) - .bold() - Spacer() - Text(dateFormatter.string(from: originServerTs)) - .font(.footnote) - } - Divider() - Spacer() - } - Text(text) - } - .listRowSeparator(.hidden) - .id(id) - case .sectionTitle(let id, let text): - LabelledDivider(label: text) - .id(id) - } - } -} - -struct LabelledDivider: View { - - let label: String - let color: Color - - init(label: String, color: Color = .gray) { - self.label = label - self.color = color - } - - var body: some View { - HStack { - line - Text(label) - .foregroundColor(color) - .fixedSize() - line - } - } - - var line: some View { - VStack { Divider().background(color) } - } -} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift new file mode 100644 index 000000000..1e6c095e7 --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift @@ -0,0 +1,40 @@ +// +// TextRoomTimelineItem.swift +// ElementX +// +// Created by Stefan Ceriu on 04.03.2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation +import SwiftUI + +enum RoomTimelineViewProvider: Identifiable, Equatable { + case text(TextRoomTimelineItem) + case separator(SeparatorRoomTimelineItem) + case image(ImageRoomTimelineItem) + + var id: String { + switch self { + case .text(let item): + return item.id + case .separator(let item): + return item.id + case .image(let item): + return item.id + } + } +} + +extension RoomTimelineViewProvider: View { + @ViewBuilder var body: some View { + switch self { + case .text(let item): + TextRoomTimelineView(timelineItem: item) + case .separator(let item): + SeparatorRoomTimelineView(timelineItem: item) + case .image(let item): + ImageRoomTimelineView(timelineItem: item) + } + } +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/SeparatorRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/SeparatorRoomTimelineItem.swift new file mode 100644 index 000000000..06d9bc88a --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItems/SeparatorRoomTimelineItem.swift @@ -0,0 +1,14 @@ +// +// SectionSeparatorTimelineItem.swift +// ElementX +// +// Created by Stefan Ceriu on 11/03/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation + +struct SeparatorRoomTimelineItem: Identifiable, Equatable { + let id: String + let text: String +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/SeparatorRoomTimelineView.swift b/ElementX/Sources/Services/Timeline/TimelineItems/SeparatorRoomTimelineView.swift new file mode 100644 index 000000000..67fb641b2 --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItems/SeparatorRoomTimelineView.swift @@ -0,0 +1,43 @@ +// +// SeparatorRoomTimelineView.swift +// ElementX +// +// Created by Stefan Ceriu on 11/03/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation +import SwiftUI + +struct SeparatorRoomTimelineView: View { + let timelineItem: SeparatorRoomTimelineItem + + var body: some View { + LabelledDivider(label: timelineItem.text) + .id(timelineItem.id) + } +} + +struct LabelledDivider: View { + let label: String + let color: Color + + init(label: String, color: Color = .gray) { + self.label = label + self.color = color + } + + var body: some View { + HStack { + line + Text(label) + .foregroundColor(color) + .fixedSize() + line + } + } + + var line: some View { + VStack { Divider().background(color) } + } +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/TextRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/TextRoomTimelineItem.swift new file mode 100644 index 000000000..366b02a34 --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItems/TextRoomTimelineItem.swift @@ -0,0 +1,17 @@ +// +// TextRoomTimelineItem.swift +// ElementX +// +// Created by Stefan Ceriu on 11/03/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation + +struct TextRoomTimelineItem: Identifiable, Equatable { + let id: String + let senderDisplayName: String + let text: String + let timestamp: String + let shouldShowSenderDetails: Bool +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/TextRoomTimelineView.swift b/ElementX/Sources/Services/Timeline/TimelineItems/TextRoomTimelineView.swift new file mode 100644 index 000000000..ef3695f97 --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItems/TextRoomTimelineView.swift @@ -0,0 +1,33 @@ +// +// TextRoomTimelineView.swift +// ElementX +// +// Created by Stefan Ceriu on 11/03/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation +import SwiftUI + +struct TextRoomTimelineView: View { + let timelineItem: TextRoomTimelineItem + + var body: some View { + VStack(alignment: .leading) { + if timelineItem.shouldShowSenderDetails { + HStack { + Text(timelineItem.senderDisplayName) + .font(.footnote) + .bold() + Spacer() + Text(timelineItem.timestamp) + .font(.footnote) + } + Divider() + Spacer() + } + Text(timelineItem.text) + } + .id(timelineItem.id) + } +}