Poll UI improvements

This commit is contained in:
Ali 2020-01-13 00:37:27 +04:00
parent 2a6f1f811b
commit 6d5bfe1f1e
12 changed files with 142 additions and 28 deletions

View File

@ -396,7 +396,7 @@ private func createPollControllerEntries(presentationData: PresentationData, pee
}
var canBePublic = true
if let channel = peer as? TelegramChannel, case .broadcast = channel.info, let username = channel.username, !username.isEmpty {
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
canBePublic = false
}

View File

@ -3049,7 +3049,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight))
case .topEdge:
headerFrame = CGRect(origin: CGPoint(x: 0.0, y: min(max(upperDisplayBound, upperBoundEdge - itemHeaderHeight), lowerBound - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight))
stickLocationDistance = headerFrame.maxY - upperBoundEdge - itemHeaderHeight
stickLocationDistance = headerFrame.minY - upperBoundEdge + itemHeaderHeight
stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight))
case .bottom:
headerFrame = CGRect(origin: CGPoint(x: 0.0, y: max(upperBound, min(lowerBound, lowerDisplayBound) - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight))

View File

@ -66,7 +66,7 @@ private final class ShimmerEffectNode: ASDisplayNode {
self.currentBackgroundColor = backgroundColor
self.currentForegroundColor = foregroundColor
self.imageNode.image = generateImage(CGSize(width: 8.0, height: 100.0), opaque: true, scale: 1.0, rotatedContext: { size, context in
self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
@ -1288,7 +1288,7 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode {
private let actionTextNode: ImmediateTextNode
private let actionButton: HighlightableButtonNode
private var stickDistanceFactor: CGFloat = 0.0
private var stickDistanceFactor: CGFloat?
public init(theme: PresentationTheme, strings: PresentationStrings, text: String, actionTitle: String?, action: (() -> Void)?) {
self.theme = theme
@ -1305,6 +1305,7 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode {
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
self.separatorNode.alpha = 0.0
let titleFont = Font.regular(13.0)
@ -1391,6 +1392,20 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode {
}
override public func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self.snappedBackgroundNode, alpha: (1.0 - factor) * 0.0 + factor * 1.0)
if self.stickDistanceFactor == factor {
return
}
self.stickDistanceFactor = factor
if let (size, leftInset, _) = self.validLayout {
if leftInset.isZero {
transition.updateAlpha(node: self.separatorNode, alpha: 1.0)
transition.updateAlpha(node: self.snappedBackgroundNode, alpha: (1.0 - factor) * 0.0 + factor * 1.0)
} else {
let distance = factor * size.height
let alpha = abs(distance) / 16.0
transition.updateAlpha(node: self.separatorNode, alpha: max(0.0, min(1.0, alpha)))
transition.updateAlpha(node: self.snappedBackgroundNode, alpha: 0.0)
}
}
}
}

View File

@ -116,8 +116,7 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
var bottomInset: CGFloat = 7.0
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
let largeTitleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseHeaderFontSize * 2.0))
let semiLargeTitleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseHeaderFontSize * 1.2))
let largeTitleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize))
let titleBoldFont = Font.semibold(item.presentationData.fontSize.itemListBaseHeaderFontSize)
let attributedText: NSAttributedString
@ -125,13 +124,7 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
case let .plain(text):
attributedText = NSAttributedString(string: text, font: titleFont, textColor: item.presentationData.theme.list.freeTextColor)
case let .large(text):
let font: UIFont
if params.width >= 330.0 {
font = largeTitleFont
} else {
font = semiLargeTitleFont
}
attributedText = NSAttributedString(string: text, font: font, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
attributedText = NSAttributedString(string: text, font: largeTitleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
case let .markdown(text):
attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: item.presentationData.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)

View File

@ -38,6 +38,13 @@ public struct ValueBoxKey: Equatable, Hashable, CustomStringConvertible, Compara
memcpy(self.memory, buffer.memory, buffer.length)
}
public func setData(_ offset: Int, value: Data) {
let valueLength = value.count
value.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> Void in
memcpy(self.memory + offset, bytes, valueLength)
}
}
public func setInt32(_ offset: Int, value: Int32) {
var bigEndianValue = Int32(bigEndian: value)
memcpy(self.memory + offset, &bigEndianValue, 4)

View File

@ -67,7 +67,8 @@ public struct Namespaces {
public static let cachedStickerQueryResults: Int8 = 5
public static let cachedSecureIdConfiguration: Int8 = 6
public static let cachedWallpapersConfiguration: Int8 = 7
public static let cachedThemesConfiguration: Int8 = 7
public static let cachedThemesConfiguration: Int8 = 8
public static let cachedPollResults: Int8 = 9
}
public struct UnorderedItemList {

View File

@ -151,6 +151,7 @@ private var declaredEncodables: Void = {
declareEncodable(WalletCollection.self, f: { WalletCollection(decoder: $0) })
declareEncodable(EmbeddedMediaStickersMessageAttribute.self, f: { EmbeddedMediaStickersMessageAttribute(decoder: $0) })
declareEncodable(TelegramMediaWebpageAttribute.self, f: { TelegramMediaWebpageAttribute(decoder: $0) })
declareEncodable(CachedPollOptionResult.self, f: { CachedPollOptionResult(decoder: $0) })
return
}()

View File

@ -124,9 +124,40 @@ public func requestClosePoll(postbox: Postbox, network: Network, stateManager: A
}
}
private let cachedPollResultsCollectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 20, highWaterItemCount: 40)
final class CachedPollOptionResult: PostboxCoding {
let peerIds: [PeerId]
let count: Int32
public static func key(pollId: MediaId, optionOpaqueIdentifier: Data) -> ValueBoxKey {
let key = ValueBoxKey(length: 4 + 8 + optionOpaqueIdentifier.count)
key.setInt32(0, value: pollId.namespace)
key.setInt64(4, value: pollId.id)
key.setData(4 + 8, value: optionOpaqueIdentifier)
return key
}
public init(peerIds: [PeerId], count: Int32) {
self.peerIds = peerIds
self.count = count
}
public init(decoder: PostboxDecoder) {
self.peerIds = decoder.decodeInt64ArrayForKey("peerIds").map(PeerId.init)
self.count = decoder.decodeInt32ForKey("count", orElse: 0)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt64Array(self.peerIds.map { $0.toInt64() }, forKey: "peerIds")
encoder.encodeInt32(self.count, forKey: "count")
}
}
private final class PollResultsOptionContext {
private let queue: Queue
private let account: Account
private let pollId: MediaId
private let messageId: MessageId
private let opaqueIdentifier: Data
private let disposable = MetaDisposable()
@ -136,15 +167,46 @@ private final class PollResultsOptionContext {
private var nextOffset: String?
private var results: [RenderedPeer] = []
private var count: Int
private var populateCache: Bool = true
let state = Promise<PollResultsOptionState>()
init(queue: Queue, account: Account, messageId: MessageId, opaqueIdentifier: Data, count: Int) {
init(queue: Queue, account: Account, pollId: MediaId, messageId: MessageId, opaqueIdentifier: Data, count: Int) {
self.queue = queue
self.account = account
self.pollId = pollId
self.messageId = messageId
self.opaqueIdentifier = opaqueIdentifier
self.count = count
self.isLoadingMore = true
self.disposable.set((account.postbox.transaction { transaction -> [RenderedPeer]? in
let cachedResult = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPollResults, key: CachedPollOptionResult.key(pollId: pollId, optionOpaqueIdentifier: opaqueIdentifier))) as? CachedPollOptionResult
if let cachedResult = cachedResult, Int(cachedResult.count) == count {
var result: [RenderedPeer] = []
for peerId in cachedResult.peerIds {
if let peer = transaction.getPeer(peerId) {
result.append(RenderedPeer(peer: peer))
} else {
return nil
}
}
return result
} else {
return nil
}
}
|> deliverOn(self.queue)).start(next: { [weak self] cachedPeers in
guard let strongSelf = self else {
return
}
strongSelf.isLoadingMore = false
if let cachedPeers = cachedPeers {
strongSelf.results = cachedPeers
strongSelf.hasLoadedOnce = true
}
strongSelf.loadMore()
}))
}
deinit {
@ -156,10 +218,12 @@ private final class PollResultsOptionContext {
return
}
self.isLoadingMore = true
let pollId = self.pollId
let messageId = self.messageId
let opaqueIdentifier = self.opaqueIdentifier
let account = self.account
let nextOffset = self.nextOffset
let populateCache = self.populateCache
self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
}
@ -193,12 +257,15 @@ private final class PollResultsOptionContext {
}
}
}
if populateCache {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPollResults, key: CachedPollOptionResult.key(pollId: pollId, optionOpaqueIdentifier: opaqueIdentifier)), entry: CachedPollOptionResult(peerIds: resultPeers.map { $0.peerId }, count: count), collectionSpec: cachedPollResultsCollectionSpec)
}
return (resultPeers, Int(count), nextOffset)
}
}
}
#if DEBUG
return signal |> delay(4.0, queue: .concurrentDefaultQueue())
//return signal |> delay(4.0, queue: .concurrentDefaultQueue())
#endif
return signal
} else {
@ -209,6 +276,10 @@ private final class PollResultsOptionContext {
guard let strongSelf = self else {
return
}
if strongSelf.populateCache {
strongSelf.populateCache = false
strongSelf.results.removeAll()
}
var existingIds = Set(strongSelf.results.map { $0.peerId })
for peer in peers {
if !existingIds.contains(peer.peerId) {
@ -266,7 +337,7 @@ private final class PollResultsContextImpl {
}
}
}
self.optionContexts[option.opaqueIdentifier] = PollResultsOptionContext(queue: self.queue, account: account, messageId: messageId, opaqueIdentifier: option.opaqueIdentifier, count: count)
self.optionContexts[option.opaqueIdentifier] = PollResultsOptionContext(queue: self.queue, account: account, pollId: poll.pollId, messageId: messageId, opaqueIdentifier: option.opaqueIdentifier, count: count)
}
self.state.set(combineLatest(queue: self.queue, self.optionContexts.map { (opaqueIdentifier, context) -> Signal<(Data, PollResultsOptionState), NoError> in

View File

@ -343,7 +343,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
primaryColor: UIColor(rgb: 0xffffff),
controlColor: UIColor(rgb: 0x4d4d4d)
),
mediaPlaceholderColor: UIColor(rgb: 0xffffff).withMultipliedBrightnessBy(0.1),
mediaPlaceholderColor: UIColor(rgb: 0xffffff).withMultipliedBrightnessBy(0.05),
scrollIndicatorColor: UIColor(rgb: 0xffffff, alpha: 0.3),
pageIndicatorInactiveColor: UIColor(white: 1.0, alpha: 0.3),
inputClearButtonColor: UIColor(rgb: 0x8b9197),

View File

@ -644,7 +644,7 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres
let message = PresentationThemeChatMessage(
incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor)), primaryTextColor: .white, secondaryTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), linkTextColor: accentColor, linkHighlightColor: accentColor.withAlphaComponent(0.5), scamColor: UIColor(rgb: 0xff6767), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: accentColor, accentControlColor: accentColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: accentColor, mediaInactiveControlColor: accentColor.withAlphaComponent(0.5), mediaControlInnerBackgroundColor: mainBackgroundColor, pendingActivityColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileTitleColor: accentColor, fileDescriptionColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileDurationColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.585, brightness: 0.23), polls: PresentationThemeChatBubblePolls(radioButton: accentColor.withMultiplied(hue: 0.995, saturation: 0.317, brightness: 0.51), radioProgress: accentColor, highlight: accentColor.withAlphaComponent(0.12), separator: mainSeparatorColor, bar: accentColor, barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: accentColor.withAlphaComponent(0.2), textSelectionKnobColor: accentColor),
outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColor, gradientFill: outgoingBubbleFillGradientColor, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleFillColor), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColor, gradientFill: outgoingBubbleFillGradientColor, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleFillColor)), primaryTextColor: outgoingPrimaryTextColor, secondaryTextColor: outgoingSecondaryTextColor, linkTextColor: outgoingLinkTextColor, linkHighlightColor: UIColor.white.withAlphaComponent(0.5), scamColor: outgoingScamColor, textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: outgoingPrimaryTextColor, accentControlColor: outgoingPrimaryTextColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: outgoingPrimaryTextColor, mediaInactiveControlColor: outgoingSecondaryTextColor, mediaControlInnerBackgroundColor: outgoingBubbleFillColor, pendingActivityColor: outgoingSecondaryTextColor, fileTitleColor: outgoingPrimaryTextColor, fileDescriptionColor: outgoingSecondaryTextColor, fileDurationColor: outgoingSecondaryTextColor, mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.804, brightness: 0.51), polls: PresentationThemeChatBubblePolls(radioButton: outgoingPrimaryTextColor, radioProgress: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0), highlight: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0).withAlphaComponent(0.12), separator: mainSeparatorColor, bar: outgoingPrimaryTextColor, barIconForeground: .white, barPositive: outgoingPrimaryTextColor, barNegative: outgoingPrimaryTextColor), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: UIColor.white.withAlphaComponent(0.2), textSelectionKnobColor: UIColor.white),
outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColor, gradientFill: outgoingBubbleFillGradientColor, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleFillColor), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColor, gradientFill: outgoingBubbleFillGradientColor, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleFillColor)), primaryTextColor: outgoingPrimaryTextColor, secondaryTextColor: outgoingSecondaryTextColor, linkTextColor: outgoingLinkTextColor, linkHighlightColor: UIColor.white.withAlphaComponent(0.5), scamColor: outgoingScamColor, textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: outgoingPrimaryTextColor, accentControlColor: outgoingPrimaryTextColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: outgoingPrimaryTextColor, mediaInactiveControlColor: outgoingSecondaryTextColor, mediaControlInnerBackgroundColor: outgoingBubbleFillColor, pendingActivityColor: outgoingSecondaryTextColor, fileTitleColor: outgoingPrimaryTextColor, fileDescriptionColor: outgoingSecondaryTextColor, fileDurationColor: outgoingSecondaryTextColor, mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.804, brightness: 0.51), polls: PresentationThemeChatBubblePolls(radioButton: outgoingPrimaryTextColor, radioProgress: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0), highlight: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0).withAlphaComponent(0.12), separator: mainSeparatorColor, bar: outgoingPrimaryTextColor, barIconForeground: .clear, barPositive: outgoingPrimaryTextColor, barNegative: outgoingPrimaryTextColor), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: UIColor.white.withAlphaComponent(0.2), textSelectionKnobColor: UIColor.white),
freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor)),
infoPrimaryTextColor: UIColor(rgb: 0xffffff),
infoLinkTextColor: accentColor,

View File

@ -1550,12 +1550,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
if !found, let itemNode = itemNode as? ChatMessageBubbleItemNode, itemNode.item?.message.id == id {
found = true
if strongSelf.selectPollOptionFeedback == nil {
strongSelf.selectPollOptionFeedback = HapticFeedback()
}
strongSelf.selectPollOptionFeedback?.error()
itemNode.animateQuizInvalidOptionSelected()
}
}
return;
}
if false {
if strongSelf.selectPollOptionFeedback == nil {
strongSelf.selectPollOptionFeedback = HapticFeedback()
}
strongSelf.selectPollOptionFeedback?.success()
strongSelf.chatDisplayNode.animateQuizCorrectOptionSelected()
return;
}
@ -1582,18 +1590,31 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
switch resultPoll.kind {
case .poll:
break
if strongSelf.selectPollOptionFeedback == nil {
strongSelf.selectPollOptionFeedback = HapticFeedback()
}
strongSelf.selectPollOptionFeedback?.success()
case .quiz:
if let voters = resultPoll.results.voters {
for voter in voters {
if voter.selected {
if voter.isCorrect {
if strongSelf.selectPollOptionFeedback == nil {
strongSelf.selectPollOptionFeedback = HapticFeedback()
}
strongSelf.selectPollOptionFeedback?.success()
strongSelf.chatDisplayNode.animateQuizCorrectOptionSelected()
} else {
var found = false
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
if !found, let itemNode = itemNode as? ChatMessageBubbleItemNode, itemNode.item?.message.id == id {
found = true
if strongSelf.selectPollOptionFeedback == nil {
strongSelf.selectPollOptionFeedback = HapticFeedback()
}
strongSelf.selectPollOptionFeedback?.error()
itemNode.animateQuizInvalidOptionSelected()
}
}
@ -1617,10 +1638,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if controllerInteraction.pollActionState.pollMessageIdsInProgress.removeValue(forKey: id) != nil {
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
}
if strongSelf.selectPollOptionFeedback == nil {
strongSelf.selectPollOptionFeedback = HapticFeedback()
}
strongSelf.selectPollOptionFeedback?.success()
}), forKey: id)
}
}, requestOpenMessagePollResults: { [weak self] messageId, pollId in

View File

@ -1260,9 +1260,18 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
var hasResults = false
if let totalVoters = poll.results.totalVoters, totalVoters != 0 {
if let _ = poll.results.voters {
hasResults = true
if poll.isClosed {
hasResults = true
} else {
if let totalVoters = poll.results.totalVoters, totalVoters != 0 {
if let voters = poll.results.voters {
for voter in voters {
if voter.selected {
hasResults = true
break
}
}
}
}
}