mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
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:
@@ -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 { }
|
||||
}
|
||||
|
||||
+37
-15
@@ -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)
|
||||
|
||||
+7
-6
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user