diff --git a/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift b/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift index 17910355cf..4eb45d38a0 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift @@ -292,7 +292,7 @@ class TimelineTableViewController: UIViewController { } } - // We only animate when there's a new last message, so its safe + // We only animate when there's a new last message, so it's safe // to animate from the bottom (which is the top as we're flipped). dataSource?.defaultRowAnimation = (UIAccessibility.isReduceMotionEnabled ? .none : .top) tableView.delegate = self diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift index 344c9bd55e..8f1bb09b76 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift @@ -45,7 +45,12 @@ struct TimelineItemBubbledStylerView: View { .zIndex(1) } - VStack(alignment: alignment, spacing: 0) { + VStack(alignment: alignment, spacing: -4) { + // -4 spacing to compensate for the Spacer we have to add to stop + // animation oversteer - see below. + // XXX: does this squidge the bubble & the SR too close together now? + // it looks like it should, but in practice it doesn't seem to. + HStack(spacing: 0) { if timelineItem.isOutgoing { Spacer() @@ -59,17 +64,36 @@ struct TimelineItemBubbledStylerView: View { if !timelineItem.isOutgoing { Spacer() } - TimelineItemStatusView(timelineItem: timelineItem, adjustedDeliveryStatus: adjustedDeliveryStatus) + TimelineItemStatusView(timelineItem: timelineItem, adjustedDeliveryStatus: adjustedDeliveryStatus, context: context) .environmentObject(context) .padding(.top, 8) .padding(.bottom, 3) } + // .background(Color.purple) + + // we need a Spacer here to top-align the bubble within the VStack, so that + // when the the animation doesn't drift around and overshoot when the SR is hidden + // see https://github.com/element-hq/element-x-ios/issues/4127 + Spacer() + // .background { Color.yellow.frame(minWidth: 10) } } .padding(.horizontal, bubbleHorizontalPadding) .padding(.leading, bubbleAvatarPadding) } } + // .background { + // Int(timelineItem.id.uniqueID.value).unsafelyUnwrapped % 4 == 0 ? Color.green : + // Int(timelineItem.id.uniqueID.value).unsafelyUnwrapped % 4 == 1 ? Color.cyan : + // Int(timelineItem.id.uniqueID.value).unsafelyUnwrapped % 4 == 2 ? Color.brown : + // Color.indigo + // } .padding(EdgeInsets(top: 1, leading: 8, bottom: 1, trailing: 8)) + // hoist the unanimated messageBubbleTopPadding to the topmost view, as otherwise the + // UITableView row pops when this View changes layout seemingly. + // N.B. we can't combine two paddings together without triggering popping. + .padding(.top, messageBubbleTopPadding) + // don't reanimate bubble layouts if we're just changing the messageBubbleTopPadding + .animation(nil, value: messageBubbleTopPadding) .highlightedTimelineItem(isFocussed) } @@ -163,7 +187,6 @@ struct TimelineItemBubbledStylerView: View { } } .pinnedIndicator(isPinned: isPinned, isOutgoing: timelineItem.isOutgoing) - .padding(.top, messageBubbleTopPadding) } var messageBubble: some View { diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift index 08c3aadf8f..0f5c6330ed 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift @@ -12,13 +12,35 @@ struct TimelineItemStatusView: View { let timelineItem: EventBasedTimelineItemProtocol let adjustedDeliveryStatus: TimelineItemDeliveryStatus? @EnvironmentObject private var context: TimelineViewModel.Context + @State private var isSendReceiptVisible: Bool private var isLastOutgoingMessage: Bool { timelineItem.isOutgoing && context.viewState.timelineState.uniqueIDs.last == timelineItem.id.uniqueID } + init(timelineItem: EventBasedTimelineItemProtocol, adjustedDeliveryStatus: TimelineItemDeliveryStatus?, context: TimelineViewModel.Context, isSendReceiptVisible: Bool = false) { + self.timelineItem = timelineItem + self.adjustedDeliveryStatus = adjustedDeliveryStatus + // Ugly - we can't call isLastOutgoingMessage here as the real `context` hasn't loaded yet + // so instead we manually pass in context to init() and duplicate isLastOutgoingMessage here. + self.isSendReceiptVisible = timelineItem.isOutgoing && context.viewState.timelineState.uniqueIDs.last == timelineItem.id.uniqueID + } + var body: some View { mainContent + .onChange(of: context.viewState.timelineState.uniqueIDs.last) { _, _ in + if isLastOutgoingMessage { + isSendReceiptVisible = true + } else if isSendReceiptVisible { + // we were the last msg in the timeline, but not any more + // so remove the SR after a short delay to avoid racing with the new msg animation + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + withAnimation { + isSendReceiptVisible = false + } + } + } + } } @ViewBuilder @@ -39,9 +61,10 @@ struct TimelineItemStatusView: View { case .sending: TimelineDeliveryStatusView(deliveryStatus: .sending) case .sent, .none: - if isLastOutgoingMessage { + if isSendReceiptVisible { // We only display the sent icon for the latest outgoing message TimelineDeliveryStatusView(deliveryStatus: .sent) + // .transition(.identity) // makes the SR disappear rapidly to avoid ugly z-index flickering } case .sendingFailed: // Bubbles handle the case internally diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/SeparatorRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/SeparatorRoomTimelineView.swift index 2535a8c20c..b40e7c9f98 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/SeparatorRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/SeparatorRoomTimelineView.swift @@ -18,6 +18,7 @@ struct SeparatorRoomTimelineView: View { .multilineTextAlignment(.center) .padding(.horizontal, 36.0) .padding(.vertical, 8.0) + // .background(Color.red) } }