import SFSafeSymbols import Shared import SwiftUI struct ClientEventsLogView: View { @Environment(\.dismiss) private var dismiss @StateObject private var viewModel = ClientEventsLogViewModel() @State private var showClearConfirmation = false var body: some View { List { typeFilter eventsList } .searchable(text: $viewModel.searchTerm) .refreshable { viewModel.loadEvents() } .navigationTitle(L10n.Settings.EventLog.title) .navigationBarTitleDisplayMode(.large) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { showClearConfirmation = true } label: { Text(verbatim: L10n.ClientEvents.View.clear) } .confirmationDialog( L10n.ClientEvents.View.ClearConfirm.title, isPresented: $showClearConfirmation, titleVisibility: .visible ) { Button(L10n.cancelLabel, role: .cancel) { /* no-op */ } Button(L10n.yesLabel, role: .destructive) { Current.clientEventStore.clearAllEvents() dismiss() } } } } .onAppear { viewModel.loadEvents() } } @ViewBuilder private var eventsList: some View { ForEach(filteredEvents, id: \.id) { event in listItem(event) } if filteredEvents.isEmpty { Text(verbatim: L10n.ClientEvents.noEvents) .frame(maxWidth: .infinity, alignment: .center) .listRowBackground(Color.clear) .font(.headline) .foregroundColor(.secondary) } } private var filteredEvents: [ClientEvent] { viewModel.events.filter({ event in if viewModel.searchTerm.isEmpty, viewModel.typeFilter == nil { return true } else { if viewModel.searchTerm.isEmpty { if let typeFilter = viewModel.typeFilter { return event.type == typeFilter } else { return true } } else { let containsSearchTerm = event.text.lowercased().contains(viewModel.searchTerm.lowercased()) if let typeFilter = viewModel.typeFilter { return event.type == typeFilter && containsSearchTerm } else { return containsSearchTerm } } } }) } private var typeFilter: some View { Section { ScrollView(.horizontal, showsIndicators: false) { HStack { Group { Button { viewModel.resetTypeFilter() } label: { PillView( text: L10n.ClientEvents.EventType.all, selected: viewModel.typeFilter == nil ) } ForEach(ClientEvent.EventType.allCases.sorted { e1, e2 in e1.displayText < e2.displayText }, id: \.self) { type in Button { viewModel.typeFilter = type } label: { PillView( text: type.displayText, selected: viewModel.typeFilter == type ) } } } .buttonStyle(.plain) .animation(.easeIn(duration: 0.2), value: viewModel.typeFilter) } } .listRowBackground(Color.clear) .modify { view in if #available(iOS 17.0, *) { view.scrollClipDisabled(true) } else { view } } } .modify { view in if #available(iOS 17.0, *) { view.listSectionSpacing(DesignSystem.Spaces.one) } else { view } } } private func listItem(_ event: ClientEvent) -> some View { NavigationLink { eventDescription(event) } label: { VStack(spacing: DesignSystem.Spaces.one) { HStack { Group { Group { dateTimeLabel(event.date) } .frame(maxWidth: .infinity, alignment: .leading) Text(event.type.displayText) .frame(alignment: .trailing) } .font(.subheadline) .foregroundColor(.secondary) } Text(event.text) .font(.headline) .frame(maxWidth: .infinity, alignment: .leading) } } } private func dateTimeLabel(_ date: Date) -> Text { Text(date, style: .date) + Text(" ") + Text(date, style: .time) } private func eventDescription(_ event: ClientEvent) -> some View { ScrollView { Text(event.jsonPayloadDescription ?? "--") .frame(maxWidth: .infinity, alignment: .leading) .multilineTextAlignment(.leading) .lineLimit(nil) .textSelection(.enabled) .padding() } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .navigationTitle(dateTimeLabel(event.date)) .navigationBarTitleDisplayMode(.inline) } } #Preview { ClientEventsLogView() }