diff --git a/submodules/GalleryData/Sources/GalleryData.swift b/submodules/GalleryData/Sources/GalleryData.swift index 2f0b187bbf..9f95085a15 100644 --- a/submodules/GalleryData/Sources/GalleryData.swift +++ b/submodules/GalleryData/Sources/GalleryData.swift @@ -113,7 +113,17 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati } } for media in message.media { - if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first, case .full = extendedMedia { + if let poll = media as? TelegramMediaPoll, let attachedMedia = poll.attachedMedia { + if attachedMedia is TelegramMediaImage { + galleryMedia = attachedMedia + } else if let file = attachedMedia as? TelegramMediaFile, file.isVideo { + galleryMedia = attachedMedia + } else if attachedMedia is TelegramMediaMap { + galleryMedia = attachedMedia + } else { + otherMedia = attachedMedia + } + } else if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first, case .full = extendedMedia { standalone = true galleryMedia = paidContent } else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index b4711502aa..4dfdbc6985 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -44,7 +44,9 @@ private func tagsForMessage(_ message: Message) -> MessageTags? { } private func galleryMediaForMedia(media: Media) -> Media? { - if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { + if let poll = media as? TelegramMediaPoll, let attachedMedia = poll.attachedMedia { + return attachedMedia + } else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia { return fullMedia } else if let media = media as? TelegramMediaImage { return media diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 51a087ab2e..871373555c 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -153,7 +153,9 @@ class ChatImageGalleryItem: GalleryItem { node.setMessage(self.message, displayInfo: !self.displayInfoOnTop, translateToLanguage: self.translateToLanguage, peerIsCopyProtected: self.peerIsCopyProtected, isSecret: self.isSecret, location: self.location) for media in self.message.media { - if let paidContent = media as? TelegramMediaPaidContent { + if let poll = media as? TelegramMediaPoll, let image = poll.attachedMedia as? TelegramMediaImage { + node.setImage(userLocation: .peer(self.message.id.peerId), imageReference: .message(message: MessageReference(self.message), media: image)) + } else if let paidContent = media as? TelegramMediaPaidContent { let mediaIndex = self.mediaIndex ?? 0 if case let .full(fullMedia) = paidContent.extendedMedia[Int(mediaIndex)], let image = fullMedia as? TelegramMediaImage { node.setImage(userLocation: .peer(self.message.id.peerId), imageReference: .message(message: MessageReference(self.message), media: image)) diff --git a/submodules/LocationUI/Sources/LocationViewControllerNode.swift b/submodules/LocationUI/Sources/LocationViewControllerNode.swift index 5eef4f69b7..82c3bf8097 100644 --- a/submodules/LocationUI/Sources/LocationViewControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationViewControllerNode.swift @@ -19,7 +19,11 @@ import DeviceAccess import TooltipUI func getLocation(from message: EngineMessage) -> TelegramMediaMap? { - return message.media.first(where: { $0 is TelegramMediaMap } ) as? TelegramMediaMap + if let poll = message.media.first(where: { $0 is TelegramMediaPoll } ) as? TelegramMediaPoll, let map = poll.attachedMedia as? TelegramMediaMap { + return map + } else { + return message.media.first(where: { $0 is TelegramMediaMap } ) as? TelegramMediaMap + } } private func areMessagesEqual(_ lhsMessage: EngineMessage, _ rhsMessage: EngineMessage) -> Bool { diff --git a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift index ce1352a1f8..432770270f 100644 --- a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift +++ b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift @@ -146,6 +146,9 @@ func presentLegacyMediaPickerGallery( return getCaptionPanelView() } paintStickersContext.livePhotoButton = { + if case .secretChat = peer { + return nil + } let livePhotoButton = LivePhotoButton() livePhotoButton.present = present livePhotoButton.context = context diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift index 40a156e745..92c5687112 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift @@ -205,8 +205,12 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { if item.presentationData.isPreview { automaticDownload = .full } - } else if let poll = media as? TelegramMediaPoll, let image = poll.attachedMedia as? TelegramMediaImage { - selectedMedia = image + } else if let poll = media as? TelegramMediaPoll { + if let image = poll.attachedMedia as? TelegramMediaImage { + selectedMedia = image + } else if let file = poll.attachedMedia as? TelegramMediaFile { + selectedMedia = file + } } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 026511a184..5d718c3aec 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -371,6 +371,7 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { private let percentageFont = Font.bold(14.5) private let percentageSmallFont = Font.bold(12.5) +private let countFont = Font.regular(12.5) private func generatePercentageImage(presentationData: ChatPresentationData, incoming: Bool, value: Int, targetValue: Int) -> UIImage { return generateImage(CGSize(width: 42.0, height: 20.0), rotatedContext: { size, context in @@ -399,10 +400,40 @@ private func generatePercentageAnimationImages(presentationData: ChatPresentatio return images } +private func generateCountImage(presentationData: ChatPresentationData, incoming: Bool, value: Int32, targetValue: Int32) -> UIImage { + let targetString = "\(targetValue)" + let targetSize = (targetString as NSString).size(withAttributes: [.font: countFont]) + let imageSize = CGSize(width: ceil(targetSize.width) + 2.0, height: 20.0) + return generateImage(imageSize, rotatedContext: { size, context in + UIGraphicsPushContext(context) + context.clear(CGRect(origin: CGPoint(), size: size)) + let string = NSAttributedString( + string: "\(value)", + font: countFont, + textColor: incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor, + paragraphAlignment: .right + ) + string.draw(in: CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: size)) + UIGraphicsPopContext() + })! +} + +private func generateCountAnimationImages(presentationData: ChatPresentationData, incoming: Bool, from fromValue: Int32, to toValue: Int32, duration: Double) -> [UIImage] { + let minimumFrameDuration = 1.0 / 40.0 + let numberOfFrames = max(1, Int(duration / minimumFrameDuration)) + var images: [UIImage] = [] + for i in 0 ..< numberOfFrames { + let t = CGFloat(i) / CGFloat(numberOfFrames) + images.append(generateCountImage(presentationData: presentationData, incoming: incoming, value: Int32((1.0 - t) * CGFloat(fromValue) + t * CGFloat(toValue)), targetValue: toValue)) + } + return images +} + private struct ChatMessagePollOptionResult: Equatable { let normalized: CGFloat let percent: Int let count: Int32 + let recentVoterPeerIds: [PeerId] } private struct ChatMessagePollOptionSelection: Equatable { @@ -414,14 +445,21 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { private static let mediaSize = CGSize(width: 40.0, height: 40.0) private static let mediaSpacing: CGFloat = 2.0 private static let mediaRightInset: CGFloat = 10.0 + private static let avatarsSize = CGSize(width: MergedAvatarsNode.defaultMergedImageSize + MergedAvatarsNode.defaultMergedImageSpacing, height: MergedAvatarsNode.defaultMergedImageSize) + private static let avatarsSpacing: CGFloat = 6.0 + private static let countSpacing: CGFloat = 6.0 private let highlightedBackgroundNode: ASDisplayNode private(set) var radioNode: ChatMessagePollOptionRadioNode? private let percentageNode: ASDisplayNode private var percentageImage: UIImage? + private let countNode: ASDisplayNode + private var countImage: UIImage? + private let avatarsNode: MergedAvatarsNode fileprivate var titleNode: TextNodeWithEntities? private let buttonNode: HighlightTrackingButtonNode let separatorNode: ASDisplayNode + private let resultBarBackgroundNode: ASImageNode private let resultBarNode: ASImageNode private let resultBarIconNode: ASImageNode private var mediaNode: TransformImageNode? @@ -429,6 +467,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { private var mediaHidden = false private(set) var mediaFrame: CGRect? private var currentMedia: Media? + private var currentRecentVoterPeerIds: [PeerId] = [] var option: TelegramMediaPollOption? private(set) var currentResult: ChatMessagePollOptionResult? private(set) var currentSelection: ChatMessagePollOptionSelection? @@ -461,24 +500,39 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true + self.resultBarBackgroundNode = ASImageNode() + self.resultBarBackgroundNode.isLayerBacked = true + self.resultBarBackgroundNode.alpha = 0.0 + self.resultBarNode = ASImageNode() self.resultBarNode.isLayerBacked = true self.resultBarNode.alpha = 0.0 self.resultBarIconNode = ASImageNode() self.resultBarIconNode.isLayerBacked = true - + self.percentageNode = ASDisplayNode() self.percentageNode.alpha = 0.0 self.percentageNode.isLayerBacked = true + + self.countNode = ASDisplayNode() + self.countNode.alpha = 0.0 + self.countNode.isLayerBacked = true + + self.avatarsNode = MergedAvatarsNode() + self.avatarsNode.alpha = 0.0 + self.avatarsNode.isUserInteractionEnabled = false super.init() self.addSubnode(self.highlightedBackgroundNode) self.addSubnode(self.separatorNode) + self.addSubnode(self.resultBarBackgroundNode) self.addSubnode(self.resultBarNode) self.addSubnode(self.resultBarIconNode) self.addSubnode(self.percentageNode) + self.addSubnode(self.countNode) + self.addSubnode(self.avatarsNode) self.addSubnode(self.buttonNode) self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) @@ -562,13 +616,19 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { let leftInset: CGFloat = 50.0 let media = option.media let hasMedia = media != nil +// let hasRecentVoterAvatars = !(optionResult?.recentVoterPeerIds.isEmpty ?? true) +// let avatarsInset: CGFloat +// if hasRecentVoterAvatars { +// avatarsInset = ChatMessagePollOptionNode.avatarsSize.width + ChatMessagePollOptionNode.avatarsSpacing +// } else { +// avatarsInset = 0.0 +// } let mediaInset: CGFloat if hasMedia { mediaInset = ChatMessagePollOptionNode.mediaSize.width + ChatMessagePollOptionNode.mediaSpacing + ChatMessagePollOptionNode.mediaRightInset } else { mediaInset = 0.0 } - let rightInset: CGFloat = 10.0 + mediaInset let incoming = message.effectivelyIncoming(context.account.peerId) @@ -594,11 +654,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { blockQuoteFont: presentationData.messageFont, message: message ) - - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: optionAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: max(1.0, constrainedWidth - leftInset - rightInset), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0))) - - let contentHeight: CGFloat = max(52.0, titleLayout.size.height + 28.0) - + let shouldHaveRadioNode = optionResult == nil let isSelectable: Bool if shouldHaveRadioNode, poll.kind.multipleAnswers, !Namespaces.Message.allNonRegular.contains(message.id.namespace) { @@ -608,13 +664,32 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } let themeUpdated = presentationData.theme.theme !== currentTheme - + var updatedPercentageImage: UIImage? if currentResult != optionResult || themeUpdated { let value = optionResult?.percent ?? 0 updatedPercentageImage = generatePercentageImage(presentationData: presentationData, incoming: incoming, value: value, targetValue: value) } + let displayCount = optionResult != nil + var updatedCountImage: UIImage? + if displayCount && (currentResult != optionResult || themeUpdated) { + let value = optionResult?.count ?? 0 + updatedCountImage = generateCountImage(presentationData: presentationData, incoming: incoming, value: value, targetValue: value) + } +// let countInset: CGFloat +// if displayCount { +// let countImage = updatedCountImage ?? maybeNode?.countImage ?? generateCountImage(presentationData: presentationData, incoming: incoming, value: optionResult?.count ?? 0, targetValue: optionResult?.count ?? 0) +// countInset = countImage.size.width + ChatMessagePollOptionNode.countSpacing +// } else { +// countInset = 0.0 +// } + let rightInset: CGFloat = 10.0 + mediaInset + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: optionAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: max(1.0, constrainedWidth - leftInset - rightInset), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0))) + + let contentHeight: CGFloat = max(52.0, titleLayout.size.height + 28.0) + var resultIcon: UIImage? var updatedResultIcon = false @@ -702,6 +777,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { node.option = option let previousMedia = node.currentMedia node.currentMedia = media + node.currentRecentVoterPeerIds = optionResult?.recentVoterPeerIds ?? [] let previousResult = node.currentResult node.currentResult = optionResult node.currentSelection = selection @@ -779,7 +855,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { radioNode.removeFromSupernode() } } - + if let updatedPercentageImage = updatedPercentageImage { node.percentageNode.contents = updatedPercentageImage.cgImage node.percentageImage = updatedPercentageImage @@ -799,14 +875,57 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } } + if let updatedCountImage = updatedCountImage { + node.countNode.contents = updatedCountImage.cgImage + node.countImage = updatedCountImage + } + + let recentVoterPeers: [Peer] + if let optionResult { + recentVoterPeers = optionResult.recentVoterPeerIds.compactMap { message.peers[$0] } + } else { + recentVoterPeers = [] + } + + var trailingOriginX = width - rightInset + let mediaFrame: CGRect? if let _ = media { mediaFrame = CGRect(origin: CGPoint(x: width - ChatMessagePollOptionNode.mediaRightInset - ChatMessagePollOptionNode.mediaSize.width, y: floor((contentHeight - ChatMessagePollOptionNode.mediaSize.height) * 0.5)), size: ChatMessagePollOptionNode.mediaSize) + trailingOriginX = mediaFrame!.minX - 12.0 } else { mediaFrame = nil } node.mediaFrame = mediaFrame + if !recentVoterPeers.isEmpty { + let avatarsFrame = CGRect(origin: CGPoint(x: trailingOriginX + 15.0 - ChatMessagePollOptionNode.avatarsSize.width, y: floor((contentHeight - ChatMessagePollOptionNode.avatarsSize.height) * 0.5)), size: ChatMessagePollOptionNode.avatarsSize) + node.avatarsNode.frame = avatarsFrame + node.avatarsNode.updateLayout(size: avatarsFrame.size) + node.avatarsNode.update(context: context, peers: recentVoterPeers, synchronousLoad: attemptSynchronous, imageSize: MergedAvatarsNode.defaultMergedImageSize, imageSpacing: MergedAvatarsNode.defaultMergedImageSpacing, borderWidth: MergedAvatarsNode.defaultBorderWidth) + node.avatarsNode.alpha = 1.0 + trailingOriginX = avatarsFrame.minX - ChatMessagePollOptionNode.avatarsSpacing + } else { + node.avatarsNode.update(context: context, peers: [], synchronousLoad: attemptSynchronous, imageSize: MergedAvatarsNode.defaultMergedImageSize, imageSpacing: MergedAvatarsNode.defaultMergedImageSpacing, borderWidth: MergedAvatarsNode.defaultBorderWidth) + node.avatarsNode.alpha = 0.0 + } + + if let image = node.countImage, displayCount { + let countFrame = CGRect(origin: CGPoint(x: trailingOriginX - image.size.width, y: floor((contentHeight - image.size.height) * 0.5)), size: image.size) + node.countNode.frame = countFrame + if animated && previousResult?.count != optionResult?.count { + let countDuration = 0.27 + let images = generateCountAnimationImages(presentationData: presentationData, incoming: incoming, from: previousResult?.count ?? 0, to: optionResult?.count ?? 0, duration: countDuration) + if !images.isEmpty { + let animation = CAKeyframeAnimation(keyPath: "contents") + animation.values = images.compactMap { $0.cgImage } + animation.duration = countDuration * UIView.animationDurationFactor() + animation.calculationMode = .discrete + node.countNode.layer.add(animation, forKey: "image") + } + } + } + var isSticker = false if let media, let mediaFrame, let file = media as? TelegramMediaFile, file.isSticker || file.isCustomEmoji { isSticker = true @@ -906,6 +1025,10 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { node.separatorNode.backgroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.separator : presentationData.theme.theme.chat.message.outgoing.polls.separator node.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - UIScreenPixel), size: CGSize(width: width - leftInset - 10.0, height: UIScreenPixel)) + if node.resultBarBackgroundNode.image == nil || themeUpdated { + node.resultBarBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: optionTextColor.withMultipliedAlpha(0.07)) + } + if node.resultBarNode.image == nil || updatedResultIcon { var isQuiz = false if case .quiz = poll.kind { @@ -940,24 +1063,36 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } let minBarWidth: CGFloat = 6.0 + let maxBarWidth = width - leftInset - rightInset let resultBarWidth = minBarWidth + floor((width - leftInset - rightInset - minBarWidth) * (optionResult?.normalized ?? 0.0)) let barFrame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - 6.0 - 1.0), size: CGSize(width: resultBarWidth, height: 4.0)) + node.resultBarBackgroundNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - 6.0 - 1.0), size: CGSize(width: maxBarWidth, height: 4.0)) node.resultBarNode.frame = barFrame node.resultBarIconNode.frame = CGRect(origin: CGPoint(x: barFrame.minX - 6.0 - 16.0, y: barFrame.minY + floor((barFrame.height - 16.0) / 2.0)), size: CGSize(width: 16.0, height: 16.0)) + node.resultBarBackgroundNode.alpha = optionResult != nil ? 1.0 : 0.0 node.resultBarNode.alpha = optionResult != nil ? 1.0 : 0.0 node.percentageNode.alpha = optionResult != nil ? 1.0 : 0.0 + node.countNode.alpha = optionResult != nil ? 1.0 : 0.0 node.separatorNode.alpha = optionResult == nil ? 1.0 : 0.0 node.resultBarIconNode.alpha = optionResult != nil ? 1.0 : 0.0 if animated, currentResult != optionResult { if (currentResult != nil) != (optionResult != nil) { if optionResult != nil { + node.resultBarBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) node.resultBarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) node.percentageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + node.countNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + if !recentVoterPeers.isEmpty { + node.avatarsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } node.separatorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08) node.resultBarIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } else { + node.resultBarBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4) node.resultBarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4) node.percentageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + node.countNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + node.avatarsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) node.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) node.resultBarIconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) } @@ -1031,6 +1166,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { private let buttonNode: HighlightableButtonNode private let statusNode: ChatMessageDateAndStatusNode private var optionNodes: [ChatMessagePollOptionNode] = [] + private var shuffledOptionOpaqueIdentifiers: [Data]? private var shimmeringNodes: [ShimmeringLinkNode] = [] private let temporaryHiddenMediaDisposable = MetaDisposable() @@ -1165,6 +1301,37 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } return nil } + + private func resolvedOptionOrder(for poll: TelegramMediaPoll) -> (orderedOptions: [(Int, TelegramMediaPollOption)], shuffledOpaqueIdentifiers: [Data]?) { + let defaultOrderedOptions = Array(poll.options.enumerated()).map { ($0.offset, $0.element) } + guard poll.shuffleAnswers else { + return (defaultOrderedOptions, nil) + } + + let currentOpaqueIdentifiers = poll.options.map(\.opaqueIdentifier) + let resolvedOpaqueIdentifiers: [Data] + if let shuffledOptionOpaqueIdentifiers = self.shuffledOptionOpaqueIdentifiers, shuffledOptionOpaqueIdentifiers.count == currentOpaqueIdentifiers.count, Set(shuffledOptionOpaqueIdentifiers) == Set(currentOpaqueIdentifiers) { + resolvedOpaqueIdentifiers = shuffledOptionOpaqueIdentifiers + } else { + var generatedOpaqueIdentifiers = currentOpaqueIdentifiers + generatedOpaqueIdentifiers.shuffle() + resolvedOpaqueIdentifiers = generatedOpaqueIdentifiers + } + + let indicesByOpaqueIdentifier = Dictionary(uniqueKeysWithValues: poll.options.enumerated().map { ($0.element.opaqueIdentifier, $0.offset) }) + let orderedOptions = resolvedOpaqueIdentifiers.compactMap { opaqueIdentifier -> (Int, TelegramMediaPollOption)? in + guard let index = indicesByOpaqueIdentifier[opaqueIdentifier] else { + return nil + } + return (index, poll.options[index]) + } + + if orderedOptions.count == poll.options.count { + return (orderedOptions, resolvedOpaqueIdentifiers) + } else { + return (defaultOrderedOptions, currentOpaqueIdentifiers) + } + } private func openOptionMedia(_ media: Media) { guard let item = self.item else { @@ -1520,11 +1687,17 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } let (votersLayout, votersApply) = makeVotersLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: votersString ?? "", font: labelsFont, textColor: messageTheme.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) - + let viewResultsString: String + if let poll = poll, let totalVoters = poll.results.totalVoters { + //TODO:localize + viewResultsString = "View Votes (\(totalVoters))" + } else { + viewResultsString = item.presentationData.strings.MessagePoll_ViewResults + } let (buttonSubmitInactiveTextLayout, buttonSubmitInactiveTextApply) = makeSubmitInactiveTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.MessagePoll_SubmitVote, font: Font.regular(17.0), textColor: messageTheme.accentControlDisabledColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) let (buttonSubmitActiveTextLayout, buttonSubmitActiveTextApply) = makeSubmitActiveTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.MessagePoll_SubmitVote, font: Font.regular(17.0), textColor: messageTheme.polls.bar), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) - let (buttonViewResultsTextLayout, buttonViewResultsTextApply) = makeViewResultsTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.MessagePoll_ViewResults, font: Font.regular(17.0), textColor: messageTheme.polls.bar), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) + let (buttonViewResultsTextLayout, buttonViewResultsTextApply) = makeViewResultsTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: viewResultsString, font: Font.regular(17.0), textColor: messageTheme.polls.bar), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) var textFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), size: textLayout.size) var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom)) @@ -1554,7 +1727,13 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } var pollOptionsFinalizeLayouts: [(CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)] = [] + var orderedPollOptions: [(Int, TelegramMediaPollOption)] = [] + var shuffledOptionOpaqueIdentifiers: [Data]? if let poll = poll { + let resolvedOptionOrder = self.resolvedOptionOrder(for: poll) + orderedPollOptions = resolvedOptionOrder.orderedOptions + shuffledOptionOpaqueIdentifiers = resolvedOptionOrder.shuffledOpaqueIdentifiers + var optionVoterCount: [Int: Int32] = [:] var maxOptionVoterCount: Int32 = 0 var totalVoterCount: Int32 = 0 @@ -1593,8 +1772,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { optionVoterCounts = Array(repeating: 0, count: poll.options.count) } - for i in 0 ..< poll.options.count { - let option = poll.options[i] + for (i, option) in orderedPollOptions { let makeLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))) if let previous = previousOptionNodeLayouts[option.opaqueIdentifier] { @@ -1603,14 +1781,21 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { makeLayout = ChatMessagePollOptionNode.asyncLayout(nil) } var optionResult: ChatMessagePollOptionResult? + var recentVoterPeerIds: [PeerId] = [] + if case .public = poll.publicity, let voters = voters { + for optionVoters in voters where optionVoters.opaqueIdentifier == option.opaqueIdentifier { + recentVoterPeerIds = optionVoters.recentVoters + break + } + } if let count = optionVoterCount[i] { if maxOptionVoterCount != 0 && totalVoterCount != 0 { - optionResult = ChatMessagePollOptionResult(normalized: CGFloat(count) / CGFloat(maxOptionVoterCount), percent: optionVoterCounts[i], count: count) + optionResult = ChatMessagePollOptionResult(normalized: CGFloat(count) / CGFloat(maxOptionVoterCount), percent: optionVoterCounts[i], count: count, recentVoterPeerIds: recentVoterPeerIds) } else if isClosed { - optionResult = ChatMessagePollOptionResult(normalized: 0, percent: 0, count: 0) + optionResult = ChatMessagePollOptionResult(normalized: 0, percent: 0, count: 0, recentVoterPeerIds: recentVoterPeerIds) } } else if isClosed { - optionResult = ChatMessagePollOptionResult(normalized: 0, percent: 0, count: 0) + optionResult = ChatMessagePollOptionResult(normalized: 0, percent: 0, count: 0, recentVoterPeerIds: recentVoterPeerIds) } var translation: TranslationMessageAttribute.Additional? @@ -1718,9 +1903,9 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { for i in 0 ..< optionNodesSizesAndApply.count { let (size, apply) = optionNodesSizesAndApply[i] var isRequesting = false - if let poll = poll, i < poll.options.count { + if i < orderedPollOptions.count { if let inProgressOpaqueIds = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] { - isRequesting = inProgressOpaqueIds.contains(poll.options[i].opaqueIdentifier) + isRequesting = inProgressOpaqueIds.contains(orderedPollOptions[i].1.opaqueIdentifier) } } let optionNode = apply(animation.isAnimated, isRequesting, synchronousLoad) @@ -1760,6 +1945,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } strongSelf.optionNodes = updatedOptionNodes + strongSelf.shuffledOptionOpaqueIdentifiers = shuffledOptionOpaqueIdentifiers if textLayout.hasRTL { strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: resultSize.width - textFrame.size.width - textInsets.left - layoutConstants.text.bubbleInsets.right - additionalTextRightInset, y: textFrame.origin.y), size: textFrame.size) @@ -1997,13 +2183,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { let disableAllActions = false - var hasSelection = false - switch poll.kind { - case .poll(true): - hasSelection = true - default: - break - } + var hasSelection = poll.kind.multipleAnswers var hasSelectedOptions = false for optionNode in self.optionNodes { diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift index 44649743d0..7ee017d3d3 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift @@ -924,7 +924,8 @@ final class ComposePollScreenComponent: Component { self.pollDescriptionMedia = nil self.state?.updated(transition: .easeInOut(duration: 0.25)) } else { - + self.pollDescriptionMedia = nil + self.state?.updated(transition: .easeInOut(duration: 0.25)) } return true } @@ -935,8 +936,10 @@ final class ComposePollScreenComponent: Component { self.quizAnswerMedia = nil self.state?.updated(transition: .easeInOut(duration: 0.25)) } else { - + self.quizAnswerMedia = nil + self.state?.updated(transition: .easeInOut(duration: 0.25)) } + self.state?.updated(transition: .easeInOut(duration: 0.25)) return true } case let .pollOption(pollOption): @@ -946,7 +949,8 @@ final class ComposePollScreenComponent: Component { pollOption.media = nil self.state?.updated(transition: .easeInOut(duration: 0.25)) } else { - + pollOption.media = nil + self.state?.updated(transition: .easeInOut(duration: 0.25)) } return true }