[WIP] Polls

This commit is contained in:
Ilya Laktyushin 2026-03-13 11:04:33 +01:00
parent ce1685be40
commit cbc5a49507
42 changed files with 4437 additions and 2537 deletions

View file

@ -3001,6 +3001,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
} else {
match = false
}
case .createBot:
break
}
if match {
return true

View file

@ -2328,6 +2328,8 @@ public final class ChatListNode: ListViewImpl {
} else {
match = false
}
case .createBot:
break
}
if match {
return true

View file

@ -56,19 +56,21 @@ public extension CheckNodeTheme {
}
}
public enum CheckNodeContent {
case check
public enum CheckNodeContent: Equatable {
case check(isRectangle: Bool)
case counter(Int)
}
private final class CheckNodeParameters: NSObject {
let isRectangle: Bool
let theme: CheckNodeTheme
let content: CheckNodeContent
let animationProgress: CGFloat
let selected: Bool
let animatingOut: Bool
init(theme: CheckNodeTheme, content: CheckNodeContent, animationProgress: CGFloat, selected: Bool, animatingOut: Bool) {
init(isRectangle: Bool, theme: CheckNodeTheme, content: CheckNodeContent, animationProgress: CGFloat, selected: Bool, animatingOut: Bool) {
self.isRectangle = isRectangle
self.theme = theme
self.content = content
self.animationProgress = animationProgress
@ -86,7 +88,7 @@ public class CheckNode: ASDisplayNode {
}
}
public init(theme: CheckNodeTheme, content: CheckNodeContent = .check) {
public init(theme: CheckNodeTheme, content: CheckNodeContent = .check(isRectangle: false)) {
self.theme = theme
self.content = content
@ -161,7 +163,7 @@ public class CheckNode: ASDisplayNode {
}
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return CheckNodeParameters(theme: self.theme, content: self.content, animationProgress: self.animationProgress, selected: self.selected, animatingOut: self.animatingOut)
return CheckNodeParameters(isRectangle: self.content == .check(isRectangle: true), theme: self.theme, content: self.content, animationProgress: self.animationProgress, selected: self.selected, animatingOut: self.animatingOut)
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
@ -200,7 +202,7 @@ public class InteractiveCheckNode: CheckNode {
public var valueChanged: ((Bool) -> Void)?
override public init(theme: CheckNodeTheme, content: CheckNodeContent = .check) {
override public init(theme: CheckNodeTheme, content: CheckNodeContent = .check(isRectangle: false)) {
self.buttonNode = HighlightTrackingButtonNode()
super.init(theme: theme, content: content)
@ -250,7 +252,7 @@ public class CheckLayer: CALayer {
public override init() {
self.theme = CheckNodeTheme(backgroundColor: .white, strokeColor: .blue, borderColor: .white, overlayBorder: false, hasInset: false, hasShadow: false)
self.content = .check
self.content = .check(isRectangle: false)
super.init()
@ -270,7 +272,7 @@ public class CheckLayer: CALayer {
self.isOpaque = false
}
public init(theme: CheckNodeTheme, content: CheckNodeContent = .check) {
public init(theme: CheckNodeTheme, content: CheckNodeContent = .check(isRectangle: false)) {
self.theme = theme
self.content = content
@ -364,7 +366,7 @@ public class CheckLayer: CALayer {
CheckLayer.drawContents(
context: context,
size: size,
parameters: CheckNodeParameters(theme: self.theme, content: self.content, animationProgress: self.animationProgress, selected: self.selected, animatingOut: self.animatingOut)
parameters: CheckNodeParameters(isRectangle: self.content == .check(isRectangle: true), theme: self.theme, content: self.content, animationProgress: self.animationProgress, selected: self.selected, animatingOut: self.animatingOut)
)
})?.cgImage
}
@ -414,13 +416,24 @@ public class CheckLayer: CALayer {
let fillProgress: CGFloat = parameters.animationProgress
context.setFillColor(parameters.theme.backgroundColor.mixedWith(parameters.theme.borderColor, alpha: 1.0 - fillProgress).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
if parameters.isRectangle {
context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 7.0).cgPath)
context.fillPath()
} else {
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
}
let innerDiameter: CGFloat = (fillProgress * 0.0) + (1.0 - fillProgress) * (size.width - borderWidth * 2.0)
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: (size.width - innerDiameter) * 0.5, y: (size.height - innerDiameter) * 0.5), size: CGSize(width: innerDiameter, height: innerDiameter)))
if parameters.isRectangle {
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: (size.width - innerDiameter) * 0.5, y: (size.height - innerDiameter) * 0.5), size: CGSize(width: innerDiameter, height: innerDiameter)), cornerRadius: 6.0).cgPath)
context.fillPath()
} else {
context.fillEllipse(in: CGRect(origin: CGPoint(x: (size.width - innerDiameter) * 0.5, y: (size.height - innerDiameter) * 0.5), size: CGSize(width: innerDiameter, height: innerDiameter)))
}
context.setBlendMode(.normal)
}
} else {

View file

@ -65,7 +65,7 @@ public final class GridMessageSelectionLayer: CALayer {
public let checkLayer: CheckLayer
public init(theme: CheckNodeTheme) {
self.checkLayer = CheckLayer(theme: theme, content: .check)
self.checkLayer = CheckLayer(theme: theme, content: .check(isRectangle: false))
super.init()

View file

@ -167,6 +167,12 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
}
public enum AssetsMode: Equatable {
public enum PollMode: Equatable {
case description
case quizAnswer
case option
}
case `default`
case wallpaper
case story
@ -174,6 +180,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
case cover
case createSticker
case createAvatar
case poll(PollMode)
}
case assets(PHAssetCollection?, AssetsMode)
@ -2037,6 +2044,17 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
self.titleView.title = presentationData.strings.MediaPicker_AddImage
case .cover:
self.titleView.title = presentationData.strings.MediaPicker_ChooseCover
case let .poll(pollMode):
self.titleView.title = presentationData.strings.MediaPicker_Recents
switch pollMode {
case .description:
self.titleView.subtitle = "Add media to the poll description"
case .quizAnswer:
self.titleView.subtitle = "Add media to the quiz explanation"
case .option:
self.titleView.subtitle = "Add media to this option"
}
self.titleView.isEnabled = true
}
}
} else {

File diff suppressed because it is too large Load diff

View file

@ -139,7 +139,7 @@ public final class ShareStartAtTimestampNode: HighlightTrackingButtonNode {
self.titleTextColor = titleTextColor
self.checkNodeTheme = checkNodeTheme
self.checkNode = CheckNode(theme: checkNodeTheme, content: .check)
self.checkNode = CheckNode(theme: checkNodeTheme, content: .check(isRectangle: false))
self.checkNode.isUserInteractionEnabled = false
self.titleTextNode = TextNode()

View file

@ -55,7 +55,7 @@ private func preparedShareItem(postbox: Postbox, network: Network, to peerId: Pe
let diminsionsSize = dimensions.cgSizeValue
return .single(.preparing(false))
|> then(
standaloneUploadedImage(postbox: postbox, network: network, peerId: peerId, text: "", data: imageData, dimensions: PixelDimensions(width: Int32(diminsionsSize.width), height: Int32(diminsionsSize.height)))
standaloneUploadedImage(postbox: postbox, network: network, peerId: peerId, text: "", source: .data(imageData), dimensions: PixelDimensions(width: Int32(diminsionsSize.width), height: Int32(diminsionsSize.height)))
|> mapError { _ -> PreparedShareItemError in
return .generic
}
@ -74,7 +74,7 @@ private func preparedShareItem(postbox: Postbox, network: Network, to peerId: Pe
if let scaledImage = scalePhotoImage(image, dimensions: dimensions), let imageData = scaledImage.jpegData(compressionQuality: 0.52) {
return .single(.preparing(false))
|> then(
standaloneUploadedImage(postbox: postbox, network: network, peerId: peerId, text: "", data: imageData, dimensions: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height)))
standaloneUploadedImage(postbox: postbox, network: network, peerId: peerId, text: "", source: .data(imageData), dimensions: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height)))
|> mapError { _ -> PreparedShareItemError in
return .generic
}
@ -265,7 +265,7 @@ private func preparedShareItem(postbox: Postbox, network: Network, to peerId: Pe
let imageData = scaledImage.jpegData(compressionQuality: 0.54)!
return .single(.preparing(false))
|> then(
standaloneUploadedImage(postbox: postbox, network: network, peerId: peerId, text: "", data: imageData, dimensions: PixelDimensions(width: Int32(scaledImage.size.width), height: Int32(scaledImage.size.height)))
standaloneUploadedImage(postbox: postbox, network: network, peerId: peerId, text: "", source: .data(imageData), dimensions: PixelDimensions(width: Int32(scaledImage.size.width), height: Int32(scaledImage.size.height)))
|> mapError { _ -> PreparedShareItemError in
return .generic
}

View file

@ -52,8 +52,8 @@ private func uploadedThumbnail(network: Network, postbox: Postbox, data: Data) -
}
}
public func standaloneUploadedImage(postbox: Postbox, network: Network, peerId: PeerId, text: String, data: Data, thumbnailData: Data? = nil, dimensions: PixelDimensions) -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> {
return multipartUpload(network: network, postbox: postbox, source: .data(data), encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false)
public func standaloneUploadedImage(postbox: Postbox, network: Network, peerId: PeerId, text: String, source: MultipartUploadSource, thumbnailData: Data? = nil, dimensions: PixelDimensions) -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> {
return multipartUpload(network: network, postbox: postbox, source: source, encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false)
|> mapError { _ -> StandaloneUploadMediaError in return .generic }
|> mapToSignal { next -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> in
switch next {
@ -103,7 +103,7 @@ public func standaloneUploadedImage(postbox: Postbox, network: Network, peerId:
switch result {
case let .encryptedFile(encryptedFileData):
let (id, accessHash, size, dcId) = (encryptedFileData.id, encryptedFileData.accessHash, encryptedFileData.size, encryptedFileData.dcId)
return .single(.result(.media(.standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: Int64(data.count), datacenterId: Int(dcId), key: key), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])))))
return .single(.result(.media(.standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: size, datacenterId: Int(dcId), key: key), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])))))
case .encryptedFileEmpty:
return .fail(.generic)
}

View file

@ -187,6 +187,13 @@ public enum TelegramMediaPollKind: Equatable, PostboxCoding {
encoder.encodeInt32(multipleAnswers ? 1 : 0, forKey: "m")
}
}
public var multipleAnswers: Bool {
switch self {
case let .poll(multipleAnswers), let .quiz(multipleAnswers):
return multipleAnswers
}
}
}
public final class TelegramMediaPoll: Media, Equatable {

View file

@ -50,6 +50,7 @@ public enum PresentationResourceKey: Int32 {
case itemListDeleteIcon
case itemListDeleteIndicatorIcon
case itemListReorderIndicatorIcon
case itemListAddIndicatorIcon
case itemListLinkIcon
case itemListAddPersonIcon
case itemListCreateGroupIcon

View file

@ -146,6 +146,20 @@ public struct PresentationResourcesItemList {
})
}
public static func itemListAddIndicatorIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListAddIndicatorIcon.rawValue, { theme in
return generateImage(CGSize(width: 17.0, height: 15.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(theme.list.itemBlocksSeparatorColor.cgColor)
let lineHeight = 1.0 + UIScreenPixel
context.addPath(CGPath(roundedRect: CGRect(x: 1.0, y: 7.0, width: 15.0, height: lineHeight), cornerWidth: lineHeight / 2.0, cornerHeight: lineHeight / 2.0, transform: nil))
context.addPath(CGPath(roundedRect: CGRect(x: 8.0, y: 0.0, width: lineHeight, height: 15.0), cornerWidth: lineHeight / 2.0, cornerHeight: lineHeight / 2.0, transform: nil))
context.fillPath()
})
})
}
public static func linkIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListLinkIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Contact List/LinkActionIcon"), color: theme.list.itemAccentColor)

View file

@ -1780,6 +1780,8 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
} else {
attributedString = NSAttributedString(string: strings.Notification_CopyProtection_Request(peerName).string, font: titleFont, textColor: primaryTextColor)
}
case .managedBotCreated:
attributedString = nil
case .unknown:
attributedString = nil
}

View file

@ -112,7 +112,7 @@ swift_library(
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/ActionSheetPeerItem:ActionSheetPeerItem",
"//submodules/ComposePollUI:ComposePollUI",
"//submodules/TelegramUI/Components/ComposePollScreen",
"//submodules/AlertUI:AlertUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/TouchDownGesture:TouchDownGesture",

View file

@ -524,48 +524,53 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon
return messages
}
)
globalMusic = .single(nil)
|> then(
context.engine.peers.resolvePeerByName(name: "lybot", referrer: nil)
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
guard case let .result(result) = result else {
return .complete()
if let data = context.currentAppConfiguration.with({ $0 }).data, let searchBot = data["music_search_username"] as? String, !searchBot.isEmpty {
globalMusic = .single(nil)
|> then(
context.engine.peers.resolvePeerByName(name: searchBot, referrer: nil)
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
guard case let .result(result) = result else {
return .complete()
}
return .single(result)
}
return .single(result)
}
|> mapToSignal { peer -> Signal<ChatContextResultCollection?, NoError> in
guard let peer = peer else {
return .single(nil)
}
return context.engine.messages.requestChatContextResults(botId: peer.id, peerId: context.account.peerId, query: query, offset: "")
|> map { results -> ChatContextResultCollection? in
return results?.results
}
|> `catch` { error -> Signal<ChatContextResultCollection?, NoError> in
return .single(nil)
}
}
|> map { contextResult in
guard let results = contextResult?.results else {
return []
}
let peerId = context.account.peerId
var messages: [Message] = []
let peers = SimpleDictionary<PeerId, Peer>()
for result in results {
switch result {
case let .internalReference(internalReference):
if let file = internalReference.file {
let stableId = UInt32(clamping: file.fileId.id % Int64(Int32.max))
messages.append(Message(stableId: stableId, stableVersion: 0, id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: Int32(stableId)), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [.music], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]))
}
default:
break
|> mapToSignal { peer -> Signal<ChatContextResultCollection?, NoError> in
guard let peer = peer else {
return .single(nil)
}
return context.engine.messages.requestChatContextResults(botId: peer.id, peerId: context.account.peerId, query: query, offset: "")
|> map { results -> ChatContextResultCollection? in
return results?.results
}
|> `catch` { error -> Signal<ChatContextResultCollection?, NoError> in
return .single(nil)
}
}
return messages
}
)
|> map { contextResult in
guard let results = contextResult?.results else {
return []
}
let peerId = context.account.peerId
var messages: [Message] = []
let peers = SimpleDictionary<PeerId, Peer>()
for result in results {
switch result {
case let .internalReference(internalReference):
if let file = internalReference.file {
let stableId = UInt32(clamping: file.fileId.id % Int64(Int32.max))
messages.append(Message(stableId: stableId, stableVersion: 0, id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: Int32(stableId)), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [.music], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]))
}
default:
break
}
}
return messages
}
)
} else {
globalMusic = .single(nil)
}
}
updateActivity(true)

View file

@ -131,6 +131,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
var hasSeparateCommentsButton = false
var addedPriceInfo = false
var addedPollMedia = false
outer: for (message, itemAttributes) in item.content {
for attribute in message.attributes {
@ -283,7 +284,22 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
result.removeAll()
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
return (result, false, true)
} else if let _ = media as? TelegramMediaPoll {
} else if let poll = media as? TelegramMediaPoll {
if let attachedMedia = poll.attachedMedia {
if let _ = attachedMedia as? TelegramMediaImage {
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default)))
} else if let file = attachedMedia as? TelegramMediaFile {
let isVideo = file.isVideo || (file.isAnimated && file.dimensions != nil)
if isVideo {
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default)))
} else {
result.append((message, ChatMessageFileBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
}
} else if let _ = attachedMedia as? TelegramMediaMap {
result.append((message, ChatMessageMapBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default)))
}
addedPollMedia = true
}
result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
} else if let _ = media as? TelegramMediaTodo {
@ -318,10 +334,12 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
isMediaInverted = updatingMedia.invertMediaAttribute != nil
} else if let _ = message.attributes.first(where: { $0 is InvertMediaMessageAttribute }) {
isMediaInverted = true
} else if let _ = message.media.first(where: { $0 is TelegramMediaPoll }) {
isMediaInverted = true
}
if isMediaInverted {
result.insert((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)), at: addedPriceInfo ? 1 : 0)
result.insert((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)), at: addedPriceInfo || addedPollMedia ? 1 : 0)
} else {
result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)))
needReactions = false

View file

@ -104,6 +104,8 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
for media in item.message.media {
if let telegramFile = media as? TelegramMediaFile {
selectedFile = telegramFile
} else if let poll = media as? TelegramMediaPoll, let telegramFile = poll.attachedMedia as? TelegramMediaFile {
selectedFile = telegramFile
}
}
if let updatingMedia = item.attributes.updatingMedia, case let .update(media) = updatingMedia.media, let file = media.media as? TelegramMediaFile {

View file

@ -205,6 +205,8 @@ 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
}
}
}

View file

@ -526,7 +526,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
return { context, presentationData, message, poll, option, translation, optionResult, constrainedWidth in
let leftInset: CGFloat = 50.0
let rightInset: CGFloat = 12.0
let rightInset: CGFloat = 10.0
let incoming = message.effectivelyIncoming(context.account.peerId)
@ -559,7 +559,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
let shouldHaveRadioNode = optionResult == nil
let isSelectable: Bool
if shouldHaveRadioNode, case .poll(multipleAnswers: true) = poll.kind, !Namespaces.Message.allNonRegular.contains(message.id.namespace) {
if shouldHaveRadioNode, poll.kind.multipleAnswers, !Namespaces.Message.allNonRegular.contains(message.id.namespace) {
isSelectable = true
} else {
isSelectable = false
@ -617,7 +617,13 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar
}
context.setFillColor(fillColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
if poll.kind.multipleAnswers {
context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 4.0).cgPath)
context.fillPath()
} else {
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
}
let strokeColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barIconForeground : presentationData.theme.theme.chat.message.outgoing.polls.barIconForeground
if strokeColor.alpha.isZero {
@ -689,9 +695,9 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
))
let titleNodeFrame: CGRect
if titleLayout.hasRTL {
titleNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - titleLayout.size.width, y: 11.0), size: titleLayout.size)
titleNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - titleLayout.size.width, y: 12.0), size: titleLayout.size)
} else {
titleNodeFrame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size)
titleNodeFrame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size)
}
if node.titleNode !== titleNode {
node.titleNode = titleNode
@ -718,7 +724,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
}
let radioSize: CGFloat = 22.0
radioNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 12.0), size: CGSize(width: radioSize, height: radioSize))
radioNode.update(isRectangle: poll.kind == .poll(multipleAnswers: true), staticColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioButton : presentationData.theme.theme.chat.message.outgoing.polls.radioButton, animatedColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioProgress : presentationData.theme.theme.chat.message.outgoing.polls.radioProgress, fillColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar, foregroundColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.barIconForeground : presentationData.theme.theme.chat.message.outgoing.polls.barIconForeground, isSelectable: isSelectable, isAnimating: inProgress)
radioNode.update(isRectangle: poll.kind.multipleAnswers, staticColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioButton : presentationData.theme.theme.chat.message.outgoing.polls.radioButton, animatedColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioProgress : presentationData.theme.theme.chat.message.outgoing.polls.radioProgress, fillColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar, foregroundColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.barIconForeground : presentationData.theme.theme.chat.message.outgoing.polls.barIconForeground, isSelectable: isSelectable, isAnimating: inProgress)
} else if let radioNode = node.radioNode {
node.radioNode = nil
if animated {
@ -754,7 +760,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
node.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: contentHeight + UIScreenPixel))
}
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, height: UIScreenPixel))
node.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - UIScreenPixel), size: CGSize(width: width - leftInset - rightInset, height: UIScreenPixel))
if node.resultBarNode.image == nil || updatedResultIcon {
var isQuiz = false
@ -1270,6 +1276,8 @@ 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 (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))

View file

@ -225,6 +225,9 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
title = strings.Conversation_SetReminder_Title
case .format:
title = strings.Conversation_FormatDate_Title
case .poll:
//TODO:localize
title = "Deadline"
}
let titleSize = self.title.update(
transition: transition,
@ -384,6 +387,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
var repeatValueFrame = CGRect()
if case .format = component.mode {
contentHeight += 8.0
} else if case .poll = component.mode {
contentHeight += 8.0
} else {
transition.setFrame(layer: self.bottomSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: UIScreenPixel)))
self.bottomSeparator.backgroundColor = environment.theme.list.itemBlocksSeparatorColor.cgColor
@ -491,6 +496,9 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
}
case .format:
buttonTitle = component.currentTime != nil ? strings.Conversation_FormatDate_EditDate : strings.Conversation_FormatDate_AddDate
case .poll:
//TODO:localize
buttonTitle = "Set Deadline"
}
let buttonSideInset: CGFloat = 30.0
@ -930,6 +938,7 @@ public class ChatScheduleTimeScreen: ViewControllerComponentContainer {
case scheduledMessages(sendWhenOnlineAvailable: Bool)
case reminders
case format
case poll
}
public struct Result {

View file

@ -1,8 +1,8 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ComposePollUI",
module_name = "ComposePollUI",
name = "ComposePollScreen",
module_name = "ComposePollScreen",
srcs = glob([
"Sources/**/*.swift",
]),
@ -21,7 +21,6 @@ swift_library(
"//submodules/PresentationDataUtils",
"//submodules/TextFormat",
"//submodules/ObjCRuntimeUtils",
"//submodules/AttachmentUI",
"//submodules/TextInputMenu",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
@ -43,6 +42,17 @@ swift_library(
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
"//submodules/TelegramUI/Components/ListComposePollOptionComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/LegacyComponents",
"//submodules/LegacyUI",
"//submodules/AttachmentUI",
"//submodules/MediaPickerUI",
"//submodules/TelegramUI/Components/LegacyCamera",
"//submodules/LegacyMediaPickerUI",
"//submodules/LocationUI",
"//submodules/TelegramUI/Components/AttachmentFileController",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/TelegramUI/Components/ChatScheduleTimeController",
"//submodules/ContextUI",
],
visibility = [
"//visibility:public",

View file

@ -0,0 +1,101 @@
import Foundation
import UIKit
import Display
import AccountContext
import TelegramCore
import Postbox
import SwiftSignalKit
import TelegramPresentationData
import LegacyComponents
import LegacyUI
import AttachmentUI
import MediaPickerUI
import LegacyCamera
import LegacyMediaPickerUI
import LocationUI
import AttachmentFileController
import ChatEntityKeyboardInputNode
public func presentPollAttachmentScreen(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
availableButtons: [AttachmentButtonType],
present: @escaping (ViewController) -> Void,
completion: @escaping (AnyMediaReference) -> Void
) {
let attachmentController = AttachmentController(
context: context,
updatedPresentationData: updatedPresentationData,
style: .glass,
chatLocation: nil,
isScheduledMessages: false,
buttons: availableButtons,
initialButton: .gallery,
makeEntityInputView: {
return nil
}
)
// attachmentController.getSourceRect = { [weak self] in
// if let strongSelf = self {
// return strongSelf.chatDisplayNode.frameForAttachmentButton()?.offsetBy(dx: strongSelf.chatDisplayNode.supernode?.frame.minX ?? 0.0, dy: 0.0)
// } else {
// return nil
// }
// }
attachmentController.requestController = { [weak attachmentController] type, controllerCompletion in
switch type {
case .gallery:
let controller = MediaPickerScreenImpl(
context: context,
updatedPresentationData: updatedPresentationData,
style: .glass,
peer: nil,
threadTitle: nil,
chatLocation: nil,
enableMultiselection: false,
subject: .assets(nil, .poll(.option))
)
controller.getCaptionPanelView = {
return nil
}
controller.legacyCompletion = { fromGallery, signals, silently, scheduleTime, parameters, getAnimatedTransitionSource, sendCompletion in
let _ = (legacyAssetPickerEnqueueMessages(context: context, account: context.account, signals: signals)
|> deliverOnMainQueue).start(next: { items in
if let item = items.first, case let .message(_, _, _, mediaReference, _, _, _, _, _, _) = item.message, let mediaReference {
completion(mediaReference)
sendCompletion()
}
})
}
controllerCompletion(controller, controller.mediaPickerContext)
return true
case .file:
let controller = context.sharedContext.makeAttachmentFileController(context: context, updatedPresentationData: updatedPresentationData, bannedSendMedia: nil, presentGallery: { [weak attachmentController] in
attachmentController?.dismiss(animated: true)
//self?.presentFileGallery()
}, presentFiles: { [weak attachmentController] in
attachmentController?.dismiss(animated: true)
//self?.presentICloudFileGallery()
}, presentDocumentScanner: {
//self?.presentDocumentScanner()
}, send: { mediaReference in
completion(mediaReference)
})
guard let controller = controller as? AttachmentFileControllerImpl else {
return false
}
controllerCompletion(controller, controller.mediaPickerContext)
return true
case .location:
let controller = LocationPickerController(context: context, style: .glass, updatedPresentationData: updatedPresentationData, mode: .share(peer: nil, selfPeer: nil, hasLiveLocation: false), completion: { location, _, _, _, _ in
completion(.standalone(media: location))
})
controllerCompletion(controller, controller.mediaPickerContext)
return true
default:
return false
}
}
attachmentController.navigationPresentation = .flatModal
present(attachmentController)
}

View file

@ -42,7 +42,7 @@ swift_library(
"//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
"//submodules/TelegramUI/Components/ListComposePollOptionComponent",
"//submodules/ComposePollUI",
"//submodules/TelegramUI/Components/ComposePollScreen",
"//submodules/Markdown",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
],

View file

@ -1623,8 +1623,8 @@ final class ComposeTodoScreenComponent: Component {
}
if let input = self.validatedInput() {
controller.completion(input)
controller.dismiss()
}
controller.dismiss()
}
)),
environment: {},

View file

@ -266,7 +266,7 @@ public final class GlassBarButtonComponent: Component {
transition.animateAlpha(view: glassBackgroundView, from: 0.0, to: 1.0)
}
glassBackgroundView.update(size: containerSize, cornerRadius: cornerRadius, isDark: component.isDark, tintColor: .init(kind: effectiveState == .tintedGlass ? .custom(style: .default, color: backgroundColor.withMultipliedAlpha(effectiveState == .tintedGlass ? 1.0 : 0.7)) : .panel), isInteractive: true, isVisible: component.isVisible, transition: glassBackgroundTransition)
glassBackgroundView.update(size: containerSize, cornerRadius: cornerRadius, isDark: component.isDark, tintColor: .init(kind: effectiveState == .tintedGlass ? .custom(style: .default, color: backgroundColor.withMultipliedAlpha(effectiveState == .tintedGlass ? 1.0 : 0.7)) : .panel), isInteractive: component.isEnabled, isVisible: component.isVisible, transition: glassBackgroundTransition)
glassBackgroundTransition.setFrame(view: glassBackgroundView, frame: bounds)
} else if case .glass = component.state {
let glassBackgroundView: GlassBackgroundView
@ -283,7 +283,7 @@ public final class GlassBarButtonComponent: Component {
transition.animateAlpha(view: glassBackgroundView, from: 0.0, to: 1.0)
}
glassBackgroundView.update(size: containerSize, cornerRadius: cornerRadius, isDark: component.isDark, tintColor: .init(kind: .panel), isInteractive: true, isVisible: component.isVisible, transition: glassBackgroundTransition)
glassBackgroundView.update(size: containerSize, cornerRadius: cornerRadius, isDark: component.isDark, tintColor: .init(kind: .panel), isInteractive: component.isEnabled, isVisible: component.isVisible, transition: glassBackgroundTransition)
glassBackgroundTransition.setFrame(view: glassBackgroundView, frame: bounds)
} else if let glassBackgroundView = self.glassBackgroundView {
self.glassBackgroundView = nil

View file

@ -323,7 +323,7 @@ public final class ListActionItemComponent: Component {
if let current = self.checkLayer {
checkLayer = current
} else {
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: theme, style: .plain), content: .check)
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: theme, style: .plain), content: .check(isRectangle: false))
self.checkLayer = checkLayer
self.layer.addSublayer(checkLayer)
}

View file

@ -20,9 +20,15 @@ swift_library(
"//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/Components/BundleIconComponent",
"//submodules/CheckNode",
"//submodules/AccountContext",
"//submodules/TextFormat",
"//submodules/PresentationDataUtils",
"//submodules/PhotoResources",
"//submodules/LocationResources",
"//submodules/SemanticStatusNode",
],
visibility = [
"//visibility:public",

View file

@ -3,6 +3,7 @@ import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import TelegramCore
import CheckNode
import ListSectionComponent
import ComponentFlow
@ -12,7 +13,13 @@ import MultilineTextComponent
import PresentationDataUtils
import LottieComponent
import PlainButtonComponent
import BundleIconComponent
import SwiftSignalKit
import PhotoResources
import LocationResources
import SemanticStatusNode
import EmojiTextAttachmentView
import TextFormat
public final class ListComposePollOptionComponent: Component {
public enum Style {
@ -34,10 +41,12 @@ public final class ListComposePollOptionComponent: Component {
public final class Selection: Equatable {
public let isSelected: Bool
public let isMultiSelection: Bool
public let toggle: () -> Void
public init(isSelected: Bool, toggle: @escaping () -> Void) {
public init(isSelected: Bool, isMultiSelection: Bool = false, toggle: @escaping () -> Void) {
self.isSelected = isSelected
self.isMultiSelection = isMultiSelection
self.toggle = toggle
}
@ -45,6 +54,34 @@ public final class ListComposePollOptionComponent: Component {
if lhs.isSelected != rhs.isSelected {
return false
}
if lhs.isMultiSelection != rhs.isMultiSelection {
return false
}
return true
}
}
public final class Attachment: Equatable {
public let media: AnyMediaReference?
public let progress: CGFloat?
public let alwaysDisplayAttachButton: Bool
public init(media: AnyMediaReference?, progress: CGFloat?, alwaysDisplayAttachButton: Bool) {
self.media = media
self.progress = progress
self.alwaysDisplayAttachButton = alwaysDisplayAttachButton
}
public static func ==(lhs: Attachment, rhs: Attachment) -> Bool {
if lhs.media != rhs.media {
return false
}
if lhs.progress != rhs.progress {
return false
}
if lhs.alwaysDisplayAttachButton != rhs.alwaysDisplayAttachButton {
return false
}
return true
}
}
@ -83,8 +120,11 @@ public final class ListComposePollOptionComponent: Component {
public let resetText: ResetText?
public let assumeIsEditing: Bool
public let characterLimit: Int?
public let hasLeftInset: Bool
public let enableInlineAnimations: Bool
public let canReorder: Bool
public let canAdd: Bool
public let attachment: Attachment?
public let emptyLineHandling: TextFieldComponent.EmptyLineHandling
public let returnKeyAction: (() -> Void)?
public let backspaceKeyAction: (() -> Void)?
@ -92,6 +132,7 @@ public final class ListComposePollOptionComponent: Component {
public let inputMode: InputMode?
public let alwaysDisplayInputModeSelector: Bool
public let toggleInputMode: (() -> Void)?
public let attachAction: (() -> Void)?
public let deleteAction: (() -> Void)?
public let paste: ((TextFieldComponent.PasteData) -> Void)?
public let tag: AnyObject?
@ -108,7 +149,10 @@ public final class ListComposePollOptionComponent: Component {
assumeIsEditing: Bool = false,
characterLimit: Int,
enableInlineAnimations: Bool = true,
hasLeftInset: Bool = false,
canReorder: Bool = false,
canAdd: Bool = false,
attachment: Attachment? = nil,
emptyLineHandling: TextFieldComponent.EmptyLineHandling,
returnKeyAction: (() -> Void)?,
backspaceKeyAction: (() -> Void)?,
@ -116,6 +160,7 @@ public final class ListComposePollOptionComponent: Component {
inputMode: InputMode?,
alwaysDisplayInputModeSelector: Bool = false,
toggleInputMode: (() -> Void)?,
attachAction: (() -> Void)? = nil,
deleteAction: (() -> Void)? = nil,
paste: ((TextFieldComponent.PasteData) -> Void)? = nil,
tag: AnyObject? = nil
@ -131,7 +176,10 @@ public final class ListComposePollOptionComponent: Component {
self.assumeIsEditing = assumeIsEditing
self.characterLimit = characterLimit
self.enableInlineAnimations = enableInlineAnimations
self.hasLeftInset = hasLeftInset
self.canReorder = canReorder
self.canAdd = canAdd
self.attachment = attachment
self.emptyLineHandling = emptyLineHandling
self.returnKeyAction = returnKeyAction
self.backspaceKeyAction = backspaceKeyAction
@ -139,6 +187,7 @@ public final class ListComposePollOptionComponent: Component {
self.inputMode = inputMode
self.alwaysDisplayInputModeSelector = alwaysDisplayInputModeSelector
self.toggleInputMode = toggleInputMode
self.attachAction = attachAction
self.deleteAction = deleteAction
self.paste = paste
self.tag = tag
@ -178,9 +227,18 @@ public final class ListComposePollOptionComponent: Component {
if lhs.enableInlineAnimations != rhs.enableInlineAnimations {
return false
}
if lhs.hasLeftInset != rhs.hasLeftInset {
return false
}
if lhs.canReorder != rhs.canReorder {
return false
}
if lhs.canAdd != rhs.canAdd {
return false
}
if lhs.attachment != rhs.attachment {
return false
}
if lhs.emptyLineHandling != rhs.emptyLineHandling {
return false
}
@ -202,6 +260,7 @@ public final class ListComposePollOptionComponent: Component {
private final class CheckView: HighlightTrackingButton {
private var checkLayer: CheckLayer?
private var theme: PresentationTheme?
private var isRectangle = false
var action: (() -> Void)?
@ -251,22 +310,27 @@ public final class ListComposePollOptionComponent: Component {
self.action?()
}
func update(size: CGSize, theme: PresentationTheme, isSelected: Bool, transition: ComponentTransition) {
func update(size: CGSize, isRectangle: Bool, theme: PresentationTheme, isSelected: Bool, transition: ComponentTransition) {
let checkLayer: CheckLayer
if let current = self.checkLayer {
checkLayer = current
} else {
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: theme, style: .plain), content: .check)
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: theme, style: .plain), content: .check(isRectangle: isRectangle))
self.checkLayer = checkLayer
self.layer.addSublayer(checkLayer)
}
if self.theme !== theme {
self.theme = theme
checkLayer.theme = CheckNodeTheme(theme: theme, style: .plain)
}
if self.isRectangle != isRectangle {
self.isRectangle = isRectangle
checkLayer.content = .check(isRectangle: isRectangle)
}
checkLayer.frame = CGRect(origin: CGPoint(), size: size)
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
}
@ -385,6 +449,13 @@ public final class ListComposePollOptionComponent: Component {
private var modeSelector: ComponentView<Empty>?
private var reorderIconView: UIImageView?
private var addIconView: UIImageView?
private var attachButton: ComponentView<Empty>?
private var imageNode: TransformImageNode?
private var statusNode: SemanticStatusNode?
private var animationLayer: InlineStickerItemLayer?
private let imageButton = HighlightTrackingButton()
private var checkView: CheckView?
@ -396,6 +467,8 @@ public final class ListComposePollOptionComponent: Component {
private var customPlaceholder: ComponentView<Empty>?
private var appliedMedia: AnyMediaReference?
private var component: ListComposePollOptionComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
@ -430,6 +503,25 @@ public final class ListComposePollOptionComponent: Component {
public override init(frame: CGRect) {
super.init(frame: CGRect())
self.imageButton.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if highlighted {
self.imageNode?.layer.removeAnimation(forKey: "opacity")
self.imageNode?.alpha = 0.4
self.animationLayer?.removeAnimation(forKey: "opacity")
self.animationLayer?.opacity = 0.4
} else {
self.imageNode?.alpha = 1.0
self.imageNode?.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
self.animationLayer?.opacity = 1.0
self.animationLayer?.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
required public init?(coder: NSCoder) {
@ -551,6 +643,13 @@ public final class ListComposePollOptionComponent: Component {
self.component?.deleteAction?()
}
}
@objc private func imageButtonPressed() {
guard let component = self.component else {
return
}
component.attachAction?()
}
func update(component: ListComposePollOptionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
@ -570,6 +669,10 @@ public final class ListComposePollOptionComponent: Component {
var rightInset: CGFloat = 16.0
let modeSelectorSize = CGSize(width: 32.0, height: 32.0)
if component.hasLeftInset {
leftInset += 46.0
}
if component.selection != nil {
leftInset += 34.0
}
@ -578,8 +681,8 @@ public final class ListComposePollOptionComponent: Component {
rightInset += 34.0
}
if component.canReorder {
rightInset += 16.0
if component.attachment != nil {
rightInset += 28.0
}
let textFieldSize = self.textField.update(
@ -641,8 +744,89 @@ public final class ListComposePollOptionComponent: Component {
)
let size = CGSize(width: availableSize.width, height: textFieldSize.height - 1.0)
let textFieldFrame = CGRect(origin: CGPoint(x: leftInset - 16.0 + self.revealOffset, y: 0.0), size: textFieldSize)
var hasReorderIcon = false
if component.canReorder, let externalState = component.externalState, externalState.hasText {
var reorderIconTransition = transition
let reorderIconView: UIImageView
if let current = self.reorderIconView {
reorderIconView = current
} else {
reorderIconTransition = reorderIconTransition.withAnimation(.none)
reorderIconView = UIImageView()
self.reorderIconView = reorderIconView
self.addSubview(reorderIconView)
if !transition.animation.isImmediate {
transition.animateAlpha(view: reorderIconView, from: 0.0, to: 1.0)
transition.animateScale(view: reorderIconView, from: 0.001, to: 1.0)
}
}
reorderIconView.image = PresentationResourcesItemList.itemListReorderIndicatorIcon(component.theme)
var reorderIconSize = CGSize()
if let icon = reorderIconView.image {
reorderIconSize = icon.size
}
let reorderIconFrame = CGRect(origin: CGPoint(x: 22.0 + self.revealOffset, y: floor((size.height - reorderIconSize.height) * 0.5)), size: reorderIconSize)
reorderIconTransition.setPosition(view: reorderIconView, position: reorderIconFrame.center)
reorderIconTransition.setBounds(view: reorderIconView, bounds: CGRect(origin: CGPoint(), size: reorderIconFrame.size))
hasReorderIcon = true
} else if let reorderIconView = self.reorderIconView {
self.reorderIconView = nil
if !transition.animation.isImmediate {
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
alphaTransition.setAlpha(view: reorderIconView, alpha: 0.0, completion: { [weak reorderIconView] _ in
reorderIconView?.removeFromSuperview()
})
alphaTransition.setScale(view: reorderIconView, scale: 0.001)
} else {
reorderIconView.removeFromSuperview()
}
}
if component.canAdd, !hasReorderIcon {
var addIconTransition = transition
let addIconView: UIImageView
if let current = self.addIconView {
addIconView = current
} else {
addIconTransition = addIconTransition.withAnimation(.none)
addIconView = UIImageView()
self.addIconView = addIconView
self.addSubview(addIconView)
if !transition.animation.isImmediate {
transition.animateAlpha(view: addIconView, from: 0.0, to: 1.0)
transition.animateScale(view: addIconView, from: 0.001, to: 1.0)
}
}
addIconView.image = PresentationResourcesItemList.itemListAddIndicatorIcon(component.theme)
var addIconSize = CGSize()
if let icon = addIconView.image {
addIconSize = icon.size
}
let addIconFrame = CGRect(origin: CGPoint(x: 22.0 + self.revealOffset, y: floor((size.height - addIconSize.height) * 0.5)), size: addIconSize)
addIconTransition.setPosition(view: addIconView, position: addIconFrame.center)
addIconTransition.setBounds(view: addIconView, bounds: CGRect(origin: CGPoint(), size: addIconFrame.size))
} else if let addIconView = self.addIconView {
self.addIconView = nil
if !transition.animation.isImmediate {
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
alphaTransition.setAlpha(view: addIconView, alpha: 0.0, completion: { [weak addIconView] _ in
addIconView?.removeFromSuperview()
})
alphaTransition.setScale(view: addIconView, scale: 0.001)
} else {
addIconView.removeFromSuperview()
}
}
let textFieldFrame = CGRect(origin: CGPoint(x: leftInset - 16.0 + self.revealOffset, y: 0.0), size: textFieldSize)
if let textFieldView = self.textField.view {
if textFieldView.superview == nil {
self.addSubview(textFieldView)
@ -673,17 +857,17 @@ public final class ListComposePollOptionComponent: Component {
}
}
let checkSize = CGSize(width: 22.0, height: 22.0)
let checkFrame = CGRect(origin: CGPoint(x: floor((leftInset - checkSize.width) * 0.5) + self.revealOffset, y: floor((size.height - checkSize.height) * 0.5)), size: checkSize)
let checkFrame = CGRect(origin: CGPoint(x: leftInset - checkSize.width - 20.0 + self.revealOffset, y: floor((size.height - checkSize.height) * 0.5)), size: checkSize)
if animateIn {
checkView.frame = CGRect(origin: CGPoint(x: -checkSize.width, y: self.bounds.height == 0.0 ? checkFrame.minY : floor((self.bounds.height - checkSize.height) * 0.5)), size: checkFrame.size)
transition.setPosition(view: checkView, position: checkFrame.center)
transition.setBounds(view: checkView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size))
checkView.update(size: checkFrame.size, theme: component.theme, isSelected: selection.isSelected, transition: .immediate)
checkView.update(size: checkFrame.size, isRectangle: selection.isMultiSelection, theme: component.theme, isSelected: selection.isSelected, transition: .immediate)
} else {
transition.setPosition(view: checkView, position: checkFrame.center)
transition.setBounds(view: checkView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size))
checkView.update(size: checkFrame.size, theme: component.theme, isSelected: selection.isSelected, transition: transition)
checkView.update(size: checkFrame.size, isRectangle: selection.isMultiSelection, theme: component.theme, isSelected: selection.isSelected, transition: transition)
}
} else if let checkView = self.checkView {
self.checkView = nil
@ -692,40 +876,237 @@ public final class ListComposePollOptionComponent: Component {
})
}
var rightIconsInset: CGFloat = 0.0
if component.canReorder, let externalState = component.externalState, externalState.hasText {
var reorderIconTransition = transition
let reorderIconView: UIImageView
if let current = self.reorderIconView {
reorderIconView = current
var rightIconsInset: CGFloat = 16.0
let minHeight: CGFloat = 52.0
if let attachment = component.attachment, attachment.alwaysDisplayAttachButton || component.externalState?.hasText == true {
var attachButtonTransition = transition
let attachButton: ComponentView<Empty>
if let current = self.attachButton {
attachButton = current
} else {
reorderIconTransition = reorderIconTransition.withAnimation(.none)
reorderIconView = UIImageView()
self.reorderIconView = reorderIconView
self.addSubview(reorderIconView)
attachButtonTransition = attachButtonTransition.withAnimation(.none)
attachButton = ComponentView()
self.attachButton = attachButton
}
reorderIconView.image = PresentationResourcesItemList.itemListReorderIndicatorIcon(component.theme)
var reorderIconSize = CGSize()
if let icon = reorderIconView.image {
reorderIconSize = icon.size
let attachButtonSize = attachButton.update(
transition: attachButtonTransition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(BundleIconComponent(
name: "Chat/Input/Text/IconAttachment",
tintColor: component.theme.chat.inputPanel.inputControlColor.blitOver(component.theme.list.itemBlocksBackgroundColor, alpha: 1.0),
maxSize: CGSize(width: 25.0, height: 25.0)
)),
effectAlignment: .center,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.attachAction?()
},
animateScale: false
)),
environment: {},
containerSize: availableSize
)
let attachButtonFrame = CGRect(origin: CGPoint(x: size.width - rightIconsInset - 7.0 - attachButtonSize.width + self.revealOffset, y: size.height - minHeight + floor((minHeight - attachButtonSize.height) * 0.5)), size: attachButtonSize)
if let attachButtonView = attachButton.view as? PlainButtonComponent.View {
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
if attachButtonView.superview == nil {
self.addSubview(attachButtonView)
ComponentTransition.immediate.setAlpha(view: attachButtonView, alpha: 0.0)
ComponentTransition.immediate.setScale(view: attachButtonView, scale: 0.001)
}
attachButtonTransition.setPosition(view: attachButtonView, position: attachButtonFrame.center)
attachButtonTransition.setBounds(view: attachButtonView, bounds: CGRect(origin: CGPoint(), size: attachButtonFrame.size))
let displaySelector = attachment.media == nil
alphaTransition.setAlpha(view: attachButtonView, alpha: displaySelector ? 1.0 : 0.0)
alphaTransition.setScale(view: attachButtonView, scale: displaySelector ? 1.0 : 0.001)
}
let reorderIconFrame = CGRect(origin: CGPoint(x: size.width - 14.0 - reorderIconSize.width + self.revealOffset, y: floor((size.height - reorderIconSize.height) * 0.5)), size: reorderIconSize)
reorderIconTransition.setPosition(view: reorderIconView, position: reorderIconFrame.center)
reorderIconTransition.setBounds(view: reorderIconView, bounds: CGRect(origin: CGPoint(), size: reorderIconFrame.size))
rightIconsInset += 42.0
} else if let attachButton = self.attachButton {
self.attachButton = nil
if let attachButtonView = attachButton.view {
if !transition.animation.isImmediate {
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
alphaTransition.setAlpha(view: attachButtonView, alpha: 0.0, completion: { [weak attachButtonView] _ in
attachButtonView?.removeFromSuperview()
})
alphaTransition.setScale(view: attachButtonView, scale: 0.001)
} else {
attachButtonView.removeFromSuperview()
}
}
}
let imageNodeSize = CGSize(width: 40.0, height: 40.0)
let imageNodeFrame = CGRect(origin: CGPoint(x: size.width - 16.0 - imageNodeSize.width + self.revealOffset, y: size.height - minHeight + floor((minHeight - imageNodeSize.height) * 0.5)), size: imageNodeSize)
var isSticker = false
if let attachment = component.attachment, let file = attachment.media?.media as? TelegramMediaFile, file.isSticker {
isSticker = true
rightIconsInset += 36.0
} else if let reorderIconView = self.reorderIconView {
self.reorderIconView = nil
let animationSize = CGSize(width: 40.0, height: 40.0)
let animationLayer: InlineStickerItemLayer
if let current = self.animationLayer {
animationLayer = current
} else {
if let animationLayer = self.animationLayer {
self.animationLayer = nil
animationLayer.removeFromSuperlayer()
}
animationLayer = InlineStickerItemLayer(
context: component.context,
userLocation: .other,
attemptSynchronousLoad: true,
emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file, custom: nil, enableAnimation: true),
file: file,
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
unique: false,
placeholderColor: component.theme.list.mediaPlaceholderColor,
pointSize: CGSize(width: animationSize.width * 2.0, height: animationSize.height * 2.0),
dynamicColor: nil,
loopCount: 2
)
self.animationLayer = animationLayer
self.layer.addSublayer(animationLayer)
}
animationLayer.frame = imageNodeFrame
if self.imageButton.superview == nil {
self.imageButton.addTarget(self, action: #selector(self.imageButtonPressed), for: .touchUpInside)
self.addSubview(self.imageButton)
}
self.imageButton.frame = imageNodeFrame
} else if let animationLayer = self.animationLayer {
self.imageNode = nil
if !transition.animation.isImmediate {
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
alphaTransition.setAlpha(view: reorderIconView, alpha: 0.0, completion: { [weak reorderIconView] _ in
reorderIconView?.removeFromSuperview()
alphaTransition.setAlpha(layer: animationLayer, alpha: 0.0, completion: { [weak animationLayer] _ in
animationLayer?.removeFromSuperlayer()
})
alphaTransition.setScale(view: reorderIconView, scale: 0.001)
alphaTransition.setScale(layer: animationLayer, scale: 0.001)
} else {
reorderIconView.removeFromSuperview()
animationLayer.removeFromSuperlayer()
}
self.imageButton.removeFromSuperview()
}
if let attachment = component.attachment, let media = attachment.media, !isSticker {
var imageNodeTransition = transition
let imageNode: TransformImageNode
if let current = self.imageNode {
imageNode = current
} else {
imageNodeTransition = imageNodeTransition.withAnimation(.none)
imageNode = TransformImageNode()
imageNode.isUserInteractionEnabled = false
self.imageNode = imageNode
self.addSubview(imageNode.view)
}
imageNodeTransition.setPosition(view: imageNode.view, position: imageNodeFrame.center)
imageNodeTransition.setBounds(view: imageNode.view, bounds: CGRect(origin: CGPoint(), size: imageNodeFrame.size))
var imageSize = imageNodeSize
var updateMedia = false
if self.appliedMedia != media {
self.appliedMedia = media
updateMedia = true
}
if let image = media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations), let photoReference = media.concrete(TelegramMediaImage.self) {
imageSize = largest.dimensions.cgSize.aspectFilled(imageNodeSize)
if updateMedia {
imageNode.setSignal(chatMessagePhoto(postbox: component.context.account.postbox, userLocation: .other, photoReference: photoReference))
}
} else if let file = media.media as? TelegramMediaFile, let fileReference = media.concrete(TelegramMediaFile.self) {
if let dimensions = file.dimensions {
imageSize = dimensions.cgSize.aspectFilled(imageNodeSize)
}
if file.mimeType.hasPrefix("image/") {
if updateMedia {
imageNode.setSignal(instantPageImageFile(account: component.context.account, userLocation: .other, fileReference: fileReference, fetched: true))
}
} else {
if updateMedia {
imageNode.setSignal(chatMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: fileReference))
}
}
} else if let map = media.media as? TelegramMediaMap {
imageSize = CGSize(width: 40.0, height: 40.0)
if updateMedia {
let resource = MapSnapshotMediaResource(latitude: map.latitude, longitude: map.longitude, width: Int32(imageSize.width), height: Int32(imageSize.height))
imageNode.setSignal(chatMapSnapshotImage(engine: component.context.engine, resource: resource))
}
}
let cornerRadius: CGFloat = 10.0
let makeLayout = imageNode.asyncLayout()
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: cornerRadius), imageSize: imageSize, boundingSize: imageNodeSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))
apply()
if self.imageButton.superview == nil {
self.imageButton.addTarget(self, action: #selector(self.imageButtonPressed), for: .touchUpInside)
self.addSubview(self.imageButton)
}
self.imageButton.frame = imageNodeFrame
if let progress = attachment.progress {
let statusNode: SemanticStatusNode
if let current = self.statusNode {
statusNode = current
} else {
statusNode = SemanticStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.5), foregroundNodeColor: .white)
self.statusNode = statusNode
self.addSubview(statusNode.view)
}
let progressFrame = imageNodeFrame.insetBy(dx: 6.0, dy: 6.0)
statusNode.frame = progressFrame
statusNode.transitionToState(.progress(value: max(0.027, min(1.0, progress)), cancelEnabled: true, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 1.0, lineWidth: 1.0 + UIScreenPixel), animateRotation: false), updateCutout: false)
} else if let statusNode = self.statusNode {
self.statusNode = nil
if !transition.animation.isImmediate {
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
alphaTransition.setAlpha(view: statusNode.view, alpha: 0.0, completion: { [weak statusNode] _ in
statusNode?.view.removeFromSuperview()
})
alphaTransition.setScale(view: statusNode.view, scale: 0.001)
} else {
statusNode.view.removeFromSuperview()
}
}
} else if let imageNode = self.imageNode {
self.imageNode = nil
if !transition.animation.isImmediate {
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
alphaTransition.setAlpha(view: imageNode.view, alpha: 0.0, completion: { [weak imageNode] _ in
imageNode?.view.removeFromSuperview()
})
alphaTransition.setScale(view: imageNode.view, scale: 0.001)
} else {
imageNode.view.removeFromSuperview()
}
self.imageButton.removeFromSuperview()
if let statusNode = self.statusNode {
self.statusNode = nil
if !transition.animation.isImmediate {
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
alphaTransition.setAlpha(view: statusNode.view, alpha: 0.0, completion: { [weak statusNode] _ in
statusNode?.view.removeFromSuperview()
})
alphaTransition.setScale(view: statusNode.view, scale: 0.001)
} else {
statusNode.view.removeFromSuperview()
}
}
}
@ -775,7 +1156,7 @@ public final class ListComposePollOptionComponent: Component {
environment: {},
containerSize: modeSelectorSize
)
let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - rightIconsInset - 4.0 - modeSelectorSize.width + self.revealOffset, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize)
let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - rightIconsInset - 4.0 - modeSelectorSize.width + self.revealOffset, y: size.height - minHeight + floor((minHeight - modeSelectorSize.height) * 0.5)), size: modeSelectorSize)
if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View {
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
@ -881,6 +1262,10 @@ public final class ListComposePollOptionComponent: Component {
var leftInset: CGFloat = 16.0
let rightInset: CGFloat = 16.0
if component.hasLeftInset {
leftInset += 46.0
}
if component.selection != nil {
leftInset += 34.0
}

View file

@ -136,6 +136,8 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
self.customTitle = self.presentationData.strings.RequestPeer_ChooseGroupTitle
case .channel:
self.customTitle = self.presentationData.strings.RequestPeer_ChooseChannelTitle
case .createBot:
break
}
} else {
self.customTitle = self.presentationData.strings.ChatImport_Title

View file

@ -1210,6 +1210,10 @@ final class PeerSelectionControllerNode: ASDisplayNode {
emptyText = ""
}
emptyButtonText = self.presentationData.strings.RequestPeer_CreateNewGroup
case .createBot:
emptyTitle = ""
emptyText = ""
emptyButtonText = ""
}
self.emptyTitleNode.attributedText = NSAttributedString(string: emptyTitle, font: Font.semibold(15.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
@ -1863,6 +1867,8 @@ private func stringForRequestPeerType(strings: PresentationStrings, peerType: Re
append(rightsString)
}
}
case .createBot:
break
}
if lines.isEmpty {
return nil

View file

@ -34,7 +34,6 @@ swift_library(
"//submodules/TelegramUI/Components/ChatTimerScreen",
"//submodules/TextFormat",
"//submodules/PhoneNumberFormat",
"//submodules/ComposePollUI",
"//submodules/TelegramIntents",
"//submodules/LegacyUI",
"//submodules/WebSearchUI",

View file

@ -15,7 +15,6 @@ import ChatEntityKeyboardInputNode
import ChatScheduleTimeController
import TextFormat
import PhoneNumberFormat
import ComposePollUI
import TelegramIntents
import LegacyUI
import WebSearchUI

View file

@ -21,7 +21,6 @@ import DeviceLocationManager
import ShareController
import UrlEscaping
import ContextUI
import ComposePollUI
import AlertUI
import PresentationDataUtils
import UndoUI

View file

@ -21,7 +21,6 @@ import DeviceLocationManager
import ShareController
import UrlEscaping
import ContextUI
import ComposePollUI
import AlertUI
import PresentationDataUtils
import UndoUI

View file

@ -21,7 +21,6 @@ import DeviceLocationManager
import ShareController
import UrlEscaping
import ContextUI
import ComposePollUI
import AlertUI
import PresentationDataUtils
import UndoUI

View file

@ -21,7 +21,6 @@ import DeviceLocationManager
import ShareController
import UrlEscaping
import ContextUI
import ComposePollUI
import AlertUI
import PresentationDataUtils
import UndoUI

View file

@ -21,7 +21,6 @@ import DeviceLocationManager
import ShareController
import UrlEscaping
import ContextUI
import ComposePollUI
import AlertUI
import PresentationDataUtils
import UndoUI

View file

@ -21,7 +21,6 @@ import DeviceLocationManager
import ShareController
import UrlEscaping
import ContextUI
import ComposePollUI
import AlertUI
import PresentationDataUtils
import UndoUI

View file

@ -21,8 +21,6 @@ import DeviceLocationManager
import ShareController
import UrlEscaping
import ContextUI
import ComposePollUI
import ComposeTodoScreen
import AlertUI
import PresentationDataUtils
import UndoUI
@ -4625,6 +4623,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
createNewGroupImpl = { [weak controller] in
switch peerType {
case .createBot:
break
case .user:
break
case let .group(group):

View file

@ -34,7 +34,7 @@ import MediaEditorScreen
import CameraScreen
import ShareController
import ComposeTodoScreen
import ComposePollUI
import ComposePollScreen
import Photos
import AttachmentFileController
@ -1291,26 +1291,6 @@ extension ChatControllerImpl {
self?.openCamera(cameraView: nil)
}
}
controller.presentWebSearch = { [weak self, weak controller] mediaGroups, activateOnDisplay in
self?.presentWebSearch(editingMessage: false, attachment: true, activateOnDisplay: activateOnDisplay, present: { [weak controller] c, a in
controller?.present(c, in: .current)
if let webSearchController = c as? WebSearchController {
webSearchController.searchingUpdated = { [weak mediaGroups] searching in
if let mediaGroups = mediaGroups, mediaGroups.isNodeLoaded {
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
transition.updateAlpha(node: mediaGroups.displayNode, alpha: searching ? 0.0 : 1.0)
mediaGroups.displayNode.isUserInteractionEnabled = !searching
}
}
webSearchController.present(mediaGroups, in: .current)
webSearchController.dismissed = {
updateMediaPickerContext(mediaPickerContext)
}
controller?.webSearchController = webSearchController
updateMediaPickerContext(webSearchController.mediaPickerContext)
}
})
}
controller.presentSchedulePicker = { [weak self] media, done in
if let strongSelf = self {
strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] time, repeatPeriod in
@ -2049,9 +2029,15 @@ extension ChatControllerImpl {
})
}
}, nil)
var attributes: [MessageAttribute] = []
if !poll.description.entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: poll.description.entities))
}
let message: EnqueueMessage = .message(
text: "",
attributes: [],
text: poll.description.string,
attributes: attributes,
inlineStickers: [:],
mediaReference: .standalone(media: TelegramMediaPoll(
pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: Int64.random(in: Int64.min...Int64.max)),
@ -2063,7 +2049,12 @@ extension ChatControllerImpl {
correctAnswers: poll.correctAnswers,
results: poll.results,
isClosed: false,
deadlineTimeout: poll.deadlineTimeout
deadlineTimeout: poll.deadlineTimeout,
openAnswers: poll.openAnswers,
revotingDisabled: poll.revotingDisabled,
shuffleAnswers: poll.shuffleAnswers,
hideResultsUntilClose: poll.hideResultsUntilClose,
attachedMedia: poll.media?.media
)),
threadId: self.chatLocation.threadId,
replyToMessageId: nil,

View file

@ -1589,7 +1589,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
hasSelected = true
}
}
if hasSelected, case .poll = activePoll.kind {
if hasSelected, !activePoll.revotingDisabled {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_UnvotePoll, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unvote"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in