mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
Hide/Show pin banner based on scroll direction (#3080)
This commit is contained in:
@@ -138,6 +138,10 @@ enum RoomScreenViewAction {
|
|||||||
case scrolledToFocussedItem
|
case scrolledToFocussedItem
|
||||||
/// The table view has loaded the first items for a new timeline.
|
/// The table view has loaded the first items for a new timeline.
|
||||||
case hasSwitchedTimeline
|
case hasSwitchedTimeline
|
||||||
|
|
||||||
|
case hasScrolled(direction: ScrollDirection)
|
||||||
|
case nextPin
|
||||||
|
case viewAllPins
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomScreenComposerAction {
|
enum RoomScreenComposerAction {
|
||||||
@@ -165,7 +169,24 @@ struct RoomScreenViewState: BindableState {
|
|||||||
var canCurrentUserRedactSelf = false
|
var canCurrentUserRedactSelf = false
|
||||||
var canCurrentUserPin = false
|
var canCurrentUserPin = false
|
||||||
var isViewSourceEnabled: Bool
|
var isViewSourceEnabled: Bool
|
||||||
|
|
||||||
var isPinningEnabled = false
|
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 canJoinCall = false
|
||||||
var hasOngoingCall = false
|
var hasOngoingCall = false
|
||||||
@@ -278,3 +299,8 @@ struct TimelineViewState {
|
|||||||
itemViewStates.contains { $0.identifier.eventID == eventID }
|
itemViewStates.contains { $0.identifier.eventID == eventID }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ScrollDirection: Equatable {
|
||||||
|
case top
|
||||||
|
case bottom
|
||||||
|
}
|
||||||
|
|||||||
@@ -194,6 +194,13 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
didScrollToFocussedItem()
|
didScrollToFocussedItem()
|
||||||
case .hasSwitchedTimeline:
|
case .hasSwitchedTimeline:
|
||||||
Task { state.timelineViewState.isSwitchingTimelines = false }
|
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
-15
@@ -29,8 +29,11 @@ struct PinnedItemsIndicatorView: View {
|
|||||||
if pinsCount <= 3 {
|
if pinsCount <= 3 {
|
||||||
return pinsCount
|
return pinsCount
|
||||||
}
|
}
|
||||||
let remainingPins = pinsCount - pinIndex
|
let maxUntruncatedIndicators = pinsCount - pinsCount % 3
|
||||||
return remainingPins >= 3 ? 3 : pinsCount % 3
|
if pinIndex < maxUntruncatedIndicators {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
return pinsCount % 3
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -46,20 +49,31 @@ struct PinnedItemsIndicatorView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct PinnedItemsIndicatorView_Previews: PreviewProvider, TestablePreview {
|
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 {
|
static var previews: some View {
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 5) {
|
||||||
PinnedItemsIndicatorView(pinIndex: 0, pinsCount: 1)
|
indicator(index: 0, count: 1)
|
||||||
PinnedItemsIndicatorView(pinIndex: 0, pinsCount: 2)
|
indicator(index: 0, count: 2)
|
||||||
PinnedItemsIndicatorView(pinIndex: 1, pinsCount: 2)
|
indicator(index: 1, count: 2)
|
||||||
PinnedItemsIndicatorView(pinIndex: 0, pinsCount: 3)
|
indicator(index: 0, count: 3)
|
||||||
PinnedItemsIndicatorView(pinIndex: 1, pinsCount: 3)
|
indicator(index: 1, count: 3)
|
||||||
PinnedItemsIndicatorView(pinIndex: 2, pinsCount: 3)
|
indicator(index: 2, count: 3)
|
||||||
PinnedItemsIndicatorView(pinIndex: 0, pinsCount: 5)
|
indicator(index: 0, count: 4)
|
||||||
PinnedItemsIndicatorView(pinIndex: 1, pinsCount: 5)
|
indicator(index: 1, count: 4)
|
||||||
PinnedItemsIndicatorView(pinIndex: 2, pinsCount: 5)
|
indicator(index: 2, count: 4)
|
||||||
PinnedItemsIndicatorView(pinIndex: 3, pinsCount: 5)
|
indicator(index: 3, count: 4)
|
||||||
PinnedItemsIndicatorView(pinIndex: 4, pinsCount: 5)
|
indicator(index: 0, count: 5)
|
||||||
PinnedItemsIndicatorView(pinIndex: 3, pinsCount: 4)
|
indicator(index: 1, count: 5)
|
||||||
|
indicator(index: 2, count: 5)
|
||||||
|
indicator(index: 3, count: 5)
|
||||||
|
indicator(index: 4, count: 5)
|
||||||
}
|
}
|
||||||
.previewLayout(.sizeThatFits)
|
.previewLayout(.sizeThatFits)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,14 +49,12 @@ struct RoomScreen: View {
|
|||||||
.environment(\.roomContext, context)
|
.environment(\.roomContext, context)
|
||||||
}
|
}
|
||||||
.overlay(alignment: .top) {
|
.overlay(alignment: .top) {
|
||||||
if context.viewState.isPinningEnabled {
|
Group {
|
||||||
// TODO: Implement tapping logic + hiding when scrolling
|
if context.viewState.shouldShowPinBanner {
|
||||||
PinnedItemsBannerView(pinIndex: 1,
|
pinnedItemsBanner
|
||||||
pinsCount: 3,
|
}
|
||||||
pinContent: .init(stringLiteral: "Content"),
|
|
||||||
onMainButtonTap: { },
|
|
||||||
onViewAllButtonTap: { })
|
|
||||||
}
|
}
|
||||||
|
.animation(.elementDefault, value: context.viewState.shouldShowPinBanner)
|
||||||
}
|
}
|
||||||
.navigationTitle(L10n.screenRoomTitle) // Hidden but used for back button text.
|
.navigationTitle(L10n.screenRoomTitle) // Hidden but used for back button text.
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.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 {
|
private var scrollToBottomButton: some View {
|
||||||
Button { context.send(viewAction: .scrollToBottom) } label: {
|
Button { context.send(viewAction: .scrollToBottom) } label: {
|
||||||
Image(systemName: "chevron.down")
|
Image(systemName: "chevron.down")
|
||||||
|
|||||||
@@ -161,6 +161,11 @@ class TimelineTableViewController: UIViewController {
|
|||||||
/// quick succession can execute before ``paginationState`` acknowledges that
|
/// quick succession can execute before ``paginationState`` acknowledges that
|
||||||
/// pagination is in progress.
|
/// pagination is in progress.
|
||||||
private let paginatePublisher = PassthroughSubject<Void, Never>()
|
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.
|
/// Whether or not the view has been shown on screen yet.
|
||||||
private var hasAppearedOnce = false
|
private var hasAppearedOnce = false
|
||||||
|
|
||||||
@@ -198,6 +203,14 @@ class TimelineTableViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.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)
|
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
self?.sendLastVisibleItemReadReceipt()
|
self?.sendLastVisibleItemReadReceipt()
|
||||||
@@ -345,6 +358,7 @@ class TimelineTableViewController: UIViewController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
tableView.scrollToRow(at: IndexPath(item: 0, section: 0), at: .top, animated: animated)
|
tableView.scrollToRow(at: IndexPath(item: 0, section: 0), at: .top, animated: animated)
|
||||||
|
scrollDirectionPublisher.send(.bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scrolls to the oldest item in the timeline.
|
/// Scrolls to the oldest item in the timeline.
|
||||||
@@ -353,6 +367,7 @@ class TimelineTableViewController: UIViewController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
tableView.scrollToRow(at: IndexPath(item: timelineItemsIDs.count - 1, section: 1), at: .bottom, animated: animated)
|
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.
|
/// 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 {
|
if scrollView.contentOffset.y == 0 {
|
||||||
scrollView.contentOffset.y = -1
|
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 {
|
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:63322034c8b0c9b6c6b0c465f2881e4e6416c54116901500d2174aef183b46a6
|
oid sha256:79651ee8761ee753217cec2b76146a00bc7e3ca962d05050aa36a678b164687c
|
||||||
size 4523
|
size 12283
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:63322034c8b0c9b6c6b0c465f2881e4e6416c54116901500d2174aef183b46a6
|
oid sha256:6ad49022cc1bbe83af7b9361a5d9f5919d7d1ec81c21a105eb7a12f063913555
|
||||||
size 4523
|
size 16147
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b779d7079302a9fa2406ee3d0d632586cb83802a2630087a2dae5e49c23b45a8
|
oid sha256:7d1e40829a9ae597796b0d9cbbcb5b6849396a18046e82dd0b0c46547d6db415
|
||||||
size 2601
|
size 8795
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b779d7079302a9fa2406ee3d0d632586cb83802a2630087a2dae5e49c23b45a8
|
oid sha256:c876c09a58ccf08a46cd597d3dec97a17bc422358ebec26bb10f76edd7417a8f
|
||||||
size 2601
|
size 15316
|
||||||
|
|||||||
Reference in New Issue
Block a user