mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Permalink Tweaks 2 (#2766)
* Missing changelog. * Parse bare room aliases as permalinks. Update the SDK. * Fix tapping the same permalink twice. Add a test. Don't clear the focussed item when reaching the bottom of the timeline. * Make sure sending a message returns to live.
This commit is contained in:
@@ -85,6 +85,7 @@ enum RoomScreenViewAction {
|
||||
case sendReadReceiptIfNeeded(TimelineItemIdentifier)
|
||||
case paginateBackwards
|
||||
case paginateForwards
|
||||
case scrollToBottom
|
||||
|
||||
case timelineItemMenu(itemID: TimelineItemIdentifier)
|
||||
case timelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction)
|
||||
@@ -112,11 +113,8 @@ enum RoomScreenViewAction {
|
||||
case focusOnEventID(String)
|
||||
/// Switch back to a live timeline (from a detached one).
|
||||
case focusLive
|
||||
/// Remove the highlighted event without switching timeline.
|
||||
///
|
||||
/// This is useful when returning to the bottom of the live timeline
|
||||
/// if `focusOnEventID` didn't use a detached timeline.
|
||||
case clearFocussedEvent
|
||||
/// The timeline scrolled to reveal the focussed item.
|
||||
case scrolledToFocussedItem
|
||||
}
|
||||
|
||||
enum RoomScreenComposerAction {
|
||||
@@ -229,7 +227,10 @@ struct TimelineViewState {
|
||||
var isLive = true
|
||||
var paginationState = PaginationState.default
|
||||
|
||||
var focussedEventID: String?
|
||||
/// The ID of the focussed event navigated to via a permalink.
|
||||
var focussedEventID: String? { didSet { focussedEventNeedsDisplay = focussedEventID != nil } }
|
||||
/// Whether the timeline should scroll to `focussedEventID` once its item has been built and added to the timeline.
|
||||
var focussedEventNeedsDisplay: Bool
|
||||
|
||||
// These can be removed when we have full swiftUI and moved as @State values in the view
|
||||
var scrollToBottomPublisher = PassthroughSubject<Void, Never>()
|
||||
|
||||
@@ -88,7 +88,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
roomAvatarURL: roomProxy.avatarURL,
|
||||
timelineStyle: appSettings.timelineStyle,
|
||||
isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom,
|
||||
timelineViewState: TimelineViewState(focussedEventID: focussedEventID),
|
||||
timelineViewState: TimelineViewState(focussedEventID: focussedEventID,
|
||||
focussedEventNeedsDisplay: focussedEventID != nil),
|
||||
ownUserID: roomProxy.ownUserID,
|
||||
hasOngoingCall: roomProxy.hasOngoingCall,
|
||||
bindings: .init(reactionsCollapsed: [:])),
|
||||
@@ -175,6 +176,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
paginateBackwards()
|
||||
case .paginateForwards:
|
||||
paginateForwards()
|
||||
case .scrollToBottom:
|
||||
scrollToBottom()
|
||||
case .poll(let pollAction):
|
||||
processPollAction(pollAction)
|
||||
case .audio(let audioAction):
|
||||
@@ -187,8 +190,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
Task { await focusOnEvent(eventID: eventID) }
|
||||
case .focusLive:
|
||||
focusLive()
|
||||
case .clearFocussedEvent:
|
||||
state.timelineViewState.focussedEventID = nil
|
||||
case .scrolledToFocussedItem:
|
||||
// Use a Task to mutate view state after the current view update.
|
||||
Task { state.timelineViewState.focussedEventNeedsDisplay = false }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,7 +369,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
.filter { $0 == .sentMessage }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
self?.state.timelineViewState.scrollToBottomPublisher.send(())
|
||||
self?.scrollToBottom()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -488,6 +492,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
}
|
||||
|
||||
private func scrollToBottom() {
|
||||
if state.timelineViewState.isLive {
|
||||
state.timelineViewState.scrollToBottomPublisher.send(())
|
||||
} else {
|
||||
focusLive()
|
||||
}
|
||||
}
|
||||
|
||||
private func sendReadReceiptIfNeeded(for lastVisibleItemID: TimelineItemIdentifier) async {
|
||||
guard appMediator.appState == .active else { return }
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ struct RoomScreen: View {
|
||||
}
|
||||
|
||||
private var scrollToBottomButton: some View {
|
||||
Button(action: scrollToBottom) {
|
||||
Button { context.send(viewAction: .scrollToBottom) } label: {
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.compound.bodyLG)
|
||||
.fontWeight(.semibold)
|
||||
@@ -121,20 +121,13 @@ struct RoomScreen: View {
|
||||
.opacity(isAtBottomAndLive ? 0.0 : 1.0)
|
||||
.accessibilityHidden(isAtBottomAndLive)
|
||||
.animation(.elementDefault, value: isAtBottomAndLive)
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomScreen.scrollToBottom)
|
||||
}
|
||||
|
||||
private var isAtBottomAndLive: Bool {
|
||||
context.isScrolledToBottom && context.viewState.timelineViewState.isLive
|
||||
}
|
||||
|
||||
private func scrollToBottom() {
|
||||
if context.viewState.timelineViewState.isLive {
|
||||
context.viewState.timelineViewState.scrollToBottomPublisher.send(())
|
||||
} else {
|
||||
context.send(viewAction: .focusLive)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var loadingIndicator: some View {
|
||||
if context.viewState.showLoading {
|
||||
|
||||
+12
-17
@@ -76,10 +76,10 @@ class TimelineTableViewController: UIViewController {
|
||||
}
|
||||
|
||||
/// There are pending items in `timelineItemsDictionary` that haven't been applied to the data source.
|
||||
var hasPendingItems = false
|
||||
private var hasPendingItems = false
|
||||
|
||||
/// The user is dragging the scroll view (or it is still decelerating after a drag).
|
||||
var isDraggingScrollView = false {
|
||||
private var isDraggingScrollView = false {
|
||||
didSet {
|
||||
if !isDraggingScrollView, hasPendingItems {
|
||||
hasPendingItems = false
|
||||
@@ -104,19 +104,17 @@ class TimelineTableViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// The ID of the focussed event if opening the room to an event permalink.
|
||||
var focussedEventID: String? {
|
||||
didSet {
|
||||
guard let focussedEventID else { return }
|
||||
|
||||
focussedEventNeedsDisplay = true
|
||||
scrollToItem(eventID: focussedEventID, animated: false)
|
||||
}
|
||||
}
|
||||
/// The ID of the focussed event if navigating to an event permalink within the room.
|
||||
var focussedEventID: String?
|
||||
|
||||
/// Whether the timeline should scroll to `focussedEventID` when that item is added to the data source.
|
||||
/// This is necessary as the focussed event can be set before the timeline builder has built its item.
|
||||
var focussedEventNeedsDisplay = false
|
||||
var focussedEventNeedsDisplay = false {
|
||||
didSet {
|
||||
guard focussedEventNeedsDisplay, let focussedEventID else { return }
|
||||
scrollToItem(eventID: focussedEventID, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to hold an observable object that the typing indicator can use
|
||||
let typingMembers = TypingMembersObservableObject(members: [])
|
||||
@@ -173,10 +171,7 @@ class TimelineTableViewController: UIViewController {
|
||||
|
||||
scrollToBottomPublisher
|
||||
.sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
|
||||
scrollToNewestItem(animated: true)
|
||||
coordinator.send(viewAction: .clearFocussedEvent)
|
||||
self?.scrollToNewestItem(animated: true)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -343,7 +338,7 @@ class TimelineTableViewController: UIViewController {
|
||||
if let kvPair = timelineItemsDictionary.first(where: { $0.value.identifier.eventID == focussedEventID }),
|
||||
let indexPath = dataSource?.indexPath(for: kvPair.key) {
|
||||
tableView.scrollToRow(at: indexPath, at: .middle, animated: animated)
|
||||
focussedEventNeedsDisplay = false
|
||||
coordinator.send(viewAction: .scrolledToFocussedItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,9 @@ struct TimelineView: UIViewControllerRepresentable {
|
||||
if tableViewController.focussedEventID != context.viewState.timelineViewState.focussedEventID {
|
||||
tableViewController.focussedEventID = context.viewState.timelineViewState.focussedEventID
|
||||
}
|
||||
if tableViewController.focussedEventNeedsDisplay != context.viewState.timelineViewState.focussedEventNeedsDisplay {
|
||||
tableViewController.focussedEventNeedsDisplay = context.viewState.timelineViewState.focussedEventNeedsDisplay
|
||||
}
|
||||
|
||||
if tableViewController.typingMembers.members != context.viewState.typingMembers {
|
||||
tableViewController.setTypingMembers(context.viewState.typingMembers)
|
||||
|
||||
Reference in New Issue
Block a user