Hide/Show pin banner based on scroll direction (#3080)

This commit is contained in:
Mauro
2024-07-24 12:15:36 +02:00
committed by GitHub
parent acc916c5cf
commit 35e69bae24
9 changed files with 106 additions and 30 deletions

View File

@@ -138,6 +138,10 @@ enum RoomScreenViewAction {
case scrolledToFocussedItem
/// The table view has loaded the first items for a new timeline.
case hasSwitchedTimeline
case hasScrolled(direction: ScrollDirection)
case nextPin
case viewAllPins
}
enum RoomScreenComposerAction {
@@ -165,7 +169,24 @@ struct RoomScreenViewState: BindableState {
var canCurrentUserRedactSelf = false
var canCurrentUserPin = false
var isViewSourceEnabled: Bool
var isPinningEnabled = false
var lastScrollDirection: ScrollDirection?
// These are just mocked items used for testing, their types might change
let pinnedItems = [
"Hello 1",
"How are you 2",
"I am fine 3",
"Thank you 4"
]
var currentPinIndex = 0
var shouldShowPinBanner: Bool {
isPinningEnabled && !pinnedItems.isEmpty && lastScrollDirection != .top
}
var selectedPinContent: AttributedString {
.init(pinnedItems[currentPinIndex])
}
var canJoinCall = false
var hasOngoingCall = false
@@ -278,3 +299,8 @@ struct TimelineViewState {
itemViewStates.contains { $0.identifier.eventID == eventID }
}
}
enum ScrollDirection: Equatable {
case top
case bottom
}

View File

@@ -194,6 +194,13 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
didScrollToFocussedItem()
case .hasSwitchedTimeline:
Task { state.timelineViewState.isSwitchingTimelines = false }
case let .hasScrolled(direction):
state.lastScrollDirection = direction
case .nextPin:
state.currentPinIndex = (state.currentPinIndex + 1) % state.pinnedItems.count
case .viewAllPins:
// TODO: Implement
break
}
}

View File

@@ -29,8 +29,11 @@ struct PinnedItemsIndicatorView: View {
if pinsCount <= 3 {
return pinsCount
}
let remainingPins = pinsCount - pinIndex
return remainingPins >= 3 ? 3 : pinsCount % 3
let maxUntruncatedIndicators = pinsCount - pinsCount % 3
if pinIndex < maxUntruncatedIndicators {
return 3
}
return pinsCount % 3
}
var body: some View {
@@ -46,20 +49,31 @@ struct PinnedItemsIndicatorView: View {
}
struct PinnedItemsIndicatorView_Previews: PreviewProvider, TestablePreview {
static func indicator(index: Int, count: Int) -> some View {
VStack(spacing: 0) {
Text("\(index + 1)/\(count)")
.font(.compound.bodyXS)
PinnedItemsIndicatorView(pinIndex: index, pinsCount: count)
}
}
static var previews: some View {
HStack(spacing: 10) {
PinnedItemsIndicatorView(pinIndex: 0, pinsCount: 1)
PinnedItemsIndicatorView(pinIndex: 0, pinsCount: 2)
PinnedItemsIndicatorView(pinIndex: 1, pinsCount: 2)
PinnedItemsIndicatorView(pinIndex: 0, pinsCount: 3)
PinnedItemsIndicatorView(pinIndex: 1, pinsCount: 3)
PinnedItemsIndicatorView(pinIndex: 2, pinsCount: 3)
PinnedItemsIndicatorView(pinIndex: 0, pinsCount: 5)
PinnedItemsIndicatorView(pinIndex: 1, pinsCount: 5)
PinnedItemsIndicatorView(pinIndex: 2, pinsCount: 5)
PinnedItemsIndicatorView(pinIndex: 3, pinsCount: 5)
PinnedItemsIndicatorView(pinIndex: 4, pinsCount: 5)
PinnedItemsIndicatorView(pinIndex: 3, pinsCount: 4)
HStack(spacing: 5) {
indicator(index: 0, count: 1)
indicator(index: 0, count: 2)
indicator(index: 1, count: 2)
indicator(index: 0, count: 3)
indicator(index: 1, count: 3)
indicator(index: 2, count: 3)
indicator(index: 0, count: 4)
indicator(index: 1, count: 4)
indicator(index: 2, count: 4)
indicator(index: 3, count: 4)
indicator(index: 0, count: 5)
indicator(index: 1, count: 5)
indicator(index: 2, count: 5)
indicator(index: 3, count: 5)
indicator(index: 4, count: 5)
}
.previewLayout(.sizeThatFits)
}

View File

@@ -49,14 +49,12 @@ struct RoomScreen: View {
.environment(\.roomContext, context)
}
.overlay(alignment: .top) {
if context.viewState.isPinningEnabled {
// TODO: Implement tapping logic + hiding when scrolling
PinnedItemsBannerView(pinIndex: 1,
pinsCount: 3,
pinContent: .init(stringLiteral: "Content"),
onMainButtonTap: { },
onViewAllButtonTap: { })
Group {
if context.viewState.shouldShowPinBanner {
pinnedItemsBanner
}
}
.animation(.elementDefault, value: context.viewState.shouldShowPinBanner)
}
.navigationTitle(L10n.screenRoomTitle) // Hidden but used for back button text.
.navigationBarTitleDisplayMode(.inline)
@@ -110,6 +108,15 @@ struct RoomScreen: View {
}
}
private var pinnedItemsBanner: some View {
PinnedItemsBannerView(pinIndex: context.viewState.currentPinIndex,
pinsCount: context.viewState.pinnedItems.count,
pinContent: context.viewState.selectedPinContent,
onMainButtonTap: { context.send(viewAction: .nextPin) },
onViewAllButtonTap: { context.send(viewAction: .viewAllPins) })
.transition(.move(edge: .top))
}
private var scrollToBottomButton: some View {
Button { context.send(viewAction: .scrollToBottom) } label: {
Image(systemName: "chevron.down")

View File

@@ -161,6 +161,11 @@ class TimelineTableViewController: UIViewController {
/// quick succession can execute before ``paginationState`` acknowledges that
/// pagination is in progress.
private let paginatePublisher = PassthroughSubject<Void, Never>()
/// A value to determine the scroll velocity threshold to detect a change in direction of the scroll view
private let scrollVelocityThreshold: CGFloat = 50.0
/// A publisher used to throttle scroll direction changes
private let scrollDirectionPublisher = PassthroughSubject<ScrollDirection, Never>()
/// Whether or not the view has been shown on screen yet.
private var hasAppearedOnce = false
@@ -198,6 +203,14 @@ class TimelineTableViewController: UIViewController {
}
.store(in: &cancellables)
scrollDirectionPublisher
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
.removeDuplicates()
.sink { direction in
coordinator.send(viewAction: .hasScrolled(direction: direction))
}
.store(in: &cancellables)
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
.sink { [weak self] _ in
self?.sendLastVisibleItemReadReceipt()
@@ -345,6 +358,7 @@ class TimelineTableViewController: UIViewController {
return
}
tableView.scrollToRow(at: IndexPath(item: 0, section: 0), at: .top, animated: animated)
scrollDirectionPublisher.send(.bottom)
}
/// Scrolls to the oldest item in the timeline.
@@ -353,6 +367,7 @@ class TimelineTableViewController: UIViewController {
return
}
tableView.scrollToRow(at: IndexPath(item: timelineItemsIDs.count - 1, section: 1), at: .bottom, animated: animated)
scrollDirectionPublisher.send(.top)
}
/// Scrolls to the item with the corresponding event ID if loaded in the timeline.
@@ -423,6 +438,13 @@ extension TimelineTableViewController: UITableViewDelegate {
if scrollView.contentOffset.y == 0 {
scrollView.contentOffset.y = -1
}
let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView.superview).y
if velocity > scrollVelocityThreshold {
scrollDirectionPublisher.send(.top)
} else if velocity < -scrollVelocityThreshold {
scrollDirectionPublisher.send(.bottom)
}
}
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:63322034c8b0c9b6c6b0c465f2881e4e6416c54116901500d2174aef183b46a6
size 4523
oid sha256:79651ee8761ee753217cec2b76146a00bc7e3ca962d05050aa36a678b164687c
size 12283

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:63322034c8b0c9b6c6b0c465f2881e4e6416c54116901500d2174aef183b46a6
size 4523
oid sha256:6ad49022cc1bbe83af7b9361a5d9f5919d7d1ec81c21a105eb7a12f063913555
size 16147

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b779d7079302a9fa2406ee3d0d632586cb83802a2630087a2dae5e49c23b45a8
size 2601
oid sha256:7d1e40829a9ae597796b0d9cbbcb5b6849396a18046e82dd0b0c46547d6db415
size 8795

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b779d7079302a9fa2406ee3d0d632586cb83802a2630087a2dae5e49c23b45a8
size 2601
oid sha256:c876c09a58ccf08a46cd597d3dec97a17bc422358ebec26bb10f76edd7417a8f
size 15316