Add polls "creator view" (#1765)

* Add end button in PollRoomTimelineView

* Add creator logic

* Refine PollRoomTimelineView previews

* Add UI tests

* Update preview tests
This commit is contained in:
Alfonso Grillo 2023-09-21 16:59:17 +02:00 committed by GitHub
parent b8e5611c00
commit 37afc8c253
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 158 additions and 56 deletions

View File

@ -21,29 +21,33 @@ extension Poll {
pollKind: Poll.Kind = .disclosed, pollKind: Poll.Kind = .disclosed,
options: [Poll.Option], options: [Poll.Option],
votes: [String: [String]] = [:], votes: [String: [String]] = [:],
ended: Bool = false) -> Self { ended: Bool = false,
createdByAccountOwner: Bool = false) -> Self {
.init(question: question, .init(question: question,
kind: pollKind, kind: pollKind,
maxSelections: 1, maxSelections: 1,
options: options, options: options,
votes: votes, votes: votes,
endDate: ended ? Date() : nil) endDate: ended ? Date() : nil,
createdByAccountOwner: createdByAccountOwner)
} }
static var disclosed: Self { static func disclosed(createdByAccountOwner: Bool = false) -> Self {
mock(question: "What country do you like most?", mock(question: "What country do you like most?",
pollKind: .disclosed, pollKind: .disclosed,
options: [.mock(text: "Italy 🇮🇹", votes: 5, allVotes: 10, isWinning: true), options: [.mock(text: "Italy 🇮🇹", votes: 5, allVotes: 10, isWinning: true),
.mock(text: "China 🇨🇳", votes: 3, allVotes: 10), .mock(text: "China 🇨🇳", votes: 3, allVotes: 10),
.mock(text: "USA 🇺🇸", votes: 2, allVotes: 10)]) .mock(text: "USA 🇺🇸", votes: 2, allVotes: 10)],
createdByAccountOwner: createdByAccountOwner)
} }
static var undisclosed: Self { static func undisclosed(createdByAccountOwner: Bool = false) -> Self {
mock(question: "What country do you like most?", mock(question: "What country do you like most?",
pollKind: .undisclosed, pollKind: .undisclosed,
options: [.mock(text: "Italy 🇮🇹", votes: 5, allVotes: 10, isWinning: true), options: [.mock(text: "Italy 🇮🇹", votes: 5, allVotes: 10, isWinning: true),
.mock(text: "China 🇨🇳", votes: 3, allVotes: 10, isSelected: true), .mock(text: "China 🇨🇳", votes: 3, allVotes: 10, isSelected: true),
.mock(text: "USA 🇺🇸", votes: 2, allVotes: 10)]) .mock(text: "USA 🇺🇸", votes: 2, allVotes: 10)],
createdByAccountOwner: createdByAccountOwner)
} }
static var endedDisclosed: Self { static var endedDisclosed: Self {
@ -77,12 +81,12 @@ extension Poll.Option {
} }
extension PollRoomTimelineItem { extension PollRoomTimelineItem {
static func mock(poll: Poll) -> Self { static func mock(poll: Poll, isOutgoing: Bool = true) -> Self {
.init(id: .random, .init(id: .init(timelineID: UUID().uuidString, eventID: UUID().uuidString),
poll: poll, poll: poll,
body: "poll", body: "poll",
timestamp: "Now", timestamp: "Now",
isOutgoing: true, isOutgoing: isOutgoing,
isEditable: false, isEditable: false,
sender: .init(id: "userID"), sender: .init(id: "userID"),
properties: .init()) properties: .init())

View File

@ -60,7 +60,8 @@ enum RoomScreenViewAction {
case sendReadReceiptIfNeeded(TimelineItemIdentifier) case sendReadReceiptIfNeeded(TimelineItemIdentifier)
case paginateBackwards case paginateBackwards
case selectedPollOption(pollStartID: String, optionID: String) case selectedPollOption(pollStartID: String, optionID: String)
case endPoll(pollStartID: String)
case timelineItemMenu(itemID: TimelineItemIdentifier) case timelineItemMenu(itemID: TimelineItemIdentifier)
case timelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction) case timelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction)

View File

@ -154,6 +154,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.longPressDisabledItemID = nil state.longPressDisabledItemID = nil
case .disableLongPress(let itemID): case .disableLongPress(let itemID):
state.longPressDisabledItemID = itemID state.longPressDisabledItemID = itemID
case let .endPoll(pollStartID):
endPoll(pollStartID: pollStartID)
} }
} }

View File

@ -29,22 +29,9 @@ struct PollRoomTimelineView: View {
TimelineStyler(timelineItem: timelineItem) { TimelineStyler(timelineItem: timelineItem) {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
questionView questionView
optionsView
ForEach(poll.options, id: \.id) { option in
Button {
guard let eventID, !option.isSelected else { return }
context.send(viewAction: .selectedPollOption(pollStartID: eventID, optionID: option.id))
feedbackGenerator.impactOccurred()
} label: {
PollOptionView(pollOption: option,
showVotes: showVotes,
isFinalResult: poll.hasEnded)
.foregroundColor(progressBarColor(for: option))
}
.disabled(poll.hasEnded || eventID == nil)
}
summaryView summaryView
toolbarView
} }
.frame(maxWidth: 450) .frame(maxWidth: 450)
} }
@ -74,6 +61,22 @@ struct PollRoomTimelineView: View {
} }
} }
private var optionsView: some View {
ForEach(poll.options, id: \.id) { option in
Button {
guard let eventID, !option.isSelected else { return }
context.send(viewAction: .selectedPollOption(pollStartID: eventID, optionID: option.id))
feedbackGenerator.impactOccurred()
} label: {
PollOptionView(pollOption: option,
showVotes: showVotes,
isFinalResult: poll.hasEnded)
.foregroundColor(progressBarColor(for: option))
}
.disabled(poll.hasEnded || eventID == nil)
}
}
@ViewBuilder @ViewBuilder
private var summaryView: some View { private var summaryView: some View {
if let summaryText = poll.summaryText { if let summaryText = poll.summaryText {
@ -85,6 +88,28 @@ struct PollRoomTimelineView: View {
} }
} }
@ViewBuilder
private var toolbarView: some View {
if !poll.hasEnded, poll.createdByAccountOwner, let eventID {
Button {
context.send(viewAction: .endPoll(pollStartID: eventID))
} label: {
Text(L10n.actionEndPoll)
.lineLimit(2, reservesSpace: false)
.font(.compound.bodyLGSemibold)
.foregroundColor(.compound.textOnSolidPrimary)
.padding(.horizontal, 24)
.padding(.vertical, 10)
.frame(maxWidth: .infinity)
.background {
Capsule()
.foregroundColor(.compound.bgActionPrimaryRest)
}
}
.padding(.top, 8)
}
}
private func progressBarColor(for option: Poll.Option) -> Color { private func progressBarColor(for option: Poll.Option) -> Color {
if poll.hasEnded { if poll.hasEnded {
return option.isWinning ? .compound.textActionAccent : .compound.textDisabled return option.isWinning ? .compound.textActionAccent : .compound.textDisabled
@ -121,12 +146,12 @@ struct PollRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static let viewModel = RoomScreenViewModel.mock static let viewModel = RoomScreenViewModel.mock
static var previews: some View { static var previews: some View {
PollRoomTimelineView(timelineItem: .mock(poll: .disclosed)) PollRoomTimelineView(timelineItem: .mock(poll: .disclosed(), isOutgoing: false))
.environment(\.timelineStyle, .bubbles) .environment(\.timelineStyle, .bubbles)
.environmentObject(viewModel.context) .environmentObject(viewModel.context)
.previewDisplayName("Disclosed, Bubble") .previewDisplayName("Disclosed, Bubble")
PollRoomTimelineView(timelineItem: .mock(poll: .undisclosed)) PollRoomTimelineView(timelineItem: .mock(poll: .undisclosed(), isOutgoing: false))
.environment(\.timelineStyle, .bubbles) .environment(\.timelineStyle, .bubbles)
.environmentObject(viewModel.context) .environmentObject(viewModel.context)
.previewDisplayName("Undisclosed, Bubble") .previewDisplayName("Undisclosed, Bubble")
@ -141,12 +166,17 @@ struct PollRoomTimelineView_Previews: PreviewProvider, TestablePreview {
.environmentObject(viewModel.context) .environmentObject(viewModel.context)
.previewDisplayName("Ended, Undisclosed, Bubble") .previewDisplayName("Ended, Undisclosed, Bubble")
PollRoomTimelineView(timelineItem: .mock(poll: .disclosed)) PollRoomTimelineView(timelineItem: .mock(poll: .disclosed(createdByAccountOwner: true)))
.environment(\.timelineStyle, .bubbles)
.environmentObject(viewModel.context)
.previewDisplayName("Creator, disclosed, Bubble")
PollRoomTimelineView(timelineItem: .mock(poll: .disclosed(), isOutgoing: false))
.environment(\.timelineStyle, .plain) .environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context) .environmentObject(viewModel.context)
.previewDisplayName("Disclosed, Plain") .previewDisplayName("Disclosed, Plain")
PollRoomTimelineView(timelineItem: .mock(poll: .undisclosed)) PollRoomTimelineView(timelineItem: .mock(poll: .undisclosed(), isOutgoing: false))
.environment(\.timelineStyle, .plain) .environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context) .environmentObject(viewModel.context)
.previewDisplayName("Undisclosed, Plain") .previewDisplayName("Undisclosed, Plain")
@ -160,5 +190,10 @@ struct PollRoomTimelineView_Previews: PreviewProvider, TestablePreview {
.environment(\.timelineStyle, .plain) .environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context) .environmentObject(viewModel.context)
.previewDisplayName("Ended, Undisclosed, Plain") .previewDisplayName("Ended, Undisclosed, Plain")
PollRoomTimelineView(timelineItem: .mock(poll: .disclosed(createdByAccountOwner: true)))
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
.previewDisplayName("Creator, disclosed, Plain")
} }
} }

View File

@ -221,14 +221,18 @@ enum RoomTimelineItemFixtures {
} }
static var disclosedPolls: [RoomTimelineItemProtocol] { static var disclosedPolls: [RoomTimelineItemProtocol] {
[PollRoomTimelineItem.mock(poll: .disclosed), [PollRoomTimelineItem.mock(poll: .disclosed(), isOutgoing: false),
PollRoomTimelineItem.mock(poll: .endedDisclosed)] PollRoomTimelineItem.mock(poll: .endedDisclosed)]
} }
static var undisclosedPolls: [RoomTimelineItemProtocol] { static var undisclosedPolls: [RoomTimelineItemProtocol] {
[PollRoomTimelineItem.mock(poll: .undisclosed), [PollRoomTimelineItem.mock(poll: .undisclosed(), isOutgoing: false),
PollRoomTimelineItem.mock(poll: .endedUndisclosed)] PollRoomTimelineItem.mock(poll: .endedUndisclosed)]
} }
static var outgoingPolls: [RoomTimelineItemProtocol] {
[PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: true), isOutgoing: true)]
}
} }
private extension TextRoomTimelineItem { private extension TextRoomTimelineItem {

View File

@ -34,6 +34,8 @@ struct Poll: Equatable {
let options: [Option] let options: [Option]
let votes: [String: [String]] let votes: [String: [String]]
let endDate: Date? let endDate: Date?
/// Whether the poll has been created by the account owner
let createdByAccountOwner: Bool
var hasEnded: Bool { var hasEnded: Bool {
endDate != nil endDate != nil

View File

@ -389,7 +389,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
maxSelections: Int(maxSelections), maxSelections: Int(maxSelections),
options: options, options: options,
votes: votes, votes: votes,
endDate: endTime.map { Date(timeIntervalSince1970: TimeInterval($0 / 1000)) }) endDate: endTime.map { Date(timeIntervalSince1970: TimeInterval($0 / 1000)) },
createdByAccountOwner: eventItemProxy.sender.id == userID)
return PollRoomTimelineItem(id: eventItemProxy.id, return PollRoomTimelineItem(id: eventItemProxy.id,
poll: poll, poll: poll,

View File

@ -354,11 +354,39 @@ class MockScreen: Identifiable {
navigationStackCoordinator.setRootCoordinator(coordinator) navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator return navigationStackCoordinator
case .roomWithDisclosedPolls, .roomWithUndisclosedPolls: case .roomWithDisclosedPolls:
let navigationStackCoordinator = NavigationStackCoordinator() let navigationStackCoordinator = NavigationStackCoordinator()
let timelineController = MockRoomTimelineController() let timelineController = MockRoomTimelineController()
timelineController.timelineItems = id == .roomWithDisclosedPolls ? RoomTimelineItemFixtures.disclosedPolls : RoomTimelineItemFixtures.undisclosedPolls timelineController.timelineItems = RoomTimelineItemFixtures.disclosedPolls
timelineController.incomingItems = []
let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(with: .init(displayName: "Polls timeline", avatarURL: URL.picturesDirectory)),
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider())
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomWithUndisclosedPolls:
let navigationStackCoordinator = NavigationStackCoordinator()
let timelineController = MockRoomTimelineController()
timelineController.timelineItems = RoomTimelineItemFixtures.undisclosedPolls
timelineController.incomingItems = []
let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(with: .init(displayName: "Polls timeline", avatarURL: URL.picturesDirectory)),
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider())
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomWithOutgoingPolls:
let navigationStackCoordinator = NavigationStackCoordinator()
let timelineController = MockRoomTimelineController()
timelineController.timelineItems = RoomTimelineItemFixtures.outgoingPolls
timelineController.incomingItems = [] timelineController.incomingItems = []
let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(with: .init(displayName: "Polls timeline", avatarURL: URL.picturesDirectory)), let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(with: .init(displayName: "Polls timeline", avatarURL: URL.picturesDirectory)),
timelineController: timelineController, timelineController: timelineController,

View File

@ -48,6 +48,7 @@ enum UITestsScreenIdentifier: String {
case roomLayoutBottom case roomLayoutBottom
case roomWithDisclosedPolls case roomWithDisclosedPolls
case roomWithUndisclosedPolls case roomWithUndisclosedPolls
case roomWithOutgoingPolls
case sessionVerification case sessionVerification
case userSessionScreen case userSessionScreen
case userSessionScreenReply case userSessionScreenReply

View File

@ -173,6 +173,12 @@ class RoomScreenUITests: XCTestCase {
try await app.assertScreenshot(.roomWithUndisclosedPolls) try await app.assertScreenshot(.roomWithUndisclosedPolls)
} }
func testTimelineOutgoingPolls() async throws {
let app = Application.launch(.roomWithOutgoingPolls)
try await app.assertScreenshot(.roomWithOutgoingPolls)
}
// MARK: - Helper Methods // MARK: - Helper Methods
private func performOperation(_ operation: UITestsSignal, using client: UITestsSignalling.Client) async throws { private func performOperation(_ operation: UITestsSignal, using client: UITestsSignalling.Client) async throws {

Binary file not shown.

Binary file not shown.