Merge branch 'todo'

This commit is contained in:
Ilya Laktyushin 2025-06-08 18:33:02 +02:00
commit 3fd4154ddd
83 changed files with 4526 additions and 2953 deletions

View File

@ -14422,3 +14422,19 @@ Sorry for the inconvenience.";
"Chat.TitleJoinGroupCall" = "Join"; "Chat.TitleJoinGroupCall" = "Join";
"Invitation.JoinGroupCall.EnableMicrophone" = "Switch on the microphone"; "Invitation.JoinGroupCall.EnableMicrophone" = "Switch on the microphone";
"Chat.PinnedGroupCallTitle" = "Pinned Group Call"; "Chat.PinnedGroupCallTitle" = "Pinned Group Call";
"Notification.TodoTasks_1" = "%@ task";
"Notification.TodoTasks_any" = "%@ tasks";
"Notification.TodoCompleted" = "%1$@ marked \"%2$@\" as done.";
"Notification.TodoIncompleted" = "%1$@ marked \"%2$@\" as undone.";
"Notification.TodoAddedTask" = "%1$@ added a new task \"%2$@\" to \"%3$@\".";
"Notification.TodoAddedMultipleTasks" = "%1$@ added %2$@ to \"%3$@\".";
"Notification.TodoCompletedYou" = "You marked \"%1$@\" as done.";
"Notification.TodoIncompletedYou" = "You marked \"%1$@\" as not done.";
"Notification.TodoAddedTaskYou" = "You added a new task \"%1$@\" to \"%2$@\".";
"Notification.TodoAddedMultipleTasksYou" = "You added %1$@ to \"%2$@\".";
"Stars.Transaction.GiftTransfer" = "Gift Transfer";
"Stars.Intro.Transaction.GiftTransfer" = "Gift Transfer";

View File

@ -43,6 +43,7 @@ public enum PremiumIntroSource {
case animatedEmoji case animatedEmoji
case messageEffects case messageEffects
case paidMessages case paidMessages
case todo
case auth(String) case auth(String)
} }
@ -82,6 +83,7 @@ public enum PremiumDemoSubject {
case business case business
case messageEffects case messageEffects
case paidMessages case paidMessages
case todo
case businessLocation case businessLocation
case businessHours case businessHours

View File

@ -21,6 +21,7 @@ public enum AttachmentButtonType: Equatable {
case gallery case gallery
case file case file
case location case location
case todo
case quickReply case quickReply
case contact case contact
case poll case poll
@ -36,6 +37,8 @@ public enum AttachmentButtonType: Equatable {
return "file" return "file"
case .location: case .location:
return "location" return "location"
case .todo:
return "todo"
case .quickReply: case .quickReply:
return "quickReply" return "quickReply"
case .contact: case .contact:
@ -71,6 +74,12 @@ public enum AttachmentButtonType: Equatable {
} else { } else {
return false return false
} }
case .todo:
if case .todo = rhs {
return true
} else {
return false
}
case .quickReply: case .quickReply:
if case .quickReply = rhs { if case .quickReply = rhs {
return true return true

View File

@ -195,6 +195,9 @@ private final class AttachButtonComponent: CombinedComponent {
case .location: case .location:
name = strings.Attachment_Location name = strings.Attachment_Location
imageName = "Chat/Attach Menu/Location" imageName = "Chat/Attach Menu/Location"
case .todo:
name = "To Do List"
imageName = "Chat/Attach Menu/Todo"
case .contact: case .contact:
name = strings.Attachment_Contact name = strings.Attachment_Contact
imageName = "Chat/Attach Menu/Contact" imageName = "Chat/Attach Menu/Contact"
@ -1267,6 +1270,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
}, openBoostToUnrestrict: { }, openBoostToUnrestrict: {
}, updateRecordingTrimRange: { _, _, _, _ in }, updateRecordingTrimRange: { _, _, _, _ in
}, dismissAllTooltips: { }, dismissAllTooltips: {
}, editTodoMessage: { _, _ in
}, updateHistoryFilter: { _ in }, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in }, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: { }, toggleChatSidebarMode: {
@ -1490,6 +1494,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
accessibilityTitle = self.presentationData.strings.Attachment_File accessibilityTitle = self.presentationData.strings.Attachment_File
case .location: case .location:
accessibilityTitle = self.presentationData.strings.Attachment_Location accessibilityTitle = self.presentationData.strings.Attachment_Location
case .todo:
//TODO:localize
accessibilityTitle = "To Do List"
case .contact: case .contact:
accessibilityTitle = self.presentationData.strings.Attachment_Contact accessibilityTitle = self.presentationData.strings.Attachment_Contact
case .poll: case .poll:

View File

@ -333,7 +333,8 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P
if let cutoutRect { if let cutoutRect {
context.setBlendMode(.copy) context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor) context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: cutoutRect.offsetBy(dx: 0.0, dy: size.height - cutoutRect.maxY - cutoutRect.height)) //TODO:fix
context.fillEllipse(in: cutoutRect)
} }
}) })
let unroundedImage: UIImage? let unroundedImage: UIImage?

View File

@ -176,6 +176,8 @@ public final class BrowserBookmarksScreen: ViewController {
}, playShakeAnimation: { }, playShakeAnimation: {
}, displayQuickShare: { _, _ ,_ in }, displayQuickShare: { _, _ ,_ in
}, updateChatLocationThread: { _, _ in }, updateChatLocationThread: { _, _ in
}, requestToggleTodoMessageItem: { _, _, _ in
}, displayTodoToggleUnavailable: { _ in
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))

View File

@ -108,7 +108,7 @@ swift_library(
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
"//submodules/TelegramUI/Components/TextFieldComponent", "//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/ComposePollUI", "//submodules/TelegramUI/Components/ListComposePollOptionComponent",
"//submodules/ChatPresentationInterfaceState", "//submodules/ChatPresentationInterfaceState",
"//submodules/ShimmerEffect:ShimmerEffect", "//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/LottieComponent",

View File

@ -20,7 +20,7 @@ import AsyncDisplayKit
import UndoUI import UndoUI
import PeerNameColorItem import PeerNameColorItem
import EntityKeyboard import EntityKeyboard
import ComposePollUI import ListComposePollOptionComponent
import ChatEntityKeyboardInputNode import ChatEntityKeyboardInputNode
import ComponentFlow import ComponentFlow
import ChatPresentationInterfaceState import ChatPresentationInterfaceState

View File

@ -8,7 +8,7 @@ import TextNodeWithEntities
import AccountContext import AccountContext
import ItemListUI import ItemListUI
import ComponentFlow import ComponentFlow
import ComposePollUI import ListComposePollOptionComponent
import TextFieldComponent import TextFieldComponent
public class ItemListFilterTitleInputItem: ListViewItem, ItemListItem { public class ItemListFilterTitleInputItem: ListViewItem, ItemListItem {

View File

@ -423,6 +423,20 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
if messageText.isEmpty, case let .Loaded(content) = webpage.content { if messageText.isEmpty, case let .Loaded(content) = webpage.content {
messageText = content.displayUrl messageText = content.displayUrl
} }
case let todo as TelegramMediaTodo:
let pollPrefix = "☑️ "
let entityOffset = (pollPrefix as NSString).length
messageText = "\(pollPrefix)\(todo.text)"
for entity in todo.textEntities {
if case let .CustomEmoji(_, fileId) = entity.type {
if customEmojiRanges == nil {
customEmojiRanges = []
}
let range = NSRange(location: entityOffset + entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
let attribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: message.associatedMedia[EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile)
customEmojiRanges?.append((range, attribute))
}
}
default: default:
break break
} }

View File

@ -177,6 +177,7 @@ public final class ChatPanelInterfaceInteraction {
public let openBoostToUnrestrict: () -> Void public let openBoostToUnrestrict: () -> Void
public let updateRecordingTrimRange: (Double, Double, Bool, Bool) -> Void public let updateRecordingTrimRange: (Double, Double, Bool, Bool) -> Void
public let dismissAllTooltips: () -> Void public let dismissAllTooltips: () -> Void
public let editTodoMessage: (MessageId, Bool) -> Void
public let requestLayout: (ContainedViewLayoutTransition) -> Void public let requestLayout: (ContainedViewLayoutTransition) -> Void
public let chatController: () -> ViewController? public let chatController: () -> ViewController?
public let statuses: ChatPanelInterfaceInteractionStatuses? public let statuses: ChatPanelInterfaceInteractionStatuses?
@ -293,6 +294,7 @@ public final class ChatPanelInterfaceInteraction {
openBoostToUnrestrict: @escaping () -> Void, openBoostToUnrestrict: @escaping () -> Void,
updateRecordingTrimRange: @escaping (Double, Double, Bool, Bool) -> Void, updateRecordingTrimRange: @escaping (Double, Double, Bool, Bool) -> Void,
dismissAllTooltips: @escaping () -> Void, dismissAllTooltips: @escaping () -> Void,
editTodoMessage: @escaping (MessageId, Bool) -> Void,
updateHistoryFilter: @escaping ((ChatPresentationInterfaceState.HistoryFilter?) -> ChatPresentationInterfaceState.HistoryFilter?) -> Void, updateHistoryFilter: @escaping ((ChatPresentationInterfaceState.HistoryFilter?) -> ChatPresentationInterfaceState.HistoryFilter?) -> Void,
updateChatLocationThread: @escaping (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void, updateChatLocationThread: @escaping (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void,
toggleChatSidebarMode: @escaping () -> Void, toggleChatSidebarMode: @escaping () -> Void,
@ -412,6 +414,7 @@ public final class ChatPanelInterfaceInteraction {
self.openBoostToUnrestrict = openBoostToUnrestrict self.openBoostToUnrestrict = openBoostToUnrestrict
self.updateRecordingTrimRange = updateRecordingTrimRange self.updateRecordingTrimRange = updateRecordingTrimRange
self.dismissAllTooltips = dismissAllTooltips self.dismissAllTooltips = dismissAllTooltips
self.editTodoMessage = editTodoMessage
self.updateHistoryFilter = updateHistoryFilter self.updateHistoryFilter = updateHistoryFilter
self.updateChatLocationThread = updateChatLocationThread self.updateChatLocationThread = updateChatLocationThread
self.toggleChatSidebarMode = toggleChatSidebarMode self.toggleChatSidebarMode = toggleChatSidebarMode
@ -540,6 +543,7 @@ public final class ChatPanelInterfaceInteraction {
}, openBoostToUnrestrict: { }, openBoostToUnrestrict: {
}, updateRecordingTrimRange: { _, _, _, _ in }, updateRecordingTrimRange: { _, _, _, _ in
}, dismissAllTooltips: { }, dismissAllTooltips: {
}, editTodoMessage: { _, _ in
}, updateHistoryFilter: { _ in }, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in }, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: { }, toggleChatSidebarMode: {

View File

@ -41,6 +41,7 @@ swift_library(
"//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/ChatPresentationInterfaceState", "//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent", "//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
"//submodules/TelegramUI/Components/ListComposePollOptionComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -26,6 +26,49 @@ import ChatPresentationInterfaceState
import EmojiSuggestionsComponent import EmojiSuggestionsComponent
import TextFormat import TextFormat
import TextFieldComponent import TextFieldComponent
import ListComposePollOptionComponent
public final class ComposedPoll {
public struct Text {
public let string: String
public let entities: [MessageTextEntity]
public init(string: String, entities: [MessageTextEntity]) {
self.string = string
self.entities = entities
}
}
public let publicity: TelegramMediaPollPublicity
public let kind: TelegramMediaPollKind
public let text: Text
public let options: [TelegramMediaPollOption]
public let correctAnswers: [Data]?
public let results: TelegramMediaPollResults
public let deadlineTimeout: Int32?
public let usedCustomEmojiFiles: [Int64: TelegramMediaFile]
public init(
publicity: TelegramMediaPollPublicity,
kind: TelegramMediaPollKind,
text: Text,
options: [TelegramMediaPollOption],
correctAnswers: [Data]?,
results: TelegramMediaPollResults,
deadlineTimeout: Int32?,
usedCustomEmojiFiles: [Int64: TelegramMediaFile]
) {
self.publicity = publicity
self.kind = kind
self.text = text
self.options = options
self.correctAnswers = correctAnswers
self.results = results
self.deadlineTimeout = deadlineTimeout
self.usedCustomEmojiFiles = usedCustomEmojiFiles
}
}
final class ComposePollScreenComponent: Component { final class ComposePollScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -1585,7 +1628,7 @@ public class ComposePollScreen: ViewControllerComponentContainer, AttachmentCont
maxPollAnwsersCount = Int(value) maxPollAnwsersCount = Int(value)
} }
return InitialData( return InitialData(
maxPollTextLength: Int(200), maxPollTextLength: 200,
maxPollOptionLength: 100, maxPollOptionLength: 100,
maxPollAnwsersCount: maxPollAnwsersCount maxPollAnwsersCount: maxPollAnwsersCount
) )

File diff suppressed because it is too large Load Diff

View File

@ -1,274 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
class CreatePollOptionActionItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let title: String
let enabled: Bool
let tag: ItemListItemTag?
let sectionId: ItemListSectionId
let action: () -> Void
init(theme: PresentationTheme, title: String, enabled: Bool, tag: ItemListItemTag?, sectionId: ItemListSectionId, action: @escaping () -> Void) {
self.theme = theme
self.title = title
self.enabled = enabled
self.tag = tag
self.sectionId = sectionId
self.action = action
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = CreatePollOptionActionItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(false) })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? CreatePollOptionActionItemNode {
let makeLayout = nodeValue.asyncLayout()
var animated = true
if case .None = animation {
animated = false
}
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply(animated)
})
}
}
}
}
}
var selectable: Bool {
return self.enabled
}
func selected(listView: ListView){
listView.clearHighlightAnimated(true)
self.action()
}
}
private let titleFont = Font.regular(17.0)
class CreatePollOptionActionItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
private let iconNode: ASImageNode
private let titleNode: TextNode
private var item: CreatePollOptionActionItem?
var tag: ItemListItemTag? {
return self.item?.tag
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.iconNode)
self.addSubnode(self.titleNode)
}
func asyncLayout() -> (_ item: CreatePollOptionActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
var updatedIcon: UIImage?
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
updatedIcon = PresentationResourcesItemList.addPhoneIcon(item.theme)
}
let leftInset: CGFloat = 60.0 + params.leftInset
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.enabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let separatorHeight = UIScreenPixel
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let contentSize = CGSize(width: params.width, height: 44.0)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] animated in
if let strongSelf = self {
strongSelf.item = item
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
}
let _ = titleApply()
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
if let updatedIcon = updatedIcon {
strongSelf.iconNode.image = updatedIcon
}
if let image = strongSelf.iconNode.image {
transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0 - 3.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size))
}
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel))
}
})
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@ -1,539 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import CheckNode
struct CreatePollOptionItemEditing {
let editable: Bool
let hasActiveRevealControls: Bool
}
class CreatePollOptionItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let id: Int
let placeholder: String
let value: String
let isSelected: Bool?
let maxLength: Int
let editing: CreatePollOptionItemEditing
let sectionId: ItemListSectionId
let setItemIdWithRevealedOptions: (Int?, Int?) -> Void
let updated: (String, Bool) -> Void
let next: (() -> Void)?
let delete: (Bool) -> Void
let canDelete: Bool
let canMove: Bool
let focused: (Bool) -> Void
let toggleSelected: () -> Void
let tag: ItemListItemTag?
init(presentationData: ItemListPresentationData, id: Int, placeholder: String, value: String, isSelected: Bool?, maxLength: Int, editing: CreatePollOptionItemEditing, sectionId: ItemListSectionId, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, updated: @escaping (String, Bool) -> Void, next: (() -> Void)?, delete: @escaping (Bool) -> Void, canDelete: Bool, canMove: Bool, focused: @escaping (Bool) -> Void, toggleSelected: @escaping () -> Void, tag: ItemListItemTag?) {
self.presentationData = presentationData
self.id = id
self.placeholder = placeholder
self.value = value
self.isSelected = isSelected
self.maxLength = maxLength
self.editing = editing
self.sectionId = sectionId
self.setItemIdWithRevealedOptions = setItemIdWithRevealedOptions
self.updated = updated
self.next = next
self.delete = delete
self.canDelete = canDelete
self.canMove = canMove
self.focused = focused
self.toggleSelected = toggleSelected
self.tag = tag
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = CreatePollOptionItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(.None) })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? CreatePollOptionItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply(animation)
})
}
}
}
}
}
var selectable: Bool = false
}
private let titleFont = Font.regular(15.0)
class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, ItemListItemFocusableNode, ASEditableTextNodeDelegate {
private let containerNode: ASDisplayNode
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private var checkNode: InteractiveCheckNode?
private let textClippingNode: ASDisplayNode
private let textNode: EditableTextNode
private let measureTextNode: TextNode
private let textLimitNode: TextNode
private let reorderControlNode: ItemListEditableReorderControlNode
private var item: CreatePollOptionItem?
private var layoutParams: ListViewItemLayoutParams?
var tag: ItemListItemTag? {
return self.item?.tag
}
override var controlsContainer: ASDisplayNode {
return self.containerNode
}
var checkNodeFrame: CGRect? {
guard let _ = self.layoutParams, let checkNode = self.checkNode else {
return nil
}
return checkNode.frame
}
init() {
self.containerNode = ASDisplayNode()
self.containerNode.clipsToBounds = true
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.reorderControlNode = ItemListEditableReorderControlNode()
self.textClippingNode = ASDisplayNode()
self.textClippingNode.clipsToBounds = true
self.textNode = EditableTextNode()
self.measureTextNode = TextNode()
self.textLimitNode = TextNode()
self.textLimitNode.isUserInteractionEnabled = false
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.containerNode)
self.textClippingNode.addSubnode(self.textNode)
self.containerNode.addSubnode(self.textClippingNode)
self.containerNode.addSubnode(self.reorderControlNode)
self.containerNode.addSubnode(self.textLimitNode)
}
override func didLoad() {
super.didLoad()
var textColor: UIColor = .black
if let item = self.item {
textColor = item.presentationData.theme.list.itemPrimaryTextColor
}
self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: textColor]
self.textNode.clipsToBounds = true
self.textNode.delegate = self
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
}
func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
self.item?.focused(true)
}
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
self.internalEditableTextNodeDidUpdateText(editableTextNode, isLosingFocus: true)
self.item?.focused(false)
}
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
guard let item = self.item else {
return false
}
if text.firstIndex(of: "\n") != nil {
if text != "\n" {
let currentText = editableTextNode.attributedText?.string ?? ""
var updatedText = (currentText as NSString).replacingCharacters(in: range, with: text)
updatedText = updatedText.replacingOccurrences(of: "\n", with: " ")
if updatedText.count == 1 {
updatedText = ""
}
let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor)
self.textNode.attributedText = updatedAttributedText
self.editableTextNodeDidUpdateText(editableTextNode)
}
if let next = item.next {
next()
} else {
editableTextNode.resignFirstResponder()
}
return false
}
return true
}
func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
self.internalEditableTextNodeDidUpdateText(editableTextNode, isLosingFocus: false)
}
private func internalEditableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode, isLosingFocus: Bool) {
if let item = self.item {
let text = self.textNode.attributedText ?? NSAttributedString()
var updatedText = text.string
var hadReturn = false
if updatedText.firstIndex(of: "\n") != nil {
hadReturn = true
updatedText = updatedText.replacingOccurrences(of: "\n", with: " ")
}
let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor)
if text.string != updatedAttributedText.string {
self.textNode.attributedText = updatedAttributedText
}
item.updated(updatedText, !isLosingFocus && editableTextNode.isFirstResponder())
if hadReturn {
if let next = item.next {
next()
} else if !isLosingFocus {
editableTextNode.resignFirstResponder()
}
}
}
}
func editableTextNodeBackspaceWhileEmpty(_ editableTextNode: ASEditableTextNode) {
self.item?.delete(editableTextNode.isFirstResponder())
}
func asyncLayout() -> (_ item: CreatePollOptionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
let makeTextLayout = TextNode.asyncLayout(self.measureTextNode)
let makeTextLimitLayout = TextNode.asyncLayout(self.textLimitNode)
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let reorderSizeAndApply = reorderControlLayout(item.presentationData.theme)
let separatorHeight = UIScreenPixel
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let leftInset: CGFloat = params.leftInset + (item.isSelected != nil ? 60.0 : 16.0)
let rightInset: CGFloat = 44.0 + params.rightInset
let textLength = item.value.count
let displayTextLimit = textLength > item.maxLength * 70 / 100
let remainingCount = item.maxLength - textLength
let (textLimitLayout, textLimitApply) = makeTextLimitLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(remainingCount)", font: Font.regular(13.0), textColor: remainingCount < 0 ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: .greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
var measureText = item.value
if measureText.hasSuffix("\n") || measureText.isEmpty {
measureText += "|"
}
let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), textColor: .black)
let attributedText = NSAttributedString(string: item.value, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor)
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedMeasureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, lineSpacing: 0.05, cutout: nil, insets: UIEdgeInsets()))
let textTopInset: CGFloat = 11.0
let textBottomInset: CGFloat = 11.0
let contentSize = CGSize(width: params.width, height: textLayout.size.height + textTopInset + textBottomInset)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPlaceholderTextColor)
return (layout, { [weak self] animation in
if let strongSelf = self {
let transition: ContainedViewLayoutTransition
switch animation {
case .System:
transition = .animated(duration: 0.3, curve: .spring)
default:
transition = .immediate
}
strongSelf.item = item
strongSelf.layoutParams = params
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
if strongSelf.isNodeLoaded {
strongSelf.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: item.presentationData.theme.list.itemPrimaryTextColor]
strongSelf.textNode.tintColor = item.presentationData.theme.list.itemAccentColor
}
}
let revealOffset = strongSelf.revealOffset
let capitalizationType: UITextAutocapitalizationType
let autocorrectionType: UITextAutocorrectionType
let keyboardType: UIKeyboardType
capitalizationType = .sentences
autocorrectionType = .default
keyboardType = UIKeyboardType.default
let _ = textApply()
if let currentText = strongSelf.textNode.attributedText {
if currentText.string != attributedText.string || updatedTheme != nil {
strongSelf.textNode.attributedText = attributedText
}
} else {
strongSelf.textNode.attributedText = attributedText
}
if strongSelf.textNode.keyboardType != keyboardType {
strongSelf.textNode.keyboardType = keyboardType
}
if strongSelf.textNode.autocapitalizationType != capitalizationType {
strongSelf.textNode.autocapitalizationType = capitalizationType
}
if strongSelf.textNode.autocorrectionType != autocorrectionType {
strongSelf.textNode.autocorrectionType = autocorrectionType
}
let returnKeyType: UIReturnKeyType
if let _ = item.next {
returnKeyType = .next
} else {
returnKeyType = .done
}
if strongSelf.textNode.returnKeyType != returnKeyType {
strongSelf.textNode.returnKeyType = returnKeyType
}
if strongSelf.textNode.attributedPlaceholderText == nil || !strongSelf.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) {
strongSelf.textNode.attributedPlaceholderText = attributedPlaceholderText
}
strongSelf.textNode.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance
let checkSize = CGSize(width: 22.0, height: 22.0)
let checkFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset + 16.0, y: floor((layout.contentSize.height - checkSize.height) / 2.0)), size: checkSize)
if let isSelected = item.isSelected {
if let checkNode = strongSelf.checkNode {
transition.updateFrame(node: checkNode, frame: checkFrame)
checkNode.setSelected(isSelected, animated: true)
} else {
let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: item.presentationData.theme.list.itemSwitchColors.positiveColor, strokeColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, borderColor: item.presentationData.theme.list.itemCheckColors.strokeColor, overlayBorder: false, hasInset: false, hasShadow: false))
checkNode.setSelected(isSelected, animated: false)
checkNode.valueChanged = { [weak self] value in
self?.item?.toggleSelected()
}
strongSelf.checkNode = checkNode
strongSelf.containerNode.addSubnode(checkNode)
checkNode.frame = checkFrame
transition.animatePositionAdditive(node: checkNode, offset: CGPoint(x: -checkFrame.maxX, y: 0.0))
}
if let checkNode = strongSelf.checkNode {
transition.updateAlpha(node: checkNode, alpha: strongSelf.textNode.textView.text.isEmpty && item.placeholder == item.presentationData.strings.CreatePoll_AddOption ? 0.0 : 1.0)
}
} else if let checkNode = strongSelf.checkNode {
strongSelf.checkNode = nil
transition.updateFrame(node: checkNode, frame: checkFrame.offsetBy(dx: -checkFrame.maxX, dy: 0.0), completion: { [weak checkNode] _ in
checkNode?.removeFromSupernode()
})
}
transition.updateFrame(node: strongSelf.textClippingNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - params.rightInset, height: textLayout.size.height)))
transition.updateFrame(node: strongSelf.textNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: params.width - leftInset - rightInset, height: textLayout.size.height + 1.0)))
if strongSelf.backgroundNode.supernode == nil {
strongSelf.containerNode.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.containerNode.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.containerNode.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.containerNode.insertSubnode(strongSelf.maskNode, at: 3)
}
let bottomStripeWasHidden = strongSelf.bottomStripeNode.isHidden
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layout.contentSize.width, height: separatorHeight))
if strongSelf.animationForKey("apparentHeight") == nil {
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
let previousX = strongSelf.bottomStripeNode.frame.minX
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layout.contentSize.width, height: separatorHeight))
if !bottomStripeWasHidden {
transition.animatePositionAdditive(node: strongSelf.bottomStripeNode, offset: CGPoint(x: previousX - strongSelf.bottomStripeNode.frame.minX, y: 0.0))
}
} else {
let previousX = strongSelf.bottomStripeNode.frame.minX
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: strongSelf.bottomStripeNode.frame.minY), size: CGSize(width: layout.contentSize.width, height: separatorHeight))
if !bottomStripeWasHidden {
transition.animatePositionAdditive(node: strongSelf.bottomStripeNode, offset: CGPoint(x: previousX - strongSelf.bottomStripeNode.frame.minX, y: 0.0))
}
}
let _ = reorderSizeAndApply.1(layout.contentSize.height, displayTextLimit, transition)
let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderSizeAndApply.0 + (!item.canMove ? 44.0 + params.rightInset : 0.0), y: 0.0), size: CGSize(width: reorderSizeAndApply.0, height: layout.contentSize.height))
transition.updateFrameAdditive(node: strongSelf.reorderControlNode, frame: reorderControlFrame)
strongSelf.reorderControlNode.isUserInteractionEnabled = item.canMove
let _ = textLimitApply()
strongSelf.textLimitNode.frame = CGRect(origin: CGPoint(x: reorderControlFrame.minX + floor((reorderControlFrame.width - textLimitLayout.size.width) / 2.0) - 4.0 - UIScreenPixel, y: max(floor(reorderControlFrame.midY + 2.0), layout.contentSize.height - 15.0 - textLimitLayout.size.height)), size: textLimitLayout.size)
strongSelf.textLimitNode.isHidden = !displayTextLimit
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
strongSelf.setRevealOptions((left: [], right: item.canDelete ? [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] : []))
}
})
}
}
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
guard let params = self.layoutParams, let item = self.item else {
return
}
let revealOffset = offset
let leftInset: CGFloat
leftInset = params.leftInset + (item.isSelected != nil ? 60.0 : 16.0)
if let checkNode = self.checkNode {
var checkNodeFrame = checkNode.frame
checkNodeFrame.origin.x = params.leftInset + 11.0 + revealOffset
transition.updateFrame(node: checkNode, frame: checkNodeFrame)
}
var reorderFrame = self.reorderControlNode.frame
reorderFrame.origin.x = params.width + revealOffset - params.rightInset - reorderFrame.width
transition.updateFrame(node: self.reorderControlNode, frame: reorderFrame)
var textClippingNodeFrame = self.textClippingNode.frame
textClippingNodeFrame.origin.x = revealOffset + leftInset
transition.updateFrame(node: self.textClippingNode, frame: textClippingNodeFrame)
}
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
self.layer.allowsGroupOpacity = true
self.updateRevealOffsetInternal(offset: -self.bounds.width - 74.0, transition: .animated(duration: 0.2, curve: .spring), completion: { [weak self] in
self?.layer.allowsGroupOpacity = false
})
self.item?.delete(self.textNode.isFirstResponder())
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
//self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
//self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
func focus() {
self.textNode.becomeFirstResponder()
}
func selectAll() {
self.textNode.textView.selectAll(nil)
}
override func isReorderable(at point: CGPoint) -> Bool {
if self.reorderControlNode.frame.contains(point), !self.reorderControlNode.isHidden, !self.isDisplayingRevealedOptions {
return true
}
return false
}
override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
super.animateFrameTransition(progress, currentValue)
var separatorFrame = self.bottomStripeNode.frame
separatorFrame.origin.y = currentValue - UIScreenPixel
self.bottomStripeNode.frame = separatorFrame
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.containerNode.bounds.width, height: currentValue))
let insets = self.insets
let separatorHeight = UIScreenPixel
guard let params = self.layoutParams else {
return
}
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: self.containerNode.bounds.width, height: currentValue + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
self.maskNode.frame = self.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
}
}

View File

@ -1,683 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import TextFormat
import ObjCRuntimeUtils
import TextInputMenu
import AccountContext
public enum CreatePollTextInputItemTextLimitMode {
case characters
case bytes
}
public struct CreatePollTextInputItemTextLimit {
public let value: Int
public let display: Bool
public let mode: CreatePollTextInputItemTextLimitMode
public init(value: Int, display: Bool, mode: CreatePollTextInputItemTextLimitMode = .characters) {
self.value = value
self.display = display
self.mode = mode
}
}
public struct ItemListMultilineInputInlineAction {
public let icon: UIImage
public let action: (() -> Void)?
public init(icon: UIImage, action: (() -> Void)?) {
self.icon = icon
self.action = action
}
}
public class CreatePollTextInputItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let text: NSAttributedString
let placeholder: String
public let sectionId: ItemListSectionId
let style: ItemListStyle
let capitalization: Bool
let autocorrection: Bool
let returnKeyType: UIReturnKeyType
let action: (() -> Void)?
let textUpdated: (NSAttributedString) -> Void
let shouldUpdateText: (String) -> Bool
let processPaste: ((String) -> Void)?
let updatedFocus: ((Bool) -> Void)?
let maxLength: CreatePollTextInputItemTextLimit?
let minimalHeight: CGFloat?
let inlineAction: ItemListMultilineInputInlineAction?
public let tag: ItemListItemTag?
public init(context: AccountContext, presentationData: ItemListPresentationData, text: NSAttributedString, placeholder: String, maxLength: CreatePollTextInputItemTextLimit?, sectionId: ItemListSectionId, style: ItemListStyle, capitalization: Bool = true, autocorrection: Bool = true, returnKeyType: UIReturnKeyType = .default, minimalHeight: CGFloat? = nil, textUpdated: @escaping (NSAttributedString) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> Void)? = nil, updatedFocus: ((Bool) -> Void)? = nil, tag: ItemListItemTag? = nil, action: (() -> Void)? = nil, inlineAction: ItemListMultilineInputInlineAction? = nil) {
self.context = context
self.presentationData = presentationData
self.text = text
self.placeholder = placeholder
self.maxLength = maxLength
self.sectionId = sectionId
self.style = style
self.capitalization = capitalization
self.autocorrection = autocorrection
self.returnKeyType = returnKeyType
self.minimalHeight = minimalHeight
self.textUpdated = textUpdated
self.shouldUpdateText = shouldUpdateText
self.processPaste = processPaste
self.updatedFocus = updatedFocus
self.tag = tag
self.action = action
self.inlineAction = inlineAction
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = CreatePollTextInputItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? CreatePollTextInputItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDelegate, ItemListItemNode, ItemListItemFocusableNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let textClippingNode: ASDisplayNode
private let textNode: EditableTextNode
private let measureTextNode: TextNode
private let limitTextNode: TextNode
private var inlineActionButtonNode: HighlightableButtonNode?
private var item: CreatePollTextInputItem?
private var layoutParams: ListViewItemLayoutParams?
private let inputMenu = TextInputMenu()
public var tag: ItemListItemTag? {
return self.item?.tag
}
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.textClippingNode = ASDisplayNode()
self.textClippingNode.clipsToBounds = true
self.textNode = EditableTextNode()
self.measureTextNode = TextNode()
self.limitTextNode = TextNode()
super.init(layerBacked: false, dynamicBounce: false)
self.textClippingNode.addSubnode(self.textNode)
self.addSubnode(self.textClippingNode)
}
override public func didLoad() {
super.didLoad()
var textColor: UIColor = .black
if let item = self.item {
textColor = item.presentationData.theme.list.itemPrimaryTextColor
self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor]
} else {
self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: textColor]
}
self.textNode.clipsToBounds = true
self.textNode.delegate = self
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
}
public func asyncLayout() -> (_ item: CreatePollTextInputItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTextLayout = TextNode.asyncLayout(self.measureTextNode)
let makeLimitTextLayout = TextNode.asyncLayout(self.limitTextNode)
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let leftInset = 16.0 + params.rightInset
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
}
var limitTextString: NSAttributedString?
var rightInset: CGFloat = params.rightInset
if let maxLength = item.maxLength, maxLength.display {
let textLength: Int
switch maxLength.mode {
case .characters:
textLength = item.text.string.count
case .bytes:
textLength = item.text.string.data(using: .utf8, allowLossyConversion: true)?.count ?? 0
}
let displayTextLimit = textLength > maxLength.value * 70 / 100
let remainingCount = maxLength.value - textLength
if displayTextLimit {
limitTextString = NSAttributedString(string: "\(remainingCount)", font: Font.regular(13.0), textColor: remainingCount < 0 ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemSecondaryTextColor)
}
rightInset += 30.0 + 4.0
}
let (limitTextLayout, limitTextApply) = makeLimitTextLayout(TextNodeLayoutArguments(attributedString: limitTextString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0), alignment: .left, cutout: nil, insets: UIEdgeInsets()))
if limitTextLayout.size.width > 30.0 {
rightInset += 30.0
}
if let inlineAction = item.inlineAction {
rightInset += inlineAction.icon.size.width + 8.0
}
let itemText = textAttributedStringForStateText(context: item.context, stateText: item.text, fontSize: 17.0, textColor: item.presentationData.theme.chat.inputPanel.primaryTextColor, accentTextColor: item.presentationData.theme.chat.inputPanel.panelControlAccentColor, writingDirection: nil, spoilersRevealed: false, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil)
let measureText = NSMutableAttributedString(attributedString: itemText)
let measureRawString = measureText.string
if measureRawString.hasSuffix("\n") || measureRawString.isEmpty {
measureText.append(NSAttributedString(string: "|", font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: .black))
}
let attributedText = itemText
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: measureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 16.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let separatorHeight = UIScreenPixel
let textTopInset: CGFloat = 11.0
let textBottomInset: CGFloat = 11.0
var contentHeight: CGFloat = textLayout.size.height + textTopInset + textBottomInset
if let minimalHeight = item.minimalHeight {
contentHeight = max(minimalHeight, contentHeight)
}
let contentSize = CGSize(width: params.width, height: contentHeight)
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPlaceholderTextColor)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
if strongSelf.isNodeLoaded {
strongSelf.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), NSAttributedString.Key.foregroundColor.rawValue: item.presentationData.theme.list.itemPrimaryTextColor]
strongSelf.textNode.tintColor = item.presentationData.theme.list.itemAccentColor
}
if let inlineAction = item.inlineAction {
strongSelf.inlineActionButtonNode?.setImage(generateTintedImage(image: inlineAction.icon, color: item.presentationData.theme.list.itemAccentColor), for: .normal)
}
strongSelf.inputMenu.updateStrings(item.presentationData.strings)
}
let capitalizationType: UITextAutocapitalizationType = item.capitalization ? .sentences : .none
let autocorrectionType: UITextAutocorrectionType = item.autocorrection ? .default : .no
if strongSelf.textNode.textView.autocapitalizationType != capitalizationType {
strongSelf.textNode.textView.autocapitalizationType = capitalizationType
}
if strongSelf.textNode.textView.autocorrectionType != autocorrectionType {
strongSelf.textNode.textView.autocorrectionType = autocorrectionType
}
if strongSelf.textNode.textView.returnKeyType != item.returnKeyType {
strongSelf.textNode.textView.returnKeyType = item.returnKeyType
}
let _ = textApply()
if let currentText = strongSelf.textNode.attributedText {
if currentText.string != attributedText.string || updatedTheme != nil {
strongSelf.textNode.attributedText = attributedText
refreshGenericTextInputAttributes(context: item.context, textView: strongSelf.textNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil)
}
} else {
strongSelf.textNode.attributedText = attributedText
refreshGenericTextInputAttributes(context: item.context, textView: strongSelf.textNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil)
}
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
if strongSelf.textNode.attributedPlaceholderText == nil || !strongSelf.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) {
strongSelf.textNode.attributedPlaceholderText = attributedPlaceholderText
}
strongSelf.textNode.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance
if strongSelf.animationForKey("apparentHeight") == nil {
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - params.rightInset, height: textLayout.size.height))
}
strongSelf.textNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width - leftInset - 16.0 - rightInset, height: textLayout.size.height + 1.0))
let _ = limitTextApply()
strongSelf.limitTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 16.0 - limitTextLayout.size.width, y: layout.contentSize.height - 15.0 - limitTextLayout.size.height), size: limitTextLayout.size)
if limitTextString != nil {
if strongSelf.limitTextNode.supernode == nil {
strongSelf.addSubnode(strongSelf.limitTextNode)
}
} else if strongSelf.limitTextNode.supernode != nil {
strongSelf.limitTextNode.removeFromSupernode()
}
if let inlineAction = item.inlineAction {
let inlineActionButtonNode: HighlightableButtonNode
if let currentInlineActionButtonNode = strongSelf.inlineActionButtonNode {
inlineActionButtonNode = currentInlineActionButtonNode
} else {
inlineActionButtonNode = HighlightableButtonNode()
inlineActionButtonNode.setImage(generateTintedImage(image: inlineAction.icon, color: item.presentationData.theme.list.itemAccentColor), for: .normal)
inlineActionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.inlineActionPressed), forControlEvents: .touchUpInside)
strongSelf.addSubnode(inlineActionButtonNode)
strongSelf.inlineActionButtonNode = inlineActionButtonNode
}
inlineActionButtonNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - inlineAction.icon.size.width - 11.0, y: 7.0), size: inlineAction.icon.size)
} else if let inlineActionButtonNode = strongSelf.inlineActionButtonNode {
inlineActionButtonNode.removeFromSupernode()
strongSelf.inlineActionButtonNode = nil
}
}
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
override public func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
super.animateFrameTransition(progress, currentValue)
guard let params = self.layoutParams else {
return
}
let separatorHeight = UIScreenPixel
let insets = self.insets
let contentSize = CGSize(width: params.width, height: max(1.0, currentValue - insets.top - insets.bottom))
let leftInset = 16.0 + params.leftInset
let textTopInset: CGFloat = 11.0
let textBottomInset: CGFloat = 11.0
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
self.maskNode.frame = self.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: self.bottomStripeNode.frame.minX, y: contentSize.height), size: CGSize(width: self.bottomStripeNode.frame.size.width, height: separatorHeight))
self.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: max(0.0, params.width - leftInset - params.rightInset), height: max(0.0, contentSize.height - textTopInset - textBottomInset)))
}
public func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
self.item?.updatedFocus?(true)
self.inputMenu.activate()
}
public func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
self.item?.updatedFocus?(false)
self.inputMenu.deactivate()
}
public func editableTextNodeTarget(forAction action: Selector) -> ASEditableTextNodeTargetForAction? {
if action == makeSelectorFromString("_showTextStyleOptions:") {
if #available(iOS 16.0, *) {
return ASEditableTextNodeTargetForAction(target: nil)
} else {
if case .general = self.inputMenu.state {
if self.textNode.attributedText == nil || self.textNode.attributedText!.length == 0 || self.textNode.selectedRange.length == 0 {
return ASEditableTextNodeTargetForAction(target: nil)
}
return ASEditableTextNodeTargetForAction(target: self)
} else {
return ASEditableTextNodeTargetForAction(target: nil)
}
}
} else if action == #selector(self.formatAttributesBold(_:)) || action == #selector(self.formatAttributesItalic(_:)) || action == #selector(self.formatAttributesMonospace(_:)) || action == #selector(self.formatAttributesLink(_:)) || action == #selector(self.formatAttributesStrikethrough(_:)) || action == #selector(self.formatAttributesUnderline(_:)) {
if case .format = self.inputMenu.state {
return ASEditableTextNodeTargetForAction(target: self)
} else {
return ASEditableTextNodeTargetForAction(target: nil)
}
}
if case .format = self.inputMenu.state {
return ASEditableTextNodeTargetForAction(target: nil)
}
return nil
}
@objc func _showTextStyleOptions(_ sender: Any) {
self.inputMenu.format(view: self.textNode.view, rect: self.textNode.selectionRect.offsetBy(dx: 0.0, dy: -self.textNode.textView.contentOffset.y).insetBy(dx: 0.0, dy: -1.0))
}
@available(iOS 16.0, *)
public func editableTextNodeMenu(_ editableTextNode: ASEditableTextNode, forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu {
var actions = suggestedActions
if editableTextNode.attributedText == nil || editableTextNode.attributedText!.length == 0 || editableTextNode.selectedRange.length == 0 {
} else if let strings = self.item?.presentationData.strings {
let children: [UIAction] = [
UIAction(title: strings.TextFormat_Bold, image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesBold(strongSelf)
}
},
UIAction(title: strings.TextFormat_Italic, image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesItalic(strongSelf)
}
},
UIAction(title: strings.TextFormat_Monospace, image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesMonospace(strongSelf)
}
},
UIAction(title: strings.TextFormat_Link, image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesLink(strongSelf)
}
},
UIAction(title: strings.TextFormat_Strikethrough, image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesStrikethrough(strongSelf)
}
},
UIAction(title: strings.TextFormat_Underline, image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesUnderline(strongSelf)
}
},
UIAction(title: strings.TextFormat_Spoiler, image: nil) { [weak self] (action) in
if let strongSelf = self {
strongSelf.formatAttributesSpoiler(strongSelf)
}
}
]
let formatMenu = UIMenu(title: strings.TextFormat_Format, image: nil, children: children)
actions.insert(formatMenu, at: 3)
}
return UIMenu(children: actions)
}
@objc func formatAttributesBold(_ sender: Any) {
self.inputMenu.back()
if let item = self.item {
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.bold, value: nil)
}
}
@objc func formatAttributesItalic(_ sender: Any) {
self.inputMenu.back()
if let item = self.item {
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.italic, value: nil)
}
}
@objc func formatAttributesMonospace(_ sender: Any) {
self.inputMenu.back()
if let item = self.item {
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.monospace, value: nil)
}
}
@objc func formatAttributesLink(_ sender: Any) {
self.inputMenu.back()
//self.interfaceInteraction?.openLinkEditing()
}
@objc func formatAttributesStrikethrough(_ sender: Any) {
self.inputMenu.back()
if let item = self.item {
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.strikethrough, value: nil)
}
}
@objc func formatAttributesUnderline(_ sender: Any) {
self.inputMenu.back()
if let item = self.item {
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.underline, value: nil)
}
}
@objc func formatAttributesSpoiler(_ sender: Any) {
self.inputMenu.back()
if let item = self.item {
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.spoiler, value: nil)
}
}
@objc func formatAttributesQuote(_ sender: Any) {
self.inputMenu.back()
if let item = self.item {
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false))
}
}
@objc func formatAttributesCodeBlock(_ sender: Any) {
self.inputMenu.back()
if let item = self.item {
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil), isCollapsed: false))
}
}
public func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if let item = self.item {
if text.count > 1, let processPaste = item.processPaste {
processPaste(text)
return false
}
if let action = item.action, text == "\n" {
action()
return false
}
let newText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text)
if !item.shouldUpdateText(newText) {
return false
}
}
return true
}
public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
if let item = self.item {
if let _ = self.textNode.attributedText {
refreshGenericTextInputAttributes(context: item.context, textView: editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil)
let updatedText = stateAttributedStringForText(self.textNode.attributedText!)
item.textUpdated(updatedText)
} else {
item.textUpdated(NSAttributedString(string: ""))
}
}
}
public func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool {
if let _ = self.item {
let text: String? = UIPasteboard.general.string
if let _ = text {
return true
}
}
return false
}
public func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) {
/*if !dueToEditing && !self.updatingInputState {
}*/
if let item = self.item {
if case .format = self.inputMenu.state {
self.inputMenu.deactivate()
UIMenuController.shared.update()
}
refreshChatTextInputTypingAttributes(editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0)
refreshGenericTextInputAttributes(context: item.context, textView: editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil)
}
}
public func focus() {
if !self.textNode.textView.isFirstResponder {
self.textNode.textView.becomeFirstResponder()
}
}
public func selectAll() {
self.textNode.textView.selectAll(nil)
}
public func animateError() {
self.textNode.layer.addShakeAnimation()
}
@objc private func inlineActionPressed() {
if let action = self.item?.inlineAction?.action {
action()
}
}
}
private func chatTextInputAddFormattingAttribute(item: CreatePollTextInputItem, textNode: EditableTextNode, theme: PresentationTheme, attribute: NSAttributedString.Key, value: Any?) {
if let currentText = textNode.attributedText, textNode.selectedRange.length > 0 {
let nsRange = NSRange(location: textNode.selectedRange.location, length: textNode.selectedRange.length)
var addAttribute = true
var attributesToRemove: [NSAttributedString.Key] = []
currentText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in
for (key, _) in attributes {
if key == attribute && range == nsRange {
addAttribute = false
attributesToRemove.append(key)
}
}
}
let result = NSMutableAttributedString(attributedString: currentText)
for attribute in attributesToRemove {
result.removeAttribute(attribute, range: nsRange)
}
if addAttribute {
result.addAttribute(attribute, value: true as Bool, range: nsRange)
}
textNode.attributedText = result
textNode.selectedRange = nsRange
refreshChatTextInputTypingAttributes(textNode.textView, theme: theme, baseFontSize: 17.0)
refreshGenericTextInputAttributes(context: item.context, textView: textNode.textView, theme: theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil)
let updatedText = stateAttributedStringForText(textNode.attributedText!)
item.textUpdated(updatedText)
}
}

View File

@ -60,9 +60,12 @@ swift_library(
"//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/LottieComponent",
"//submodules/Components/MultilineTextComponent", "//submodules/Components/MultilineTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/BalancedTextComponent", "//submodules/Components/BalancedTextComponent",
"//submodules/Components/ComponentDisplayAdapters", "//submodules/Components/ComponentDisplayAdapters",
"//submodules/ComponentFlow", "//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/ToastComponent",
"//submodules/SemanticStatusNode",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -33,6 +33,9 @@ import RasterizedCompositionComponent
import BadgeComponent import BadgeComponent
import ComponentFlow import ComponentFlow
import ComponentDisplayAdapters import ComponentDisplayAdapters
import ToastComponent
import MultilineTextComponent
import BundleIconComponent
public enum UniversalVideoGalleryItemContentInfo { public enum UniversalVideoGalleryItemContentInfo {
case message(Message, Int?) case message(Message, Int?)
@ -1390,7 +1393,93 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
if dismiss { if dismiss {
self.dismiss() self.dismiss()
} }
if self.adDisposable == nil, let contentInfo = self.item?.contentInfo, case let .message(message, _) = contentInfo {
let adContext = self.context.engine.messages.adMessages(peerId: message.id.peerId, messageId: message.id)
self.adContext = adContext
self.adDisposable = (adContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let self else {
return
} }
if let message = state.messages.first {
Queue.mainQueue().after(2.0, {
self.adMessage = message
if let validLayout = self.validLayout {
self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .immediate)
}
})
}
})
}
if let adMessage = self.adMessage {
let sideInset: CGFloat = 16.0
let title = adMessage.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
let adSize = self.adView.update(
transition: .immediate,
component: AnyComponent(
ToastContentComponent(
icon: AnyComponent(
BundleIconComponent(name: "Components/AdMock", tintColor: nil, maxSize: CGSize(width: 30.0, height: 30.0))
),
content: AnyComponent(
HStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(
VStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(
HStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white))))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(Image(image: PresentationResourcesChatList.searchAdIcon(presentationData.theme, strings: presentationData.strings), size: CGSize(width: 31.0, height: 15.0))))
], spacing: 5.0)
)),
AnyComponentWithIdentity(id: 1, component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: adMessage.text, font: Font.regular(14.0), textColor: .white)))
))
], alignment: .left, spacing: 3.0, fillWidth: false)
)),
AnyComponentWithIdentity(id: 1, component: AnyComponent(
AdRemainingProgressComponent(action: { [weak self] in
guard let self else {
return
}
self.adMessage = nil
if let validLayout = self.validLayout {
self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .immediate)
}
})
))
], spacing: 16.0, alignment: .alternatingLeftRight)
), action: { [weak self] in
if let self, let item = self.item, let ad = adMessage.adAttribute {
item.performAction(.url(url: ad.url, concealed: false))
}
}
)
),
environment: {},
containerSize: CGSize(width: layout.size.width - sideInset * 2.0, height: 70.0)
)
if let adView = self.adView.view {
if adView.superview == nil {
self.view.addSubview(adView)
adView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
adView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
adView.frame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - adSize.height - 145.0), size: adSize)
}
} else if let adView = self.adView.view {
adView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
adView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
}
}
private var adView = ComponentView<Empty>()
private var adContext: AdMessagesHistoryContext?
private var adDisposable: Disposable?
private var adMessage: Message?
func setupItem(_ item: UniversalVideoGalleryItem) { func setupItem(_ item: UniversalVideoGalleryItem) {
if self.item?.content.id != item.content.id { if self.item?.content.id != item.content.id {

View File

@ -1118,6 +1118,26 @@ private final class DemoSheetContent: CombinedComponent {
) )
) )
//TODO:localize
availableItems[.todo] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.todo,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .top,
videoFile: configuration.videos["todo"],
decoration: .badgeStars
)),
title: "To-Do Lists",
text: "Plan, assign and complete tasks seamlessly and efficiently.",
textColor: textColor
)
)
)
)
let index: Int = 0 let index: Int = 0
var items: [DemoPagerComponent.Item] = [] var items: [DemoPagerComponent.Item] = []
if let item = availableItems.first(where: { $0.value.content.id == component.subject as AnyHashable }) { if let item = availableItems.first(where: { $0.value.content.id == component.subject as AnyHashable }) {
@ -1216,6 +1236,9 @@ private final class DemoSheetContent: CombinedComponent {
text = strings.Premium_MessageEffectsInfo text = strings.Premium_MessageEffectsInfo
case .paidMessages: case .paidMessages:
text = strings.Premium_PaidMessagesInfo text = strings.Premium_PaidMessagesInfo
case .todo:
//TODO:localize
text = "Plan, assign and complete tasks seamlessly and efficiently."
default: default:
text = "" text = ""
} }
@ -1302,6 +1325,8 @@ private final class DemoSheetContent: CombinedComponent {
buttonAnimationName = "premium_unlock" buttonAnimationName = "premium_unlock"
case .paidMessages: case .paidMessages:
buttonText = strings.Premium_PaidMessages_Proceed buttonText = strings.Premium_PaidMessages_Proceed
case .todo:
buttonText = strings.Premium_PaidMessages_Proceed
default: default:
buttonText = strings.Common_OK buttonText = strings.Common_OK
} }
@ -1492,6 +1517,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
case folderTags case folderTags
case messageEffects case messageEffects
case paidMessages case paidMessages
case todo
case businessLocation case businessLocation
case businessHours case businessHours
@ -1552,6 +1578,8 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
return .messageEffects return .messageEffects
case .paidMessages: case .paidMessages:
return .paidMessages return .paidMessages
case .todo:
return .todo
case .businessLocation: case .businessLocation:
return .businessLocation return .businessLocation
case .businessHours: case .businessHours:

View File

@ -309,6 +309,12 @@ public enum PremiumSource: Equatable {
} else { } else {
return false return false
} }
case .todo:
if case .todo = rhs {
return true
} else {
return false
}
case let .auth(lhsPrice): case let .auth(lhsPrice):
if case let .auth(rhsPrice) = rhs, lhsPrice == rhsPrice { if case let .auth(rhsPrice) = rhs, lhsPrice == rhsPrice {
return true return true
@ -363,6 +369,7 @@ public enum PremiumSource: Equatable {
case folderTags case folderTags
case messageEffects case messageEffects
case paidMessages case paidMessages
case todo
case auth(String) case auth(String)
var identifier: String? { var identifier: String? {
@ -459,6 +466,8 @@ public enum PremiumSource: Equatable {
return "effects" return "effects"
case .paidMessages: case .paidMessages:
return "paid_messages" return "paid_messages"
case .todo:
return "todo"
case .auth: case .auth:
return "auth" return "auth"
} }
@ -490,6 +499,7 @@ public enum PremiumPerk: CaseIterable {
case folderTags case folderTags
case messageEffects case messageEffects
case paidMessages case paidMessages
case todo
case businessLocation case businessLocation
case businessHours case businessHours
@ -601,6 +611,8 @@ public enum PremiumPerk: CaseIterable {
return "effects" return "effects"
case .paidMessages: case .paidMessages:
return "paid_messages" return "paid_messages"
case .todo:
return "todo"
case .business: case .business:
return "business" return "business"
case .businessLocation: case .businessLocation:
@ -672,6 +684,9 @@ public enum PremiumPerk: CaseIterable {
return strings.Premium_MessageEffects return strings.Premium_MessageEffects
case .paidMessages: case .paidMessages:
return strings.Premium_PaidMessages return strings.Premium_PaidMessages
case .todo:
//TODO:localize
return "To-Do Lists"
case .businessLocation: case .businessLocation:
return strings.Business_Location return strings.Business_Location
case .businessHours: case .businessHours:
@ -741,6 +756,9 @@ public enum PremiumPerk: CaseIterable {
return strings.Premium_MessageEffectsInfo return strings.Premium_MessageEffectsInfo
case .paidMessages: case .paidMessages:
return strings.Premium_PaidMessagesInfo return strings.Premium_PaidMessagesInfo
case .todo:
//TODO:localize
return "Plan, assign and complete tasks seamlessly and efficiently."
case .businessLocation: case .businessLocation:
return strings.Business_LocationInfo return strings.Business_LocationInfo
case .businessHours: case .businessHours:
@ -810,6 +828,8 @@ public enum PremiumPerk: CaseIterable {
return "Premium/Perk/MessageEffects" return "Premium/Perk/MessageEffects"
case .paidMessages: case .paidMessages:
return "Premium/Perk/PaidMessages" return "Premium/Perk/PaidMessages"
case .todo:
return "Premium/Perk/PaidMessages"
case .businessLocation: case .businessLocation:
return "Premium/BusinessPerk/Location" return "Premium/BusinessPerk/Location"
case .businessHours: case .businessHours:

View File

@ -410,6 +410,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-440664550] = { return Api.InputMedia.parse_inputMediaPhotoExternal($0) } dict[-440664550] = { return Api.InputMedia.parse_inputMediaPhotoExternal($0) }
dict[261416433] = { return Api.InputMedia.parse_inputMediaPoll($0) } dict[261416433] = { return Api.InputMedia.parse_inputMediaPoll($0) }
dict[-1979852936] = { return Api.InputMedia.parse_inputMediaStory($0) } dict[-1979852936] = { return Api.InputMedia.parse_inputMediaStory($0) }
dict[-1614454818] = { return Api.InputMedia.parse_inputMediaTodo($0) }
dict[58495792] = { return Api.InputMedia.parse_inputMediaUploadedDocument($0) } dict[58495792] = { return Api.InputMedia.parse_inputMediaUploadedDocument($0) }
dict[505969924] = { return Api.InputMedia.parse_inputMediaUploadedPhoto($0) } dict[505969924] = { return Api.InputMedia.parse_inputMediaUploadedPhoto($0) }
dict[-1052959727] = { return Api.InputMedia.parse_inputMediaVenue($0) } dict[-1052959727] = { return Api.InputMedia.parse_inputMediaVenue($0) }
@ -558,7 +559,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-808853502] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) } dict[-808853502] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) }
dict[-356721331] = { return Api.Message.parse_message($0) } dict[-356721331] = { return Api.Message.parse_message($0) }
dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) } dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) }
dict[-741178048] = { return Api.Message.parse_messageService($0) } dict[2055212554] = { return Api.Message.parse_messageService($0) }
dict[-872240531] = { return Api.MessageAction.parse_messageActionBoostApply($0) } dict[-872240531] = { return Api.MessageAction.parse_messageActionBoostApply($0) }
dict[-988359047] = { return Api.MessageAction.parse_messageActionBotAllowed($0) } dict[-988359047] = { return Api.MessageAction.parse_messageActionBotAllowed($0) }
dict[-1781355374] = { return Api.MessageAction.parse_messageActionChannelCreate($0) } dict[-1781355374] = { return Api.MessageAction.parse_messageActionChannelCreate($0) }
@ -606,6 +607,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1192749220] = { return Api.MessageAction.parse_messageActionStarGift($0) } dict[1192749220] = { return Api.MessageAction.parse_messageActionStarGift($0) }
dict[775611918] = { return Api.MessageAction.parse_messageActionStarGiftUnique($0) } dict[775611918] = { return Api.MessageAction.parse_messageActionStarGiftUnique($0) }
dict[1474192222] = { return Api.MessageAction.parse_messageActionSuggestProfilePhoto($0) } dict[1474192222] = { return Api.MessageAction.parse_messageActionSuggestProfilePhoto($0) }
dict[-940721021] = { return Api.MessageAction.parse_messageActionTodoAppendTasks($0) }
dict[-864265079] = { return Api.MessageAction.parse_messageActionTodoCompletions($0) }
dict[228168278] = { return Api.MessageAction.parse_messageActionTopicCreate($0) } dict[228168278] = { return Api.MessageAction.parse_messageActionTopicCreate($0) }
dict[-1064024032] = { return Api.MessageAction.parse_messageActionTopicEdit($0) } dict[-1064024032] = { return Api.MessageAction.parse_messageActionTopicEdit($0) }
dict[-1262252875] = { return Api.MessageAction.parse_messageActionWebViewDataSent($0) } dict[-1262252875] = { return Api.MessageAction.parse_messageActionWebViewDataSent($0) }
@ -648,6 +651,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1766936791] = { return Api.MessageMedia.parse_messageMediaPhoto($0) } dict[1766936791] = { return Api.MessageMedia.parse_messageMediaPhoto($0) }
dict[1272375192] = { return Api.MessageMedia.parse_messageMediaPoll($0) } dict[1272375192] = { return Api.MessageMedia.parse_messageMediaPoll($0) }
dict[1758159491] = { return Api.MessageMedia.parse_messageMediaStory($0) } dict[1758159491] = { return Api.MessageMedia.parse_messageMediaStory($0) }
dict[-1974226924] = { return Api.MessageMedia.parse_messageMediaToDo($0) }
dict[-1618676578] = { return Api.MessageMedia.parse_messageMediaUnsupported($0) } dict[-1618676578] = { return Api.MessageMedia.parse_messageMediaUnsupported($0) }
dict[784356159] = { return Api.MessageMedia.parse_messageMediaVenue($0) } dict[784356159] = { return Api.MessageMedia.parse_messageMediaVenue($0) }
dict[-571405253] = { return Api.MessageMedia.parse_messageMediaWebPage($0) } dict[-571405253] = { return Api.MessageMedia.parse_messageMediaWebPage($0) }
@ -935,7 +939,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-651419003] = { return Api.SendMessageAction.parse_speakingInGroupCallAction($0) } dict[-651419003] = { return Api.SendMessageAction.parse_speakingInGroupCallAction($0) }
dict[-1239335713] = { return Api.ShippingOption.parse_shippingOption($0) } dict[-1239335713] = { return Api.ShippingOption.parse_shippingOption($0) }
dict[-425595208] = { return Api.SmsJob.parse_smsJob($0) } dict[-425595208] = { return Api.SmsJob.parse_smsJob($0) }
dict[1301522832] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } dict[2109703795] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) }
dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) } dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) }
dict[-963180333] = { return Api.SponsoredPeer.parse_sponsoredPeer($0) } dict[-963180333] = { return Api.SponsoredPeer.parse_sponsoredPeer($0) }
dict[-970274264] = { return Api.StarGift.parse_starGift($0) } dict[-970274264] = { return Api.StarGift.parse_starGift($0) }
@ -999,6 +1003,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1609668650] = { return Api.Theme.parse_theme($0) } dict[-1609668650] = { return Api.Theme.parse_theme($0) }
dict[-94849324] = { return Api.ThemeSettings.parse_themeSettings($0) } dict[-94849324] = { return Api.ThemeSettings.parse_themeSettings($0) }
dict[-7173643] = { return Api.Timezone.parse_timezone($0) } dict[-7173643] = { return Api.Timezone.parse_timezone($0) }
dict[1287725239] = { return Api.TodoCompletion.parse_todoCompletion($0) }
dict[-878074577] = { return Api.TodoItem.parse_todoItem($0) }
dict[1236871718] = { return Api.TodoList.parse_todoList($0) }
dict[-305282981] = { return Api.TopPeer.parse_topPeer($0) } dict[-305282981] = { return Api.TopPeer.parse_topPeer($0) }
dict[-39945236] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsApp($0) } dict[-39945236] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsApp($0) }
dict[344356834] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsInline($0) } dict[344356834] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsInline($0) }
@ -1391,7 +1398,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1404185519] = { return Api.messages.SearchResultsPositions.parse_searchResultsPositions($0) } dict[1404185519] = { return Api.messages.SearchResultsPositions.parse_searchResultsPositions($0) }
dict[-1802240206] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedFile($0) } dict[-1802240206] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedFile($0) }
dict[1443858741] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedMessage($0) } dict[1443858741] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedMessage($0) }
dict[-907141753] = { return Api.messages.SponsoredMessages.parse_sponsoredMessages($0) } dict[-2464403] = { return Api.messages.SponsoredMessages.parse_sponsoredMessages($0) }
dict[406407439] = { return Api.messages.SponsoredMessages.parse_sponsoredMessagesEmpty($0) } dict[406407439] = { return Api.messages.SponsoredMessages.parse_sponsoredMessagesEmpty($0) }
dict[1846886166] = { return Api.messages.StickerSet.parse_stickerSet($0) } dict[1846886166] = { return Api.messages.StickerSet.parse_stickerSet($0) }
dict[-738646805] = { return Api.messages.StickerSet.parse_stickerSetNotModified($0) } dict[-738646805] = { return Api.messages.StickerSet.parse_stickerSetNotModified($0) }
@ -2203,6 +2210,12 @@ public extension Api {
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.Timezone: case let _1 as Api.Timezone:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.TodoCompletion:
_1.serialize(buffer, boxed)
case let _1 as Api.TodoItem:
_1.serialize(buffer, boxed)
case let _1 as Api.TodoList:
_1.serialize(buffer, boxed)
case let _1 as Api.TopPeer: case let _1 as Api.TopPeer:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.TopPeerCategory: case let _1 as Api.TopPeerCategory:

View File

@ -572,6 +572,7 @@ public extension Api {
case inputMediaPhotoExternal(flags: Int32, url: String, ttlSeconds: Int32?) case inputMediaPhotoExternal(flags: Int32, url: String, ttlSeconds: Int32?)
case inputMediaPoll(flags: Int32, poll: Api.Poll, correctAnswers: [Buffer]?, solution: String?, solutionEntities: [Api.MessageEntity]?) case inputMediaPoll(flags: Int32, poll: Api.Poll, correctAnswers: [Buffer]?, solution: String?, solutionEntities: [Api.MessageEntity]?)
case inputMediaStory(peer: Api.InputPeer, id: Int32) case inputMediaStory(peer: Api.InputPeer, id: Int32)
case inputMediaTodo(todo: Api.TodoList)
case inputMediaUploadedDocument(flags: Int32, file: Api.InputFile, thumb: Api.InputFile?, mimeType: String, attributes: [Api.DocumentAttribute], stickers: [Api.InputDocument]?, videoCover: Api.InputPhoto?, videoTimestamp: Int32?, ttlSeconds: Int32?) case inputMediaUploadedDocument(flags: Int32, file: Api.InputFile, thumb: Api.InputFile?, mimeType: String, attributes: [Api.DocumentAttribute], stickers: [Api.InputDocument]?, videoCover: Api.InputPhoto?, videoTimestamp: Int32?, ttlSeconds: Int32?)
case inputMediaUploadedPhoto(flags: Int32, file: Api.InputFile, stickers: [Api.InputDocument]?, ttlSeconds: Int32?) case inputMediaUploadedPhoto(flags: Int32, file: Api.InputFile, stickers: [Api.InputDocument]?, ttlSeconds: Int32?)
case inputMediaVenue(geoPoint: Api.InputGeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String) case inputMediaVenue(geoPoint: Api.InputGeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String)
@ -712,6 +713,12 @@ public extension Api {
peer.serialize(buffer, true) peer.serialize(buffer, true)
serializeInt32(id, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false)
break break
case .inputMediaTodo(let todo):
if boxed {
buffer.appendInt32(-1614454818)
}
todo.serialize(buffer, true)
break
case .inputMediaUploadedDocument(let flags, let file, let thumb, let mimeType, let attributes, let stickers, let videoCover, let videoTimestamp, let ttlSeconds): case .inputMediaUploadedDocument(let flags, let file, let thumb, let mimeType, let attributes, let stickers, let videoCover, let videoTimestamp, let ttlSeconds):
if boxed { if boxed {
buffer.appendInt32(58495792) buffer.appendInt32(58495792)
@ -798,6 +805,8 @@ public extension Api {
return ("inputMediaPoll", [("flags", flags as Any), ("poll", poll as Any), ("correctAnswers", correctAnswers as Any), ("solution", solution as Any), ("solutionEntities", solutionEntities as Any)]) return ("inputMediaPoll", [("flags", flags as Any), ("poll", poll as Any), ("correctAnswers", correctAnswers as Any), ("solution", solution as Any), ("solutionEntities", solutionEntities as Any)])
case .inputMediaStory(let peer, let id): case .inputMediaStory(let peer, let id):
return ("inputMediaStory", [("peer", peer as Any), ("id", id as Any)]) return ("inputMediaStory", [("peer", peer as Any), ("id", id as Any)])
case .inputMediaTodo(let todo):
return ("inputMediaTodo", [("todo", todo as Any)])
case .inputMediaUploadedDocument(let flags, let file, let thumb, let mimeType, let attributes, let stickers, let videoCover, let videoTimestamp, let ttlSeconds): case .inputMediaUploadedDocument(let flags, let file, let thumb, let mimeType, let attributes, let stickers, let videoCover, let videoTimestamp, let ttlSeconds):
return ("inputMediaUploadedDocument", [("flags", flags as Any), ("file", file as Any), ("thumb", thumb as Any), ("mimeType", mimeType as Any), ("attributes", attributes as Any), ("stickers", stickers as Any), ("videoCover", videoCover as Any), ("videoTimestamp", videoTimestamp as Any), ("ttlSeconds", ttlSeconds as Any)]) return ("inputMediaUploadedDocument", [("flags", flags as Any), ("file", file as Any), ("thumb", thumb as Any), ("mimeType", mimeType as Any), ("attributes", attributes as Any), ("stickers", stickers as Any), ("videoCover", videoCover as Any), ("videoTimestamp", videoTimestamp as Any), ("ttlSeconds", ttlSeconds as Any)])
case .inputMediaUploadedPhoto(let flags, let file, let stickers, let ttlSeconds): case .inputMediaUploadedPhoto(let flags, let file, let stickers, let ttlSeconds):
@ -1098,6 +1107,19 @@ public extension Api {
return nil return nil
} }
} }
public static func parse_inputMediaTodo(_ reader: BufferReader) -> InputMedia? {
var _1: Api.TodoList?
if let signature = reader.readInt32() {
_1 = Api.parse(reader, signature: signature) as? Api.TodoList
}
let _c1 = _1 != nil
if _c1 {
return Api.InputMedia.inputMediaTodo(todo: _1!)
}
else {
return nil
}
}
public static func parse_inputMediaUploadedDocument(_ reader: BufferReader) -> InputMedia? { public static func parse_inputMediaUploadedDocument(_ reader: BufferReader) -> InputMedia? {
var _1: Int32? var _1: Int32?
_1 = reader.readInt32() _1 = reader.readInt32()

View File

@ -62,7 +62,7 @@ public extension Api {
indirect enum Message: TypeConstructorDescription { indirect enum Message: TypeConstructorDescription {
case message(flags: Int32, flags2: Int32, id: Int32, fromId: Api.Peer?, fromBoostsApplied: Int32?, peerId: Api.Peer, savedPeerId: Api.Peer?, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, viaBusinessBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?, quickReplyShortcutId: Int32?, effect: Int64?, factcheck: Api.FactCheck?, reportDeliveryUntilDate: Int32?, paidMessageStars: Int64?) case message(flags: Int32, flags2: Int32, id: Int32, fromId: Api.Peer?, fromBoostsApplied: Int32?, peerId: Api.Peer, savedPeerId: Api.Peer?, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, viaBusinessBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?, quickReplyShortcutId: Int32?, effect: Int64?, factcheck: Api.FactCheck?, reportDeliveryUntilDate: Int32?, paidMessageStars: Int64?)
case messageEmpty(flags: Int32, id: Int32, peerId: Api.Peer?) case messageEmpty(flags: Int32, id: Int32, peerId: Api.Peer?)
case messageService(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, replyTo: Api.MessageReplyHeader?, date: Int32, action: Api.MessageAction, reactions: Api.MessageReactions?, ttlPeriod: Int32?) case messageService(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, savedPeerId: Api.Peer?, replyTo: Api.MessageReplyHeader?, date: Int32, action: Api.MessageAction, reactions: Api.MessageReactions?, ttlPeriod: Int32?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
@ -117,14 +117,15 @@ public extension Api {
serializeInt32(id, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {peerId!.serialize(buffer, true)} if Int(flags) & Int(1 << 0) != 0 {peerId!.serialize(buffer, true)}
break break
case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let reactions, let ttlPeriod): case .messageService(let flags, let id, let fromId, let peerId, let savedPeerId, let replyTo, let date, let action, let reactions, let ttlPeriod):
if boxed { if boxed {
buffer.appendInt32(-741178048) buffer.appendInt32(2055212554)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(id, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 8) != 0 {fromId!.serialize(buffer, true)} if Int(flags) & Int(1 << 8) != 0 {fromId!.serialize(buffer, true)}
peerId.serialize(buffer, true) peerId.serialize(buffer, true)
if Int(flags) & Int(1 << 28) != 0 {savedPeerId!.serialize(buffer, true)}
if Int(flags) & Int(1 << 3) != 0 {replyTo!.serialize(buffer, true)} if Int(flags) & Int(1 << 3) != 0 {replyTo!.serialize(buffer, true)}
serializeInt32(date, buffer: buffer, boxed: false) serializeInt32(date, buffer: buffer, boxed: false)
action.serialize(buffer, true) action.serialize(buffer, true)
@ -140,8 +141,8 @@ public extension Api {
return ("message", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("fromId", fromId as Any), ("fromBoostsApplied", fromBoostsApplied as Any), ("peerId", peerId as Any), ("savedPeerId", savedPeerId as Any), ("fwdFrom", fwdFrom as Any), ("viaBotId", viaBotId as Any), ("viaBusinessBotId", viaBusinessBotId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("message", message as Any), ("media", media as Any), ("replyMarkup", replyMarkup as Any), ("entities", entities as Any), ("views", views as Any), ("forwards", forwards as Any), ("replies", replies as Any), ("editDate", editDate as Any), ("postAuthor", postAuthor as Any), ("groupedId", groupedId as Any), ("reactions", reactions as Any), ("restrictionReason", restrictionReason as Any), ("ttlPeriod", ttlPeriod as Any), ("quickReplyShortcutId", quickReplyShortcutId as Any), ("effect", effect as Any), ("factcheck", factcheck as Any), ("reportDeliveryUntilDate", reportDeliveryUntilDate as Any), ("paidMessageStars", paidMessageStars as Any)]) return ("message", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("fromId", fromId as Any), ("fromBoostsApplied", fromBoostsApplied as Any), ("peerId", peerId as Any), ("savedPeerId", savedPeerId as Any), ("fwdFrom", fwdFrom as Any), ("viaBotId", viaBotId as Any), ("viaBusinessBotId", viaBusinessBotId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("message", message as Any), ("media", media as Any), ("replyMarkup", replyMarkup as Any), ("entities", entities as Any), ("views", views as Any), ("forwards", forwards as Any), ("replies", replies as Any), ("editDate", editDate as Any), ("postAuthor", postAuthor as Any), ("groupedId", groupedId as Any), ("reactions", reactions as Any), ("restrictionReason", restrictionReason as Any), ("ttlPeriod", ttlPeriod as Any), ("quickReplyShortcutId", quickReplyShortcutId as Any), ("effect", effect as Any), ("factcheck", factcheck as Any), ("reportDeliveryUntilDate", reportDeliveryUntilDate as Any), ("paidMessageStars", paidMessageStars as Any)])
case .messageEmpty(let flags, let id, let peerId): case .messageEmpty(let flags, let id, let peerId):
return ("messageEmpty", [("flags", flags as Any), ("id", id as Any), ("peerId", peerId as Any)]) return ("messageEmpty", [("flags", flags as Any), ("id", id as Any), ("peerId", peerId as Any)])
case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let reactions, let ttlPeriod): case .messageService(let flags, let id, let fromId, let peerId, let savedPeerId, let replyTo, let date, let action, let reactions, let ttlPeriod):
return ("messageService", [("flags", flags as Any), ("id", id as Any), ("fromId", fromId as Any), ("peerId", peerId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("action", action as Any), ("reactions", reactions as Any), ("ttlPeriod", ttlPeriod as Any)]) return ("messageService", [("flags", flags as Any), ("id", id as Any), ("fromId", fromId as Any), ("peerId", peerId as Any), ("savedPeerId", savedPeerId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("action", action as Any), ("reactions", reactions as Any), ("ttlPeriod", ttlPeriod as Any)])
} }
} }
@ -299,33 +300,38 @@ public extension Api {
if let signature = reader.readInt32() { if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.Peer _4 = Api.parse(reader, signature: signature) as? Api.Peer
} }
var _5: Api.MessageReplyHeader? var _5: Api.Peer?
if Int(_1!) & Int(1 << 28) != 0 {if let signature = reader.readInt32() {
_5 = Api.parse(reader, signature: signature) as? Api.Peer
} }
var _6: Api.MessageReplyHeader?
if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() {
_5 = Api.parse(reader, signature: signature) as? Api.MessageReplyHeader _6 = Api.parse(reader, signature: signature) as? Api.MessageReplyHeader
} } } }
var _6: Int32? var _7: Int32?
_6 = reader.readInt32() _7 = reader.readInt32()
var _7: Api.MessageAction? var _8: Api.MessageAction?
if let signature = reader.readInt32() { if let signature = reader.readInt32() {
_7 = Api.parse(reader, signature: signature) as? Api.MessageAction _8 = Api.parse(reader, signature: signature) as? Api.MessageAction
} }
var _8: Api.MessageReactions? var _9: Api.MessageReactions?
if Int(_1!) & Int(1 << 20) != 0 {if let signature = reader.readInt32() { if Int(_1!) & Int(1 << 20) != 0 {if let signature = reader.readInt32() {
_8 = Api.parse(reader, signature: signature) as? Api.MessageReactions _9 = Api.parse(reader, signature: signature) as? Api.MessageReactions
} } } }
var _9: Int32? var _10: Int32?
if Int(_1!) & Int(1 << 25) != 0 {_9 = reader.readInt32() } if Int(_1!) & Int(1 << 25) != 0 {_10 = reader.readInt32() }
let _c1 = _1 != nil let _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 8) == 0) || _3 != nil let _c3 = (Int(_1!) & Int(1 << 8) == 0) || _3 != nil
let _c4 = _4 != nil let _c4 = _4 != nil
let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil let _c5 = (Int(_1!) & Int(1 << 28) == 0) || _5 != nil
let _c6 = _6 != nil let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil
let _c7 = _7 != nil let _c7 = _7 != nil
let _c8 = (Int(_1!) & Int(1 << 20) == 0) || _8 != nil let _c8 = _8 != nil
let _c9 = (Int(_1!) & Int(1 << 25) == 0) || _9 != nil let _c9 = (Int(_1!) & Int(1 << 20) == 0) || _9 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { let _c10 = (Int(_1!) & Int(1 << 25) == 0) || _10 != nil
return Api.Message.messageService(flags: _1!, id: _2!, fromId: _3, peerId: _4!, replyTo: _5, date: _6!, action: _7!, reactions: _8, ttlPeriod: _9) if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 {
return Api.Message.messageService(flags: _1!, id: _2!, fromId: _3, peerId: _4!, savedPeerId: _5, replyTo: _6, date: _7!, action: _8!, reactions: _9, ttlPeriod: _10)
} }
else { else {
return nil return nil
@ -383,6 +389,8 @@ public extension Api {
case messageActionStarGift(flags: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, convertStars: Int64?, upgradeMsgId: Int32?, upgradeStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?) case messageActionStarGift(flags: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, convertStars: Int64?, upgradeMsgId: Int32?, upgradeStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?)
case messageActionStarGiftUnique(flags: Int32, gift: Api.StarGift, canExportAt: Int32?, transferStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, resaleStars: Int64?, canTransferAt: Int32?, canResellAt: Int32?) case messageActionStarGiftUnique(flags: Int32, gift: Api.StarGift, canExportAt: Int32?, transferStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, resaleStars: Int64?, canTransferAt: Int32?, canResellAt: Int32?)
case messageActionSuggestProfilePhoto(photo: Api.Photo) case messageActionSuggestProfilePhoto(photo: Api.Photo)
case messageActionTodoAppendTasks(list: [Api.TodoItem])
case messageActionTodoCompletions(completed: [Int32], incompleted: [Int32])
case messageActionTopicCreate(flags: Int32, title: String, iconColor: Int32, iconEmojiId: Int64?) case messageActionTopicCreate(flags: Int32, title: String, iconColor: Int32, iconEmojiId: Int64?)
case messageActionTopicEdit(flags: Int32, title: String?, iconEmojiId: Int64?, closed: Api.Bool?, hidden: Api.Bool?) case messageActionTopicEdit(flags: Int32, title: String?, iconEmojiId: Int64?, closed: Api.Bool?, hidden: Api.Bool?)
case messageActionWebViewDataSent(text: String) case messageActionWebViewDataSent(text: String)
@ -789,6 +797,31 @@ public extension Api {
} }
photo.serialize(buffer, true) photo.serialize(buffer, true)
break break
case .messageActionTodoAppendTasks(let list):
if boxed {
buffer.appendInt32(-940721021)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(list.count))
for item in list {
item.serialize(buffer, true)
}
break
case .messageActionTodoCompletions(let completed, let incompleted):
if boxed {
buffer.appendInt32(-864265079)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(completed.count))
for item in completed {
serializeInt32(item, buffer: buffer, boxed: false)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(incompleted.count))
for item in incompleted {
serializeInt32(item, buffer: buffer, boxed: false)
}
break
case .messageActionTopicCreate(let flags, let title, let iconColor, let iconEmojiId): case .messageActionTopicCreate(let flags, let title, let iconColor, let iconEmojiId):
if boxed { if boxed {
buffer.appendInt32(228168278) buffer.appendInt32(228168278)
@ -920,6 +953,10 @@ public extension Api {
return ("messageActionStarGiftUnique", [("flags", flags as Any), ("gift", gift as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any), ("resaleStars", resaleStars as Any), ("canTransferAt", canTransferAt as Any), ("canResellAt", canResellAt as Any)]) return ("messageActionStarGiftUnique", [("flags", flags as Any), ("gift", gift as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any), ("resaleStars", resaleStars as Any), ("canTransferAt", canTransferAt as Any), ("canResellAt", canResellAt as Any)])
case .messageActionSuggestProfilePhoto(let photo): case .messageActionSuggestProfilePhoto(let photo):
return ("messageActionSuggestProfilePhoto", [("photo", photo as Any)]) return ("messageActionSuggestProfilePhoto", [("photo", photo as Any)])
case .messageActionTodoAppendTasks(let list):
return ("messageActionTodoAppendTasks", [("list", list as Any)])
case .messageActionTodoCompletions(let completed, let incompleted):
return ("messageActionTodoCompletions", [("completed", completed as Any), ("incompleted", incompleted as Any)])
case .messageActionTopicCreate(let flags, let title, let iconColor, let iconEmojiId): case .messageActionTopicCreate(let flags, let title, let iconColor, let iconEmojiId):
return ("messageActionTopicCreate", [("flags", flags as Any), ("title", title as Any), ("iconColor", iconColor as Any), ("iconEmojiId", iconEmojiId as Any)]) return ("messageActionTopicCreate", [("flags", flags as Any), ("title", title as Any), ("iconColor", iconColor as Any), ("iconEmojiId", iconEmojiId as Any)])
case .messageActionTopicEdit(let flags, let title, let iconEmojiId, let closed, let hidden): case .messageActionTopicEdit(let flags, let title, let iconEmojiId, let closed, let hidden):
@ -1715,6 +1752,37 @@ public extension Api {
return nil return nil
} }
} }
public static func parse_messageActionTodoAppendTasks(_ reader: BufferReader) -> MessageAction? {
var _1: [Api.TodoItem]?
if let _ = reader.readInt32() {
_1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.TodoItem.self)
}
let _c1 = _1 != nil
if _c1 {
return Api.MessageAction.messageActionTodoAppendTasks(list: _1!)
}
else {
return nil
}
}
public static func parse_messageActionTodoCompletions(_ reader: BufferReader) -> MessageAction? {
var _1: [Int32]?
if let _ = reader.readInt32() {
_1 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self)
}
var _2: [Int32]?
if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.MessageAction.messageActionTodoCompletions(completed: _1!, incompleted: _2!)
}
else {
return nil
}
}
public static func parse_messageActionTopicCreate(_ reader: BufferReader) -> MessageAction? { public static func parse_messageActionTopicCreate(_ reader: BufferReader) -> MessageAction? {
var _1: Int32? var _1: Int32?
_1 = reader.readInt32() _1 = reader.readInt32()

View File

@ -722,6 +722,7 @@ public extension Api {
case messageMediaPhoto(flags: Int32, photo: Api.Photo?, ttlSeconds: Int32?) case messageMediaPhoto(flags: Int32, photo: Api.Photo?, ttlSeconds: Int32?)
case messageMediaPoll(poll: Api.Poll, results: Api.PollResults) case messageMediaPoll(poll: Api.Poll, results: Api.PollResults)
case messageMediaStory(flags: Int32, peer: Api.Peer, id: Int32, story: Api.StoryItem?) case messageMediaStory(flags: Int32, peer: Api.Peer, id: Int32, story: Api.StoryItem?)
case messageMediaToDo(flags: Int32, todo: Api.TodoList, completions: [Api.TodoCompletion]?)
case messageMediaUnsupported case messageMediaUnsupported
case messageMediaVenue(geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String) case messageMediaVenue(geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String)
case messageMediaWebPage(flags: Int32, webpage: Api.WebPage) case messageMediaWebPage(flags: Int32, webpage: Api.WebPage)
@ -878,6 +879,18 @@ public extension Api {
serializeInt32(id, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {story!.serialize(buffer, true)} if Int(flags) & Int(1 << 0) != 0 {story!.serialize(buffer, true)}
break break
case .messageMediaToDo(let flags, let todo, let completions):
if boxed {
buffer.appendInt32(-1974226924)
}
serializeInt32(flags, buffer: buffer, boxed: false)
todo.serialize(buffer, true)
if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(completions!.count))
for item in completions! {
item.serialize(buffer, true)
}}
break
case .messageMediaUnsupported: case .messageMediaUnsupported:
if boxed { if boxed {
buffer.appendInt32(-1618676578) buffer.appendInt32(-1618676578)
@ -935,6 +948,8 @@ public extension Api {
return ("messageMediaPoll", [("poll", poll as Any), ("results", results as Any)]) return ("messageMediaPoll", [("poll", poll as Any), ("results", results as Any)])
case .messageMediaStory(let flags, let peer, let id, let story): case .messageMediaStory(let flags, let peer, let id, let story):
return ("messageMediaStory", [("flags", flags as Any), ("peer", peer as Any), ("id", id as Any), ("story", story as Any)]) return ("messageMediaStory", [("flags", flags as Any), ("peer", peer as Any), ("id", id as Any), ("story", story as Any)])
case .messageMediaToDo(let flags, let todo, let completions):
return ("messageMediaToDo", [("flags", flags as Any), ("todo", todo as Any), ("completions", completions as Any)])
case .messageMediaUnsupported: case .messageMediaUnsupported:
return ("messageMediaUnsupported", []) return ("messageMediaUnsupported", [])
case .messageMediaVenue(let geo, let title, let address, let provider, let venueId, let venueType): case .messageMediaVenue(let geo, let title, let address, let provider, let venueId, let venueType):
@ -1262,6 +1277,27 @@ public extension Api {
return nil return nil
} }
} }
public static func parse_messageMediaToDo(_ reader: BufferReader) -> MessageMedia? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Api.TodoList?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.TodoList
}
var _3: [Api.TodoCompletion]?
if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.TodoCompletion.self)
} }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil
if _c1 && _c2 && _c3 {
return Api.MessageMedia.messageMediaToDo(flags: _1!, todo: _2!, completions: _3)
}
else {
return nil
}
}
public static func parse_messageMediaUnsupported(_ reader: BufferReader) -> MessageMedia? { public static func parse_messageMediaUnsupported(_ reader: BufferReader) -> MessageMedia? {
return Api.MessageMedia.messageMediaUnsupported return Api.MessageMedia.messageMediaUnsupported
} }

View File

@ -442,13 +442,13 @@ public extension Api {
} }
public extension Api { public extension Api {
indirect enum SponsoredMessage: TypeConstructorDescription { indirect enum SponsoredMessage: TypeConstructorDescription {
case sponsoredMessage(flags: Int32, randomId: Buffer, url: String, title: String, message: String, entities: [Api.MessageEntity]?, photo: Api.Photo?, media: Api.MessageMedia?, color: Api.PeerColor?, buttonText: String, sponsorInfo: String?, additionalInfo: String?) case sponsoredMessage(flags: Int32, randomId: Buffer, url: String, title: String, message: String, entities: [Api.MessageEntity]?, photo: Api.Photo?, media: Api.MessageMedia?, color: Api.PeerColor?, buttonText: String, sponsorInfo: String?, additionalInfo: String?, minDisplayDuration: Int32?, maxDisplayDuration: Int32?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let media, let color, let buttonText, let sponsorInfo, let additionalInfo): case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let media, let color, let buttonText, let sponsorInfo, let additionalInfo, let minDisplayDuration, let maxDisplayDuration):
if boxed { if boxed {
buffer.appendInt32(1301522832) buffer.appendInt32(2109703795)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
serializeBytes(randomId, buffer: buffer, boxed: false) serializeBytes(randomId, buffer: buffer, boxed: false)
@ -466,14 +466,16 @@ public extension Api {
serializeString(buttonText, buffer: buffer, boxed: false) serializeString(buttonText, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 7) != 0 {serializeString(sponsorInfo!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 7) != 0 {serializeString(sponsorInfo!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 8) != 0 {serializeString(additionalInfo!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 8) != 0 {serializeString(additionalInfo!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 15) != 0 {serializeInt32(minDisplayDuration!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 15) != 0 {serializeInt32(maxDisplayDuration!, buffer: buffer, boxed: false)}
break break
} }
} }
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { switch self {
case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let media, let color, let buttonText, let sponsorInfo, let additionalInfo): case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let media, let color, let buttonText, let sponsorInfo, let additionalInfo, let minDisplayDuration, let maxDisplayDuration):
return ("sponsoredMessage", [("flags", flags as Any), ("randomId", randomId as Any), ("url", url as Any), ("title", title as Any), ("message", message as Any), ("entities", entities as Any), ("photo", photo as Any), ("media", media as Any), ("color", color as Any), ("buttonText", buttonText as Any), ("sponsorInfo", sponsorInfo as Any), ("additionalInfo", additionalInfo as Any)]) return ("sponsoredMessage", [("flags", flags as Any), ("randomId", randomId as Any), ("url", url as Any), ("title", title as Any), ("message", message as Any), ("entities", entities as Any), ("photo", photo as Any), ("media", media as Any), ("color", color as Any), ("buttonText", buttonText as Any), ("sponsorInfo", sponsorInfo as Any), ("additionalInfo", additionalInfo as Any), ("minDisplayDuration", minDisplayDuration as Any), ("maxDisplayDuration", maxDisplayDuration as Any)])
} }
} }
@ -510,6 +512,10 @@ public extension Api {
if Int(_1!) & Int(1 << 7) != 0 {_11 = parseString(reader) } if Int(_1!) & Int(1 << 7) != 0 {_11 = parseString(reader) }
var _12: String? var _12: String?
if Int(_1!) & Int(1 << 8) != 0 {_12 = parseString(reader) } if Int(_1!) & Int(1 << 8) != 0 {_12 = parseString(reader) }
var _13: Int32?
if Int(_1!) & Int(1 << 15) != 0 {_13 = reader.readInt32() }
var _14: Int32?
if Int(_1!) & Int(1 << 15) != 0 {_14 = reader.readInt32() }
let _c1 = _1 != nil let _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
@ -522,8 +528,10 @@ public extension Api {
let _c10 = _10 != nil let _c10 = _10 != nil
let _c11 = (Int(_1!) & Int(1 << 7) == 0) || _11 != nil let _c11 = (Int(_1!) & Int(1 << 7) == 0) || _11 != nil
let _c12 = (Int(_1!) & Int(1 << 8) == 0) || _12 != nil let _c12 = (Int(_1!) & Int(1 << 8) == 0) || _12 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 { let _c13 = (Int(_1!) & Int(1 << 15) == 0) || _13 != nil
return Api.SponsoredMessage.sponsoredMessage(flags: _1!, randomId: _2!, url: _3!, title: _4!, message: _5!, entities: _6, photo: _7, media: _8, color: _9, buttonText: _10!, sponsorInfo: _11, additionalInfo: _12) let _c14 = (Int(_1!) & Int(1 << 15) == 0) || _14 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 {
return Api.SponsoredMessage.sponsoredMessage(flags: _1!, randomId: _2!, url: _3!, title: _4!, message: _5!, entities: _6, photo: _7, media: _8, color: _9, buttonText: _10!, sponsorInfo: _11, additionalInfo: _12, minDisplayDuration: _13, maxDisplayDuration: _14)
} }
else { else {
return nil return nil

View File

@ -762,6 +762,144 @@ public extension Api {
} }
} }
public extension Api {
enum TodoCompletion: TypeConstructorDescription {
case todoCompletion(id: Int32, completedBy: Int64, date: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .todoCompletion(let id, let completedBy, let date):
if boxed {
buffer.appendInt32(1287725239)
}
serializeInt32(id, buffer: buffer, boxed: false)
serializeInt64(completedBy, buffer: buffer, boxed: false)
serializeInt32(date, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .todoCompletion(let id, let completedBy, let date):
return ("todoCompletion", [("id", id as Any), ("completedBy", completedBy as Any), ("date", date as Any)])
}
}
public static func parse_todoCompletion(_ reader: BufferReader) -> TodoCompletion? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int64?
_2 = reader.readInt64()
var _3: Int32?
_3 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
if _c1 && _c2 && _c3 {
return Api.TodoCompletion.todoCompletion(id: _1!, completedBy: _2!, date: _3!)
}
else {
return nil
}
}
}
}
public extension Api {
enum TodoItem: TypeConstructorDescription {
case todoItem(id: Int32, title: Api.TextWithEntities)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .todoItem(let id, let title):
if boxed {
buffer.appendInt32(-878074577)
}
serializeInt32(id, buffer: buffer, boxed: false)
title.serialize(buffer, true)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .todoItem(let id, let title):
return ("todoItem", [("id", id as Any), ("title", title as Any)])
}
}
public static func parse_todoItem(_ reader: BufferReader) -> TodoItem? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Api.TextWithEntities?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.TextWithEntities
}
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.TodoItem.todoItem(id: _1!, title: _2!)
}
else {
return nil
}
}
}
}
public extension Api {
enum TodoList: TypeConstructorDescription {
case todoList(flags: Int32, title: Api.TextWithEntities, list: [Api.TodoItem])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .todoList(let flags, let title, let list):
if boxed {
buffer.appendInt32(1236871718)
}
serializeInt32(flags, buffer: buffer, boxed: false)
title.serialize(buffer, true)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(list.count))
for item in list {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .todoList(let flags, let title, let list):
return ("todoList", [("flags", flags as Any), ("title", title as Any), ("list", list as Any)])
}
}
public static func parse_todoList(_ reader: BufferReader) -> TodoList? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Api.TextWithEntities?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.TextWithEntities
}
var _3: [Api.TodoItem]?
if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.TodoItem.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
if _c1 && _c2 && _c3 {
return Api.TodoList.todoList(flags: _1!, title: _2!, list: _3!)
}
else {
return nil
}
}
}
}
public extension Api { public extension Api {
enum TopPeer: TypeConstructorDescription { enum TopPeer: TypeConstructorDescription {
case topPeer(peer: Api.Peer, rating: Double) case topPeer(peer: Api.Peer, rating: Double)

View File

@ -304,17 +304,19 @@ public extension Api.messages {
} }
public extension Api.messages { public extension Api.messages {
enum SponsoredMessages: TypeConstructorDescription { enum SponsoredMessages: TypeConstructorDescription {
case sponsoredMessages(flags: Int32, postsBetween: Int32?, messages: [Api.SponsoredMessage], chats: [Api.Chat], users: [Api.User]) case sponsoredMessages(flags: Int32, postsBetween: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Api.SponsoredMessage], chats: [Api.Chat], users: [Api.User])
case sponsoredMessagesEmpty case sponsoredMessagesEmpty
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
case .sponsoredMessages(let flags, let postsBetween, let messages, let chats, let users): case .sponsoredMessages(let flags, let postsBetween, let startDelay, let betweenDelay, let messages, let chats, let users):
if boxed { if boxed {
buffer.appendInt32(-907141753) buffer.appendInt32(-2464403)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(postsBetween!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeInt32(postsBetween!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(startDelay!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 2) != 0 {serializeInt32(betweenDelay!, buffer: buffer, boxed: false)}
buffer.appendInt32(481674261) buffer.appendInt32(481674261)
buffer.appendInt32(Int32(messages.count)) buffer.appendInt32(Int32(messages.count))
for item in messages { for item in messages {
@ -342,8 +344,8 @@ public extension Api.messages {
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { switch self {
case .sponsoredMessages(let flags, let postsBetween, let messages, let chats, let users): case .sponsoredMessages(let flags, let postsBetween, let startDelay, let betweenDelay, let messages, let chats, let users):
return ("sponsoredMessages", [("flags", flags as Any), ("postsBetween", postsBetween as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) return ("sponsoredMessages", [("flags", flags as Any), ("postsBetween", postsBetween as Any), ("startDelay", startDelay as Any), ("betweenDelay", betweenDelay as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)])
case .sponsoredMessagesEmpty: case .sponsoredMessagesEmpty:
return ("sponsoredMessagesEmpty", []) return ("sponsoredMessagesEmpty", [])
} }
@ -354,25 +356,31 @@ public extension Api.messages {
_1 = reader.readInt32() _1 = reader.readInt32()
var _2: Int32? var _2: Int32?
if Int(_1!) & Int(1 << 0) != 0 {_2 = reader.readInt32() } if Int(_1!) & Int(1 << 0) != 0 {_2 = reader.readInt32() }
var _3: [Api.SponsoredMessage]? var _3: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_3 = reader.readInt32() }
var _4: Int32?
if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() }
var _5: [Api.SponsoredMessage]?
if let _ = reader.readInt32() { if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SponsoredMessage.self) _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SponsoredMessage.self)
} }
var _4: [Api.Chat]? var _6: [Api.Chat]?
if let _ = reader.readInt32() { if let _ = reader.readInt32() {
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
} }
var _5: [Api.User]? var _7: [Api.User]?
if let _ = reader.readInt32() { if let _ = reader.readInt32() {
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
} }
let _c1 = _1 != nil let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil
let _c3 = _3 != nil let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
let _c4 = _4 != nil let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil
let _c5 = _5 != nil let _c5 = _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 { let _c6 = _6 != nil
return Api.messages.SponsoredMessages.sponsoredMessages(flags: _1!, postsBetween: _2, messages: _3!, chats: _4!, users: _5!) let _c7 = _7 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
return Api.messages.SponsoredMessages.sponsoredMessages(flags: _1!, postsBetween: _2, startDelay: _3, betweenDelay: _4, messages: _5!, chats: _6!, users: _7!)
} }
else { else {
return nil return nil

View File

@ -21,22 +21,6 @@ public extension Api.functions.account {
}) })
} }
} }
public extension Api.functions.account {
static func addNoPaidMessagesException(flags: Int32, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(1869122215)
serializeInt32(flags, buffer: buffer, boxed: false)
userId.serialize(buffer, true)
return (FunctionDescription(name: "account.addNoPaidMessagesException", parameters: [("flags", String(describing: flags)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Bool
}
return result
})
}
}
public extension Api.functions.account { public extension Api.functions.account {
static func cancelPasswordEmail() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) { static func cancelPasswordEmail() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer() let buffer = Buffer()
@ -666,11 +650,13 @@ public extension Api.functions.account {
} }
} }
public extension Api.functions.account { public extension Api.functions.account {
static func getPaidMessagesRevenue(userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.account.PaidMessagesRevenue>) { static func getPaidMessagesRevenue(flags: Int32, parentPeer: Api.InputPeer?, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.account.PaidMessagesRevenue>) {
let buffer = Buffer() let buffer = Buffer()
buffer.appendInt32(-249139400) buffer.appendInt32(431639143)
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {parentPeer!.serialize(buffer, true)}
userId.serialize(buffer, true) userId.serialize(buffer, true)
return (FunctionDescription(name: "account.getPaidMessagesRevenue", parameters: [("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.PaidMessagesRevenue? in return (FunctionDescription(name: "account.getPaidMessagesRevenue", parameters: [("flags", String(describing: flags)), ("parentPeer", String(describing: parentPeer)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.PaidMessagesRevenue? in
let reader = BufferReader(buffer) let reader = BufferReader(buffer)
var result: Api.account.PaidMessagesRevenue? var result: Api.account.PaidMessagesRevenue?
if let signature = reader.readInt32() { if let signature = reader.readInt32() {
@ -1437,6 +1423,23 @@ public extension Api.functions.account {
}) })
} }
} }
public extension Api.functions.account {
static func toggleNoPaidMessagesException(flags: Int32, parentPeer: Api.InputPeer?, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(-30483850)
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {parentPeer!.serialize(buffer, true)}
userId.serialize(buffer, true)
return (FunctionDescription(name: "account.toggleNoPaidMessagesException", parameters: [("flags", String(describing: flags)), ("parentPeer", String(describing: parentPeer)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Bool
}
return result
})
}
}
public extension Api.functions.account { public extension Api.functions.account {
static func toggleSponsoredMessages(enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) { static func toggleSponsoredMessages(enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer() let buffer = Buffer()
@ -5067,6 +5070,27 @@ public extension Api.functions.messages {
}) })
} }
} }
public extension Api.functions.messages {
static func appendTodoList(peer: Api.InputPeer, msgId: Int32, list: [Api.TodoItem]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()
buffer.appendInt32(564531287)
peer.serialize(buffer, true)
serializeInt32(msgId, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(list.count))
for item in list {
item.serialize(buffer, true)
}
return (FunctionDescription(name: "messages.appendTodoList", parameters: [("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("list", String(describing: list))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
let reader = BufferReader(buffer)
var result: Api.Updates?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Updates
}
return result
})
}
}
public extension Api.functions.messages { public extension Api.functions.messages {
static func checkChatInvite(hash: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.ChatInvite>) { static func checkChatInvite(hash: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.ChatInvite>) {
let buffer = Buffer() let buffer = Buffer()
@ -7065,11 +7089,13 @@ public extension Api.functions.messages {
} }
} }
public extension Api.functions.messages { public extension Api.functions.messages {
static func getSponsoredMessages(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.SponsoredMessages>) { static func getSponsoredMessages(flags: Int32, peer: Api.InputPeer, msgId: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.SponsoredMessages>) {
let buffer = Buffer() let buffer = Buffer()
buffer.appendInt32(-1680673735) buffer.appendInt32(1030547536)
serializeInt32(flags, buffer: buffer, boxed: false)
peer.serialize(buffer, true) peer.serialize(buffer, true)
return (FunctionDescription(name: "messages.getSponsoredMessages", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SponsoredMessages? in if Int(flags) & Int(1 << 0) != 0 {serializeInt32(msgId!, buffer: buffer, boxed: false)}
return (FunctionDescription(name: "messages.getSponsoredMessages", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("msgId", String(describing: msgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SponsoredMessages? in
let reader = BufferReader(buffer) let reader = BufferReader(buffer)
var result: Api.messages.SponsoredMessages? var result: Api.messages.SponsoredMessages?
if let signature = reader.readInt32() { if let signature = reader.readInt32() {
@ -8880,6 +8906,32 @@ public extension Api.functions.messages {
}) })
} }
} }
public extension Api.functions.messages {
static func toggleTodoCompleted(peer: Api.InputPeer, msgId: Int32, completed: [Int32], incompleted: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()
buffer.appendInt32(-740282076)
peer.serialize(buffer, true)
serializeInt32(msgId, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(completed.count))
for item in completed {
serializeInt32(item, buffer: buffer, boxed: false)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(incompleted.count))
for item in incompleted {
serializeInt32(item, buffer: buffer, boxed: false)
}
return (FunctionDescription(name: "messages.toggleTodoCompleted", parameters: [("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("completed", String(describing: completed)), ("incompleted", String(describing: incompleted))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
let reader = BufferReader(buffer)
var result: Api.Updates?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Updates
}
return result
})
}
}
public extension Api.functions.messages { public extension Api.functions.messages {
static func transcribeAudio(peer: Api.InputPeer, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.TranscribedAudio>) { static func transcribeAudio(peer: Api.InputPeer, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.TranscribedAudio>) {
let buffer = Buffer() let buffer = Buffer()

View File

@ -233,6 +233,9 @@ private var declaredEncodables: Void = {
declareEncodable(TelegramMediaPaidContent.self, f: { TelegramMediaPaidContent(decoder: $0) }) declareEncodable(TelegramMediaPaidContent.self, f: { TelegramMediaPaidContent(decoder: $0) })
declareEncodable(ReportDeliveryMessageAttribute.self, f: { ReportDeliveryMessageAttribute(decoder: $0) }) declareEncodable(ReportDeliveryMessageAttribute.self, f: { ReportDeliveryMessageAttribute(decoder: $0) })
declareEncodable(PaidStarsMessageAttribute.self, f: { PaidStarsMessageAttribute(decoder: $0) }) declareEncodable(PaidStarsMessageAttribute.self, f: { PaidStarsMessageAttribute(decoder: $0) })
declareEncodable(TelegramMediaTodo.self, f: { TelegramMediaTodo(decoder: $0) })
declareEncodable(TelegramMediaTodo.Item.self, f: { TelegramMediaTodo.Item(decoder: $0) })
declareEncodable(TelegramMediaTodo.Completion.self, f: { TelegramMediaTodo.Completion(decoder: $0) })
return return
}() }()

View File

@ -15,8 +15,10 @@ public final class AdMessageAttribute: MessageAttribute {
public let additionalInfo: String? public let additionalInfo: String?
public let canReport: Bool public let canReport: Bool
public let hasContentMedia: Bool public let hasContentMedia: Bool
public let minDisplayDuration: Int32?
public let maxDisplayDuration: Int32?
public init(opaqueId: Data, messageType: MessageType, url: String, buttonText: String, sponsorInfo: String?, additionalInfo: String?, canReport: Bool, hasContentMedia: Bool) { public init(opaqueId: Data, messageType: MessageType, url: String, buttonText: String, sponsorInfo: String?, additionalInfo: String?, canReport: Bool, hasContentMedia: Bool, minDisplayDuration: Int32?, maxDisplayDuration: Int32?) {
self.opaqueId = opaqueId self.opaqueId = opaqueId
self.messageType = messageType self.messageType = messageType
self.url = url self.url = url
@ -25,6 +27,8 @@ public final class AdMessageAttribute: MessageAttribute {
self.additionalInfo = additionalInfo self.additionalInfo = additionalInfo
self.canReport = canReport self.canReport = canReport
self.hasContentMedia = hasContentMedia self.hasContentMedia = hasContentMedia
self.minDisplayDuration = minDisplayDuration
self.maxDisplayDuration = maxDisplayDuration
} }
public init(decoder: PostboxDecoder) { public init(decoder: PostboxDecoder) {

View File

@ -137,7 +137,7 @@ func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? {
} else { } else {
return nil return nil
} }
case let .messageService(_, _, _, chatPeerId, _, _, _, _, _): case let .messageService(_, _, _, chatPeerId, _, _, _, _, _, _):
return chatPeerId.peerId return chatPeerId.peerId
} }
} }
@ -218,7 +218,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
return result return result
case .messageEmpty: case .messageEmpty:
return [] return []
case let .messageService(_, _, fromId, chatPeerId, _, _, action, _, _): case let .messageService(_, _, fromId, chatPeerId, savedPeerId, _, _, action, _, _):
let peerId: PeerId = chatPeerId.peerId let peerId: PeerId = chatPeerId.peerId
var result = [peerId] var result = [peerId]
@ -227,9 +227,12 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
if resolvedFromId != peerId { if resolvedFromId != peerId {
result.append(resolvedFromId) result.append(resolvedFromId)
} }
if let savedPeerId, resolvedFromId != savedPeerId.peerId {
result.append(savedPeerId.peerId)
}
switch action { switch action {
case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe, .messageActionStarGift, .messageActionStarGiftUnique, .messageActionPaidMessagesRefunded, .messageActionPaidMessagesPrice: case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe, .messageActionStarGift, .messageActionStarGiftUnique, .messageActionPaidMessagesRefunded, .messageActionPaidMessagesPrice, .messageActionTodoCompletions, .messageActionTodoAppendTasks:
break break
case let .messageActionChannelMigrateFrom(_, chatId): case let .messageActionChannelMigrateFrom(_, chatId):
result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))) result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)))
@ -301,7 +304,7 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: Refere
} }
case .messageEmpty: case .messageEmpty:
break break
case let .messageService(_, id, _, chatPeerId, replyHeader, _, _, _, _): case let .messageService(_, id, _, chatPeerId, _, replyHeader, _, _, _, _):
if let replyHeader = replyHeader { if let replyHeader = replyHeader {
switch replyHeader { switch replyHeader {
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities, quoteOffset): case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities, quoteOffset):
@ -428,6 +431,30 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil, nil, nil) return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil, nil, nil)
} }
case let .messageMediaToDo(_, todo, completions):
switch todo {
case let .todoList(apiFlags, title, list):
var flags: TelegramMediaTodo.Flags = []
if (apiFlags & (1 << 0)) != 0 {
flags.insert(.othersCanAppend)
}
if (apiFlags & (1 << 1)) != 0 {
flags.insert(.othersCanComplete)
}
let todoText: String
let todoEntities: [MessageTextEntity]
switch title {
case let .textWithEntities(text, entities):
todoText = text
todoEntities = messageTextEntitiesFromApiEntities(entities)
}
var todoCompletions: [TelegramMediaTodo.Completion] = []
if let completions {
todoCompletions = completions.map(TelegramMediaTodo.Completion.init(apiCompletion:))
}
return (TelegramMediaTodo(flags: flags, text: todoText, textEntities: todoEntities, items: list.map(TelegramMediaTodo.Item.init(apiItem:)), completions: todoCompletions), nil, nil, nil, nil, nil)
}
case let .messageMediaDice(value, emoticon): case let .messageMediaDice(value, emoticon):
return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil, nil, nil) return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil, nil, nil)
case let .messageMediaStory(flags, peerId, id, _): case let .messageMediaStory(flags, peerId, id, _):
@ -1037,14 +1064,16 @@ extension StoreMessage {
self.init(id: MessageId(peerId: peerId, namespace: namespace, id: id), globallyUniqueId: nil, groupingKey: groupingId, threadId: threadId, timestamp: date, flags: storeFlags, tags: tags, globalTags: globalTags, localTags: [], forwardInfo: forwardInfo, authorId: authorId, text: messageText, attributes: attributes, media: medias) self.init(id: MessageId(peerId: peerId, namespace: namespace, id: id), globallyUniqueId: nil, groupingKey: groupingId, threadId: threadId, timestamp: date, flags: storeFlags, tags: tags, globalTags: globalTags, localTags: [], forwardInfo: forwardInfo, authorId: authorId, text: messageText, attributes: attributes, media: medias)
case .messageEmpty: case .messageEmpty:
return nil return nil
case let .messageService(flags, id, fromId, chatPeerId, replyTo, date, action, reactions, ttlPeriod): case let .messageService(flags, id, fromId, chatPeerId, savedPeerId, replyTo, date, action, reactions, ttlPeriod):
let peerId: PeerId = chatPeerId.peerId let peerId: PeerId = chatPeerId.peerId
let authorId: PeerId? = fromId?.peerId ?? chatPeerId.peerId let authorId: PeerId? = fromId?.peerId ?? chatPeerId.peerId
var attributes: [MessageAttribute] = [] var attributes: [MessageAttribute] = []
var threadId: Int64? var threadId: Int64?
if let replyTo = replyTo { if let savedPeerId {
threadId = savedPeerId.peerId.toInt64()
} else if let replyTo = replyTo {
var threadMessageId: MessageId? var threadMessageId: MessageId?
switch replyTo { switch replyTo {
case let .messageReplyHeader(innerFlags, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities, quoteOffset): case let .messageReplyHeader(innerFlags, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities, quoteOffset):

View File

@ -224,6 +224,10 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
flags: mappedFlags, flags: mappedFlags,
otherParticipants: otherParticipants.flatMap({ return $0.map(\.peerId) }) ?? [] otherParticipants: otherParticipants.flatMap({ return $0.map(\.peerId) }) ?? []
))) )))
case let .messageActionTodoCompletions(completed, incompleted):
return TelegramMediaAction(action: .todoCompletions(completed: completed, incompleted: incompleted))
case let .messageActionTodoAppendTasks(list):
return TelegramMediaAction(action: .todoAppendTasks(list.map { TelegramMediaTodo.Item(apiItem: $0) }))
} }
} }

View File

@ -0,0 +1,33 @@
import Foundation
import Postbox
import TelegramApi
extension TelegramMediaTodo.Item {
init(apiItem: Api.TodoItem) {
switch apiItem {
case let .todoItem(id, title):
let itemText: String
let itemEntities: [MessageTextEntity]
switch title {
case let .textWithEntities(text, entities):
itemText = text
itemEntities = messageTextEntitiesFromApiEntities(entities)
}
self.init(text: itemText, entities: itemEntities, id: id)
}
}
var apiItem: Api.TodoItem {
return .todoItem(id: self.id, title: .textWithEntities(text: self.text, entities: apiEntitiesFromMessageTextEntities(self.entities, associatedPeers: SimpleDictionary())))
}
}
extension TelegramMediaTodo.Completion {
init(apiCompletion: Api.TodoCompletion) {
switch apiCompletion {
case let .todoCompletion(id, completedBy, date):
self.init(id: id, date: date, completedBy: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(completedBy)))
}
}
}

View File

@ -319,6 +319,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post
if poll.deadlineTimeout != nil { if poll.deadlineTimeout != nil {
pollFlags |= 1 << 4 pollFlags |= 1 << 4
} }
var mappedSolution: String? var mappedSolution: String?
var mappedSolutionEntities: [Api.MessageEntity]? var mappedSolutionEntities: [Api.MessageEntity]?
if let solution = poll.results.solution { if let solution = poll.results.solution {
@ -328,10 +329,20 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post
} }
let inputPoll = Api.InputMedia.inputMediaPoll(flags: pollMediaFlags, poll: Api.Poll.poll(id: 0, flags: pollFlags, question: .textWithEntities(text: poll.text, entities: apiEntitiesFromMessageTextEntities(poll.textEntities, associatedPeers: SimpleDictionary())), answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: mappedSolution, solutionEntities: mappedSolutionEntities) let inputPoll = Api.InputMedia.inputMediaPoll(flags: pollMediaFlags, poll: Api.Poll.poll(id: 0, flags: pollFlags, question: .textWithEntities(text: poll.text, entities: apiEntitiesFromMessageTextEntities(poll.textEntities, associatedPeers: SimpleDictionary())), answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: mappedSolution, solutionEntities: mappedSolutionEntities)
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputPoll, text), reuploadInfo: nil, cacheReferenceKey: nil))) return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputPoll, text), reuploadInfo: nil, cacheReferenceKey: nil)))
} else if let media = media as? TelegramMediaDice { } else if let todo = media as? TelegramMediaTodo {
let inputDice = Api.InputMedia.inputMediaDice(emoticon: media.emoji) var flags: Int32 = 0
if todo.flags.contains(.othersCanAppend) {
flags |= 1 << 0
}
if todo.flags.contains(.othersCanComplete) {
flags |= 1 << 1
}
let inputTodo = Api.InputMedia.inputMediaTodo(todo: .todoList(flags: flags, title: .textWithEntities(text: todo.text, entities: apiEntitiesFromMessageTextEntities(todo.textEntities, associatedPeers: SimpleDictionary())), list: todo.items.map { $0.apiItem }))
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputTodo, text), reuploadInfo: nil, cacheReferenceKey: nil)))
} else if let dice = media as? TelegramMediaDice {
let inputDice = Api.InputMedia.inputMediaDice(emoticon: dice.emoji)
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputDice, text), reuploadInfo: nil, cacheReferenceKey: nil))) return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputDice, text), reuploadInfo: nil, cacheReferenceKey: nil)))
} else if let media = media as? TelegramMediaWebpage, case let .Loaded(content) = media.content { } else if let webPage = media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content {
var flags: Int32 = 0 var flags: Int32 = 0
flags |= 1 << 2 flags |= 1 << 2
if let attribute = attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute { if let attribute = attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute {

View File

@ -68,7 +68,18 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox,
} }
return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, forceNoBigParts: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: attributes, mediaReference: nil) return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, forceNoBigParts: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: attributes, mediaReference: nil)
} }
if let uploadSignal = generateUploadSignal(forceReupload) { if let todo = media.media as? TelegramMediaTodo {
var flags: Int32 = 0
if todo.flags.contains(.othersCanAppend) {
flags |= 1 << 0
}
if todo.flags.contains(.othersCanComplete) {
flags |= 1 << 1
}
let inputTodo = Api.InputMedia.inputMediaTodo(todo: .todoList(flags: flags, title: .textWithEntities(text: todo.text, entities: apiEntitiesFromMessageTextEntities(todo.textEntities, associatedPeers: SimpleDictionary())), list: todo.items.map { $0.apiItem }))
uploadedMedia = .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputTodo, text), reuploadInfo: nil, cacheReferenceKey: nil)))
}
else if let uploadSignal = generateUploadSignal(forceReupload) {
uploadedMedia = .single(.progress(PendingMessageUploadedContentProgress(progress: 0.027))) uploadedMedia = .single(.progress(PendingMessageUploadedContentProgress(progress: 0.027)))
|> then(uploadSignal) |> then(uploadSignal)
|> map { result -> PendingMessageUploadedContentResult? in |> map { result -> PendingMessageUploadedContentResult? in
@ -110,7 +121,7 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox,
if text.isEmpty { if text.isEmpty {
for media in message.media { for media in message.media {
switch media { switch media {
case _ as TelegramMediaImage, _ as TelegramMediaFile: case _ as TelegramMediaImage, _ as TelegramMediaFile, _ as TelegramMediaTodo:
break break
default: default:
if let _ = scheduleTime { if let _ = scheduleTime {

View File

@ -108,7 +108,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
updatedTimestamp = date updatedTimestamp = date
case .messageEmpty: case .messageEmpty:
break break
case let .messageService(_, _, _, _, _, date, _, _, _): case let .messageService(_, _, _, _, _, _, date, _, _, _):
updatedTimestamp = date updatedTimestamp = date
} }
} else { } else {

View File

@ -11,7 +11,7 @@ func _internal_getPaidMessagesRevenue(account: Account, peerId: PeerId) -> Signa
guard let inputUser else { guard let inputUser else {
return .single(nil) return .single(nil)
} }
return account.network.request(Api.functions.account.getPaidMessagesRevenue(userId: inputUser)) return account.network.request(Api.functions.account.getPaidMessagesRevenue(flags: 0, parentPeer: nil, userId: inputUser))
|> map(Optional.init) |> map(Optional.init)
|> `catch` { _ -> Signal<Api.account.PaidMessagesRevenue?, NoError> in |> `catch` { _ -> Signal<Api.account.PaidMessagesRevenue?, NoError> in
return .single(nil) return .single(nil)
@ -40,7 +40,7 @@ func _internal_addNoPaidMessagesException(account: Account, peerId: PeerId, refu
if refundCharged { if refundCharged {
flags |= (1 << 0) flags |= (1 << 0)
} }
return account.network.request(Api.functions.account.addNoPaidMessagesException(flags: flags, userId: inputUser)) return account.network.request(Api.functions.account.toggleNoPaidMessagesException(flags: flags, parentPeer: nil, userId: inputUser))
|> `catch` { _ -> Signal<Api.Bool, NoError> in |> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse) return .single(.boolFalse)
} |> mapToSignal { _ in } |> mapToSignal { _ in

View File

@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization { public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt { public func currentLayer() -> UInt {
return 204 return 205
} }
public func parseMessage(_ data: Data!) -> Any! { public func parseMessage(_ data: Data!) -> Any! {

View File

@ -108,7 +108,7 @@ extension Api.Message {
return id return id
case let .messageEmpty(_, id, _): case let .messageEmpty(_, id, _):
return id return id
case let .messageService(_, id, _, _, _, _, _, _, _): case let .messageService(_, id, _, _, _, _, _, _, _, _):
return id return id
} }
} }
@ -128,7 +128,7 @@ extension Api.Message {
} else { } else {
return nil return nil
} }
case let .messageService(_, id, _, chatPeerId, _, _, _, _, _): case let .messageService(_, id, _, chatPeerId, _, _, _, _, _, _):
let peerId: PeerId = chatPeerId.peerId let peerId: PeerId = chatPeerId.peerId
return MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id) return MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id)
} }
@ -141,7 +141,7 @@ extension Api.Message {
return peerId return peerId
case let .messageEmpty(_, _, peerId): case let .messageEmpty(_, _, peerId):
return peerId?.peerId return peerId?.peerId
case let .messageService(_, _, _, chatPeerId, _, _, _, _, _): case let .messageService(_, _, _, chatPeerId, _, _, _, _, _, _):
let peerId: PeerId = chatPeerId.peerId let peerId: PeerId = chatPeerId.peerId
return peerId return peerId
} }
@ -151,7 +151,7 @@ extension Api.Message {
switch self { switch self {
case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return date return date
case let .messageService(_, _, _, _, _, date, _, _, _): case let .messageService(_, _, _, _, _, _, date, _, _, _):
return date return date
case .messageEmpty: case .messageEmpty:
return nil return nil

View File

@ -160,6 +160,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case paidMessagesRefunded(count: Int32, stars: Int64) case paidMessagesRefunded(count: Int32, stars: Int64)
case paidMessagesPriceEdited(stars: Int64, broadcastMessagesAllowed: Bool) case paidMessagesPriceEdited(stars: Int64, broadcastMessagesAllowed: Bool)
case conferenceCall(ConferenceCall) case conferenceCall(ConferenceCall)
case todoCompletions(completed: [Int32], incompleted: [Int32])
case todoAppendTasks([TelegramMediaTodo.Item])
public init(decoder: PostboxDecoder) { public init(decoder: PostboxDecoder) {
let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0)
@ -295,6 +297,15 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
flags: ConferenceCall.Flags(rawValue: decoder.decodeInt32ForKey("flags", orElse: 0)), flags: ConferenceCall.Flags(rawValue: decoder.decodeInt32ForKey("flags", orElse: 0)),
otherParticipants: decoder.decodeInt64ArrayForKey("part").map(PeerId.init) otherParticipants: decoder.decodeInt64ArrayForKey("part").map(PeerId.init)
)) ))
case 49:
self = .todoCompletions(
completed: decoder.decodeInt32ArrayForKey("completed"),
incompleted: decoder.decodeInt32ArrayForKey("incompleted")
)
case 50:
self = .todoAppendTasks(
decoder.decodeObjectArrayWithDecoderForKey("tasks")
)
default: default:
self = .unknown self = .unknown
} }
@ -698,6 +709,13 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} }
encoder.encodeInt32(conferenceCall.flags.rawValue, forKey: "flags") encoder.encodeInt32(conferenceCall.flags.rawValue, forKey: "flags")
encoder.encodeInt64Array(conferenceCall.otherParticipants.map({ $0.toInt64() }), forKey: "part") encoder.encodeInt64Array(conferenceCall.otherParticipants.map({ $0.toInt64() }), forKey: "part")
case let .todoCompletions(completed, incompleted):
encoder.encodeInt32(49, forKey: "_rawValue")
encoder.encodeInt32Array(completed, forKey: "completed")
encoder.encodeInt32Array(incompleted, forKey: "incompleted")
case let .todoAppendTasks(tasks):
encoder.encodeInt32(50, forKey: "_rawValue")
encoder.encodeObjectArray(tasks, forKey: "tasks")
} }
} }

View File

@ -0,0 +1,154 @@
import Foundation
import Postbox
public final class TelegramMediaTodo: Media, Equatable {
public struct Flags: OptionSet {
public var rawValue: Int32
public init() {
self.rawValue = 0
}
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public static let othersCanAppend = Flags(rawValue: 1 << 0)
public static let othersCanComplete = Flags(rawValue: 1 << 1)
}
public struct Item: Equatable, PostboxCoding {
public let text: String
public let entities: [MessageTextEntity]
public let id: Int32
public init(text: String, entities: [MessageTextEntity], id: Int32) {
self.text = text
self.entities = entities
self.id = id
}
public init(decoder: PostboxDecoder) {
self.text = decoder.decodeStringForKey("t", orElse: "")
self.entities = decoder.decodeObjectArrayWithDecoderForKey("et")
self.id = decoder.decodeInt32ForKey("i", orElse: 0)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeString(self.text, forKey: "t")
encoder.encodeObjectArray(self.entities, forKey: "et")
encoder.encodeInt32(self.id, forKey: "i")
}
}
public struct Completion: Equatable, PostboxCoding {
public let id: Int32
public let date: Int32
public let completedBy: EnginePeer.Id
public init(id: Int32, date: Int32, completedBy: EnginePeer.Id) {
self.id = id
self.date = date
self.completedBy = completedBy
}
public init(decoder: PostboxDecoder) {
self.id = decoder.decodeInt32ForKey("i", orElse: 0)
self.date = decoder.decodeInt32ForKey("d", orElse: 0)
self.completedBy = PeerId(decoder.decodeInt64ForKey("p", orElse: 0))
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.id, forKey: "i")
encoder.encodeInt32(self.date, forKey: "d")
encoder.encodeInt64(self.completedBy.toInt64(), forKey: "p")
}
}
public var id: MediaId? {
return nil
}
public var peerIds: [PeerId] {
return self.completions.map { $0.completedBy }
}
public let flags: Flags
public let text: String
public let textEntities: [MessageTextEntity]
public let items: [Item]
public let completions: [Completion]
public init(flags: Flags, text: String, textEntities: [MessageTextEntity], items: [Item], completions: [Completion] = []) {
self.flags = flags
self.text = text
self.textEntities = textEntities
self.items = items
self.completions = completions
}
public init(decoder: PostboxDecoder) {
self.flags = Flags(rawValue: decoder.decodeInt32ForKey("f", orElse: 0))
self.text = decoder.decodeStringForKey("t", orElse: "")
self.textEntities = decoder.decodeObjectArrayWithDecoderForKey("te")
self.items = decoder.decodeObjectArrayWithDecoderForKey("is")
self.completions = decoder.decodeObjectArrayWithDecoderForKey("cs")
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.flags.rawValue, forKey: "f")
encoder.encodeString(self.text, forKey: "t")
encoder.encodeObjectArray(self.textEntities, forKey: "te")
encoder.encodeObjectArray(self.items, forKey: "is")
encoder.encodeObjectArray(self.completions, forKey: "cs")
}
public func isEqual(to other: Media) -> Bool {
guard let other = other as? TelegramMediaTodo else {
return false
}
return self == other
}
public func isSemanticallyEqual(to other: Media) -> Bool {
return self.isEqual(to: other)
}
public static func ==(lhs: TelegramMediaTodo, rhs: TelegramMediaTodo) -> Bool {
if lhs.flags != rhs.flags {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textEntities != rhs.textEntities {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.completions != rhs.completions {
return false
}
return true
}
func withUpdated(items: [TelegramMediaTodo.Item]) -> TelegramMediaTodo {
return TelegramMediaTodo(
flags: self.flags,
text: self.text,
textEntities: self.textEntities,
items: items,
completions: self.completions
)
}
func withUpdated(completions: [TelegramMediaTodo.Completion]) -> TelegramMediaTodo {
return TelegramMediaTodo(
flags: self.flags,
text: self.text,
textEntities: self.textEntities,
items: self.items,
completions: completions
)
}
}

View File

@ -20,6 +20,8 @@ private class AdMessagesHistoryContextImpl {
case sponsorInfo case sponsorInfo
case additionalInfo case additionalInfo
case canReport case canReport
case minDisplayDuration
case maxDisplayDuration
} }
enum MessageType: Int32, Codable { enum MessageType: Int32, Codable {
@ -41,6 +43,8 @@ private class AdMessagesHistoryContextImpl {
public let sponsorInfo: String? public let sponsorInfo: String?
public let additionalInfo: String? public let additionalInfo: String?
public let canReport: Bool public let canReport: Bool
public let minDisplayDuration: Int32?
public let maxDisplayDuration: Int32?
public init( public init(
opaqueId: Data, opaqueId: Data,
@ -56,7 +60,9 @@ private class AdMessagesHistoryContextImpl {
buttonText: String, buttonText: String,
sponsorInfo: String?, sponsorInfo: String?,
additionalInfo: String?, additionalInfo: String?,
canReport: Bool canReport: Bool,
minDisplayDuration: Int32?,
maxDisplayDuration: Int32?
) { ) {
self.opaqueId = opaqueId self.opaqueId = opaqueId
self.messageType = messageType self.messageType = messageType
@ -72,6 +78,8 @@ private class AdMessagesHistoryContextImpl {
self.sponsorInfo = sponsorInfo self.sponsorInfo = sponsorInfo
self.additionalInfo = additionalInfo self.additionalInfo = additionalInfo
self.canReport = canReport self.canReport = canReport
self.minDisplayDuration = minDisplayDuration
self.maxDisplayDuration = maxDisplayDuration
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
@ -109,6 +117,9 @@ private class AdMessagesHistoryContextImpl {
self.additionalInfo = try container.decodeIfPresent(String.self, forKey: .additionalInfo) self.additionalInfo = try container.decodeIfPresent(String.self, forKey: .additionalInfo)
self.canReport = try container.decodeIfPresent(Bool.self, forKey: .canReport) ?? false self.canReport = try container.decodeIfPresent(Bool.self, forKey: .canReport) ?? false
self.minDisplayDuration = try container.decodeIfPresent(Int32.self, forKey: .minDisplayDuration)
self.maxDisplayDuration = try container.decodeIfPresent(Int32.self, forKey: .maxDisplayDuration)
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
@ -144,6 +155,9 @@ private class AdMessagesHistoryContextImpl {
try container.encodeIfPresent(self.additionalInfo, forKey: .additionalInfo) try container.encodeIfPresent(self.additionalInfo, forKey: .additionalInfo)
try container.encode(self.canReport, forKey: .canReport) try container.encode(self.canReport, forKey: .canReport)
try container.encodeIfPresent(self.minDisplayDuration, forKey: .minDisplayDuration)
try container.encodeIfPresent(self.maxDisplayDuration, forKey: .maxDisplayDuration)
} }
public static func ==(lhs: CachedMessage, rhs: CachedMessage) -> Bool { public static func ==(lhs: CachedMessage, rhs: CachedMessage) -> Bool {
@ -193,6 +207,12 @@ private class AdMessagesHistoryContextImpl {
if lhs.canReport != rhs.canReport { if lhs.canReport != rhs.canReport {
return false return false
} }
if lhs.minDisplayDuration != rhs.minDisplayDuration {
return false
}
if lhs.maxDisplayDuration != rhs.maxDisplayDuration {
return false
}
return true return true
} }
@ -206,10 +226,22 @@ private class AdMessagesHistoryContextImpl {
case .recommended: case .recommended:
mappedMessageType = .recommended mappedMessageType = .recommended
} }
attributes.append(AdMessageAttribute(opaqueId: self.opaqueId, messageType: mappedMessageType, url: self.url, buttonText: self.buttonText, sponsorInfo: self.sponsorInfo, additionalInfo: self.additionalInfo, canReport: self.canReport, hasContentMedia: !self.contentMedia.isEmpty)) let adAttribute = AdMessageAttribute(
opaqueId: self.opaqueId,
messageType: mappedMessageType,
url: self.url,
buttonText: self.buttonText,
sponsorInfo: self.sponsorInfo,
additionalInfo: self.additionalInfo,
canReport: self.canReport,
hasContentMedia: !self.contentMedia.isEmpty,
minDisplayDuration: self.minDisplayDuration,
maxDisplayDuration: self.maxDisplayDuration
)
attributes.append(adAttribute)
if !self.textEntities.isEmpty { if !self.textEntities.isEmpty {
let attribute = TextEntitiesMessageAttribute(entities: self.textEntities) let entitiesAttribute = TextEntitiesMessageAttribute(entities: self.textEntities)
attributes.append(attribute) attributes.append(entitiesAttribute)
} }
var messagePeers = SimpleDictionary<PeerId, Peer>() var messagePeers = SimpleDictionary<PeerId, Peer>()
@ -282,7 +314,8 @@ private class AdMessagesHistoryContextImpl {
private let queue: Queue private let queue: Queue
private let account: Account private let account: Account
private let peerId: PeerId private let peerId: EnginePeer.Id
private let messageId: EngineMessage.Id?
private let maskAsSeenDisposables = DisposableDict<Data>() private let maskAsSeenDisposables = DisposableDict<Data>()
@ -369,12 +402,20 @@ private class AdMessagesHistoryContextImpl {
struct State: Equatable { struct State: Equatable {
var interPostInterval: Int32? var interPostInterval: Int32?
var startDelay: Int32?
var betweenDelay: Int32?
var messages: [Message] var messages: [Message]
static func ==(lhs: State, rhs: State) -> Bool { static func ==(lhs: State, rhs: State) -> Bool {
if lhs.interPostInterval != rhs.interPostInterval { if lhs.interPostInterval != rhs.interPostInterval {
return false return false
} }
if lhs.startDelay != rhs.startDelay {
return false
}
if lhs.betweenDelay != rhs.betweenDelay {
return false
}
if lhs.messages.count != rhs.messages.count { if lhs.messages.count != rhs.messages.count {
return false return false
} }
@ -401,15 +442,17 @@ private class AdMessagesHistoryContextImpl {
private let disposable = MetaDisposable() private let disposable = MetaDisposable()
init(queue: Queue, account: Account, peerId: PeerId) { init(queue: Queue, account: Account, peerId: EnginePeer.Id, messageId: EngineMessage.Id?) {
self.queue = queue self.queue = queue
self.account = account self.account = account
self.peerId = peerId self.peerId = peerId
self.messageId = messageId
let accountPeerId = account.peerId let accountPeerId = account.peerId
self.stateValue = State(interPostInterval: nil, messages: []) self.stateValue = State(interPostInterval: nil, messages: [])
if messageId == nil {
self.state.set(CachedState.getCached(postbox: account.postbox, peerId: peerId) self.state.set(CachedState.getCached(postbox: account.postbox, peerId: peerId)
|> mapToSignal { cachedState -> Signal<State, NoError> in |> mapToSignal { cachedState -> Signal<State, NoError> in
if let cachedState = cachedState, cachedState.timestamp >= Int32(Date().timeIntervalSince1970) - 5 * 60 { if let cachedState = cachedState, cachedState.timestamp >= Int32(Date().timeIntervalSince1970) - 5 * 60 {
@ -422,27 +465,32 @@ private class AdMessagesHistoryContextImpl {
return .single(State(interPostInterval: nil, messages: [])) return .single(State(interPostInterval: nil, messages: []))
} }
}) })
}
let signal: Signal<(interPostInterval: Int32?, messages: [Message]), NoError> = account.postbox.transaction { transaction -> Api.InputPeer? in let signal: Signal<(interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]), NoError> = account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer) return transaction.getPeer(peerId).flatMap(apiInputPeer)
} }
|> mapToSignal { inputPeer -> Signal<(interPostInterval: Int32?, messages: [Message]), NoError> in |> mapToSignal { inputPeer -> Signal<(interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]), NoError> in
guard let inputPeer else { guard let inputPeer else {
return .single((nil, [])) return .single((nil, nil, nil, []))
} }
return account.network.request(Api.functions.messages.getSponsoredMessages(peer: inputPeer)) var flags: Int32 = 0
if let _ = messageId {
flags |= (1 << 0)
}
return account.network.request(Api.functions.messages.getSponsoredMessages(flags: flags, peer: inputPeer, msgId: messageId?.id))
|> map(Optional.init) |> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.SponsoredMessages?, NoError> in |> `catch` { _ -> Signal<Api.messages.SponsoredMessages?, NoError> in
return .single(nil) return .single(nil)
} }
|> mapToSignal { result -> Signal<(interPostInterval: Int32?, messages: [Message]), NoError> in |> mapToSignal { result -> Signal<(interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]), NoError> in
guard let result = result else { guard let result = result else {
return .single((nil, [])) return .single((nil, nil, nil, []))
} }
return account.postbox.transaction { transaction -> (interPostInterval: Int32?, messages: [Message]) in return account.postbox.transaction { transaction -> (interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]) in
switch result { switch result {
case let .sponsoredMessages(_, postsBetween, messages, chats, users): case let .sponsoredMessages(_, postsBetween, startDelay, betweenDelay, messages, chats, users):
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
@ -450,7 +498,7 @@ private class AdMessagesHistoryContextImpl {
for message in messages { for message in messages {
switch message { switch message {
case let .sponsoredMessage(flags, randomId, url, title, message, entities, photo, media, color, buttonText, sponsorInfo, additionalInfo): case let .sponsoredMessage(flags, randomId, url, title, message, entities, photo, media, color, buttonText, sponsorInfo, additionalInfo, minDisplayDuration, maxDisplayDuration):
var parsedEntities: [MessageTextEntity] = [] var parsedEntities: [MessageTextEntity] = []
if let entities = entities { if let entities = entities {
parsedEntities = messageTextEntitiesFromApiEntities(entities) parsedEntities = messageTextEntitiesFromApiEntities(entities)
@ -486,29 +534,33 @@ private class AdMessagesHistoryContextImpl {
buttonText: buttonText, buttonText: buttonText,
sponsorInfo: sponsorInfo, sponsorInfo: sponsorInfo,
additionalInfo: additionalInfo, additionalInfo: additionalInfo,
canReport: canReport canReport: canReport,
minDisplayDuration: minDisplayDuration,
maxDisplayDuration: maxDisplayDuration
)) ))
} }
} }
if messageId == nil {
CachedState.setCached(transaction: transaction, peerId: peerId, state: CachedState(timestamp: Int32(Date().timeIntervalSince1970), interPostInterval: postsBetween, messages: parsedMessages)) CachedState.setCached(transaction: transaction, peerId: peerId, state: CachedState(timestamp: Int32(Date().timeIntervalSince1970), interPostInterval: postsBetween, messages: parsedMessages))
}
return (postsBetween, parsedMessages.compactMap { message -> Message? in return (postsBetween, startDelay, betweenDelay, parsedMessages.compactMap { message -> Message? in
return message.toMessage(peerId: peerId, transaction: transaction) return message.toMessage(peerId: peerId, transaction: transaction)
}) })
case .sponsoredMessagesEmpty: case .sponsoredMessagesEmpty:
return (nil, []) return (nil, nil, nil, [])
} }
} }
} }
} }
self.disposable.set((signal self.disposable.set((signal
|> deliverOn(self.queue)).start(next: { [weak self] interPostInterval, messages in |> deliverOn(self.queue)).start(next: { [weak self] interPostInterval, startDelay, betweenDelay, messages in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.stateValue = State(interPostInterval: interPostInterval, messages: messages) strongSelf.stateValue = State(interPostInterval: interPostInterval, startDelay: startDelay, betweenDelay: betweenDelay, messages: messages)
})) }))
} }
@ -560,14 +612,15 @@ public class AdMessagesHistoryContext {
private let queue = Queue() private let queue = Queue()
private let impl: QueueLocalObject<AdMessagesHistoryContextImpl> private let impl: QueueLocalObject<AdMessagesHistoryContextImpl>
public let peerId: EnginePeer.Id public let peerId: EnginePeer.Id
public let messageId: EngineMessage.Id?
public var state: Signal<(interPostInterval: Int32?, messages: [Message]), NoError> { public var state: Signal<(interPostInterval: Int32?, messages: [Message], startDelay: Int32?, betweenDelay: Int32?), NoError> {
return Signal { subscriber in return Signal { subscriber in
let disposable = MetaDisposable() let disposable = MetaDisposable()
self.impl.with { impl in self.impl.with { impl in
let stateDisposable = impl.state.get().start(next: { state in let stateDisposable = impl.state.get().start(next: { state in
subscriber.putNext((state.interPostInterval, state.messages)) subscriber.putNext((state.interPostInterval, state.messages, state.startDelay, state.betweenDelay))
}) })
disposable.set(stateDisposable) disposable.set(stateDisposable)
} }
@ -576,11 +629,13 @@ public class AdMessagesHistoryContext {
} }
} }
public init(account: Account, peerId: PeerId) { public init(account: Account, peerId: EnginePeer.Id, messageId: EngineMessage.Id? = nil) {
self.peerId = peerId self.peerId = peerId
self.messageId = messageId
let queue = self.queue let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: { self.impl = QueueLocalObject(queue: queue, generate: {
return AdMessagesHistoryContextImpl(queue: queue, account: account, peerId: peerId) return AdMessagesHistoryContextImpl(queue: queue, account: account, peerId: peerId, messageId: messageId)
}) })
} }

View File

@ -20,6 +20,7 @@ public enum EngineMedia: Equatable {
case giveaway(TelegramMediaGiveaway) case giveaway(TelegramMediaGiveaway)
case giveawayResults(TelegramMediaGiveawayResults) case giveawayResults(TelegramMediaGiveawayResults)
case paidContent(TelegramMediaPaidContent) case paidContent(TelegramMediaPaidContent)
case todo(TelegramMediaTodo)
} }
public extension EngineMedia { public extension EngineMedia {
@ -59,6 +60,8 @@ public extension EngineMedia {
return giveawayResults.id return giveawayResults.id
case let .paidContent(paidContent): case let .paidContent(paidContent):
return paidContent.id return paidContent.id
case .todo:
return nil
} }
} }
} }
@ -100,6 +103,8 @@ public extension EngineMedia {
self = .giveawayResults(giveawayResults) self = .giveawayResults(giveawayResults)
case let paidContent as TelegramMediaPaidContent: case let paidContent as TelegramMediaPaidContent:
self = .paidContent(paidContent) self = .paidContent(paidContent)
case let todo as TelegramMediaTodo:
self = .todo(todo)
default: default:
preconditionFailure() preconditionFailure()
} }
@ -141,6 +146,8 @@ public extension EngineMedia {
return giveawayResults return giveawayResults
case let .paidContent(paidContent): case let .paidContent(paidContent):
return paidContent return paidContent
case let .todo(todo):
return todo
} }
} }
} }

View File

@ -204,6 +204,14 @@ public extension TelegramEngine {
return PollResultsContext(account: self.account, messageId: messageId, poll: poll) return PollResultsContext(account: self.account, messageId: messageId, poll: poll)
} }
public func requestUpdateTodoMessageItems(messageId: MessageId, completedIds: [Int32], incompletedIds: [Int32]) -> Signal<Never, RequestUpdateTodoMessageError> {
return _internal_requestUpdateTodoMessageItems(account: self.account, messageId: messageId, completedIds: completedIds, incompletedIds: incompletedIds)
}
public func appendTodoMessageItems(messageId: MessageId, items: [TelegramMediaTodo.Item]) -> Signal<Never, AppendTodoMessageError> {
return _internal_appendTodoMessageItems(account: self.account, messageId: messageId, items: items)
}
public func earliestUnseenPersonalMentionMessage(peerId: PeerId, threadId: Int64?) -> Signal<EarliestUnseenPersonalMentionMessageResult, NoError> { public func earliestUnseenPersonalMentionMessage(peerId: PeerId, threadId: Int64?) -> Signal<EarliestUnseenPersonalMentionMessageResult, NoError> {
let account = self.account let account = self.account
return _internal_earliestUnseenPersonalMentionMessage(account: self.account, peerId: peerId, threadId: threadId) return _internal_earliestUnseenPersonalMentionMessage(account: self.account, peerId: peerId, threadId: threadId)
@ -407,8 +415,8 @@ public extension TelegramEngine {
} }
} }
public func adMessages(peerId: PeerId) -> AdMessagesHistoryContext { public func adMessages(peerId: PeerId, messageId: EngineMessage.Id? = nil) -> AdMessagesHistoryContext {
return AdMessagesHistoryContext(account: self.account, peerId: peerId) return AdMessagesHistoryContext(account: self.account, peerId: peerId, messageId: messageId)
} }
public func messageReadStats(id: MessageId) -> Signal<MessageReadStats?, NoError> { public func messageReadStats(id: MessageId) -> Signal<MessageReadStats?, NoError> {

View File

@ -0,0 +1,76 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import MtProtoKit
public enum RequestUpdateTodoMessageError {
case generic
}
func _internal_requestUpdateTodoMessageItems(account: Account, messageId: MessageId, completedIds: [Int32], incompletedIds: [Int32]) -> Signal<Never, RequestUpdateTodoMessageError> {
return account.postbox.transaction { transaction -> Signal<Never, RequestUpdateTodoMessageError> in
guard let peer = transaction.getPeer(messageId.peerId), let inputPeer = apiInputPeer(peer) else {
return .complete()
}
transaction.updateMessage(messageId, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var media: [Media] = []
if let todo = currentMessage.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo {
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
var updatedCompletions = todo.completions
for id in completedIds {
updatedCompletions.append(TelegramMediaTodo.Completion(id: id, date: timestamp, completedBy: account.peerId))
}
updatedCompletions.removeAll(where: { incompletedIds.contains($0.id) })
media = [todo.withUpdated(completions: updatedCompletions)]
}
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: media))
})
return account.network.request(Api.functions.messages.toggleTodoCompleted(peer: inputPeer, msgId: messageId.id, completed: completedIds, incompleted: incompletedIds))
|> mapError { _ -> RequestUpdateTodoMessageError in
return .generic
}
|> map { result in
account.stateManager.addUpdates(result)
}
|> ignoreValues
}
|> castError(RequestUpdateTodoMessageError.self)
|> switchToLatest
|> ignoreValues
}
public enum AppendTodoMessageError {
case generic
}
func _internal_appendTodoMessageItems(account: Account, messageId: MessageId, items: [TelegramMediaTodo.Item]) -> Signal<Never, AppendTodoMessageError> {
return account.postbox.transaction { transaction -> Signal<Never, AppendTodoMessageError> in
guard let peer = transaction.getPeer(messageId.peerId), let inputPeer = apiInputPeer(peer) else {
return .complete()
}
transaction.updateMessage(messageId, update: { currentMessage in
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var media: [Media] = []
if let todo = currentMessage.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo {
var updatedItems = todo.items
updatedItems.append(contentsOf: items)
media = [todo.withUpdated(items: updatedItems)]
}
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: media))
})
return account.network.request(Api.functions.messages.appendTodoList(peer: inputPeer, msgId: messageId.id, list: items.map { $0.apiItem }))
|> mapError { _ -> AppendTodoMessageError in
return .generic
}
|> map { result in
account.stateManager.addUpdates(result)
}
|> ignoreValues
}
|> castError(AppendTodoMessageError.self)
|> switchToLatest
|> ignoreValues
}

View File

@ -168,6 +168,11 @@ public enum PresentationResourceKey: Int32 {
case chatBubbleFileCloudFetchedIncomingIcon case chatBubbleFileCloudFetchedIncomingIcon
case chatBubbleFileCloudFetchedOutgoingIcon case chatBubbleFileCloudFetchedOutgoingIcon
case chatBubbleTodoDotIncomingIcon
case chatBubbleTodoDotOutgoingIcon
case chatBubbleTodoCheckIncomingIcon
case chatBubbleTodoCheckOutgoingIcon
case chatBubbleReplyThumbnailPlayImage case chatBubbleReplyThumbnailPlayImage
case chatBubbleDeliveryFailedIcon case chatBubbleDeliveryFailedIcon

View File

@ -1402,4 +1402,28 @@ public struct PresentationResourcesChat {
return nil return nil
}) })
} }
public static func chatBubbleTodoDotIncomingIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatBubbleTodoDotIncomingIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/TodoDot"), color: theme.chat.message.incoming.accentTextColor)
})
}
public static func chatBubbleTodoDotOutgoingIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatBubbleTodoDotOutgoingIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/TodoDot"), color: theme.chat.message.outgoing.accentTextColor)
})
}
public static func chatBubbleTodoCheckIncomingIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatBubbleTodoCheckIncomingIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/TodoCheck"), color: theme.chat.message.incoming.accentTextColor)
})
}
public static func chatBubbleTodoCheckOutgoingIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatBubbleTodoCheckOutgoingIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/TodoCheck"), color: theme.chat.message.outgoing.accentTextColor)
})
}
} }

View File

@ -1286,6 +1286,80 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
} }
attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes) attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes)
} }
case let .todoCompletions(completed, incompleted):
var todo: TelegramMediaTodo?
for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] {
for media in message.media {
if let media = media as? TelegramMediaTodo {
todo = media
}
}
}
}
if let todo {
if message.author?.id == accountPeerId {
let resultString: PresentationStrings.FormattedString
if let completedTaskId = completed.first, let completedTask = todo.items.first(where: { $0.id == completedTaskId }) {
resultString = strings.Notification_TodoCompletedYou(completedTask.text)
} else if let incompletedTaskId = incompleted.first, let incompletedTask = todo.items.first(where: { $0.id == incompletedTaskId }) {
resultString = strings.Notification_TodoIncompletedYou(incompletedTask.text)
} else {
fatalError()
}
attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
} else {
let peerName = message.author?.compactDisplayTitle ?? ""
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])
attributes[1] = boldAttributes
let resultString: PresentationStrings.FormattedString
if let completedTaskId = completed.first, let completedTask = todo.items.first(where: { $0.id == completedTaskId }) {
resultString = strings.Notification_TodoCompleted(peerName, completedTask.text)
} else if let incompletedTaskId = incompleted.first, let incompletedTask = todo.items.first(where: { $0.id == incompletedTaskId }) {
resultString = strings.Notification_TodoIncompleted(peerName, incompletedTask.text)
} else {
fatalError()
}
attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
} else {
attributedString = NSAttributedString(string: ".")
}
case let .todoAppendTasks(tasks):
var todoTitle = ""
for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] {
for media in message.media {
if let todo = media as? TelegramMediaTodo {
todoTitle = todo.text
}
}
}
}
if message.author?.id == accountPeerId {
let resultString: PresentationStrings.FormattedString
if tasks.count == 1, let task = tasks.first {
resultString = strings.Notification_TodoAddedTaskYou(task.text, todoTitle)
} else {
resultString = strings.Notification_TodoAddedMultipleTasksYou(strings.Notification_TodoTasks(Int32(tasks.count)), todoTitle)
}
attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes])
} else {
let peerName = message.author?.compactDisplayTitle ?? ""
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])
attributes[1] = boldAttributes
attributes[2] = boldAttributes
let resultString: PresentationStrings.FormattedString
if tasks.count == 1, let task = tasks.first {
resultString = strings.Notification_TodoAddedTask(peerName, task.text, todoTitle)
} else {
resultString = strings.Notification_TodoAddedMultipleTasks(peerName, strings.Notification_TodoTasks(Int32(tasks.count)), todoTitle)
}
attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
case .unknown: case .unknown:
attributedString = nil attributedString = nil
} }

View File

@ -481,6 +481,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel", "//submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel",
"//submodules/TelegramUI/Components/GifVideoLayer", "//submodules/TelegramUI/Components/GifVideoLayer",
"//submodules/TelegramUI/Components/BatchVideoRendering", "//submodules/TelegramUI/Components/BatchVideoRendering",
"//submodules/TelegramUI/Components/ComposeTodoScreen",
] + select({ ] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [], "//build-system:ios_sim_arm64": [],

View File

@ -51,6 +51,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItem", "//submodules/TelegramUI/Components/Chat/ChatMessageItem",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemView", "//submodules/TelegramUI/Components/Chat/ChatMessageItemView",
"//submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode", "//submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode",

View File

@ -41,6 +41,7 @@ import ChatMessageInteractiveFileNode
import ChatMessageFileBubbleContentNode import ChatMessageFileBubbleContentNode
import ChatMessageWebpageBubbleContentNode import ChatMessageWebpageBubbleContentNode
import ChatMessagePollBubbleContentNode import ChatMessagePollBubbleContentNode
import ChatMessageTodoBubbleContentNode
import ChatMessageItem import ChatMessageItem
import ChatMessageItemView import ChatMessageItemView
import ChatMessageSwipeToReplyNode import ChatMessageSwipeToReplyNode
@ -269,6 +270,9 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
} else if let _ = media as? TelegramMediaPoll { } else if let _ = media as? TelegramMediaPoll {
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 {
result.append((message, ChatMessageTodoBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
} else if let _ = media as? TelegramMediaGiveaway { } else if let _ = media as? TelegramMediaGiveaway {
result.append((message, ChatMessageGiveawayBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) result.append((message, ChatMessageGiveawayBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false needReactions = false

View File

@ -449,10 +449,12 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
self.buttonNode.highligthedChanged = { [weak self] highlighted in self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self { if let strongSelf = self {
if highlighted { if highlighted {
if "".isEmpty, let contentNode = strongSelf.supernode as? ChatMessagePollBubbleContentNode, let backdropNode = contentNode.bubbleBackgroundNode?.backdropNode { if let theme = strongSelf.theme, theme.overallDarkAppearance, let contentNode = strongSelf.supernode as? ChatMessagePollBubbleContentNode, let backdropNode = contentNode.bubbleBackgroundNode?.backdropNode {
strongSelf.highlightedBackgroundNode.layer.compositingFilter = "overlayBlendMode" strongSelf.highlightedBackgroundNode.layer.compositingFilter = "overlayBlendMode"
strongSelf.highlightedBackgroundNode.frame = strongSelf.view.convert(strongSelf.highlightedBackgroundNode.frame, to: backdropNode.view) strongSelf.highlightedBackgroundNode.frame = strongSelf.view.convert(strongSelf.highlightedBackgroundNode.frame, to: backdropNode.view)
backdropNode.addSubnode(strongSelf.highlightedBackgroundNode) backdropNode.addSubnode(strongSelf.highlightedBackgroundNode)
} else {
strongSelf.insertSubnode(strongSelf.highlightedBackgroundNode, at: 0)
} }
strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity") strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity")

View File

@ -0,0 +1,35 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageTodoBubbleContentNode",
module_name = "ChatMessageTodoBubbleContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/Postbox",
"//submodules/TextFormat",
"//submodules/UrlEscaping",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AccountContext",
"//submodules/AvatarNode",
"//submodules/TelegramPresentationData",
"//submodules/ChatMessageBackground",
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/PollBubbleTimerNode",
"//submodules/TelegramUI/Components/Chat/MergedAvatarsNode",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/Chat/ShimmeringLinkNode",
],
visibility = [
"//visibility:public",
],
)

View File

@ -170,6 +170,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
}, openBoostToUnrestrict: { }, openBoostToUnrestrict: {
}, updateRecordingTrimRange: { _, _, _, _ in }, updateRecordingTrimRange: { _, _, _, _ in
}, dismissAllTooltips: { }, dismissAllTooltips: {
}, editTodoMessage: { _, _ in
}, updateHistoryFilter: { _ in }, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in }, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: { }, toggleChatSidebarMode: {

View File

@ -647,6 +647,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, playShakeAnimation: { }, playShakeAnimation: {
}, displayQuickShare: { _, _ ,_ in }, displayQuickShare: { _, _ ,_ in
}, updateChatLocationThread: { _, _ in }, updateChatLocationThread: { _, _ in
}, requestToggleTodoMessageItem: { _, _, _ in
}, displayTodoToggleUnavailable: { _ in
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode)) pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode))
self.controllerInteraction = controllerInteraction self.controllerInteraction = controllerInteraction

View File

@ -502,6 +502,8 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
}, playShakeAnimation: { }, playShakeAnimation: {
}, displayQuickShare: { _, _ ,_ in }, displayQuickShare: { _, _ ,_ in
}, updateChatLocationThread: { _, _ in }, updateChatLocationThread: { _, _ in
}, requestToggleTodoMessageItem: { _, _, _ in
}, displayTodoToggleUnavailable: { _ in
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: self.context, backgroundNode: self.wallpaperBackgroundNode)) pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: self.context, backgroundNode: self.wallpaperBackgroundNode))

View File

@ -281,7 +281,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
public let playShakeAnimation: () -> Void public let playShakeAnimation: () -> Void
public let displayQuickShare: (MessageId, ASDisplayNode, ContextGesture) -> Void public let displayQuickShare: (MessageId, ASDisplayNode, ContextGesture) -> Void
public let updateChatLocationThread: (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void public let updateChatLocationThread: (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void
public let requestToggleTodoMessageItem: (MessageId, Int32, Bool) -> Void
public let displayTodoToggleUnavailable: (MessageId) -> Void
public var canPlayMedia: Bool = false public var canPlayMedia: Bool = false
public var hiddenMedia: [MessageId: [Media]] = [:] public var hiddenMedia: [MessageId: [Media]] = [:]
public var expandedTranslationMessageStableIds: Set<UInt32> = Set() public var expandedTranslationMessageStableIds: Set<UInt32> = Set()
@ -444,6 +445,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
playShakeAnimation: @escaping () -> Void, playShakeAnimation: @escaping () -> Void,
displayQuickShare: @escaping (MessageId, ASDisplayNode, ContextGesture) -> Void, displayQuickShare: @escaping (MessageId, ASDisplayNode, ContextGesture) -> Void,
updateChatLocationThread: @escaping (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void, updateChatLocationThread: @escaping (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void,
requestToggleTodoMessageItem: @escaping (MessageId, Int32, Bool) -> Void,
displayTodoToggleUnavailable: @escaping (MessageId) -> Void,
automaticMediaDownloadSettings: MediaAutoDownloadSettings, automaticMediaDownloadSettings: MediaAutoDownloadSettings,
pollActionState: ChatInterfacePollActionState, pollActionState: ChatInterfacePollActionState,
stickerSettings: ChatInterfaceStickerSettings, stickerSettings: ChatInterfaceStickerSettings,
@ -563,6 +566,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
self.playShakeAnimation = playShakeAnimation self.playShakeAnimation = playShakeAnimation
self.displayQuickShare = displayQuickShare self.displayQuickShare = displayQuickShare
self.updateChatLocationThread = updateChatLocationThread self.updateChatLocationThread = updateChatLocationThread
self.requestToggleTodoMessageItem = requestToggleTodoMessageItem
self.displayTodoToggleUnavailable = displayTodoToggleUnavailable
self.automaticMediaDownloadSettings = automaticMediaDownloadSettings self.automaticMediaDownloadSettings = automaticMediaDownloadSettings

View File

@ -0,0 +1,51 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ComposeTodoScreen",
module_name = "ComposeTodoScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/ItemListUI",
"//submodules/AccountContext",
"//submodules/AlertUI",
"//submodules/PresentationDataUtils",
"//submodules/TextFormat",
"//submodules/ObjCRuntimeUtils",
"//submodules/AttachmentUI",
"//submodules/TextInputMenu",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/AppBundle",
"//submodules/TelegramUI/Components/EntityKeyboard",
"//submodules/UndoUI",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/PeerAllowedReactionsScreen",
"//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
"//submodules/TelegramUI/Components/ListComposePollOptionComponent",
"//submodules/ComposePollUI",
"//submodules/Markdown",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,30 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ListComposePollOptionComponent",
module_name = "ListComposePollOptionComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/SliderComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/CheckNode",
"//submodules/AccountContext",
"//submodules/PresentationDataUtils",
],
visibility = [
"//visibility:public",
],
)

View File

@ -73,6 +73,7 @@ public final class ListComposePollOptionComponent: Component {
public let theme: PresentationTheme public let theme: PresentationTheme
public let strings: PresentationStrings public let strings: PresentationStrings
public let placeholder: NSAttributedString? public let placeholder: NSAttributedString?
public let isEnabled: Bool
public let resetText: ResetText? public let resetText: ResetText?
public let assumeIsEditing: Bool public let assumeIsEditing: Bool
public let characterLimit: Int? public let characterLimit: Int?
@ -92,6 +93,7 @@ public final class ListComposePollOptionComponent: Component {
theme: PresentationTheme, theme: PresentationTheme,
strings: PresentationStrings, strings: PresentationStrings,
placeholder: NSAttributedString? = nil, placeholder: NSAttributedString? = nil,
isEnabled: Bool = true,
resetText: ResetText? = nil, resetText: ResetText? = nil,
assumeIsEditing: Bool = false, assumeIsEditing: Bool = false,
characterLimit: Int, characterLimit: Int,
@ -110,6 +112,7 @@ public final class ListComposePollOptionComponent: Component {
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.placeholder = placeholder self.placeholder = placeholder
self.isEnabled = isEnabled
self.resetText = resetText self.resetText = resetText
self.assumeIsEditing = assumeIsEditing self.assumeIsEditing = assumeIsEditing
self.characterLimit = characterLimit self.characterLimit = characterLimit
@ -140,6 +143,9 @@ public final class ListComposePollOptionComponent: Component {
if lhs.placeholder != rhs.placeholder { if lhs.placeholder != rhs.placeholder {
return false return false
} }
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.resetText != rhs.resetText { if lhs.resetText != rhs.resetText {
return false return false
} }
@ -400,6 +406,9 @@ public final class ListComposePollOptionComponent: Component {
self.textField.parentState = state self.textField.parentState = state
} }
transition.setFrame(view: textFieldView, frame: textFieldFrame) transition.setFrame(view: textFieldView, frame: textFieldFrame)
transition.setAlpha(view: textFieldView, alpha: component.isEnabled ? 1.0 : 0.3)
textFieldView.isUserInteractionEnabled = component.isEnabled
} }
if let selection = component.selection { if let selection = component.selection {

View File

@ -76,6 +76,7 @@ public final class ListSectionContentView: UIView {
private let contentItemContainerView: UIView private let contentItemContainerView: UIView
public let externalContentBackgroundView: DynamicCornerRadiusView public let externalContentBackgroundView: DynamicCornerRadiusView
public var automaticallyLayoutExternalContentBackgroundView = true
public var itemViews: [AnyHashable: ItemView] = [:] public var itemViews: [AnyHashable: ItemView] = [:]
private var highlightedItemId: AnyHashable? private var highlightedItemId: AnyHashable?
@ -283,7 +284,9 @@ public final class ListSectionContentView: UIView {
} }
self.externalContentBackgroundView.update(size: backgroundFrame.size, corners: corners, transition: transition) self.externalContentBackgroundView.update(size: backgroundFrame.size, corners: corners, transition: transition)
} }
if self.automaticallyLayoutExternalContentBackgroundView {
transition.setFrame(view: self.externalContentBackgroundView, frame: backgroundFrame) transition.setFrame(view: self.externalContentBackgroundView, frame: backgroundFrame)
}
transition.setAlpha(view: self.externalContentBackgroundView, alpha: backgroundAlpha) transition.setAlpha(view: self.externalContentBackgroundView, alpha: backgroundAlpha)
transition.setCornerRadius(layer: self.layer, cornerRadius: contentCornerRadius) transition.setCornerRadius(layer: self.layer, cornerRadius: contentCornerRadius)

View File

@ -435,6 +435,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
}, openBoostToUnrestrict: { }, openBoostToUnrestrict: {
}, updateRecordingTrimRange: { _, _, _, _ in }, updateRecordingTrimRange: { _, _, _, _ in
}, dismissAllTooltips: { }, dismissAllTooltips: {
}, editTodoMessage: { _, _ in
}, updateHistoryFilter: { _ in }, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in }, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: { }, toggleChatSidebarMode: {
@ -3847,6 +3848,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, playShakeAnimation: { }, playShakeAnimation: {
}, displayQuickShare: { _, _ ,_ in }, displayQuickShare: { _, _ ,_ in
}, updateChatLocationThread: { _, _ in }, updateChatLocationThread: { _, _ in
}, requestToggleTodoMessageItem: { _, _, _ in
}, displayTodoToggleUnavailable: { _ in
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in

View File

@ -823,6 +823,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}, openBoostToUnrestrict: { }, openBoostToUnrestrict: {
}, updateRecordingTrimRange: { _, _, _, _ in }, updateRecordingTrimRange: { _, _, _, _ in
}, dismissAllTooltips: { }, dismissAllTooltips: {
}, editTodoMessage: { _, _ in
}, updateHistoryFilter: { _ in }, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in }, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: { }, toggleChatSidebarMode: {

View File

@ -835,11 +835,21 @@ private final class StarsTransactionSheetContent: CombinedComponent {
) )
)) ))
} else if case .unique = giftAnimationSubject { } else if case .unique = giftAnimationSubject {
let reason: String
if count < StarsAmount.zero, case let .transaction(transaction, _) = subject {
if transaction.flags.contains(.isStarGiftResale) {
reason = strings.Stars_Transaction_GiftPurchase
} else {
reason = strings.Stars_Transaction_GiftTransfer
}
} else {
reason = strings.Stars_Transaction_GiftSale
}
tableItems.append(.init( tableItems.append(.init(
id: "reason", id: "reason",
title: strings.Stars_Transaction_Giveaway_Reason, title: strings.Stars_Transaction_Giveaway_Reason,
component: AnyComponent( component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: count < StarsAmount.zero ? strings.Stars_Transaction_GiftPurchase : strings.Stars_Transaction_GiftSale, font: tableFont, textColor: tableTextColor))) MultilineTextComponent(text: .plain(NSAttributedString(string: reason, font: tableFont, textColor: tableTextColor)))
) )
)) ))
} }

View File

@ -328,7 +328,15 @@ final class StarsTransactionsListPanelComponent: Component {
break break
} }
} }
itemSubtitle = item.count > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_GiftSale : environment.strings.Stars_Intro_Transaction_GiftPurchase if item.count > StarsAmount.zero {
itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftSale
} else {
if item.flags.contains(.isStarGiftResale) {
itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftPurchase
} else {
itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftTransfer
}
}
} }
} }
} else if let _ = item.giveawayMessageId { } else if let _ = item.giveawayMessageId {

View File

@ -9,17 +9,20 @@ public final class ToastContentComponent: Component {
public let content: AnyComponent<Empty> public let content: AnyComponent<Empty>
public let insets: UIEdgeInsets public let insets: UIEdgeInsets
public let iconSpacing: CGFloat public let iconSpacing: CGFloat
public let action: (() -> Void)?
public init( public init(
icon: AnyComponent<Empty>, icon: AnyComponent<Empty>,
content: AnyComponent<Empty>, content: AnyComponent<Empty>,
insets: UIEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0), insets: UIEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0),
iconSpacing: CGFloat = 10.0 iconSpacing: CGFloat = 10.0,
action: (() -> Void)? = nil
) { ) {
self.icon = icon self.icon = icon
self.content = content self.content = content
self.insets = insets self.insets = insets
self.iconSpacing = iconSpacing self.iconSpacing = iconSpacing
self.action = action
} }
public static func ==(lhs: ToastContentComponent, rhs: ToastContentComponent) -> Bool { public static func ==(lhs: ToastContentComponent, rhs: ToastContentComponent) -> Bool {
@ -39,6 +42,8 @@ public final class ToastContentComponent: Component {
} }
public final class View: UIView { public final class View: UIView {
private var component: ToastContentComponent?
private let backgroundView: BlurredBackgroundView private let backgroundView: BlurredBackgroundView
private let icon = ComponentView<Empty>() private let icon = ComponentView<Empty>()
private let content = ComponentView<Empty>() private let content = ComponentView<Empty>()
@ -63,9 +68,21 @@ public final class ToastContentComponent: Component {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
@objc private func tapped() {
self.component?.action?()
}
func update(component: ToastContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize { func update(component: ToastContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
var contentHeight: CGFloat = 0.0 var contentHeight: CGFloat = 0.0
if self.component == nil {
if let _ = component.action {
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped)))
}
}
self.component = component
let leftInset: CGFloat = component.insets.left let leftInset: CGFloat = component.insets.left
let rightInset: CGFloat = component.insets.right let rightInset: CGFloat = component.insets.right
let topInset: CGFloat = component.insets.top let topInset: CGFloat = component.insets.top

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "todolist_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "todo_check.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "todo_dot.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

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
@ -4148,6 +4147,11 @@ extension ChatControllerImpl {
return return
} }
self.dismissAllTooltips() self.dismissAllTooltips()
}, editTodoMessage: { [weak self] messageId, append in
guard let self else {
return
}
self.openTodoEditing(messageId: messageId, append: append)
}, updateHistoryFilter: { [weak self] update in }, updateHistoryFilter: { [weak self] update in
guard let self else { guard let self else {
return return

View File

@ -22,6 +22,7 @@ import ShareController
import UrlEscaping import UrlEscaping
import ContextUI import ContextUI
import ComposePollUI import ComposePollUI
import ComposeTodoScreen
import AlertUI import AlertUI
import PresentationDataUtils import PresentationDataUtils
import UndoUI import UndoUI
@ -342,6 +343,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var selectMessagePollOptionDisposables: DisposableDict<MessageId>? var selectMessagePollOptionDisposables: DisposableDict<MessageId>?
var selectPollOptionFeedback: HapticFeedback? var selectPollOptionFeedback: HapticFeedback?
var updateMessageTodoDisposables: DisposableDict<MessageId>?
var resolveUrlDisposable: MetaDisposable? var resolveUrlDisposable: MetaDisposable?
var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:] var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:]
@ -4789,6 +4792,68 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return direction ? .right : .left return direction ? .right : .left
} }
self.updateChatLocationThread(threadId: threadId, animationDirection: animationDirection ?? defaultDirection) self.updateChatLocationThread(threadId: threadId, animationDirection: animationDirection ?? defaultDirection)
}, requestToggleTodoMessageItem: { [weak self] messageId, itemId, value in
guard let self else {
return
}
let disposables: DisposableDict<MessageId>
if let current = self.updateMessageTodoDisposables {
disposables = current
} else {
disposables = DisposableDict()
self.updateMessageTodoDisposables = disposables
}
var completedIds: [Int32] = []
var incompletedIds: [Int32] = []
if value {
completedIds.append(itemId)
} else {
incompletedIds.append(itemId)
}
let signal = self.context.engine.messages.requestUpdateTodoMessageItems(messageId: messageId, completedIds: completedIds, incompletedIds: incompletedIds)
disposables.set((signal
|> deliverOnMainQueue).startStrict(next: { todo in
}, error: { _ in
}), forKey: messageId)
}, displayTodoToggleUnavailable: { [weak self] messageId in
guard let self else {
return
}
self.dismissAllUndoControllers()
//TODO:localize
if !self.context.isPremium {
let controller = UndoOverlayController(
presentationData: self.presentationData,
content: .premiumPaywall(title: nil, text: "Only [Telegram Premium]() subscribers can mark tasks as done.", customUndoText: nil, timeout: nil, linkAction: nil),
action: { [weak self] action in
guard let self else {
return false
}
if case .info = action {
let controller = self.context.sharedContext.makePremiumIntroController(context: context, source: .presence, forceDark: false, dismissed: nil)
self.push(controller)
}
return false
}
)
self.present(controller, in: .current)
} else if let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
var peerName = ""
if let author = message.author {
peerName = EnginePeer(author).compactDisplayTitle
}
let controller = UndoOverlayController(
presentationData: self.presentationData,
content: .universalImage(image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/Lock"), color: .white)!, size: nil, title: nil, text: "\(peerName) has restricted others from editing this to do list.", customUndoText: nil, timeout: nil),
action: { _ in
return false
}
)
self.present(controller, in: .current)
}
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode)) }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode))
controllerInteraction.enableFullTranslucency = context.sharedContext.energyUsageSettings.fullTranslucency controllerInteraction.enableFullTranslucency = context.sharedContext.energyUsageSettings.fullTranslucency
@ -5845,6 +5910,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.updateChatLocationThreadDisposable?.dispose() self.updateChatLocationThreadDisposable?.dispose()
self.accountPeerDisposable?.dispose() self.accountPeerDisposable?.dispose()
self.contentDataDisposable?.dispose() self.contentDataDisposable?.dispose()
self.updateMessageTodoDisposables?.dispose()
} }
public func updatePresentationMode(_ mode: ChatControllerPresentationMode) { public func updatePresentationMode(_ mode: ChatControllerPresentationMode) {
@ -7577,56 +7643,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.present(tooltipScreen, in: .current) self.present(tooltipScreen, in: .current)
} }
func configurePollCreation(isQuiz: Bool? = nil) -> ViewController? {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return nil
}
return createPollController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), isQuiz: isQuiz, completion: { [weak self] poll in
guard let strongSelf = self else {
return
}
strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in
guard let strongSelf = self else {
return
}
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.chatDisplayNode.collapseInput()
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) }
})
}
}, nil)
let message: EnqueueMessage = .message(
text: "",
attributes: [],
inlineStickers: [:],
mediaReference: .standalone(media: TelegramMediaPoll(
pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: Int64.random(in: Int64.min ... Int64.max)),
publicity: poll.publicity,
kind: poll.kind,
text: poll.text.string,
textEntities: poll.text.entities,
options: poll.options,
correctAnswers: poll.correctAnswers,
results: poll.results,
isClosed: false,
deadlineTimeout: poll.deadlineTimeout
)),
threadId: strongSelf.chatLocation.threadId,
replyToMessageId: nil,
replyToStoryId: nil,
localGroupingKey: nil,
correlationId: nil,
bubbleUpEmojiOrStickersets: []
)
strongSelf.sendMessages([message.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel)])
})
})
}
func transformEnqueueMessages(_ messages: [EnqueueMessage], postpone: Bool = false) -> [EnqueueMessage] { func transformEnqueueMessages(_ messages: [EnqueueMessage], postpone: Bool = false) -> [EnqueueMessage] {
let silentPosting = self.presentationInterfaceState.interfaceState.silentPosting let silentPosting = self.presentationInterfaceState.interfaceState.silentPosting
return transformEnqueueMessages(messages, silentPosting: silentPosting, postpone: postpone) return transformEnqueueMessages(messages, silentPosting: silentPosting, postpone: postpone)

View File

@ -33,6 +33,8 @@ import AutomaticBusinessMessageSetupScreen
import MediaEditorScreen import MediaEditorScreen
import CameraScreen import CameraScreen
import ShareController import ShareController
import ComposeTodoScreen
import ComposePollUI
extension ChatControllerImpl { extension ChatControllerImpl {
enum AttachMenuSubject { enum AttachMenuSubject {
@ -113,6 +115,8 @@ extension ChatControllerImpl {
availableButtons.insert(.poll, at: max(0, availableButtons.count - 1)) availableButtons.insert(.poll, at: max(0, availableButtons.count - 1))
} }
availableButtons.append(.todo)
let presentationData = self.presentationData let presentationData = self.presentationData
var isScheduledMessages = false var isScheduledMessages = false
@ -622,6 +626,26 @@ extension ChatControllerImpl {
completion(controller, controller.mediaPickerContext) completion(controller, controller.mediaPickerContext)
strongSelf.controllerNavigationDisposable.set(nil) strongSelf.controllerNavigationDisposable.set(nil)
} }
case .todo:
if strongSelf.context.isPremium {
if let controller = strongSelf.configureTodoCreation() as? AttachmentContainable {
completion(controller, controller.mediaPickerContext)
strongSelf.controllerNavigationDisposable.set(nil)
}
} else {
var replaceImpl: ((ViewController) -> Void)?
let demoController = strongSelf.context.sharedContext.makePremiumDemoController(context: strongSelf.context, subject: .todo, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .todo, forceDark: false, dismissed: nil)
replaceImpl?(controller)
}, dismissed: nil)
replaceImpl = { [weak demoController] c in
demoController?.replace(with: c)
}
strongSelf.push(demoController)
Queue.mainQueue().after(0.4) {
strongSelf.attachmentController?.dismiss(animated: false)
}
}
case .gift: case .gift:
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer, let starsContext = context.starsContext { if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer, let starsContext = context.starsContext {
let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions
@ -1967,4 +1991,147 @@ extension ChatControllerImpl {
mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.push(mainController) self.push(mainController)
} }
func configurePollCreation(isQuiz: Bool? = nil) -> ViewController? {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return nil
}
return ComposePollScreen(
context: self.context,
initialData: ComposePollScreen.initialData(context: self.context),
peer: EnginePeer(peer),
isQuiz: isQuiz,
completion: { [weak self] poll in
guard let self else {
return
}
self.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in
guard let self else {
return
}
let replyMessageSubject = self.presentationInterfaceState.interfaceState.replyMessageSubject
self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in
if let self {
self.chatDisplayNode.collapseInput()
self.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) }
})
}
}, nil)
let message: EnqueueMessage = .message(
text: "",
attributes: [],
inlineStickers: [:],
mediaReference: .standalone(media: TelegramMediaPoll(
pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: Int64.random(in: Int64.min...Int64.max)),
publicity: poll.publicity,
kind: poll.kind,
text: poll.text.string,
textEntities: poll.text.entities,
options: poll.options,
correctAnswers: poll.correctAnswers,
results: poll.results,
isClosed: false,
deadlineTimeout: poll.deadlineTimeout
)),
threadId: self.chatLocation.threadId,
replyToMessageId: nil,
replyToStoryId: nil,
localGroupingKey: nil,
correlationId: nil,
bubbleUpEmojiOrStickersets: []
)
self.sendMessages([message.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel)])
})
}
)
}
func configureTodoCreation() -> ViewController? {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return nil
}
return ComposeTodoScreen(
context: self.context,
initialData: ComposeTodoScreen.initialData(
context: self.context
),
peer: EnginePeer(peer),
completion: { [weak self] todo in
guard let self else {
return
}
self.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in
guard let self else {
return
}
let replyMessageSubject = self.presentationInterfaceState.interfaceState.replyMessageSubject
self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in
if let self {
self.chatDisplayNode.collapseInput()
self.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) }
})
}
}, nil)
let message: EnqueueMessage = .message(
text: "",
attributes: [],
inlineStickers: [:],
mediaReference: .standalone(media: todo),
threadId: self.chatLocation.threadId,
replyToMessageId: nil,
replyToStoryId: nil,
localGroupingKey: nil,
correlationId: nil,
bubbleUpEmojiOrStickersets: []
)
self.sendMessages([message.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel)])
})
}
)
}
func openTodoEditing(messageId: EngineMessage.Id, append: Bool) {
guard let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId), let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
guard let existingTodo = message.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo else {
return
}
let canEdit = canEditMessage(context: self.context, limitsConfiguration: self.context.currentLimitsConfiguration.with { EngineConfiguration.Limits($0) }, message: message)
let controller = ComposeTodoScreen(
context: self.context,
initialData: ComposeTodoScreen.initialData(
context: self.context,
existingTodo: existingTodo,
append: append,
canEdit: canEdit
),
peer: EnginePeer(peer),
completion: { [weak self] todo in
guard let self else {
return
}
if canEdit {
let _ = self.context.engine.messages.requestEditMessage(
messageId: messageId,
text: "",
media: .update(.standalone(media: todo)),
entities: nil,
inlineStickers: [:]
).start()
} else {
let appendedItems = Array(todo.items[existingTodo.items.count ..< todo.items.count])
let _ = self.context.engine.messages.appendTodoMessageItems(messageId: messageId, items: appendedItems).start()
}
}
)
controller.navigationPresentation = .modal
self.push(controller)
}
} }

View File

@ -791,22 +791,22 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
self.prefetchManager = InChatPrefetchManager(context: context) self.prefetchManager = InChatPrefetchManager(context: context)
self.adMessagesContext = adMessagesContext self.adMessagesContext = adMessagesContext
var adMessages: Signal<(interPostInterval: Int32?, messages: [Message]), NoError> var adMessages: Signal<(interPostInterval: Int32?, messages: [Message], startDelay: Int32?, betweenDelay: Int32?), NoError>
if case .bubbles = mode, let adMessagesContext { if case .bubbles = mode, let adMessagesContext {
let peerId = adMessagesContext.peerId let peerId = adMessagesContext.peerId
if peerId.namespace == Namespaces.Peer.CloudUser { if peerId.namespace == Namespaces.Peer.CloudUser {
adMessages = .single((nil, [])) adMessages = .single((nil, [], nil, nil))
} else { } else {
if context.sharedContext.immediateExperimentalUISettings.fakeAds { if context.sharedContext.immediateExperimentalUISettings.fakeAds {
adMessages = context.engine.data.get( adMessages = context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
) )
|> map { peer -> (interPostInterval: Int32?, messages: [Message]) in |> map { peer -> (interPostInterval: Int32?, messages: [Message], startDelay: Int32?, betweenDelay: Int32?) in
let fakeAdMessages: [Message] = (0 ..< 10).map { i -> Message in let fakeAdMessages: [Message] = (0 ..< 10).map { i -> Message in
var attributes: [MessageAttribute] = [] var attributes: [MessageAttribute] = []
let mappedMessageType: AdMessageAttribute.MessageType = .sponsored let mappedMessageType: AdMessageAttribute.MessageType = .sponsored
attributes.append(AdMessageAttribute(opaqueId: "fake_ad_\(i)".data(using: .utf8)!, messageType: mappedMessageType, url: "t.me/telegram", buttonText: "VIEW", sponsorInfo: nil, additionalInfo: nil, canReport: false, hasContentMedia: false)) attributes.append(AdMessageAttribute(opaqueId: "fake_ad_\(i)".data(using: .utf8)!, messageType: mappedMessageType, url: "t.me/telegram", buttonText: "VIEW", sponsorInfo: nil, additionalInfo: nil, canReport: false, hasContentMedia: false, minDisplayDuration: nil, maxDisplayDuration: nil))
var messagePeers = SimpleDictionary<PeerId, Peer>() var messagePeers = SimpleDictionary<PeerId, Peer>()
@ -875,14 +875,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
associatedStories: [:] associatedStories: [:]
) )
} }
return (10, fakeAdMessages) return (10, fakeAdMessages, nil, nil)
} }
} else { } else {
adMessages = adMessagesContext.state adMessages = adMessagesContext.state
} }
} }
} else { } else {
adMessages = .single((nil, [])) adMessages = .single((nil, [], nil, nil))
} }
let clientId = Atomic<Int32>(value: nextClientId) let clientId = Atomic<Int32>(value: nextClientId)
@ -1224,9 +1224,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
self.beginChatHistoryTransitions(resetScrolling: true, switchedToAnotherSource: false) self.beginChatHistoryTransitions(resetScrolling: true, switchedToAnotherSource: false)
} }
private func beginAdMessageManagement(adMessages: Signal<(interPostInterval: Int32?, messages: [Message]), NoError>) { private func beginAdMessageManagement(adMessages: Signal<(interPostInterval: Int32?, messages: [Message], startDelay: Int32?, betweenDelay: Int32?), NoError>) {
self.adMessagesDisposable = (adMessages self.adMessagesDisposable = (adMessages
|> deliverOnMainQueue).startStrict(next: { [weak self] interPostInterval, messages in |> deliverOnMainQueue).startStrict(next: { [weak self] interPostInterval, messages, _, _ in
guard let self else { guard let self else {
return return
} }

View File

@ -1479,25 +1479,33 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
isMigrated = false isMigrated = false
} }
if data.canEdit && !isPinnedMessages && !isMigrated {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
}, action: { c, f in
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
f(.custom(transition))
})
})))
}
var activePoll: TelegramMediaPoll? var activePoll: TelegramMediaPoll?
var activeTodo: TelegramMediaTodo?
for media in message.media { for media in message.media {
if let poll = media as? TelegramMediaPoll, !poll.isClosed, message.id.namespace == Namespaces.Message.Cloud, poll.pollId.namespace == Namespaces.Media.CloudPoll { if let poll = media as? TelegramMediaPoll, !poll.isClosed, message.id.namespace == Namespaces.Message.Cloud, poll.pollId.namespace == Namespaces.Media.CloudPoll {
if !isPollEffectivelyClosed(message: message, poll: poll) { if !isPollEffectivelyClosed(message: message, poll: poll) {
activePoll = poll activePoll = poll
} }
} else if let todo = media as? TelegramMediaTodo {
activeTodo = todo
} }
} }
if data.canEdit && !isPinnedMessages && !isMigrated {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
}, action: { c, f in
if let _ = activeTodo {
interfaceInteraction.editTodoMessage(messages[0].id, false)
f(.dismissWithoutContent)
} else {
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
f(.custom(transition))
})
}
})))
}
if let activePoll = activePoll, let voters = activePoll.results.voters { if let activePoll = activePoll, let voters = activePoll.results.voters {
var hasSelected = false var hasSelected = false
for result in voters { for result in voters {
@ -1515,6 +1523,21 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
} }
} }
if let activeTodo {
var canAppend = false
if message.author?.id == context.account.peerId || activeTodo.flags.contains(.othersCanAppend) {
canAppend = true
}
if canAppend {
actions.append(.action(ContextMenuActionItem(text: "Add a Task", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddCircle"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
interfaceInteraction.editTodoMessage(messages[0].id, true)
f(.dismissWithoutContent)
})))
}
}
var canPin = data.canPin var canPin = data.canPin
if case let .replyThread(message) = chatPresentationInterfaceState.chatLocation { if case let .replyThread(message) = chatPresentationInterfaceState.chatLocation {
if !message.isForumPost { if !message.isForumPost {

View File

@ -194,6 +194,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}, playShakeAnimation: { }, playShakeAnimation: {
}, displayQuickShare: { _, _ ,_ in }, displayQuickShare: { _, _ ,_ in
}, updateChatLocationThread: { _, _ in }, updateChatLocationThread: { _, _ in
}, requestToggleTodoMessageItem: { _, _, _ in
}, displayTodoToggleUnavailable: { _ in
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
self.dimNode = ASDisplayNode() self.dimNode = ASDisplayNode()

View File

@ -2400,6 +2400,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, playShakeAnimation: { }, playShakeAnimation: {
}, displayQuickShare: { _, _ ,_ in }, displayQuickShare: { _, _ ,_ in
}, updateChatLocationThread: { _, _ in }, updateChatLocationThread: { _, _ in
}, requestToggleTodoMessageItem: { _, _, _ in
}, displayTodoToggleUnavailable: { _ in
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode)) pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode))
@ -2715,6 +2717,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mappedSource = .animatedEmoji mappedSource = .animatedEmoji
case .paidMessages: case .paidMessages:
mappedSource = .paidMessages mappedSource = .paidMessages
case .todo:
mappedSource = .paidMessages
case let .auth(price): case let .auth(price):
mappedSource = .auth(price) mappedSource = .auth(price)
} }
@ -2793,6 +2797,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mappedSubject = .messageEffects mappedSubject = .messageEffects
case .paidMessages: case .paidMessages:
mappedSubject = .paidMessages mappedSubject = .paidMessages
case .todo:
mappedSubject = .todo
case .business: case .business:
mappedSubject = .business mappedSubject = .business
buttonText = presentationData.strings.Chat_EmptyStateIntroFooterPremiumActionButton buttonText = presentationData.strings.Chat_EmptyStateIntroFooterPremiumActionButton