Hide/Show pin banner based on scroll direction (#3080)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:63322034c8b0c9b6c6b0c465f2881e4e6416c54116901500d2174aef183b46a6
|
||||
size 4523
|
||||
oid sha256:79651ee8761ee753217cec2b76146a00bc7e3ca962d05050aa36a678b164687c
|
||||
size 12283
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:63322034c8b0c9b6c6b0c465f2881e4e6416c54116901500d2174aef183b46a6
|
||||
size 4523
|
||||
oid sha256:6ad49022cc1bbe83af7b9361a5d9f5919d7d1ec81c21a105eb7a12f063913555
|
||||
size 16147
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b779d7079302a9fa2406ee3d0d632586cb83802a2630087a2dae5e49c23b45a8
|
||||
size 2601
|
||||
oid sha256:7d1e40829a9ae597796b0d9cbbcb5b6849396a18046e82dd0b0c46547d6db415
|
||||
size 8795
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b779d7079302a9fa2406ee3d0d632586cb83802a2630087a2dae5e49c23b45a8
|
||||
size 2601
|
||||
oid sha256:c876c09a58ccf08a46cd597d3dec97a17bc422358ebec26bb10f76edd7417a8f
|
||||
size 15316
|
||||
|
||||
Reference in New Issue
Block a user