Skip to content

Commit

Permalink
Refine polls UI in the timeline (#1474)
Browse files Browse the repository at this point in the history
* Add summary view in PollRoomTimelineView

* Add selectedPollOption action

* Handle undisclosed polls

* Add more logics in PollRoomTimelineView

* Refine ended poll UI

* Refine poll layout

* More UI refinements

* Fix layout issue

* Refine progress bar

* Add poll mocks and UI tests

* Cleanup
  • Loading branch information
alfogrillo authored Aug 11, 2023
1 parent f641596 commit 25f66d2
Show file tree
Hide file tree
Showing 27 changed files with 345 additions and 56 deletions.
4 changes: 4 additions & 0 deletions ElementX.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A2932515EA11D3DD8A3506 /* TimelineItemBubbledStylerView.swift */; };
158A2D528CC78C4E7A8ED608 /* MockRoomTimelineControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71556206CD5E8B1F53F07178 /* MockRoomTimelineControllerFactory.swift */; };
167D00CAA13FAFB822298021 /* MediaProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A81CCC2516D9CF9322DF01 /* MediaProviderTests.swift */; };
16CBD087038DE3815CDA512C /* PollMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D38391154120264910D19528 /* PollMock.swift */; };
1702981A8085BE4FB0EC001B /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33116993D54FADC0C721C1F /* Application.swift */; };
172E6E9A612ADCF10A62CF13 /* BugReportServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */; };
1772AFA97DDA51CF1B293A78 /* RoomAttachmentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E6A9B9DFEE964962C179DE3 /* RoomAttachmentPicker.swift */; };
Expand Down Expand Up @@ -1396,6 +1397,7 @@
D263254AFE5B7993FFBBF324 /* NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NSE.entitlements; sourceTree = "<group>"; };
D316BB02636AF2174F2580E6 /* SoftLogoutScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenViewModelProtocol.swift; sourceTree = "<group>"; };
D33116993D54FADC0C721C1F /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
D38391154120264910D19528 /* PollMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollMock.swift; sourceTree = "<group>"; };
D39D7F513A36C9C1951DB44C /* AnalyticsSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreen.swift; sourceTree = "<group>"; };
D3D455BC2423D911A62ACFB2 /* NSELogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = "<group>"; };
D49B9785E3AD7D1C15A29F2F /* MediaSourceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSourceProxy.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1913,6 +1915,7 @@
children = (
69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */,
382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */,
D38391154120264910D19528 /* PollMock.swift */,
36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */,
F5D1BAA90F3A073D91B4F16B /* RoomNotificationSettingsProxyMock.swift */,
1ABDE6F66532CBEB0E016F94 /* RoomProxyMock.swift */,
Expand Down Expand Up @@ -4515,6 +4518,7 @@
962A4F8AD6312804E2C6BB6E /* PhotoLibraryPicker.swift in Sources */,
9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */,
1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */,
16CBD087038DE3815CDA512C /* PollMock.swift in Sources */,
6B4BF4A6450F55939B49FAEF /* PollOptionView.swift in Sources */,
864C0D3A4077BF433DBC691F /* PollRoomTimelineItem.swift in Sources */,
153E22E8227F46545E5D681C /* PollRoomTimelineView.swift in Sources */,
Expand Down
6 changes: 0 additions & 6 deletions ElementX/Resources/Assets.xcassets/images/polls/Contents.json

This file was deleted.

Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "equalizer.pdf",
"filename" : "timeline-poll.pdf",
"idiom" : "universal"
}
],
Expand Down
Binary file not shown.
3 changes: 3 additions & 0 deletions ElementX/Resources/Localizations/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@
"common_password" = "Password";
"common_people" = "People";
"common_permalink" = "Permalink";
"common_poll_final_votes" = "Final votes: %1$@";
"common_poll_total_votes" = "Total votes: %1$@";
"common_poll_undisclosed_text" = "Results will show after the poll has ended";
"common_privacy_policy" = "Privacy policy";
"common_reactions" = "Reactions";
"common_refreshing" = "Refreshing…";
Expand Down
2 changes: 1 addition & 1 deletion ElementX/Sources/Generated/Assets.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ internal enum Asset {
internal static let locationPin = ImageAsset(name: "images/location-pin")
internal static let locationPointerFull = ImageAsset(name: "images/location-pointer-full")
internal static let locationPointer = ImageAsset(name: "images/location-pointer")
internal static let equalizer = ImageAsset(name: "images/equalizer")
internal static let timelineComposerSendMessage = ImageAsset(name: "images/timeline-composer-send-message")
internal static let timelinePoll = ImageAsset(name: "images/timeline-poll")
internal static let timelineReactionAddMore = ImageAsset(name: "images/timeline-reaction-add-more")
internal static let waitingGradient = ImageAsset(name: "images/waiting-gradient")
}
Expand Down
10 changes: 10 additions & 0 deletions ElementX/Sources/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,16 @@ public enum L10n {
public static var commonPeople: String { return L10n.tr("Localizable", "common_people") }
/// Permalink
public static var commonPermalink: String { return L10n.tr("Localizable", "common_permalink") }
/// Final votes: %1$@
public static func commonPollFinalVotes(_ p1: Any) -> String {
return L10n.tr("Localizable", "common_poll_final_votes", String(describing: p1))
}
/// Total votes: %1$@
public static func commonPollTotalVotes(_ p1: Any) -> String {
return L10n.tr("Localizable", "common_poll_total_votes", String(describing: p1))
}
/// Results will show after the poll has ended
public static var commonPollUndisclosedText: String { return L10n.tr("Localizable", "common_poll_undisclosed_text") }
/// Plural format key: "%#@COUNT@"
public static func commonPollVotesCount(_ p1: Int) -> String {
return L10n.tr("Localizable", "common_poll_votes_count", p1)
Expand Down
90 changes: 90 additions & 0 deletions ElementX/Sources/Mocks/PollMock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// Copyright 2023 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 Foundation

extension Poll {
static func mock(question: String,
pollKind: Poll.Kind = .disclosed,
options: [Poll.Option],
votes: [String: [String]] = [:],
ended: Bool = false) -> Self {
.init(question: question,
kind: pollKind,
maxSelections: 1,
options: options,
votes: votes,
endDate: ended ? Date() : nil)
}

static var disclosed: Self {
mock(question: "What country do you like most?",
pollKind: .disclosed,
options: [.mock(text: "Italy 🇮🇹", votes: 5, allVotes: 10, isWinning: true),
.mock(text: "China 🇨🇳", votes: 3, allVotes: 10),
.mock(text: "USA 🇺🇸", votes: 2, allVotes: 10)])
}

static var undisclosed: Self {
mock(question: "What country do you like most?",
pollKind: .undisclosed,
options: [.mock(text: "Italy 🇮🇹", votes: 5, allVotes: 10, isWinning: true),
.mock(text: "China 🇨🇳", votes: 3, allVotes: 10, isSelected: true),
.mock(text: "USA 🇺🇸", votes: 2, allVotes: 10)])
}

static var endedDisclosed: Self {
mock(question: "What country do you like most?",
pollKind: .disclosed,
options: [.mock(text: "Italy 🇮🇹", votes: 5, allVotes: 10, isWinning: true),
.mock(text: "China 🇨🇳", votes: 3, allVotes: 10, isSelected: true),
.mock(text: "USA 🇺🇸", votes: 2, allVotes: 10)],
ended: true)
}

static var endedUndisclosed: Self {
mock(question: "What country do you like most?",
pollKind: .undisclosed,
options: [.mock(text: "Italy 🇮🇹", votes: 5, allVotes: 10, isWinning: true),
.mock(text: "China 🇨🇳", votes: 3, allVotes: 10, isSelected: true),
.mock(text: "USA 🇺🇸", votes: 2, allVotes: 10)],
ended: true)
}
}

extension Poll.Option {
static func mock(text: String, votes: Int = 0, allVotes: Int = 0, isSelected: Bool = false, isWinning: Bool = false) -> Self {
.init(id: UUID().uuidString,
text: text,
votes: votes,
allVotes: allVotes,
isSelected: isSelected,
isWinning: isWinning)
}
}

extension PollRoomTimelineItem {
static func mock(poll: Poll) -> Self {
.init(id: .random,
poll: poll,
body: "poll",
timestamp: "Now",
isOutgoing: true,
isEditable: false,
sender: .init(id: "userID"),
properties: .init())
}
}
1 change: 1 addition & 0 deletions ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ enum RoomScreenViewAction {
case toggleReaction(key: String, itemID: TimelineItemIdentifier)
case sendReadReceiptIfNeeded(TimelineItemIdentifier)
case paginateBackwards
case selectedPollOption(poll: Poll, optionID: String)

case timelineItemMenu(itemID: TimelineItemIdentifier)
case timelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction)
Expand Down
2 changes: 2 additions & 0 deletions ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
if state.swiftUITimelineEnabled {
renderPendingTimelineItems()
}
case .selectedPollOption:
break
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,26 @@ import SwiftUI

struct PollOptionView: View {
let pollOption: Poll.Option
let showVotes: Bool
let isFinalResult: Bool

var body: some View {
HStack(alignment: .top, spacing: 8) {
HStack(alignment: .firstTextBaseline, spacing: 12) {
FormRowAccessory(kind: .multipleSelection(isSelected: pollOption.isSelected))

VStack(spacing: 10) {
HStack(alignment: .lastTextBaseline) {
Text(pollOption.text)
.font(isFinalWinningOption ? .compound.bodyLGSemibold : .compound.bodyLG)
.multilineTextAlignment(.leading)
.foregroundColor(.compound.textPrimary)
.frame(maxWidth: .infinity, alignment: .leading)

Text(L10n.commonPollVotesCount(pollOption.votes))
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
if showVotes {
Text(L10n.commonPollVotesCount(pollOption.votes))
.font(isFinalWinningOption ? .compound.bodySMSemibold : .compound.bodySM)
.foregroundColor(isFinalWinningOption ? .compound.textPrimary : .compound.textSecondary)
}
}

progressView
Expand All @@ -41,8 +48,39 @@ struct PollOptionView: View {
// MARK: - Private

private var progressView: some View {
ProgressView(value: Double(pollOption.votes) / Double(pollOption.allVotes))
.progressViewStyle(LinearProgressViewStyle(tint: .compound.textPrimary))
PollProgressView(progress: progress)
}

private var progress: Double {
switch (showVotes, pollOption.allVotes, pollOption.isSelected) {
case (true, let allVotes, _) where allVotes > 0:
return Double(pollOption.votes) / Double(allVotes)
case (false, _, true):
return 1
default:
return 0
}
}

private var isFinalWinningOption: Bool {
pollOption.isWinning && isFinalResult
}
}

private struct PollProgressView: View {
let progress: Double

var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Capsule()
.foregroundColor(.compound.borderDisabled)

Capsule()
.frame(maxWidth: progress * geometry.size.width)
}
}
.frame(height: 6)
}
}

Expand All @@ -54,13 +92,28 @@ struct PollOptionView_Previews: PreviewProvider {
text: "Italian 🇮🇹",
votes: 1,
allVotes: 10,
isSelected: true))
isSelected: true,
isWinning: false),
showVotes: false,
isFinalResult: false)

PollOptionView(pollOption: .init(id: "2",
text: "Chinese 🇨🇳",
votes: 9,
allVotes: 10,
isSelected: false,
isWinning: true),
showVotes: true,
isFinalResult: false)

PollOptionView(pollOption: .init(id: "2",
text: "Chinese 🇨🇳",
votes: 9,
allVotes: 10,
isSelected: false))
isSelected: false,
isWinning: true),
showVotes: true,
isFinalResult: true)
}
.padding()
}
Expand Down
Loading

0 comments on commit 25f66d2

Please sign in to comment.