"View only" polls in the timeline (#1399)

* Add stub methods in RoomTimelineItemFactory

* Add PollRoomTimelineView and PollEndRoomTimelineView

* Add timeline skeleton

* Start poll mapping

* Add comment

* Refine poll UI

* Add feature flags for polls

* Cleanup

* Delete poll end event

* Refine poll mapping

* Refine poll layout

* Reuse FormRowAccessory

* Refine poll bubble layout

* Localise strings

* Update project

* Refine poll preview

* Map poll properties
This commit is contained in:
Alfonso Grillo
2023-07-27 15:34:27 +02:00
committed by GitHub
parent 8f2904701b
commit 0e7a48d961
19 changed files with 483 additions and 43 deletions
@@ -196,8 +196,6 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
case .vertical:
GridRow {
localizedSendInfo
.padding(.bottom, 4)
.padding(.trailing, 4)
.gridColumnAlignment(.trailing)
}
}
@@ -284,7 +282,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
}
private extension View {
func bubbleStyle(insets: CGFloat, color: Color? = nil, cornerRadius: CGFloat = 12, corners: UIRectCorner) -> some View {
func bubbleStyle(insets: EdgeInsets, color: Color? = nil, cornerRadius: CGFloat = 12, corners: UIRectCorner) -> some View {
padding(insets)
.background(color)
.cornerRadius(cornerRadius, corners: corners)
@@ -293,18 +291,18 @@ private extension View {
// Describes how the content and the send info should be arranged inside a bubble
private enum BubbleSendInfoLayoutType {
case horizontal
case vertical
case horizontal(spacing: CGFloat = 4)
case vertical(spacing: CGFloat = 4)
case overlay(capsuleStyle: Bool)
var layout: AnyLayout {
let layout: any Layout
switch self {
case .horizontal:
layout = HStackLayout(alignment: .bottom, spacing: 4)
case .vertical:
layout = GridLayout(alignment: .leading, verticalSpacing: 4)
case .horizontal(let spacing):
layout = HStackLayout(alignment: .bottom, spacing: spacing)
case .vertical(let spacing):
layout = GridLayout(alignment: .leading, verticalSpacing: spacing)
case .overlay:
layout = ZStackLayout(alignment: .bottomTrailing)
}
@@ -329,23 +327,25 @@ private extension EventBasedTimelineItemProtocol {
// The insets for the full bubble content.
// Padding affecting just the "send info" should be added inside `layoutedLocalizedSendInfo`
var bubbleInsets: CGFloat {
let defaultPadding: CGFloat = 8
var bubbleInsets: EdgeInsets {
let defaultInsets: EdgeInsets = .init(around: 8)
switch self {
case is ImageRoomTimelineItem,
is VideoRoomTimelineItem,
is StickerRoomTimelineItem:
return 0
return .zero
case is PollRoomTimelineItem:
return .init(top: 12, leading: 12, bottom: 4, trailing: 12)
case let locationTimelineItem as LocationRoomTimelineItem:
return locationTimelineItem.content.geoURI == nil ? defaultPadding : 0
return locationTimelineItem.content.geoURI == nil ? defaultInsets : .zero
default:
return defaultPadding
return defaultInsets
}
}
var bubbleSendInfoLayoutType: BubbleSendInfoLayoutType {
let defaultTimestampLayout: BubbleSendInfoLayoutType = .horizontal
let defaultTimestampLayout: BubbleSendInfoLayoutType = .horizontal()
switch self {
case is TextBasedRoomTimelineItem:
@@ -356,6 +356,8 @@ private extension EventBasedTimelineItemProtocol {
return .overlay(capsuleStyle: true)
case let locationTimelineItem as LocationRoomTimelineItem:
return .overlay(capsuleStyle: locationTimelineItem.content.geoURI != nil)
case is PollRoomTimelineItem:
return .vertical(spacing: 16)
default:
return defaultTimestampLayout
}
@@ -415,3 +417,11 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
.environmentObject(viewModel.context)
}
}
private extension EdgeInsets {
init(around: CGFloat) {
self.init(top: around, leading: around, bottom: around, trailing: around)
}
static var zero: Self = .init(around: 0)
}
@@ -0,0 +1,68 @@
//
// 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 SwiftUI
struct PollOptionView: View {
let pollOption: Poll.Option
var body: some View {
HStack(alignment: .top, spacing: 8) {
FormRowAccessory(kind: .multipleSelection(isSelected: pollOption.isSelected))
VStack(spacing: 10) {
HStack(alignment: .lastTextBaseline) {
Text(pollOption.text)
.frame(maxWidth: .infinity, alignment: .leading)
Text(L10n.commonPollVotesCount(pollOption.votes))
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
}
progressView
}
}
}
// MARK: - Private
private var progressView: some View {
ProgressView(value: Double(pollOption.votes) / Double(pollOption.allVotes))
.progressViewStyle(LinearProgressViewStyle(tint: .compound.textPrimary))
}
}
struct PollOptionView_Previews: PreviewProvider {
static var previews: some View {
VStack {
Group {
PollOptionView(pollOption: .init(id: "1",
text: "Italian 🇮🇹",
votes: 1,
allVotes: 10,
isSelected: true))
PollOptionView(pollOption: .init(id: "2",
text: "Chinese 🇨🇳",
votes: 9,
allVotes: 10,
isSelected: false))
}
.padding()
}
}
}
@@ -0,0 +1,91 @@
//
// 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 SwiftUI
struct PollRoomTimelineView: View {
let timelineItem: PollRoomTimelineItem
@Environment(\.timelineStyle) var timelineStyle
var body: some View {
TimelineStyler(timelineItem: timelineItem) {
VStack(alignment: .leading, spacing: 16) {
questionView
ForEach(poll.options, id: \.id) { option in
Button { } label: {
PollOptionView(pollOption: option)
}
}
}
.frame(maxWidth: 450)
}
}
// MARK: - Private
private var poll: Poll {
timelineItem.poll
}
private var questionView: some View {
HStack(spacing: 4) {
Image(Asset.Images.equalizer.name)
Text(poll.question)
.font(.compound.bodyLGSemibold)
}
}
}
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()))
.environment(\.timelineStyle, .bubbles)
.environmentObject(viewModel.context)
.previewDisplayName("Poll bubble style")
PollRoomTimelineView(timelineItem: .init(id: .random,
poll: .mock,
body: "Foo",
timestamp: "Now",
isOutgoing: false,
isEditable: false,
sender: .init(id: "Bob"),
properties: .init()))
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
.previewDisplayName("Poll plain style")
}
}
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)
}