From 25f66d2557dc4cd437c449c2bfcf0218ffb87fd8 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 11 Aug 2023 12:25:16 +0200 Subject: [PATCH] Refine polls UI in the timeline (#1474) * 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 --- ElementX.xcodeproj/project.pbxproj | 4 + .../images/polls/Contents.json | 6 - .../polls/equalizer.imageset/equalizer.pdf | 89 ------------- .../timeline-poll.imageset}/Contents.json | 2 +- .../timeline-poll.imageset/timeline-poll.pdf | 121 +++++++++++++++++ .../en.lproj/Localizable.strings | 3 + ElementX/Sources/Generated/Assets.swift | 2 +- ElementX/Sources/Generated/Strings.swift | 10 ++ ElementX/Sources/Mocks/PollMock.swift | 90 +++++++++++++ .../Screens/RoomScreen/RoomScreenModels.swift | 1 + .../RoomScreen/RoomScreenViewModel.swift | 2 + .../View/Timeline/PollOptionView.swift | 69 ++++++++-- .../View/Timeline/PollRoomTimelineView.swift | 126 +++++++++++++----- .../Fixtures/RoomTimelineItemFixtures.swift | 10 ++ .../Items/Other/PollRoomTimelineItem.swift | 3 +- .../RoomTimelineItemFactory.swift | 17 ++- .../UITests/UITestsAppCoordinator.swift | 14 ++ .../UITests/UITestsScreenIdentifier.swift | 2 + UITests/Sources/RoomScreenUITests.swift | 14 +- ...-9th-generation.roomWithDisclosedPolls.png | 3 + ...th-generation.roomWithUndisclosedPolls.png | 3 + ...en-GB-iPhone-14.roomWithDisclosedPolls.png | 3 + ...-GB-iPhone-14.roomWithUndisclosedPolls.png | 3 + ...-9th-generation.roomWithDisclosedPolls.png | 3 + ...th-generation.roomWithUndisclosedPolls.png | 3 + ...seudo-iPhone-14.roomWithDisclosedPolls.png | 3 + ...udo-iPhone-14.roomWithUndisclosedPolls.png | 3 + 27 files changed, 465 insertions(+), 144 deletions(-) delete mode 100644 ElementX/Resources/Assets.xcassets/images/polls/Contents.json delete mode 100644 ElementX/Resources/Assets.xcassets/images/polls/equalizer.imageset/equalizer.pdf rename ElementX/Resources/Assets.xcassets/images/{polls/equalizer.imageset => timeline/timeline-poll.imageset}/Contents.json (75%) create mode 100644 ElementX/Resources/Assets.xcassets/images/timeline/timeline-poll.imageset/timeline-poll.pdf create mode 100644 ElementX/Sources/Mocks/PollMock.swift create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomWithDisclosedPolls.png create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomWithUndisclosedPolls.png create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomWithDisclosedPolls.png create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomWithUndisclosedPolls.png create mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomWithDisclosedPolls.png create mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomWithUndisclosedPolls.png create mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomWithDisclosedPolls.png create mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomWithUndisclosedPolls.png diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 1cde4d4bf..b5fb841cd 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -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 */; }; @@ -1396,6 +1397,7 @@ D263254AFE5B7993FFBBF324 /* NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NSE.entitlements; sourceTree = ""; }; D316BB02636AF2174F2580E6 /* SoftLogoutScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenViewModelProtocol.swift; sourceTree = ""; }; D33116993D54FADC0C721C1F /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; + D38391154120264910D19528 /* PollMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollMock.swift; sourceTree = ""; }; D39D7F513A36C9C1951DB44C /* AnalyticsSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreen.swift; sourceTree = ""; }; D3D455BC2423D911A62ACFB2 /* NSELogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = ""; }; D49B9785E3AD7D1C15A29F2F /* MediaSourceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSourceProxy.swift; sourceTree = ""; }; @@ -1913,6 +1915,7 @@ children = ( 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */, 382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */, + D38391154120264910D19528 /* PollMock.swift */, 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */, F5D1BAA90F3A073D91B4F16B /* RoomNotificationSettingsProxyMock.swift */, 1ABDE6F66532CBEB0E016F94 /* RoomProxyMock.swift */, @@ -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 */, diff --git a/ElementX/Resources/Assets.xcassets/images/polls/Contents.json b/ElementX/Resources/Assets.xcassets/images/polls/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/ElementX/Resources/Assets.xcassets/images/polls/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ElementX/Resources/Assets.xcassets/images/polls/equalizer.imageset/equalizer.pdf b/ElementX/Resources/Assets.xcassets/images/polls/equalizer.imageset/equalizer.pdf deleted file mode 100644 index ce6547a31..000000000 --- a/ElementX/Resources/Assets.xcassets/images/polls/equalizer.imageset/equalizer.pdf +++ /dev/null @@ -1,89 +0,0 @@ -%PDF-1.7 - -1 0 obj - << >> -endobj - -2 0 obj - << /Length 3 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 4.000000 4.000000 cm -0.196078 0.196078 0.196078 scn -8.000000 0.000000 m -9.100000 0.000000 10.000000 0.900000 10.000000 2.000000 c -10.000000 14.000000 l -10.000000 15.100000 9.100000 16.000000 8.000000 16.000000 c -6.900000 16.000000 6.000000 15.100000 6.000000 14.000000 c -6.000000 2.000000 l -6.000000 0.900000 6.900000 0.000000 8.000000 0.000000 c -h -2.000000 0.000000 m -3.100000 0.000000 4.000000 0.900000 4.000000 2.000000 c -4.000000 6.000000 l -4.000000 7.100000 3.100000 8.000000 2.000000 8.000000 c -0.900000 8.000000 0.000000 7.100000 0.000000 6.000000 c -0.000000 2.000000 l -0.000000 0.900000 0.900000 0.000000 2.000000 0.000000 c -h -12.000000 9.000000 m -12.000000 2.000000 l -12.000000 0.900000 12.900000 0.000000 14.000000 0.000000 c -15.100000 0.000000 16.000000 0.900000 16.000000 2.000000 c -16.000000 9.000000 l -16.000000 10.100000 15.100000 11.000000 14.000000 11.000000 c -12.900000 11.000000 12.000000 10.100000 12.000000 9.000000 c -h -f -n -Q - -endstream -endobj - -3 0 obj - 1014 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] - /Resources 1 0 R - /Contents 2 0 R - /Parent 5 0 R - >> -endobj - -5 0 obj - << /Kids [ 4 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -6 0 obj - << /Pages 5 0 R - /Type /Catalog - >> -endobj - -xref -0 7 -0000000000 65535 f -0000000010 00000 n -0000000034 00000 n -0000001104 00000 n -0000001127 00000 n -0000001300 00000 n -0000001374 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -1433 -%%EOF \ No newline at end of file diff --git a/ElementX/Resources/Assets.xcassets/images/polls/equalizer.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/images/timeline/timeline-poll.imageset/Contents.json similarity index 75% rename from ElementX/Resources/Assets.xcassets/images/polls/equalizer.imageset/Contents.json rename to ElementX/Resources/Assets.xcassets/images/timeline/timeline-poll.imageset/Contents.json index a8e3b81fd..1fc3f5c11 100644 --- a/ElementX/Resources/Assets.xcassets/images/polls/equalizer.imageset/Contents.json +++ b/ElementX/Resources/Assets.xcassets/images/timeline/timeline-poll.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "equalizer.pdf", + "filename" : "timeline-poll.pdf", "idiom" : "universal" } ], diff --git a/ElementX/Resources/Assets.xcassets/images/timeline/timeline-poll.imageset/timeline-poll.pdf b/ElementX/Resources/Assets.xcassets/images/timeline/timeline-poll.imageset/timeline-poll.pdf new file mode 100644 index 000000000..302d8eee5 --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/images/timeline/timeline-poll.imageset/timeline-poll.pdf @@ -0,0 +1,121 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.750000 2.750000 cm +0.105882 0.113725 0.133333 scn +4.583333 3.666666 m +4.843056 3.666666 5.060764 3.754514 5.236458 3.930208 c +5.412153 4.105903 5.500000 4.323611 5.500000 4.583333 c +5.500000 9.166666 l +5.500000 9.426389 5.412153 9.644096 5.236458 9.819792 c +5.060764 9.995485 4.843056 10.083333 4.583333 10.083333 c +4.323611 10.083333 4.105903 9.995485 3.930208 9.819792 c +3.754514 9.644096 3.666667 9.426389 3.666667 9.166666 c +3.666667 4.583333 l +3.666667 4.323611 3.754514 4.105903 3.930208 3.930208 c +4.105903 3.754514 4.323611 3.666666 4.583333 3.666666 c +h +8.250000 3.666666 m +8.509723 3.666666 8.727430 3.754514 8.903125 3.930208 c +9.078819 4.105903 9.166667 4.323611 9.166667 4.583333 c +9.166667 11.916666 l +9.166667 12.176389 9.078819 12.394097 8.903125 12.569792 c +8.727430 12.745486 8.509723 12.833333 8.250000 12.833333 c +7.990278 12.833333 7.772570 12.745486 7.596875 12.569792 c +7.421181 12.394097 7.333333 12.176389 7.333333 11.916666 c +7.333333 4.583333 l +7.333333 4.323611 7.421181 4.105903 7.596875 3.930208 c +7.772570 3.754514 7.990278 3.666666 8.250000 3.666666 c +h +11.916667 3.666666 m +12.176389 3.666666 12.394097 3.754514 12.569792 3.930208 c +12.745486 4.105903 12.833334 4.323611 12.833334 4.583333 c +12.833334 6.416666 l +12.833334 6.676389 12.745486 6.894097 12.569792 7.069792 c +12.394097 7.245486 12.176389 7.333333 11.916667 7.333333 c +11.656944 7.333333 11.439237 7.245486 11.263542 7.069792 c +11.087848 6.894097 11.000000 6.676389 11.000000 6.416666 c +11.000000 4.583333 l +11.000000 4.323611 11.087848 4.105903 11.263542 3.930208 c +11.439237 3.754514 11.656944 3.666666 11.916667 3.666666 c +h +1.833333 0.000000 m +1.329167 0.000000 0.897569 0.179514 0.538542 0.538542 c +0.179514 0.897569 0.000000 1.329166 0.000000 1.833333 c +0.000000 14.666667 l +0.000000 15.170834 0.179514 15.602430 0.538542 15.961458 c +0.897569 16.320486 1.329167 16.500000 1.833333 16.500000 c +14.666667 16.500000 l +15.170834 16.500000 15.602431 16.320486 15.961458 15.961458 c +16.320486 15.602430 16.500000 15.170834 16.500000 14.666667 c +16.500000 1.833333 l +16.500000 1.329166 16.320486 0.897569 15.961458 0.538542 c +15.602431 0.179514 15.170834 0.000000 14.666667 0.000000 c +1.833333 0.000000 l +h +1.833333 1.833333 m +14.666667 1.833333 l +14.666667 14.666667 l +1.833333 14.666667 l +1.833333 1.833333 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 2382 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 22.000000 22.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002472 00000 n +0000002495 00000 n +0000002668 00000 n +0000002742 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2801 +%%EOF \ No newline at end of file diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index c250167a0..e2cfa3060 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -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โ€ฆ"; diff --git a/ElementX/Sources/Generated/Assets.swift b/ElementX/Sources/Generated/Assets.swift index 99da36847..8c1e53449 100644 --- a/ElementX/Sources/Generated/Assets.swift +++ b/ElementX/Sources/Generated/Assets.swift @@ -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") } diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index df63d48c0..800998529 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -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) diff --git a/ElementX/Sources/Mocks/PollMock.swift b/ElementX/Sources/Mocks/PollMock.swift new file mode 100644 index 000000000..a0c365926 --- /dev/null +++ b/ElementX/Sources/Mocks/PollMock.swift @@ -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()) + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 479cf9fbb..8997a4b3b 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -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) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index c15dcab7f..0c92bdaba 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -139,6 +139,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol if state.swiftUITimelineEnabled { renderPendingTimelineItems() } + case .selectedPollOption: + break } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollOptionView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollOptionView.swift index 08981f196..0a19525f9 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollOptionView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollOptionView.swift @@ -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 @@ -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) } } @@ -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)) + isSelected: false, + isWinning: true), + showVotes: true, + isFinalResult: false) + + PollOptionView(pollOption: .init(id: "2", + text: "Chinese ๐Ÿ‡จ๐Ÿ‡ณ", + votes: 9, + allVotes: 10, + isSelected: false, + isWinning: true), + showVotes: true, + isFinalResult: true) } .padding() } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift index 83a506e65..836ef9dd1 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift @@ -19,6 +19,8 @@ import SwiftUI struct PollRoomTimelineView: View { let timelineItem: PollRoomTimelineItem @Environment(\.timelineStyle) var timelineStyle + @EnvironmentObject private var context: RoomScreenViewModel.Context + @ScaledMetric private var summaryPadding = 32 var body: some View { TimelineStyler(timelineItem: timelineItem) { @@ -26,10 +28,18 @@ struct PollRoomTimelineView: View { questionView ForEach(poll.options, id: \.id) { option in - Button { } label: { - PollOptionView(pollOption: option) + Button { + context.send(viewAction: .selectedPollOption(poll: poll, optionID: option.id)) + } label: { + PollOptionView(pollOption: option, + showVotes: showVotes, + isFinalResult: poll.hasEnded) + .foregroundColor(progressBarColor(for: option)) } + .disabled(poll.hasEnded) } + + summaryView } .frame(maxWidth: 450) } @@ -42,50 +52,104 @@ struct PollRoomTimelineView: View { } private var questionView: some View { - HStack(spacing: 4) { - Image(Asset.Images.equalizer.name) + HStack(alignment: .top, spacing: 12) { + Image(Asset.Images.timelinePoll.name) Text(poll.question) + .multilineTextAlignment(.leading) .font(.compound.bodyLGSemibold) } } + + @ViewBuilder + private var summaryView: some View { + if let summaryText = poll.summaryText { + Text(summaryText) + .font(.compound.bodySM) + .padding(.leading, showVotes ? 0 : summaryPadding) + .foregroundColor(.compound.textSecondary) + .frame(maxWidth: .infinity, alignment: showVotes ? .trailing : .leading) + } + } + + private func progressBarColor(for option: Poll.Option) -> Color { + if poll.hasEnded { + return option.isWinning ? .compound.textActionAccent : .compound.textDisabled + } else { + return .compound.textPrimary + } + } + + private var showVotes: Bool { + poll.hasEnded || poll.kind == .disclosed + } +} + +private extension Poll { + var summaryText: String? { + guard !hasEnded else { + return options.first.map { + L10n.commonPollFinalVotes($0.allVotes) + } + } + + switch kind { + case .disclosed: + return options.first.map { + L10n.commonPollTotalVotes($0.allVotes) + } + case .undisclosed: + return L10n.commonPollUndisclosedText + } + } + + var hasEnded: Bool { + endDate != nil + } } struct PollRoomTimelineView_Previews: PreviewProvider { static let viewModel = RoomScreenViewModel.mock static var previews: some View { - PollRoomTimelineView(timelineItem: .init(id: .random, - poll: .mock, - body: "Foo", - timestamp: "Now", - isOutgoing: false, - isEditable: false, - sender: .init(id: "Bob"), - properties: .init())) + PollRoomTimelineView(timelineItem: .mock(poll: .disclosed)) .environment(\.timelineStyle, .bubbles) .environmentObject(viewModel.context) - .previewDisplayName("Poll bubble style") + .previewDisplayName("Disclosed, Bubble") - PollRoomTimelineView(timelineItem: .init(id: .random, - poll: .mock, - body: "Foo", - timestamp: "Now", - isOutgoing: false, - isEditable: false, - sender: .init(id: "Bob"), - properties: .init())) + PollRoomTimelineView(timelineItem: .mock(poll: .undisclosed)) + .environment(\.timelineStyle, .bubbles) + .environmentObject(viewModel.context) + .previewDisplayName("Undisclosed, Bubble") + + PollRoomTimelineView(timelineItem: .mock(poll: .endedDisclosed)) + .environment(\.timelineStyle, .bubbles) + .environmentObject(viewModel.context) + .previewDisplayName("Ended, Disclosed, Bubble") + + PollRoomTimelineView(timelineItem: .mock(poll: .endedUndisclosed)) + .environment(\.timelineStyle, .bubbles) + .environmentObject(viewModel.context) + .previewDisplayName("Ended, Undisclosed, Bubble") + + PollRoomTimelineView(timelineItem: .mock(poll: .disclosed)) .environment(\.timelineStyle, .plain) .environmentObject(viewModel.context) - .previewDisplayName("Poll plain style") + .previewDisplayName("Disclosed, Plain") + + PollRoomTimelineView(timelineItem: .mock(poll: .undisclosed)) + .environment(\.timelineStyle, .plain) + .environmentObject(viewModel.context) + .previewDisplayName("Undisclosed, Plain") + + PollRoomTimelineView(timelineItem: .mock(poll: .endedDisclosed)) + .environment(\.timelineStyle, .plain) + .environmentObject(viewModel.context) + .previewDisplayName("Ended, Disclosed, Plain") + + PollRoomTimelineView(timelineItem: .mock(poll: .endedUndisclosed)) + .environment(\.timelineStyle, .plain) + .environmentObject(viewModel.context) + .previewDisplayName("Ended, Undisclosed, Plain") } } - -private extension Poll { - static let mock: Self = .init(question: "Do you like polls?", - pollKind: .disclosed, - maxSelections: 1, - options: [.init(id: "1", text: "Yes", votes: 1, allVotes: 3, isSelected: true), .init(id: "2", text: "No", votes: 2, allVotes: 3, isSelected: false)], - votes: [:], - endDate: nil) -} diff --git a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift index 1ca505d7b..fbe8610b6 100644 --- a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift +++ b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift @@ -212,6 +212,16 @@ enum RoomTimelineItemFixtures { TextRoomTimelineItem(text: "Pork buffalo mollit culpa strip steak in leberkas flank cow.", senderDisplayName: "Alice")] } + + static var disclosedPolls: [RoomTimelineItemProtocol] { + [PollRoomTimelineItem.mock(poll: .disclosed), + PollRoomTimelineItem.mock(poll: .endedDisclosed)] + } + + static var undisclosedPolls: [RoomTimelineItemProtocol] { + [PollRoomTimelineItem.mock(poll: .undisclosed), + PollRoomTimelineItem.mock(poll: .endedUndisclosed)] + } } private extension TextRoomTimelineItem { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift index 7e6e6d80b..9af2f2573 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift @@ -29,7 +29,7 @@ struct PollRoomTimelineItem: Equatable, EventBasedTimelineItemProtocol { struct Poll: Equatable { let question: String - let pollKind: Kind + let kind: Kind let maxSelections: Int let options: [Option] let votes: [String: [String]] @@ -46,5 +46,6 @@ struct Poll: Equatable { let votes: Int let allVotes: Int let isSelected: Bool + let isWinning: Bool } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index 943824127..d5f0131fe 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -333,16 +333,21 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { count + pair.value.count } + let maxOptionVotes = votes.map(\.value.count).max() + let options = answers.map { answer in - Poll.Option(id: answer.id, - text: answer.text, - votes: votes[answer.id]?.count ?? 0, - allVotes: allVotes, - isSelected: votes[answer.id]?.contains(userID) ?? false) + let optionVotesCount = votes[answer.id]?.count + + return Poll.Option(id: answer.id, + text: answer.text, + votes: optionVotesCount ?? 0, + allVotes: allVotes, + isSelected: votes[answer.id]?.contains(userID) ?? false, + isWinning: optionVotesCount.map { $0 == maxOptionVotes } ?? false) } let poll = Poll(question: question, - pollKind: .init(pollKind: pollKind), + kind: .init(pollKind: pollKind), maxSelections: Int(maxSelections), options: options, votes: votes, diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 694c63e19..d296b6732 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -322,6 +322,20 @@ class MockScreen: Identifiable { emojiProvider: EmojiProvider()) let coordinator = RoomScreenCoordinator(parameters: parameters) + navigationStackCoordinator.setRootCoordinator(coordinator) + return navigationStackCoordinator + case .roomWithDisclosedPolls, .roomWithUndisclosedPolls: + let navigationStackCoordinator = NavigationStackCoordinator() + + let timelineController = MockRoomTimelineController() + timelineController.timelineItems = id == .roomWithDisclosedPolls ? RoomTimelineItemFixtures.disclosedPolls : 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 .sessionVerification: diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift index 843f2b674..31370e24d 100644 --- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift @@ -45,6 +45,8 @@ enum UITestsScreenIdentifier: String { case roomLayoutTop case roomLayoutMiddle case roomLayoutBottom + case roomWithDisclosedPolls + case roomWithUndisclosedPolls case sessionVerification case userSessionScreen case roomDetailsScreen diff --git a/UITests/Sources/RoomScreenUITests.swift b/UITests/Sources/RoomScreenUITests.swift index 70a2dd902..4219ff094 100644 --- a/UITests/Sources/RoomScreenUITests.swift +++ b/UITests/Sources/RoomScreenUITests.swift @@ -160,7 +160,19 @@ class RoomScreenUITests: XCTestCase { // The messages should be bottom aligned. try await app.assertScreenshot(.roomSmallTimelineWithReadReceipts) } - + + func testTimelineDisclosedPolls() async throws { + let app = Application.launch(.roomWithDisclosedPolls) + + try await app.assertScreenshot(.roomWithDisclosedPolls) + } + + func testTimelineUndisclosedPolls() async throws { + let app = Application.launch(.roomWithUndisclosedPolls) + + try await app.assertScreenshot(.roomWithUndisclosedPolls) + } + // MARK: - Helper Methods private func performOperation(_ operation: UITestsSignal, using client: UITestsSignalling.Client) async throws { diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomWithDisclosedPolls.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomWithDisclosedPolls.png new file mode 100644 index 000000000..d96f822eb --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomWithDisclosedPolls.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cec6247e280f09bfb34570a96c8aeb583839f8d0380e75d69c1b1e042e8278d0 +size 165504 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomWithUndisclosedPolls.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomWithUndisclosedPolls.png new file mode 100644 index 000000000..0e523b92e --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomWithUndisclosedPolls.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce7fb6d098b437b2450b1ddd92a23b4bc6620c0fe2b0d99ec1a2f85e3656821a +size 162702 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomWithDisclosedPolls.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomWithDisclosedPolls.png new file mode 100644 index 000000000..de142be16 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomWithDisclosedPolls.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6fcd81ef389e8d04087f43303bd2bec1e7d4bb8183bcfe6bda00c2b07ec46a34 +size 256534 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomWithUndisclosedPolls.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomWithUndisclosedPolls.png new file mode 100644 index 000000000..43c2b1a37 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomWithUndisclosedPolls.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a29f94a8baff42766e10084885512a6219f1bc3fe32887ff318a526584f428f +size 253383 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomWithDisclosedPolls.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomWithDisclosedPolls.png new file mode 100644 index 000000000..c3d62a6bf --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomWithDisclosedPolls.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d39f400c814bf25259ac47d6f9855c660dec90c8f442c920623d9e1191cb262c +size 191391 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomWithUndisclosedPolls.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomWithUndisclosedPolls.png new file mode 100644 index 000000000..05c423bf2 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomWithUndisclosedPolls.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75180dc2435ab30d9de7621200c8c10a3ca3f234689186779ebe8b71e53bc8d8 +size 180361 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomWithDisclosedPolls.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomWithDisclosedPolls.png new file mode 100644 index 000000000..15f32e96c --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomWithDisclosedPolls.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97a19207793f911a796adfbc0b507f3c531a9ac9a83a9d190670c61ec5af3a9b +size 275591 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomWithUndisclosedPolls.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomWithUndisclosedPolls.png new file mode 100644 index 000000000..c123ccdf3 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomWithUndisclosedPolls.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a1f560fd354786f25a61551a0f522555d09ba76522ed6a1347b903ce141ea6f +size 268467