mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
7f1b6086ae
* Add a developer flag for the search tab Introduces a globalSearchEnabled user preference (off by default) with a toggle in Developer Options, used to gate the new search experience. * Support search role tabs in the tab coordinator Migrates the NavigationTabCoordinator to SwiftUI's value-based Tab API and adds an isSearch option so a tab can adopt the iOS 26 .search role, which detaches it into the trailing search button in the tab bar. * Moves room search to a new screen Adds a new screen that filters the alternate room summary provider as you type, shows an empty state, and lists matching rooms with their matrix identifier. Supports hardware keyboard selection (arrow keys to move through results, return to open) by reaching into the system search field. * Present search in a dedicated tab Adds the search tab to the chats flow on iOS 26 when the flag is enabled, rendered as the detached .search role button. The Cmd+K shortcut now selects this tab, and the home screen's inline search is hidden while the tab is available. * Remove the old global search screen The dedicated search tab replaces the Cmd+K floating window, so the GlobalSearchScreen, its window plumbing in the WindowManager, and its presentation in the chats flow are all removed.
177 lines
8.0 KiB
Swift
177 lines
8.0 KiB
Swift
//
|
|
// Copyright 2025 Element Creations Ltd.
|
|
// Copyright 2024-2025 New Vector Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
// Please see LICENSE files in the repository root for full details.
|
|
//
|
|
|
|
import Compound
|
|
import SentrySwiftUI
|
|
import SwiftUI
|
|
|
|
struct HomeScreenContent: View {
|
|
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
|
|
|
@ObservedObject var context: HomeScreenViewModel.Context
|
|
let scrollViewAdapter: ScrollViewAdapter
|
|
|
|
@State private var topSectionHeight: CGFloat = 0
|
|
|
|
var body: some View {
|
|
roomList
|
|
.sentryTrace("\(Self.self)")
|
|
}
|
|
|
|
private var roomList: some View {
|
|
GeometryReader { geometry in
|
|
ScrollView {
|
|
switch context.viewState.roomListMode {
|
|
case .skeletons:
|
|
LazyVStack(spacing: 0) {
|
|
ForEach(context.viewState.visibleRooms) { room in
|
|
HomeScreenRoomCell(room: room, isSelected: false, mediaProvider: context.mediaProvider, action: context.send)
|
|
.redacted(reason: .placeholder)
|
|
.shimmer() // Putting this directly on the LazyVStack creates an accordion animation on iOS 16.
|
|
}
|
|
}
|
|
.disabled(true)
|
|
.accessibilityRepresentation {
|
|
Text(L10n.commonLoading)
|
|
}
|
|
case .empty:
|
|
HomeScreenEmptyStateLayout(minHeight: geometry.size.height) {
|
|
topSection
|
|
|
|
HomeScreenEmptyStateView(context: context)
|
|
.layoutPriority(1)
|
|
}
|
|
case .rooms:
|
|
LazyVStack(spacing: 0) {
|
|
Section {
|
|
if context.viewState.shouldShowEmptyFilterState {
|
|
RoomListFiltersEmptyStateView(state: context.filtersState)
|
|
.frame(maxWidth: .infinity, minHeight: max(0, geometry.size.height - topSectionHeight))
|
|
} else {
|
|
HomeScreenRoomList(context: context)
|
|
}
|
|
} header: {
|
|
topSection
|
|
}
|
|
}
|
|
.roomListSearchable(isEnabled: context.viewState.isRoomListSearchEnabled,
|
|
isSearchFieldFocused: $context.isSearchFieldFocused,
|
|
searchQuery: $context.searchQuery)
|
|
}
|
|
}
|
|
.introspect(.scrollView, on: .supportedVersions) { scrollView in
|
|
guard scrollView != scrollViewAdapter.scrollView else { return }
|
|
scrollViewAdapter.scrollView = scrollView
|
|
}
|
|
.onReceive(scrollViewAdapter.didScroll) { _ in
|
|
updateVisibleRange()
|
|
}
|
|
.onReceive(scrollViewAdapter.isScrolling) { _ in
|
|
updateVisibleRange()
|
|
}
|
|
.onChange(of: context.searchQuery) {
|
|
updateVisibleRange()
|
|
}
|
|
.onChange(of: context.viewState.visibleRooms) {
|
|
updateVisibleRange()
|
|
|
|
// We have been seeing a lot of issues around the room list not updating properly after
|
|
// rooms shifting around:
|
|
// * Tapping on the room list doesn't always take you to the right room - https://github.com/element-hq/element-x-ios/issues/2386
|
|
// * Big blank gaps in the room list - https://github.com/element-hq/element-x-ios/issues/3026
|
|
//
|
|
// We initially thought it's caused by the filters header or the geometry reader but
|
|
// the problem is still reproducible without those.
|
|
//
|
|
// As a last attempt we will manually force it to update by shifting the
|
|
// inner scroll view by a point every time the room list is updated
|
|
DispatchQueue.main.async {
|
|
guard !scrollViewAdapter.isScrolling.value, let scrollView = scrollViewAdapter.scrollView else {
|
|
return
|
|
}
|
|
|
|
let oldOffset = scrollView.contentOffset
|
|
var newOffset = scrollView.contentOffset
|
|
newOffset.y += 1
|
|
|
|
scrollView.setContentOffset(newOffset, animated: false)
|
|
scrollView.setContentOffset(oldOffset, animated: false)
|
|
}
|
|
}
|
|
.scrollDismissesKeyboard(.immediately)
|
|
.scrollDisabled(context.viewState.roomListMode == .skeletons)
|
|
.scrollBounceBehavior(context.viewState.roomListMode == .empty ? .basedOnSize : .automatic)
|
|
.animation(.elementDefault, value: context.viewState.roomListMode)
|
|
.animation(.none, value: context.viewState.visibleRooms)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var topSection: some View {
|
|
// An empty VStack causes glitches within the room list
|
|
if context.viewState.shouldShowFilters || context.viewState.shouldShowBanner {
|
|
VStack(spacing: 0) {
|
|
if context.viewState.shouldShowFilters {
|
|
RoomListFiltersView(state: $context.filtersState)
|
|
}
|
|
|
|
if case let .show(state) = context.viewState.securityBannerMode {
|
|
HomeScreenRecoveryKeyConfirmationBanner(state: state, context: context)
|
|
} else if context.viewState.shouldShowNewSoundBanner {
|
|
HomeScreenNewSoundBanner { context.send(viewAction: .dismissNewSoundBanner) }
|
|
}
|
|
}
|
|
.background(Color.compound.bgCanvasDefault)
|
|
.readHeight($topSectionHeight)
|
|
}
|
|
}
|
|
|
|
/// Often times the scroll view's content size isn't correct yet when this method is called e.g. when cancelling a search
|
|
/// Dispatch it with a delay to allow the UI to update and the computations to be correct
|
|
/// Once we move to iOS 17 we should remove all of this and use scroll anchors instead
|
|
private func updateVisibleRange() {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { delayedUpdateVisibleRange() }
|
|
}
|
|
|
|
private func delayedUpdateVisibleRange() {
|
|
guard let scrollView = scrollViewAdapter.scrollView,
|
|
scrollViewAdapter.isScrolling.value == false, // Ignore while scrolling
|
|
context.searchQuery.isEmpty == true, // Ignore while filtering
|
|
context.viewState.visibleRooms.count > 0 else {
|
|
return
|
|
}
|
|
|
|
guard scrollView.contentSize.height > scrollView.bounds.height else {
|
|
return
|
|
}
|
|
|
|
let adjustedContentSize = max(scrollView.contentSize.height - scrollView.contentInset.top - scrollView.contentInset.bottom, scrollView.bounds.height)
|
|
let cellHeight = adjustedContentSize / Double(context.viewState.visibleRooms.count)
|
|
|
|
let firstIndex = Int(max(0.0, scrollView.contentOffset.y + scrollView.contentInset.top) / cellHeight)
|
|
let lastIndex = Int(max(0.0, scrollView.contentOffset.y + scrollView.bounds.height) / cellHeight)
|
|
|
|
// This will be deduped and throttled on the view model layer
|
|
context.send(viewAction: .updateVisibleItemRange(firstIndex..<lastIndex))
|
|
}
|
|
}
|
|
|
|
private extension View {
|
|
@ViewBuilder
|
|
func roomListSearchable(isEnabled: Bool, isSearchFieldFocused: Binding<Bool>, searchQuery: Binding<String>) -> some View {
|
|
if isEnabled {
|
|
isSearching(isSearchFieldFocused)
|
|
.searchable(text: searchQuery, placement: .navigationBarDrawer(displayMode: .always))
|
|
.compoundSearchField()
|
|
.disableAutocorrection(true)
|
|
} else {
|
|
self
|
|
}
|
|
}
|
|
}
|