mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Switched to discrete timeline items that directly expose view builders.
This commit is contained in:
@@ -24,25 +24,7 @@ enum RoomScreenViewAction {
|
||||
case loadPreviousPage
|
||||
}
|
||||
|
||||
private var dateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .short
|
||||
dateFormatter.timeStyle = .short
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
struct RoomScreenMessage: Identifiable, Equatable {
|
||||
let id: String
|
||||
let sender: String
|
||||
let text: String
|
||||
let originServerTs: Date
|
||||
|
||||
var timestamp: String {
|
||||
dateFormatter.string(from: originServerTs)
|
||||
}
|
||||
}
|
||||
|
||||
struct RoomScreenViewState: BindableState {
|
||||
var roomTitle: String?
|
||||
var messages: [RoomScreenMessage] = []
|
||||
var roomTitle: String = ""
|
||||
var messages: [RoomTimelineItem] = []
|
||||
}
|
||||
|
||||
@@ -40,14 +40,15 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
|
||||
super.init(initialViewState: RoomScreenViewState())
|
||||
|
||||
state.messages = buildRoomScreenMessages(timelineController.timelineItems)
|
||||
state.roomTitle = roomProxy.name ?? ""
|
||||
state.messages = timelineController.timelineItems
|
||||
|
||||
timelineController.callbacks.sink { [weak self] callback in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch callback {
|
||||
case .updatedTimelineItems:
|
||||
self.state.messages = self.buildRoomScreenMessages(timelineController.timelineItems)
|
||||
self.state.messages = timelineController.timelineItems
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
@@ -60,13 +61,4 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
timelineController.paginateBackwards(Constants.backPaginationPageSize)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func buildRoomScreenMessages(_ timelineItems: [RoomTimelineItemProtocol]) -> [RoomScreenMessage] {
|
||||
timelineItems.map { RoomScreenMessage(id: $0.id,
|
||||
sender: $0.senderDisplayName,
|
||||
text: $0.text,
|
||||
originServerTs: $0.originServerTs) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import Combine
|
||||
struct RoomScreen: View {
|
||||
|
||||
@State private var scrollViewObserver: ScrollViewObserver = ScrollViewObserver()
|
||||
@State private var messages: [RoomScreenMessage] = []
|
||||
@State private var messages: [RoomTimelineItem] = []
|
||||
|
||||
@State private var didRequestBackPagination = false
|
||||
@State private var hasPendingMessages = false
|
||||
@@ -37,16 +37,19 @@ struct RoomScreen: View {
|
||||
ScrollViewReader { scrollViewProxy in
|
||||
List {
|
||||
if didRequestBackPagination == false {
|
||||
Color
|
||||
.clear
|
||||
.onAppear {
|
||||
guard didRequestBackPagination == false else {
|
||||
return
|
||||
}
|
||||
|
||||
didRequestBackPagination = true
|
||||
context.send(viewAction: .loadPreviousPage)
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
Spacer()
|
||||
}
|
||||
.onAppear {
|
||||
guard didRequestBackPagination == false else {
|
||||
return
|
||||
}
|
||||
|
||||
didRequestBackPagination = true
|
||||
context.send(viewAction: .loadPreviousPage)
|
||||
}
|
||||
} else {
|
||||
HStack {
|
||||
Spacer()
|
||||
@@ -56,17 +59,7 @@ struct RoomScreen: View {
|
||||
}
|
||||
|
||||
ForEach(messages) { message in
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text(message.sender)
|
||||
Spacer()
|
||||
Text(message.timestamp)
|
||||
}
|
||||
.font(.footnote)
|
||||
Text(message.text)
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
.id(message.id)
|
||||
message.body
|
||||
}
|
||||
|
||||
Color.clear
|
||||
@@ -74,6 +67,7 @@ struct RoomScreen: View {
|
||||
.id(timelineBottomAnchor)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.navigationTitle(context.viewState.roomTitle)
|
||||
.environment(\.defaultMinListRowHeight, 0.0)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
// Fetch the underlying UIScrollView and start observing it
|
||||
|
||||
@@ -10,8 +10,10 @@ import Foundation
|
||||
import Combine
|
||||
|
||||
class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
||||
let timelineItems: [RoomTimelineItemProtocol] = [TextRoomTimelineItem(id: UUID().uuidString, senderDisplayName: "Anne", text: "You rock!", originServerTs: .now),
|
||||
TextRoomTimelineItem(id: UUID().uuidString, senderDisplayName: "Bob", text: "You rule!", originServerTs: .now)]
|
||||
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<RoomTimelineControllerCallback, Never>()
|
||||
|
||||
func paginateBackwards(_ count: UInt) {
|
||||
|
||||
@@ -14,13 +14,20 @@ 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<AnyCancellable>()
|
||||
|
||||
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
|
||||
|
||||
private(set) var timelineItems = [RoomTimelineItemProtocol]()
|
||||
private(set) var timelineItems = [RoomTimelineItem]()
|
||||
|
||||
init(timelineProvider: RoomTimelineProvider) {
|
||||
self.timelineProvider = timelineProvider
|
||||
@@ -30,13 +37,36 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
|
||||
switch callback {
|
||||
case .updatedMessages:
|
||||
self.timelineItems = self.timelineProvider.messages.map { message in
|
||||
var newTimelineItems = [RoomTimelineItem]()
|
||||
|
||||
var previousMessage: Message?
|
||||
var previousSender: String?
|
||||
for message in self.timelineProvider.messages {
|
||||
let timestamp = Date(timeIntervalSince1970: TimeInterval(message.originServerTs()))
|
||||
return TextRoomTimelineItem(id: message.id(),
|
||||
senderDisplayName: message.sender(),
|
||||
text: message.content(),
|
||||
originServerTs: timestamp)
|
||||
|
||||
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 areMessagesFromTheSameSender = previousSender == message.sender()
|
||||
let shouldShowSenderDetails = !areMessagesFromTheSameSender || !areMessagesFromTheSameDay
|
||||
|
||||
newTimelineItems.append(RoomTimelineItem.text(id: message.id(),
|
||||
senderDisplayName: message.sender(),
|
||||
text: message.content(),
|
||||
originServerTs: timestamp,
|
||||
shouldShowSenderDetails: shouldShowSenderDetails))
|
||||
|
||||
previousMessage = message
|
||||
previousSender = message.sender()
|
||||
}
|
||||
|
||||
self.timelineItems = newTimelineItems
|
||||
|
||||
self.callbacks.send(.updatedTimelineItems)
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
@@ -45,4 +75,18 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
func paginateBackwards(_ count: UInt) {
|
||||
timelineProvider.paginateBackwards(count)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func haveSameDay(lhs: Message?, rhs: Message?) -> Bool {
|
||||
guard let lhs = lhs, let rhs = rhs else {
|
||||
return false
|
||||
}
|
||||
|
||||
let lhsTimestamp = Date(timeIntervalSince1970: TimeInterval(lhs.originServerTs()))
|
||||
let rhsTimestamp = Date(timeIntervalSince1970: TimeInterval(rhs.originServerTs()))
|
||||
|
||||
return Calendar.current.isDate(lhsTimestamp, inSameDayAs: rhsTimestamp)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
import Combine
|
||||
|
||||
protocol RoomTimelineControllerProtocol {
|
||||
var timelineItems: [RoomTimelineItemProtocol] { get }
|
||||
var timelineItems: [RoomTimelineItem] { get }
|
||||
var callbacks: PassthroughSubject<RoomTimelineControllerCallback, Never> { get }
|
||||
|
||||
func paginateBackwards(_ count: UInt)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// 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) }
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
//
|
||||
// RoomTimelineItemProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 04.03.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol RoomTimelineItemProtocol {
|
||||
var id: String { get }
|
||||
var senderDisplayName: String { get }
|
||||
var text: String { get }
|
||||
var originServerTs: Date { get }
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
//
|
||||
// TextRoomTimelineItem.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 04.03.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TextRoomTimelineItem: RoomTimelineItemProtocol {
|
||||
let id: String
|
||||
let senderDisplayName: String
|
||||
let text: String
|
||||
let originServerTs: Date
|
||||
}
|
||||
Reference in New Issue
Block a user