diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 3b5fb5bafa..d29010455a 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -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 +} diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 1b8bd99275..945ccc997b 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -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 } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsIndicatorView.swift b/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsIndicatorView.swift index 3afc12e2b3..bc11192ca7 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsIndicatorView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsIndicatorView.swift @@ -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) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 93836cf227..c62571c1c3 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -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") diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift index b74dd5df5a..9ebd316db7 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift @@ -161,6 +161,11 @@ class TimelineTableViewController: UIViewController { /// quick succession can execute before ``paginationState`` acknowledges that /// pagination is in progress. private let paginatePublisher = PassthroughSubject() + + /// 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() /// 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 { diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsIndicatorView-iPad-en-GB.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsIndicatorView-iPad-en-GB.1.png index 78cde9524c..3a447d926e 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsIndicatorView-iPad-en-GB.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsIndicatorView-iPad-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63322034c8b0c9b6c6b0c465f2881e4e6416c54116901500d2174aef183b46a6 -size 4523 +oid sha256:79651ee8761ee753217cec2b76146a00bc7e3ca962d05050aa36a678b164687c +size 12283 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsIndicatorView-iPad-pseudo.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsIndicatorView-iPad-pseudo.1.png index 78cde9524c..e524a30bbf 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsIndicatorView-iPad-pseudo.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsIndicatorView-iPad-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63322034c8b0c9b6c6b0c465f2881e4e6416c54116901500d2174aef183b46a6 -size 4523 +oid sha256:6ad49022cc1bbe83af7b9361a5d9f5919d7d1ec81c21a105eb7a12f063913555 +size 16147 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsIndicatorView-iPhone-15-en-GB.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsIndicatorView-iPhone-15-en-GB.1.png index 4363b7c91a..db26065a4d 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsIndicatorView-iPhone-15-en-GB.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsIndicatorView-iPhone-15-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b779d7079302a9fa2406ee3d0d632586cb83802a2630087a2dae5e49c23b45a8 -size 2601 +oid sha256:7d1e40829a9ae597796b0d9cbbcb5b6849396a18046e82dd0b0c46547d6db415 +size 8795 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsIndicatorView-iPhone-15-pseudo.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsIndicatorView-iPhone-15-pseudo.1.png index 4363b7c91a..b949859ad7 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsIndicatorView-iPhone-15-pseudo.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsIndicatorView-iPhone-15-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b779d7079302a9fa2406ee3d0d632586cb83802a2630087a2dae5e49c23b45a8 -size 2601 +oid sha256:c876c09a58ccf08a46cd597d3dec97a17bc422358ebec26bb10f76edd7417a8f +size 15316