Move timeline item tap gestures to the items themselves instead of the bubbled styler (#3553)

* Stop observing the timeline context where not necessary.
* Rename the timeline `itemTapped` action to `mediaTapped`
This commit is contained in:
Stefan Ceriu
2024-11-26 16:25:46 +02:00
committed by GitHub
parent 3f0f442937
commit 36980840ed
13 changed files with 53 additions and 29 deletions
@@ -204,7 +204,7 @@ private struct LoadableImageContent<TransformerView: View, PlaceholderView: View
ZStack { ZStack {
Color.black.opacity(0.6) Color.black.opacity(0.6)
.contentShape(.rect) .contentShape(.rect)
.onTapGesture { /* Empty gesture to block the itemTapped action */ } .onTapGesture { /* Empty gesture to block the `mediaTapped` action */ }
// Don't use a real Button as it sometimes triggers simultaneously with the long press gesture. // Don't use a real Button as it sometimes triggers simultaneously with the long press gesture.
Text(L10n.actionShow) Text(L10n.actionShow)
@@ -43,7 +43,7 @@ enum TimelineViewAction {
case itemAppeared(itemID: TimelineItemIdentifier) case itemAppeared(itemID: TimelineItemIdentifier)
case itemDisappeared(itemID: TimelineItemIdentifier) case itemDisappeared(itemID: TimelineItemIdentifier)
case itemTapped(itemID: TimelineItemIdentifier) case mediaTapped(itemID: TimelineItemIdentifier)
case itemSendInfoTapped(itemID: TimelineItemIdentifier) case itemSendInfoTapped(itemID: TimelineItemIdentifier)
case toggleReaction(key: String, itemID: TimelineItemIdentifier) case toggleReaction(key: String, itemID: TimelineItemIdentifier)
case sendReadReceiptIfNeeded(TimelineItemIdentifier) case sendReadReceiptIfNeeded(TimelineItemIdentifier)
@@ -128,8 +128,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
Task { await timelineController.processItemAppearance(id) } Task { await timelineController.processItemAppearance(id) }
case .itemDisappeared(let id): case .itemDisappeared(let id):
Task { await timelineController.processItemDisappearance(id) } Task { await timelineController.processItemDisappearance(id) }
case .itemTapped(let id): case .mediaTapped(let id):
Task { await handleItemTapped(with: id) } Task { await handleMediaTapped(with: id) }
case .itemSendInfoTapped(let itemID): case .itemSendInfoTapped(let itemID):
handleItemSendInfoTapped(itemID: itemID) handleItemSendInfoTapped(itemID: itemID)
case .toggleReaction(let emoji, let itemID): case .toggleReaction(let emoji, let itemID):
@@ -533,7 +533,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
await timelineController.sendReadReceipt(for: lastVisibleItemID) await timelineController.sendReadReceipt(for: lastVisibleItemID)
} }
private func handleItemTapped(with itemID: TimelineItemIdentifier) async { private func handleMediaTapped(with itemID: TimelineItemIdentifier) async {
state.showLoading = true state.showLoading = true
let action = await timelineInteractionHandler.processItemTap(itemID) let action = await timelineInteractionHandler.processItemTap(itemID)
@@ -124,11 +124,6 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
var messageBubbleWithActions: some View { var messageBubbleWithActions: some View {
messageBubble messageBubble
.onTapGesture {
context.send(viewAction: .itemTapped(itemID: timelineItem.id))
}
// We need a tap gesture before this long one so that it doesn't
// steal away the gestures from the scroll view
.longPressWithFeedback { .longPressWithFeedback {
context.send(viewAction: .displayTimelineItemMenu(itemID: timelineItem.id)) context.send(viewAction: .displayTimelineItemMenu(itemID: timelineItem.id))
} }
@@ -396,6 +391,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
} }
} }
.environmentObject(viewModel.context) .environmentObject(viewModel.context)
.environment(\.timelineContext, viewModel.context)
} }
static var replies: some View { static var replies: some View {
@@ -427,6 +423,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
groupStyle: .single)) groupStyle: .single))
} }
.environmentObject(viewModel.context) .environmentObject(viewModel.context)
.environment(\.timelineContext, viewModel.context)
} }
static var threads: some View { static var threads: some View {
@@ -434,6 +431,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
MockTimelineContent(isThreaded: true) MockTimelineContent(isThreaded: true)
} }
.environmentObject(viewModel.context) .environmentObject(viewModel.context)
.environment(\.timelineContext, viewModel.context)
} }
static var pinned: some View { static var pinned: some View {
@@ -441,6 +439,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
MockTimelineContent(isPinned: true) MockTimelineContent(isPinned: true)
} }
.environmentObject(viewModelWithPins.context) .environmentObject(viewModelWithPins.context)
.environment(\.timelineContext, viewModel.context)
} }
static var encryptionAuthenticity: some View { static var encryptionAuthenticity: some View {
@@ -523,6 +522,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
waveform: EstimatedWaveform.mockWaveform)) waveform: EstimatedWaveform.mockWaveform))
} }
.environmentObject(viewModel.context) .environmentObject(viewModel.context)
.environment(\.timelineContext, viewModel.context)
} }
} }
@@ -13,7 +13,8 @@ struct AudioRoomTimelineView: View {
var body: some View { var body: some View {
TimelineStyler(timelineItem: timelineItem) { TimelineStyler(timelineItem: timelineItem) {
MediaFileRoomTimelineContent(filename: timelineItem.content.filename, MediaFileRoomTimelineContent(timelineItemID: timelineItem.id,
filename: timelineItem.content.filename,
fileSize: timelineItem.content.fileSize, fileSize: timelineItem.content.fileSize,
caption: timelineItem.content.caption, caption: timelineItem.content.caption,
formattedCaption: timelineItem.content.formattedCaption, formattedCaption: timelineItem.content.formattedCaption,
@@ -10,7 +10,7 @@ import Foundation
import SwiftUI import SwiftUI
struct CallNotificationRoomTimelineView: View { struct CallNotificationRoomTimelineView: View {
@EnvironmentObject private var context: TimelineViewModel.Context @Environment(\.timelineContext) private var context
let timelineItem: CallNotificationRoomTimelineItem let timelineItem: CallNotificationRoomTimelineItem
@@ -20,7 +20,7 @@ struct CallNotificationRoomTimelineView: View {
name: timelineItem.sender.displayName ?? timelineItem.sender.id, name: timelineItem.sender.displayName ?? timelineItem.sender.id,
contentID: timelineItem.sender.id, contentID: timelineItem.sender.id,
avatarSize: .user(on: .timeline), avatarSize: .user(on: .timeline),
mediaProvider: context.mediaProvider) mediaProvider: context?.mediaProvider)
.accessibilityHidden(true) .accessibilityHidden(true)
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
@@ -13,7 +13,8 @@ struct FileRoomTimelineView: View {
var body: some View { var body: some View {
TimelineStyler(timelineItem: timelineItem) { TimelineStyler(timelineItem: timelineItem) {
MediaFileRoomTimelineContent(filename: timelineItem.content.filename, MediaFileRoomTimelineContent(timelineItemID: timelineItem.id,
filename: timelineItem.content.filename,
fileSize: timelineItem.content.fileSize, fileSize: timelineItem.content.fileSize,
caption: timelineItem.content.caption, caption: timelineItem.content.caption,
formattedCaption: timelineItem.content.formattedCaption, formattedCaption: timelineItem.content.formattedCaption,
@@ -26,6 +27,9 @@ struct FileRoomTimelineView: View {
// MARK: Content // MARK: Content
struct MediaFileRoomTimelineContent: View { struct MediaFileRoomTimelineContent: View {
@Environment(\.timelineContext) private var context
let timelineItemID: TimelineItemIdentifier
let filename: String let filename: String
let fileSize: UInt? let fileSize: UInt?
let caption: String? let caption: String?
@@ -40,6 +44,9 @@ struct MediaFileRoomTimelineContent: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
filePreview filePreview
.onTapGesture {
context?.send(viewAction: .mediaTapped(itemID: timelineItemID))
}
if let formattedCaption { if let formattedCaption {
FormattedBodyText(attributedString: formattedCaption, FormattedBodyText(attributedString: formattedCaption,
@@ -9,7 +9,7 @@ import Foundation
import SwiftUI import SwiftUI
struct ImageRoomTimelineView: View { struct ImageRoomTimelineView: View {
@EnvironmentObject private var context: TimelineViewModel.Context @Environment(\.timelineContext) private var context
let timelineItem: ImageRoomTimelineItem let timelineItem: ImageRoomTimelineItem
var hasMediaCaption: Bool { timelineItem.content.caption != nil } var hasMediaCaption: Bool { timelineItem.content.caption != nil }
@@ -23,6 +23,9 @@ struct ImageRoomTimelineView: View {
// This clip shape is distinct from the one in the styler as that one // This clip shape is distinct from the one in the styler as that one
// operates on the entire message so wouldn't round the bottom corners. // operates on the entire message so wouldn't round the bottom corners.
.clipShape(RoundedRectangle(cornerRadius: hasMediaCaption ? 6 : 0)) .clipShape(RoundedRectangle(cornerRadius: hasMediaCaption ? 6 : 0))
.onTapGesture {
context?.send(viewAction: .mediaTapped(itemID: timelineItem.id))
}
if let attributedCaption = timelineItem.content.formattedCaption { if let attributedCaption = timelineItem.content.formattedCaption {
FormattedBodyText(attributedString: attributedCaption, FormattedBodyText(attributedString: attributedCaption,
@@ -44,7 +47,7 @@ struct ImageRoomTimelineView: View {
mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID.id), mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID.id),
blurhash: timelineItem.content.blurhash, blurhash: timelineItem.content.blurhash,
size: timelineItem.content.imageInfo.size, size: timelineItem.content.imageInfo.size,
mediaProvider: context.mediaProvider) { mediaProvider: context?.mediaProvider) {
placeholder placeholder
} }
.timelineMediaFrame(imageInfo: timelineItem.content.imageInfo) .timelineMediaFrame(imageInfo: timelineItem.content.imageInfo)
@@ -53,7 +56,7 @@ struct ImageRoomTimelineView: View {
mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID.id), mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID.id),
blurhash: timelineItem.content.blurhash, blurhash: timelineItem.content.blurhash,
size: timelineItem.content.thumbnailInfo?.size ?? timelineItem.content.imageInfo.size, size: timelineItem.content.thumbnailInfo?.size ?? timelineItem.content.imageInfo.size,
mediaProvider: context.mediaProvider) { mediaProvider: context?.mediaProvider) {
placeholder placeholder
} }
.timelineMediaFrame(imageInfo: timelineItem.content.thumbnailInfo ?? timelineItem.content.imageInfo) .timelineMediaFrame(imageInfo: timelineItem.content.thumbnailInfo ?? timelineItem.content.imageInfo)
@@ -73,6 +76,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static var previews: some View { static var previews: some View {
body body
.environmentObject(viewModel.context) .environmentObject(viewModel.context)
.environment(\.timelineContext, viewModel.context)
} }
static var body: some View { static var body: some View {
@@ -8,6 +8,7 @@
import SwiftUI import SwiftUI
struct LocationRoomTimelineView: View { struct LocationRoomTimelineView: View {
@Environment(\.timelineContext) private var context
let timelineItem: LocationRoomTimelineItem let timelineItem: LocationRoomTimelineItem
var body: some View { var body: some View {
@@ -15,6 +16,9 @@ struct LocationRoomTimelineView: View {
mainContent mainContent
.accessibilityElement(children: .ignore) .accessibilityElement(children: .ignore)
.accessibilityLabel(accessibilityLabel) .accessibilityLabel(accessibilityLabel)
.onTapGesture {
context?.send(viewAction: .mediaTapped(itemID: timelineItem.id))
}
} }
} }
@@ -9,7 +9,7 @@ import Foundation
import SwiftUI import SwiftUI
struct StickerRoomTimelineView: View { struct StickerRoomTimelineView: View {
@EnvironmentObject private var context: TimelineViewModel.Context @Environment(\.timelineContext) private var context
let timelineItem: StickerRoomTimelineItem let timelineItem: StickerRoomTimelineItem
var body: some View { var body: some View {
@@ -18,12 +18,15 @@ struct StickerRoomTimelineView: View {
mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID.id), mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID.id),
blurhash: timelineItem.blurhash, blurhash: timelineItem.blurhash,
size: timelineItem.imageInfo.size, size: timelineItem.imageInfo.size,
mediaProvider: context.mediaProvider) { mediaProvider: context?.mediaProvider) {
placeholder placeholder
} }
.timelineMediaFrame(imageInfo: timelineItem.imageInfo) .timelineMediaFrame(imageInfo: timelineItem.imageInfo)
.accessibilityElement(children: .ignore) .accessibilityElement(children: .ignore)
.accessibilityLabel("\(L10n.commonSticker), \(timelineItem.body)") .accessibilityLabel("\(L10n.commonSticker), \(timelineItem.body)")
.onTapGesture {
context?.send(viewAction: .mediaTapped(itemID: timelineItem.id))
}
} }
} }
@@ -38,7 +41,9 @@ struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static let viewModel = TimelineViewModel.mock static let viewModel = TimelineViewModel.mock
static var previews: some View { static var previews: some View {
body.environmentObject(viewModel.context) body
.environmentObject(viewModel.context)
.environment(\.timelineContext, viewModel.context)
} }
static var body: some View { static var body: some View {
@@ -9,7 +9,7 @@ import Foundation
import SwiftUI import SwiftUI
struct VideoRoomTimelineView: View { struct VideoRoomTimelineView: View {
@EnvironmentObject private var context: TimelineViewModel.Context @Environment(\.timelineContext) private var context
let timelineItem: VideoRoomTimelineItem let timelineItem: VideoRoomTimelineItem
private var hasMediaCaption: Bool { timelineItem.content.caption != nil } private var hasMediaCaption: Bool { timelineItem.content.caption != nil }
@@ -24,6 +24,9 @@ struct VideoRoomTimelineView: View {
// This clip shape is distinct from the one in the styler as that one // This clip shape is distinct from the one in the styler as that one
// operates on the entire message so wouldn't round the bottom corners. // operates on the entire message so wouldn't round the bottom corners.
.clipShape(RoundedRectangle(cornerRadius: hasMediaCaption ? 6 : 0)) .clipShape(RoundedRectangle(cornerRadius: hasMediaCaption ? 6 : 0))
.onTapGesture {
context?.send(viewAction: .mediaTapped(itemID: timelineItem.id))
}
if let attributedCaption = timelineItem.content.formattedCaption { if let attributedCaption = timelineItem.content.formattedCaption {
FormattedBodyText(attributedString: attributedCaption, FormattedBodyText(attributedString: attributedCaption,
@@ -45,7 +48,7 @@ struct VideoRoomTimelineView: View {
mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID.id), mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID.id),
blurhash: timelineItem.content.blurhash, blurhash: timelineItem.content.blurhash,
size: timelineItem.content.thumbnailInfo?.size, size: timelineItem.content.thumbnailInfo?.size,
mediaProvider: context.mediaProvider) { imageView in mediaProvider: context?.mediaProvider) { imageView in
imageView imageView
.overlay { playIcon } .overlay { playIcon }
} placeholder: { } placeholder: {
@@ -9,7 +9,7 @@ import Foundation
import SwiftUI import SwiftUI
struct TimelineSenderAvatarView: View { struct TimelineSenderAvatarView: View {
@EnvironmentObject private var context: TimelineViewModel.Context @Environment(\.timelineContext) private var context
let timelineItem: EventBasedTimelineItemProtocol let timelineItem: EventBasedTimelineItemProtocol
@@ -18,7 +18,7 @@ struct TimelineSenderAvatarView: View {
name: timelineItem.sender.displayName, name: timelineItem.sender.displayName,
contentID: timelineItem.sender.id, contentID: timelineItem.sender.id,
avatarSize: .user(on: .timeline), avatarSize: .user(on: .timeline),
mediaProvider: context.mediaProvider) mediaProvider: context?.mediaProvider)
.overlay { .overlay {
Circle().stroke(Color.compound.bgCanvasDefault, lineWidth: 3) Circle().stroke(Color.compound.bgCanvasDefault, lineWidth: 3)
} }
@@ -9,7 +9,7 @@ import Foundation
import SwiftUI import SwiftUI
struct VoiceMessageRoomTimelineView: View { struct VoiceMessageRoomTimelineView: View {
@EnvironmentObject private var context: TimelineViewModel.Context @Environment(\.timelineContext) private var context
private let timelineItem: VoiceMessageRoomTimelineItem private let timelineItem: VoiceMessageRoomTimelineItem
private let playerState: AudioPlayerState private let playerState: AudioPlayerState
@State private var resumePlaybackAfterScrubbing = false @State private var resumePlaybackAfterScrubbing = false
@@ -31,22 +31,22 @@ struct VoiceMessageRoomTimelineView: View {
} }
private func onPlaybackPlayPause() { private func onPlaybackPlayPause() {
context.send(viewAction: .handleAudioPlayerAction(.playPause(itemID: timelineItem.id))) context?.send(viewAction: .handleAudioPlayerAction(.playPause(itemID: timelineItem.id)))
} }
private func onPlaybackSeek(_ progress: Double) { private func onPlaybackSeek(_ progress: Double) {
context.send(viewAction: .handleAudioPlayerAction(.seek(itemID: timelineItem.id, progress: progress))) context?.send(viewAction: .handleAudioPlayerAction(.seek(itemID: timelineItem.id, progress: progress)))
} }
private func onPlaybackScrubbing(_ dragging: Bool) { private func onPlaybackScrubbing(_ dragging: Bool) {
if dragging { if dragging {
if playerState.playbackState == .playing { if playerState.playbackState == .playing {
resumePlaybackAfterScrubbing = true resumePlaybackAfterScrubbing = true
context.send(viewAction: .handleAudioPlayerAction(.playPause(itemID: timelineItem.id))) context?.send(viewAction: .handleAudioPlayerAction(.playPause(itemID: timelineItem.id)))
} }
} else { } else {
if resumePlaybackAfterScrubbing { if resumePlaybackAfterScrubbing {
context.send(viewAction: .handleAudioPlayerAction(.playPause(itemID: timelineItem.id))) context?.send(viewAction: .handleAudioPlayerAction(.playPause(itemID: timelineItem.id)))
resumePlaybackAfterScrubbing = false resumePlaybackAfterScrubbing = false
} }
} }