// // Copyright 2025 Element Creations Ltd. // Copyright 2022-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 SwiftUI struct PinnedEventsTimelineScreen: View { @ObservedObject var context: PinnedEventsTimelineScreenViewModel.Context @ObservedObject var timelineContext: TimelineViewModel.Context private var isMessageSelectionActive: Bool { timelineContext.viewState.messageSelection.isActive } private var title: String { let pinnedEventIDs = timelineContext.viewState.pinnedEventIDs guard !pinnedEventIDs.isEmpty else { return L10n.screenPinnedTimelineScreenTitleEmpty } return L10n.screenPinnedTimelineScreenTitle(pinnedEventIDs.count) } var body: some View { content .navigationTitle(title) .navigationBarTitleDisplayMode(.inline) .toolbar { toolbar } .safeAreaInset(edge: .bottom, spacing: 0) { if isMessageSelectionActive { TimelineMessageSelectionActionBar(context: timelineContext) } } .background(.compound.bgCanvasDefault) .interactiveDismissDisabled() .timelineMediaPreview(viewModel: $context.mediaPreviewViewModel) } @ViewBuilder private var content: some View { if timelineContext.viewState.pinnedEventIDs.isEmpty { VStack(spacing: 16) { BigIcon(icon: \.pin) Text(L10n.screenPinnedTimelineEmptyStateHeadline) .font(.compound.headingSMSemibold) .foregroundStyle(.compound.textPrimary) .multilineTextAlignment(.center) Text(L10n.screenPinnedTimelineEmptyStateDescription(L10n.actionPin)) .font(.compound.bodyMD) .foregroundStyle(.compound.textSecondary) .multilineTextAlignment(.center) Spacer() } .padding(.top, 48) .padding(.horizontal, 16) } else { TimelineView(timelineContext: timelineContext) } } @ToolbarContentBuilder private var toolbar: some ToolbarContent { if isMessageSelectionActive { TimelineMessageSelectionToolbar { timelineContext.send(viewAction: .clearMessageSelection) } } else { ToolbarItem(placement: .confirmationAction) { Button(L10n.actionClose) { context.send(viewAction: .close) } } } } } // MARK: - Previews struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview { static let viewModel = PinnedEventsTimelineScreenViewModel(roomProxy: JoinedRoomProxyMock(.init()), userIndicatorController: UserIndicatorControllerMock(), userSettings: .mock(), analyticsService: AnalyticsServiceMock(.init())) static let emptyTimelineViewModel = makeTimelineViewModel(timelineItems: []) static let selectingTimelineViewModel = makeTimelineViewModel(timelineItems: TimelineFixtures.default, isSelecting: true) static var previews: some View { ElementNavigationStack { PinnedEventsTimelineScreen(context: viewModel.context, timelineContext: emptyTimelineViewModel.context) } .previewDisplayName("Empty") ElementNavigationStack { PinnedEventsTimelineScreen(context: viewModel.context, timelineContext: selectingTimelineViewModel.context) } .previewDisplayName("Selecting") .snapshotPreferences(expect: selectingTimelineViewModel.context.$viewState.map(\.messageSelection.isActive)) } static func makeTimelineViewModel(timelineItems: [RoomTimelineItemProtocol], isSelecting: Bool = false) -> TimelineViewModel { let eventIDs = timelineItems.compactMap { ($0 as? EventBasedTimelineItemProtocol)?.id.eventID } let timelineController = TimelineControllerMock(.init(timelineKind: .pinned, timelineItems: timelineItems)) let userSettings = UserSettings.mock() userSettings.app.messageMultiSelectEnabled = isSelecting let userSession = UserSessionMock(.init(userSettings: userSettings)) let timelineViewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room", pinnedEventIDs: Set(eventIDs))), timelineController: timelineController, userSession: userSession, mediaPlayerProvider: MediaPlayerProviderMock(), userIndicatorController: UserIndicatorControllerMock(), appMediator: AppMediatorMock(.init()), analyticsService: AnalyticsServiceMock(.init()), emojiProvider: EmojiProvider(userSettings: .mock()), linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) if isSelecting { timelineViewModel.state.messageSelection.isActive = true timelineViewModel.state.messageSelection.selectedEventIDs = Set(eventIDs.prefix(2)) } return timelineViewModel } }