* replace NavigationStack with ElementNavigationStack to allow the content to be rendered without a NavigationStack in a11y tests * fix a11y tests * update xcodeproject * swiftformat fix * use iOS 26.1 for CI * use a wrapper to solve the issue for a11y tests * ElementNavigationStack only uses the trick in DEBUG mode, and added a swiftlint rule to prevent the usage of NavigationStack
159 lines
6.3 KiB
Swift
159 lines
6.3 KiB
Swift
//
|
|
// 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 RoomPollsHistoryScreen: View {
|
|
@ObservedObject var context: RoomPollsHistoryScreenViewModel.Context
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .center, spacing: 16) {
|
|
modePicker
|
|
|
|
polls
|
|
|
|
if context.viewState.pollTimelineItems.isEmpty {
|
|
emptyStateMessage
|
|
.padding(.top, 48)
|
|
}
|
|
|
|
if context.viewState.canBackPaginate {
|
|
loadMoreButton
|
|
.padding(.top, context.viewState.pollTimelineItems.isEmpty ? 0 : 16)
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
.alert(item: $context.alertInfo)
|
|
.scrollContentBackground(.hidden)
|
|
.background(.compound.bgSubtleSecondaryLevel0)
|
|
.navigationTitle(context.viewState.title)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private var modePicker: some View {
|
|
Picker("", selection: $context.filter) {
|
|
ForEach(context.viewState.filters, id: \.self) { filter in
|
|
Text(filter.description)
|
|
}
|
|
}
|
|
.pickerStyle(.segmented)
|
|
.readableFrame(maxWidth: 475)
|
|
.onChange(of: context.filter) { _, newValue in
|
|
context.send(viewAction: .filter(newValue))
|
|
}
|
|
}
|
|
|
|
private var polls: some View {
|
|
ForEach(context.viewState.pollTimelineItems, id: \.item.id.eventID) { pollTimelineItem in
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text(DateFormatter.pollTimestamp.string(from: pollTimelineItem.timestamp))
|
|
.font(.compound.bodySM)
|
|
.foregroundColor(.compound.textSecondary)
|
|
PollView(poll: pollTimelineItem.item.poll,
|
|
state: .full(isEditable: pollTimelineItem.item.isEditable), sender: pollTimelineItem.item.sender) { action in
|
|
switch action {
|
|
case .selectOption(let optionID):
|
|
guard let pollStartID = pollTimelineItem.item.id.eventID else { return }
|
|
context.send(viewAction: .sendPollResponse(pollStartID: pollStartID, optionID: optionID))
|
|
case .edit:
|
|
guard let pollStartID = pollTimelineItem.item.id.eventID else { return }
|
|
context.send(viewAction: .edit(pollStartID: pollStartID, poll: pollTimelineItem.item.poll))
|
|
case .end:
|
|
guard let pollStartID = pollTimelineItem.item.id.eventID else { return }
|
|
context.send(viewAction: .end(pollStartID: pollStartID))
|
|
}
|
|
}
|
|
}
|
|
.padding(.init(top: 12, leading: 12, bottom: 12, trailing: 12))
|
|
.background(.compound.bgCanvasDefaultLevel1)
|
|
.cornerRadius(12, corners: .allCorners)
|
|
}
|
|
}
|
|
|
|
private var emptyStateMessage: some View {
|
|
Text(context.viewState.emptyStateMessage)
|
|
.font(.compound.bodyLG)
|
|
.foregroundColor(.compound.textSecondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.vertical, 12)
|
|
}
|
|
|
|
private var loadMoreButton: some View {
|
|
Button {
|
|
context.send(viewAction: .loadMore)
|
|
} label: {
|
|
Text(L10n.actionLoadMore)
|
|
.font(.compound.bodyLGSemibold)
|
|
.padding(.horizontal, 12)
|
|
}
|
|
.accessibilityIdentifier(A11yIdentifiers.roomPollsHistoryScreen.loadMore)
|
|
.buttonStyle(.compound(.secondary))
|
|
.fixedSize()
|
|
.disabled(context.viewState.isBackPaginating)
|
|
}
|
|
}
|
|
|
|
private extension DateFormatter {
|
|
static let pollTimestamp: DateFormatter = {
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.dateStyle = .short
|
|
return dateFormatter
|
|
}()
|
|
}
|
|
|
|
// MARK: - Previews
|
|
|
|
struct RoomPollsHistoryScreen_Previews: PreviewProvider, TestablePreview {
|
|
static let viewModelEmpty: RoomPollsHistoryScreenViewModel = {
|
|
let timelineController = MockTimelineController()
|
|
timelineController.timelineItems = []
|
|
let roomProxyMockConfiguration = JoinedRoomProxyMockConfiguration(name: "Polls")
|
|
return RoomPollsHistoryScreenViewModel(pollInteractionHandler: PollInteractionHandlerMock(),
|
|
timelineController: timelineController,
|
|
userIndicatorController: UserIndicatorControllerMock())
|
|
}()
|
|
|
|
static let viewModel: RoomPollsHistoryScreenViewModel = {
|
|
let timelineController = MockTimelineController()
|
|
|
|
let polls = [PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: false)),
|
|
PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: true)),
|
|
PollRoomTimelineItem.mock(poll: .emptyDisclosed, isEditable: true)]
|
|
|
|
timelineController.timelineItems = polls
|
|
|
|
for i in 0..<polls.count {
|
|
let item = polls[i]
|
|
let date: Date! = DateComponents(calendar: .current, timeZone: .gmt, year: 2023, month: 12, day: 1 + i, hour: 12).date
|
|
timelineController.timelineItemsTimestamp[item.id] = date
|
|
}
|
|
|
|
let roomProxyMockConfiguration = JoinedRoomProxyMockConfiguration(name: "Polls", timelineStartReached: true)
|
|
return RoomPollsHistoryScreenViewModel(pollInteractionHandler: PollInteractionHandlerMock(),
|
|
timelineController: timelineController,
|
|
userIndicatorController: UserIndicatorControllerMock())
|
|
}()
|
|
|
|
static var previews: some View {
|
|
ElementNavigationStack {
|
|
RoomPollsHistoryScreen(context: viewModelEmpty.context)
|
|
}
|
|
.previewDisplayName("No polls")
|
|
|
|
ElementNavigationStack {
|
|
RoomPollsHistoryScreen(context: viewModel.context)
|
|
}
|
|
.previewDisplayName("polls")
|
|
}
|
|
}
|