mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-07-17 08:41:10 +00:00
Merge branch 'todo'
This commit is contained in:
commit
3fd4154ddd
@ -14422,3 +14422,19 @@ Sorry for the inconvenience.";
|
||||
"Chat.TitleJoinGroupCall" = "Join";
|
||||
"Invitation.JoinGroupCall.EnableMicrophone" = "Switch on the microphone";
|
||||
"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";
|
||||
|
@ -43,6 +43,7 @@ public enum PremiumIntroSource {
|
||||
case animatedEmoji
|
||||
case messageEffects
|
||||
case paidMessages
|
||||
case todo
|
||||
case auth(String)
|
||||
}
|
||||
|
||||
@ -82,6 +83,7 @@ public enum PremiumDemoSubject {
|
||||
case business
|
||||
case messageEffects
|
||||
case paidMessages
|
||||
case todo
|
||||
|
||||
case businessLocation
|
||||
case businessHours
|
||||
|
@ -21,6 +21,7 @@ public enum AttachmentButtonType: Equatable {
|
||||
case gallery
|
||||
case file
|
||||
case location
|
||||
case todo
|
||||
case quickReply
|
||||
case contact
|
||||
case poll
|
||||
@ -36,6 +37,8 @@ public enum AttachmentButtonType: Equatable {
|
||||
return "file"
|
||||
case .location:
|
||||
return "location"
|
||||
case .todo:
|
||||
return "todo"
|
||||
case .quickReply:
|
||||
return "quickReply"
|
||||
case .contact:
|
||||
@ -71,6 +74,12 @@ public enum AttachmentButtonType: Equatable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .todo:
|
||||
if case .todo = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .quickReply:
|
||||
if case .quickReply = rhs {
|
||||
return true
|
||||
|
@ -195,6 +195,9 @@ private final class AttachButtonComponent: CombinedComponent {
|
||||
case .location:
|
||||
name = strings.Attachment_Location
|
||||
imageName = "Chat/Attach Menu/Location"
|
||||
case .todo:
|
||||
name = "To Do List"
|
||||
imageName = "Chat/Attach Menu/Todo"
|
||||
case .contact:
|
||||
name = strings.Attachment_Contact
|
||||
imageName = "Chat/Attach Menu/Contact"
|
||||
@ -1267,6 +1270,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
||||
}, openBoostToUnrestrict: {
|
||||
}, updateRecordingTrimRange: { _, _, _, _ in
|
||||
}, dismissAllTooltips: {
|
||||
}, editTodoMessage: { _, _ in
|
||||
}, updateHistoryFilter: { _ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, toggleChatSidebarMode: {
|
||||
@ -1490,6 +1494,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
||||
accessibilityTitle = self.presentationData.strings.Attachment_File
|
||||
case .location:
|
||||
accessibilityTitle = self.presentationData.strings.Attachment_Location
|
||||
case .todo:
|
||||
//TODO:localize
|
||||
accessibilityTitle = "To Do List"
|
||||
case .contact:
|
||||
accessibilityTitle = self.presentationData.strings.Attachment_Contact
|
||||
case .poll:
|
||||
|
@ -333,7 +333,8 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P
|
||||
if let cutoutRect {
|
||||
context.setBlendMode(.copy)
|
||||
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?
|
||||
|
@ -176,6 +176,8 @@ public final class BrowserBookmarksScreen: ViewController {
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, requestToggleTodoMessageItem: { _, _, _ in
|
||||
}, displayTodoToggleUnavailable: { _ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||
|
||||
|
||||
|
@ -108,7 +108,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
|
||||
"//submodules/TelegramUI/Components/TextFieldComponent",
|
||||
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
|
||||
"//submodules/ComposePollUI",
|
||||
"//submodules/TelegramUI/Components/ListComposePollOptionComponent",
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
|
@ -20,7 +20,7 @@ import AsyncDisplayKit
|
||||
import UndoUI
|
||||
import PeerNameColorItem
|
||||
import EntityKeyboard
|
||||
import ComposePollUI
|
||||
import ListComposePollOptionComponent
|
||||
import ChatEntityKeyboardInputNode
|
||||
import ComponentFlow
|
||||
import ChatPresentationInterfaceState
|
||||
|
@ -8,7 +8,7 @@ import TextNodeWithEntities
|
||||
import AccountContext
|
||||
import ItemListUI
|
||||
import ComponentFlow
|
||||
import ComposePollUI
|
||||
import ListComposePollOptionComponent
|
||||
import TextFieldComponent
|
||||
|
||||
public class ItemListFilterTitleInputItem: ListViewItem, ItemListItem {
|
||||
|
@ -423,6 +423,20 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
||||
if messageText.isEmpty, case let .Loaded(content) = webpage.content {
|
||||
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:
|
||||
break
|
||||
}
|
||||
|
@ -177,6 +177,7 @@ public final class ChatPanelInterfaceInteraction {
|
||||
public let openBoostToUnrestrict: () -> Void
|
||||
public let updateRecordingTrimRange: (Double, Double, Bool, Bool) -> Void
|
||||
public let dismissAllTooltips: () -> Void
|
||||
public let editTodoMessage: (MessageId, Bool) -> Void
|
||||
public let requestLayout: (ContainedViewLayoutTransition) -> Void
|
||||
public let chatController: () -> ViewController?
|
||||
public let statuses: ChatPanelInterfaceInteractionStatuses?
|
||||
@ -293,6 +294,7 @@ public final class ChatPanelInterfaceInteraction {
|
||||
openBoostToUnrestrict: @escaping () -> Void,
|
||||
updateRecordingTrimRange: @escaping (Double, Double, Bool, Bool) -> Void,
|
||||
dismissAllTooltips: @escaping () -> Void,
|
||||
editTodoMessage: @escaping (MessageId, Bool) -> Void,
|
||||
updateHistoryFilter: @escaping ((ChatPresentationInterfaceState.HistoryFilter?) -> ChatPresentationInterfaceState.HistoryFilter?) -> Void,
|
||||
updateChatLocationThread: @escaping (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void,
|
||||
toggleChatSidebarMode: @escaping () -> Void,
|
||||
@ -412,6 +414,7 @@ public final class ChatPanelInterfaceInteraction {
|
||||
self.openBoostToUnrestrict = openBoostToUnrestrict
|
||||
self.updateRecordingTrimRange = updateRecordingTrimRange
|
||||
self.dismissAllTooltips = dismissAllTooltips
|
||||
self.editTodoMessage = editTodoMessage
|
||||
self.updateHistoryFilter = updateHistoryFilter
|
||||
self.updateChatLocationThread = updateChatLocationThread
|
||||
self.toggleChatSidebarMode = toggleChatSidebarMode
|
||||
@ -540,6 +543,7 @@ public final class ChatPanelInterfaceInteraction {
|
||||
}, openBoostToUnrestrict: {
|
||||
}, updateRecordingTrimRange: { _, _, _, _ in
|
||||
}, dismissAllTooltips: {
|
||||
}, editTodoMessage: { _, _ in
|
||||
}, updateHistoryFilter: { _ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, toggleChatSidebarMode: {
|
||||
|
@ -41,6 +41,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
|
||||
"//submodules/TelegramUI/Components/ListComposePollOptionComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -26,6 +26,49 @@ import ChatPresentationInterfaceState
|
||||
import EmojiSuggestionsComponent
|
||||
import TextFormat
|
||||
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 {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -1585,7 +1628,7 @@ public class ComposePollScreen: ViewControllerComponentContainer, AttachmentCont
|
||||
maxPollAnwsersCount = Int(value)
|
||||
}
|
||||
return InitialData(
|
||||
maxPollTextLength: Int(200),
|
||||
maxPollTextLength: 200,
|
||||
maxPollOptionLength: 100,
|
||||
maxPollAnwsersCount: maxPollAnwsersCount
|
||||
)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -60,9 +60,12 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/AnimatedTextComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramUI/Components/ToastComponent",
|
||||
"//submodules/SemanticStatusNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -33,6 +33,9 @@ import RasterizedCompositionComponent
|
||||
import BadgeComponent
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
import ToastComponent
|
||||
import MultilineTextComponent
|
||||
import BundleIconComponent
|
||||
|
||||
public enum UniversalVideoGalleryItemContentInfo {
|
||||
case message(Message, Int?)
|
||||
@ -1390,8 +1393,94 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
if 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) {
|
||||
if self.item?.content.id != item.content.id {
|
||||
var chapters = parseMediaPlayerChapters(item.caption)
|
||||
|
@ -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
|
||||
var items: [DemoPagerComponent.Item] = []
|
||||
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
|
||||
case .paidMessages:
|
||||
text = strings.Premium_PaidMessagesInfo
|
||||
case .todo:
|
||||
//TODO:localize
|
||||
text = "Plan, assign and complete tasks – seamlessly and efficiently."
|
||||
default:
|
||||
text = ""
|
||||
}
|
||||
@ -1302,6 +1325,8 @@ private final class DemoSheetContent: CombinedComponent {
|
||||
buttonAnimationName = "premium_unlock"
|
||||
case .paidMessages:
|
||||
buttonText = strings.Premium_PaidMessages_Proceed
|
||||
case .todo:
|
||||
buttonText = strings.Premium_PaidMessages_Proceed
|
||||
default:
|
||||
buttonText = strings.Common_OK
|
||||
}
|
||||
@ -1492,6 +1517,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
|
||||
case folderTags
|
||||
case messageEffects
|
||||
case paidMessages
|
||||
case todo
|
||||
|
||||
case businessLocation
|
||||
case businessHours
|
||||
@ -1552,6 +1578,8 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
|
||||
return .messageEffects
|
||||
case .paidMessages:
|
||||
return .paidMessages
|
||||
case .todo:
|
||||
return .todo
|
||||
case .businessLocation:
|
||||
return .businessLocation
|
||||
case .businessHours:
|
||||
|
@ -309,6 +309,12 @@ public enum PremiumSource: Equatable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .todo:
|
||||
if case .todo = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .auth(lhsPrice):
|
||||
if case let .auth(rhsPrice) = rhs, lhsPrice == rhsPrice {
|
||||
return true
|
||||
@ -363,6 +369,7 @@ public enum PremiumSource: Equatable {
|
||||
case folderTags
|
||||
case messageEffects
|
||||
case paidMessages
|
||||
case todo
|
||||
case auth(String)
|
||||
|
||||
var identifier: String? {
|
||||
@ -459,6 +466,8 @@ public enum PremiumSource: Equatable {
|
||||
return "effects"
|
||||
case .paidMessages:
|
||||
return "paid_messages"
|
||||
case .todo:
|
||||
return "todo"
|
||||
case .auth:
|
||||
return "auth"
|
||||
}
|
||||
@ -490,6 +499,7 @@ public enum PremiumPerk: CaseIterable {
|
||||
case folderTags
|
||||
case messageEffects
|
||||
case paidMessages
|
||||
case todo
|
||||
|
||||
case businessLocation
|
||||
case businessHours
|
||||
@ -601,6 +611,8 @@ public enum PremiumPerk: CaseIterable {
|
||||
return "effects"
|
||||
case .paidMessages:
|
||||
return "paid_messages"
|
||||
case .todo:
|
||||
return "todo"
|
||||
case .business:
|
||||
return "business"
|
||||
case .businessLocation:
|
||||
@ -672,6 +684,9 @@ public enum PremiumPerk: CaseIterable {
|
||||
return strings.Premium_MessageEffects
|
||||
case .paidMessages:
|
||||
return strings.Premium_PaidMessages
|
||||
case .todo:
|
||||
//TODO:localize
|
||||
return "To-Do Lists"
|
||||
case .businessLocation:
|
||||
return strings.Business_Location
|
||||
case .businessHours:
|
||||
@ -741,6 +756,9 @@ public enum PremiumPerk: CaseIterable {
|
||||
return strings.Premium_MessageEffectsInfo
|
||||
case .paidMessages:
|
||||
return strings.Premium_PaidMessagesInfo
|
||||
case .todo:
|
||||
//TODO:localize
|
||||
return "Plan, assign and complete tasks – seamlessly and efficiently."
|
||||
case .businessLocation:
|
||||
return strings.Business_LocationInfo
|
||||
case .businessHours:
|
||||
@ -810,6 +828,8 @@ public enum PremiumPerk: CaseIterable {
|
||||
return "Premium/Perk/MessageEffects"
|
||||
case .paidMessages:
|
||||
return "Premium/Perk/PaidMessages"
|
||||
case .todo:
|
||||
return "Premium/Perk/PaidMessages"
|
||||
case .businessLocation:
|
||||
return "Premium/BusinessPerk/Location"
|
||||
case .businessHours:
|
||||
|
@ -410,6 +410,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-440664550] = { return Api.InputMedia.parse_inputMediaPhotoExternal($0) }
|
||||
dict[261416433] = { return Api.InputMedia.parse_inputMediaPoll($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[505969924] = { return Api.InputMedia.parse_inputMediaUploadedPhoto($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[-356721331] = { return Api.Message.parse_message($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[-988359047] = { return Api.MessageAction.parse_messageActionBotAllowed($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[775611918] = { return Api.MessageAction.parse_messageActionStarGiftUnique($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[-1064024032] = { return Api.MessageAction.parse_messageActionTopicEdit($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[1272375192] = { return Api.MessageMedia.parse_messageMediaPoll($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[784356159] = { return Api.MessageMedia.parse_messageMediaVenue($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[-1239335713] = { return Api.ShippingOption.parse_shippingOption($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[-963180333] = { return Api.SponsoredPeer.parse_sponsoredPeer($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[-94849324] = { return Api.ThemeSettings.parse_themeSettings($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[-39945236] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsApp($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[-1802240206] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedFile($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[1846886166] = { return Api.messages.StickerSet.parse_stickerSet($0) }
|
||||
dict[-738646805] = { return Api.messages.StickerSet.parse_stickerSetNotModified($0) }
|
||||
@ -2203,6 +2210,12 @@ public extension Api {
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.Timezone:
|
||||
_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:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.TopPeerCategory:
|
||||
|
@ -572,6 +572,7 @@ public extension Api {
|
||||
case inputMediaPhotoExternal(flags: Int32, url: String, ttlSeconds: Int32?)
|
||||
case inputMediaPoll(flags: Int32, poll: Api.Poll, correctAnswers: [Buffer]?, solution: String?, solutionEntities: [Api.MessageEntity]?)
|
||||
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 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)
|
||||
@ -712,6 +713,12 @@ public extension Api {
|
||||
peer.serialize(buffer, true)
|
||||
serializeInt32(id, buffer: buffer, boxed: false)
|
||||
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):
|
||||
if boxed {
|
||||
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)])
|
||||
case .inputMediaStory(let peer, let id):
|
||||
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):
|
||||
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):
|
||||
@ -1098,6 +1107,19 @@ public extension Api {
|
||||
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? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
|
@ -62,7 +62,7 @@ public extension Api {
|
||||
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 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) {
|
||||
switch self {
|
||||
@ -117,14 +117,15 @@ public extension Api {
|
||||
serializeInt32(id, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 0) != 0 {peerId!.serialize(buffer, true)}
|
||||
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 {
|
||||
buffer.appendInt32(-741178048)
|
||||
buffer.appendInt32(2055212554)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeInt32(id, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 8) != 0 {fromId!.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)}
|
||||
serializeInt32(date, buffer: buffer, boxed: false)
|
||||
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)])
|
||||
case .messageEmpty(let flags, let id, let peerId):
|
||||
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):
|
||||
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)])
|
||||
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), ("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() {
|
||||
_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() {
|
||||
_5 = Api.parse(reader, signature: signature) as? Api.MessageReplyHeader
|
||||
_6 = Api.parse(reader, signature: signature) as? Api.MessageReplyHeader
|
||||
} }
|
||||
var _6: Int32?
|
||||
_6 = reader.readInt32()
|
||||
var _7: Api.MessageAction?
|
||||
var _7: Int32?
|
||||
_7 = reader.readInt32()
|
||||
var _8: Api.MessageAction?
|
||||
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() {
|
||||
_8 = Api.parse(reader, signature: signature) as? Api.MessageReactions
|
||||
_9 = Api.parse(reader, signature: signature) as? Api.MessageReactions
|
||||
} }
|
||||
var _9: Int32?
|
||||
if Int(_1!) & Int(1 << 25) != 0 {_9 = reader.readInt32() }
|
||||
var _10: Int32?
|
||||
if Int(_1!) & Int(1 << 25) != 0 {_10 = reader.readInt32() }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = (Int(_1!) & Int(1 << 8) == 0) || _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil
|
||||
let _c6 = _6 != nil
|
||||
let _c5 = (Int(_1!) & Int(1 << 28) == 0) || _5 != nil
|
||||
let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil
|
||||
let _c7 = _7 != nil
|
||||
let _c8 = (Int(_1!) & Int(1 << 20) == 0) || _8 != nil
|
||||
let _c9 = (Int(_1!) & Int(1 << 25) == 0) || _9 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 {
|
||||
return Api.Message.messageService(flags: _1!, id: _2!, fromId: _3, peerId: _4!, replyTo: _5, date: _6!, action: _7!, reactions: _8, ttlPeriod: _9)
|
||||
let _c8 = _8 != nil
|
||||
let _c9 = (Int(_1!) & Int(1 << 20) == 0) || _9 != nil
|
||||
let _c10 = (Int(_1!) & Int(1 << 25) == 0) || _10 != nil
|
||||
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 {
|
||||
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 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 messageActionTodoAppendTasks(list: [Api.TodoItem])
|
||||
case messageActionTodoCompletions(completed: [Int32], incompleted: [Int32])
|
||||
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 messageActionWebViewDataSent(text: String)
|
||||
@ -789,6 +797,31 @@ public extension Api {
|
||||
}
|
||||
photo.serialize(buffer, true)
|
||||
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):
|
||||
if boxed {
|
||||
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)])
|
||||
case .messageActionSuggestProfilePhoto(let photo):
|
||||
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):
|
||||
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):
|
||||
@ -1715,6 +1752,37 @@ public extension Api {
|
||||
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? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
|
@ -722,6 +722,7 @@ public extension Api {
|
||||
case messageMediaPhoto(flags: Int32, photo: Api.Photo?, ttlSeconds: Int32?)
|
||||
case messageMediaPoll(poll: Api.Poll, results: Api.PollResults)
|
||||
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 messageMediaVenue(geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String)
|
||||
case messageMediaWebPage(flags: Int32, webpage: Api.WebPage)
|
||||
@ -878,6 +879,18 @@ public extension Api {
|
||||
serializeInt32(id, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 0) != 0 {story!.serialize(buffer, true)}
|
||||
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:
|
||||
if boxed {
|
||||
buffer.appendInt32(-1618676578)
|
||||
@ -935,6 +948,8 @@ public extension Api {
|
||||
return ("messageMediaPoll", [("poll", poll as Any), ("results", results as Any)])
|
||||
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)])
|
||||
case .messageMediaToDo(let flags, let todo, let completions):
|
||||
return ("messageMediaToDo", [("flags", flags as Any), ("todo", todo as Any), ("completions", completions as Any)])
|
||||
case .messageMediaUnsupported:
|
||||
return ("messageMediaUnsupported", [])
|
||||
case .messageMediaVenue(let geo, let title, let address, let provider, let venueId, let venueType):
|
||||
@ -1262,6 +1277,27 @@ public extension Api {
|
||||
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? {
|
||||
return Api.MessageMedia.messageMediaUnsupported
|
||||
}
|
||||
|
@ -442,13 +442,13 @@ public extension Api {
|
||||
}
|
||||
public extension Api {
|
||||
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) {
|
||||
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 {
|
||||
buffer.appendInt32(1301522832)
|
||||
buffer.appendInt32(2109703795)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeBytes(randomId, buffer: buffer, boxed: false)
|
||||
@ -466,14 +466,16 @@ public extension Api {
|
||||
serializeString(buttonText, 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 << 15) != 0 {serializeInt32(minDisplayDuration!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 15) != 0 {serializeInt32(maxDisplayDuration!, buffer: buffer, boxed: false)}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
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):
|
||||
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)])
|
||||
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), ("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) }
|
||||
var _12: String?
|
||||
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 _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
@ -522,8 +528,10 @@ public extension Api {
|
||||
let _c10 = _10 != nil
|
||||
let _c11 = (Int(_1!) & Int(1 << 7) == 0) || _11 != nil
|
||||
let _c12 = (Int(_1!) & Int(1 << 8) == 0) || _12 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 {
|
||||
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 _c13 = (Int(_1!) & Int(1 << 15) == 0) || _13 != nil
|
||||
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 {
|
||||
return nil
|
||||
|
@ -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 {
|
||||
enum TopPeer: TypeConstructorDescription {
|
||||
case topPeer(peer: Api.Peer, rating: Double)
|
||||
|
@ -304,17 +304,19 @@ public extension Api.messages {
|
||||
}
|
||||
public extension Api.messages {
|
||||
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
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
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 {
|
||||
buffer.appendInt32(-907141753)
|
||||
buffer.appendInt32(-2464403)
|
||||
}
|
||||
serializeInt32(flags, 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(Int32(messages.count))
|
||||
for item in messages {
|
||||
@ -342,8 +344,8 @@ public extension Api.messages {
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .sponsoredMessages(let flags, let postsBetween, 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)])
|
||||
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), ("startDelay", startDelay as Any), ("betweenDelay", betweenDelay as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)])
|
||||
case .sponsoredMessagesEmpty:
|
||||
return ("sponsoredMessagesEmpty", [])
|
||||
}
|
||||
@ -354,25 +356,31 @@ public extension Api.messages {
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int32?
|
||||
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() {
|
||||
_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() {
|
||||
_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() {
|
||||
_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 _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
|
||||
let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil
|
||||
let _c5 = _5 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 {
|
||||
return Api.messages.SponsoredMessages.sponsoredMessages(flags: _1!, postsBetween: _2, messages: _3!, chats: _4!, users: _5!)
|
||||
let _c6 = _6 != nil
|
||||
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 {
|
||||
return nil
|
||||
|
@ -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 {
|
||||
static func cancelPasswordEmail() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
@ -666,11 +650,13 @@ 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()
|
||||
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)
|
||||
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)
|
||||
var result: Api.account.PaidMessagesRevenue?
|
||||
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 {
|
||||
static func toggleSponsoredMessages(enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
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 {
|
||||
static func checkChatInvite(hash: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.ChatInvite>) {
|
||||
let buffer = Buffer()
|
||||
@ -7065,11 +7089,13 @@ 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()
|
||||
buffer.appendInt32(-1680673735)
|
||||
buffer.appendInt32(1030547536)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
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)
|
||||
var result: Api.messages.SponsoredMessages?
|
||||
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 {
|
||||
static func transcribeAudio(peer: Api.InputPeer, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.TranscribedAudio>) {
|
||||
let buffer = Buffer()
|
||||
|
@ -233,6 +233,9 @@ private var declaredEncodables: Void = {
|
||||
declareEncodable(TelegramMediaPaidContent.self, f: { TelegramMediaPaidContent(decoder: $0) })
|
||||
declareEncodable(ReportDeliveryMessageAttribute.self, f: { ReportDeliveryMessageAttribute(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
|
||||
}()
|
||||
|
||||
|
@ -15,8 +15,10 @@ public final class AdMessageAttribute: MessageAttribute {
|
||||
public let additionalInfo: String?
|
||||
public let canReport: 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.messageType = messageType
|
||||
self.url = url
|
||||
@ -25,6 +27,8 @@ public final class AdMessageAttribute: MessageAttribute {
|
||||
self.additionalInfo = additionalInfo
|
||||
self.canReport = canReport
|
||||
self.hasContentMedia = hasContentMedia
|
||||
self.minDisplayDuration = minDisplayDuration
|
||||
self.maxDisplayDuration = maxDisplayDuration
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
|
@ -137,7 +137,7 @@ func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? {
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case let .messageService(_, _, _, chatPeerId, _, _, _, _, _):
|
||||
case let .messageService(_, _, _, chatPeerId, _, _, _, _, _, _):
|
||||
return chatPeerId.peerId
|
||||
}
|
||||
}
|
||||
@ -218,7 +218,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
|
||||
return result
|
||||
case .messageEmpty:
|
||||
return []
|
||||
case let .messageService(_, _, fromId, chatPeerId, _, _, action, _, _):
|
||||
case let .messageService(_, _, fromId, chatPeerId, savedPeerId, _, _, action, _, _):
|
||||
let peerId: PeerId = chatPeerId.peerId
|
||||
var result = [peerId]
|
||||
|
||||
@ -227,9 +227,12 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
|
||||
if resolvedFromId != peerId {
|
||||
result.append(resolvedFromId)
|
||||
}
|
||||
if let savedPeerId, resolvedFromId != savedPeerId.peerId {
|
||||
result.append(savedPeerId.peerId)
|
||||
}
|
||||
|
||||
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
|
||||
case let .messageActionChannelMigrateFrom(_, 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:
|
||||
break
|
||||
case let .messageService(_, id, _, chatPeerId, replyHeader, _, _, _, _):
|
||||
case let .messageService(_, id, _, chatPeerId, _, replyHeader, _, _, _, _):
|
||||
if let replyHeader = replyHeader {
|
||||
switch replyHeader {
|
||||
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)
|
||||
}
|
||||
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):
|
||||
return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil, nil, nil)
|
||||
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)
|
||||
case .messageEmpty:
|
||||
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 authorId: PeerId? = fromId?.peerId ?? chatPeerId.peerId
|
||||
|
||||
var attributes: [MessageAttribute] = []
|
||||
|
||||
var threadId: Int64?
|
||||
if let replyTo = replyTo {
|
||||
if let savedPeerId {
|
||||
threadId = savedPeerId.peerId.toInt64()
|
||||
} else if let replyTo = replyTo {
|
||||
var threadMessageId: MessageId?
|
||||
switch replyTo {
|
||||
case let .messageReplyHeader(innerFlags, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities, quoteOffset):
|
||||
|
@ -224,6 +224,10 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
|
||||
flags: mappedFlags,
|
||||
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) }))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
@ -438,7 +438,7 @@ public struct NetworkInitializationArguments {
|
||||
public let externalRecaptchaRequestVerification: (String, String) -> Signal<String?, NoError>
|
||||
public let autolockDeadine: Signal<Int32?, NoError>
|
||||
public let encryptionProvider: EncryptionProvider
|
||||
public let deviceModelName:String?
|
||||
public let deviceModelName: String?
|
||||
public let useBetaFeatures: Bool
|
||||
public let isICloudEnabled: Bool
|
||||
|
||||
|
@ -319,6 +319,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post
|
||||
if poll.deadlineTimeout != nil {
|
||||
pollFlags |= 1 << 4
|
||||
}
|
||||
|
||||
var mappedSolution: String?
|
||||
var mappedSolutionEntities: [Api.MessageEntity]?
|
||||
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)
|
||||
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputPoll, text), reuploadInfo: nil, cacheReferenceKey: nil)))
|
||||
} else if let media = media as? TelegramMediaDice {
|
||||
let inputDice = Api.InputMedia.inputMediaDice(emoticon: media.emoji)
|
||||
} else if let todo = 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 }))
|
||||
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)))
|
||||
} 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
|
||||
flags |= 1 << 2
|
||||
if let attribute = attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute {
|
||||
|
@ -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)
|
||||
}
|
||||
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)))
|
||||
|> then(uploadSignal)
|
||||
|> map { result -> PendingMessageUploadedContentResult? in
|
||||
@ -110,7 +121,7 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox,
|
||||
if text.isEmpty {
|
||||
for media in message.media {
|
||||
switch media {
|
||||
case _ as TelegramMediaImage, _ as TelegramMediaFile:
|
||||
case _ as TelegramMediaImage, _ as TelegramMediaFile, _ as TelegramMediaTodo:
|
||||
break
|
||||
default:
|
||||
if let _ = scheduleTime {
|
||||
|
@ -108,7 +108,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
|
||||
updatedTimestamp = date
|
||||
case .messageEmpty:
|
||||
break
|
||||
case let .messageService(_, _, _, _, _, date, _, _, _):
|
||||
case let .messageService(_, _, _, _, _, _, date, _, _, _):
|
||||
updatedTimestamp = date
|
||||
}
|
||||
} else {
|
||||
|
@ -11,7 +11,7 @@ func _internal_getPaidMessagesRevenue(account: Account, peerId: PeerId) -> Signa
|
||||
guard let inputUser else {
|
||||
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)
|
||||
|> `catch` { _ -> Signal<Api.account.PaidMessagesRevenue?, NoError> in
|
||||
return .single(nil)
|
||||
@ -40,7 +40,7 @@ func _internal_addNoPaidMessagesException(account: Account, peerId: PeerId, refu
|
||||
if refundCharged {
|
||||
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
|
||||
return .single(.boolFalse)
|
||||
} |> mapToSignal { _ in
|
||||
|
@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
|
||||
|
||||
public class Serialization: NSObject, MTSerialization {
|
||||
public func currentLayer() -> UInt {
|
||||
return 204
|
||||
return 205
|
||||
}
|
||||
|
||||
public func parseMessage(_ data: Data!) -> Any! {
|
||||
|
@ -108,7 +108,7 @@ extension Api.Message {
|
||||
return id
|
||||
case let .messageEmpty(_, id, _):
|
||||
return id
|
||||
case let .messageService(_, id, _, _, _, _, _, _, _):
|
||||
case let .messageService(_, id, _, _, _, _, _, _, _, _):
|
||||
return id
|
||||
}
|
||||
}
|
||||
@ -128,7 +128,7 @@ extension Api.Message {
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case let .messageService(_, id, _, chatPeerId, _, _, _, _, _):
|
||||
case let .messageService(_, id, _, chatPeerId, _, _, _, _, _, _):
|
||||
let peerId: PeerId = chatPeerId.peerId
|
||||
return MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id)
|
||||
}
|
||||
@ -141,7 +141,7 @@ extension Api.Message {
|
||||
return peerId
|
||||
case let .messageEmpty(_, _, peerId):
|
||||
return peerId?.peerId
|
||||
case let .messageService(_, _, _, chatPeerId, _, _, _, _, _):
|
||||
case let .messageService(_, _, _, chatPeerId, _, _, _, _, _, _):
|
||||
let peerId: PeerId = chatPeerId.peerId
|
||||
return peerId
|
||||
}
|
||||
@ -151,7 +151,7 @@ extension Api.Message {
|
||||
switch self {
|
||||
case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
return date
|
||||
case let .messageService(_, _, _, _, _, date, _, _, _):
|
||||
case let .messageService(_, _, _, _, _, _, date, _, _, _):
|
||||
return date
|
||||
case .messageEmpty:
|
||||
return nil
|
||||
|
@ -160,6 +160,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
||||
case paidMessagesRefunded(count: Int32, stars: Int64)
|
||||
case paidMessagesPriceEdited(stars: Int64, broadcastMessagesAllowed: Bool)
|
||||
case conferenceCall(ConferenceCall)
|
||||
case todoCompletions(completed: [Int32], incompleted: [Int32])
|
||||
case todoAppendTasks([TelegramMediaTodo.Item])
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
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)),
|
||||
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:
|
||||
self = .unknown
|
||||
}
|
||||
@ -698,6 +709,13 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
||||
}
|
||||
encoder.encodeInt32(conferenceCall.flags.rawValue, forKey: "flags")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -20,6 +20,8 @@ private class AdMessagesHistoryContextImpl {
|
||||
case sponsorInfo
|
||||
case additionalInfo
|
||||
case canReport
|
||||
case minDisplayDuration
|
||||
case maxDisplayDuration
|
||||
}
|
||||
|
||||
enum MessageType: Int32, Codable {
|
||||
@ -41,7 +43,9 @@ private class AdMessagesHistoryContextImpl {
|
||||
public let sponsorInfo: String?
|
||||
public let additionalInfo: String?
|
||||
public let canReport: Bool
|
||||
|
||||
public let minDisplayDuration: Int32?
|
||||
public let maxDisplayDuration: Int32?
|
||||
|
||||
public init(
|
||||
opaqueId: Data,
|
||||
messageType: MessageType,
|
||||
@ -56,7 +60,9 @@ private class AdMessagesHistoryContextImpl {
|
||||
buttonText: String,
|
||||
sponsorInfo: String?,
|
||||
additionalInfo: String?,
|
||||
canReport: Bool
|
||||
canReport: Bool,
|
||||
minDisplayDuration: Int32?,
|
||||
maxDisplayDuration: Int32?
|
||||
) {
|
||||
self.opaqueId = opaqueId
|
||||
self.messageType = messageType
|
||||
@ -72,6 +78,8 @@ private class AdMessagesHistoryContextImpl {
|
||||
self.sponsorInfo = sponsorInfo
|
||||
self.additionalInfo = additionalInfo
|
||||
self.canReport = canReport
|
||||
self.minDisplayDuration = minDisplayDuration
|
||||
self.maxDisplayDuration = maxDisplayDuration
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@ -109,6 +117,9 @@ private class AdMessagesHistoryContextImpl {
|
||||
self.additionalInfo = try container.decodeIfPresent(String.self, forKey: .additionalInfo)
|
||||
|
||||
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 {
|
||||
@ -144,6 +155,9 @@ private class AdMessagesHistoryContextImpl {
|
||||
try container.encodeIfPresent(self.additionalInfo, forKey: .additionalInfo)
|
||||
|
||||
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 {
|
||||
@ -193,6 +207,12 @@ private class AdMessagesHistoryContextImpl {
|
||||
if lhs.canReport != rhs.canReport {
|
||||
return false
|
||||
}
|
||||
if lhs.minDisplayDuration != rhs.minDisplayDuration {
|
||||
return false
|
||||
}
|
||||
if lhs.maxDisplayDuration != rhs.maxDisplayDuration {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -206,10 +226,22 @@ private class AdMessagesHistoryContextImpl {
|
||||
case .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 {
|
||||
let attribute = TextEntitiesMessageAttribute(entities: self.textEntities)
|
||||
attributes.append(attribute)
|
||||
let entitiesAttribute = TextEntitiesMessageAttribute(entities: self.textEntities)
|
||||
attributes.append(entitiesAttribute)
|
||||
}
|
||||
|
||||
var messagePeers = SimpleDictionary<PeerId, Peer>()
|
||||
@ -282,7 +314,8 @@ private class AdMessagesHistoryContextImpl {
|
||||
|
||||
private let queue: Queue
|
||||
private let account: Account
|
||||
private let peerId: PeerId
|
||||
private let peerId: EnginePeer.Id
|
||||
private let messageId: EngineMessage.Id?
|
||||
|
||||
private let maskAsSeenDisposables = DisposableDict<Data>()
|
||||
|
||||
@ -369,12 +402,20 @@ private class AdMessagesHistoryContextImpl {
|
||||
|
||||
struct State: Equatable {
|
||||
var interPostInterval: Int32?
|
||||
var startDelay: Int32?
|
||||
var betweenDelay: Int32?
|
||||
var messages: [Message]
|
||||
|
||||
static func ==(lhs: State, rhs: State) -> Bool {
|
||||
if lhs.interPostInterval != rhs.interPostInterval {
|
||||
return false
|
||||
}
|
||||
if lhs.startDelay != rhs.startDelay {
|
||||
return false
|
||||
}
|
||||
if lhs.betweenDelay != rhs.betweenDelay {
|
||||
return false
|
||||
}
|
||||
if lhs.messages.count != rhs.messages.count {
|
||||
return false
|
||||
}
|
||||
@ -401,48 +442,55 @@ private class AdMessagesHistoryContextImpl {
|
||||
|
||||
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.account = account
|
||||
self.peerId = peerId
|
||||
self.messageId = messageId
|
||||
|
||||
let accountPeerId = account.peerId
|
||||
|
||||
self.stateValue = State(interPostInterval: nil, messages: [])
|
||||
|
||||
self.state.set(CachedState.getCached(postbox: account.postbox, peerId: peerId)
|
||||
|> mapToSignal { cachedState -> Signal<State, NoError> in
|
||||
if let cachedState = cachedState, cachedState.timestamp >= Int32(Date().timeIntervalSince1970) - 5 * 60 {
|
||||
return account.postbox.transaction { transaction -> State in
|
||||
return State(interPostInterval: cachedState.interPostInterval, messages: cachedState.messages.compactMap { message -> Message? in
|
||||
return message.toMessage(peerId: peerId, transaction: transaction)
|
||||
})
|
||||
if messageId == nil {
|
||||
self.state.set(CachedState.getCached(postbox: account.postbox, peerId: peerId)
|
||||
|> mapToSignal { cachedState -> Signal<State, NoError> in
|
||||
if let cachedState = cachedState, cachedState.timestamp >= Int32(Date().timeIntervalSince1970) - 5 * 60 {
|
||||
return account.postbox.transaction { transaction -> State in
|
||||
return State(interPostInterval: cachedState.interPostInterval, messages: cachedState.messages.compactMap { message -> Message? in
|
||||
return message.toMessage(peerId: peerId, transaction: transaction)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return .single(State(interPostInterval: nil, messages: []))
|
||||
}
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
|> 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 {
|
||||
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)
|
||||
|> `catch` { _ -> Signal<Api.messages.SponsoredMessages?, NoError> in
|
||||
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 {
|
||||
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 {
|
||||
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)
|
||||
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
|
||||
|
||||
@ -450,7 +498,7 @@ private class AdMessagesHistoryContextImpl {
|
||||
|
||||
for message in messages {
|
||||
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] = []
|
||||
if let entities = entities {
|
||||
parsedEntities = messageTextEntitiesFromApiEntities(entities)
|
||||
@ -486,29 +534,33 @@ private class AdMessagesHistoryContextImpl {
|
||||
buttonText: buttonText,
|
||||
sponsorInfo: sponsorInfo,
|
||||
additionalInfo: additionalInfo,
|
||||
canReport: canReport
|
||||
canReport: canReport,
|
||||
minDisplayDuration: minDisplayDuration,
|
||||
maxDisplayDuration: maxDisplayDuration
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
CachedState.setCached(transaction: transaction, peerId: peerId, state: CachedState(timestamp: Int32(Date().timeIntervalSince1970), interPostInterval: postsBetween, messages: parsedMessages))
|
||||
|
||||
return (postsBetween, parsedMessages.compactMap { message -> Message? in
|
||||
if messageId == nil {
|
||||
CachedState.setCached(transaction: transaction, peerId: peerId, state: CachedState(timestamp: Int32(Date().timeIntervalSince1970), interPostInterval: postsBetween, messages: parsedMessages))
|
||||
}
|
||||
|
||||
return (postsBetween, startDelay, betweenDelay, parsedMessages.compactMap { message -> Message? in
|
||||
return message.toMessage(peerId: peerId, transaction: transaction)
|
||||
})
|
||||
case .sponsoredMessagesEmpty:
|
||||
return (nil, [])
|
||||
return (nil, nil, nil, [])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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 impl: QueueLocalObject<AdMessagesHistoryContextImpl>
|
||||
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
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
self.impl.with { impl 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)
|
||||
}
|
||||
@ -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.messageId = messageId
|
||||
|
||||
let queue = self.queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return AdMessagesHistoryContextImpl(queue: queue, account: account, peerId: peerId)
|
||||
return AdMessagesHistoryContextImpl(queue: queue, account: account, peerId: peerId, messageId: messageId)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ public enum EngineMedia: Equatable {
|
||||
case giveaway(TelegramMediaGiveaway)
|
||||
case giveawayResults(TelegramMediaGiveawayResults)
|
||||
case paidContent(TelegramMediaPaidContent)
|
||||
case todo(TelegramMediaTodo)
|
||||
}
|
||||
|
||||
public extension EngineMedia {
|
||||
@ -59,6 +60,8 @@ public extension EngineMedia {
|
||||
return giveawayResults.id
|
||||
case let .paidContent(paidContent):
|
||||
return paidContent.id
|
||||
case .todo:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -100,6 +103,8 @@ public extension EngineMedia {
|
||||
self = .giveawayResults(giveawayResults)
|
||||
case let paidContent as TelegramMediaPaidContent:
|
||||
self = .paidContent(paidContent)
|
||||
case let todo as TelegramMediaTodo:
|
||||
self = .todo(todo)
|
||||
default:
|
||||
preconditionFailure()
|
||||
}
|
||||
@ -141,6 +146,8 @@ public extension EngineMedia {
|
||||
return giveawayResults
|
||||
case let .paidContent(paidContent):
|
||||
return paidContent
|
||||
case let .todo(todo):
|
||||
return todo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -204,6 +204,14 @@ public extension TelegramEngine {
|
||||
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> {
|
||||
let account = self.account
|
||||
return _internal_earliestUnseenPersonalMentionMessage(account: self.account, peerId: peerId, threadId: threadId)
|
||||
@ -407,8 +415,8 @@ public extension TelegramEngine {
|
||||
}
|
||||
}
|
||||
|
||||
public func adMessages(peerId: PeerId) -> AdMessagesHistoryContext {
|
||||
return AdMessagesHistoryContext(account: self.account, peerId: peerId)
|
||||
public func adMessages(peerId: PeerId, messageId: EngineMessage.Id? = nil) -> AdMessagesHistoryContext {
|
||||
return AdMessagesHistoryContext(account: self.account, peerId: peerId, messageId: messageId)
|
||||
}
|
||||
|
||||
public func messageReadStats(id: MessageId) -> Signal<MessageReadStats?, NoError> {
|
||||
|
@ -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
|
||||
}
|
@ -168,6 +168,11 @@ public enum PresentationResourceKey: Int32 {
|
||||
case chatBubbleFileCloudFetchedIncomingIcon
|
||||
case chatBubbleFileCloudFetchedOutgoingIcon
|
||||
|
||||
case chatBubbleTodoDotIncomingIcon
|
||||
case chatBubbleTodoDotOutgoingIcon
|
||||
case chatBubbleTodoCheckIncomingIcon
|
||||
case chatBubbleTodoCheckOutgoingIcon
|
||||
|
||||
case chatBubbleReplyThumbnailPlayImage
|
||||
|
||||
case chatBubbleDeliveryFailedIcon
|
||||
|
@ -1402,4 +1402,28 @@ public struct PresentationResourcesChat {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1286,6 +1286,80 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
}
|
||||
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:
|
||||
attributedString = nil
|
||||
}
|
||||
|
@ -481,6 +481,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel",
|
||||
"//submodules/TelegramUI/Components/GifVideoLayer",
|
||||
"//submodules/TelegramUI/Components/BatchVideoRendering",
|
||||
"//submodules/TelegramUI/Components/ComposeTodoScreen",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
|
@ -51,6 +51,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageItem",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageItemView",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode",
|
||||
|
@ -41,6 +41,7 @@ import ChatMessageInteractiveFileNode
|
||||
import ChatMessageFileBubbleContentNode
|
||||
import ChatMessageWebpageBubbleContentNode
|
||||
import ChatMessagePollBubbleContentNode
|
||||
import ChatMessageTodoBubbleContentNode
|
||||
import ChatMessageItem
|
||||
import ChatMessageItemView
|
||||
import ChatMessageSwipeToReplyNode
|
||||
@ -269,6 +270,9 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
} else if let _ = media as? TelegramMediaPoll {
|
||||
result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
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 {
|
||||
result.append((message, ChatMessageGiveawayBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
|
@ -449,10 +449,12 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
|
||||
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
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.frame = strongSelf.view.convert(strongSelf.highlightedBackgroundNode.frame, to: backdropNode.view)
|
||||
backdropNode.addSubnode(strongSelf.highlightedBackgroundNode)
|
||||
} else {
|
||||
strongSelf.insertSubnode(strongSelf.highlightedBackgroundNode, at: 0)
|
||||
}
|
||||
|
||||
strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
File diff suppressed because it is too large
Load Diff
@ -170,6 +170,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
|
||||
}, openBoostToUnrestrict: {
|
||||
}, updateRecordingTrimRange: { _, _, _, _ in
|
||||
}, dismissAllTooltips: {
|
||||
}, editTodoMessage: { _, _ in
|
||||
}, updateHistoryFilter: { _ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, toggleChatSidebarMode: {
|
||||
|
@ -647,6 +647,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, requestToggleTodoMessageItem: { _, _, _ in
|
||||
}, displayTodoToggleUnavailable: { _ in
|
||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode))
|
||||
self.controllerInteraction = controllerInteraction
|
||||
|
@ -502,6 +502,8 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, requestToggleTodoMessageItem: { _, _, _ in
|
||||
}, displayTodoToggleUnavailable: { _ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: self.context, backgroundNode: self.wallpaperBackgroundNode))
|
||||
|
||||
|
@ -281,7 +281,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
public let playShakeAnimation: () -> Void
|
||||
public let displayQuickShare: (MessageId, ASDisplayNode, ContextGesture) -> 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 hiddenMedia: [MessageId: [Media]] = [:]
|
||||
public var expandedTranslationMessageStableIds: Set<UInt32> = Set()
|
||||
@ -444,6 +445,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
playShakeAnimation: @escaping () -> Void,
|
||||
displayQuickShare: @escaping (MessageId, ASDisplayNode, ContextGesture) -> Void,
|
||||
updateChatLocationThread: @escaping (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void,
|
||||
requestToggleTodoMessageItem: @escaping (MessageId, Int32, Bool) -> Void,
|
||||
displayTodoToggleUnavailable: @escaping (MessageId) -> Void,
|
||||
automaticMediaDownloadSettings: MediaAutoDownloadSettings,
|
||||
pollActionState: ChatInterfacePollActionState,
|
||||
stickerSettings: ChatInterfaceStickerSettings,
|
||||
@ -563,6 +566,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
self.playShakeAnimation = playShakeAnimation
|
||||
self.displayQuickShare = displayQuickShare
|
||||
self.updateChatLocationThread = updateChatLocationThread
|
||||
self.requestToggleTodoMessageItem = requestToggleTodoMessageItem
|
||||
self.displayTodoToggleUnavailable = displayTodoToggleUnavailable
|
||||
|
||||
self.automaticMediaDownloadSettings = automaticMediaDownloadSettings
|
||||
|
||||
|
51
submodules/TelegramUI/Components/ComposeTodoScreen/BUILD
Normal file
51
submodules/TelegramUI/Components/ComposeTodoScreen/BUILD
Normal 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",
|
||||
],
|
||||
)
|
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
],
|
||||
)
|
||||
|
@ -73,6 +73,7 @@ public final class ListComposePollOptionComponent: Component {
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let placeholder: NSAttributedString?
|
||||
public let isEnabled: Bool
|
||||
public let resetText: ResetText?
|
||||
public let assumeIsEditing: Bool
|
||||
public let characterLimit: Int?
|
||||
@ -92,6 +93,7 @@ public final class ListComposePollOptionComponent: Component {
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
placeholder: NSAttributedString? = nil,
|
||||
isEnabled: Bool = true,
|
||||
resetText: ResetText? = nil,
|
||||
assumeIsEditing: Bool = false,
|
||||
characterLimit: Int,
|
||||
@ -110,6 +112,7 @@ public final class ListComposePollOptionComponent: Component {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.placeholder = placeholder
|
||||
self.isEnabled = isEnabled
|
||||
self.resetText = resetText
|
||||
self.assumeIsEditing = assumeIsEditing
|
||||
self.characterLimit = characterLimit
|
||||
@ -140,6 +143,9 @@ public final class ListComposePollOptionComponent: Component {
|
||||
if lhs.placeholder != rhs.placeholder {
|
||||
return false
|
||||
}
|
||||
if lhs.isEnabled != rhs.isEnabled {
|
||||
return false
|
||||
}
|
||||
if lhs.resetText != rhs.resetText {
|
||||
return false
|
||||
}
|
||||
@ -400,6 +406,9 @@ public final class ListComposePollOptionComponent: Component {
|
||||
self.textField.parentState = state
|
||||
}
|
||||
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 {
|
@ -76,6 +76,7 @@ public final class ListSectionContentView: UIView {
|
||||
private let contentItemContainerView: UIView
|
||||
|
||||
public let externalContentBackgroundView: DynamicCornerRadiusView
|
||||
public var automaticallyLayoutExternalContentBackgroundView = true
|
||||
|
||||
public var itemViews: [AnyHashable: ItemView] = [:]
|
||||
private var highlightedItemId: AnyHashable?
|
||||
@ -283,7 +284,9 @@ public final class ListSectionContentView: UIView {
|
||||
}
|
||||
self.externalContentBackgroundView.update(size: backgroundFrame.size, corners: corners, transition: transition)
|
||||
}
|
||||
transition.setFrame(view: self.externalContentBackgroundView, frame: backgroundFrame)
|
||||
if self.automaticallyLayoutExternalContentBackgroundView {
|
||||
transition.setFrame(view: self.externalContentBackgroundView, frame: backgroundFrame)
|
||||
}
|
||||
transition.setAlpha(view: self.externalContentBackgroundView, alpha: backgroundAlpha)
|
||||
transition.setCornerRadius(layer: self.layer, cornerRadius: contentCornerRadius)
|
||||
|
||||
|
@ -435,6 +435,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
|
||||
}, openBoostToUnrestrict: {
|
||||
}, updateRecordingTrimRange: { _, _, _, _ in
|
||||
}, dismissAllTooltips: {
|
||||
}, editTodoMessage: { _, _ in
|
||||
}, updateHistoryFilter: { _ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, toggleChatSidebarMode: {
|
||||
@ -3847,6 +3848,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, requestToggleTodoMessageItem: { _, _, _ in
|
||||
}, displayTodoToggleUnavailable: { _ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||
self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in
|
||||
|
@ -823,6 +823,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
}, openBoostToUnrestrict: {
|
||||
}, updateRecordingTrimRange: { _, _, _, _ in
|
||||
}, dismissAllTooltips: {
|
||||
}, editTodoMessage: { _, _ in
|
||||
}, updateHistoryFilter: { _ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, toggleChatSidebarMode: {
|
||||
|
@ -835,11 +835,21 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
||||
)
|
||||
))
|
||||
} 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(
|
||||
id: "reason",
|
||||
title: strings.Stars_Transaction_Giveaway_Reason,
|
||||
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)))
|
||||
)
|
||||
))
|
||||
}
|
||||
|
@ -328,7 +328,15 @@ final class StarsTransactionsListPanelComponent: Component {
|
||||
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 {
|
||||
|
@ -9,17 +9,20 @@ public final class ToastContentComponent: Component {
|
||||
public let content: AnyComponent<Empty>
|
||||
public let insets: UIEdgeInsets
|
||||
public let iconSpacing: CGFloat
|
||||
public let action: (() -> Void)?
|
||||
|
||||
public init(
|
||||
icon: AnyComponent<Empty>,
|
||||
content: AnyComponent<Empty>,
|
||||
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.content = content
|
||||
self.insets = insets
|
||||
self.iconSpacing = iconSpacing
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public static func ==(lhs: ToastContentComponent, rhs: ToastContentComponent) -> Bool {
|
||||
@ -39,6 +42,8 @@ public final class ToastContentComponent: Component {
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var component: ToastContentComponent?
|
||||
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private let icon = ComponentView<Empty>()
|
||||
private let content = ComponentView<Empty>()
|
||||
@ -63,9 +68,21 @@ public final class ToastContentComponent: Component {
|
||||
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 {
|
||||
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 rightInset: CGFloat = component.insets.right
|
||||
let topInset: CGFloat = component.insets.top
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Todo.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Todo.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "todolist_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Todo.imageset/todolist_30.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Todo.imageset/todolist_30.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Message/TodoCheck.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Message/TodoCheck.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "todo_check.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Message/TodoCheck.imageset/todo_check.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Message/TodoCheck.imageset/todo_check.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Message/TodoDot.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Message/TodoDot.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "todo_dot.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Message/TodoDot.imageset/todo_dot.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Message/TodoDot.imageset/todo_dot.pdf
vendored
Normal file
Binary file not shown.
@ -21,7 +21,6 @@ import DeviceLocationManager
|
||||
import ShareController
|
||||
import UrlEscaping
|
||||
import ContextUI
|
||||
import ComposePollUI
|
||||
import AlertUI
|
||||
import PresentationDataUtils
|
||||
import UndoUI
|
||||
@ -4148,6 +4147,11 @@ extension ChatControllerImpl {
|
||||
return
|
||||
}
|
||||
self.dismissAllTooltips()
|
||||
}, editTodoMessage: { [weak self] messageId, append in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openTodoEditing(messageId: messageId, append: append)
|
||||
}, updateHistoryFilter: { [weak self] update in
|
||||
guard let self else {
|
||||
return
|
||||
|
@ -22,6 +22,7 @@ import ShareController
|
||||
import UrlEscaping
|
||||
import ContextUI
|
||||
import ComposePollUI
|
||||
import ComposeTodoScreen
|
||||
import AlertUI
|
||||
import PresentationDataUtils
|
||||
import UndoUI
|
||||
@ -342,6 +343,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
var selectMessagePollOptionDisposables: DisposableDict<MessageId>?
|
||||
var selectPollOptionFeedback: HapticFeedback?
|
||||
|
||||
var updateMessageTodoDisposables: DisposableDict<MessageId>?
|
||||
|
||||
var resolveUrlDisposable: MetaDisposable?
|
||||
|
||||
var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:]
|
||||
@ -4789,6 +4792,68 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return direction ? .right : .left
|
||||
}
|
||||
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))
|
||||
controllerInteraction.enableFullTranslucency = context.sharedContext.energyUsageSettings.fullTranslucency
|
||||
|
||||
@ -5845,6 +5910,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
self.updateChatLocationThreadDisposable?.dispose()
|
||||
self.accountPeerDisposable?.dispose()
|
||||
self.contentDataDisposable?.dispose()
|
||||
self.updateMessageTodoDisposables?.dispose()
|
||||
}
|
||||
|
||||
public func updatePresentationMode(_ mode: ChatControllerPresentationMode) {
|
||||
@ -7576,57 +7642,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
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] {
|
||||
let silentPosting = self.presentationInterfaceState.interfaceState.silentPosting
|
||||
return transformEnqueueMessages(messages, silentPosting: silentPosting, postpone: postpone)
|
||||
|
@ -33,6 +33,8 @@ import AutomaticBusinessMessageSetupScreen
|
||||
import MediaEditorScreen
|
||||
import CameraScreen
|
||||
import ShareController
|
||||
import ComposeTodoScreen
|
||||
import ComposePollUI
|
||||
|
||||
extension ChatControllerImpl {
|
||||
enum AttachMenuSubject {
|
||||
@ -113,6 +115,8 @@ extension ChatControllerImpl {
|
||||
availableButtons.insert(.poll, at: max(0, availableButtons.count - 1))
|
||||
}
|
||||
|
||||
availableButtons.append(.todo)
|
||||
|
||||
let presentationData = self.presentationData
|
||||
|
||||
var isScheduledMessages = false
|
||||
@ -622,6 +626,26 @@ extension ChatControllerImpl {
|
||||
completion(controller, controller.mediaPickerContext)
|
||||
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:
|
||||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer, let starsContext = context.starsContext {
|
||||
let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions
|
||||
@ -677,12 +701,12 @@ extension ChatControllerImpl {
|
||||
let _ = self.context.engine.messages.acceptAttachMenuBotDisclaimer(botId: bot.peer.id).startStandalone()
|
||||
}
|
||||
let _ = (self.context.engine.messages.addBotToAttachMenu(botId: bot.peer.id, allowWrite: allowWrite)
|
||||
|> deliverOnMainQueue).startStandalone(error: { _ in
|
||||
|> deliverOnMainQueue).startStandalone(error: { _ in
|
||||
}, completed: { [weak controller] in
|
||||
controller?.refresh()
|
||||
})
|
||||
},
|
||||
dismissed: {
|
||||
dismissed: {
|
||||
strongSelf.attachmentController?.dismiss(animated: true)
|
||||
})
|
||||
strongSelf.present(alertController, in: .window(.root))
|
||||
@ -1967,4 +1991,147 @@ extension ChatControllerImpl {
|
||||
mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -791,22 +791,22 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
self.prefetchManager = InChatPrefetchManager(context: context)
|
||||
|
||||
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 {
|
||||
let peerId = adMessagesContext.peerId
|
||||
if peerId.namespace == Namespaces.Peer.CloudUser {
|
||||
adMessages = .single((nil, []))
|
||||
adMessages = .single((nil, [], nil, nil))
|
||||
} else {
|
||||
if context.sharedContext.immediateExperimentalUISettings.fakeAds {
|
||||
adMessages = context.engine.data.get(
|
||||
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
|
||||
var attributes: [MessageAttribute] = []
|
||||
|
||||
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>()
|
||||
|
||||
@ -875,14 +875,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
associatedStories: [:]
|
||||
)
|
||||
}
|
||||
return (10, fakeAdMessages)
|
||||
return (10, fakeAdMessages, nil, nil)
|
||||
}
|
||||
} else {
|
||||
adMessages = adMessagesContext.state
|
||||
}
|
||||
}
|
||||
} else {
|
||||
adMessages = .single((nil, []))
|
||||
adMessages = .single((nil, [], nil, nil))
|
||||
}
|
||||
|
||||
let clientId = Atomic<Int32>(value: nextClientId)
|
||||
@ -1224,9 +1224,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
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
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] interPostInterval, messages in
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] interPostInterval, messages, _, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
@ -1479,25 +1479,33 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
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 activeTodo: TelegramMediaTodo?
|
||||
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 !isPollEffectivelyClosed(message: message, poll: 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 {
|
||||
var hasSelected = false
|
||||
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
|
||||
if case let .replyThread(message) = chatPresentationInterfaceState.chatLocation {
|
||||
if !message.isForumPost {
|
||||
|
@ -194,6 +194,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, requestToggleTodoMessageItem: { _, _, _ in
|
||||
}, displayTodoToggleUnavailable: { _ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
|
@ -2400,6 +2400,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, requestToggleTodoMessageItem: { _, _, _ in
|
||||
}, displayTodoToggleUnavailable: { _ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode))
|
||||
|
||||
@ -2715,6 +2717,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
mappedSource = .animatedEmoji
|
||||
case .paidMessages:
|
||||
mappedSource = .paidMessages
|
||||
case .todo:
|
||||
mappedSource = .paidMessages
|
||||
case let .auth(price):
|
||||
mappedSource = .auth(price)
|
||||
}
|
||||
@ -2793,6 +2797,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
mappedSubject = .messageEffects
|
||||
case .paidMessages:
|
||||
mappedSubject = .paidMessages
|
||||
case .todo:
|
||||
mappedSubject = .todo
|
||||
case .business:
|
||||
mappedSubject = .business
|
||||
buttonText = presentationData.strings.Chat_EmptyStateIntroFooterPremiumActionButton
|
||||
|
Loading…
x
Reference in New Issue
Block a user