mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
UI POC for room list filters (#2382)
Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RoomListFilterView: View {
|
||||
let filter: RoomListFilter
|
||||
@StateObject var state: RoomListFiltersState
|
||||
|
||||
var body: some View {
|
||||
let binding = Binding<Bool>(get: {
|
||||
state.isEnabled(filter)
|
||||
}, set: { isEnabled, _ in
|
||||
state.set(filter, isEnabled: isEnabled)
|
||||
})
|
||||
Toggle(isOn: binding) {
|
||||
Text(filter.localizedName)
|
||||
}
|
||||
.toggleStyle(FilterToggleStyle())
|
||||
}
|
||||
}
|
||||
|
||||
struct RoomListFilterView_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
RoomListFilterView(filter: .people, state: .init())
|
||||
RoomListFilterView(filter: .people, state: .init(enabledFilters: [.people]))
|
||||
}
|
||||
}
|
||||
|
||||
private struct FilterToggleStyle: ToggleStyle {
|
||||
private func strokeColor(isOn: Bool) -> Color {
|
||||
isOn ? .compound.bgActionPrimaryRest : .compound.borderInteractiveSecondary
|
||||
}
|
||||
|
||||
private func backgroundColor(isOn: Bool) -> Color {
|
||||
isOn ? .compound.bgActionPrimaryRest : .compound.bgCanvasDefault
|
||||
}
|
||||
|
||||
private func foregroundColor(isOn: Bool) -> Color {
|
||||
isOn ? .compound.textOnSolidPrimary : .compound.textPrimary
|
||||
}
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
let shape = RoundedRectangle(cornerRadius: 20)
|
||||
configuration.label
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundColor(foregroundColor(isOn: configuration.isOn))
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
.background(shape.fill(backgroundColor(isOn: configuration.isOn)))
|
||||
.overlay {
|
||||
shape
|
||||
.inset(by: 0.5)
|
||||
.stroke(strokeColor(isOn: configuration.isOn))
|
||||
}
|
||||
.drawingGroup()
|
||||
// The button breaks the animation for some reason, so better to use the label directly with an onTapGesture
|
||||
.onTapGesture {
|
||||
withAnimation(.elementDefault) {
|
||||
configuration.isOn.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RoomListFiltersView: View {
|
||||
@StateObject var state: RoomListFiltersState
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal) {
|
||||
LazyHStack(spacing: 8) {
|
||||
if state.isFiltering {
|
||||
clearButton
|
||||
} else {
|
||||
// This solves a weird issue withe the LazyHStack
|
||||
// where it is resized when the button appears and disappears
|
||||
clearButton
|
||||
.hidden()
|
||||
.frame(width: 0)
|
||||
}
|
||||
ForEach(state.sortedEnabledFilters) { filter in
|
||||
RoomListFilterView(filter: filter, state: state)
|
||||
}
|
||||
ForEach(state.sortedAvailableFilters) { filter in
|
||||
RoomListFilterView(filter: filter, state: state)
|
||||
}
|
||||
}
|
||||
.padding(.leading, !state.isFiltering ? 8 : 16)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
.scrollIndicators(.hidden)
|
||||
}
|
||||
|
||||
private var clearButton: some View {
|
||||
Button(action: {
|
||||
withAnimation(.elementDefault) {
|
||||
state.clearFilters()
|
||||
}
|
||||
}, label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 24))
|
||||
.foregroundColor(.compound.bgActionPrimaryRest)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct RoomListFiltersView_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
RoomListFiltersView(state: .init())
|
||||
RoomListFiltersView(state: .init(enabledFilters: [.rooms, .favourites]))
|
||||
}
|
||||
}
|
||||
@@ -52,10 +52,12 @@ struct HomeScreen: View {
|
||||
.layoutPriority(1)
|
||||
}
|
||||
case .rooms:
|
||||
topSection
|
||||
|
||||
LazyVStack(spacing: 0) {
|
||||
HomeScreenRoomList(context: context)
|
||||
LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
|
||||
Section {
|
||||
HomeScreenRoomList(context: context)
|
||||
} header: {
|
||||
topSection
|
||||
}
|
||||
}
|
||||
.searchable(text: $context.searchQuery)
|
||||
.compoundSearchField()
|
||||
@@ -191,20 +193,30 @@ struct HomeScreen: View {
|
||||
@ViewBuilder
|
||||
/// The session verification banner and invites button if either are needed.
|
||||
private var topSection: some View {
|
||||
if context.viewState.showSessionVerificationBanner {
|
||||
HomeScreenSessionVerificationBanner(context: context)
|
||||
} else if context.viewState.showRecoveryKeyConfirmationBanner {
|
||||
HomeScreenRecoveryKeyConfirmationBanner(context: context)
|
||||
}
|
||||
|
||||
if context.viewState.hasPendingInvitations, !context.isSearchFieldFocused {
|
||||
HomeScreenInvitesButton(title: L10n.actionInvitesList, hasBadge: context.viewState.hasUnreadPendingInvitations) {
|
||||
context.send(viewAction: .selectInvites)
|
||||
VStack(spacing: 0) {
|
||||
if context.viewState.shouldShowFilters {
|
||||
filters
|
||||
}
|
||||
|
||||
if context.viewState.showSessionVerificationBanner {
|
||||
HomeScreenSessionVerificationBanner(context: context)
|
||||
} else if context.viewState.showRecoveryKeyConfirmationBanner {
|
||||
HomeScreenRecoveryKeyConfirmationBanner(context: context)
|
||||
}
|
||||
|
||||
if context.viewState.hasPendingInvitations, !context.isSearchFieldFocused {
|
||||
HomeScreenInvitesButton(title: L10n.actionInvitesList, hasBadge: context.viewState.hasUnreadPendingInvitations) {
|
||||
context.send(viewAction: .selectInvites)
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.homeScreen.invites)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.homeScreen.invites)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.padding(.vertical, -8.0)
|
||||
}
|
||||
.background(Color.compound.bgCanvasDefault)
|
||||
}
|
||||
|
||||
private var filters: some View {
|
||||
RoomListFiltersView(state: context.viewState.filtersState)
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
|
||||
@@ -34,7 +34,6 @@ struct HomeScreenInvitesButton: View {
|
||||
}
|
||||
.padding(.trailing, 16.0)
|
||||
.padding(.leading, 64.0)
|
||||
.padding(.vertical, 8.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user