Skip to content

Commit d413a67

Browse files
committed
Switched to discrete timeline items that directly expose view builders.
1 parent 8d4f6f3 commit d413a67

File tree

11 files changed

+175
-106
lines changed

11 files changed

+175
-106
lines changed

ElementX.xcodeproj/project.pbxproj

+4-10
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919
1859CF5527D7A6FF00E86E4E /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 1859CF5427D7A6FF00E86E4E /* MatrixRustSDK */; };
2020
1863A3FC27BA5A9100B52E4D /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 1863A3FB27BA5A9100B52E4D /* KeychainAccess */; };
2121
1863A40627BA6DFC00B52E4D /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = 1863A40527BA6DFC00B52E4D /* SwiftyBeaver */; };
22-
18A318DC27DA42C9000867CD /* RoomTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A318DA27DA42C9000867CD /* RoomTimelineItemProtocol.swift */; };
23-
18A318DD27DA42C9000867CD /* TextRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A318DB27DA42C9000867CD /* TextRoomTimelineItem.swift */; };
22+
18A318DD27DA42C9000867CD /* RoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A318DB27DA42C9000867CD /* RoomTimelineItem.swift */; };
2423
18F2BAD727D25B4000DD1988 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BA7327D25B4000DD1988 /* RoomProxyProtocol.swift */; };
2524
18F2BAD827D25B4000DD1988 /* RoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BA7427D25B4000DD1988 /* RoomProxy.swift */; };
2625
18F2BAD927D25B4000DD1988 /* MockRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2BA7527D25B4000DD1988 /* MockRoomProxy.swift */; };
@@ -112,9 +111,7 @@
112111
1850256727B6A135002E6B18 /* ElementX.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = ElementX.entitlements; sourceTree = "<group>"; };
113112
1850256827B6A135002E6B18 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
114113
1850256A27B6A135002E6B18 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
115-
18A318D827D9E7AD000867CD /* matrix-rust-components-swift */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "matrix-rust-components-swift"; path = "../matrix-rust-components-swift"; sourceTree = "<group>"; };
116-
18A318DA27DA42C9000867CD /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = "<group>"; };
117-
18A318DB27DA42C9000867CD /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = "<group>"; };
114+
18A318DB27DA42C9000867CD /* RoomTimelineItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomTimelineItem.swift; sourceTree = "<group>"; };
118115
18F2BA7327D25B4000DD1988 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = "<group>"; };
119116
18F2BA7427D25B4000DD1988 /* RoomProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = "<group>"; };
120117
18F2BA7527D25B4000DD1988 /* MockRoomProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockRoomProxy.swift; sourceTree = "<group>"; };
@@ -211,7 +208,6 @@
211208
1850251B27B6918C002E6B18 = {
212209
isa = PBXGroup;
213210
children = (
214-
18A318D827D9E7AD000867CD /* matrix-rust-components-swift */,
215211
1850252627B6918C002E6B18 /* ElementX */,
216212
1850253D27B6918D002E6B18 /* ElementXTests */,
217213
1850254727B6918D002E6B18 /* ElementXUITests */,
@@ -281,8 +277,7 @@
281277
18A318D927DA42C9000867CD /* TimelineItems */ = {
282278
isa = PBXGroup;
283279
children = (
284-
18A318DA27DA42C9000867CD /* RoomTimelineItemProtocol.swift */,
285-
18A318DB27DA42C9000867CD /* TextRoomTimelineItem.swift */,
280+
18A318DB27DA42C9000867CD /* RoomTimelineItem.swift */,
286281
);
287282
path = TimelineItems;
288283
sourceTree = "<group>";
@@ -742,7 +737,6 @@
742737
18F2BAFF27D25B4000DD1988 /* HomeScreenModels.swift in Sources */,
743738
18F2BB1527D25B4000DD1988 /* LoginScreenViewModelProtocol.swift in Sources */,
744739
18F2BAEB27D25B4000DD1988 /* LabelledActivityIndicatorView.swift in Sources */,
745-
18A318DC27DA42C9000867CD /* RoomTimelineItemProtocol.swift in Sources */,
746740
18F2BAE427D25B4000DD1988 /* Presentable.swift in Sources */,
747741
18F2BAF927D25B4000DD1988 /* SplashViewController.swift in Sources */,
748742
18F2BAE327D25B4000DD1988 /* RootRouter.swift in Sources */,
@@ -782,7 +776,7 @@
782776
18F2BB0127D25B4000DD1988 /* HomeScreenViewModel.swift in Sources */,
783777
18F2BAF027D25B4000DD1988 /* ActivityDismissal.swift in Sources */,
784778
18F2BADD27D25B4000DD1988 /* KeychainController.swift in Sources */,
785-
18A318DD27DA42C9000867CD /* TextRoomTimelineItem.swift in Sources */,
779+
18A318DD27DA42C9000867CD /* RoomTimelineItem.swift in Sources */,
786780
18F2BAFB27D25B4000DD1988 /* HomeScreenCoordinator.swift in Sources */,
787781
18F2BB0C27D25B4000DD1988 /* RoomScreenCoordinator.swift in Sources */,
788782
18F2BB0E27D25B4000DD1988 /* RoomScreenViewModelProtocol.swift in Sources */,

ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

+12-3
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,25 @@
1515
"repositoryURL": "https://github.com/onevcat/Kingfisher",
1616
"state": {
1717
"branch": null,
18-
"revision": "0c02c46cfdc0656ce74fd0963a75e5000a0b7f23",
19-
"version": "7.1.2"
18+
"revision": "32e4acdf6971f58f5ad552389cf2d7d016334eaf",
19+
"version": "7.2.0"
2020
}
2121
},
2222
{
2323
"package": "MatrixRustSDK",
2424
"repositoryURL": "https://github.com/matrix-org/matrix-rust-components-swift.git",
2525
"state": {
2626
"branch": "main",
27-
"revision": "497122432c79488e370df2164ae5637f32f82ca3",
27+
"revision": "6741f728fedbceb53154c043486dc1790ed37811",
28+
"version": null
29+
}
30+
},
31+
{
32+
"package": "Introspect",
33+
"repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git",
34+
"state": {
35+
"branch": "master",
36+
"revision": "72a509c93166540c0adf8323fd2652daade7f9f6",
2837
"version": null
2938
}
3039
},

ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift

+2-20
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,7 @@ enum RoomScreenViewAction {
2424
case loadPreviousPage
2525
}
2626

27-
private var dateFormatter: DateFormatter = {
28-
let dateFormatter = DateFormatter()
29-
dateFormatter.dateStyle = .short
30-
dateFormatter.timeStyle = .short
31-
return dateFormatter
32-
}()
33-
34-
struct RoomScreenMessage: Identifiable, Equatable {
35-
let id: String
36-
let sender: String
37-
let text: String
38-
let originServerTs: Date
39-
40-
var timestamp: String {
41-
dateFormatter.string(from: originServerTs)
42-
}
43-
}
44-
4527
struct RoomScreenViewState: BindableState {
46-
var roomTitle: String?
47-
var messages: [RoomScreenMessage] = []
28+
var roomTitle: String = ""
29+
var messages: [RoomTimelineItem] = []
4830
}

ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift

+3-11
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,15 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
4040

4141
super.init(initialViewState: RoomScreenViewState())
4242

43-
state.messages = buildRoomScreenMessages(timelineController.timelineItems)
43+
state.roomTitle = roomProxy.name ?? ""
44+
state.messages = timelineController.timelineItems
4445

4546
timelineController.callbacks.sink { [weak self] callback in
4647
guard let self = self else { return }
4748

4849
switch callback {
4950
case .updatedTimelineItems:
50-
self.state.messages = self.buildRoomScreenMessages(timelineController.timelineItems)
51+
self.state.messages = timelineController.timelineItems
5152
}
5253
}.store(in: &cancellables)
5354
}
@@ -60,13 +61,4 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
6061
timelineController.paginateBackwards(Constants.backPaginationPageSize)
6162
}
6263
}
63-
64-
// MARK: - Private
65-
66-
private func buildRoomScreenMessages(_ timelineItems: [RoomTimelineItemProtocol]) -> [RoomScreenMessage] {
67-
timelineItems.map { RoomScreenMessage(id: $0.id,
68-
sender: $0.senderDisplayName,
69-
text: $0.text,
70-
originServerTs: $0.originServerTs) }
71-
}
7264
}

ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift

+15-21
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import Combine
2121
struct RoomScreen: View {
2222

2323
@State private var scrollViewObserver: ScrollViewObserver = ScrollViewObserver()
24-
@State private var messages: [RoomScreenMessage] = []
24+
@State private var messages: [RoomTimelineItem] = []
2525

2626
@State private var didRequestBackPagination = false
2727
@State private var hasPendingMessages = false
@@ -37,16 +37,19 @@ struct RoomScreen: View {
3737
ScrollViewReader { scrollViewProxy in
3838
List {
3939
if didRequestBackPagination == false {
40-
Color
41-
.clear
42-
.onAppear {
43-
guard didRequestBackPagination == false else {
44-
return
45-
}
46-
47-
didRequestBackPagination = true
48-
context.send(viewAction: .loadPreviousPage)
40+
HStack {
41+
Spacer()
42+
ProgressView()
43+
Spacer()
44+
}
45+
.onAppear {
46+
guard didRequestBackPagination == false else {
47+
return
4948
}
49+
50+
didRequestBackPagination = true
51+
context.send(viewAction: .loadPreviousPage)
52+
}
5053
} else {
5154
HStack {
5255
Spacer()
@@ -56,24 +59,15 @@ struct RoomScreen: View {
5659
}
5760

5861
ForEach(messages) { message in
59-
VStack(alignment: .leading) {
60-
HStack {
61-
Text(message.sender)
62-
Spacer()
63-
Text(message.timestamp)
64-
}
65-
.font(.footnote)
66-
Text(message.text)
67-
}
68-
.listRowSeparator(.hidden)
69-
.id(message.id)
62+
message.body
7063
}
7164

7265
Color.clear
7366
.listRowSeparator(.hidden)
7467
.id(timelineBottomAnchor)
7568
}
7669
.listStyle(.plain)
70+
.navigationTitle(context.viewState.roomTitle)
7771
.environment(\.defaultMinListRowHeight, 0.0)
7872
.navigationBarTitleDisplayMode(.inline)
7973
// Fetch the underlying UIScrollView and start observing it

ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift

+4-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import Foundation
1010
import Combine
1111

1212
class MockRoomTimelineController: RoomTimelineControllerProtocol {
13-
let timelineItems: [RoomTimelineItemProtocol] = [TextRoomTimelineItem(id: UUID().uuidString, senderDisplayName: "Anne", text: "You rock!", originServerTs: .now),
14-
TextRoomTimelineItem(id: UUID().uuidString, senderDisplayName: "Bob", text: "You rule!", originServerTs: .now)]
13+
let timelineItems: [RoomTimelineItem] = [RoomTimelineItem.text(id: UUID().uuidString, senderDisplayName: "Anne", text: "You rock!", originServerTs: .now, shouldShowSenderDetails: true),
14+
RoomTimelineItem.text(id: UUID().uuidString, senderDisplayName: "Anne", text: "Some other message from Anne", originServerTs: .now, shouldShowSenderDetails: false),
15+
RoomTimelineItem.sectionTitle(id: UUID().uuidString, text: "The next day"),
16+
RoomTimelineItem.text(id: UUID().uuidString, senderDisplayName: "Bob", text: "You rule!", originServerTs: .now, shouldShowSenderDetails: true)]
1517
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
1618

1719
func paginateBackwards(_ count: UInt) {

ElementX/Sources/Services/Timeline/RoomTimelineController.swift

+50-6
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,20 @@ enum RoomTimelineControllerCallback {
1414
case updatedTimelineItems
1515
}
1616

17+
private var sectionTitleDateFormatter: DateFormatter = {
18+
let dateFormatter = DateFormatter()
19+
dateFormatter.dateStyle = .long
20+
dateFormatter.timeStyle = .none
21+
return dateFormatter
22+
}()
23+
1724
class RoomTimelineController: RoomTimelineControllerProtocol {
1825
private let timelineProvider: RoomTimelineProvider
1926
private var cancellables = Set<AnyCancellable>()
2027

2128
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
2229

23-
private(set) var timelineItems = [RoomTimelineItemProtocol]()
30+
private(set) var timelineItems = [RoomTimelineItem]()
2431

2532
init(timelineProvider: RoomTimelineProvider) {
2633
self.timelineProvider = timelineProvider
@@ -30,13 +37,36 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
3037

3138
switch callback {
3239
case .updatedMessages:
33-
self.timelineItems = self.timelineProvider.messages.map { message in
40+
var newTimelineItems = [RoomTimelineItem]()
41+
42+
var previousMessage: Message?
43+
var previousSender: String?
44+
for message in self.timelineProvider.messages {
3445
let timestamp = Date(timeIntervalSince1970: TimeInterval(message.originServerTs()))
35-
return TextRoomTimelineItem(id: message.id(),
36-
senderDisplayName: message.sender(),
37-
text: message.content(),
38-
originServerTs: timestamp)
46+
47+
let areMessagesFromTheSameDay = self.haveSameDay(lhs: previousMessage, rhs: message)
48+
// let shouldAddSectionHeader = !areMessagesFromTheSameDay
49+
//
50+
// if shouldAddSectionHeader {
51+
// newTimelineItems.append(RoomTimelineItem.sectionTitle(id: message.id(),
52+
// text: sectionTitleDateFormatter.string(from: timestamp)))
53+
// }
54+
55+
let areMessagesFromTheSameSender = previousSender == message.sender()
56+
let shouldShowSenderDetails = !areMessagesFromTheSameSender || !areMessagesFromTheSameDay
57+
58+
newTimelineItems.append(RoomTimelineItem.text(id: message.id(),
59+
senderDisplayName: message.sender(),
60+
text: message.content(),
61+
originServerTs: timestamp,
62+
shouldShowSenderDetails: shouldShowSenderDetails))
63+
64+
previousMessage = message
65+
previousSender = message.sender()
3966
}
67+
68+
self.timelineItems = newTimelineItems
69+
4070
self.callbacks.send(.updatedTimelineItems)
4171
}
4272
}.store(in: &cancellables)
@@ -45,4 +75,18 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
4575
func paginateBackwards(_ count: UInt) {
4676
timelineProvider.paginateBackwards(count)
4777
}
78+
79+
// MARK: - Private
80+
81+
private func haveSameDay(lhs: Message?, rhs: Message?) -> Bool {
82+
guard let lhs = lhs, let rhs = rhs else {
83+
return false
84+
}
85+
86+
let lhsTimestamp = Date(timeIntervalSince1970: TimeInterval(lhs.originServerTs()))
87+
let rhsTimestamp = Date(timeIntervalSince1970: TimeInterval(rhs.originServerTs()))
88+
89+
return Calendar.current.isDate(lhsTimestamp, inSameDayAs: rhsTimestamp)
90+
91+
}
4892
}

ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010
import Combine
1111

1212
protocol RoomTimelineControllerProtocol {
13-
var timelineItems: [RoomTimelineItemProtocol] { get }
13+
var timelineItems: [RoomTimelineItem] { get }
1414
var callbacks: PassthroughSubject<RoomTimelineControllerCallback, Never> { get }
1515

1616
func paginateBackwards(_ count: UInt)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//
2+
// TextRoomTimelineItem.swift
3+
// ElementX
4+
//
5+
// Created by Stefan Ceriu on 04.03.2022.
6+
// Copyright © 2022 Element. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import SwiftUI
11+
12+
private var dateFormatter: DateFormatter = {
13+
let dateFormatter = DateFormatter()
14+
dateFormatter.dateStyle = .none
15+
dateFormatter.timeStyle = .short
16+
return dateFormatter
17+
}()
18+
19+
enum RoomTimelineItem: Identifiable, Equatable {
20+
case text(id: String, senderDisplayName: String, text: String, originServerTs: Date, shouldShowSenderDetails: Bool)
21+
case sectionTitle(id: String, text: String)
22+
23+
var id: String {
24+
switch self {
25+
case .text(let id, _, _, _, _):
26+
return id
27+
case .sectionTitle(let id, _):
28+
return id
29+
}
30+
}
31+
}
32+
33+
extension RoomTimelineItem: View {
34+
var body: some View {
35+
switch self {
36+
case .text(let id, let senderDisplayName, let text, let originServerTs, let shouldShowSenderDetails):
37+
VStack(alignment: .leading) {
38+
if shouldShowSenderDetails {
39+
HStack {
40+
Text(senderDisplayName)
41+
.font(.footnote)
42+
.bold()
43+
Spacer()
44+
Text(dateFormatter.string(from: originServerTs))
45+
.font(.footnote)
46+
}
47+
Divider()
48+
Spacer()
49+
}
50+
Text(text)
51+
}
52+
.listRowSeparator(.hidden)
53+
.id(id)
54+
case .sectionTitle(let id, let text):
55+
LabelledDivider(label: text)
56+
.id(id)
57+
}
58+
}
59+
}
60+
61+
struct LabelledDivider: View {
62+
63+
let label: String
64+
let color: Color
65+
66+
init(label: String, color: Color = .gray) {
67+
self.label = label
68+
self.color = color
69+
}
70+
71+
var body: some View {
72+
HStack {
73+
line
74+
Text(label)
75+
.foregroundColor(color)
76+
.fixedSize()
77+
line
78+
}
79+
}
80+
81+
var line: some View {
82+
VStack { Divider().background(color) }
83+
}
84+
}

0 commit comments

Comments
 (0)