[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 { } else {
match = false match = false
} }
case .createBot:
break
} }
if match { if match {
return true return true

View file

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

View file

@ -56,19 +56,21 @@ public extension CheckNodeTheme {
} }
} }
public enum CheckNodeContent { public enum CheckNodeContent: Equatable {
case check case check(isRectangle: Bool)
case counter(Int) case counter(Int)
} }
private final class CheckNodeParameters: NSObject { private final class CheckNodeParameters: NSObject {
let isRectangle: Bool
let theme: CheckNodeTheme let theme: CheckNodeTheme
let content: CheckNodeContent let content: CheckNodeContent
let animationProgress: CGFloat let animationProgress: CGFloat
let selected: Bool let selected: Bool
let animatingOut: 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.theme = theme
self.content = content self.content = content
self.animationProgress = animationProgress 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.theme = theme
self.content = content self.content = content
@ -161,7 +163,7 @@ public class CheckNode: ASDisplayNode {
} }
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { 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) { @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)? 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() self.buttonNode = HighlightTrackingButtonNode()
super.init(theme: theme, content: content) super.init(theme: theme, content: content)
@ -250,7 +252,7 @@ public class CheckLayer: CALayer {
public override init() { public override init() {
self.theme = CheckNodeTheme(backgroundColor: .white, strokeColor: .blue, borderColor: .white, overlayBorder: false, hasInset: false, hasShadow: false) 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() super.init()
@ -270,7 +272,7 @@ public class CheckLayer: CALayer {
self.isOpaque = false self.isOpaque = false
} }
public init(theme: CheckNodeTheme, content: CheckNodeContent = .check) { public init(theme: CheckNodeTheme, content: CheckNodeContent = .check(isRectangle: false)) {
self.theme = theme self.theme = theme
self.content = content self.content = content
@ -364,7 +366,7 @@ public class CheckLayer: CALayer {
CheckLayer.drawContents( CheckLayer.drawContents(
context: context, context: context,
size: size, 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 })?.cgImage
} }
@ -414,13 +416,24 @@ public class CheckLayer: CALayer {
let fillProgress: CGFloat = parameters.animationProgress let fillProgress: CGFloat = parameters.animationProgress
context.setFillColor(parameters.theme.backgroundColor.mixedWith(parameters.theme.borderColor, alpha: 1.0 - fillProgress).cgColor) 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) let innerDiameter: CGFloat = (fillProgress * 0.0) + (1.0 - fillProgress) * (size.width - borderWidth * 2.0)
context.setBlendMode(.copy) context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor) 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) context.setBlendMode(.normal)
} }
} else { } else {

View file

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

View file

@ -167,6 +167,12 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
} }
public enum AssetsMode: Equatable { public enum AssetsMode: Equatable {
public enum PollMode: Equatable {
case description
case quizAnswer
case option
}
case `default` case `default`
case wallpaper case wallpaper
case story case story
@ -174,6 +180,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
case cover case cover
case createSticker case createSticker
case createAvatar case createAvatar
case poll(PollMode)
} }
case assets(PHAssetCollection?, AssetsMode) case assets(PHAssetCollection?, AssetsMode)
@ -2037,6 +2044,17 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
self.titleView.title = presentationData.strings.MediaPicker_AddImage self.titleView.title = presentationData.strings.MediaPicker_AddImage
case .cover: case .cover:
self.titleView.title = presentationData.strings.MediaPicker_ChooseCover 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 { } 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.titleTextColor = titleTextColor
self.checkNodeTheme = checkNodeTheme self.checkNodeTheme = checkNodeTheme
self.checkNode = CheckNode(theme: checkNodeTheme, content: .check) self.checkNode = CheckNode(theme: checkNodeTheme, content: .check(isRectangle: false))
self.checkNode.isUserInteractionEnabled = false self.checkNode.isUserInteractionEnabled = false
self.titleTextNode = TextNode() self.titleTextNode = TextNode()

View file

@ -55,7 +55,7 @@ private func preparedShareItem(postbox: Postbox, network: Network, to peerId: Pe
let diminsionsSize = dimensions.cgSizeValue let diminsionsSize = dimensions.cgSizeValue
return .single(.preparing(false)) return .single(.preparing(false))
|> then( |> 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 |> mapError { _ -> PreparedShareItemError in
return .generic 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) { if let scaledImage = scalePhotoImage(image, dimensions: dimensions), let imageData = scaledImage.jpegData(compressionQuality: 0.52) {
return .single(.preparing(false)) return .single(.preparing(false))
|> then( |> 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 |> mapError { _ -> PreparedShareItemError in
return .generic return .generic
} }
@ -265,7 +265,7 @@ private func preparedShareItem(postbox: Postbox, network: Network, to peerId: Pe
let imageData = scaledImage.jpegData(compressionQuality: 0.54)! let imageData = scaledImage.jpegData(compressionQuality: 0.54)!
return .single(.preparing(false)) return .single(.preparing(false))
|> then( |> 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 |> mapError { _ -> PreparedShareItemError in
return .generic 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> { 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: .data(data), encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) 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 } |> mapError { _ -> StandaloneUploadMediaError in return .generic }
|> mapToSignal { next -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> in |> mapToSignal { next -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> in
switch next { switch next {
@ -103,7 +103,7 @@ public func standaloneUploadedImage(postbox: Postbox, network: Network, peerId:
switch result { switch result {
case let .encryptedFile(encryptedFileData): case let .encryptedFile(encryptedFileData):
let (id, accessHash, size, dcId) = (encryptedFileData.id, encryptedFileData.accessHash, encryptedFileData.size, encryptedFileData.dcId) 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: case .encryptedFileEmpty:
return .fail(.generic) return .fail(.generic)
} }

View file

@ -187,6 +187,13 @@ public enum TelegramMediaPollKind: Equatable, PostboxCoding {
encoder.encodeInt32(multipleAnswers ? 1 : 0, forKey: "m") 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 { public final class TelegramMediaPoll: Media, Equatable {

View file

@ -50,6 +50,7 @@ public enum PresentationResourceKey: Int32 {
case itemListDeleteIcon case itemListDeleteIcon
case itemListDeleteIndicatorIcon case itemListDeleteIndicatorIcon
case itemListReorderIndicatorIcon case itemListReorderIndicatorIcon
case itemListAddIndicatorIcon
case itemListLinkIcon case itemListLinkIcon
case itemListAddPersonIcon case itemListAddPersonIcon
case itemListCreateGroupIcon 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? { public static func linkIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListLinkIcon.rawValue, { theme in return theme.image(PresentationResourceKey.itemListLinkIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Contact List/LinkActionIcon"), color: theme.list.itemAccentColor) return generateTintedImage(image: UIImage(bundleImageName: "Contact List/LinkActionIcon"), color: theme.list.itemAccentColor)

View file

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

View file

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

View file

@ -524,48 +524,53 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon
return messages return messages
} }
) )
globalMusic = .single(nil)
|> then( if let data = context.currentAppConfiguration.with({ $0 }).data, let searchBot = data["music_search_username"] as? String, !searchBot.isEmpty {
context.engine.peers.resolvePeerByName(name: "lybot", referrer: nil) globalMusic = .single(nil)
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in |> then(
guard case let .result(result) = result else { context.engine.peers.resolvePeerByName(name: searchBot, referrer: nil)
return .complete() |> 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 {
|> mapToSignal { peer -> Signal<ChatContextResultCollection?, NoError> in return .single(nil)
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 context.engine.messages.requestChatContextResults(botId: peer.id, peerId: context.account.peerId, query: query, offset: "") return results?.results
|> map { results -> ChatContextResultCollection? in }
return results?.results |> `catch` { error -> Signal<ChatContextResultCollection?, NoError> in
} return .single(nil)
|> `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
} }
} }
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) updateActivity(true)

View file

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

View file

@ -104,6 +104,8 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
for media in item.message.media { for media in item.message.media {
if let telegramFile = media as? TelegramMediaFile { if let telegramFile = media as? TelegramMediaFile {
selectedFile = telegramFile 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 { 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 { if item.presentationData.isPreview {
automaticDownload = .full 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 return { context, presentationData, message, poll, option, translation, optionResult, constrainedWidth in
let leftInset: CGFloat = 50.0 let leftInset: CGFloat = 50.0
let rightInset: CGFloat = 12.0 let rightInset: CGFloat = 10.0
let incoming = message.effectivelyIncoming(context.account.peerId) let incoming = message.effectivelyIncoming(context.account.peerId)
@ -559,7 +559,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
let shouldHaveRadioNode = optionResult == nil let shouldHaveRadioNode = optionResult == nil
let isSelectable: Bool 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 isSelectable = true
} else { } else {
isSelectable = false 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 fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar
} }
context.setFillColor(fillColor.cgColor) 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 let strokeColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barIconForeground : presentationData.theme.theme.chat.message.outgoing.polls.barIconForeground
if strokeColor.alpha.isZero { if strokeColor.alpha.isZero {
@ -689,9 +695,9 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
)) ))
let titleNodeFrame: CGRect let titleNodeFrame: CGRect
if titleLayout.hasRTL { 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 { } 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 { if node.titleNode !== titleNode {
node.titleNode = titleNode node.titleNode = titleNode
@ -718,7 +724,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
} }
let radioSize: CGFloat = 22.0 let radioSize: CGFloat = 22.0
radioNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 12.0), size: CGSize(width: radioSize, height: radioSize)) 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 { } else if let radioNode = node.radioNode {
node.radioNode = nil node.radioNode = nil
if animated { 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.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.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 { if node.resultBarNode.image == nil || updatedResultIcon {
var isQuiz = false 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 (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 (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 (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: 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 title = strings.Conversation_SetReminder_Title
case .format: case .format:
title = strings.Conversation_FormatDate_Title title = strings.Conversation_FormatDate_Title
case .poll:
//TODO:localize
title = "Deadline"
} }
let titleSize = self.title.update( let titleSize = self.title.update(
transition: transition, transition: transition,
@ -384,6 +387,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
var repeatValueFrame = CGRect() var repeatValueFrame = CGRect()
if case .format = component.mode { if case .format = component.mode {
contentHeight += 8.0 contentHeight += 8.0
} else if case .poll = component.mode {
contentHeight += 8.0
} else { } else {
transition.setFrame(layer: self.bottomSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: UIScreenPixel))) 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 self.bottomSeparator.backgroundColor = environment.theme.list.itemBlocksSeparatorColor.cgColor
@ -491,6 +496,9 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
} }
case .format: case .format:
buttonTitle = component.currentTime != nil ? strings.Conversation_FormatDate_EditDate : strings.Conversation_FormatDate_AddDate buttonTitle = component.currentTime != nil ? strings.Conversation_FormatDate_EditDate : strings.Conversation_FormatDate_AddDate
case .poll:
//TODO:localize
buttonTitle = "Set Deadline"
} }
let buttonSideInset: CGFloat = 30.0 let buttonSideInset: CGFloat = 30.0
@ -930,6 +938,7 @@ public class ChatScheduleTimeScreen: ViewControllerComponentContainer {
case scheduledMessages(sendWhenOnlineAvailable: Bool) case scheduledMessages(sendWhenOnlineAvailable: Bool)
case reminders case reminders
case format case format
case poll
} }
public struct Result { public struct Result {

View file

@ -1,8 +1,8 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library( swift_library(
name = "ComposePollUI", name = "ComposePollScreen",
module_name = "ComposePollUI", module_name = "ComposePollScreen",
srcs = glob([ srcs = glob([
"Sources/**/*.swift", "Sources/**/*.swift",
]), ]),
@ -21,7 +21,6 @@ swift_library(
"//submodules/PresentationDataUtils", "//submodules/PresentationDataUtils",
"//submodules/TextFormat", "//submodules/TextFormat",
"//submodules/ObjCRuntimeUtils", "//submodules/ObjCRuntimeUtils",
"//submodules/AttachmentUI",
"//submodules/TextInputMenu", "//submodules/TextInputMenu",
"//submodules/ComponentFlow", "//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters", "//submodules/Components/ComponentDisplayAdapters",
@ -43,6 +42,17 @@ swift_library(
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent", "//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
"//submodules/TelegramUI/Components/ListComposePollOptionComponent", "//submodules/TelegramUI/Components/ListComposePollOptionComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent", "//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 = [
"//visibility:public", "//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/ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent", "//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
"//submodules/TelegramUI/Components/ListComposePollOptionComponent", "//submodules/TelegramUI/Components/ListComposePollOptionComponent",
"//submodules/ComposePollUI", "//submodules/TelegramUI/Components/ComposePollScreen",
"//submodules/Markdown", "//submodules/Markdown",
"//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/TelegramUI/Components/GlassBarButtonComponent",
], ],

View file

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

View file

@ -266,7 +266,7 @@ public final class GlassBarButtonComponent: Component {
transition.animateAlpha(view: glassBackgroundView, from: 0.0, to: 1.0) 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) glassBackgroundTransition.setFrame(view: glassBackgroundView, frame: bounds)
} else if case .glass = component.state { } else if case .glass = component.state {
let glassBackgroundView: GlassBackgroundView let glassBackgroundView: GlassBackgroundView
@ -283,7 +283,7 @@ public final class GlassBarButtonComponent: Component {
transition.animateAlpha(view: glassBackgroundView, from: 0.0, to: 1.0) 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) glassBackgroundTransition.setFrame(view: glassBackgroundView, frame: bounds)
} else if let glassBackgroundView = self.glassBackgroundView { } else if let glassBackgroundView = self.glassBackgroundView {
self.glassBackgroundView = nil self.glassBackgroundView = nil

View file

@ -323,7 +323,7 @@ public final class ListActionItemComponent: Component {
if let current = self.checkLayer { if let current = self.checkLayer {
checkLayer = current checkLayer = current
} else { } 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.checkLayer = checkLayer
self.layer.addSublayer(checkLayer) self.layer.addSublayer(checkLayer)
} }

View file

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

View file

@ -3,6 +3,7 @@ import UIKit
import Display import Display
import AsyncDisplayKit import AsyncDisplayKit
import TelegramPresentationData import TelegramPresentationData
import TelegramCore
import CheckNode import CheckNode
import ListSectionComponent import ListSectionComponent
import ComponentFlow import ComponentFlow
@ -12,7 +13,13 @@ import MultilineTextComponent
import PresentationDataUtils import PresentationDataUtils
import LottieComponent import LottieComponent
import PlainButtonComponent import PlainButtonComponent
import BundleIconComponent
import SwiftSignalKit import SwiftSignalKit
import PhotoResources
import LocationResources
import SemanticStatusNode
import EmojiTextAttachmentView
import TextFormat
public final class ListComposePollOptionComponent: Component { public final class ListComposePollOptionComponent: Component {
public enum Style { public enum Style {
@ -34,10 +41,12 @@ public final class ListComposePollOptionComponent: Component {
public final class Selection: Equatable { public final class Selection: Equatable {
public let isSelected: Bool public let isSelected: Bool
public let isMultiSelection: Bool
public let toggle: () -> Void 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.isSelected = isSelected
self.isMultiSelection = isMultiSelection
self.toggle = toggle self.toggle = toggle
} }
@ -45,6 +54,34 @@ public final class ListComposePollOptionComponent: Component {
if lhs.isSelected != rhs.isSelected { if lhs.isSelected != rhs.isSelected {
return false 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 return true
} }
} }
@ -83,8 +120,11 @@ public final class ListComposePollOptionComponent: Component {
public let resetText: ResetText? public let resetText: ResetText?
public let assumeIsEditing: Bool public let assumeIsEditing: Bool
public let characterLimit: Int? public let characterLimit: Int?
public let hasLeftInset: Bool
public let enableInlineAnimations: Bool public let enableInlineAnimations: Bool
public let canReorder: Bool public let canReorder: Bool
public let canAdd: Bool
public let attachment: Attachment?
public let emptyLineHandling: TextFieldComponent.EmptyLineHandling public let emptyLineHandling: TextFieldComponent.EmptyLineHandling
public let returnKeyAction: (() -> Void)? public let returnKeyAction: (() -> Void)?
public let backspaceKeyAction: (() -> Void)? public let backspaceKeyAction: (() -> Void)?
@ -92,6 +132,7 @@ public final class ListComposePollOptionComponent: Component {
public let inputMode: InputMode? public let inputMode: InputMode?
public let alwaysDisplayInputModeSelector: Bool public let alwaysDisplayInputModeSelector: Bool
public let toggleInputMode: (() -> Void)? public let toggleInputMode: (() -> Void)?
public let attachAction: (() -> Void)?
public let deleteAction: (() -> Void)? public let deleteAction: (() -> Void)?
public let paste: ((TextFieldComponent.PasteData) -> Void)? public let paste: ((TextFieldComponent.PasteData) -> Void)?
public let tag: AnyObject? public let tag: AnyObject?
@ -108,7 +149,10 @@ public final class ListComposePollOptionComponent: Component {
assumeIsEditing: Bool = false, assumeIsEditing: Bool = false,
characterLimit: Int, characterLimit: Int,
enableInlineAnimations: Bool = true, enableInlineAnimations: Bool = true,
hasLeftInset: Bool = false,
canReorder: Bool = false, canReorder: Bool = false,
canAdd: Bool = false,
attachment: Attachment? = nil,
emptyLineHandling: TextFieldComponent.EmptyLineHandling, emptyLineHandling: TextFieldComponent.EmptyLineHandling,
returnKeyAction: (() -> Void)?, returnKeyAction: (() -> Void)?,
backspaceKeyAction: (() -> Void)?, backspaceKeyAction: (() -> Void)?,
@ -116,6 +160,7 @@ public final class ListComposePollOptionComponent: Component {
inputMode: InputMode?, inputMode: InputMode?,
alwaysDisplayInputModeSelector: Bool = false, alwaysDisplayInputModeSelector: Bool = false,
toggleInputMode: (() -> Void)?, toggleInputMode: (() -> Void)?,
attachAction: (() -> Void)? = nil,
deleteAction: (() -> Void)? = nil, deleteAction: (() -> Void)? = nil,
paste: ((TextFieldComponent.PasteData) -> Void)? = nil, paste: ((TextFieldComponent.PasteData) -> Void)? = nil,
tag: AnyObject? = nil tag: AnyObject? = nil
@ -131,7 +176,10 @@ public final class ListComposePollOptionComponent: Component {
self.assumeIsEditing = assumeIsEditing self.assumeIsEditing = assumeIsEditing
self.characterLimit = characterLimit self.characterLimit = characterLimit
self.enableInlineAnimations = enableInlineAnimations self.enableInlineAnimations = enableInlineAnimations
self.hasLeftInset = hasLeftInset
self.canReorder = canReorder self.canReorder = canReorder
self.canAdd = canAdd
self.attachment = attachment
self.emptyLineHandling = emptyLineHandling self.emptyLineHandling = emptyLineHandling
self.returnKeyAction = returnKeyAction self.returnKeyAction = returnKeyAction
self.backspaceKeyAction = backspaceKeyAction self.backspaceKeyAction = backspaceKeyAction
@ -139,6 +187,7 @@ public final class ListComposePollOptionComponent: Component {
self.inputMode = inputMode self.inputMode = inputMode
self.alwaysDisplayInputModeSelector = alwaysDisplayInputModeSelector self.alwaysDisplayInputModeSelector = alwaysDisplayInputModeSelector
self.toggleInputMode = toggleInputMode self.toggleInputMode = toggleInputMode
self.attachAction = attachAction
self.deleteAction = deleteAction self.deleteAction = deleteAction
self.paste = paste self.paste = paste
self.tag = tag self.tag = tag
@ -178,9 +227,18 @@ public final class ListComposePollOptionComponent: Component {
if lhs.enableInlineAnimations != rhs.enableInlineAnimations { if lhs.enableInlineAnimations != rhs.enableInlineAnimations {
return false return false
} }
if lhs.hasLeftInset != rhs.hasLeftInset {
return false
}
if lhs.canReorder != rhs.canReorder { if lhs.canReorder != rhs.canReorder {
return false return false
} }
if lhs.canAdd != rhs.canAdd {
return false
}
if lhs.attachment != rhs.attachment {
return false
}
if lhs.emptyLineHandling != rhs.emptyLineHandling { if lhs.emptyLineHandling != rhs.emptyLineHandling {
return false return false
} }
@ -202,6 +260,7 @@ public final class ListComposePollOptionComponent: Component {
private final class CheckView: HighlightTrackingButton { private final class CheckView: HighlightTrackingButton {
private var checkLayer: CheckLayer? private var checkLayer: CheckLayer?
private var theme: PresentationTheme? private var theme: PresentationTheme?
private var isRectangle = false
var action: (() -> Void)? var action: (() -> Void)?
@ -251,22 +310,27 @@ public final class ListComposePollOptionComponent: Component {
self.action?() 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 let checkLayer: CheckLayer
if let current = self.checkLayer { if let current = self.checkLayer {
checkLayer = current checkLayer = current
} else { } 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.checkLayer = checkLayer
self.layer.addSublayer(checkLayer) self.layer.addSublayer(checkLayer)
} }
if self.theme !== theme { if self.theme !== theme {
self.theme = theme self.theme = theme
checkLayer.theme = CheckNodeTheme(theme: theme, style: .plain) 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.frame = CGRect(origin: CGPoint(), size: size)
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate) checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
} }
@ -385,6 +449,13 @@ public final class ListComposePollOptionComponent: Component {
private var modeSelector: ComponentView<Empty>? private var modeSelector: ComponentView<Empty>?
private var reorderIconView: UIImageView? 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? private var checkView: CheckView?
@ -396,6 +467,8 @@ public final class ListComposePollOptionComponent: Component {
private var customPlaceholder: ComponentView<Empty>? private var customPlaceholder: ComponentView<Empty>?
private var appliedMedia: AnyMediaReference?
private var component: ListComposePollOptionComponent? private var component: ListComposePollOptionComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
private var isUpdating: Bool = false private var isUpdating: Bool = false
@ -430,6 +503,25 @@ public final class ListComposePollOptionComponent: Component {
public override init(frame: CGRect) { public override init(frame: CGRect) {
super.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) { required public init?(coder: NSCoder) {
@ -551,6 +643,13 @@ public final class ListComposePollOptionComponent: Component {
self.component?.deleteAction?() 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 { func update(component: ListComposePollOptionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true self.isUpdating = true
@ -570,6 +669,10 @@ public final class ListComposePollOptionComponent: Component {
var rightInset: CGFloat = 16.0 var rightInset: CGFloat = 16.0
let modeSelectorSize = CGSize(width: 32.0, height: 32.0) let modeSelectorSize = CGSize(width: 32.0, height: 32.0)
if component.hasLeftInset {
leftInset += 46.0
}
if component.selection != nil { if component.selection != nil {
leftInset += 34.0 leftInset += 34.0
} }
@ -578,8 +681,8 @@ public final class ListComposePollOptionComponent: Component {
rightInset += 34.0 rightInset += 34.0
} }
if component.canReorder { if component.attachment != nil {
rightInset += 16.0 rightInset += 28.0
} }
let textFieldSize = self.textField.update( 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 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 let textFieldView = self.textField.view {
if textFieldView.superview == nil { if textFieldView.superview == nil {
self.addSubview(textFieldView) self.addSubview(textFieldView)
@ -673,17 +857,17 @@ public final class ListComposePollOptionComponent: Component {
} }
} }
let checkSize = CGSize(width: 22.0, height: 22.0) 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 { 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) 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.setPosition(view: checkView, position: checkFrame.center)
transition.setBounds(view: checkView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size)) 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 { } else {
transition.setPosition(view: checkView, position: checkFrame.center) transition.setPosition(view: checkView, position: checkFrame.center)
transition.setBounds(view: checkView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size)) 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 { } else if let checkView = self.checkView {
self.checkView = nil self.checkView = nil
@ -692,40 +876,237 @@ public final class ListComposePollOptionComponent: Component {
}) })
} }
var rightIconsInset: CGFloat = 0.0 var rightIconsInset: CGFloat = 16.0
if component.canReorder, let externalState = component.externalState, externalState.hasText { let minHeight: CGFloat = 52.0
var reorderIconTransition = transition
let reorderIconView: UIImageView if let attachment = component.attachment, attachment.alwaysDisplayAttachButton || component.externalState?.hasText == true {
if let current = self.reorderIconView { var attachButtonTransition = transition
reorderIconView = current let attachButton: ComponentView<Empty>
if let current = self.attachButton {
attachButton = current
} else { } else {
reorderIconTransition = reorderIconTransition.withAnimation(.none) attachButtonTransition = attachButtonTransition.withAnimation(.none)
reorderIconView = UIImageView() attachButton = ComponentView()
self.reorderIconView = reorderIconView self.attachButton = attachButton
self.addSubview(reorderIconView)
} }
reorderIconView.image = PresentationResourcesItemList.itemListReorderIndicatorIcon(component.theme)
let attachButtonSize = attachButton.update(
var reorderIconSize = CGSize() transition: attachButtonTransition,
if let icon = reorderIconView.image { component: AnyComponent(PlainButtonComponent(
reorderIconSize = icon.size 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) rightIconsInset += 42.0
reorderIconTransition.setPosition(view: reorderIconView, position: reorderIconFrame.center) } else if let attachButton = self.attachButton {
reorderIconTransition.setBounds(view: reorderIconView, bounds: CGRect(origin: CGPoint(), size: reorderIconFrame.size)) 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 let animationSize = CGSize(width: 40.0, height: 40.0)
} else if let reorderIconView = self.reorderIconView { let animationLayer: InlineStickerItemLayer
self.reorderIconView = nil 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 { if !transition.animation.isImmediate {
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
alphaTransition.setAlpha(view: reorderIconView, alpha: 0.0, completion: { [weak reorderIconView] _ in alphaTransition.setAlpha(layer: animationLayer, alpha: 0.0, completion: { [weak animationLayer] _ in
reorderIconView?.removeFromSuperview() animationLayer?.removeFromSuperlayer()
}) })
alphaTransition.setScale(view: reorderIconView, scale: 0.001) alphaTransition.setScale(layer: animationLayer, scale: 0.001)
} else { } 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: {}, environment: {},
containerSize: modeSelectorSize 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 { if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View {
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
@ -881,6 +1262,10 @@ public final class ListComposePollOptionComponent: Component {
var leftInset: CGFloat = 16.0 var leftInset: CGFloat = 16.0
let rightInset: CGFloat = 16.0 let rightInset: CGFloat = 16.0
if component.hasLeftInset {
leftInset += 46.0
}
if component.selection != nil { if component.selection != nil {
leftInset += 34.0 leftInset += 34.0
} }

View file

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

View file

@ -1210,6 +1210,10 @@ final class PeerSelectionControllerNode: ASDisplayNode {
emptyText = "" emptyText = ""
} }
emptyButtonText = self.presentationData.strings.RequestPeer_CreateNewGroup 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) 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) append(rightsString)
} }
} }
case .createBot:
break
} }
if lines.isEmpty { if lines.isEmpty {
return nil return nil

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -34,7 +34,7 @@ import MediaEditorScreen
import CameraScreen import CameraScreen
import ShareController import ShareController
import ComposeTodoScreen import ComposeTodoScreen
import ComposePollUI import ComposePollScreen
import Photos import Photos
import AttachmentFileController import AttachmentFileController
@ -1291,26 +1291,6 @@ extension ChatControllerImpl {
self?.openCamera(cameraView: nil) 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 controller.presentSchedulePicker = { [weak self] media, done in
if let strongSelf = self { if let strongSelf = self {
strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] time, repeatPeriod in strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] time, repeatPeriod in
@ -2049,9 +2029,15 @@ extension ChatControllerImpl {
}) })
} }
}, nil) }, nil)
var attributes: [MessageAttribute] = []
if !poll.description.entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: poll.description.entities))
}
let message: EnqueueMessage = .message( let message: EnqueueMessage = .message(
text: "", text: poll.description.string,
attributes: [], attributes: attributes,
inlineStickers: [:], inlineStickers: [:],
mediaReference: .standalone(media: TelegramMediaPoll( mediaReference: .standalone(media: TelegramMediaPoll(
pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: Int64.random(in: Int64.min...Int64.max)), pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: Int64.random(in: Int64.min...Int64.max)),
@ -2063,7 +2049,12 @@ extension ChatControllerImpl {
correctAnswers: poll.correctAnswers, correctAnswers: poll.correctAnswers,
results: poll.results, results: poll.results,
isClosed: false, 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, threadId: self.chatLocation.threadId,
replyToMessageId: nil, replyToMessageId: nil,

View file

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