Reverting to TableView but with SwiftUI compatibility (#1407)

* revert but also allows us to keep the existing SwiftUI implementation if we need it

* code improvement

* pr review

* FF is volatile

* message that communicates that is volatile
This commit is contained in:
Mauro
2023-07-26 15:53:43 +02:00
committed by GitHub
parent ba6ad3236f
commit 99b5bf5150
12 changed files with 497 additions and 37 deletions
@@ -34,6 +34,7 @@ final class AppSettings {
case readReceiptsEnabled
case hasShownWelcomeScreen
case notificationSettingsEnabled
case swiftUITimelineEnabled
}
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
@@ -213,4 +214,7 @@ final class AppSettings {
@UserPreference(key: UserDefaultsKeys.notificationSettingsEnabled, defaultValue: false, storageType: .userDefaults(store))
var notificationSettingsEnabled
@UserPreference(key: UserDefaultsKeys.swiftUITimelineEnabled, defaultValue: false, storageType: .volatile)
var swiftUITimelineEnabled
}
@@ -61,6 +61,7 @@ enum RoomScreenViewAction {
case cancelEdit
/// Mark the entire room as read - this is heavy handed as a starting point for now.
case markRoomAsRead
case paginateBackwards
case timelineItemMenu(itemID: TimelineItemIdentifier)
case timelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction)
@@ -92,6 +93,7 @@ struct RoomScreenViewState: BindableState {
var isEncryptedOneToOneRoom = false
var timelineViewState = TimelineViewState() // check the doc before changing this
var composerMode: RoomScreenComposerMode = .default
var swiftUITimelineEnabled = false
var bindings: RoomScreenViewStateBindings
@@ -177,6 +179,10 @@ struct RoomMemberState {
struct TimelineViewState {
var canBackPaginate = true
var isBackPaginating = false
// These can be removed when we have full swiftUI and moved as @State values in the view
var scrollToBottomPublisher = PassthroughSubject<Void, Never>()
var itemsDictionary = OrderedDictionary<String, RoomTimelineItemViewState>()
var timelineIDs: [String] {
@@ -186,6 +192,4 @@ struct TimelineViewState {
var itemViewStates: [RoomTimelineItemViewState] {
itemsDictionary.values.elements
}
var paginateAction: (() -> Void)?
}
@@ -61,11 +61,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom,
bindings: .init(composerText: "", composerFocused: false, reactionsCollapsed: [:])),
imageProvider: mediaProvider)
state.timelineViewState.paginateAction = { [weak self] in
self?.paginateBackwards()
}
setupSubscriptions()
state.timelineItemMenuActionProvider = { [weak self] itemId -> TimelineItemMenuActions? in
@@ -140,12 +136,18 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
Task { await handleRetrySend(itemID: itemID) }
case .cancelSend(let itemID):
Task { await handleCancelSend(itemID: itemID) }
case .paginateBackwards:
paginateBackwards()
}
}
// MARK: - Private
private func setupSubscriptions() {
appSettings.$swiftUITimelineEnabled
.weakAssign(to: \.state.swiftUITimelineEnabled, on: self)
.store(in: &cancellables)
timelineController.callbacks
.receive(on: DispatchQueue.main)
.sink { [weak self] callback in
@@ -274,19 +276,19 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
if itemGroup.count == 1 {
if let firstItem = itemGroup.first {
timelineItemsDictionary.updateValue(RoomTimelineItemViewState(item: firstItem, groupStyle: .single),
timelineItemsDictionary.updateValue(updateViewstate(item: firstItem, groupStyle: .single),
forKey: firstItem.id.timelineID)
}
} else {
for (index, item) in itemGroup.enumerated() {
if index == 0 {
timelineItemsDictionary.updateValue(RoomTimelineItemViewState(item: item, groupStyle: .first),
timelineItemsDictionary.updateValue(updateViewstate(item: item, groupStyle: .first),
forKey: item.id.timelineID)
} else if index == itemGroup.count - 1 {
timelineItemsDictionary.updateValue(RoomTimelineItemViewState(item: item, groupStyle: .last),
timelineItemsDictionary.updateValue(updateViewstate(item: item, groupStyle: .last),
forKey: item.id.timelineID)
} else {
timelineItemsDictionary.updateValue(RoomTimelineItemViewState(item: item, groupStyle: .middle),
timelineItemsDictionary.updateValue(updateViewstate(item: item, groupStyle: .middle),
forKey: item.id.timelineID)
}
}
@@ -296,6 +298,16 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.timelineViewState.itemsDictionary = timelineItemsDictionary
}
private func updateViewstate(item: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) -> RoomTimelineItemViewState {
if let timelineItemViewState = state.timelineViewState.itemsDictionary[item.id.timelineID] {
timelineItemViewState.groupStyle = groupStyle
timelineItemViewState.type = .init(item: item)
return timelineItemViewState
} else {
return RoomTimelineItemViewState(item: item, groupStyle: groupStyle)
}
}
private func canGroupItem(timelineItem: RoomTimelineItemProtocol, with otherTimelineItem: RoomTimelineItemProtocol) -> Bool {
if timelineItem is CollapsibleTimelineItem || otherTimelineItem is CollapsibleTimelineItem {
return false
@@ -83,12 +83,48 @@ struct RoomScreen: View {
}
private var timeline: some View {
TimelineView(viewState: context.viewState.timelineViewState)
timelineSwitch
.id(context.viewState.roomId)
.environmentObject(context)
.environment(\.timelineStyle, context.viewState.timelineStyle)
.environment(\.readReceiptsEnabled, context.viewState.readReceiptsEnabled)
}
@ViewBuilder
private var timelineSwitch: some View {
if context.viewState.swiftUITimelineEnabled {
TimelineView(viewState: context.viewState.timelineViewState,
scrollToBottomButtonVisible: $context.scrollToBottomButtonVisible) {
context.send(viewAction: .paginateBackwards)
}
} else {
UITimelineView()
.overlay(alignment: .bottomTrailing) {
scrollToBottomButton
}
}
}
private var scrollToBottomButton: some View {
Button { context.viewState.timelineViewState.scrollToBottomPublisher.send(()) } label: {
Image(systemName: "chevron.down")
.font(.compound.bodyLG)
.fontWeight(.semibold)
.foregroundColor(.compound.iconSecondary)
.padding(13)
.offset(y: 1)
.background {
Circle()
.fill(Color.compound.iconOnSolidPrimary)
// Intentionally using system primary colour to get white/black.
.shadow(color: .primary.opacity(0.33), radius: 2.0)
}
.padding()
}
.opacity(context.scrollToBottomButtonVisible ? 1.0 : 0.0)
.accessibilityHidden(!context.scrollToBottomButtonVisible)
.animation(.elementDefault, value: context.scrollToBottomButtonVisible)
}
private var messageComposer: some View {
MessageComposer(text: $context.composerText,
@@ -0,0 +1,283 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
import OrderedCollections
/// A table view cell that displays a timeline item in a room. The cell is intended
/// to be configured to display a SwiftUI view and not use any UIKit.
class TimelineItemCell: UITableViewCell {
static let reuseIdentifier = "TimelineItemCell"
var item: RoomTimelineItemViewState?
override func prepareForReuse() {
item = nil
}
}
/// A table view controller that displays the timeline of a room.
///
/// This class subclasses `UIViewController` as `UITableViewController` adds some
/// extra keyboard handling magic that wasn't playing well with SwiftUI (as of iOS 16.1).
/// Also this TableViewController uses a **flipped tableview**
class TimelineTableViewController: UIViewController {
private let coordinator: UITimelineView.Coordinator
private let tableView = UITableView(frame: .zero, style: .plain)
var timelineStyle: TimelineStyle
var timelineItemsDictionary = OrderedDictionary<String, RoomTimelineItemViewState>() {
didSet {
applySnapshot()
if timelineItemsDictionary.isEmpty {
paginateBackwardsPublisher.send()
}
}
}
/// The mode of the message composer. This is used to render selected
/// items in the timeline when replying, editing etc.
var composerMode: RoomScreenComposerMode = .default
/// Whether or not the timeline has more messages to back paginate.
var canBackPaginate = true
/// Whether or not the timeline is waiting for more messages to be added to the top.
var isBackPaginating = false {
didSet {
// Paginate again if the threshold hasn't been satisfied.
paginateBackwardsPublisher.send(())
}
}
var contextMenuActionProvider: (@MainActor (_ itemID: TimelineItemIdentifier) -> TimelineItemMenuActions?)?
@Binding private var scrollToBottomButtonVisible: Bool
private var timelineItemsIDs: [String] {
timelineItemsDictionary.keys.elements.reversed()
}
/// The table's diffable data source.
private var dataSource: UITableViewDiffableDataSource<TimelineSection, String>?
private var cancellables: Set<AnyCancellable> = []
/// A publisher used to throttle back pagination requests.
///
/// Our view actions get wrapped in a `Task` so it is possible that a second call in
/// quick succession can execute before ``isBackPaginating`` becomes `true`.
private let paginateBackwardsPublisher = PassthroughSubject<Void, Never>()
/// Whether or not the view has been shown on screen yet.
private var hasAppearedOnce = false
/// Whether the scroll and the animations should happen
private var shouldAnimate = false
init(coordinator: UITimelineView.Coordinator,
timelineStyle: TimelineStyle,
scrollToBottomButtonVisible: Binding<Bool>,
scrollToBottomPublisher: PassthroughSubject<Void, Never>) {
self.coordinator = coordinator
self.timelineStyle = timelineStyle
_scrollToBottomButtonVisible = scrollToBottomButtonVisible
super.init(nibName: nil, bundle: nil)
tableView.register(TimelineItemCell.self, forCellReuseIdentifier: TimelineItemCell.reuseIdentifier)
tableView.separatorStyle = .none
tableView.allowsSelection = false
tableView.keyboardDismissMode = .onDrag
tableView.backgroundColor = UIColor(.compound.bgCanvasDefault)
tableView.transform = CGAffineTransform(scaleX: 1, y: -1)
view.addSubview(tableView)
// Prevents XCUITest from invoking the diffable dataSource's cellProvider
// for each possible cell, causing layout issues
tableView.accessibilityElementsHidden = Tests.shouldDisableTimelineAccessibility
scrollToBottomPublisher
.sink { [weak self] _ in
self?.scrollToBottom(animated: true)
}
.store(in: &cancellables)
paginateBackwardsPublisher
.collect(.byTime(DispatchQueue.main, 0.1))
.sink { [weak self] _ in
self?.paginateBackwardsIfNeeded()
}
.store(in: &cancellables)
configureDataSource()
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not available.") }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard !hasAppearedOnce else { return }
tableView.contentOffset.y = -1
hasAppearedOnce = true
paginateBackwardsPublisher.send()
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.shouldAnimate = true
}
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
guard tableView.frame.size != view.frame.size else {
return
}
tableView.frame = CGRect(origin: .zero, size: view.frame.size)
}
/// Configures a diffable data source for the timeline's table view.
private func configureDataSource() {
dataSource = .init(tableView: tableView) { [weak self] tableView, indexPath, id in
let cell = tableView.dequeueReusableCell(withIdentifier: TimelineItemCell.reuseIdentifier, for: indexPath)
guard let self, let cell = cell as? TimelineItemCell else { return cell }
// A local reference to avoid capturing self in the cell configuration.
let coordinator = self.coordinator
let viewState = timelineItemsDictionary[id]
cell.item = viewState
guard let viewState else {
return cell
}
cell.contentConfiguration = UIHostingConfiguration {
RoomTimelineItemView(viewState: viewState)
.id(id)
.frame(maxWidth: .infinity, alignment: .leading)
.environmentObject(coordinator.context) // Attempted fix at a crash in TimelineItemContextMenu
.onAppear {
coordinator.send(viewAction: .itemAppeared(itemID: viewState.identifier))
}
.onDisappear {
coordinator.send(viewAction: .itemDisappeared(itemID: viewState.identifier))
}
.environment(\.openURL, OpenURLAction { url in
coordinator.send(viewAction: .linkClicked(url: url))
return .systemAction
})
}
.margins(.all, self.timelineStyle.rowInsets)
.minSize(height: 1)
.background(Color.clear)
// Flipping the cell can create some issues with cell resizing, so flip the content View
cell.contentView.transform = CGAffineTransform(scaleX: 1, y: -1)
return cell
}
dataSource?.defaultRowAnimation = .fade
tableView.delegate = self
}
/// Updates the table view with the latest items from the ``timelineItems`` array. After
/// updating the data, the table will be scrolled to the bottom if it was visible otherwise
/// the scroll position will be updated to maintain the position of the last visible item.
private func applySnapshot() {
guard let dataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, String>()
snapshot.appendSections([.main])
snapshot.appendItems(timelineItemsIDs)
let currentSnapshot = dataSource.snapshot()
MXLog.verbose("DIFF: \(snapshot.itemIdentifiers.difference(from: currentSnapshot.itemIdentifiers))")
// We only animate when new items come at the end of the timeline
let animated = shouldAnimate &&
snapshot.itemIdentifiers.first != currentSnapshot.itemIdentifiers.first
dataSource.apply(snapshot, animatingDifferences: animated)
}
/// Scrolls to the bottom of the timeline.
private func scrollToBottom(animated: Bool) {
guard !timelineItemsIDs.isEmpty else {
return
}
tableView.scrollToRow(at: IndexPath(item: 0, section: 0), at: .top, animated: animated)
}
/// Scrolls to the top of the timeline.
private func scrollToTop(animated: Bool) {
guard !timelineItemsIDs.isEmpty else {
return
}
tableView.scrollToRow(at: IndexPath(item: timelineItemsIDs.count - 1, section: 0), at: .bottom, animated: animated)
}
/// Checks whether or a backwards pagination is needed and requests one if so.
///
/// Prefer not to call this directly, instead using ``paginateBackwardsPublisher`` to throttle requests.
private func paginateBackwardsIfNeeded() {
guard canBackPaginate,
!isBackPaginating,
tableView.contentOffset.y > tableView.contentSize.height - tableView.visibleSize.height * 2.0
else { return }
coordinator.send(viewAction: .paginateBackwards)
}
}
// MARK: - UITableViewDelegate
extension TimelineTableViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
paginateBackwardsPublisher.send(())
// Dispatch to fix runtime warning about making changes during a view update.
DispatchQueue.main.async { [weak self] in
guard let self else { return }
let scrollToBottomButtonVisible = scrollView.contentOffset.y > 0
// Only update the binding on changes to avoid needlessly recomputing the hierarchy when scrolling.
if self.scrollToBottomButtonVisible != scrollToBottomButtonVisible {
self.scrollToBottomButtonVisible = scrollToBottomButtonVisible
}
}
// We never want the table view to be fully at the bottom to allow the status bar tap to work properly
if scrollView.contentOffset.y == 0 {
scrollView.contentOffset.y = -1
}
}
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
scrollToTop(animated: true)
return false
}
}
// MARK: - Layout Types
extension TimelineTableViewController {
/// The sections of the table view used in the diffable data source.
enum TimelineSection {
case main
}
}
@@ -0,0 +1,97 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
/// A table view wrapper that displays the timeline of a room.
struct UITimelineView: UIViewControllerRepresentable {
@EnvironmentObject private var viewModelContext: RoomScreenViewModel.Context
@Environment(\.timelineStyle) private var timelineStyle
func makeUIViewController(context: Context) -> TimelineTableViewController {
let tableViewController = TimelineTableViewController(coordinator: context.coordinator,
timelineStyle: timelineStyle,
scrollToBottomButtonVisible: $viewModelContext.scrollToBottomButtonVisible,
scrollToBottomPublisher: viewModelContext.viewState.timelineViewState.scrollToBottomPublisher)
return tableViewController
}
func updateUIViewController(_ uiViewController: TimelineTableViewController, context: Context) {
context.coordinator.update(tableViewController: uiViewController, timelineStyle: timelineStyle)
}
func makeCoordinator() -> Coordinator {
Coordinator(viewModelContext: viewModelContext)
}
// MARK: - Coordinator
@MainActor
class Coordinator {
let context: RoomScreenViewModel.Context
init(viewModelContext: RoomScreenViewModel.Context) {
context = viewModelContext
if viewModelContext.viewState.timelineViewState.itemViewStates.isEmpty {
viewModelContext.send(viewAction: .paginateBackwards)
}
}
/// Updates the specified table view's properties from the current view state.
func update(tableViewController: TimelineTableViewController, timelineStyle: TimelineStyle) {
if tableViewController.timelineStyle != timelineStyle {
tableViewController.timelineStyle = timelineStyle
}
if tableViewController.timelineItemsDictionary != context.viewState.timelineViewState.itemsDictionary {
tableViewController.timelineItemsDictionary = context.viewState.timelineViewState.itemsDictionary
}
if tableViewController.canBackPaginate != context.viewState.timelineViewState.canBackPaginate {
tableViewController.canBackPaginate = context.viewState.timelineViewState.canBackPaginate
}
if tableViewController.isBackPaginating != context.viewState.timelineViewState.isBackPaginating {
tableViewController.isBackPaginating = context.viewState.timelineViewState.isBackPaginating
}
if tableViewController.composerMode != context.viewState.composerMode {
tableViewController.composerMode = context.viewState.composerMode
}
// Doesn't have an equatable conformance :(
tableViewController.contextMenuActionProvider = context.viewState.timelineItemMenuActionProvider
}
func send(viewAction: RoomScreenViewAction) {
context.send(viewAction: viewAction)
}
}
}
// MARK: - Previews
struct UITimelineView_Previews: PreviewProvider {
static let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
roomProxy: RoomProxyMock(with: .init(displayName: "Preview room")),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
static var previews: some View {
NavigationStack {
RoomScreen(context: viewModel.context)
}
}
}
@@ -21,6 +21,9 @@ import SwiftUIIntrospect
struct TimelineView: View {
let viewState: TimelineViewState
@Binding var scrollToBottomButtonVisible: Bool
let paginationAction: () -> Void
@Environment(\.timelineStyle) private var timelineStyle
private let bottomID = "RoomTimelineBottomPinIdentifier"
@@ -28,8 +31,6 @@ struct TimelineView: View {
@State private var scrollViewAdapter = ScrollViewAdapter()
@State private var paginateBackwardsPublisher = PassthroughSubject<Void, Never>()
@State private var scrollToBottomPublisher = PassthroughSubject<Void, Never>()
@State private var scrollToBottomButtonVisible = false
var body: some View {
ScrollViewReader { scrollView in
@@ -49,9 +50,10 @@ struct TimelineView: View {
// Allows the scroll to top to work properly
uiScrollView.contentOffset.y -= 1
paginateBackwardsPublisher.send()
}
.scaleEffect(x: 1, y: -1)
.onReceive(scrollToBottomPublisher) { _ in
.onReceive(viewState.scrollToBottomPublisher) { _ in
withElementAnimation {
scrollView.scrollTo(bottomID)
}
@@ -59,7 +61,7 @@ struct TimelineView: View {
.scrollDismissesKeyboard(.immediately)
}
.overlay(scrollToBottomButton, alignment: .bottomTrailing)
.animation(.elementDefault, value: viewState.itemViewStates)
.animation(.elementDefault, value: viewState.timelineIDs)
.onReceive(scrollViewAdapter.didScroll) { _ in
guard let scrollView = scrollViewAdapter.scrollView else {
return
@@ -120,7 +122,7 @@ struct TimelineView: View {
private var scrollToBottomButton: some View {
Button {
scrollToBottomPublisher.send()
viewState.scrollToBottomPublisher.send()
} label: {
Image(systemName: "chevron.down")
.font(.compound.bodyLG)
@@ -142,8 +144,7 @@ struct TimelineView: View {
}
private func paginateBackwardsIfNeeded() {
guard let paginateAction = viewState.paginateAction,
let scrollView = scrollViewAdapter.scrollView,
guard let scrollView = scrollViewAdapter.scrollView,
viewState.canBackPaginate,
!viewState.isBackPaginating else {
return
@@ -158,20 +159,20 @@ struct TimelineView: View {
return
}
paginateAction()
paginationAction()
}
}
// MARK: - Previews
struct TimelineTableView_Previews: PreviewProvider {
struct TimelineView_Previews: PreviewProvider {
static let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
roomProxy: RoomProxyMock(with: .init(displayName: "Preview room")),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
static var previews: some View {
NavigationStack {
RoomScreen(context: viewModel.context)
@@ -47,6 +47,7 @@ protocol DeveloperOptionsProtocol: AnyObject {
var userSuggestionsEnabled: Bool { get set }
var readReceiptsEnabled: Bool { get set }
var notificationSettingsEnabled: Bool { get set }
var swiftUITimelineEnabled: Bool { get set }
}
extension AppSettings: DeveloperOptionsProtocol { }
@@ -31,6 +31,11 @@ struct DeveloperOptionsScreen: View {
Text("Show read receipts")
Text("Requires app reboot")
}
Toggle(isOn: $context.swiftUITimelineEnabled) {
Text("SwiftUI Timeline")
Text("Resets on reboot")
}
}
Section("Notifications") {
@@ -17,10 +17,12 @@ import SwiftUI
struct RoomTimelineItemView: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context
let viewState: RoomTimelineItemViewState
@ObservedObject var viewState: RoomTimelineItemViewState
var body: some View {
timelineView
.animation(.elementDefault, value: viewState.groupStyle)
.animation(.elementDefault, value: viewState.type)
.environmentObject(context)
.environment(\.timelineGroupStyle, viewState.groupStyle)
.onAppear {
@@ -16,9 +16,13 @@
import Foundation
struct RoomTimelineItemViewState: Identifiable, Equatable {
let type: RoomTimelineItemType
let groupStyle: TimelineGroupStyle
final class RoomTimelineItemViewState: Identifiable, Equatable, ObservableObject {
static func == (lhs: RoomTimelineItemViewState, rhs: RoomTimelineItemViewState) -> Bool {
lhs.type == rhs.type && lhs.groupStyle == rhs.groupStyle
}
@Published var type: RoomTimelineItemType
@Published var groupStyle: TimelineGroupStyle
/// Contains all the identification info of the item, `timelineID`, `eventID` and `transactionID`
var identifier: TimelineItemIdentifier {
@@ -33,10 +37,13 @@ struct RoomTimelineItemViewState: Identifiable, Equatable {
var isReactable: Bool {
type.isReactable
}
}
extension RoomTimelineItemViewState {
init(item: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) {
init(type: RoomTimelineItemType, groupStyle: TimelineGroupStyle) {
self.type = type
self.groupStyle = groupStyle
}
convenience init(item: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) {
self.init(type: .init(item: item), groupStyle: groupStyle)
}
}