Extract room screen action handling into separate component (#2045)

* Extract room screen action handling into separate component

* Fix tracing configuration unit tests

* Move the custom reactionsCollapsedBinding to the TimelineReactionsView

* Move voice message playback handling from the timelineController to the room view model

* Reorder methods

* Remove the need for a media player provider in the timeline controller

* Move room attachments opening to the view model, remove the need for a media provider in the timeline controller

* Rename RoomScreenActionsHandler -> RoomScreenInteractionHandler

* Move message sending retry and cancellation to the timeline controller.

* Move audio playback, attachment loading and  user tapping handling into the InteractionHandler

* Fix unit tests

* Switch back swiftlint file_length rule error to 1000 lines
This commit is contained in:
Stefan Ceriu
2023-11-08 18:31:39 +02:00
committed by GitHub
parent 07313926c9
commit abe2080ee2
25 changed files with 1085 additions and 925 deletions
@@ -55,14 +55,16 @@ struct RoomHeaderView_Previews: PreviewProvider, TestablePreview {
@ViewBuilder
static var bodyPlain: some View {
let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some Room name", avatarURL: URL.picturesDirectory)),
timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(),
roomProxy: RoomProxyMock(with: .init(displayName: "Some Room name", avatarURL: URL.picturesDirectory)),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: ApplicationMock.default)
application: ApplicationMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationCenter: NotificationCenterMock())
RoomHeaderView(context: viewModel.context)
.previewLayout(.sizeThatFits)
@@ -71,14 +73,16 @@ struct RoomHeaderView_Previews: PreviewProvider, TestablePreview {
@ViewBuilder
static var bodyEncrypted: some View {
let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some Room name")),
timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(),
roomProxy: RoomProxyMock(with: .init(displayName: "Some Room name")),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: ApplicationMock.default)
application: ApplicationMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationCenter: NotificationCenterMock())
RoomHeaderView(context: viewModel.context)
.previewLayout(.sizeThatFits)
@@ -187,14 +187,16 @@ struct RoomScreen: View {
// MARK: - Previews
struct RoomScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Preview room", isCallOngoing: true)),
timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(),
roomProxy: RoomProxyMock(with: .init(displayName: "Preview room", isCallOngoing: true)),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: ApplicationMock.default)
application: ApplicationMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationCenter: NotificationCenterMock())
static var previews: some View {
NavigationStack {
@@ -103,10 +103,10 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
.timelineAccessibility(timelineItem)
if !timelineItem.properties.reactions.isEmpty {
TimelineReactionsView(itemID: timelineItem.id,
TimelineReactionsView(context: context,
itemID: timelineItem.id,
reactions: timelineItem.properties.reactions,
isLayoutRTL: timelineItem.isOutgoing,
collapsed: context.reactionsCollapsedBinding(for: timelineItem.id))
isLayoutRTL: timelineItem.isOutgoing)
// Workaround to stop the message long press stealing the touch from the reaction buttons
.onTapGesture { }
}
@@ -129,9 +129,9 @@ struct TimelineItemPlainStylerView<Content: View>: View {
}
if !timelineItem.properties.reactions.isEmpty {
TimelineReactionsView(itemID: timelineItem.id,
reactions: timelineItem.properties.reactions,
collapsed: context.reactionsCollapsedBinding(for: timelineItem.id))
TimelineReactionsView(context: context,
itemID: timelineItem.id,
reactions: timelineItem.properties.reactions)
// Workaround to stop the message long press stealing the touch from the reaction buttons
.onTapGesture { }
}
@@ -16,17 +16,35 @@
import SwiftUI
@MainActor
struct TimelineReactionsView: View {
private static let horizontalSpacing: CGFloat = 4
private static let verticalSpacing: CGFloat = 4
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
@EnvironmentObject private var context: RoomScreenViewModel.Context
@Environment(\.layoutDirection) private var layoutDirection: LayoutDirection
let context: RoomScreenViewModel.Context
let itemID: TimelineItemIdentifier
let reactions: [AggregatedReaction]
var isLayoutRTL = false
@Binding var collapsed: Bool
let isLayoutRTL: Bool
private var collapsed: Binding<Bool>
init(context: RoomScreenViewModel.Context,
itemID: TimelineItemIdentifier,
reactions: [AggregatedReaction],
isLayoutRTL: Bool = false) {
self.context = context
self.itemID = itemID
self.reactions = reactions
self.isLayoutRTL = isLayoutRTL
collapsed = Binding(get: {
context.reactionsCollapsed[itemID] ?? true
}, set: {
context.reactionsCollapsed[itemID] = $0
})
}
var reactionsLayoutDirection: LayoutDirection {
guard isLayoutRTL else { return layoutDirection }
@@ -48,9 +66,9 @@ struct TimelineReactionsView: View {
if isCollapsible {
Button {
collapsed.toggle()
collapsed.wrappedValue.toggle()
} label: {
TimelineCollapseButtonLabel(collapsed: collapsed)
TimelineCollapseButtonLabel(collapsed: collapsed.wrappedValue)
.transaction { $0.animation = nil }
}
.reactionLayoutItem(.expandCollapse)
@@ -79,7 +97,7 @@ struct TimelineReactionsView: View {
if isCollapsible {
return AnyLayout(CollapsibleReactionLayout(itemSpacing: 4,
rowSpacing: 4,
collapsed: collapsed,
collapsed: collapsed.wrappedValue,
rowsBeforeCollapsible: 2))
}
@@ -188,21 +206,25 @@ struct TimelineReactionAddMoreButtonLabel: View {
}
struct TimelineReactionViewPreviewsContainer: View {
@State private var collapseState1 = false
@State private var collapseState2 = true
var body: some View {
VStack {
TimelineReactionsView(itemID: .init(timelineID: "1"),
TimelineReactionsView(context: RoomScreenViewModel.mock.context,
itemID: .init(timelineID: "1"),
reactions: [AggregatedReaction.mockReactionWithLongText,
AggregatedReaction.mockReactionWithLongTextRTL],
collapsed: .constant(true))
AggregatedReaction.mockReactionWithLongTextRTL])
Divider()
TimelineReactionsView(itemID: .init(timelineID: "2"), reactions: Array(AggregatedReaction.mockReactions.prefix(3)), collapsed: .constant(true))
TimelineReactionsView(context: RoomScreenViewModel.mock.context,
itemID: .init(timelineID: "2"),
reactions: Array(AggregatedReaction.mockReactions.prefix(3)))
Divider()
TimelineReactionsView(itemID: .init(timelineID: "3"), reactions: AggregatedReaction.mockReactions, collapsed: $collapseState1)
TimelineReactionsView(context: RoomScreenViewModel.mock.context,
itemID: .init(timelineID: "3"),
reactions: AggregatedReaction.mockReactions)
Divider()
TimelineReactionsView(itemID: .init(timelineID: "4"), reactions: AggregatedReaction.mockReactions, isLayoutRTL: true, collapsed: $collapseState2)
TimelineReactionsView(context: RoomScreenViewModel.mock.context,
itemID: .init(timelineID: "4"),
reactions: AggregatedReaction.mockReactions,
isLayoutRTL: true)
}
.background(Color.red)
.frame(maxWidth: 250, alignment: .leading)
@@ -58,15 +58,16 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
.mockMe
]
static let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Test", members: members)),
timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(),
roomProxy: RoomProxyMock(with: .init(displayName: "Test",
members: members)),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: ApplicationMock.default)
application: ApplicationMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationCenter: NotificationCenterMock())
static let singleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now")]
static let doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"),
@@ -80,14 +80,16 @@ struct UITimelineView: UIViewControllerRepresentable {
// MARK: - Previews
struct UITimelineView_Previews: PreviewProvider, TestablePreview {
static let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Preview room")),
timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(),
roomProxy: RoomProxyMock(with: .init(displayName: "Preview room")),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: ApplicationMock.default)
application: ApplicationMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationCenter: NotificationCenterMock())
static var previews: some View {
NavigationStack {
@@ -168,15 +168,17 @@ struct TimelineView: View {
// MARK: - Previews
struct TimelineView_Previews: PreviewProvider, TestablePreview {
static let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Preview room")),
timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(),
roomProxy: RoomProxyMock(with: .init(displayName: "Preview room")),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: ApplicationMock.default)
application: ApplicationMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationCenter: NotificationCenterMock())
static var previews: some View {
NavigationStack {
RoomScreen(context: viewModel.context, composerToolbar: ComposerToolbar.mock())