Merge branch 'gift-resale'

This commit is contained in:
Ilya Laktyushin 2025-04-18 13:49:45 +04:00
commit 7a261aec95
89 changed files with 6964 additions and 999 deletions

View file

@ -14188,3 +14188,12 @@ Sorry for the inconvenience.";
"SendInviteLink.TextCallsRestrictedMultipleUsers_1" = "{user_list}, and **%d** more person do not accept calls.";
"SendInviteLink.TextCallsRestrictedMultipleUsers_any" = "{user_list}, and **%d** more people do not accept calls.";
"SendInviteLink.TextCallsRestrictedSendInviteLink" = "You can try to send an invite link instead.";
"Story.Privacy.ShareStories" = "Share Stories";
"Story.Privacy.PostStoriesAsHeader" = "POST STORIES AS";
"Story.Privacy.WhoCanViewStoriesHeader" = "WHO CAN VIEW THIS STORIES";
"Story.Privacy.PostStories_1" = "Post %@ Story";
"Story.Privacy.PostStories_any" = "Post %@ Stories";
"Story.Privacy.KeepOnMyPageManyInfo" = "Keep these stories on your profile even after they expire in %@. Privacy settings will apply.";
"Story.Privacy.KeepOnChannelPageManyInfo" = "Keep these stories on the channel profile even after they expire in %@.";
"Story.Privacy.KeepOnGroupPageManyInfo" = "Keep these stories on the group page even after they expire in %@.";

View file

@ -653,6 +653,7 @@ public enum ChatListSearchFilter: Equatable {
case files
case music
case voice
case instantVideo
case peer(PeerId, Bool, String, String)
case date(Int32?, Int32, String)
case publicPosts
@ -679,8 +680,10 @@ public enum ChatListSearchFilter: Equatable {
return 8
case .voice:
return 9
case .publicPosts:
case .instantVideo:
return 10
case .publicPosts:
return 11
case let .peer(peerId, _, _, _):
return peerId.id._internalGetInt64Value()
case let .date(_, date, _):
@ -806,7 +809,7 @@ public protocol MediaEditorScreenResult {
public protocol TelegramRootControllerInterface: NavigationController {
@discardableResult
func openStoryCamera(customTarget: Stories.PendingTarget?, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator?
func proceedWithStoryUpload(target: Stories.PendingTarget, result: MediaEditorScreenResult, existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void)
func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void)
func getContactsController() -> ViewController?
func getChatsController() -> ViewController?
@ -1131,6 +1134,7 @@ public protocol SharedAccountContext: AnyObject {
func makeStarsGiftController(context: AccountContext, birthdays: [EnginePeer.Id: TelegramBirthday]?, completion: @escaping (([EnginePeer.Id]) -> Void)) -> ViewController
func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Signal<Never, TransferStarGiftError>)?) -> ViewController
func makeGiftOptionsController(context: AccountContext, peerId: EnginePeer.Id, premiumOptions: [CachedPremiumGiftOption], hasBirthday: Bool, completion: (() -> Void)?) -> ViewController
func makeGiftStoreController(context: AccountContext, peerId: EnginePeer.Id, gift: StarGift.Gift) -> ViewController
func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController
func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController
@ -1148,7 +1152,7 @@ public protocol SharedAccountContext: AnyObject {
func makeAvatarMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any]) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController
func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController
func makeStickerPickerScreen(context: AccountContext, inputData: Promise<StickerPickerInput>, completion: @escaping (FileMediaReference) -> Void) -> ViewController
@ -1174,6 +1178,7 @@ public protocol SharedAccountContext: AnyObject {
func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsWithdrawalScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarGiftResellScreen(context: AccountContext, update: Bool, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController
func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController
func makeStarsIntroScreen(context: AccountContext) -> ViewController
@ -1442,7 +1447,10 @@ public struct StarsSubscriptionConfiguration {
usdWithdrawRate: 1200,
paidMessageMaxAmount: 10000,
paidMessageCommissionPermille: 850,
paidMessagesAvailable: false
paidMessagesAvailable: false,
starGiftResaleMinAmount: 125,
starGiftResaleMaxAmount: 3500,
starGiftCommissionPermille: 80
)
}
@ -1451,19 +1459,28 @@ public struct StarsSubscriptionConfiguration {
public let paidMessageMaxAmount: Int64
public let paidMessageCommissionPermille: Int32
public let paidMessagesAvailable: Bool
public let starGiftResaleMinAmount: Int64
public let starGiftResaleMaxAmount: Int64
public let starGiftCommissionPermille: Int32
fileprivate init(
maxFee: Int64,
usdWithdrawRate: Int64,
paidMessageMaxAmount: Int64,
paidMessageCommissionPermille: Int32,
paidMessagesAvailable: Bool
paidMessagesAvailable: Bool,
starGiftResaleMinAmount: Int64,
starGiftResaleMaxAmount: Int64,
starGiftCommissionPermille: Int32
) {
self.maxFee = maxFee
self.usdWithdrawRate = usdWithdrawRate
self.paidMessageMaxAmount = paidMessageMaxAmount
self.paidMessageCommissionPermille = paidMessageCommissionPermille
self.paidMessagesAvailable = paidMessagesAvailable
self.starGiftResaleMinAmount = starGiftResaleMinAmount
self.starGiftResaleMaxAmount = starGiftResaleMaxAmount
self.starGiftCommissionPermille = starGiftCommissionPermille
}
public static func with(appConfiguration: AppConfiguration) -> StarsSubscriptionConfiguration {
@ -1473,13 +1490,19 @@ public struct StarsSubscriptionConfiguration {
let paidMessageMaxAmount = (data["stars_paid_message_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.paidMessageMaxAmount
let paidMessageCommissionPermille = (data["stars_paid_message_commission_permille"] as? Double).flatMap(Int32.init) ?? StarsSubscriptionConfiguration.defaultValue.paidMessageCommissionPermille
let paidMessagesAvailable = (data["stars_paid_messages_available"] as? Bool) ?? StarsSubscriptionConfiguration.defaultValue.paidMessagesAvailable
let starGiftResaleMinAmount = (data["stars_stargift_resale_amount_min"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.starGiftResaleMinAmount
let starGiftResaleMaxAmount = (data["stars_stargift_resale_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.starGiftResaleMaxAmount
let starGiftCommissionPermille = (data["stars_stargift_resale_commission_permille"] as? Double).flatMap(Int32.init) ?? StarsSubscriptionConfiguration.defaultValue.starGiftCommissionPermille
return StarsSubscriptionConfiguration(
maxFee: maxFee,
usdWithdrawRate: usdWithdrawRate,
paidMessageMaxAmount: paidMessageMaxAmount,
paidMessageCommissionPermille: paidMessageCommissionPermille,
paidMessagesAvailable: paidMessagesAvailable
paidMessagesAvailable: paidMessagesAvailable,
starGiftResaleMinAmount: starGiftResaleMinAmount,
starGiftResaleMaxAmount: starGiftResaleMaxAmount,
starGiftCommissionPermille: starGiftCommissionPermille
)
} else {
return .defaultValue

View file

@ -41,6 +41,8 @@ public struct AttachmentMainButtonState {
public let progress: Progress
public let isEnabled: Bool
public let hasShimmer: Bool
public let iconName: String?
public let smallSpacing: Bool
public let position: Position?
public init(
@ -53,6 +55,8 @@ public struct AttachmentMainButtonState {
progress: Progress,
isEnabled: Bool,
hasShimmer: Bool,
iconName: String? = nil,
smallSpacing: Bool = false,
position: Position? = nil
) {
self.text = text
@ -64,6 +68,8 @@ public struct AttachmentMainButtonState {
self.progress = progress
self.isEnabled = isEnabled
self.hasShimmer = hasShimmer
self.iconName = iconName
self.smallSpacing = smallSpacing
self.position = position
}

View file

@ -608,7 +608,7 @@ public struct ChatTextInputStateText: Codable, Equatable {
return lhs.text == rhs.text && lhs.attributes == rhs.attributes
}
public func attributedText() -> NSAttributedString {
public func attributedText(files: [Int64: TelegramMediaFile] = [:]) -> NSAttributedString {
let result = NSMutableAttributedString(string: self.text)
for attribute in self.attributes {
switch attribute.type {
@ -623,7 +623,7 @@ public struct ChatTextInputStateText: Codable, Equatable {
case let .textUrl(url):
result.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
case let .customEmoji(_, fileId, enableAnimation):
result.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil, enableAnimation: enableAnimation), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
result.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: files[fileId], enableAnimation: enableAnimation), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
case .strikethrough:
result.addAttribute(ChatTextInputAttributes.strikethrough, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
case .underline:

View file

@ -476,6 +476,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
private var size: CGSize?
private let backgroundAnimationNode: ASImageNode
private var iconNode: ASImageNode?
fileprivate let textNode: ImmediateTextNode
private var badgeNode: BadgeNode?
private let statusNode: SemanticStatusNode
@ -781,6 +782,26 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
badgeNode.removeFromSupernode()
}
if let iconName = state.iconName {
let iconNode: ASImageNode
if let current = self.iconNode {
iconNode = current
} else {
iconNode = ASImageNode()
iconNode.displaysAsynchronously = false
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: state.textColor)
self.iconNode = iconNode
self.addSubnode(iconNode)
}
if let iconSize = iconNode.image?.size {
textFrame.origin.x += (iconSize.width + 6.0) / 2.0
iconNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - iconSize.width - 6.0, y: textFrame.minY + floorToScreenPixels((textFrame.height - iconSize.height) * 0.5)), size: iconSize)
}
} else if let iconNode = self.iconNode {
self.iconNode = nil
iconNode.removeFromSupernode()
}
if self.textNode.frame.width.isZero {
self.textNode.frame = textFrame
} else {
@ -795,7 +816,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
self.transitionFromProgress()
}
}
if let shimmerView = self.shimmerView, let borderView = self.borderView, let borderMaskView = self.borderMaskView, let borderShimmerView = self.borderShimmerView {
let buttonFrame = CGRect(origin: .zero, size: size)
let buttonWidth = size.width
@ -1786,7 +1807,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
} else {
height = bounds.height + 8.0
}
if !isNarrowButton {
if isTwoVerticalButtons && self.secondaryButtonState.smallSpacing {
} else if !isNarrowButton {
height += 9.0
}
if isTwoVerticalButtons {
@ -1876,7 +1899,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
mainButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY + sideInset + buttonSize.height), size: buttonSize)
case .bottom:
mainButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY), size: buttonSize)
secondaryButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY + sideInset + buttonSize.height), size: buttonSize)
let buttonSpacing = self.secondaryButtonState.smallSpacing ? 8.0 : sideInset
secondaryButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY + buttonSpacing + buttonSize.height), size: buttonSize)
case .left:
secondaryButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY), size: buttonSize)
mainButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX + buttonSize.width + sideInset, y: buttonOriginY), size: buttonSize)

View file

@ -357,6 +357,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
key = .music
case .voice:
key = .voice
case .instantVideo:
key = .instantVideo
case .publicPosts:
key = .publicPosts
case let .date(minDate, maxDate, title):
@ -685,6 +687,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
filterKey = .music
case .voice:
filterKey = .voice
case .instantVideo:
filterKey = .instantVideo
case .publicPosts:
filterKey = .publicPosts
}
@ -725,6 +729,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
key = .music
case .voice:
key = .voice
case .instantVideo:
key = .instantVideo
case .downloads:
key = .downloads
default:

View file

@ -108,6 +108,9 @@ private final class ItemNode: ASDisplayNode {
case .voice:
title = presentationData.strings.ChatList_Search_FilterVoice
icon = nil
case .instantVideo:
title = presentationData.strings.ChatList_Search_FilterVoice
icon = nil
case .publicPosts:
title = presentationData.strings.ChatList_Search_FilterPublicPosts
icon = nil

View file

@ -1587,8 +1587,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
private let recentListNode: ListView
private let shimmerNode: ChatListSearchShimmerNode
private let listNode: ListView
private let mediaNode: ChatListSearchMediaNode
private let listNode: ListView?
private let mediaNode: ChatListSearchMediaNode?
private var enqueuedRecentTransitions: [(ChatListSearchContainerRecentTransition, Bool)] = []
private var enqueuedTransitions: [(ChatListSearchContainerTransition, Bool)] = []
@ -1714,6 +1714,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
tagMask = .music
case .voice:
tagMask = .voiceOrInstantVideo
case .instantVideo:
tagMask = .roundVideo
}
self.tagMask = tagMask
@ -1737,8 +1739,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
self.shimmerNode.allowsGroupOpacity = true
self.listNode = ListView()
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
self.listNode.accessibilityPageScrolledString = { row, count in
self.listNode?.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
self.listNode?.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
@ -1746,13 +1748,17 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
var transitionNodeImpl: ((EngineMessage.Id, EngineMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?)?
var addToTransitionSurfaceImpl: ((UIView) -> Void)?
self.mediaNode = ChatListSearchMediaNode(context: self.context, contentType: .photoOrVideo, openMessage: { message, mode in
openMediaMessageImpl?(EngineMessage(message), mode)
}, messageContextAction: { message, node, rect, gesture in
interaction.mediaMessageContextAction(EngineMessage(message), node, rect, gesture)
}, toggleMessageSelection: { messageId, selected in
interaction.toggleMessageSelection(messageId, selected)
})
if key == .media {
self.mediaNode = ChatListSearchMediaNode(context: self.context, contentType: .photoOrVideo, openMessage: { message, mode in
openMediaMessageImpl?(EngineMessage(message), mode)
}, messageContextAction: { message, node, rect, gesture in
interaction.mediaMessageContextAction(EngineMessage(message), node, rect, gesture)
}, toggleMessageSelection: { messageId, selected in
interaction.toggleMessageSelection(messageId, selected)
})
} else {
self.mediaNode = nil
}
self.mediaAccessoryPanelContainer = PassthroughContainerNode()
self.mediaAccessoryPanelContainer.clipsToBounds = true
@ -1822,8 +1828,12 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
self.addSubnode(recentEmptyNode)
}
self.addSubnode(self.listNode)
self.addSubnode(self.mediaNode)
if let listNode = self.listNode {
self.addSubnode(listNode)
}
if let mediaNode = self.mediaNode {
self.addSubnode(mediaNode)
}
self.addSubnode(self.emptyResultsAnimationNode)
self.addSubnode(self.emptyResultsTitleNode)
@ -1850,8 +1860,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
}
self.listNode.isHidden = true
self.mediaNode.isHidden = true
self.listNode?.isHidden = true
self.mediaNode?.isHidden = true
self.recentListNode.isHidden = peersFilter.contains(.excludeRecent)
let currentRemotePeers = Atomic<([FoundPeer], [FoundPeer], [AdPeer])?>(value: nil)
@ -3227,16 +3237,16 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
transitionNodeImpl = { [weak self] messageId, media in
if let strongSelf = self {
return strongSelf.mediaNode.transitionNodeForGallery(messageId: messageId, media: media._asMedia())
if let self {
return self.mediaNode?.transitionNodeForGallery(messageId: messageId, media: media._asMedia())
} else {
return nil
}
}
addToTransitionSurfaceImpl = { [weak self] view in
if let strongSelf = self {
strongSelf.mediaNode.addToTransitionSurface(view: view)
if let self {
self.mediaNode?.addToTransitionSurface(view: view)
}
}
@ -3270,7 +3280,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
case .savedMessagesChats:
break
}
self?.listNode.clearHighlightAnimated(true)
self?.listNode?.clearHighlightAnimated(true)
}, disabledPeerSelected: { _, _, _ in
}, togglePeerSelected: { _, _ in
}, togglePeersSelection: { _, _ in
@ -3280,12 +3290,12 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
if let strongSelf = self, let peer = message.peers[message.id.peerId] {
interaction.openMessage(EnginePeer(peer), threadId, message.id, strongSelf.key == .chats)
}
self?.listNode.clearHighlightAnimated(true)
self?.listNode?.clearHighlightAnimated(true)
}, groupSelected: { _ in
}, addContact: { [weak self] phoneNumber in
interaction.dismissInput()
interaction.addContact(phoneNumber)
self?.listNode.clearHighlightAnimated(true)
self?.listNode?.clearHighlightAnimated(true)
}, setPeerIdWithRevealedOptions: { _, _ in
}, setItemPinned: { _, _ in
}, setPeerMuted: { _, _ in
@ -3328,7 +3338,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
interaction.dismissInput()
interaction.openPeer(peer, peer, threadId, false)
self.listNode.clearHighlightAnimated(true)
self.listNode?.clearHighlightAnimated(true)
})
}, openStorageManagement: {
}, openPasswordSetup: {
@ -3397,7 +3407,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}, transitionNode: { messageId, media, _ in
var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
if let strongSelf = self {
strongSelf.listNode.forEachItemNode { itemNode in
strongSelf.listNode?.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListMessageNode {
if let result = itemNode.transitionNode(id: messageId, media: media, adjustRect: false) {
transitionNode = result
@ -3556,7 +3566,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
if isSearching && (entries?.isEmpty ?? true) {
entries = nil
}
strongSelf.mediaNode.updateHistory(entries: entries, totalCount: 0, updateType: .Initial)
strongSelf.mediaNode?.updateHistory(entries: entries, totalCount: 0, updateType: .Initial)
} else if strongSelf.tagMask == .roundVideo {
}
var peers: [EnginePeer] = []
@ -4349,7 +4361,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
if strongSelf.backgroundColor != nil {
strongSelf.backgroundColor = presentationData.theme.chatList.backgroundColor
}
strongSelf.listNode.forEachItemHeaderNode({ itemHeaderNode in
strongSelf.listNode?.forEachItemHeaderNode({ itemHeaderNode in
if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode {
itemHeaderNode.updateTheme(theme: presentationData.theme)
}
@ -4367,26 +4379,26 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
interaction.dismissInput()
}
self.listNode.beganInteractiveDragging = { _ in
self.listNode?.beganInteractiveDragging = { _ in
interaction.dismissInput()
}
self.mediaNode.beganInteractiveDragging = {
self.mediaNode?.beganInteractiveDragging = {
interaction.dismissInput()
}
self.listNode.visibleBottomContentOffsetChanged = { offset in
self.listNode?.visibleBottomContentOffsetChanged = { offset in
guard case let .known(value) = offset, value < 160.0 else {
return
}
loadMore()
}
self.mediaNode.loadMore = {
self.mediaNode?.loadMore = {
loadMore()
}
if [.file, .music, .voiceOrInstantVideo].contains(tagMask) || self.key == .downloads {
if [.file, .music, .voiceOrInstantVideo, .voice, .roundVideo].contains(tagMask) || self.key == .downloads {
let key = self.key
self.mediaStatusDisposable = (context.sharedContext.mediaManager.globalMediaPlayerState
|> mapToSignal { playlistStateAndType -> Signal<(Account, SharedMediaPlayerItemPlaybackState, MediaManagerPlayerType)?, NoError> in
@ -4396,7 +4408,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
if let playlistId = state.playlistId as? PeerMessagesMediaPlaylistId, case .custom = playlistId {
switch type {
case .voice:
if tagMask != .voiceOrInstantVideo {
if ![.voiceOrInstantVideo, .voice, .roundVideo].contains(tagMask) {
return .single(nil) |> delay(0.2, queue: .mainQueue())
}
case .music:
@ -4524,8 +4536,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
func scrollToTop() -> Bool {
if !self.mediaNode.isHidden {
return self.mediaNode.scrollToTop()
if let mediaNode = self.mediaNode, !mediaNode.isHidden {
return mediaNode.scrollToTop()
} else if !self.recentListNode.isHidden {
let offset = self.recentListNode.visibleContentOffset()
switch offset {
@ -4535,15 +4547,17 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
return true
}
} else {
let offset = self.listNode.visibleContentOffset()
} else if let listNode = self.listNode {
let offset = listNode.visibleContentOffset()
switch offset {
case let .known(value) where value <= CGFloat.ulpOfOne:
return false
default:
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
return true
}
} else {
return false
}
}
@ -4852,11 +4866,11 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
emptyRecentAnimationNode.updateLayout(size: emptyRecentAnimationSize)
}
self.listNode.frame = CGRect(origin: CGPoint(), size: size)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
self.listNode?.frame = CGRect(origin: CGPoint(), size: size)
self.listNode?.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
self.mediaNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: size.width, height: size.height))
self.mediaNode.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: false, expandProgress: 1.0, presentationData: self.presentationData, synchronous: true, transition: transition)
self.mediaNode?.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: size.width, height: size.height))
self.mediaNode?.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: false, expandProgress: 1.0, presentationData: self.presentationData, synchronous: true, transition: transition)
do {
let padding: CGFloat = 16.0
@ -4887,7 +4901,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
func updateHiddenMedia() {
self.listNode.forEachItemNode { itemNode in
self.listNode?.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListMessageNode {
itemNode.updateHiddenMedia()
}
@ -4899,7 +4913,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
func transitionNodeForGallery(messageId: EngineMessage.Id, media: EngineMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
self.listNode.forEachItemNode { itemNode in
self.listNode?.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListMessageNode {
if let result = itemNode.transitionNode(id: messageId, media: media._asMedia(), adjustRect: false) {
transitionNode = result
@ -4915,8 +4929,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
func updateSelectedMessages(animated: Bool) {
self.selectedMessages = self.interaction.getSelectedMessageIds()
self.mediaNode.selectedMessageIds = self.selectedMessages
self.mediaNode.updateSelectedMessages(animated: animated)
self.mediaNode?.selectedMessageIds = self.selectedMessages
self.mediaNode?.updateSelectedMessages(animated: animated)
}
func removeAds() {
@ -5006,16 +5020,16 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
options.insert(.PreferSynchronousResourceLoading)
}
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
self.listNode?.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
if let strongSelf = self {
let searchOptions = strongSelf.searchOptionsValue
strongSelf.listNode.isHidden = strongSelf.tagMask == .photoOrVideo && (strongSelf.searchQueryValue ?? "").isEmpty
strongSelf.mediaNode.isHidden = !strongSelf.listNode.isHidden
strongSelf.listNode?.isHidden = strongSelf.tagMask == .photoOrVideo && (strongSelf.searchQueryValue ?? "").isEmpty
strongSelf.mediaNode?.isHidden = !(strongSelf.listNode?.isHidden ?? true)
let displayingResults = transition.displayingResults
if !displayingResults {
strongSelf.listNode.isHidden = true
strongSelf.mediaNode.isHidden = true
strongSelf.listNode?.isHidden = true
strongSelf.mediaNode?.isHidden = true
}
let emptyResults = displayingResults && transition.isEmpty
@ -5103,7 +5117,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
} else {
let adjustedLocation = self.convert(location, to: self.listNode)
self.listNode.forEachItemNode { itemNode in
self.listNode?.forEachItemNode { itemNode in
if itemNode.frame.contains(adjustedLocation) {
selectedItemNode = itemNode
}
@ -5506,7 +5520,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
)
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: context, chatLocation: .peer(id: peer1.id), interaction: ListMessageItemInteraction.default, message: message._asMessage(), selection: hasSelection ? .selectable(selected: false) : .none, displayHeader: false, customHeader: nil, hintIsLink: false, isGlobalSearchResult: true)
case .voice:
case .voice, .instantVideo:
var media: [EngineMedia] = []
media.append(.file(TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: [.Audio(isVoice: true, duration: 0, title: nil, performer: nil, waveform: Data())], alternativeRepresentations: [])))
let message = EngineMessage(

View file

@ -61,6 +61,7 @@ public enum ChatListSearchPaneKey {
case files
case music
case voice
case instantVideo
}
extension ChatListSearchPaneKey {
@ -88,6 +89,8 @@ extension ChatListSearchPaneKey {
return .music
case .voice:
return .voice
case .instantVideo:
return .instantVideo
}
}
}

View file

@ -126,6 +126,7 @@ public final class ContextMenuActionItem {
public let id: AnyHashable?
public let text: String
public let entities: [MessageTextEntity]
public let entityFiles: [Int64: TelegramMediaFile]
public let enableEntityAnimations: Bool
public let textColor: ContextMenuActionItemTextColor
public let textFont: ContextMenuActionItemFont
@ -147,6 +148,7 @@ public final class ContextMenuActionItem {
id: AnyHashable? = nil,
text: String,
entities: [MessageTextEntity] = [],
entityFiles: [Int64: TelegramMediaFile] = [:],
enableEntityAnimations: Bool = true,
textColor: ContextMenuActionItemTextColor = .primary,
textLayout: ContextMenuActionItemTextLayout = .twoLinesMax,
@ -168,6 +170,7 @@ public final class ContextMenuActionItem {
id: id,
text: text,
entities: entities,
entityFiles: entityFiles,
enableEntityAnimations: enableEntityAnimations,
textColor: textColor,
textLayout: textLayout,
@ -199,6 +202,7 @@ public final class ContextMenuActionItem {
id: AnyHashable? = nil,
text: String,
entities: [MessageTextEntity] = [],
entityFiles: [Int64: TelegramMediaFile] = [:],
enableEntityAnimations: Bool = true,
textColor: ContextMenuActionItemTextColor = .primary,
textLayout: ContextMenuActionItemTextLayout = .twoLinesMax,
@ -219,6 +223,7 @@ public final class ContextMenuActionItem {
self.id = id
self.text = text
self.entities = entities
self.entityFiles = entityFiles
self.enableEntityAnimations = enableEntityAnimations
self.textColor = textColor
self.textFont = textFont
@ -2140,6 +2145,8 @@ public protocol ContextReferenceContentSource: AnyObject {
var shouldBeDismissed: Signal<Bool, NoError> { get }
var forceDisplayBelowKeyboard: Bool { get }
func transitionInfo() -> ContextControllerReferenceViewInfo?
}
@ -2148,6 +2155,10 @@ public extension ContextReferenceContentSource {
return false
}
var forceDisplayBelowKeyboard: Bool {
return false
}
var shouldBeDismissed: Signal<Bool, NoError> {
return .single(false)
}
@ -2739,7 +2750,9 @@ public final class ContextController: ViewController, StandalonePresentableContr
}
public func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) {
if viewTreeContainsFirstResponder(view: self.view) {
if let mainSource = self.configuration.sources.first(where: { $0.id == self.configuration.initialId }), case let .reference(source) = mainSource.source, source.forceDisplayBelowKeyboard {
} else if viewTreeContainsFirstResponder(view: self.view) {
self.dismissOnInputClose = (result, completion)
self.view.endEditing(true)
return

View file

@ -273,7 +273,7 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
return super.hitTest(point, with: event)
}
func setItem(item: ContextMenuActionItem) {
public func setItem(item: ContextMenuActionItem) {
self.item = item
self.accessibilityLabel = item.text
}
@ -361,14 +361,25 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
let inputStateText = ChatTextInputStateText(text: self.item.text, attributes: self.item.entities.compactMap { entity -> ChatTextInputStateTextAttribute? in
if case let .CustomEmoji(_, fileId) = entity.type {
return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId, enableAnimation: true), range: entity.range)
} else if case .Bold = entity.type {
return ChatTextInputStateTextAttribute(type: .bold, range: entity.range)
} else if case .Italic = entity.type {
return ChatTextInputStateTextAttribute(type: .italic, range: entity.range)
}
return nil
})
let result = NSMutableAttributedString(attributedString: inputStateText.attributedText())
let result = NSMutableAttributedString(attributedString: inputStateText.attributedText(files: self.item.entityFiles))
result.addAttributes([
.font: titleFont,
.foregroundColor: titleColor
], range: NSRange(location: 0, length: result.length))
for attribute in inputStateText.attributes {
if case .bold = attribute.type {
result.addAttribute(NSAttributedString.Key.font, value: Font.semibold(presentationData.listsFontSize.baseDisplaySize), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
} else if case .italic = attribute.type {
result.addAttribute(NSAttributedString.Key.font, value: Font.semibold(15.0), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
}
}
attributedText = result
} else {
attributedText = parseMarkdownIntoAttributedString(

View file

@ -500,6 +500,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
func wantsDisplayBelowKeyboard() -> Bool {
if let reactionContextNode = self.reactionContextNode {
return reactionContextNode.wantsDisplayBelowKeyboard()
} else if case let .reference(source) = self.source {
return source.forceDisplayBelowKeyboard
} else {
return false
}

View file

@ -437,8 +437,8 @@ public extension CALayer {
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.size.height", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
}
func animateBoundsOriginXAdditive(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.x", timingFunction: timingFunction, duration: duration, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion)
func animateBoundsOriginXAdditive(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.x", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion)
}
func animateBoundsOriginYAdditive(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {

View file

@ -566,6 +566,14 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
self.hasSelectionChanged(false)
}
public func clearAll() {
for case let view as DrawingEntityView in self.subviews {
view.reset()
view.selectionView?.removeFromSuperview()
view.removeFromSuperview()
}
}
private func clear(animated: Bool = false) {
if animated {
for case let view as DrawingEntityView in self.subviews {

View file

@ -184,7 +184,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
private let bannedSendVideos: (Int32, Bool)?
private let canBoostToUnrestrict: Bool
fileprivate let paidMediaAllowed: Bool
private let subject: Subject
fileprivate let subject: Subject
fileprivate let forCollage: Bool
private let saveEditedPhotos: Bool
@ -1823,8 +1823,10 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
private var isDismissing = false
fileprivate let mainButtonStatePromise = Promise<AttachmentMainButtonState?>(nil)
fileprivate let secondaryButtonStatePromise = Promise<AttachmentMainButtonState?>(nil)
private let mainButtonAction: (() -> Void)?
private let secondaryButtonAction: (() -> Void)?
public init(
context: AccountContext,
@ -1844,7 +1846,8 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
selectionContext: TGMediaSelectionContext? = nil,
saveEditedPhotos: Bool = false,
mainButtonState: AttachmentMainButtonState? = nil,
mainButtonAction: (() -> Void)? = nil
mainButtonAction: (() -> Void)? = nil,
secondaryButtonAction: (() -> Void)? = nil
) {
self.context = context
@ -1864,6 +1867,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
self.saveEditedPhotos = saveEditedPhotos
self.mainButtonStatePromise.set(.single(mainButtonState))
self.mainButtonAction = mainButtonAction
self.secondaryButtonAction = secondaryButtonAction
let selectionContext = selectionContext ?? TGMediaSelectionContext()
@ -1997,7 +2001,14 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
} else if collection == nil {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
var hasSelect = false
if forCollage {
hasSelect = true
} else if case .story = mode {
hasSelect = true
}
if hasSelect {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Select, target: self, action: #selector(self.selectPressed))
} else {
if [.createSticker].contains(mode) {
@ -2337,6 +2348,9 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
var moreIsVisible = false
if case let .assets(_, mode) = self.subject, [.story, .createSticker].contains(mode) {
if count == 1 {
self.requestAttachmentMenuExpansion()
}
moreIsVisible = true
} else if case let .media(media) = self.subject {
self.titleView.title = media.count == 1 ? self.presentationData.strings.Attachment_Pasteboard : self.presentationData.strings.Attachment_SelectedMedia(count)
@ -2380,9 +2394,23 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
transition.updateAlpha(node: self.moreButtonNode.iconNode, alpha: moreIsVisible ? 1.0 : 0.0)
transition.updateTransformScale(node: self.moreButtonNode.iconNode, scale: moreIsVisible ? 1.0 : 0.1)
//if self. {
//self.mainButtonStatePromise.set(.single(AttachmentMainButtonState(text: "Add", badge: "\(count)", font: .bold, background: .color(self.presentationData.theme.actionSheet.controlAccentColor), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, isVisible: count > 0, progress: .none, isEnabled: true, hasShimmer: false)))
//}
if case .assets(_, .story) = self.subject, self.selectionCount > 0 {
//TODO:localize
var text = "Create 1 Story"
if self.selectionCount > 1 {
text = "Create \(self.selectionCount) Stories"
}
self.mainButtonStatePromise.set(.single(AttachmentMainButtonState(text: text, badge: nil, font: .bold, background: .color(self.presentationData.theme.actionSheet.controlAccentColor), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, position: .top)))
if self.selectionCount > 1 && self.selectionCount <= 6 {
self.secondaryButtonStatePromise.set(.single(AttachmentMainButtonState(text: "Combine into Collage", badge: nil, font: .regular, background: .color(.clear), textColor: self.presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, iconName: "Media Editor/Collage", smallSpacing: true, position: .bottom)))
} else {
self.secondaryButtonStatePromise.set(.single(nil))
}
} else {
self.mainButtonStatePromise.set(.single(nil))
self.secondaryButtonStatePromise.set(.single(nil))
}
}
private func updateThemeAndStrings() {
@ -2412,6 +2440,10 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
self.mainButtonAction?()
}
func secondaryButtonPressed() {
self.secondaryButtonAction?()
}
func dismissAllTooltips() {
self.undoOverlayController?.dismissWithCommitAction()
}
@ -2795,7 +2827,7 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
private weak var controller: MediaPickerScreenImpl?
var selectionCount: Signal<Int, NoError> {
if self.controller?.forCollage == true {
if let controller = self.controller, case .assets(_, .story) = controller.subject {
return .single(0)
} else {
return Signal { [weak self] subscriber in
@ -2933,6 +2965,10 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
return self.controller?.mainButtonStatePromise.get() ?? .single(nil)
}
public var secondaryButtonState: Signal<AttachmentMainButtonState?, NoError> {
return self.controller?.secondaryButtonStatePromise.get() ?? .single(nil)
}
init(controller: MediaPickerScreenImpl) {
self.controller = controller
}
@ -2952,6 +2988,10 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
func mainButtonAction() {
self.controller?.mainButtonPressed()
}
func secondaryButtonAction() {
self.controller?.secondaryButtonPressed()
}
}
private final class MediaPickerContextReferenceContentSource: ContextReferenceContentSource {
@ -3139,7 +3179,7 @@ public func storyMediaPickerController(
selectionLimit: Int?,
getSourceRect: @escaping () -> CGRect,
completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void,
multipleCompletion: @escaping ([Any]) -> Void,
multipleCompletion: @escaping ([Any], Bool) -> Void,
dismissed: @escaping () -> Void,
groupsPresented: @escaping () -> Void
) -> ViewController {
@ -3158,9 +3198,18 @@ public func storyMediaPickerController(
}
}
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: {
return nil
})
let controller = AttachmentController(
context: context,
updatedPresentationData: updatedPresentationData,
chatLocation: nil,
buttons: [.standalone],
initialButton: .standalone,
fromMenu: false,
hasTextInput: false,
makeEntityInputView: {
return nil
}
)
controller.forceSourceRect = true
controller.getSourceRect = getSourceRect
controller.requestController = { _, present in
@ -3184,7 +3233,18 @@ public func storyMediaPickerController(
results.append(asset)
}
}
multipleCompletion(results)
multipleCompletion(results, false)
}
},
secondaryButtonAction: { [weak selectionContext] in
if let selectionContext, let selectedItems = selectionContext.selectedItems() {
var results: [Any] = []
for item in selectedItems {
if let item = item as? TGMediaAsset, let asset = item.backingAsset {
results.append(asset)
}
}
multipleCompletion(results, true)
}
}
)

View file

@ -391,6 +391,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-625298705] = { return Api.InputInvoice.parse_inputInvoicePremiumGiftStars($0) }
dict[-1020867857] = { return Api.InputInvoice.parse_inputInvoiceSlug($0) }
dict[-396206446] = { return Api.InputInvoice.parse_inputInvoiceStarGift($0) }
dict[1674298252] = { return Api.InputInvoice.parse_inputInvoiceStarGiftResale($0) }
dict[1247763417] = { return Api.InputInvoice.parse_inputInvoiceStarGiftTransfer($0) }
dict[1300335965] = { return Api.InputInvoice.parse_inputInvoiceStarGiftUpgrade($0) }
dict[1710230755] = { return Api.InputInvoice.parse_inputInvoiceStars($0) }
@ -600,7 +601,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1348510708] = { return Api.MessageAction.parse_messageActionSetChatWallPaper($0) }
dict[1007897979] = { return Api.MessageAction.parse_messageActionSetMessagesTTL($0) }
dict[1192749220] = { return Api.MessageAction.parse_messageActionStarGift($0) }
dict[-1394619519] = { return Api.MessageAction.parse_messageActionStarGiftUnique($0) }
dict[1600878025] = { return Api.MessageAction.parse_messageActionStarGiftUnique($0) }
dict[1474192222] = { return Api.MessageAction.parse_messageActionSuggestProfilePhoto($0) }
dict[228168278] = { return Api.MessageAction.parse_messageActionTopicCreate($0) }
dict[-1064024032] = { return Api.MessageAction.parse_messageActionTopicEdit($0) }
@ -932,12 +933,16 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1301522832] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) }
dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) }
dict[-963180333] = { return Api.SponsoredPeer.parse_sponsoredPeer($0) }
dict[46953416] = { return Api.StarGift.parse_starGift($0) }
dict[1549979985] = { return Api.StarGift.parse_starGiftUnique($0) }
dict[-1809377438] = { return Api.StarGiftAttribute.parse_starGiftAttributeBackdrop($0) }
dict[-970274264] = { return Api.StarGift.parse_starGift($0) }
dict[1678891913] = { return Api.StarGift.parse_starGiftUnique($0) }
dict[-650279524] = { return Api.StarGiftAttribute.parse_starGiftAttributeBackdrop($0) }
dict[970559507] = { return Api.StarGiftAttribute.parse_starGiftAttributeModel($0) }
dict[-524291476] = { return Api.StarGiftAttribute.parse_starGiftAttributeOriginalDetails($0) }
dict[330104601] = { return Api.StarGiftAttribute.parse_starGiftAttributePattern($0) }
dict[783398488] = { return Api.StarGiftAttributeCounter.parse_starGiftAttributeCounter($0) }
dict[520210263] = { return Api.StarGiftAttributeId.parse_starGiftAttributeIdBackdrop($0) }
dict[1219145276] = { return Api.StarGiftAttributeId.parse_starGiftAttributeIdModel($0) }
dict[1242965043] = { return Api.StarGiftAttributeId.parse_starGiftAttributeIdPattern($0) }
dict[-586389774] = { return Api.StarRefProgram.parse_starRefProgram($0) }
dict[-1145654109] = { return Api.StarsAmount.parse_starsAmount($0) }
dict[1577421297] = { return Api.StarsGiftOption.parse_starsGiftOption($0) }
@ -1405,6 +1410,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-625215430] = { return Api.payments.PaymentReceipt.parse_paymentReceiptStars($0) }
dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) }
dict[-666824391] = { return Api.payments.PaymentResult.parse_paymentVerificationNeeded($0) }
dict[-1803939105] = { return Api.payments.ResaleStarGifts.parse_resaleStarGifts($0) }
dict[-74456004] = { return Api.payments.SavedInfo.parse_savedInfo($0) }
dict[-1779201615] = { return Api.payments.SavedStarGifts.parse_savedStarGifts($0) }
dict[377215243] = { return Api.payments.StarGiftUpgradePreview.parse_starGiftUpgradePreview($0) }
@ -2117,6 +2123,10 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.StarGiftAttribute:
_1.serialize(buffer, boxed)
case let _1 as Api.StarGiftAttributeCounter:
_1.serialize(buffer, boxed)
case let _1 as Api.StarGiftAttributeId:
_1.serialize(buffer, boxed)
case let _1 as Api.StarRefProgram:
_1.serialize(buffer, boxed)
case let _1 as Api.StarsAmount:
@ -2505,6 +2515,8 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.payments.PaymentResult:
_1.serialize(buffer, boxed)
case let _1 as Api.payments.ResaleStarGifts:
_1.serialize(buffer, boxed)
case let _1 as Api.payments.SavedInfo:
_1.serialize(buffer, boxed)
case let _1 as Api.payments.SavedStarGifts:

View file

@ -255,6 +255,7 @@ public extension Api {
case inputInvoicePremiumGiftStars(flags: Int32, userId: Api.InputUser, months: Int32, message: Api.TextWithEntities?)
case inputInvoiceSlug(slug: String)
case inputInvoiceStarGift(flags: Int32, peer: Api.InputPeer, giftId: Int64, message: Api.TextWithEntities?)
case inputInvoiceStarGiftResale(slug: String, toId: Api.InputPeer)
case inputInvoiceStarGiftTransfer(stargift: Api.InputSavedStarGift, toId: Api.InputPeer)
case inputInvoiceStarGiftUpgrade(flags: Int32, stargift: Api.InputSavedStarGift)
case inputInvoiceStars(purpose: Api.InputStorePaymentPurpose)
@ -312,6 +313,13 @@ public extension Api {
serializeInt64(giftId, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {message!.serialize(buffer, true)}
break
case .inputInvoiceStarGiftResale(let slug, let toId):
if boxed {
buffer.appendInt32(1674298252)
}
serializeString(slug, buffer: buffer, boxed: false)
toId.serialize(buffer, true)
break
case .inputInvoiceStarGiftTransfer(let stargift, let toId):
if boxed {
buffer.appendInt32(1247763417)
@ -351,6 +359,8 @@ public extension Api {
return ("inputInvoiceSlug", [("slug", slug as Any)])
case .inputInvoiceStarGift(let flags, let peer, let giftId, let message):
return ("inputInvoiceStarGift", [("flags", flags as Any), ("peer", peer as Any), ("giftId", giftId as Any), ("message", message as Any)])
case .inputInvoiceStarGiftResale(let slug, let toId):
return ("inputInvoiceStarGiftResale", [("slug", slug as Any), ("toId", toId as Any)])
case .inputInvoiceStarGiftTransfer(let stargift, let toId):
return ("inputInvoiceStarGiftTransfer", [("stargift", stargift as Any), ("toId", toId as Any)])
case .inputInvoiceStarGiftUpgrade(let flags, let stargift):
@ -480,6 +490,22 @@ public extension Api {
return nil
}
}
public static func parse_inputInvoiceStarGiftResale(_ reader: BufferReader) -> InputInvoice? {
var _1: String?
_1 = parseString(reader)
var _2: Api.InputPeer?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.InputPeer
}
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.InputInvoice.inputInvoiceStarGiftResale(slug: _1!, toId: _2!)
}
else {
return nil
}
}
public static func parse_inputInvoiceStarGiftTransfer(_ reader: BufferReader) -> InputInvoice? {
var _1: Api.InputSavedStarGift?
if let signature = reader.readInt32() {

View file

@ -381,7 +381,7 @@ public extension Api {
case messageActionSetChatWallPaper(flags: Int32, wallpaper: Api.WallPaper)
case messageActionSetMessagesTTL(flags: Int32, period: Int32, autoSettingFrom: Int64?)
case messageActionStarGift(flags: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, convertStars: Int64?, upgradeMsgId: Int32?, upgradeStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?)
case messageActionStarGiftUnique(flags: Int32, gift: Api.StarGift, canExportAt: Int32?, transferStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?)
case messageActionStarGiftUnique(flags: Int32, gift: Api.StarGift, canExportAt: Int32?, transferStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, resaleStars: Int64?)
case messageActionSuggestProfilePhoto(photo: Api.Photo)
case messageActionTopicCreate(flags: Int32, title: String, iconColor: Int32, iconEmojiId: Int64?)
case messageActionTopicEdit(flags: Int32, title: String?, iconEmojiId: Int64?, closed: Api.Bool?, hidden: Api.Bool?)
@ -767,9 +767,9 @@ public extension Api {
if Int(flags) & Int(1 << 12) != 0 {peer!.serialize(buffer, true)}
if Int(flags) & Int(1 << 12) != 0 {serializeInt64(savedId!, buffer: buffer, boxed: false)}
break
case .messageActionStarGiftUnique(let flags, let gift, let canExportAt, let transferStars, let fromId, let peer, let savedId):
case .messageActionStarGiftUnique(let flags, let gift, let canExportAt, let transferStars, let fromId, let peer, let savedId, let resaleStars):
if boxed {
buffer.appendInt32(-1394619519)
buffer.appendInt32(1600878025)
}
serializeInt32(flags, buffer: buffer, boxed: false)
gift.serialize(buffer, true)
@ -778,6 +778,7 @@ public extension Api {
if Int(flags) & Int(1 << 6) != 0 {fromId!.serialize(buffer, true)}
if Int(flags) & Int(1 << 7) != 0 {peer!.serialize(buffer, true)}
if Int(flags) & Int(1 << 7) != 0 {serializeInt64(savedId!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 8) != 0 {serializeInt64(resaleStars!, buffer: buffer, boxed: false)}
break
case .messageActionSuggestProfilePhoto(let photo):
if boxed {
@ -912,8 +913,8 @@ public extension Api {
return ("messageActionSetMessagesTTL", [("flags", flags as Any), ("period", period as Any), ("autoSettingFrom", autoSettingFrom as Any)])
case .messageActionStarGift(let flags, let gift, let message, let convertStars, let upgradeMsgId, let upgradeStars, let fromId, let peer, let savedId):
return ("messageActionStarGift", [("flags", flags as Any), ("gift", gift as Any), ("message", message as Any), ("convertStars", convertStars as Any), ("upgradeMsgId", upgradeMsgId as Any), ("upgradeStars", upgradeStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any)])
case .messageActionStarGiftUnique(let flags, let gift, let canExportAt, let transferStars, let fromId, let peer, let savedId):
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)])
case .messageActionStarGiftUnique(let flags, let gift, let canExportAt, let transferStars, let fromId, let peer, let savedId, let resaleStars):
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)])
case .messageActionSuggestProfilePhoto(let photo):
return ("messageActionSuggestProfilePhoto", [("photo", photo as Any)])
case .messageActionTopicCreate(let flags, let title, let iconColor, let iconEmojiId):
@ -1672,6 +1673,8 @@ public extension Api {
} }
var _7: Int64?
if Int(_1!) & Int(1 << 7) != 0 {_7 = reader.readInt64() }
var _8: Int64?
if Int(_1!) & Int(1 << 8) != 0 {_8 = reader.readInt64() }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil
@ -1679,8 +1682,9 @@ public extension Api {
let _c5 = (Int(_1!) & Int(1 << 6) == 0) || _5 != nil
let _c6 = (Int(_1!) & Int(1 << 7) == 0) || _6 != nil
let _c7 = (Int(_1!) & Int(1 << 7) == 0) || _7 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
return Api.MessageAction.messageActionStarGiftUnique(flags: _1!, gift: _2!, canExportAt: _3, transferStars: _4, fromId: _5, peer: _6, savedId: _7)
let _c8 = (Int(_1!) & Int(1 << 8) == 0) || _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.MessageAction.messageActionStarGiftUnique(flags: _1!, gift: _2!, canExportAt: _3, transferStars: _4, fromId: _5, peer: _6, savedId: _7, resaleStars: _8)
}
else {
return nil

View file

@ -628,14 +628,14 @@ public extension Api {
}
public extension Api {
enum StarGift: TypeConstructorDescription {
case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?, upgradeStars: Int64?)
case starGiftUnique(flags: Int32, id: Int64, title: String, slug: String, num: Int32, ownerId: Api.Peer?, ownerName: String?, ownerAddress: String?, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32, giftAddress: String?)
case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, availabilityResale: Int64?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?, upgradeStars: Int64?, resellMinStars: Int64?, title: String?)
case starGiftUnique(flags: Int32, id: Int64, title: String, slug: String, num: Int32, ownerId: Api.Peer?, ownerName: String?, ownerAddress: String?, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32, giftAddress: String?, resellStars: Int64?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars):
case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title):
if boxed {
buffer.appendInt32(46953416)
buffer.appendInt32(-970274264)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(id, buffer: buffer, boxed: false)
@ -643,14 +643,17 @@ public extension Api {
serializeInt64(stars, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(availabilityRemains!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(availabilityTotal!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 4) != 0 {serializeInt64(availabilityResale!, buffer: buffer, boxed: false)}
serializeInt64(convertStars, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(firstSaleDate!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(lastSaleDate!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 3) != 0 {serializeInt64(upgradeStars!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 4) != 0 {serializeInt64(resellMinStars!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 5) != 0 {serializeString(title!, buffer: buffer, boxed: false)}
break
case .starGiftUnique(let flags, let id, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress):
case .starGiftUnique(let flags, let id, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellStars):
if boxed {
buffer.appendInt32(1549979985)
buffer.appendInt32(1678891913)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(id, buffer: buffer, boxed: false)
@ -668,16 +671,17 @@ public extension Api {
serializeInt32(availabilityIssued, buffer: buffer, boxed: false)
serializeInt32(availabilityTotal, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 3) != 0 {serializeString(giftAddress!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 4) != 0 {serializeInt64(resellStars!, buffer: buffer, boxed: false)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars):
return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any), ("upgradeStars", upgradeStars as Any)])
case .starGiftUnique(let flags, let id, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress):
return ("starGiftUnique", [("flags", flags as Any), ("id", id as Any), ("title", title as Any), ("slug", slug as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("ownerName", ownerName as Any), ("ownerAddress", ownerAddress as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any), ("giftAddress", giftAddress as Any)])
case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title):
return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("availabilityResale", availabilityResale as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any), ("upgradeStars", upgradeStars as Any), ("resellMinStars", resellMinStars as Any), ("title", title as Any)])
case .starGiftUnique(let flags, let id, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellStars):
return ("starGiftUnique", [("flags", flags as Any), ("id", id as Any), ("title", title as Any), ("slug", slug as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("ownerName", ownerName as Any), ("ownerAddress", ownerAddress as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any), ("giftAddress", giftAddress as Any), ("resellStars", resellStars as Any)])
}
}
@ -697,25 +701,34 @@ public extension Api {
var _6: Int32?
if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt32() }
var _7: Int64?
_7 = reader.readInt64()
var _8: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_8 = reader.readInt32() }
if Int(_1!) & Int(1 << 4) != 0 {_7 = reader.readInt64() }
var _8: Int64?
_8 = reader.readInt64()
var _9: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_9 = reader.readInt32() }
var _10: Int64?
if Int(_1!) & Int(1 << 3) != 0 {_10 = reader.readInt64() }
var _10: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_10 = reader.readInt32() }
var _11: Int64?
if Int(_1!) & Int(1 << 3) != 0 {_11 = reader.readInt64() }
var _12: Int64?
if Int(_1!) & Int(1 << 4) != 0 {_12 = reader.readInt64() }
var _13: String?
if Int(_1!) & Int(1 << 5) != 0 {_13 = parseString(reader) }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil
let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil
let _c7 = _7 != nil
let _c8 = (Int(_1!) & Int(1 << 1) == 0) || _8 != nil
let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil
let _c8 = _8 != nil
let _c9 = (Int(_1!) & Int(1 << 1) == 0) || _9 != nil
let _c10 = (Int(_1!) & Int(1 << 3) == 0) || _10 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 {
return Api.StarGift.starGift(flags: _1!, id: _2!, sticker: _3!, stars: _4!, availabilityRemains: _5, availabilityTotal: _6, convertStars: _7!, firstSaleDate: _8, lastSaleDate: _9, upgradeStars: _10)
let _c10 = (Int(_1!) & Int(1 << 1) == 0) || _10 != nil
let _c11 = (Int(_1!) & Int(1 << 3) == 0) || _11 != nil
let _c12 = (Int(_1!) & Int(1 << 4) == 0) || _12 != nil
let _c13 = (Int(_1!) & Int(1 << 5) == 0) || _13 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 {
return Api.StarGift.starGift(flags: _1!, id: _2!, sticker: _3!, stars: _4!, availabilityRemains: _5, availabilityTotal: _6, availabilityResale: _7, convertStars: _8!, firstSaleDate: _9, lastSaleDate: _10, upgradeStars: _11, resellMinStars: _12, title: _13)
}
else {
return nil
@ -750,6 +763,8 @@ public extension Api {
_11 = reader.readInt32()
var _12: String?
if Int(_1!) & Int(1 << 3) != 0 {_12 = parseString(reader) }
var _13: Int64?
if Int(_1!) & Int(1 << 4) != 0 {_13 = reader.readInt64() }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
@ -762,8 +777,9 @@ public extension Api {
let _c10 = _10 != nil
let _c11 = _11 != nil
let _c12 = (Int(_1!) & Int(1 << 3) == 0) || _12 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 {
return Api.StarGift.starGiftUnique(flags: _1!, id: _2!, title: _3!, slug: _4!, num: _5!, ownerId: _6, ownerName: _7, ownerAddress: _8, attributes: _9!, availabilityIssued: _10!, availabilityTotal: _11!, giftAddress: _12)
let _c13 = (Int(_1!) & Int(1 << 4) == 0) || _13 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 {
return Api.StarGift.starGiftUnique(flags: _1!, id: _2!, title: _3!, slug: _4!, num: _5!, ownerId: _6, ownerName: _7, ownerAddress: _8, attributes: _9!, availabilityIssued: _10!, availabilityTotal: _11!, giftAddress: _12, resellStars: _13)
}
else {
return nil
@ -774,18 +790,19 @@ public extension Api {
}
public extension Api {
enum StarGiftAttribute: TypeConstructorDescription {
case starGiftAttributeBackdrop(name: String, centerColor: Int32, edgeColor: Int32, patternColor: Int32, textColor: Int32, rarityPermille: Int32)
case starGiftAttributeBackdrop(name: String, backdropId: Int32, centerColor: Int32, edgeColor: Int32, patternColor: Int32, textColor: Int32, rarityPermille: Int32)
case starGiftAttributeModel(name: String, document: Api.Document, rarityPermille: Int32)
case starGiftAttributeOriginalDetails(flags: Int32, senderId: Api.Peer?, recipientId: Api.Peer, date: Int32, message: Api.TextWithEntities?)
case starGiftAttributePattern(name: String, document: Api.Document, rarityPermille: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .starGiftAttributeBackdrop(let name, let centerColor, let edgeColor, let patternColor, let textColor, let rarityPermille):
case .starGiftAttributeBackdrop(let name, let backdropId, let centerColor, let edgeColor, let patternColor, let textColor, let rarityPermille):
if boxed {
buffer.appendInt32(-1809377438)
buffer.appendInt32(-650279524)
}
serializeString(name, buffer: buffer, boxed: false)
serializeInt32(backdropId, buffer: buffer, boxed: false)
serializeInt32(centerColor, buffer: buffer, boxed: false)
serializeInt32(edgeColor, buffer: buffer, boxed: false)
serializeInt32(patternColor, buffer: buffer, boxed: false)
@ -823,8 +840,8 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .starGiftAttributeBackdrop(let name, let centerColor, let edgeColor, let patternColor, let textColor, let rarityPermille):
return ("starGiftAttributeBackdrop", [("name", name as Any), ("centerColor", centerColor as Any), ("edgeColor", edgeColor as Any), ("patternColor", patternColor as Any), ("textColor", textColor as Any), ("rarityPermille", rarityPermille as Any)])
case .starGiftAttributeBackdrop(let name, let backdropId, let centerColor, let edgeColor, let patternColor, let textColor, let rarityPermille):
return ("starGiftAttributeBackdrop", [("name", name as Any), ("backdropId", backdropId as Any), ("centerColor", centerColor as Any), ("edgeColor", edgeColor as Any), ("patternColor", patternColor as Any), ("textColor", textColor as Any), ("rarityPermille", rarityPermille as Any)])
case .starGiftAttributeModel(let name, let document, let rarityPermille):
return ("starGiftAttributeModel", [("name", name as Any), ("document", document as Any), ("rarityPermille", rarityPermille as Any)])
case .starGiftAttributeOriginalDetails(let flags, let senderId, let recipientId, let date, let message):
@ -847,14 +864,17 @@ public extension Api {
_5 = reader.readInt32()
var _6: Int32?
_6 = reader.readInt32()
var _7: Int32?
_7 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
let _c6 = _6 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
return Api.StarGiftAttribute.starGiftAttributeBackdrop(name: _1!, centerColor: _2!, edgeColor: _3!, patternColor: _4!, textColor: _5!, rarityPermille: _6!)
let _c7 = _7 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
return Api.StarGiftAttribute.starGiftAttributeBackdrop(name: _1!, backdropId: _2!, centerColor: _3!, edgeColor: _4!, patternColor: _5!, textColor: _6!, rarityPermille: _7!)
}
else {
return nil
@ -931,55 +951,39 @@ public extension Api {
}
}
public extension Api {
enum StarRefProgram: TypeConstructorDescription {
case starRefProgram(flags: Int32, botId: Int64, commissionPermille: Int32, durationMonths: Int32?, endDate: Int32?, dailyRevenuePerUser: Api.StarsAmount?)
enum StarGiftAttributeCounter: TypeConstructorDescription {
case starGiftAttributeCounter(attribute: Api.StarGiftAttributeId, count: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .starRefProgram(let flags, let botId, let commissionPermille, let durationMonths, let endDate, let dailyRevenuePerUser):
case .starGiftAttributeCounter(let attribute, let count):
if boxed {
buffer.appendInt32(-586389774)
buffer.appendInt32(783398488)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(botId, buffer: buffer, boxed: false)
serializeInt32(commissionPermille, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(durationMonths!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(endDate!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 2) != 0 {dailyRevenuePerUser!.serialize(buffer, true)}
attribute.serialize(buffer, true)
serializeInt32(count, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .starRefProgram(let flags, let botId, let commissionPermille, let durationMonths, let endDate, let dailyRevenuePerUser):
return ("starRefProgram", [("flags", flags as Any), ("botId", botId as Any), ("commissionPermille", commissionPermille as Any), ("durationMonths", durationMonths as Any), ("endDate", endDate as Any), ("dailyRevenuePerUser", dailyRevenuePerUser as Any)])
case .starGiftAttributeCounter(let attribute, let count):
return ("starGiftAttributeCounter", [("attribute", attribute as Any), ("count", count as Any)])
}
}
public static func parse_starRefProgram(_ reader: BufferReader) -> StarRefProgram? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int64?
_2 = reader.readInt64()
var _3: Int32?
_3 = reader.readInt32()
var _4: Int32?
if Int(_1!) & Int(1 << 0) != 0 {_4 = reader.readInt32() }
var _5: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() }
var _6: Api.StarsAmount?
if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() {
_6 = Api.parse(reader, signature: signature) as? Api.StarsAmount
} }
public static func parse_starGiftAttributeCounter(_ reader: BufferReader) -> StarGiftAttributeCounter? {
var _1: Api.StarGiftAttributeId?
if let signature = reader.readInt32() {
_1 = Api.parse(reader, signature: signature) as? Api.StarGiftAttributeId
}
var _2: Int32?
_2 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil
let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil
let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
return Api.StarRefProgram.starRefProgram(flags: _1!, botId: _2!, commissionPermille: _3!, durationMonths: _4, endDate: _5, dailyRevenuePerUser: _6)
if _c1 && _c2 {
return Api.StarGiftAttributeCounter.starGiftAttributeCounter(attribute: _1!, count: _2!)
}
else {
return nil
@ -989,37 +993,73 @@ public extension Api {
}
}
public extension Api {
enum StarsAmount: TypeConstructorDescription {
case starsAmount(amount: Int64, nanos: Int32)
enum StarGiftAttributeId: TypeConstructorDescription {
case starGiftAttributeIdBackdrop(backdropId: Int32)
case starGiftAttributeIdModel(documentId: Int64)
case starGiftAttributeIdPattern(documentId: Int64)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .starsAmount(let amount, let nanos):
case .starGiftAttributeIdBackdrop(let backdropId):
if boxed {
buffer.appendInt32(-1145654109)
buffer.appendInt32(520210263)
}
serializeInt64(amount, buffer: buffer, boxed: false)
serializeInt32(nanos, buffer: buffer, boxed: false)
serializeInt32(backdropId, buffer: buffer, boxed: false)
break
case .starGiftAttributeIdModel(let documentId):
if boxed {
buffer.appendInt32(1219145276)
}
serializeInt64(documentId, buffer: buffer, boxed: false)
break
case .starGiftAttributeIdPattern(let documentId):
if boxed {
buffer.appendInt32(1242965043)
}
serializeInt64(documentId, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .starsAmount(let amount, let nanos):
return ("starsAmount", [("amount", amount as Any), ("nanos", nanos as Any)])
case .starGiftAttributeIdBackdrop(let backdropId):
return ("starGiftAttributeIdBackdrop", [("backdropId", backdropId as Any)])
case .starGiftAttributeIdModel(let documentId):
return ("starGiftAttributeIdModel", [("documentId", documentId as Any)])
case .starGiftAttributeIdPattern(let documentId):
return ("starGiftAttributeIdPattern", [("documentId", documentId as Any)])
}
}
public static func parse_starsAmount(_ reader: BufferReader) -> StarsAmount? {
public static func parse_starGiftAttributeIdBackdrop(_ reader: BufferReader) -> StarGiftAttributeId? {
var _1: Int32?
_1 = reader.readInt32()
let _c1 = _1 != nil
if _c1 {
return Api.StarGiftAttributeId.starGiftAttributeIdBackdrop(backdropId: _1!)
}
else {
return nil
}
}
public static func parse_starGiftAttributeIdModel(_ reader: BufferReader) -> StarGiftAttributeId? {
var _1: Int64?
_1 = reader.readInt64()
var _2: Int32?
_2 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.StarsAmount.starsAmount(amount: _1!, nanos: _2!)
if _c1 {
return Api.StarGiftAttributeId.starGiftAttributeIdModel(documentId: _1!)
}
else {
return nil
}
}
public static func parse_starGiftAttributeIdPattern(_ reader: BufferReader) -> StarGiftAttributeId? {
var _1: Int64?
_1 = reader.readInt64()
let _c1 = _1 != nil
if _c1 {
return Api.StarGiftAttributeId.starGiftAttributeIdPattern(documentId: _1!)
}
else {
return nil

View file

@ -1,3 +1,101 @@
public extension Api {
enum StarRefProgram: TypeConstructorDescription {
case starRefProgram(flags: Int32, botId: Int64, commissionPermille: Int32, durationMonths: Int32?, endDate: Int32?, dailyRevenuePerUser: Api.StarsAmount?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .starRefProgram(let flags, let botId, let commissionPermille, let durationMonths, let endDate, let dailyRevenuePerUser):
if boxed {
buffer.appendInt32(-586389774)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(botId, buffer: buffer, boxed: false)
serializeInt32(commissionPermille, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(durationMonths!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(endDate!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 2) != 0 {dailyRevenuePerUser!.serialize(buffer, true)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .starRefProgram(let flags, let botId, let commissionPermille, let durationMonths, let endDate, let dailyRevenuePerUser):
return ("starRefProgram", [("flags", flags as Any), ("botId", botId as Any), ("commissionPermille", commissionPermille as Any), ("durationMonths", durationMonths as Any), ("endDate", endDate as Any), ("dailyRevenuePerUser", dailyRevenuePerUser as Any)])
}
}
public static func parse_starRefProgram(_ reader: BufferReader) -> StarRefProgram? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int64?
_2 = reader.readInt64()
var _3: Int32?
_3 = reader.readInt32()
var _4: Int32?
if Int(_1!) & Int(1 << 0) != 0 {_4 = reader.readInt32() }
var _5: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() }
var _6: Api.StarsAmount?
if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() {
_6 = Api.parse(reader, signature: signature) as? Api.StarsAmount
} }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil
let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil
let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
return Api.StarRefProgram.starRefProgram(flags: _1!, botId: _2!, commissionPermille: _3!, durationMonths: _4, endDate: _5, dailyRevenuePerUser: _6)
}
else {
return nil
}
}
}
}
public extension Api {
enum StarsAmount: TypeConstructorDescription {
case starsAmount(amount: Int64, nanos: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .starsAmount(let amount, let nanos):
if boxed {
buffer.appendInt32(-1145654109)
}
serializeInt64(amount, buffer: buffer, boxed: false)
serializeInt32(nanos, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .starsAmount(let amount, let nanos):
return ("starsAmount", [("amount", amount as Any), ("nanos", nanos as Any)])
}
}
public static func parse_starsAmount(_ reader: BufferReader) -> StarsAmount? {
var _1: Int64?
_1 = reader.readInt64()
var _2: Int32?
_2 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.StarsAmount.starsAmount(amount: _1!, nanos: _2!)
}
else {
return nil
}
}
}
}
public extension Api {
enum StarsGiftOption: TypeConstructorDescription {
case starsGiftOption(flags: Int32, stars: Int64, storeProduct: String?, currency: String, amount: Int64)

View file

@ -1,3 +1,101 @@
public extension Api.payments {
enum ResaleStarGifts: TypeConstructorDescription {
case resaleStarGifts(flags: Int32, count: Int32, gifts: [Api.StarGift], nextOffset: String?, attributes: [Api.StarGiftAttribute]?, attributesHash: Int64?, chats: [Api.Chat], counters: [Api.StarGiftAttributeCounter]?, users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .resaleStarGifts(let flags, let count, let gifts, let nextOffset, let attributes, let attributesHash, let chats, let counters, let users):
if boxed {
buffer.appendInt32(-1803939105)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(count, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(gifts.count))
for item in gifts {
item.serialize(buffer, true)
}
if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(attributes!.count))
for item in attributes! {
item.serialize(buffer, true)
}}
if Int(flags) & Int(1 << 1) != 0 {serializeInt64(attributesHash!, buffer: buffer, boxed: false)}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(chats.count))
for item in chats {
item.serialize(buffer, true)
}
if Int(flags) & Int(1 << 2) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(counters!.count))
for item in counters! {
item.serialize(buffer, true)
}}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(users.count))
for item in users {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .resaleStarGifts(let flags, let count, let gifts, let nextOffset, let attributes, let attributesHash, let chats, let counters, let users):
return ("resaleStarGifts", [("flags", flags as Any), ("count", count as Any), ("gifts", gifts as Any), ("nextOffset", nextOffset as Any), ("attributes", attributes as Any), ("attributesHash", attributesHash as Any), ("chats", chats as Any), ("counters", counters as Any), ("users", users as Any)])
}
}
public static func parse_resaleStarGifts(_ reader: BufferReader) -> ResaleStarGifts? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int32?
_2 = reader.readInt32()
var _3: [Api.StarGift]?
if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarGift.self)
}
var _4: String?
if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) }
var _5: [Api.StarGiftAttribute]?
if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() {
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarGiftAttribute.self)
} }
var _6: Int64?
if Int(_1!) & Int(1 << 1) != 0 {_6 = reader.readInt64() }
var _7: [Api.Chat]?
if let _ = reader.readInt32() {
_7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
}
var _8: [Api.StarGiftAttributeCounter]?
if Int(_1!) & Int(1 << 2) != 0 {if let _ = reader.readInt32() {
_8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarGiftAttributeCounter.self)
} }
var _9: [Api.User]?
if let _ = reader.readInt32() {
_9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil
let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil
let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil
let _c7 = _7 != nil
let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil
let _c9 = _9 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 {
return Api.payments.ResaleStarGifts.resaleStarGifts(flags: _1!, count: _2!, gifts: _3!, nextOffset: _4, attributes: _5, attributesHash: _6, chats: _7!, counters: _8, users: _9!)
}
else {
return nil
}
}
}
}
public extension Api.payments {
enum SavedInfo: TypeConstructorDescription {
case savedInfo(flags: Int32, savedInfo: Api.PaymentRequestedInfo?)
@ -1598,171 +1696,3 @@ public extension Api.stats {
}
}
public extension Api.stats {
enum BroadcastStats: TypeConstructorDescription {
case broadcastStats(period: Api.StatsDateRangeDays, followers: Api.StatsAbsValueAndPrev, viewsPerPost: Api.StatsAbsValueAndPrev, sharesPerPost: Api.StatsAbsValueAndPrev, reactionsPerPost: Api.StatsAbsValueAndPrev, viewsPerStory: Api.StatsAbsValueAndPrev, sharesPerStory: Api.StatsAbsValueAndPrev, reactionsPerStory: Api.StatsAbsValueAndPrev, enabledNotifications: Api.StatsPercentValue, growthGraph: Api.StatsGraph, followersGraph: Api.StatsGraph, muteGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, interactionsGraph: Api.StatsGraph, ivInteractionsGraph: Api.StatsGraph, viewsBySourceGraph: Api.StatsGraph, newFollowersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, reactionsByEmotionGraph: Api.StatsGraph, storyInteractionsGraph: Api.StatsGraph, storyReactionsByEmotionGraph: Api.StatsGraph, recentPostsInteractions: [Api.PostInteractionCounters])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .broadcastStats(let period, let followers, let viewsPerPost, let sharesPerPost, let reactionsPerPost, let viewsPerStory, let sharesPerStory, let reactionsPerStory, let enabledNotifications, let growthGraph, let followersGraph, let muteGraph, let topHoursGraph, let interactionsGraph, let ivInteractionsGraph, let viewsBySourceGraph, let newFollowersBySourceGraph, let languagesGraph, let reactionsByEmotionGraph, let storyInteractionsGraph, let storyReactionsByEmotionGraph, let recentPostsInteractions):
if boxed {
buffer.appendInt32(963421692)
}
period.serialize(buffer, true)
followers.serialize(buffer, true)
viewsPerPost.serialize(buffer, true)
sharesPerPost.serialize(buffer, true)
reactionsPerPost.serialize(buffer, true)
viewsPerStory.serialize(buffer, true)
sharesPerStory.serialize(buffer, true)
reactionsPerStory.serialize(buffer, true)
enabledNotifications.serialize(buffer, true)
growthGraph.serialize(buffer, true)
followersGraph.serialize(buffer, true)
muteGraph.serialize(buffer, true)
topHoursGraph.serialize(buffer, true)
interactionsGraph.serialize(buffer, true)
ivInteractionsGraph.serialize(buffer, true)
viewsBySourceGraph.serialize(buffer, true)
newFollowersBySourceGraph.serialize(buffer, true)
languagesGraph.serialize(buffer, true)
reactionsByEmotionGraph.serialize(buffer, true)
storyInteractionsGraph.serialize(buffer, true)
storyReactionsByEmotionGraph.serialize(buffer, true)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(recentPostsInteractions.count))
for item in recentPostsInteractions {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .broadcastStats(let period, let followers, let viewsPerPost, let sharesPerPost, let reactionsPerPost, let viewsPerStory, let sharesPerStory, let reactionsPerStory, let enabledNotifications, let growthGraph, let followersGraph, let muteGraph, let topHoursGraph, let interactionsGraph, let ivInteractionsGraph, let viewsBySourceGraph, let newFollowersBySourceGraph, let languagesGraph, let reactionsByEmotionGraph, let storyInteractionsGraph, let storyReactionsByEmotionGraph, let recentPostsInteractions):
return ("broadcastStats", [("period", period as Any), ("followers", followers as Any), ("viewsPerPost", viewsPerPost as Any), ("sharesPerPost", sharesPerPost as Any), ("reactionsPerPost", reactionsPerPost as Any), ("viewsPerStory", viewsPerStory as Any), ("sharesPerStory", sharesPerStory as Any), ("reactionsPerStory", reactionsPerStory as Any), ("enabledNotifications", enabledNotifications as Any), ("growthGraph", growthGraph as Any), ("followersGraph", followersGraph as Any), ("muteGraph", muteGraph as Any), ("topHoursGraph", topHoursGraph as Any), ("interactionsGraph", interactionsGraph as Any), ("ivInteractionsGraph", ivInteractionsGraph as Any), ("viewsBySourceGraph", viewsBySourceGraph as Any), ("newFollowersBySourceGraph", newFollowersBySourceGraph as Any), ("languagesGraph", languagesGraph as Any), ("reactionsByEmotionGraph", reactionsByEmotionGraph as Any), ("storyInteractionsGraph", storyInteractionsGraph as Any), ("storyReactionsByEmotionGraph", storyReactionsByEmotionGraph as Any), ("recentPostsInteractions", recentPostsInteractions as Any)])
}
}
public static func parse_broadcastStats(_ reader: BufferReader) -> BroadcastStats? {
var _1: Api.StatsDateRangeDays?
if let signature = reader.readInt32() {
_1 = Api.parse(reader, signature: signature) as? Api.StatsDateRangeDays
}
var _2: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _3: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_3 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _4: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _5: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_5 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _6: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_6 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _7: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_7 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _8: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_8 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _9: Api.StatsPercentValue?
if let signature = reader.readInt32() {
_9 = Api.parse(reader, signature: signature) as? Api.StatsPercentValue
}
var _10: Api.StatsGraph?
if let signature = reader.readInt32() {
_10 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _11: Api.StatsGraph?
if let signature = reader.readInt32() {
_11 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _12: Api.StatsGraph?
if let signature = reader.readInt32() {
_12 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _13: Api.StatsGraph?
if let signature = reader.readInt32() {
_13 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _14: Api.StatsGraph?
if let signature = reader.readInt32() {
_14 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _15: Api.StatsGraph?
if let signature = reader.readInt32() {
_15 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _16: Api.StatsGraph?
if let signature = reader.readInt32() {
_16 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _17: Api.StatsGraph?
if let signature = reader.readInt32() {
_17 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _18: Api.StatsGraph?
if let signature = reader.readInt32() {
_18 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _19: Api.StatsGraph?
if let signature = reader.readInt32() {
_19 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _20: Api.StatsGraph?
if let signature = reader.readInt32() {
_20 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _21: Api.StatsGraph?
if let signature = reader.readInt32() {
_21 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _22: [Api.PostInteractionCounters]?
if let _ = reader.readInt32() {
_22 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PostInteractionCounters.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
let _c6 = _6 != nil
let _c7 = _7 != nil
let _c8 = _8 != nil
let _c9 = _9 != nil
let _c10 = _10 != nil
let _c11 = _11 != nil
let _c12 = _12 != nil
let _c13 = _13 != nil
let _c14 = _14 != nil
let _c15 = _15 != nil
let _c16 = _16 != nil
let _c17 = _17 != nil
let _c18 = _18 != nil
let _c19 = _19 != nil
let _c20 = _20 != nil
let _c21 = _21 != nil
let _c22 = _22 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 {
return Api.stats.BroadcastStats.broadcastStats(period: _1!, followers: _2!, viewsPerPost: _3!, sharesPerPost: _4!, reactionsPerPost: _5!, viewsPerStory: _6!, sharesPerStory: _7!, reactionsPerStory: _8!, enabledNotifications: _9!, growthGraph: _10!, followersGraph: _11!, muteGraph: _12!, topHoursGraph: _13!, interactionsGraph: _14!, ivInteractionsGraph: _15!, viewsBySourceGraph: _16!, newFollowersBySourceGraph: _17!, languagesGraph: _18!, reactionsByEmotionGraph: _19!, storyInteractionsGraph: _20!, storyReactionsByEmotionGraph: _21!, recentPostsInteractions: _22!)
}
else {
return nil
}
}
}
}

View file

@ -1,3 +1,171 @@
public extension Api.stats {
enum BroadcastStats: TypeConstructorDescription {
case broadcastStats(period: Api.StatsDateRangeDays, followers: Api.StatsAbsValueAndPrev, viewsPerPost: Api.StatsAbsValueAndPrev, sharesPerPost: Api.StatsAbsValueAndPrev, reactionsPerPost: Api.StatsAbsValueAndPrev, viewsPerStory: Api.StatsAbsValueAndPrev, sharesPerStory: Api.StatsAbsValueAndPrev, reactionsPerStory: Api.StatsAbsValueAndPrev, enabledNotifications: Api.StatsPercentValue, growthGraph: Api.StatsGraph, followersGraph: Api.StatsGraph, muteGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, interactionsGraph: Api.StatsGraph, ivInteractionsGraph: Api.StatsGraph, viewsBySourceGraph: Api.StatsGraph, newFollowersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, reactionsByEmotionGraph: Api.StatsGraph, storyInteractionsGraph: Api.StatsGraph, storyReactionsByEmotionGraph: Api.StatsGraph, recentPostsInteractions: [Api.PostInteractionCounters])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .broadcastStats(let period, let followers, let viewsPerPost, let sharesPerPost, let reactionsPerPost, let viewsPerStory, let sharesPerStory, let reactionsPerStory, let enabledNotifications, let growthGraph, let followersGraph, let muteGraph, let topHoursGraph, let interactionsGraph, let ivInteractionsGraph, let viewsBySourceGraph, let newFollowersBySourceGraph, let languagesGraph, let reactionsByEmotionGraph, let storyInteractionsGraph, let storyReactionsByEmotionGraph, let recentPostsInteractions):
if boxed {
buffer.appendInt32(963421692)
}
period.serialize(buffer, true)
followers.serialize(buffer, true)
viewsPerPost.serialize(buffer, true)
sharesPerPost.serialize(buffer, true)
reactionsPerPost.serialize(buffer, true)
viewsPerStory.serialize(buffer, true)
sharesPerStory.serialize(buffer, true)
reactionsPerStory.serialize(buffer, true)
enabledNotifications.serialize(buffer, true)
growthGraph.serialize(buffer, true)
followersGraph.serialize(buffer, true)
muteGraph.serialize(buffer, true)
topHoursGraph.serialize(buffer, true)
interactionsGraph.serialize(buffer, true)
ivInteractionsGraph.serialize(buffer, true)
viewsBySourceGraph.serialize(buffer, true)
newFollowersBySourceGraph.serialize(buffer, true)
languagesGraph.serialize(buffer, true)
reactionsByEmotionGraph.serialize(buffer, true)
storyInteractionsGraph.serialize(buffer, true)
storyReactionsByEmotionGraph.serialize(buffer, true)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(recentPostsInteractions.count))
for item in recentPostsInteractions {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .broadcastStats(let period, let followers, let viewsPerPost, let sharesPerPost, let reactionsPerPost, let viewsPerStory, let sharesPerStory, let reactionsPerStory, let enabledNotifications, let growthGraph, let followersGraph, let muteGraph, let topHoursGraph, let interactionsGraph, let ivInteractionsGraph, let viewsBySourceGraph, let newFollowersBySourceGraph, let languagesGraph, let reactionsByEmotionGraph, let storyInteractionsGraph, let storyReactionsByEmotionGraph, let recentPostsInteractions):
return ("broadcastStats", [("period", period as Any), ("followers", followers as Any), ("viewsPerPost", viewsPerPost as Any), ("sharesPerPost", sharesPerPost as Any), ("reactionsPerPost", reactionsPerPost as Any), ("viewsPerStory", viewsPerStory as Any), ("sharesPerStory", sharesPerStory as Any), ("reactionsPerStory", reactionsPerStory as Any), ("enabledNotifications", enabledNotifications as Any), ("growthGraph", growthGraph as Any), ("followersGraph", followersGraph as Any), ("muteGraph", muteGraph as Any), ("topHoursGraph", topHoursGraph as Any), ("interactionsGraph", interactionsGraph as Any), ("ivInteractionsGraph", ivInteractionsGraph as Any), ("viewsBySourceGraph", viewsBySourceGraph as Any), ("newFollowersBySourceGraph", newFollowersBySourceGraph as Any), ("languagesGraph", languagesGraph as Any), ("reactionsByEmotionGraph", reactionsByEmotionGraph as Any), ("storyInteractionsGraph", storyInteractionsGraph as Any), ("storyReactionsByEmotionGraph", storyReactionsByEmotionGraph as Any), ("recentPostsInteractions", recentPostsInteractions as Any)])
}
}
public static func parse_broadcastStats(_ reader: BufferReader) -> BroadcastStats? {
var _1: Api.StatsDateRangeDays?
if let signature = reader.readInt32() {
_1 = Api.parse(reader, signature: signature) as? Api.StatsDateRangeDays
}
var _2: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _3: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_3 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _4: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _5: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_5 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _6: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_6 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _7: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_7 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _8: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_8 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _9: Api.StatsPercentValue?
if let signature = reader.readInt32() {
_9 = Api.parse(reader, signature: signature) as? Api.StatsPercentValue
}
var _10: Api.StatsGraph?
if let signature = reader.readInt32() {
_10 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _11: Api.StatsGraph?
if let signature = reader.readInt32() {
_11 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _12: Api.StatsGraph?
if let signature = reader.readInt32() {
_12 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _13: Api.StatsGraph?
if let signature = reader.readInt32() {
_13 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _14: Api.StatsGraph?
if let signature = reader.readInt32() {
_14 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _15: Api.StatsGraph?
if let signature = reader.readInt32() {
_15 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _16: Api.StatsGraph?
if let signature = reader.readInt32() {
_16 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _17: Api.StatsGraph?
if let signature = reader.readInt32() {
_17 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _18: Api.StatsGraph?
if let signature = reader.readInt32() {
_18 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _19: Api.StatsGraph?
if let signature = reader.readInt32() {
_19 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _20: Api.StatsGraph?
if let signature = reader.readInt32() {
_20 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _21: Api.StatsGraph?
if let signature = reader.readInt32() {
_21 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _22: [Api.PostInteractionCounters]?
if let _ = reader.readInt32() {
_22 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PostInteractionCounters.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
let _c6 = _6 != nil
let _c7 = _7 != nil
let _c8 = _8 != nil
let _c9 = _9 != nil
let _c10 = _10 != nil
let _c11 = _11 != nil
let _c12 = _12 != nil
let _c13 = _13 != nil
let _c14 = _14 != nil
let _c15 = _15 != nil
let _c16 = _16 != nil
let _c17 = _17 != nil
let _c18 = _18 != nil
let _c19 = _19 != nil
let _c20 = _20 != nil
let _c21 = _21 != nil
let _c22 = _22 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 {
return Api.stats.BroadcastStats.broadcastStats(period: _1!, followers: _2!, viewsPerPost: _3!, sharesPerPost: _4!, reactionsPerPost: _5!, viewsPerStory: _6!, sharesPerStory: _7!, reactionsPerStory: _8!, enabledNotifications: _9!, growthGraph: _10!, followersGraph: _11!, muteGraph: _12!, topHoursGraph: _13!, interactionsGraph: _14!, ivInteractionsGraph: _15!, viewsBySourceGraph: _16!, newFollowersBySourceGraph: _17!, languagesGraph: _18!, reactionsByEmotionGraph: _19!, storyInteractionsGraph: _20!, storyReactionsByEmotionGraph: _21!, recentPostsInteractions: _22!)
}
else {
return nil
}
}
}
}
public extension Api.stats {
enum MegagroupStats: TypeConstructorDescription {
case megagroupStats(period: Api.StatsDateRangeDays, members: Api.StatsAbsValueAndPrev, messages: Api.StatsAbsValueAndPrev, viewers: Api.StatsAbsValueAndPrev, posters: Api.StatsAbsValueAndPrev, growthGraph: Api.StatsGraph, membersGraph: Api.StatsGraph, newMembersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, messagesGraph: Api.StatsGraph, actionsGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, weekdaysGraph: Api.StatsGraph, topPosters: [Api.StatsGroupTopPoster], topAdmins: [Api.StatsGroupTopAdmin], topInviters: [Api.StatsGroupTopInviter], users: [Api.User])
@ -1444,59 +1612,3 @@ public extension Api.updates {
}
}
public extension Api.upload {
enum CdnFile: TypeConstructorDescription {
case cdnFile(bytes: Buffer)
case cdnFileReuploadNeeded(requestToken: Buffer)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .cdnFile(let bytes):
if boxed {
buffer.appendInt32(-1449145777)
}
serializeBytes(bytes, buffer: buffer, boxed: false)
break
case .cdnFileReuploadNeeded(let requestToken):
if boxed {
buffer.appendInt32(-290921362)
}
serializeBytes(requestToken, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .cdnFile(let bytes):
return ("cdnFile", [("bytes", bytes as Any)])
case .cdnFileReuploadNeeded(let requestToken):
return ("cdnFileReuploadNeeded", [("requestToken", requestToken as Any)])
}
}
public static func parse_cdnFile(_ reader: BufferReader) -> CdnFile? {
var _1: Buffer?
_1 = parseBytes(reader)
let _c1 = _1 != nil
if _c1 {
return Api.upload.CdnFile.cdnFile(bytes: _1!)
}
else {
return nil
}
}
public static func parse_cdnFileReuploadNeeded(_ reader: BufferReader) -> CdnFile? {
var _1: Buffer?
_1 = parseBytes(reader)
let _c1 = _1 != nil
if _c1 {
return Api.upload.CdnFile.cdnFileReuploadNeeded(requestToken: _1!)
}
else {
return nil
}
}
}
}

View file

@ -1,3 +1,59 @@
public extension Api.upload {
enum CdnFile: TypeConstructorDescription {
case cdnFile(bytes: Buffer)
case cdnFileReuploadNeeded(requestToken: Buffer)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .cdnFile(let bytes):
if boxed {
buffer.appendInt32(-1449145777)
}
serializeBytes(bytes, buffer: buffer, boxed: false)
break
case .cdnFileReuploadNeeded(let requestToken):
if boxed {
buffer.appendInt32(-290921362)
}
serializeBytes(requestToken, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .cdnFile(let bytes):
return ("cdnFile", [("bytes", bytes as Any)])
case .cdnFileReuploadNeeded(let requestToken):
return ("cdnFileReuploadNeeded", [("requestToken", requestToken as Any)])
}
}
public static func parse_cdnFile(_ reader: BufferReader) -> CdnFile? {
var _1: Buffer?
_1 = parseBytes(reader)
let _c1 = _1 != nil
if _c1 {
return Api.upload.CdnFile.cdnFile(bytes: _1!)
}
else {
return nil
}
}
public static func parse_cdnFileReuploadNeeded(_ reader: BufferReader) -> CdnFile? {
var _1: Buffer?
_1 = parseBytes(reader)
let _c1 = _1 != nil
if _c1 {
return Api.upload.CdnFile.cdnFileReuploadNeeded(requestToken: _1!)
}
else {
return nil
}
}
}
}
public extension Api.upload {
enum File: TypeConstructorDescription {
case file(type: Api.storage.FileType, mtime: Int32, bytes: Buffer)

View file

@ -9332,6 +9332,30 @@ public extension Api.functions.payments {
})
}
}
public extension Api.functions.payments {
static func getResaleStarGifts(flags: Int32, attributesHash: Int64?, giftId: Int64, attributes: [Api.StarGiftAttributeId]?, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.ResaleStarGifts>) {
let buffer = Buffer()
buffer.appendInt32(2053087798)
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeInt64(attributesHash!, buffer: buffer, boxed: false)}
serializeInt64(giftId, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(attributes!.count))
for item in attributes! {
item.serialize(buffer, true)
}}
serializeString(offset, buffer: buffer, boxed: false)
serializeInt32(limit, buffer: buffer, boxed: false)
return (FunctionDescription(name: "payments.getResaleStarGifts", parameters: [("flags", String(describing: flags)), ("attributesHash", String(describing: attributesHash)), ("giftId", String(describing: giftId)), ("attributes", String(describing: attributes)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.ResaleStarGifts? in
let reader = BufferReader(buffer)
var result: Api.payments.ResaleStarGifts?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.payments.ResaleStarGifts
}
return result
})
}
}
public extension Api.functions.payments {
static func getSavedInfo() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.SavedInfo>) {
let buffer = Buffer()
@ -9766,6 +9790,22 @@ public extension Api.functions.payments {
})
}
}
public extension Api.functions.payments {
static func updateStarGiftPrice(slug: String, resellStars: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()
buffer.appendInt32(-489360582)
serializeString(slug, buffer: buffer, boxed: false)
serializeInt64(resellStars, buffer: buffer, boxed: false)
return (FunctionDescription(name: "payments.updateStarGiftPrice", parameters: [("slug", String(describing: slug)), ("resellStars", String(describing: resellStars))]), 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.payments {
static func upgradeStarGift(flags: Int32, stargift: Api.InputSavedStarGift) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()

View file

@ -192,11 +192,11 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
return nil
}
return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, upgradeMessageId: upgradeMessageId, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId))
case let .messageActionStarGiftUnique(flags, apiGift, canExportAt, transferStars, fromId, peer, savedId):
case let .messageActionStarGiftUnique(flags, apiGift, canExportAt, transferStars, fromId, peer, savedId, resaleStars):
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId))
return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleStars: resaleStars))
case let .messageActionPaidMessagesRefunded(count, stars):
return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars))
case let .messageActionPaidMessagesPrice(stars):

View file

@ -109,6 +109,10 @@ func messageFilterForTagMask(_ tagMask: MessageTags) -> Api.MessagesFilter? {
return Api.MessagesFilter.inputMessagesFilterGif
} else if tagMask == .pinned {
return Api.MessagesFilter.inputMessagesFilterPinned
} else if tagMask == .voice {
return Api.MessagesFilter.inputMessagesFilterVoice
} else if tagMask == .roundVideo {
return Api.MessagesFilter.inputMessagesFilterRoundVideo
} else {
return nil
}

View file

@ -350,10 +350,11 @@ func managedUniqueStarGifts(accountPeerId: PeerId, postbox: Postbox, network: Ne
attributes: [
.model(name: "", file: file, rarity: 0),
.pattern(name: "", file: patternFile, rarity: 0),
.backdrop(name: "", innerColor: innerColor, outerColor: outerColor, patternColor: patternColor, textColor: textColor, rarity: 0)
.backdrop(name: "", id: 0, innerColor: innerColor, outerColor: outerColor, patternColor: patternColor, textColor: textColor, rarity: 0)
],
availability: StarGift.UniqueGift.Availability(issued: 0, total: 0),
giftAddress: nil
giftAddress: nil,
resellStars: nil
)
if let entry = CodableEntry(RecentStarGiftItem(gift)) {
items.append(OrderedItemListEntry(id: RecentStarGiftItemId(id).rawValue, contents: entry))

View file

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

View file

@ -170,8 +170,10 @@ public extension MessageTags {
static let video = MessageTags(rawValue: 1 << 9)
static let pinned = MessageTags(rawValue: 1 << 10)
static let unseenReaction = MessageTags(rawValue: 1 << 11)
static let voice = MessageTags(rawValue: 1 << 12)
static let roundVideo = MessageTags(rawValue: 1 << 13)
static let all: MessageTags = [.photoOrVideo, .file, .music, .webPage, .voiceOrInstantVideo, .unseenPersonalMessage, .liveLocation, .gif, .photo, .video, .pinned, .unseenReaction]
static let all: MessageTags = [.photoOrVideo, .file, .music, .webPage, .voiceOrInstantVideo, .unseenPersonalMessage, .liveLocation, .gif, .photo, .video, .pinned, .unseenReaction, .voice, .roundVideo]
}
public extension GlobalMessageTags {

View file

@ -156,7 +156,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?)
case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?)
case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?)
case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?)
case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleStars: Int64?)
case paidMessagesRefunded(count: Int32, stars: Int64)
case paidMessagesPriceEdited(stars: Int64)
case conferenceCall(ConferenceCall)
@ -283,7 +283,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case 44:
self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId"), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"))
case 45:
self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"))
self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), resaleStars: decoder.decodeOptionalInt64ForKey("resaleStars"))
case 46:
self = .paidMessagesRefunded(count: decoder.decodeInt32ForKey("count", orElse: 0), stars: decoder.decodeInt64ForKey("stars", orElse: 0))
case 47:
@ -633,7 +633,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "savedId")
}
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, peerId, senderId, savedId):
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, peerId, senderId, savedId, resaleStars):
encoder.encodeInt32(45, forKey: "_rawValue")
encoder.encodeObject(gift, forKey: "gift")
encoder.encodeBool(isUpgrade, forKey: "isUpgrade")
@ -665,6 +665,11 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "savedId")
}
if let resaleStars {
encoder.encodeInt64(resaleStars, forKey: "resaleStars")
} else {
encoder.encodeNil(forKey: "resaleStars")
}
case let .paidMessagesRefunded(count, stars):
encoder.encodeInt32(46, forKey: "_rawValue")
encoder.encodeInt32(count, forKey: "count")
@ -718,7 +723,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
peerIds.append(senderId)
}
return peerIds
case let .starGiftUnique(_, _, _, _, _, _, _, peerId, senderId, _):
case let .starGiftUnique(_, _, _, _, _, _, _, peerId, senderId, _, _):
var peerIds: [PeerId] = []
if let peerId {
peerIds.append(peerId)

View file

@ -94,7 +94,7 @@ public extension TelegramEngine {
file = fileValue
case let .pattern(_, patternFileValue, _):
patternFile = patternFileValue
case let .backdrop(_, innerColorValue, outerColorValue, patternColorValue, textColorValue, _):
case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, textColorValue, _):
innerColor = innerColorValue
outerColor = outerColorValue
patternColor = patternColorValue

View file

@ -17,6 +17,7 @@ public enum BotPaymentInvoiceSource {
case starGiftUpgrade(keepOriginalInfo: Bool, reference: StarGiftReference)
case starGiftTransfer(reference: StarGiftReference, toPeerId: EnginePeer.Id)
case premiumGift(peerId: EnginePeer.Id, option: CachedPremiumGiftOption, text: String?, entities: [MessageTextEntity]?)
case starGiftResale(slug: String, toPeerId: EnginePeer.Id)
}
public struct BotPaymentInvoiceFields: OptionSet {
@ -400,6 +401,11 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv
message = .textWithEntities(text: text, entities: entities.flatMap { apiEntitiesFromMessageTextEntities($0, associatedPeers: SimpleDictionary()) } ?? [])
}
return .inputInvoicePremiumGiftStars(flags: flags, userId: inputUser, months: option.months, message: message)
case let .starGiftResale(slug, toPeerId):
guard let peer = transaction.getPeer(toPeerId), let inputPeer = apiInputPeer(peer) else {
return nil
}
return .inputInvoiceStarGiftResale(slug: slug, toId: inputPeer)
}
}
@ -733,7 +739,7 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa
receiptMessageId = id
}
}
case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer, .premiumGift:
case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer, .premiumGift, .starGiftResale:
receiptMessageId = nil
}
}

View file

@ -46,6 +46,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
enum CodingKeys: String, CodingKey {
case id
case title
case file
case price
case convertStars
@ -59,24 +60,54 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
enum CodingKeys: String, CodingKey {
case remains
case total
case resale
case minResaleStars
}
public let remains: Int32
public let total: Int32
public let resale: Int64
public let minResaleStars: Int64?
public init(remains: Int32, total: Int32) {
public init(remains: Int32, total: Int32, resale: Int64, minResaleStars: Int64?) {
self.remains = remains
self.total = total
self.resale = resale
self.minResaleStars = minResaleStars
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.remains = try container.decode(Int32.self, forKey: .remains)
self.total = try container.decode(Int32.self, forKey: .total)
self.resale = (try? container.decodeIfPresent(Int64.self, forKey: .resale)) ?? 0
self.minResaleStars = try? container.decodeIfPresent(Int64.self, forKey: .minResaleStars)
}
public init(decoder: PostboxDecoder) {
self.remains = decoder.decodeInt32ForKey(CodingKeys.remains.rawValue, orElse: 0)
self.total = decoder.decodeInt32ForKey(CodingKeys.total.rawValue, orElse: 0)
self.resale = decoder.decodeInt64ForKey(CodingKeys.resale.rawValue, orElse: 0)
self.minResaleStars = decoder.decodeInt64ForKey(CodingKeys.minResaleStars.rawValue, orElse: 0)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.remains, forKey: .remains)
try container.encode(self.total, forKey: .total)
try container.encode(self.resale, forKey: .resale)
try container.encodeIfPresent(self.minResaleStars, forKey: .minResaleStars)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.remains, forKey: CodingKeys.remains.rawValue)
encoder.encodeInt32(self.total, forKey: CodingKeys.total.rawValue)
encoder.encodeInt64(self.resale, forKey: CodingKeys.resale.rawValue)
if let minResaleStars = self.minResaleStars {
encoder.encodeInt64(minResaleStars, forKey: CodingKeys.minResaleStars.rawValue)
} else {
encoder.encodeNil(forKey: CodingKeys.minResaleStars.rawValue)
}
}
}
@ -110,6 +141,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
}
public let id: Int64
public let title: String?
public let file: TelegramMediaFile
public let price: Int64
public let convertStars: Int64
@ -118,8 +150,9 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
public let flags: Flags
public let upgradeStars: Int64?
public init(id: Int64, file: TelegramMediaFile, price: Int64, convertStars: Int64, availability: Availability?, soldOut: SoldOut?, flags: Flags, upgradeStars: Int64?) {
public init(id: Int64, title: String?, file: TelegramMediaFile, price: Int64, convertStars: Int64, availability: Availability?, soldOut: SoldOut?, flags: Flags, upgradeStars: Int64?) {
self.id = id
self.title = title
self.file = file
self.price = price
self.convertStars = convertStars
@ -132,6 +165,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
self.title = try container.decodeIfPresent(String.self, forKey: .title)
if let fileData = try container.decodeIfPresent(Data.self, forKey: .file), let file = PostboxDecoder(buffer: MemoryBuffer(data: fileData)).decodeRootObject() as? TelegramMediaFile {
self.file = file
@ -149,6 +183,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
public init(decoder: PostboxDecoder) {
self.id = decoder.decodeInt64ForKey(CodingKeys.id.rawValue, orElse: 0)
self.title = decoder.decodeOptionalStringForKey(CodingKeys.title.rawValue)
self.file = decoder.decodeObjectForKey(CodingKeys.file.rawValue) as! TelegramMediaFile
self.price = decoder.decodeInt64ForKey(CodingKeys.price.rawValue, orElse: 0)
self.convertStars = decoder.decodeInt64ForKey(CodingKeys.convertStars.rawValue, orElse: 0)
@ -161,6 +196,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encodeIfPresent(self.title, forKey: .title)
let encoder = PostboxEncoder()
encoder.encodeRootObject(self.file)
@ -177,6 +213,11 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt64(self.id, forKey: CodingKeys.id.rawValue)
if let title = self.title {
encoder.encodeString(title, forKey: CodingKeys.title.rawValue)
} else {
encoder.encodeNil(forKey: CodingKeys.title.rawValue)
}
encoder.encodeObject(self.file, forKey: CodingKeys.file.rawValue)
encoder.encodeInt64(self.price, forKey: CodingKeys.price.rawValue)
encoder.encodeInt64(self.convertStars, forKey: CodingKeys.convertStars.rawValue)
@ -211,6 +252,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
case attributes
case availability
case giftAddress
case resellStars
}
public enum Attribute: Equatable, Codable, PostboxCoding {
@ -218,6 +260,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
case type
case name
case file
case id
case innerColor
case outerColor
case patternColor
@ -239,7 +282,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
case model(name: String, file: TelegramMediaFile, rarity: Int32)
case pattern(name: String, file: TelegramMediaFile, rarity: Int32)
case backdrop(name: String, innerColor: Int32, outerColor: Int32, patternColor: Int32, textColor: Int32, rarity: Int32)
case backdrop(name: String, id: Int32, innerColor: Int32, outerColor: Int32, patternColor: Int32, textColor: Int32, rarity: Int32)
case originalInfo(senderPeerId: EnginePeer.Id?, recipientPeerId: EnginePeer.Id, date: Int32, text: String?, entities: [MessageTextEntity]?)
public var attributeType: AttributeType {
@ -275,6 +318,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
case 2:
self = .backdrop(
name: try container.decode(String.self, forKey: .name),
id: try container.decodeIfPresent(Int32.self, forKey: .id) ?? 0,
innerColor: try container.decode(Int32.self, forKey: .innerColor),
outerColor: try container.decode(Int32.self, forKey: .outerColor),
patternColor: try container.decode(Int32.self, forKey: .patternColor),
@ -313,6 +357,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
case 2:
self = .backdrop(
name: decoder.decodeStringForKey(CodingKeys.name.rawValue, orElse: ""),
id: decoder.decodeInt32ForKey(CodingKeys.id.rawValue, orElse: 0),
innerColor: decoder.decodeInt32ForKey(CodingKeys.innerColor.rawValue, orElse: 0),
outerColor: decoder.decodeInt32ForKey(CodingKeys.outerColor.rawValue, orElse: 0),
patternColor: decoder.decodeInt32ForKey(CodingKeys.patternColor.rawValue, orElse: 0),
@ -346,9 +391,10 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
try container.encode(name, forKey: .name)
try container.encode(file, forKey: .file)
try container.encode(rarity, forKey: .rarity)
case let .backdrop(name, innerColor, outerColor, patternColor, textColor, rarity):
case let .backdrop(name, id, innerColor, outerColor, patternColor, textColor, rarity):
try container.encode(Int32(2), forKey: .type)
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
try container.encode(innerColor, forKey: .innerColor)
try container.encode(outerColor, forKey: .outerColor)
try container.encode(patternColor, forKey: .patternColor)
@ -376,9 +422,10 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
encoder.encodeString(name, forKey: CodingKeys.name.rawValue)
encoder.encodeObject(file, forKey: CodingKeys.file.rawValue)
encoder.encodeInt32(rarity, forKey: CodingKeys.rarity.rawValue)
case let .backdrop(name, innerColor, outerColor, patternColor, textColor, rarity):
case let .backdrop(name, id, innerColor, outerColor, patternColor, textColor, rarity):
encoder.encodeInt32(2, forKey: CodingKeys.type.rawValue)
encoder.encodeString(name, forKey: CodingKeys.name.rawValue)
encoder.encodeInt32(id, forKey: CodingKeys.id.rawValue)
encoder.encodeInt32(innerColor, forKey: CodingKeys.innerColor.rawValue)
encoder.encodeInt32(outerColor, forKey: CodingKeys.outerColor.rawValue)
encoder.encodeInt32(patternColor, forKey: CodingKeys.patternColor.rawValue)
@ -451,8 +498,9 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
public let attributes: [Attribute]
public let availability: Availability
public let giftAddress: String?
public let resellStars: Int64?
public init(id: Int64, title: String, number: Int32, slug: String, owner: Owner, attributes: [Attribute], availability: Availability, giftAddress: String?) {
public init(id: Int64, title: String, number: Int32, slug: String, owner: Owner, attributes: [Attribute], availability: Availability, giftAddress: String?, resellStars: Int64?) {
self.id = id
self.title = title
self.number = number
@ -461,6 +509,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
self.attributes = attributes
self.availability = availability
self.giftAddress = giftAddress
self.resellStars = resellStars
}
public init(from decoder: Decoder) throws {
@ -481,6 +530,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
self.attributes = try container.decode([UniqueGift.Attribute].self, forKey: .attributes)
self.availability = try container.decode(UniqueGift.Availability.self, forKey: .availability)
self.giftAddress = try container.decodeIfPresent(String.self, forKey: .giftAddress)
self.resellStars = try container.decodeIfPresent(Int64.self, forKey: .resellStars)
}
public init(decoder: PostboxDecoder) {
@ -500,6 +550,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
self.attributes = (try? decoder.decodeObjectArrayWithCustomDecoderForKey(CodingKeys.attributes.rawValue, decoder: { UniqueGift.Attribute(decoder: $0) })) ?? []
self.availability = decoder.decodeObjectForKey(CodingKeys.availability.rawValue, decoder: { UniqueGift.Availability(decoder: $0) }) as! UniqueGift.Availability
self.giftAddress = decoder.decodeOptionalStringForKey(CodingKeys.giftAddress.rawValue)
self.resellStars = decoder.decodeOptionalInt64ForKey(CodingKeys.resellStars.rawValue)
}
public func encode(to encoder: Encoder) throws {
@ -519,6 +570,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
try container.encode(self.attributes, forKey: .attributes)
try container.encode(self.availability, forKey: .availability)
try container.encodeIfPresent(self.giftAddress, forKey: .giftAddress)
try container.encodeIfPresent(self.resellStars, forKey: .resellStars)
}
public func encode(_ encoder: PostboxEncoder) {
@ -541,6 +593,25 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
} else {
encoder.encodeNil(forKey: CodingKeys.giftAddress.rawValue)
}
if let resellStars = self.resellStars {
encoder.encodeInt64(resellStars, forKey: CodingKeys.resellStars.rawValue)
} else {
encoder.encodeNil(forKey: CodingKeys.resellStars.rawValue)
}
}
public func withResellStars(_ resellStars: Int64?) -> UniqueGift {
return UniqueGift(
id: self.id,
title: self.title,
number: self.number,
slug: self.slug,
owner: self.owner,
attributes: self.attributes,
availability: self.availability,
giftAddress: self.giftAddress,
resellStars: resellStars
)
}
}
@ -608,7 +679,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
extension StarGift {
init?(apiStarGift: Api.StarGift) {
switch apiStarGift {
case let .starGift(apiFlags, id, sticker, stars, availabilityRemains, availabilityTotal, convertStars, firstSale, lastSale, upgradeStars):
case let .starGift(apiFlags, id, sticker, stars, availabilityRemains, availabilityTotal, availabilityResale, convertStars, firstSale, lastSale, upgradeStars, minResaleStars, title):
var flags = StarGift.Gift.Flags()
if (apiFlags & (1 << 2)) != 0 {
flags.insert(.isBirthdayGift)
@ -616,7 +687,12 @@ extension StarGift {
var availability: StarGift.Gift.Availability?
if let availabilityRemains, let availabilityTotal {
availability = StarGift.Gift.Availability(remains: availabilityRemains, total: availabilityTotal)
availability = StarGift.Gift.Availability(
remains: availabilityRemains,
total: availabilityTotal,
resale: availabilityResale ?? 0,
minResaleStars: minResaleStars
)
}
var soldOut: StarGift.Gift.SoldOut?
if let firstSale, let lastSale {
@ -625,8 +701,8 @@ extension StarGift {
guard let file = telegramMediaFileFromApiDocument(sticker, altDocuments: nil) else {
return nil
}
self = .generic(StarGift.Gift(id: id, file: file, price: stars, convertStars: convertStars, availability: availability, soldOut: soldOut, flags: flags, upgradeStars: upgradeStars))
case let .starGiftUnique(_, id, title, slug, num, ownerPeerId, ownerName, ownerAddress, attributes, availabilityIssued, availabilityTotal, giftAddress):
self = .generic(StarGift.Gift(id: id, title: title, file: file, price: stars, convertStars: convertStars, availability: availability, soldOut: soldOut, flags: flags, upgradeStars: upgradeStars))
case let .starGiftUnique(_, id, title, slug, num, ownerPeerId, ownerName, ownerAddress, attributes, availabilityIssued, availabilityTotal, giftAddress, reselltars):
let owner: StarGift.UniqueGift.Owner
if let ownerAddress {
owner = .address(ownerAddress)
@ -637,7 +713,7 @@ extension StarGift {
} else {
return nil
}
self = .unique(StarGift.UniqueGift(id: id, title: title, number: num, slug: slug, owner: owner, attributes: attributes.compactMap { UniqueGift.Attribute(apiAttribute: $0) }, availability: UniqueGift.Availability(issued: availabilityIssued, total: availabilityTotal), giftAddress: giftAddress))
self = .unique(StarGift.UniqueGift(id: id, title: title, number: num, slug: slug, owner: owner, attributes: attributes.compactMap { UniqueGift.Attribute(apiAttribute: $0) }, availability: UniqueGift.Availability(issued: availabilityIssued, total: availabilityTotal), giftAddress: giftAddress, resellStars: reselltars))
}
}
}
@ -769,6 +845,34 @@ public enum TransferStarGiftError {
case disallowedStarGift
}
public enum BuyStarGiftError {
case generic
}
public enum UpgradeStarGiftError {
case generic
}
func _internal_buyStarGift(account: Account, slug: String, peerId: EnginePeer.Id) -> Signal<Never, BuyStarGiftError> {
let source: BotPaymentInvoiceSource = .starGiftResale(slug: slug, toPeerId: peerId)
return _internal_fetchBotPaymentForm(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, source: source, themeParams: nil)
|> map(Optional.init)
|> `catch` { error -> Signal<BotPaymentForm?, BuyStarGiftError> in
return .fail(.generic)
}
|> mapToSignal { paymentForm in
if let paymentForm {
return _internal_sendStarsPaymentForm(account: account, formId: paymentForm.id, source: source)
|> mapError { _ -> BuyStarGiftError in
return .generic
}
|> ignoreValues
} else {
return .fail(.generic)
}
}
}
func _internal_transferStarGift(account: Account, prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) -> Signal<Never, TransferStarGiftError> {
return account.postbox.transaction { transaction -> (Api.InputPeer, Api.InputSavedStarGift)? in
guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputPeer), let starGift = reference.apiStarGiftReference(transaction: transaction) else {
@ -821,10 +925,6 @@ func _internal_transferStarGift(account: Account, prepaid: Bool, reference: Star
}
}
public enum UpgradeStarGiftError {
case generic
}
func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: StarGiftReference, keepOriginalInfo: Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError> {
if let formId {
let source: BotPaymentInvoiceSource = .starGiftUpgrade(keepOriginalInfo: keepOriginalInfo, reference: reference)
@ -863,7 +963,7 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star
case let .updateNewMessage(message, _, _):
if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: false) {
for media in message.media {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, peerId, _, savedId) = action.action, case let .Id(messageId) = message.id {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, peerId, _, savedId, _) = action.action, case let .Id(messageId) = message.id {
let reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)
@ -1090,7 +1190,7 @@ private final class ProfileGiftsContextImpl {
if !filter.contains(.unique) {
flags |= (1 << 4)
}
return network.request(Api.functions.payments.getSavedStarGifts(flags: flags, peer: inputPeer, offset: initialNextOffset ?? "", limit: 32))
return network.request(Api.functions.payments.getSavedStarGifts(flags: flags, peer: inputPeer, offset: initialNextOffset ?? "", limit: 36))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.payments.SavedStarGifts?, NoError> in
return .single(nil)
@ -1359,6 +1459,27 @@ private final class ProfileGiftsContextImpl {
return _internal_transferStarGift(account: self.account, prepaid: prepaid, reference: reference, peerId: peerId)
}
func buyStarGift(slug: String, peerId: EnginePeer.Id) -> Signal<Never, BuyStarGiftError> {
if let count = self.count {
self.count = max(0, count - 1)
}
self.gifts.removeAll(where: { gift in
if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug {
return true
}
return false
})
self.filteredGifts.removeAll(where: { gift in
if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug {
return true
}
return false
})
self.pushState()
return _internal_buyStarGift(account: self.account, slug: slug, peerId: peerId)
}
func upgradeStarGift(formId: Int64?, reference: StarGiftReference, keepOriginalInfo: Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError> {
return Signal { [weak self] subscriber in
guard let self else {
@ -1388,6 +1509,41 @@ private final class ProfileGiftsContextImpl {
}
}
func updateStarGiftResellPrice(slug: String, price: Int64?) {
self.actionDisposable.set(
_internal_updateStarGiftResalePrice(account: self.account, slug: slug, price: price).startStrict()
)
if let index = self.gifts.firstIndex(where: { gift in
if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug {
return true
}
return false
}) {
if case let .unique(uniqueGift) = self.gifts[index].gift {
let updatedUniqueGift = uniqueGift.withResellStars(price)
let updatedGift = self.gifts[index].withGift(.unique(updatedUniqueGift))
self.gifts[index] = updatedGift
}
}
if let index = self.filteredGifts.firstIndex(where: { gift in
if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug {
return true
}
return false
}) {
if case let .unique(uniqueGift) = self.filteredGifts[index].gift {
let updatedUniqueGift = uniqueGift.withResellStars(price)
let updatedGift = self.filteredGifts[index].withGift(.unique(updatedUniqueGift))
self.filteredGifts[index] = updatedGift
}
}
self.pushState()
}
func toggleStarGiftsNotifications(enabled: Bool) {
self.actionDisposable.set(
_internal_toggleStarGiftsNotifications(account: self.account, peerId: self.peerId, enabled: enabled).startStrict()
@ -1583,6 +1739,25 @@ public final class ProfileGiftsContext {
try container.encodeIfPresent(self.transferStars, forKey: .transferStars)
}
public func withGift(_ gift: TelegramCore.StarGift) -> StarGift {
return StarGift(
gift: gift,
reference: self.reference,
fromPeer: self.fromPeer,
date: self.date,
text: self.text,
entities: self.entities,
nameHidden: self.nameHidden,
savedToProfile: self.savedToProfile,
pinnedToTop: self.pinnedToTop,
convertStars: self.convertStars,
canUpgrade: self.canUpgrade,
canExportDate: self.canExportDate,
upgradeStars: self.upgradeStars,
transferStars: self.transferStars
)
}
public func withSavedToProfile(_ savedToProfile: Bool) -> StarGift {
return StarGift(
gift: self.gift,
@ -1720,6 +1895,20 @@ public final class ProfileGiftsContext {
}
}
public func buyStarGift(slug: String, peerId: EnginePeer.Id) -> Signal<Never, BuyStarGiftError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.buyStarGift(slug: slug, peerId: peerId).start(error: { error in
subscriber.putError(error)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
public func transferStarGift(prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) -> Signal<Never, TransferStarGiftError> {
return Signal { subscriber in
let disposable = MetaDisposable()
@ -1750,6 +1939,12 @@ public final class ProfileGiftsContext {
}
}
public func updateStarGiftResellPrice(slug: String, price: Int64?) {
self.impl.with { impl in
impl.updateStarGiftResellPrice(slug: slug, price: price)
}
}
public func toggleStarGiftsNotifications(enabled: Bool) {
self.impl.with { impl in
impl.toggleStarGiftsNotifications(enabled: enabled)
@ -1841,8 +2036,8 @@ extension StarGift.UniqueGift.Attribute {
return nil
}
self = .pattern(name: name, file: file, rarity: rarityPermille)
case let .starGiftAttributeBackdrop(name, centerColor, edgeColor, patternColor, textColor, rarityPermille):
self = .backdrop(name: name, innerColor: centerColor, outerColor: edgeColor, patternColor: patternColor, textColor: textColor, rarity: rarityPermille)
case let .starGiftAttributeBackdrop(name, id, centerColor, edgeColor, patternColor, textColor, rarityPermille):
self = .backdrop(name: name, id: id, innerColor: centerColor, outerColor: edgeColor, patternColor: patternColor, textColor: textColor, rarity: rarityPermille)
case let .starGiftAttributeOriginalDetails(_, sender, recipient, date, message):
var text: String?
var entities: [MessageTextEntity]?
@ -2070,6 +2265,21 @@ func _internal_toggleStarGiftsNotifications(account: Account, peerId: EnginePeer
}
}
func _internal_updateStarGiftResalePrice(account: Account, slug: String, price: Int64?) -> Signal<Never, NoError> {
return account.network.request(Api.functions.payments.updateStarGiftPrice(slug: slug, resellStars: price ?? 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Void, NoError> in
if let updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
|> ignoreValues
}
public extension StarGift.UniqueGift {
var itemFile: TelegramMediaFile? {
for attribute in self.attributes {
@ -2080,3 +2290,289 @@ public extension StarGift.UniqueGift {
return nil
}
}
private final class ResaleGiftsContextImpl {
private let queue: Queue
private let account: Account
private let giftId: Int64
private let disposable = MetaDisposable()
private var sorting: ResaleGiftsContext.Sorting = .value
private var filterAttributes: [ResaleGiftsContext.Attribute] = []
private var gifts: [StarGift] = []
private var attributes: [StarGift.UniqueGift.Attribute] = []
private var attributeCount: [ResaleGiftsContext.Attribute: Int32] = [:]
private var count: Int32?
private var dataState: ResaleGiftsContext.State.DataState = .ready(canLoadMore: true, nextOffset: nil)
var _state: ResaleGiftsContext.State?
private let stateValue = Promise<ResaleGiftsContext.State>()
var state: Signal<ResaleGiftsContext.State, NoError> {
return self.stateValue.get()
}
init(
queue: Queue,
account: Account,
giftId: Int64
) {
self.queue = queue
self.account = account
self.giftId = giftId
self.loadMore()
}
deinit {
self.disposable.dispose()
}
func reload() {
self.gifts = []
self.dataState = .ready(canLoadMore: true, nextOffset: nil)
self.loadMore(reload: true)
}
func loadMore(reload: Bool = false) {
let giftId = self.giftId
let accountPeerId = self.account.peerId
let network = self.account.network
let postbox = self.account.postbox
let sorting = self.sorting
let filterAttributes = self.filterAttributes
let dataState = self.dataState
if case let .ready(true, initialNextOffset) = dataState {
self.dataState = .loading
if !reload {
self.pushState()
}
var flags: Int32 = 0
switch sorting {
case .date:
break
case .value:
flags |= (1 << 1)
case .number:
flags |= (1 << 2)
}
var apiAttributes: [Api.StarGiftAttributeId]?
if !filterAttributes.isEmpty {
flags |= (1 << 3)
apiAttributes = filterAttributes.map {
switch $0 {
case let .model(id):
return .starGiftAttributeIdModel(documentId: id)
case let .pattern(id):
return .starGiftAttributeIdPattern(documentId: id)
case let .backdrop(id):
return .starGiftAttributeIdBackdrop(backdropId: id)
}
}
}
var attributesHash: Int64?
if "".isEmpty {
flags |= (1 << 0)
attributesHash = 0
}
let signal = network.request(Api.functions.payments.getResaleStarGifts(flags: flags, attributesHash: attributesHash, giftId: giftId, attributes: apiAttributes, offset: initialNextOffset ?? "", limit: 36))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.payments.ResaleStarGifts?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<([StarGift], [StarGift.UniqueGift.Attribute], [ResaleGiftsContext.Attribute: Int32], Int32, String?), NoError> in
guard let result else {
return .single(([], [], [:], 0, nil))
}
return postbox.transaction { transaction -> ([StarGift], [StarGift.UniqueGift.Attribute], [ResaleGiftsContext.Attribute: Int32], Int32, String?) in
switch result {
case let .resaleStarGifts(_, count, gifts, nextOffset, attributes, attributesHash, chats, counters, users):
let _ = attributesHash
var resultAttributes: [StarGift.UniqueGift.Attribute] = []
if let attributes {
resultAttributes = attributes.compactMap { StarGift.UniqueGift.Attribute(apiAttribute: $0) }
}
var attributeCount: [ResaleGiftsContext.Attribute: Int32] = [:]
if let counters {
for counter in counters {
switch counter {
case let .starGiftAttributeCounter(attribute, count):
switch attribute {
case let .starGiftAttributeIdModel(documentId):
attributeCount[.model(documentId)] = count
case let .starGiftAttributeIdPattern(documentId):
attributeCount[.pattern(documentId)] = count
case let .starGiftAttributeIdBackdrop(backdropId):
attributeCount[.backdrop(backdropId)] = count
}
}
}
}
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
var mappedGifts: [StarGift] = []
for gift in gifts {
if let mappedGift = StarGift(apiStarGift: gift), case let .unique(uniqueGift) = mappedGift, let resellStars = uniqueGift.resellStars, resellStars > 0 {
mappedGifts.append(mappedGift)
}
}
return (mappedGifts, resultAttributes, attributeCount, count, nextOffset)
}
}
}
self.disposable.set((signal
|> deliverOn(self.queue)).start(next: { [weak self] (gifts, attributes, attributeCount, count, nextOffset) in
guard let self else {
return
}
if initialNextOffset == nil || reload {
self.gifts = gifts
} else {
self.gifts.append(contentsOf: gifts)
}
let updatedCount = max(Int32(self.gifts.count), count)
self.count = updatedCount
self.attributes = attributes
if !attributeCount.isEmpty {
self.attributeCount = attributeCount
}
self.dataState = .ready(canLoadMore: count != 0 && updatedCount > self.gifts.count && nextOffset != nil, nextOffset: nextOffset)
self.pushState()
}))
}
}
func updateFilterAttributes(_ filterAttributes: [ResaleGiftsContext.Attribute]) {
guard self.filterAttributes != filterAttributes else {
return
}
self.filterAttributes = filterAttributes
self.dataState = .ready(canLoadMore: true, nextOffset: nil)
self.pushState()
self.loadMore()
}
func updateSorting(_ sorting: ResaleGiftsContext.Sorting) {
guard self.sorting != sorting else {
return
}
self.sorting = sorting
self.dataState = .ready(canLoadMore: true, nextOffset: nil)
self.pushState()
self.loadMore()
}
private func pushState() {
let state = ResaleGiftsContext.State(
sorting: self.sorting,
filterAttributes: self.filterAttributes,
gifts: self.gifts,
attributes: self.attributes,
attributeCount: self.attributeCount,
count: self.count,
dataState: self.dataState
)
self._state = state
self.stateValue.set(.single(state))
}
}
public final class ResaleGiftsContext {
public enum Sorting: Equatable {
case date
case value
case number
}
public enum Attribute: Equatable, Hashable {
case model(Int64)
case pattern(Int64)
case backdrop(Int32)
}
public struct State: Equatable {
public enum DataState: Equatable {
case loading
case ready(canLoadMore: Bool, nextOffset: String?)
}
public var sorting: Sorting
public var filterAttributes: [Attribute]
public var gifts: [StarGift]
public var attributes: [StarGift.UniqueGift.Attribute]
public var attributeCount: [Attribute: Int32]
public var count: Int32?
public var dataState: ResaleGiftsContext.State.DataState
}
private let queue: Queue = .mainQueue()
private let impl: QueueLocalObject<ResaleGiftsContextImpl>
public var state: Signal<ResaleGiftsContext.State, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.state.start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
public init(
account: Account,
giftId: Int64
) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return ResaleGiftsContextImpl(queue: queue, account: account, giftId: giftId)
})
}
public func loadMore() {
self.impl.with { impl in
impl.loadMore()
}
}
public func updateSorting(_ sorting: ResaleGiftsContext.Sorting) {
self.impl.with { impl in
impl.updateSorting(sorting)
}
}
public func updateFilterAttributes(_ attributes: [ResaleGiftsContext.Attribute]) {
self.impl.with { impl in
impl.updateFilterAttributes(attributes)
}
}
public var currentState: ResaleGiftsContext.State? {
var state: ResaleGiftsContext.State?
self.impl.syncWith { impl in
state = impl._state
}
return state
}
}

View file

@ -1524,10 +1524,10 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
receiptMessageId = id
}
}
case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer, .premiumGift:
case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer, .premiumGift, .starGiftResale:
receiptMessageId = nil
}
} else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, peerId, _, savedId) = action.action, case let .Id(messageId) = message.id {
} else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, peerId, _, savedId, _) = action.action, case let .Id(messageId) = message.id {
let reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)

View file

@ -125,6 +125,10 @@ public extension TelegramEngine {
return _internal_transferStarGift(account: self.account, prepaid: prepaid, reference: reference, peerId: peerId)
}
public func buyStarGift(slug: String, peerId: EnginePeer.Id) -> Signal<Never, BuyStarGiftError> {
return _internal_buyStarGift(account: self.account, slug: slug, peerId: peerId)
}
public func upgradeStarGift(formId: Int64?, reference: StarGiftReference, keepOriginalInfo: Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError> {
return _internal_upgradeStarGift(account: self.account, formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo)
}
@ -148,6 +152,10 @@ public extension TelegramEngine {
public func toggleStarGiftsNotifications(peerId: EnginePeer.Id, enabled: Bool) -> Signal<Never, NoError> {
return _internal_toggleStarGiftsNotifications(account: self.account, peerId: peerId, enabled: enabled)
}
public func updateStarGiftResalePrice(slug: String, price: Int64?) -> Signal<Never, NoError> {
return _internal_updateStarGiftResalePrice(account: self.account, slug: slug, price: price)
}
}
}

View file

@ -326,7 +326,7 @@ func _internal_updatePeerStarGiftStatus(account: Account, peerId: PeerId, starGi
file = fileValue
case let .pattern(_, patternFileValue, _):
patternFile = patternFileValue
case let .backdrop(_, innerColorValue, outerColorValue, patternColorValue, textColorValue, _):
case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, textColorValue, _):
innerColor = innerColorValue
outerColor = outerColorValue
patternColor = patternColorValue

View file

@ -1165,7 +1165,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
}
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, peerId, senderId, _):
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, peerId, senderId, _, _):
if case let .unique(gift) = gift {
if !forAdditionalServiceMessage && !"".isEmpty {
attributedString = NSAttributedString(string: "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))", font: titleFont, textColor: primaryTextColor)

View file

@ -463,6 +463,7 @@ swift_library(
"//submodules/TelegramUI/Components/MiniAppListScreen",
"//submodules/TelegramUI/Components/Stars/StarsIntroScreen",
"//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen",
"//submodules/TelegramUI/Components/Gifts/GiftStoreScreen",
"//submodules/TelegramUI/Components/ContentReportScreen",
"//submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen",
"//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent",
@ -472,6 +473,7 @@ swift_library(
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/TelegramUI/Components/CheckComponent",
"//submodules/TelegramUI/Components/MarqueeComponent",
"//third-party/recaptcha:RecaptchaEnterprise",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View file

@ -1651,6 +1651,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
case videoCollage(VideoCollage)
case asset(PHAsset)
case draft(MediaEditorDraft)
case assets([PHAsset])
func withPIPPosition(_ position: CameraScreenImpl.PIPPosition) -> Result {
switch self {
@ -2726,9 +2727,13 @@ public class CameraScreenImpl: ViewController, CameraScreen {
self.additionalPreviewView.isEnabled = false
self.collageView?.isEnabled = false
#if targetEnvironment(simulator)
#else
Queue.mainQueue().after(0.3) {
self.previewBlurPromise.set(true)
}
#endif
self.camera?.stopCapture()
self.cameraIsActive = false
@ -3627,7 +3632,11 @@ public class CameraScreenImpl: ViewController, CameraScreen {
if self.cameraState.isCollageEnabled, let collage = self.node.collage {
selectionLimit = collage.grid.count - collage.results.count
} else {
selectionLimit = 6
if self.cameraState.isCollageEnabled {
selectionLimit = 6
} else {
selectionLimit = 10
}
}
controller = self.context.sharedContext.makeStoryMediaPickerScreen(
context: self.context,
@ -3698,44 +3707,52 @@ public class CameraScreenImpl: ViewController, CameraScreen {
}
}
}
}, multipleCompletion: { [weak self] results in
}, multipleCompletion: { [weak self] results, collage in
guard let self else {
return
}
if !self.cameraState.isCollageEnabled {
var selectedGrid: Camera.CollageGrid = collageGrids.first!
for grid in collageGrids {
if grid.count == results.count {
selectedGrid = grid
break
if collage {
if !self.cameraState.isCollageEnabled {
var selectedGrid: Camera.CollageGrid = collageGrids.first!
for grid in collageGrids {
if grid.count == results.count {
selectedGrid = grid
break
}
}
self.updateCameraState({
$0.updatedIsCollageEnabled(true).updatedCollageProgress(0.0).updatedIsDualCameraEnabled(false).updatedCollageGrid(selectedGrid)
}, transition: .spring(duration: 0.3))
}
self.updateCameraState({
$0.updatedIsCollageEnabled(true).updatedCollageProgress(0.0).updatedIsDualCameraEnabled(false).updatedCollageGrid(selectedGrid)
}, transition: .spring(duration: 0.3))
}
if let assets = results as? [PHAsset] {
var results: [Signal<CameraScreenImpl.Result, NoError>] = []
for asset in assets {
if asset.mediaType == .video && asset.duration > 1.0 {
results.append(.single(.asset(asset)))
} else {
results.append(
assetImage(asset: asset, targetSize: CGSize(width: 1080, height: 1080), exact: false, deliveryMode: .highQualityFormat)
|> runOn(Queue.concurrentDefaultQueue())
|> mapToSignal { image -> Signal<CameraScreenImpl.Result, NoError> in
if let image {
return .single(.image(Result.Image(image: image, additionalImage: nil, additionalImagePosition: .topLeft)))
} else {
return .complete()
if let assets = results as? [PHAsset] {
var results: [Signal<CameraScreenImpl.Result, NoError>] = []
for asset in assets {
if asset.mediaType == .video && asset.duration > 1.0 {
results.append(.single(.asset(asset)))
} else {
results.append(
assetImage(asset: asset, targetSize: CGSize(width: 1080, height: 1080), exact: false, deliveryMode: .highQualityFormat)
|> runOn(Queue.concurrentDefaultQueue())
|> mapToSignal { image -> Signal<CameraScreenImpl.Result, NoError> in
if let image {
return .single(.image(Result.Image(image: image, additionalImage: nil, additionalImagePosition: .topLeft)))
} else {
return .complete()
}
}
}
)
)
}
}
self.node.collage?.addResults(signals: results)
}
} else {
if let assets = results as? [PHAsset] {
self.completion(.single(.assets(assets)), nil, {
})
}
self.node.collage?.addResults(signals: results)
}
self.galleryController = nil

View file

@ -560,7 +560,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
buttonTitle = item.presentationData.strings.Notification_StarGift_View
}
}
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _):
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _):
if case let .unique(uniqueGift) = gift {
isStarGift = true
@ -594,7 +594,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
case let .model(name, file, _):
modelValue = name
animationFile = file
case let .backdrop(name, innerColor, outerColor, patternColor, _, _):
case let .backdrop(name, _, innerColor, outerColor, patternColor, _, _):
uniqueBackgroundColor = UIColor(rgb: UInt32(bitPattern: outerColor))
uniqueSecondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColor))
uniquePatternColor = UIColor(rgb: UInt32(bitPattern: patternColor))

View file

@ -2189,7 +2189,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
context: context,
theme: presentationData.theme.theme,
strings: presentationData.strings,
subject: .uniqueGift(gift: gift),
subject: .uniqueGift(gift: gift, price: nil),
mode: .preview
)
),

View file

@ -128,7 +128,7 @@ public final class EntityKeyboardAnimationData: Equatable {
for attribute in gift.attributes {
if case let .model(_, fileValue, _) = attribute {
file = fileValue
} else if case let .backdrop(_, innerColor, outerColor, _, _, _) = attribute {
} else if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
color = UIColor(rgb: UInt32(bitPattern: innerColor))
let _ = outerColor
}

View file

@ -149,6 +149,11 @@ public final class GiftCompositionComponent: Component {
previewTimer.invalidate()
self.previewTimer = nil
}
if !self.fetchedFiles.contains(file.fileId.id) {
self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
self.fetchedFiles.insert(file.fileId.id)
}
case let .unique(gift):
for attribute in gift.attributes {
switch attribute {
@ -161,7 +166,7 @@ public final class GiftCompositionComponent: Component {
case let .pattern(_, file, _):
patternFile = file
files[file.fileId.id] = file
case let .backdrop(_, innerColorValue, outerColorValue, patternColorValue, _, _):
case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _):
backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue))
secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue))
patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue))
@ -222,7 +227,7 @@ public final class GiftCompositionComponent: Component {
files[file.fileId.id] = file
}
if case let .backdrop(_, innerColorValue, outerColorValue, patternColorValue, _, _) = self.previewBackdrops[Int(self.previewBackdropIndex)] {
if case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _) = self.previewBackdrops[Int(self.previewBackdropIndex)] {
backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue))
secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue))
patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue))

View file

@ -20,7 +20,7 @@ public final class GiftItemComponent: Component {
public enum Subject: Equatable {
case premium(months: Int32, price: String)
case starGift(gift: StarGift.Gift, price: String)
case uniqueGift(gift: StarGift.UniqueGift)
case uniqueGift(gift: StarGift.UniqueGift, price: String?)
}
public struct Ribbon: Equatable {
@ -28,6 +28,7 @@ public final class GiftItemComponent: Component {
case red
case blue
case purple
case green
case custom(Int32, Int32)
func colors(theme: PresentationTheme) -> [UIColor] {
@ -61,6 +62,11 @@ public final class GiftItemComponent: Component {
UIColor(rgb: 0x747bf6),
UIColor(rgb: 0xe367d8)
]
case .green:
return [
UIColor(rgb: 0x4bb121),
UIColor(rgb: 0x53d654)
]
case let .custom(topColor, _):
return [
UIColor(rgb: UInt32(bitPattern: topColor)).withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89),
@ -72,17 +78,25 @@ public final class GiftItemComponent: Component {
public enum Font {
case generic
case larger
case monospaced
}
public let text: String
public let font: Font
public let color: Color
public let outline: UIColor?
public init(text: String, font: Font = .generic, color: Color) {
public init(
text: String,
font: Font = .generic,
color: Color,
outline: UIColor? = nil
) {
self.text = text
self.font = font
self.color = color
self.outline = outline
}
}
@ -108,6 +122,7 @@ public final class GiftItemComponent: Component {
let subtitle: String?
let label: String?
let ribbon: Ribbon?
let resellPrice: Int64?
let isLoading: Bool
let isHidden: Bool
let isSoldOut: Bool
@ -128,6 +143,7 @@ public final class GiftItemComponent: Component {
subtitle: String? = nil,
label: String? = nil,
ribbon: Ribbon? = nil,
resellPrice: Int64? = nil,
isLoading: Bool = false,
isHidden: Bool = false,
isSoldOut: Bool = false,
@ -147,6 +163,7 @@ public final class GiftItemComponent: Component {
self.subtitle = subtitle
self.label = label
self.ribbon = ribbon
self.resellPrice = resellPrice
self.isLoading = isLoading
self.isHidden = isHidden
self.isSoldOut = isSoldOut
@ -186,6 +203,9 @@ public final class GiftItemComponent: Component {
if lhs.ribbon != rhs.ribbon {
return false
}
if lhs.resellPrice != rhs.resellPrice {
return false
}
if lhs.isLoading != rhs.isLoading {
return false
}
@ -229,6 +249,8 @@ public final class GiftItemComponent: Component {
private let subtitle = ComponentView<Empty>()
private let button = ComponentView<Empty>()
private let label = ComponentView<Empty>()
private let ribbonOutline = UIImageView()
private let ribbon = UIImageView()
private let ribbonText = ComponentView<Empty>()
@ -244,6 +266,9 @@ public final class GiftItemComponent: Component {
private var hiddenIcon: UIImageView?
private var pinnedIcon: UIImageView?
private var resellBackground: BlurredBackgroundView?
private let reselLabel = ComponentView<Empty>()
override init(frame: CGRect) {
super.init(frame: frame)
@ -383,7 +408,7 @@ public final class GiftItemComponent: Component {
file: gift.file
)
animationOffset = 16.0
case let .uniqueGift(gift):
case let .uniqueGift(gift, _):
animationOffset = 16.0
for attribute in gift.attributes {
switch attribute {
@ -396,7 +421,7 @@ public final class GiftItemComponent: Component {
case let .pattern(_, file, _):
patternFile = file
files[file.fileId.id] = file
case let .backdrop(_, innerColorValue, outerColorValue, patternColorValue, _, _):
case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _):
backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue))
secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue))
patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue))
@ -530,6 +555,7 @@ public final class GiftItemComponent: Component {
let buttonColor: UIColor
var starsColor: UIColor?
var tinted = false
let price: String
switch component.subject {
case let .premium(_, priceValue), let .starGift(_, priceValue):
@ -542,9 +568,10 @@ public final class GiftItemComponent: Component {
buttonColor = component.theme.list.itemAccentColor
}
price = priceValue
case .uniqueGift:
case let .uniqueGift(_, priceValue):
buttonColor = UIColor.white
price = component.strings.Gift_Options_Gift_Transfer
price = priceValue ?? component.strings.Gift_Options_Gift_Transfer
tinted = true
}
let buttonSize = self.button.update(
@ -554,6 +581,7 @@ public final class GiftItemComponent: Component {
context: component.context,
text: price,
color: buttonColor,
tinted: tinted,
starsColor: starsColor
)
),
@ -623,6 +651,8 @@ public final class GiftItemComponent: Component {
switch ribbon.font {
case .generic:
ribbonFont = Font.semibold(ribbonFontSize)
case .larger:
ribbonFont = Font.semibold(10.0)
case .monospaced:
ribbonFont = Font.with(size: 10.0, design: .monospace, weight: .semibold)
}
@ -645,6 +675,18 @@ public final class GiftItemComponent: Component {
}
ribbonTextView.bounds = CGRect(origin: .zero, size: ribbonTextSize)
if let _ = component.ribbon?.outline {
if self.ribbonOutline.image == nil || themeUpdated || previousComponent?.ribbon?.outline != component.ribbon?.outline {
self.ribbonOutline.image = ribbonOutlineImage
self.ribbonOutline.tintColor = component.ribbon?.outline
if self.ribbonOutline.superview == nil {
self.insertSubview(self.ribbonOutline, belowSubview: self.ribbon)
}
}
} else if self.ribbonOutline.superview != nil {
self.ribbonOutline.removeFromSuperview()
}
if self.ribbon.image == nil || themeUpdated || previousComponent?.ribbon?.color != component.ribbon?.color {
var direction: GradientImageDirection = .mirroredDiagonal
if case .custom = ribbon.color {
@ -661,11 +703,16 @@ public final class GiftItemComponent: Component {
if let ribbonImage = self.ribbon.image {
self.ribbon.frame = CGRect(origin: CGPoint(x: size.width - ribbonImage.size.width + ribbonOffset.x, y: ribbonOffset.y), size: ribbonImage.size)
}
if let ribbonOutlineImage = self.ribbonOutline.image {
self.ribbonOutline.frame = ribbonOutlineImage.size.centered(around: self.ribbon.center.offsetBy(dx: 0.0, dy: 2.0))
}
ribbonTextView.transform = CGAffineTransform(rotationAngle: .pi / 4.0)
ribbonTextView.center = CGPoint(x: size.width - 22.0 + ribbonOffset.x, y: 22.0 + ribbonOffset.y)
}
} else {
if self.ribbonText.view?.superview != nil {
self.ribbonOutline.removeFromSuperview()
self.ribbon.removeFromSuperview()
self.ribbonText.view?.removeFromSuperview()
}
@ -808,6 +855,72 @@ public final class GiftItemComponent: Component {
hiddenIcon.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
}
if let resellPrice = component.resellPrice {
let labelColor = UIColor.white
let attributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.semibold(11.0), textColor: labelColor),
bold: MarkdownAttributeSet(font: Font.semibold(11.0), textColor: labelColor),
link: MarkdownAttributeSet(font: Font.regular(11.0), textColor: labelColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
let labelText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("#\(resellPrice)", attributes: attributes))
if let range = labelText.string.range(of: "#") {
labelText.addAttribute(NSAttributedString.Key.font, value: Font.semibold(10.0), range: NSRange(range, in: labelText.string))
labelText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: labelText.string))
}
let resellSize = self.reselLabel.update(
transition: transition,
component: AnyComponent(
MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: .white,
text: .plain(labelText),
horizontalAlignment: .center
)
),
environment: {},
containerSize: availableSize
)
let resellFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - resellSize.width) / 2.0), y: size.height - 20.0), size: resellSize)
let resellBackground: BlurredBackgroundView
var resellBackgroundTransition = transition
if let currentBackground = self.resellBackground {
resellBackground = currentBackground
} else {
resellBackgroundTransition = .immediate
resellBackground = BlurredBackgroundView(color: UIColor(rgb: 0x000000, alpha: 0.3), enableBlur: true) //UIVisualEffectView(effect: blurEffect)
resellBackground.clipsToBounds = true
self.resellBackground = resellBackground
self.addSubview(resellBackground)
}
let resellBackgroundFrame = resellFrame.insetBy(dx: -6.0, dy: -4.0)
resellBackgroundTransition.setFrame(view: resellBackground, frame: resellBackgroundFrame)
resellBackground.update(size: resellBackgroundFrame.size, cornerRadius: resellBackgroundFrame.size.height / 2.0, transition: resellBackgroundTransition.containedViewLayoutTransition)
if let resellLabelView = self.reselLabel.view {
if resellLabelView.superview == nil {
self.addSubview(resellLabelView)
}
transition.setFrame(view: resellLabelView, frame: resellFrame)
}
} else {
self.reselLabel.view?.removeFromSuperview()
if let resellBackground = self.resellBackground {
self.resellBackground = nil
resellBackground.removeFromSuperview()
}
}
if case .grid = component.mode {
let lineWidth: CGFloat = 2.0
let selectionFrame = backgroundFrame.insetBy(dx: 3.0, dy: 3.0)
@ -873,17 +986,20 @@ private final class ButtonContentComponent: Component {
let context: AccountContext
let text: String
let color: UIColor
let tinted: Bool
let starsColor: UIColor?
public init(
context: AccountContext,
text: String,
color: UIColor,
tinted: Bool = false,
starsColor: UIColor? = nil
) {
self.context = context
self.text = text
self.color = color
self.tinted = tinted
self.starsColor = starsColor
}
@ -897,6 +1013,9 @@ private final class ButtonContentComponent: Component {
if lhs.color != rhs.color {
return false
}
if lhs.tinted != rhs.tinted {
return false
}
if lhs.starsColor != rhs.starsColor {
return false
}
@ -930,7 +1049,7 @@ private final class ButtonContentComponent: Component {
let attributedText = NSMutableAttributedString(string: component.text, font: Font.semibold(11.0), textColor: component.color)
let range = (attributedText.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: component.tinted)), range: range)
attributedText.addAttribute(.font, value: Font.semibold(15.0), range: range)
attributedText.addAttribute(.baselineOffset, value: 2.0, range: NSRange(location: range.upperBound, length: attributedText.length - range.upperBound))
}
@ -1057,3 +1176,11 @@ private final class StarsButtonEffectLayer: SimpleLayer {
self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
}
}
private var ribbonOutlineImage: UIImage? = {
if let image = UIImage(bundleImageName: "Premium/GiftRibbon") {
return generateScaledImage(image: image, size: CGSize(width: image.size.width + 8.0, height: image.size.height + 8.0), opaque: false)?.withRenderingMode(.alwaysTemplate)
} else {
return UIImage()
}
}()

View file

@ -79,6 +79,7 @@ final class GiftOptionsScreenComponent: Component {
case all
case limited
case inStock
case resale
case stars(Int64)
case transfer
@ -92,6 +93,8 @@ final class GiftOptionsScreenComponent: Component {
self = .inStock
case -3:
self = .transfer
case -4:
self = .resale
default:
self = .stars(rawValue)
}
@ -107,6 +110,8 @@ final class GiftOptionsScreenComponent: Component {
return -2
case .transfer:
return -3
case .resale:
return -4
case let .stars(stars):
return stars
}
@ -136,6 +141,7 @@ final class GiftOptionsScreenComponent: Component {
private var starsItems: [AnyHashable: ComponentView<Empty>] = [:]
private let tabSelector = ComponentView<Empty>()
private var starsFilter: StarsFilter = .all
private var switchingFilter = false
private var _effectiveStarGifts: ([StarGift], StarsFilter)?
private var effectiveStarGifts: [StarGift]? {
@ -191,6 +197,12 @@ final class GiftOptionsScreenComponent: Component {
return true
}
}
case .resale:
if case let .generic(gift) = $0 {
if let availability = gift.availability, availability.resale > 0 {
return true
}
}
case .transfer:
break
}
@ -216,6 +228,7 @@ final class GiftOptionsScreenComponent: Component {
private(set) weak var state: State?
private var environment: EnvironmentType?
private var tabSelectorOrigin: CGFloat = 0.0
private var starsItemsOrigin: CGFloat = 0.0
private var dismissed = false
@ -355,12 +368,28 @@ final class GiftOptionsScreenComponent: Component {
var ribbon: GiftItemComponent.Ribbon?
var isSoldOut = false
if case let .generic(gift) = gift {
if let _ = gift.soldOut {
switch gift {
case let .generic(gift):
if let availability = gift.availability, availability.resale > 0 {
//TODO:localize
//TODO:unmock
ribbon = GiftItemComponent.Ribbon(
text: environment.strings.Gift_Options_Gift_SoldOut,
color: .red
text: "resale",
color: .green
)
} else if let _ = gift.soldOut {
if let availability = gift.availability, availability.resale > 0 {
//TODO:localize
ribbon = GiftItemComponent.Ribbon(
text: "resale",
color: .green
)
} else {
ribbon = GiftItemComponent.Ribbon(
text: environment.strings.Gift_Options_Gift_SoldOut,
color: .red
)
}
isSoldOut = true
} else if let _ = gift.availability {
ribbon = GiftItemComponent.Ribbon(
@ -368,14 +397,31 @@ final class GiftOptionsScreenComponent: Component {
color: .blue
)
}
case let .unique(gift):
var ribbonColor: GiftItemComponent.Ribbon.Color = .blue
for attribute in gift.attributes {
if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
ribbonColor = .custom(outerColor, innerColor)
break
}
}
ribbon = GiftItemComponent.Ribbon(
text: "#\(gift.number)",
font: .monospaced,
color: ribbonColor
)
}
let subject: GiftItemComponent.Subject
switch gift {
case let .generic(gift):
subject = .starGift(gift: gift, price: "⭐️ \(gift.price)")
if let availability = gift.availability, let minResaleStars = availability.minResaleStars {
subject = .starGift(gift: gift, price: "⭐️ \(minResaleStars)+")
} else {
subject = .starGift(gift: gift, price: "⭐️ \(gift.price)")
}
case let .unique(gift):
subject = .uniqueGift(gift: gift)
subject = .uniqueGift(gift: gift, price: nil)
}
let _ = visibleItem.update(
@ -404,13 +450,27 @@ final class GiftOptionsScreenComponent: Component {
mainController = controller
}
if case let .generic(gift) = gift {
if gift.availability?.remains == 0 {
let giftController = GiftViewScreen(
context: component.context,
subject: .soldOutGift(gift)
)
mainController.push(giftController)
} else {
var forceStore = !"".isEmpty
#if DEBUG
forceStore = true
#endif
if let availability = gift.availability, availability.remains == 0 || (availability.resale > 0 && forceStore) {
if availability.resale > 0 {
let storeController = component.context.sharedContext.makeGiftStoreController(
context: component.context,
peerId: component.peerId,
gift: gift
)
mainController.push(storeController)
} else {
let giftController = GiftViewScreen(
context: component.context,
subject: .soldOutGift(gift)
)
mainController.push(giftController)
}
} else {
var forceUnique = false
if let disallowedGifts = self.state?.disallowedGifts, disallowedGifts.contains(.limited) && !disallowedGifts.contains(.unique) {
forceUnique = true
@ -475,6 +535,53 @@ final class GiftOptionsScreenComponent: Component {
}
}
var topPanelHeight = environment.navigationHeight
let tabSelectorThreshold = self.tabSelectorOrigin - 8.0
if contentOffset > tabSelectorThreshold - environment.navigationHeight {
topPanelHeight += 39.0
}
if let tabSelectorView = self.tabSelector.view {
let tabSelectorSize = tabSelectorView.bounds.size
transition.setFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableWidth - tabSelectorSize.width) / 2.0), y: max(56.0, self.tabSelectorOrigin - contentOffset)), size: tabSelectorSize))
}
var panelTransition = transition
if self.topPanel.view?.superview != nil && !self.switchingFilter {
panelTransition = .spring(duration: 0.3)
}
let topPanelSize = self.topPanel.update(
transition: panelTransition,
component: AnyComponent(BlurredBackgroundComponent(
color: environment.theme.rootController.navigationBar.blurredBackgroundColor
)),
environment: {},
containerSize: CGSize(width: availableWidth, height: topPanelHeight)
)
let topSeparatorSize = self.topSeparator.update(
transition: panelTransition,
component: AnyComponent(Rectangle(
color: environment.theme.rootController.navigationBar.separatorColor
)),
environment: {},
containerSize: CGSize(width: availableWidth, height: UIScreenPixel)
)
let topPanelFrame = CGRect(origin: .zero, size: CGSize(width: availableWidth, height: topPanelSize.height))
let topSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelSize.height), size: CGSize(width: topSeparatorSize.width, height: topSeparatorSize.height))
if let topPanelView = self.topPanel.view, let topSeparatorView = self.topSeparator.view {
if topPanelView.superview == nil {
if let headerView = self.header.view {
self.insertSubview(topSeparatorView, aboveSubview: headerView)
self.insertSubview(topPanelView, aboveSubview: headerView)
}
}
panelTransition.setFrame(view: topPanelView, frame: topPanelFrame)
panelTransition.setFrame(view: topSeparatorView, frame: topSeparatorFrame)
}
let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height)
if interactive, bottomContentOffset < 320.0, case .transfer = self.starsFilter {
self.state?.starGiftsContext.loadMore()
@ -755,34 +862,34 @@ final class GiftOptionsScreenComponent: Component {
}
transition.setBounds(view: headerView, bounds: CGRect(origin: .zero, size: headerSize))
}
let topPanelSize = self.topPanel.update(
transition: transition,
component: AnyComponent(BlurredBackgroundComponent(
color: theme.rootController.navigationBar.blurredBackgroundColor
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: environment.navigationHeight)
)
let topSeparatorSize = self.topSeparator.update(
transition: transition,
component: AnyComponent(Rectangle(
color: theme.rootController.navigationBar.separatorColor
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: UIScreenPixel)
)
let topPanelFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: topPanelSize.height))
let topSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelSize.height), size: CGSize(width: topSeparatorSize.width, height: topSeparatorSize.height))
if let topPanelView = self.topPanel.view, let topSeparatorView = self.topSeparator.view {
if topPanelView.superview == nil {
self.addSubview(topPanelView)
self.addSubview(topSeparatorView)
}
transition.setFrame(view: topPanelView, frame: topPanelFrame)
transition.setFrame(view: topSeparatorView, frame: topSeparatorFrame)
}
// let topPanelSize = self.topPanel.update(
// transition: transition,
// component: AnyComponent(BlurredBackgroundComponent(
// color: theme.rootController.navigationBar.blurredBackgroundColor
// )),
// environment: {},
// containerSize: CGSize(width: availableSize.width, height: environment.navigationHeight)
// )
//
// let topSeparatorSize = self.topSeparator.update(
// transition: transition,
// component: AnyComponent(Rectangle(
// color: theme.rootController.navigationBar.separatorColor
// )),
// environment: {},
// containerSize: CGSize(width: availableSize.width, height: UIScreenPixel)
// )
// let topPanelFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: topPanelSize.height))
// let topSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelSize.height), size: CGSize(width: topSeparatorSize.width, height: topSeparatorSize.height))
// if let topPanelView = self.topPanel.view, let topSeparatorView = self.topSeparator.view {
// if topPanelView.superview == nil {
// self.addSubview(topPanelView)
// self.addSubview(topSeparatorView)
// }
// transition.setFrame(view: topPanelView, frame: topPanelFrame)
// transition.setFrame(view: topSeparatorView, frame: topSeparatorFrame)
// }
let cancelButtonSize = self.cancelButton.update(
transition: transition,
@ -1186,13 +1293,17 @@ final class GiftOptionsScreenComponent: Component {
}
var hasLimited = false
var hasResale = false
var starsAmountsSet = Set<Int64>()
if let starGifts = self.state?.starGifts {
for gift in starGifts {
if case let .generic(gift) = gift {
starsAmountsSet.insert(gift.price)
if gift.availability != nil {
if let availability = gift.availability {
hasLimited = true
if availability.resale > 0 {
hasResale = true
}
}
}
}
@ -1210,6 +1321,14 @@ final class GiftOptionsScreenComponent: Component {
title: strings.Gift_Options_Gift_Filter_InStock
))
if hasResale {
//TODO:localize
tabSelectorItems.append(TabSelectorComponent.Item(
id: AnyHashable(StarsFilter.resale.rawValue),
title: "Resale"
))
}
let starsAmounts = Array(starsAmountsSet).sorted()
for amount in starsAmounts {
tabSelectorItems.append(TabSelectorComponent.Item(
@ -1235,17 +1354,26 @@ final class GiftOptionsScreenComponent: Component {
}
let starsFilter = StarsFilter(rawValue: idValue)
if self.starsFilter != starsFilter {
if self.scrollView.contentOffset.y > self.tabSelectorOrigin - 56.0 {
self.scrollView.setContentOffset(CGPoint(x: 0.0, y: self.tabSelectorOrigin - 56.0), animated: true)
}
self.switchingFilter = true
self.starsFilter = starsFilter
self.state?.updated(transition: .easeInOut(duration: 0.25))
Queue.mainQueue().after(0.1, {
self.switchingFilter = false
})
}
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0)
)
self.tabSelectorOrigin = contentHeight
if let tabSelectorView = self.tabSelector.view {
if tabSelectorView.superview == nil {
self.scrollView.addSubview(tabSelectorView)
self.addSubview(tabSelectorView)
}
transition.setFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - tabSelectorSize.width) / 2.0), y: contentHeight), size: tabSelectorSize))
}

View file

@ -0,0 +1,52 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GiftStoreScreen",
module_name = "GiftStoreScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/ItemListUI",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/Components/SheetComponent",
"//submodules/UndoUI",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/ScrollComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
"//submodules/ConfettiEffect",
"//submodules/InAppPurchaseManager",
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/Gifts/GiftSetupScreen",
"//submodules/TelegramUI/Components/Gifts/GiftViewScreen",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/TextFieldComponent",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,355 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import PlainButtonComponent
import MultilineTextWithEntitiesComponent
import BundleIconComponent
import TextFormat
import AccountContext
public final class FilterSelectorComponent: Component {
public struct Colors: Equatable {
public var foreground: UIColor
public var background: UIColor
public init(
foreground: UIColor,
background: UIColor
) {
self.foreground = foreground
self.background = background
}
}
public struct Item: Equatable {
public var id: AnyHashable
public var iconName: String?
public var title: String
public var action: (UIView) -> Void
public init(
id: AnyHashable,
iconName: String? = nil,
title: String,
action: @escaping (UIView) -> Void
) {
self.id = id
self.iconName = iconName
self.title = title
self.action = action
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
return lhs.id == rhs.id && lhs.iconName == rhs.iconName && lhs.title == rhs.title
}
}
public let context: AccountContext?
public let colors: Colors
public let items: [Item]
public init(
context: AccountContext? = nil,
colors: Colors,
items: [Item]
) {
self.context = context
self.colors = colors
self.items = items
}
public static func ==(lhs: FilterSelectorComponent, rhs: FilterSelectorComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.colors != rhs.colors {
return false
}
if lhs.items != rhs.items {
return false
}
return true
}
private final class VisibleItem {
let title = ComponentView<Empty>()
init() {
}
}
public final class View: UIScrollView {
private var component: FilterSelectorComponent?
private weak var state: EmptyComponentState?
private var visibleItems: [AnyHashable: VisibleItem] = [:]
override init(frame: CGRect) {
super.init(frame: frame)
self.showsVerticalScrollIndicator = false
self.showsHorizontalScrollIndicator = false
self.scrollsToTop = false
self.delaysContentTouches = false
self.canCancelContentTouches = true
self.contentInsetAdjustmentBehavior = .never
self.alwaysBounceVertical = false
self.clipsToBounds = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
func update(component: FilterSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let baseHeight: CGFloat = 28.0
var spacing: CGFloat = 6.0
let itemFont = Font.semibold(14.0)
let allowScroll = true
var innerContentWidth: CGFloat = 0.0
var validIds: [AnyHashable] = []
var index = 0
var itemViews: [AnyHashable: (VisibleItem, CGSize, ComponentTransition)] = [:]
for item in component.items {
var itemTransition = transition
let itemView: VisibleItem
if let current = self.visibleItems[item.id] {
itemView = current
} else {
itemView = VisibleItem()
self.visibleItems[item.id] = itemView
itemTransition = itemTransition.withAnimation(.none)
}
let itemId = item.id
validIds.append(itemId)
let itemSize = itemView.title.update(
transition: .immediate,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(ItemComponent(
context: component.context,
iconName: item.iconName,
text: item.title,
font: itemFont,
color: component.colors.foreground,
backgroundColor: component.colors.background
)),
effectAlignment: .center,
minSize: nil,
action: { [weak itemView] in
if let view = itemView?.title.view {
item.action(view)
}
},
animateScale: false
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 100.0)
)
innerContentWidth += itemSize.width
itemViews[item.id] = (itemView, itemSize, itemTransition)
index += 1
}
let estimatedContentWidth = 2.0 * spacing + innerContentWidth + (CGFloat(component.items.count - 1) * spacing)
if estimatedContentWidth > availableSize.width && !allowScroll {
spacing = (availableSize.width - innerContentWidth) / CGFloat(component.items.count + 1)
}
var contentWidth: CGFloat = spacing
for item in component.items {
guard let (itemView, itemSize, itemTransition) = itemViews[item.id] else {
continue
}
if contentWidth > spacing {
contentWidth += spacing
}
let itemFrame = CGRect(origin: CGPoint(x: contentWidth, y: floor((baseHeight - itemSize.height) * 0.5)), size: itemSize)
contentWidth = itemFrame.maxX
if let itemTitleView = itemView.title.view {
if itemTitleView.superview == nil {
itemTitleView.layer.anchorPoint = CGPoint()
self.addSubview(itemTitleView)
}
itemTransition.setPosition(view: itemTitleView, position: itemFrame.origin)
itemTransition.setBounds(view: itemTitleView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
}
}
contentWidth += spacing
var removeIds: [AnyHashable] = []
for (id, itemView) in self.visibleItems {
if !validIds.contains(id) {
removeIds.append(id)
itemView.title.view?.removeFromSuperview()
}
}
for id in removeIds {
self.visibleItems.removeValue(forKey: id)
}
self.contentSize = CGSize(width: contentWidth, height: baseHeight)
self.disablesInteractiveTransitionGestureRecognizer = contentWidth > availableSize.width
return CGSize(width: min(contentWidth, availableSize.width), height: baseHeight)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
extension CGRect {
func interpolate(with other: CGRect, fraction: CGFloat) -> CGRect {
return CGRect(
x: self.origin.x * (1.0 - fraction) + (other.origin.x) * fraction,
y: self.origin.y * (1.0 - fraction) + (other.origin.y) * fraction,
width: self.size.width * (1.0 - fraction) + (other.size.width) * fraction,
height: self.size.height * (1.0 - fraction) + (other.size.height) * fraction
)
}
}
private final class ItemComponent: CombinedComponent {
let context: AccountContext?
let iconName: String?
let text: String
let font: UIFont
let color: UIColor
let backgroundColor: UIColor
init(
context: AccountContext?,
iconName: String?,
text: String,
font: UIFont,
color: UIColor,
backgroundColor: UIColor
) {
self.context = context
self.iconName = iconName
self.text = text
self.font = font
self.color = color
self.backgroundColor = backgroundColor
}
static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.font != rhs.font {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
return true
}
static var body: Body {
let background = Child(RoundedRectangle.self)
let title = Child(MultilineTextWithEntitiesComponent.self)
let icon = Child(BundleIconComponent.self)
return { context in
let component = context.component
let attributedTitle = NSMutableAttributedString(string: component.text, font: component.font, textColor: component.color)
let range = (attributedTitle.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
}
let title = title.update(
component: MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context?.animationCache,
animationRenderer: component.context?.animationRenderer,
placeholderColor: .white,
text: .plain(attributedTitle)
),
availableSize: context.availableSize,
transition: .immediate
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName ?? "Item List/ExpandableSelectorArrows",
tintColor: component.color,
maxSize: component.iconName != nil ? CGSize(width: 22.0, height: 22.0) : nil
),
availableSize: CGSize(width: 100, height: 100),
transition: .immediate
)
let padding: CGFloat = 12.0
var leftPadding = padding
if let _ = component.iconName {
leftPadding -= 4.0
}
let spacing: CGFloat = 4.0
let totalWidth = title.size.width + icon.size.width + spacing
let size = CGSize(width: totalWidth + leftPadding + padding, height: 28.0)
let background = background.update(
component: RoundedRectangle(
color: component.backgroundColor,
cornerRadius: 14.0
),
availableSize: size,
transition: .immediate
)
context.add(background
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
)
if let _ = component.iconName {
context.add(title
.position(CGPoint(x: size.width - padding - title.size.width / 2.0, y: size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: leftPadding + icon.size.width / 2.0, y: size.height / 2.0))
)
} else {
context.add(title
.position(CGPoint(x: padding + title.size.width / 2.0, y: size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: size.width - padding - icon.size.width / 2.0, y: size.height / 2.0))
)
}
return size
}
}
}

View file

@ -0,0 +1,509 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TelegramPresentationData
import ContextUI
final class GiftAttributeListContextItem: ContextMenuCustomItem {
let context: AccountContext
let attributes: [StarGift.UniqueGift.Attribute]
let selectedAttributes: [ResaleGiftsContext.Attribute]
let attributeCount: [ResaleGiftsContext.Attribute: Int32]
let searchQuery: Signal<String, NoError>
let attributeSelected: (ResaleGiftsContext.Attribute, Bool) -> Void
let selectAll: () -> Void
init(
context: AccountContext,
attributes: [StarGift.UniqueGift.Attribute],
selectedAttributes: [ResaleGiftsContext.Attribute],
attributeCount: [ResaleGiftsContext.Attribute: Int32],
searchQuery: Signal<String, NoError>,
attributeSelected: @escaping (ResaleGiftsContext.Attribute, Bool) -> Void,
selectAll: @escaping () -> Void
) {
self.context = context
self.attributes = attributes
self.selectedAttributes = selectedAttributes
self.attributeCount = attributeCount
self.searchQuery = searchQuery
self.attributeSelected = attributeSelected
self.selectAll = selectAll
}
func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
return GiftAttributeListContextItemNode(
presentationData: presentationData,
item: self,
getController: getController,
actionSelected: actionSelected
)
}
}
private func actionForAttribute(attribute: StarGift.UniqueGift.Attribute, presentationData: PresentationData, selectedAttributes: Set<ResaleGiftsContext.Attribute>, searchQuery: String, item: GiftAttributeListContextItem, getController: @escaping () -> ContextControllerProtocol?) -> ContextMenuActionItem? {
let searchComponents = searchQuery.lowercased().components(separatedBy: .whitespaces).filter { !$0.isEmpty }
switch attribute {
case let .model(name, file, _), let .pattern(name, file, _):
let attributeId: ResaleGiftsContext.Attribute
if case .model = attribute {
attributeId = .model(file.fileId.id)
} else {
attributeId = .pattern(file.fileId.id)
}
let isSelected = selectedAttributes.isEmpty || selectedAttributes.contains(attributeId)
var entities: [MessageTextEntity] = []
var entityFiles: [Int64: TelegramMediaFile] = [:]
entities = [
MessageTextEntity(
range: 0..<1,
type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id)
)
]
entityFiles[file.fileId.id] = file
var title = "# \(name)"
var count = ""
if let counter = item.attributeCount[.model(file.fileId.id)] {
count = " \(presentationStringsFormattedNumber(counter, presentationData.dateTimeFormat.groupingSeparator))"
entities.append(
MessageTextEntity(
range: title.count ..< title.count + count.count,
type: .Italic
)
)
title += count
}
let words = title.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty }
var wordStartIndices: [String.Index] = []
var currentIndex = title.startIndex
for word in words {
while currentIndex < title.endIndex {
let range = title.range(of: word, range: currentIndex..<title.endIndex)
if let range = range {
wordStartIndices.append(range.lowerBound)
currentIndex = range.upperBound
break
}
currentIndex = title.index(after: currentIndex)
}
}
for (wordIndex, word) in words.enumerated() {
let lowercaseWord = word.lowercased()
for component in searchComponents {
if lowercaseWord.hasPrefix(component) {
let startIndex = wordStartIndices[wordIndex]
let prefixRange = startIndex..<title.index(startIndex, offsetBy: min(component.count, word.count))
entities.append(
MessageTextEntity(
range: title.distance(from: title.startIndex, to: prefixRange.lowerBound)..<title.distance(from: title.startIndex, to: prefixRange.upperBound),
type: .Bold
)
)
}
}
}
return ContextMenuActionItem(text: title, entities: entities, entityFiles: entityFiles, enableEntityAnimations: false, parseMarkdown: true, icon: { theme in
return isSelected ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, f in
getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
item.attributeSelected(attributeId, false)
}, longPressAction: { _, f in
getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
item.attributeSelected(attributeId, true)
})
case let .backdrop(name, id, innerColor, outerColor, _, _, _):
let attributeId: ResaleGiftsContext.Attribute = .backdrop(id)
let isSelected = selectedAttributes.isEmpty || selectedAttributes.contains(attributeId)
var entities: [MessageTextEntity] = []
var title = " \(name)"
var count = ""
if let counter = item.attributeCount[attributeId] {
count = " \(presentationStringsFormattedNumber(counter, presentationData.dateTimeFormat.groupingSeparator))"
entities.append(
MessageTextEntity(range: title.count ..< title.count + count.count, type: .Italic)
)
title += count
}
let words = title.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty }
var wordStartIndices: [String.Index] = []
var currentIndex = title.startIndex
for word in words {
while currentIndex < title.endIndex {
let range = title.range(of: word, range: currentIndex..<title.endIndex)
if let range = range {
wordStartIndices.append(range.lowerBound)
currentIndex = range.upperBound
break
}
currentIndex = title.index(after: currentIndex)
}
}
for (wordIndex, word) in words.enumerated() {
let lowercaseWord = word.lowercased()
for component in searchComponents {
if lowercaseWord.hasPrefix(component) {
let startIndex = wordStartIndices[wordIndex]
let prefixRange = startIndex..<title.index(startIndex, offsetBy: min(component.count, word.count))
entities.append(
MessageTextEntity(
range: title.distance(from: title.startIndex, to: prefixRange.lowerBound)..<title.distance(from: title.startIndex, to: prefixRange.upperBound),
type: .Bold
)
)
}
}
}
return ContextMenuActionItem(text: title, entities: entities, icon: { theme in
return isSelected ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, additionalLeftIcon: { _ in
return generateGradientFilledCircleImage(diameter: 24.0, colors: [UIColor(rgb: UInt32(bitPattern: innerColor)).cgColor, UIColor(rgb: UInt32(bitPattern: outerColor)).cgColor])
}, action: { _, f in
getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
item.attributeSelected(attributeId, false)
}, longPressAction: { _, f in
getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
item.attributeSelected(attributeId, true)
})
default:
return nil
}
}
private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol, ASScrollViewDelegate {
private let item: GiftAttributeListContextItem
private let presentationData: PresentationData
private let getController: () -> ContextControllerProtocol?
private let actionSelected: (ContextMenuActionResult) -> Void
private let scrollNode: ASScrollNode
private let actionNodes: [ContextControllerActionsListActionItemNode]
private let separatorNodes: [ASDisplayNode]
private var searchDisposable: Disposable?
private var searchQuery = ""
init(presentationData: PresentationData, item: GiftAttributeListContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
self.item = item
self.presentationData = presentationData
self.getController = getController
self.actionSelected = actionSelected
self.scrollNode = ASScrollNode()
var actionNodes: [ContextControllerActionsListActionItemNode] = []
var separatorNodes: [ASDisplayNode] = []
let selectedAttributes = Set(item.selectedAttributes)
//TODO:localize
let selectAllAction = ContextMenuActionItem(text: "Select All", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor)
}, iconPosition: .left, action: { _, f in
getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
item.selectAll()
})
let selectAllActionNode = ContextControllerActionsListActionItemNode(context: item.context, getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: selectAllAction)
actionNodes.append(selectAllActionNode)
let separatorNode = ASDisplayNode()
separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
separatorNodes.append(separatorNode)
for attribute in item.attributes {
guard let action = actionForAttribute(attribute: attribute, presentationData: presentationData, selectedAttributes: selectedAttributes, searchQuery: self.searchQuery, item: item, getController: getController) else {
continue
}
let actionNode = ContextControllerActionsListActionItemNode(context: item.context, getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: action)
actionNodes.append(actionNode)
if actionNodes.count != item.attributes.count {
let separatorNode = ASDisplayNode()
separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
separatorNodes.append(separatorNode)
}
}
let nopAction: ((ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void)? = nil
let emptyResultsAction = ContextMenuActionItem(text: "No Results", textFont: .small, icon: { _ in return nil }, action: nopAction)
let emptyResultsActionNode = ContextControllerActionsListActionItemNode(context: item.context, getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: emptyResultsAction)
actionNodes.append(emptyResultsActionNode)
self.actionNodes = actionNodes
self.separatorNodes = separatorNodes
super.init()
self.addSubnode(self.scrollNode)
for separatorNode in self.separatorNodes {
self.scrollNode.addSubnode(separatorNode)
}
for actionNode in self.actionNodes {
self.scrollNode.addSubnode(actionNode)
}
self.searchDisposable = (item.searchQuery
|> deliverOnMainQueue).start(next: { [weak self] searchQuery in
guard let self, self.searchQuery != searchQuery else {
return
}
self.searchQuery = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
var i = 1
for attribute in item.attributes {
guard let action = actionForAttribute(attribute: attribute, presentationData: presentationData, selectedAttributes: selectedAttributes, searchQuery: self.searchQuery, item: item, getController: getController) else {
continue
}
self.actionNodes[i].setItem(item: action)
i += 1
}
self.getController()?.requestLayout(transition: .immediate)
})
}
deinit {
self.searchDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.scrollNode.view.alwaysBounceVertical = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 5.0, right: 0.0)
}
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
let minActionsWidth: CGFloat = 250.0
let maxActionsWidth: CGFloat = 300.0
let constrainedWidth = min(constrainedWidth, maxActionsWidth)
var maxWidth: CGFloat = 0.0
var contentHeight: CGFloat = 0.0
var heightsAndCompletions: [(Int, CGFloat, (CGSize, ContainedViewLayoutTransition) -> Void)] = []
let effectiveAttributes: [StarGift.UniqueGift.Attribute]
if self.searchQuery.isEmpty {
effectiveAttributes = self.item.attributes
} else {
effectiveAttributes = filteredAttributes(attributes: self.item.attributes, query: self.searchQuery)
}
let visibleAttributes = Set(effectiveAttributes.map { attribute -> AnyHashable in
switch attribute {
case let .model(_, file, _):
return file.fileId.id
case let .pattern(_, file, _):
return file.fileId.id
case let .backdrop(_, id, _, _, _, _, _):
return id
default:
fatalError()
}
})
for i in 0 ..< self.actionNodes.count {
let itemNode = self.actionNodes[i]
if !self.searchQuery.isEmpty && i == 0 {
itemNode.isHidden = true
continue
}
if i > 0 && i < self.actionNodes.count - 1 {
let attribute = self.item.attributes[i - 1]
let attributeId: AnyHashable
switch attribute {
case let .model(_, file, _):
attributeId = AnyHashable(file.fileId.id)
case let .pattern(_, file, _):
attributeId = AnyHashable(file.fileId.id)
case let .backdrop(_, id, _, _, _, _, _):
attributeId = AnyHashable(id)
default:
fatalError()
}
if !visibleAttributes.contains(attributeId) {
itemNode.isHidden = true
continue
}
}
if i == self.actionNodes.count - 1 {
if !visibleAttributes.isEmpty {
itemNode.isHidden = true
continue
} else {
}
}
itemNode.isHidden = false
let (minSize, complete) = itemNode.update(presentationData: self.presentationData, constrainedSize: CGSize(width: constrainedWidth, height: constrainedHeight))
maxWidth = max(maxWidth, minSize.width)
heightsAndCompletions.append((i, minSize.height, complete))
contentHeight += minSize.height
}
maxWidth = max(maxWidth, minActionsWidth)
let maxHeight: CGFloat = min(360.0, constrainedHeight - 108.0)
return (CGSize(width: maxWidth, height: min(maxHeight, contentHeight)), { size, transition in
var verticalOffset: CGFloat = 0.0
for (i, itemHeight, itemCompletion) in heightsAndCompletions {
let itemNode = self.actionNodes[i]
let itemSize = CGSize(width: maxWidth, height: itemHeight)
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: itemSize))
itemCompletion(itemSize, transition)
verticalOffset += itemHeight
if i < self.actionNodes.count - 2 {
let separatorNode = self.separatorNodes[i]
separatorNode.frame = CGRect(x: 0, y: verticalOffset, width: size.width, height: UIScreenPixel)
}
}
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
self.scrollNode.view.contentSize = CGSize(width: size.width, height: contentHeight)
})
}
func updateTheme(presentationData: PresentationData) {
}
var isActionEnabled: Bool {
return true
}
func performAction() {
}
func setIsHighlighted(_ value: Bool) {
}
func canBeHighlighted() -> Bool {
return self.isActionEnabled
}
func updateIsHighlighted(isHighlighted: Bool) {
self.setIsHighlighted(isHighlighted)
}
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol {
// for actionNode in self.actionNodes {
// let frame = actionNode.convert(actionNode.bounds, to: self)
// if frame.contains(point) {
// return actionNode
// }
// }
return self
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
for actionNode in self.actionNodes {
actionNode.updateIsHighlighted(isHighlighted: false)
}
}
}
private func stringTokens(_ string: String) -> [ValueBoxKey] {
let nsString = string.folding(options: .diacriticInsensitive, locale: .current).lowercased() as NSString
let flag = UInt(kCFStringTokenizerUnitWord)
let tokenizer = CFStringTokenizerCreate(kCFAllocatorDefault, nsString, CFRangeMake(0, nsString.length), flag, CFLocaleCopyCurrent())
var tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer)
var tokens: [ValueBoxKey] = []
var addedTokens = Set<ValueBoxKey>()
while tokenType != [] {
let currentTokenRange = CFStringTokenizerGetCurrentTokenRange(tokenizer)
if currentTokenRange.location >= 0 && currentTokenRange.length != 0 {
let token = ValueBoxKey(length: currentTokenRange.length * 2)
nsString.getCharacters(token.memory.assumingMemoryBound(to: unichar.self), range: NSMakeRange(currentTokenRange.location, currentTokenRange.length))
if !addedTokens.contains(token) {
tokens.append(token)
addedTokens.insert(token)
}
}
tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer)
}
return tokens
}
private func matchStringTokens(_ tokens: [ValueBoxKey], with other: [ValueBoxKey]) -> Bool {
if other.isEmpty {
return false
} else if other.count == 1 {
let otherToken = other[0]
for token in tokens {
if otherToken.isPrefix(to: token) {
return true
}
}
} else {
for otherToken in other {
var found = false
for token in tokens {
if otherToken.isPrefix(to: token) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
return false
}
private func filteredAttributes(attributes: [StarGift.UniqueGift.Attribute], query: String) -> [StarGift.UniqueGift.Attribute] {
let queryTokens = stringTokens(query.lowercased())
var result: [StarGift.UniqueGift.Attribute] = []
for attribute in attributes {
let string: String
switch attribute {
case let .model(name, _, _):
string = name
case let .pattern(name, _, _):
string = name
case let .backdrop(name, _, _, _, _, _, _):
string = name
default:
continue
}
let tokens = stringTokens(string)
if matchStringTokens(tokens, with: queryTokens) {
result.append(attribute)
}
}
return result
}

View file

@ -0,0 +1,186 @@
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import TelegramPresentationData
private final class SearchShimmerEffectNode: ASDisplayNode {
private var currentBackgroundColor: UIColor?
private var currentForegroundColor: UIColor?
private let imageNodeContainer: ASDisplayNode
private let imageNode: ASImageNode
private var absoluteLocation: (CGRect, CGSize)?
private var isCurrentlyInHierarchy = false
private var shouldBeAnimating = false
override init() {
self.imageNodeContainer = ASDisplayNode()
self.imageNodeContainer.isLayerBacked = true
self.imageNode = ASImageNode()
self.imageNode.isLayerBacked = true
self.imageNode.displaysAsynchronously = false
self.imageNode.displayWithoutProcessing = true
self.imageNode.contentMode = .scaleToFill
super.init()
self.isLayerBacked = true
self.clipsToBounds = true
self.imageNodeContainer.addSubnode(self.imageNode)
self.addSubnode(self.imageNodeContainer)
}
override func didEnterHierarchy() {
super.didEnterHierarchy()
self.isCurrentlyInHierarchy = true
self.updateAnimation()
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.isCurrentlyInHierarchy = false
self.updateAnimation()
}
func update(backgroundColor: UIColor, foregroundColor: UIColor) {
if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.argb == backgroundColor.argb, let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.argb == foregroundColor.argb {
return
}
self.currentBackgroundColor = backgroundColor
self.currentForegroundColor = foregroundColor
self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.clip(to: CGRect(origin: CGPoint(), size: size))
let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor
let peakColor = foregroundColor.cgColor
var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
}
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize {
return
}
let sizeUpdated = self.absoluteLocation?.1 != containerSize
let frameUpdated = self.absoluteLocation?.0 != rect
self.absoluteLocation = (rect, containerSize)
if sizeUpdated {
if self.shouldBeAnimating {
self.imageNode.layer.removeAnimation(forKey: "shimmer")
self.addImageAnimation()
}
}
if frameUpdated {
self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize)
}
self.updateAnimation()
}
private func updateAnimation() {
let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil
if shouldBeAnimating != self.shouldBeAnimating {
self.shouldBeAnimating = shouldBeAnimating
if shouldBeAnimating {
self.addImageAnimation()
} else {
self.imageNode.layer.removeAnimation(forKey: "shimmer")
}
}
}
private func addImageAnimation() {
guard let containerSize = self.absoluteLocation?.1 else {
return
}
let gradientHeight: CGFloat = 250.0
self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight))
let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
animation.repeatCount = Float.infinity
animation.beginTime = 1.0
self.imageNode.layer.add(animation, forKey: "shimmer")
}
}
final class LoadingShimmerNode: ASDisplayNode {
private let backgroundColorNode: ASDisplayNode
private let effectNode: SearchShimmerEffectNode
private let maskNode: ASImageNode
private var currentParams: (size: CGSize, theme: PresentationTheme)?
override init() {
self.backgroundColorNode = ASDisplayNode()
self.effectNode = SearchShimmerEffectNode()
self.maskNode = ASImageNode()
super.init()
self.allowsGroupOpacity = true
self.isUserInteractionEnabled = false
self.addSubnode(self.backgroundColorNode)
self.addSubnode(self.effectNode)
self.addSubnode(self.maskNode)
}
func update(size: CGSize, theme: PresentationTheme, transition: ContainedViewLayoutTransition) {
let color = theme.list.itemSecondaryTextColor.mixedWith(theme.list.blocksBackgroundColor, alpha: 0.85)
if self.currentParams?.size != size || self.currentParams?.theme !== theme {
self.currentParams = (size, theme)
self.backgroundColorNode.backgroundColor = color
self.maskNode.image = generateImage(size, rotatedContext: { size, context in
context.setFillColor(theme.list.blocksBackgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
var currentY: CGFloat = 0.0
var rowIndex: Int = 0
let sideInset: CGFloat = 16.0// + environment.safeInsets.left
let optionSpacing: CGFloat = 10.0
let optionWidth = (size.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
let itemSize = CGSize(width: optionWidth, height: 154.0)
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
while currentY < size.height {
for i in 0 ..< 3 {
let itemOrigin = CGPoint(x: sideInset + CGFloat(i) * (itemSize.width + optionSpacing), y: 2.0 + CGFloat(rowIndex) * (itemSize.height + optionSpacing))
context.addPath(CGPath(roundedRect: CGRect(origin: itemOrigin, size: itemSize), cornerWidth: 10.0, cornerHeight: 10.0, transform: nil))
}
currentY += itemSize.height
rowIndex += 1
}
context.fillPath()
})
self.effectNode.update(backgroundColor: color, foregroundColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4))
self.effectNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size)
}
transition.updateFrame(node: self.backgroundColorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
}
}

View file

@ -0,0 +1,223 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TelegramPresentationData
import ContextUI
import TextFieldComponent
import MultilineTextComponent
import BundleIconComponent
final class SearchContextItem: ContextMenuCustomItem {
let context: AccountContext
let placeholder: String
let value: String
let valueChanged: (String) -> Void
init(
context: AccountContext,
placeholder: String,
value: String,
valueChanged: @escaping (String) -> Void
) {
self.context = context
self.placeholder = placeholder
self.value = value
self.valueChanged = valueChanged
}
func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
return SearchContextItemNode(
presentationData: presentationData,
item: self,
getController: getController,
actionSelected: actionSelected
)
}
}
private final class SearchContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol, ASScrollViewDelegate {
private let item: SearchContextItem
private let presentationData: PresentationData
private let getController: () -> ContextControllerProtocol?
private let actionSelected: (ContextMenuActionResult) -> Void
private let state = EmptyComponentState()
private let icon = ComponentView<Empty>()
private let inputField = ComponentView<Empty>()
private let inputFieldExternalState = TextFieldComponent.ExternalState()
private let inputPlaceholderView = ComponentView<Empty>()
private let inputClear = ComponentView<Empty>()
private var inputText = ""
private var validLayout: CGSize?
init(presentationData: PresentationData, item: SearchContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
self.item = item
self.presentationData = presentationData
self.getController = getController
self.actionSelected = actionSelected
super.init()
self.state._updated = { [weak self] transition, _ in
guard let self, let size = self.validLayout else {
return
}
self.internalUpdateLayout(size: size, transition: transition)
}
}
func internalUpdateLayout(size: CGSize, transition: ComponentTransition) {
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Search", tintColor: self.presentationData.theme.contextMenu.primaryColor)),
environment: {},
containerSize: size
)
let iconFrame = CGRect(origin: CGPoint(x: 17.0, y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.view.addSubview(iconView)
}
transition.setFrame(view: iconView, frame: iconFrame)
}
let inputInset: CGFloat = 42.0
self.inputField.parentState = self.state
let inputFieldSize = self.inputField.update(
transition: .immediate,
component: AnyComponent(TextFieldComponent(
context: self.item.context,
theme: self.presentationData.theme,
strings: self.presentationData.strings,
externalState: self.inputFieldExternalState,
fontSize: self.presentationData.listsFontSize.baseDisplaySize,
textColor: self.presentationData.theme.contextMenu.primaryColor,
accentColor: self.presentationData.theme.contextMenu.primaryColor,
insets: UIEdgeInsets(top: 8.0, left: 2.0, bottom: 8.0, right: 2.0),
hideKeyboard: false,
customInputView: nil,
resetText: nil,
isOneLineWhenUnfocused: false,
emptyLineHandling: .notAllowed,
formatMenuAvailability: .none,
returnKeyType: .search,
lockedFormatAction: {
},
present: { _ in
},
paste: { _ in
},
returnKeyAction: nil,
backspaceKeyAction: nil
)),
environment: {},
containerSize: CGSize(width: size.width - inputInset - 40.0, height: size.height)
)
let inputFieldFrame = CGRect(origin: CGPoint(x: inputInset, y: floorToScreenPixels((size.height - inputFieldSize.height) / 2.0)), size: inputFieldSize)
if let inputFieldView = self.inputField.view as? TextFieldComponent.View {
if inputFieldView.superview == nil {
self.view.addSubview(inputFieldView)
}
transition.setFrame(view: inputFieldView, frame: inputFieldFrame)
}
if self.inputText != self.inputFieldExternalState.text.string {
self.inputText = self.inputFieldExternalState.text.string
self.item.valueChanged(self.inputText)
}
let inputPlaceholderSize = self.inputPlaceholderView.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(
string: self.item.placeholder,
font: Font.regular(self.presentationData.listsFontSize.baseDisplaySize),
textColor: self.presentationData.theme.contextMenu.secondaryColor
)))
),
environment: {},
containerSize: size
)
let inputPlaceholderFrame = CGRect(origin: CGPoint(x: inputInset + 10.0, y: floorToScreenPixels(inputFieldFrame.midY - inputPlaceholderSize.height / 2.0)), size: inputPlaceholderSize)
if let inputPlaceholderView = self.inputPlaceholderView.view {
if inputPlaceholderView.superview == nil {
inputPlaceholderView.isUserInteractionEnabled = false
self.view.addSubview(inputPlaceholderView)
}
inputPlaceholderView.frame = inputPlaceholderFrame
inputPlaceholderView.isHidden = self.inputFieldExternalState.hasText
}
let inputClearSize = self.inputClear.update(
transition: .immediate,
component: AnyComponent(
Button(
content: AnyComponent(
BundleIconComponent(name: "Components/Search Bar/Clear", tintColor: self.presentationData.theme.contextMenu.secondaryColor, maxSize: CGSize(width: 24.0, height: 24.0))
),
action: { [weak self] in
guard let self else {
return
}
if let inputFieldView = self.inputField.view as? TextFieldComponent.View {
inputFieldView.updateText(NSAttributedString(), selectionRange: 0..<0)
}
}
)
),
environment: {},
containerSize: CGSize(width: 30.0, height: 30.0)
)
let inputClearFrame = CGRect(origin: CGPoint(x: size.width - inputClearSize.width - 16.0, y: floorToScreenPixels(inputFieldFrame.midY - inputClearSize.height / 2.0)), size: inputClearSize)
if let inputClearView = self.inputClear.view {
if inputClearView.superview == nil {
self.view.addSubview(inputClearView)
}
inputClearView.frame = inputClearFrame
inputClearView.isHidden = !self.inputFieldExternalState.hasText
}
}
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
let maxWidth: CGFloat = 220.0
let height: CGFloat = 42.0
return (CGSize(width: maxWidth, height: height), { size, transition in
self.validLayout = size
self.internalUpdateLayout(size: size, transition: ComponentTransition(transition))
})
}
func updateTheme(presentationData: PresentationData) {
}
var isActionEnabled: Bool {
return true
}
func performAction() {
}
func setIsHighlighted(_ value: Bool) {
}
func canBeHighlighted() -> Bool {
return false
}
func updateIsHighlighted(isHighlighted: Bool) {
}
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol {
return self
}
}

View file

@ -9,6 +9,95 @@ import MoreButtonNode
import AccountContext
import TelegramPresentationData
final class PriceButtonComponent: Component {
let price: Int64
init(
price: Int64
) {
self.price = price
}
static func ==(lhs: PriceButtonComponent, rhs: PriceButtonComponent) -> Bool {
return lhs.price == rhs.price
}
final class View: UIView {
private let backgroundView = UIView()
private let icon = UIImageView()
private let text = ComponentView<Empty>()
private var component: PriceButtonComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundView.clipsToBounds = true
self.addSubview(self.backgroundView)
self.icon.image = UIImage(bundleImageName: "Premium/Stars/ButtonStar")?.withRenderingMode(.alwaysTemplate)
self.backgroundView.addSubview(self.icon)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: PriceButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
var backgroundSize = CGSize(width: 42.0, height: 30.0)
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "\(component.price)",
font: Font.semibold(11.0),
textColor: UIColor(rgb: 0xffffff)
))
)),
environment: {},
containerSize: availableSize
)
let textFrame = CGRect(origin: CGPoint(x: 32.0, y: floorToScreenPixels((backgroundSize.height - textSize.height) / 2.0)), size: textSize)
if let textView = self.text.view {
if textView.superview == nil {
self.backgroundView.addSubview(textView)
}
transition.setFrame(view: textView, frame: textFrame)
}
backgroundSize.width += textSize.width
self.backgroundView.layer.cornerRadius = backgroundSize.height / 2.0
let backgroundColor: UIColor = UIColor(rgb: 0xffffff, alpha: 0.1)
transition.setBackgroundColor(view: self.backgroundView, color: backgroundColor)
let backgroundFrame = CGRect(origin: .zero, size: backgroundSize)
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
if let iconSize = self.icon.image?.size {
let iconFrame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((backgroundSize.height - iconSize.height) / 2.0)), size: iconSize)
transition.setFrame(view: self.icon, frame: iconFrame)
}
self.icon.tintColor = UIColor(rgb: 0xffffff)
return backgroundSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class ButtonsComponent: Component {
let theme: PresentationTheme
let isOverlay: Bool

View file

@ -164,7 +164,7 @@ private final class GiftTransferAlertContentNode: AlertContentNode {
theme: self.presentationTheme,
strings: self.strings,
peer: nil,
subject: .uniqueGift(gift: self.gift),
subject: .uniqueGift(gift: self.gift, price: nil),
mode: .thumbnail
)
),

View file

@ -159,7 +159,7 @@ private final class SheetContent: CombinedComponent {
var ribbonColor: GiftItemComponent.Ribbon.Color = .blue
for attribute in displayGift.attributes {
if case let .backdrop(_, innerColor, outerColor, _, _, _) = attribute {
if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
ribbonColor = .custom(outerColor, innerColor)
break
}
@ -175,7 +175,7 @@ private final class SheetContent: CombinedComponent {
context: component.context,
theme: theme,
strings: strings,
subject: .uniqueGift(gift: displayGift),
subject: .uniqueGift(gift: displayGift, price: nil),
ribbon: GiftItemComponent.Ribbon(text: "#\(displayGift.number)", font: .monospaced, color: ribbonColor),
mode: .grid
)

View file

@ -148,7 +148,7 @@ private final class GiftWithdrawAlertContentNode: AlertContentNode {
theme: self.presentationTheme,
strings: self.strings,
peer: nil,
subject: .uniqueGift(gift: self.gift),
subject: .uniqueGift(gift: self.gift, price: nil),
mode: .thumbnail
)
),

View file

@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MarqueeComponent",
module_name = "MarqueeComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,180 @@
import Foundation
import UIKit
import Display
import ComponentFlow
private let animationDuration: TimeInterval = 12.0
private let animationDelay: TimeInterval = 2.0
private let spacing: CGFloat = 20.0
public final class MarqueeComponent: Component {
public static let innerPadding: CGFloat = 16.0
private final class MeasureState: Equatable {
let attributedText: NSAttributedString
let availableSize: CGSize
let size: CGSize
init(attributedText: NSAttributedString, availableSize: CGSize, size: CGSize) {
self.attributedText = attributedText
self.availableSize = availableSize
self.size = size
}
static func ==(lhs: MeasureState, rhs: MeasureState) -> Bool {
if !lhs.attributedText.isEqual(rhs.attributedText) {
return false
}
if lhs.availableSize != rhs.availableSize {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
}
public final class View: UIView {
private var measureState: MeasureState?
private let containerLayer = SimpleLayer()
private let textLayer = SimpleLayer()
private let duplicateTextLayer = SimpleLayer()
private let gradientMaskLayer = SimpleGradientLayer()
private var isAnimating = false
private var isOverflowing = false
override init(frame: CGRect) {
super.init(frame: frame)
self.clipsToBounds = true
self.containerLayer.masksToBounds = true
self.layer.addSublayer(self.containerLayer)
self.containerLayer.addSublayer(self.textLayer)
self.containerLayer.addSublayer(self.duplicateTextLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(component: MarqueeComponent, availableSize: CGSize) -> CGSize {
let attributedText = component.attributedText
if let measureState = self.measureState {
if measureState.attributedText.isEqual(to: attributedText) && measureState.availableSize == availableSize {
return measureState.size
}
}
var boundingRect = attributedText.boundingRect(with: CGSize(width: 10000, height: availableSize.height), options: .usesLineFragmentOrigin, context: nil)
boundingRect.size.width = ceil(boundingRect.size.width)
boundingRect.size.height = ceil(boundingRect.size.height)
let measureState = MeasureState(attributedText: attributedText, availableSize: availableSize, size: boundingRect.size)
self.measureState = measureState
self.containerLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: measureState.size.width + innerPadding * 2.0, height: measureState.size.height))
let isOverflowing = boundingRect.width > availableSize.width
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: measureState.size))
let image = renderer.image { context in
UIGraphicsPushContext(context.cgContext)
measureState.attributedText.draw(at: CGPoint())
UIGraphicsPopContext()
}
if isOverflowing {
self.setupMarqueeTextLayers(textImage: image.cgImage!, textWidth: boundingRect.width, containerWidth: availableSize.width)
self.setupGradientMask(size: CGSize(width: availableSize.width, height: boundingRect.height))
self.startAnimation()
} else {
self.stopAnimation()
self.textLayer.frame = CGRect(origin: CGPoint(x: innerPadding, y: 0.0), size: boundingRect.size)
self.textLayer.contents = image.cgImage
self.duplicateTextLayer.frame = .zero
self.duplicateTextLayer.contents = nil
self.layer.mask = nil
}
return CGSize(width: min(measureState.size.width + innerPadding * 2.0, availableSize.width), height: measureState.size.height)
}
private func setupMarqueeTextLayers(textImage: CGImage, textWidth: CGFloat, containerWidth: CGFloat) {
self.textLayer.frame = CGRect(x: innerPadding, y: 0, width: textWidth, height: self.containerLayer.bounds.height)
self.textLayer.contents = textImage
self.duplicateTextLayer.frame = CGRect(x: innerPadding + textWidth + spacing, y: 0, width: textWidth, height: self.containerLayer.bounds.height)
self.duplicateTextLayer.contents = textImage
self.containerLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: textWidth * 2.0 + spacing, height: self.containerLayer.bounds.height))
}
private func setupGradientMask(size: CGSize) {
self.gradientMaskLayer.frame = CGRect(origin: .zero, size: size)
self.gradientMaskLayer.colors = [
UIColor.clear.cgColor,
UIColor.clear.cgColor,
UIColor.black.cgColor,
UIColor.black.cgColor,
UIColor.clear.cgColor,
UIColor.clear.cgColor
]
self.gradientMaskLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
self.gradientMaskLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
let edgePercentage = innerPadding / size.width
self.gradientMaskLayer.locations = [
0.0,
NSNumber(value: edgePercentage * 0.4),
NSNumber(value: edgePercentage),
NSNumber(value: 1.0 - edgePercentage),
NSNumber(value: 1.0 - edgePercentage * 0.4),
1.0
]
self.layer.mask = self.gradientMaskLayer
}
private func startAnimation() {
guard !self.isAnimating else {
return
}
self.isAnimating = true
self.containerLayer.removeAllAnimations()
self.containerLayer.animateBoundsOriginXAdditive(from: 0.0, to: self.textLayer.frame.width + spacing, duration: animationDuration, delay: animationDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, completion: { _ in
self.isAnimating = false
self.startAnimation()
})
}
private func stopAnimation() {
self.containerLayer.removeAllAnimations()
self.isAnimating = false
}
}
public let attributedText: NSAttributedString
public init(attributedText: NSAttributedString) {
self.attributedText = attributedText
}
public static func ==(lhs: MarqueeComponent, rhs: MarqueeComponent) -> Bool {
if lhs.attributedText != rhs.attributedText {
return false
}
return true
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize)
}
}

View file

@ -65,6 +65,8 @@ swift_library(
"//submodules/UrlEscaping",
"//submodules/DeviceLocationManager",
"//submodules/TelegramUI/Components/SaveProgressScreen",
"//submodules/TelegramUI/Components/MediaAssetsContext",
"//submodules/CheckNode",
],
visibility = [
"//visibility:public",

View file

@ -160,7 +160,7 @@ public extension MediaEditorScreenImpl {
} else {
existingMedia = storyItem.media
}
rootController.proceedWithStoryUpload(target: target, result: result as! MediaEditorScreenResult, existingMedia: existingMedia, forwardInfo: forwardInfo, externalState: externalState, commit: commit)
rootController.proceedWithStoryUpload(target: target, results: [result as! MediaEditorScreenResult], existingMedia: existingMedia, forwardInfo: forwardInfo, externalState: externalState, commit: commit)
}
})
} else {

View file

@ -326,6 +326,9 @@ final class MediaEditorScreenComponent: Component {
private let switchCameraButton = ComponentView<Empty>()
private let selectionButton = ComponentView<Empty>()
private let selectionPanel = ComponentView<Empty>()
private let textCancelButton = ComponentView<Empty>()
private let textDoneButton = ComponentView<Empty>()
private let textSize = ComponentView<Empty>()
@ -335,6 +338,8 @@ final class MediaEditorScreenComponent: Component {
private var isEditingCaption = false
private var currentInputMode: MessageInputPanelComponent.InputMode = .text
private var isSelectionPanelOpen = false
private var didInitializeInputMediaNodeDataPromise = false
private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData?
private var inputMediaNodeDataPromise = Promise<ChatEntityKeyboardInputNode.InputData>()
@ -737,6 +742,13 @@ final class MediaEditorScreenComponent: Component {
return inputText
}
func setInputText(_ text: NSAttributedString) {
guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else {
return
}
inputPanelView.setSendMessageInput(value: .text(text), updateState: true)
}
private func updateCoverPosition() {
guard let controller = self.environment?.controller() as? MediaEditorScreenImpl, let mediaEditor = controller.node.mediaEditor else {
return
@ -1988,6 +2000,120 @@ final class MediaEditorScreenComponent: Component {
transition.setScale(view: switchCameraButtonView, scale: isRecordingAdditionalVideo ? 1.0 : 0.01)
transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0)
}
if controller.node.items.count > 1 {
let selectionButtonSize = self.selectionButton.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(
SelectionPanelButtonContentComponent(
count: Int32(controller.node.items.count(where: { $0.isEnabled })),
isSelected: self.isSelectionPanelOpen,
tag: nil
)
),
effectAlignment: .center,
action: { [weak self] in
if let self {
self.isSelectionPanelOpen = !self.isSelectionPanelOpen
self.state?.updated()
}
},
animateAlpha: false
)),
environment: {},
containerSize: CGSize(width: 33.0, height: 33.0)
)
let selectionButtonFrame = CGRect(
origin: CGPoint(x: availableSize.width - selectionButtonSize.width - 12.0, y: inputPanelFrame.minY - selectionButtonSize.height - 3.0),
size: selectionButtonSize
)
if let selectionButtonView = self.selectionButton.view as? PlainButtonComponent.View {
if selectionButtonView.superview == nil {
self.addSubview(selectionButtonView)
}
transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center)
transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size))
transition.setScale(view: selectionButtonView, scale: displayTopButtons ? 1.0 : 0.01)
transition.setAlpha(view: selectionButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0)
if self.isSelectionPanelOpen {
let selectionPanelFrame = CGRect(
origin: CGPoint(x: 12.0, y: inputPanelFrame.minY - selectionButtonSize.height - 3.0 - 130.0),
size: CGSize(width: availableSize.width - 24.0, height: 120.0)
)
var selectedItemId = ""
if case let .asset(asset) = controller.node.subject {
selectedItemId = asset.localIdentifier
}
let _ = self.selectionPanel.update(
transition: transition,
component: AnyComponent(
SelectionPanelComponent(
previewContainerView: controller.node.previewContentContainerView,
frame: selectionPanelFrame,
items: controller.node.items,
selectedItemId: selectedItemId,
itemTapped: { [weak self, weak controller] id in
guard let self, let controller else {
return
}
self.isSelectionPanelOpen = false
self.state?.updated()
if let id {
controller.node.switchToItem(id)
}
},
itemSelectionToggled: { [weak self, weak controller] id in
guard let self, let controller else {
return
}
if let itemIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == id }) {
controller.node.items[itemIndex].isEnabled = !controller.node.items[itemIndex].isEnabled
}
self.state?.updated(transition: .spring(duration: 0.3))
},
itemReordered: { [weak self, weak controller] fromId, toId in
guard let self, let controller else {
return
}
guard let fromIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == fromId }), let toIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == toId }), toIndex < controller.node.items.count else {
return
}
let fromItem = controller.node.items[fromIndex]
let toItem = controller.node.items[toIndex]
controller.node.items[fromIndex] = toItem
controller.node.items[toIndex] = fromItem
self.state?.updated(transition: .spring(duration: 0.3))
}
)
),
environment: {},
containerSize: availableSize
)
if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View {
if selectionPanelView.superview == nil {
self.insertSubview(selectionPanelView, belowSubview: selectionButtonView)
if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View {
selectionPanelView.animateIn(from: buttonView)
}
}
selectionPanelView.frame = CGRect(origin: .zero, size: availableSize)
}
} else if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View {
if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View {
selectionPanelView.animateOut(to: buttonView, completion: { [weak selectionPanelView] in
selectionPanelView?.removeFromSuperview()
})
} else {
selectionPanelView.removeFromSuperview()
}
}
}
}
} else {
inputPanelSize = CGSize(width: 0.0, height: 12.0)
}
@ -2756,6 +2882,38 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
}
struct EditingItem: Equatable {
let asset: PHAsset
var values: MediaEditorValues?
var caption = NSAttributedString()
var thumbnail: UIImage?
var isEnabled = true
var version: Int = 0
init(asset: PHAsset) {
self.asset = asset
}
public static func ==(lhs: EditingItem, rhs: EditingItem) -> Bool {
if lhs.asset.localIdentifier != rhs.asset.localIdentifier {
return false
}
if lhs.values != rhs.values {
return false
}
if lhs.caption != rhs.caption {
return false
}
if lhs.thumbnail != rhs.thumbnail {
return false
}
if lhs.version != rhs.version {
return false
}
return true
}
}
final class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate, UIScrollViewDelegate {
private weak var controller: MediaEditorScreenImpl?
private let context: AccountContext
@ -2764,6 +2922,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
var subject: MediaEditorScreenImpl.Subject?
var actualSubject: MediaEditorScreenImpl.Subject?
var items: [EditingItem] = []
private var subjectDisposable: Disposable?
private var appInForegroundDisposable: Disposable?
@ -2852,6 +3011,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
private let readyValue = Promise<Bool>()
var componentHostView: MediaEditorScreenComponent.View? {
return self.componentHost.view as? MediaEditorScreenComponent.View
}
init(controller: MediaEditorScreenImpl) {
self.controller = controller
self.context = controller.context
@ -3023,7 +3186,46 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|> deliverOnMainQueue
).start(next: { [weak self] subject in
if let self, let subject {
self.setup(with: subject)
self.actualSubject = subject
var effectiveSubject = subject
switch subject {
case let .assets(assets):
effectiveSubject = .asset(assets.first!)
self.items = assets.map { EditingItem(asset: $0) }
case let .draft(draft, _):
for entity in draft.values.entities {
if case let .sticker(sticker) = entity {
switch sticker.content {
case let .message(ids, _, _, _, _):
effectiveSubject = .message(ids)
case let .gift(gift, _):
effectiveSubject = .gift(gift)
default:
break
}
}
}
default:
break
}
var privacy: MediaEditorResultPrivacy?
var values: MediaEditorValues?
var isDraft = false
if case let .draft(draft, _) = subject {
privacy = draft.privacy
values = draft.values
isDraft = true
}
self.setup(
subject: effectiveSubject,
privacy: privacy,
values: values,
caption: nil,
isDraft: isDraft
)
}
})
@ -3144,35 +3346,24 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self.stickerCutoutStatusDisposable?.dispose()
}
private func setup(with subject: MediaEditorScreenImpl.Subject) {
func setup(
subject: MediaEditorScreenImpl.Subject,
privacy: MediaEditorResultPrivacy? = nil,
values: MediaEditorValues?,
caption: NSAttributedString?,
isDraft: Bool = false
) {
guard let controller = self.controller else {
return
}
self.actualSubject = subject
var effectiveSubject = subject
if case let .draft(draft, _ ) = subject {
for entity in draft.values.entities {
if case let .sticker(sticker) = entity {
switch sticker.content {
case let .message(ids, _, _, _, _):
effectiveSubject = .message(ids)
case let .gift(gift, _):
effectiveSubject = .gift(gift)
default:
break
}
}
}
}
self.subject = effectiveSubject
self.subject = subject
Queue.mainQueue().justDispatch {
controller.setupAudioSessionIfNeeded()
}
if case let .draft(draft, _) = subject, let privacy = draft.privacy {
if let privacy {
controller.state.privacy = privacy
}
@ -3190,7 +3381,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
controller.isSavingAvailable = isSavingAvailable
controller.requestLayout(transition: .immediate)
let mediaDimensions = effectiveSubject.dimensions
let mediaDimensions = subject.dimensions
let maxSide: CGFloat = 1920.0 / UIScreen.main.scale
let fittedSize = mediaDimensions.cgSize.fitted(CGSize(width: maxSide, height: maxSide))
let mediaEntity = DrawingMediaEntity(size: fittedSize)
@ -3229,27 +3420,28 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
let initialValues: MediaEditorValues?
if case let .draft(draft, _) = subject {
initialValues = draft.values
if let values {
initialValues = values
for entity in draft.values.entities {
for entity in values.entities {
self.entitiesView.add(entity.entity.duplicate(copy: true), announce: false)
}
if let drawingData = initialValues?.drawing?.pngData() {
if let drawingData = values.drawing?.pngData() {
self.drawingView.setup(withDrawing: drawingData)
}
} else {
initialValues = nil
}
var mediaEditorMode: MediaEditor.Mode = .default
if case .stickerEditor = controller.mode {
let mediaEditorMode: MediaEditor.Mode
switch controller.mode {
case .stickerEditor:
mediaEditorMode = .sticker
} else if case .avatarEditor = controller.mode {
mediaEditorMode = .avatar
} else if case .coverEditor = controller.mode {
case .avatarEditor, .coverEditor:
mediaEditorMode = .avatar
default:
mediaEditorMode = .default
}
if let mediaEntityView = self.entitiesView.add(mediaEntity, announce: false) as? DrawingMediaEntityView {
@ -3275,7 +3467,13 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
}
let mediaEditor = MediaEditor(context: self.context, mode: mediaEditorMode, subject: effectiveSubject.editorSubject, values: initialValues, hasHistogram: true)
let mediaEditor = MediaEditor(
context: self.context,
mode: mediaEditorMode,
subject: subject.editorSubject,
values: initialValues,
hasHistogram: true
)
if case .avatarEditor = controller.mode {
mediaEditor.setVideoIsMuted(true)
} else if case let .coverEditor(dimensions) = controller.mode {
@ -3288,7 +3486,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
mediaEditor.seek(initialVideoPosition, andPlay: true)
}
}
if self.context.sharedContext.currentPresentationData.with({$0}).autoNightModeTriggered {
if !isDraft, self.context.sharedContext.currentPresentationData.with({$0}).autoNightModeTriggered {
switch subject {
case .message, .gift:
mediaEditor.setNightTheme(true)
@ -3308,46 +3506,48 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut))
}
}
self.stickerCutoutStatusDisposable = (mediaEditor.cutoutStatus
|> deliverOnMainQueue).start(next: { [weak self] cutoutStatus in
guard let self else {
return
if case .stickerEditor = controller.mode {
self.stickerCutoutStatusDisposable = (mediaEditor.cutoutStatus
|> deliverOnMainQueue).start(next: { [weak self] cutoutStatus in
guard let self else {
return
}
self.stickerCutoutStatus = cutoutStatus
self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25))
})
mediaEditor.maskUpdated = { [weak self] mask, apply in
guard let self else {
return
}
if self.stickerMaskDrawingView == nil {
self.setupMaskDrawingView(size: mask.size)
}
if apply, let maskData = mask.pngData() {
self.stickerMaskDrawingView?.setup(withDrawing: maskData, storeAsClear: true)
}
}
self.stickerCutoutStatus = cutoutStatus
self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25))
})
mediaEditor.maskUpdated = { [weak self] mask, apply in
guard let self else {
return
mediaEditor.classificationUpdated = { [weak self] classes in
guard let self else {
return
}
self.controller?.stickerRecommendedEmoji = emojiForClasses(classes.map { $0.0 })
}
if self.stickerMaskDrawingView == nil {
self.setupMaskDrawingView(size: mask.size)
}
if apply, let maskData = mask.pngData() {
self.stickerMaskDrawingView?.setup(withDrawing: maskData, storeAsClear: true)
}
}
mediaEditor.classificationUpdated = { [weak self] classes in
guard let self else {
return
}
self.controller?.stickerRecommendedEmoji = emojiForClasses(classes.map { $0.0 })
}
mediaEditor.attachPreviewView(self.previewView, andPlay: !(self.controller?.isEditingStoryCover ?? false))
if case .empty = effectiveSubject {
if case .empty = subject {
self.stickerMaskDrawingView?.emptyColor = .black
self.stickerMaskDrawingView?.clearWithEmptyColor()
}
switch effectiveSubject {
switch subject {
case .message, .gift:
break
default:
self.readyValue.set(.single(true))
}
switch effectiveSubject {
switch subject {
case let .image(_, _, additionalImage, position):
if let additionalImage {
let image = generateImage(CGSize(width: additionalImage.size.width, height: additionalImage.size.width), contextGenerator: { size, context in
@ -3392,7 +3592,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
case .message, .gift:
var isGift = false
let messages: Signal<[Message], NoError>
if case let .message(messageIds) = effectiveSubject {
if case let .message(messageIds) = subject {
messages = self.context.engine.data.get(
EngineDataMap(messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:)))
)
@ -3405,9 +3605,9 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
return messages
}
} else if case let .gift(gift) = effectiveSubject {
} else if case let .gift(gift) = subject {
isGift = true
let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, peerId: nil, senderId: nil, savedId: nil))]
let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, peerId: nil, senderId: nil, savedId: nil, resaleStars: nil))]
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
messages = .single([message])
} else {
@ -3429,7 +3629,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
let wallpaperColors: Signal<(UIColor?, UIColor?), NoError>
if let subject = self.subject, case .gift = subject {
if case .gift = subject {
wallpaperColors = self.mediaEditorPromise.get()
|> mapToSignal { mediaEditor in
if let mediaEditor {
@ -3459,7 +3659,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
let renderer = DrawingMessageRenderer(context: self.context, messages: messages, parentView: self.view, isGift: isGift, wallpaperDayColor: wallpaperColors.0, wallpaperNightColor: wallpaperColors.1)
renderer.render(completion: { result in
if case .draft = subject, let existingEntityView = self.entitiesView.getView(where: { entityView in
if isDraft, let existingEntityView = self.entitiesView.getView(where: { entityView in
if let stickerEntityView = entityView as? DrawingStickerEntityView {
if case .message = (stickerEntityView.entity as! DrawingStickerEntity).content {
return true
@ -3469,13 +3669,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
return false
}) as? DrawingStickerEntityView {
#if DEBUG
if let data = result.dayImage.pngData() {
let path = NSTemporaryDirectory() + "\(Int(Date().timeIntervalSince1970)).png"
try? data.write(to: URL(fileURLWithPath: path))
}
#endif
existingEntityView.isNightTheme = isNightTheme
let messageEntity = existingEntityView.entity as! DrawingStickerEntity
messageEntity.renderImage = result.dayImage
@ -3485,7 +3678,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} else {
var content: DrawingStickerEntity.Content
var position: CGPoint
switch effectiveSubject {
switch subject {
case let .message(messageIds):
content = .message(messageIds, result.size, messageFile, result.mediaFrame?.rect, result.mediaFrame?.cornerRadius)
position = CGPoint(x: storyDimensions.width / 2.0 - 54.0, y: storyDimensions.height / 2.0)
@ -3540,7 +3733,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self.backgroundDimView.isHidden = false
})
}
} else if CACurrentMediaTime() - self.initializationTimestamp > 0.2, case .image = subject {
} else if CACurrentMediaTime() - self.initializationTimestamp > 0.2, case .image = subject {
self.previewContainerView.alpha = 1.0
self.previewContainerView.layer.allowsGroupOpacity = true
self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in
@ -3569,7 +3762,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
})
}
if effectiveSubject.isPhoto {
if subject.isPhoto {
self.previewContainerView.layer.allowsGroupOpacity = true
self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in
self.previewContainerView.layer.allowsGroupOpacity = false
@ -3582,6 +3775,12 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
}
}
} else {
if let caption {
mediaEditor.onFirstDisplay = { [weak self] in
self?.componentHostView?.setInputText(caption)
}
}
}
mediaEditor.onPlaybackAction = { [weak self] action in
@ -3776,7 +3975,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
return nil
}
let colorSpace = CGColorSpaceCreateDeviceRGB()
let imageSize = CGSize(width: 1080, height: 1920)
let imageSize = storyDimensions
if let context = DrawingContext(size: imageSize, scale: 1.0, opaque: true, colorSpace: colorSpace) {
context.withFlippedContext { context in
if let image = mediaEditor.resultImage?.cgImage {
@ -4175,9 +4374,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
completion()
case .camera:
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateIn(from: .camera, completion: completion)
}
self.componentHostView?.animateIn(from: .camera, completion: completion)
if let subject = self.subject, case let .video(_, mainTransitionImage, _, _, additionalTransitionImage, _, _, positionChangeTimestamps, pipPosition) = subject, let mainTransitionImage {
var transitionImage = mainTransitionImage
@ -4188,7 +4385,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
backgroundImage = additionalTransitionImage
foregroundImage = mainTransitionImage
}
if let combinedTransitionImage = generateImage(CGSize(width: 1080, height: 1920), scale: 1.0, rotatedContext: { size, context in
if let combinedTransitionImage = generateImage(storyDimensions, scale: 1.0, rotatedContext: { size, context in
UIGraphicsPushContext(context)
backgroundImage.draw(in: CGRect(origin: CGPoint(x: (size.width - backgroundImage.size.width) / 2.0, y: (size.height - backgroundImage.size.height) / 2.0), size: backgroundImage.size))
@ -4214,9 +4411,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self.setupTransitionImage(sourceImage)
}
if let sourceView = transitionIn.sourceView {
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateIn(from: .gallery)
}
self.componentHostView?.animateIn(from: .gallery)
let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view)
let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width
@ -4234,7 +4429,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self.backgroundDimView.isHidden = false
self.backgroundDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.35)
if let componentView = self.componentHost.view {
if let componentView = self.componentHostView {
componentView.layer.animatePosition(from: sourceLocalFrame.center, to: componentView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
componentView.layer.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: timingFunction)
componentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
@ -4254,8 +4449,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
if animateIn, let layout = self.validLayout {
self.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height), to: .zero, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
completion()
} else if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateIn(from: .camera, completion: completion)
} else {
self.componentHostView?.animateIn(from: .camera, completion: completion)
}
}
}
@ -4307,9 +4502,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
destinationTransitionView = destinationTransitionOutView
destinationTransitionRect = galleryTransitionIn.sourceRect
}
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateOut(to: .gallery)
}
self.componentHostView?.animateOut(to: .gallery)
}
let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view)
let destinationScale = destinationLocalFrame.width / self.previewContainerView.frame.width
@ -4407,7 +4600,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
removeOnCompletion: false
)
if let componentView = self.componentHost.view {
if let componentView = self.componentHostView {
componentView.clipsToBounds = true
componentView.layer.animatePosition(from: componentView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
componentView.layer.animateScale(from: 1.0, to: destinationScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
@ -4425,18 +4618,14 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
}
} else if let transitionIn = controller.transitionIn, case .camera = transitionIn {
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateOut(to: .camera)
}
self.componentHostView?.animateOut(to: .camera)
let transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))
transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in
completion()
})
} else {
if controller.isEmbeddedEditor {
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateOut(to: .gallery)
}
self.componentHostView?.animateOut(to: .gallery)
self.layer.allowsGroupOpacity = true
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, removeOnCompletion: false, completion: { _ in
@ -4456,9 +4645,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self.isDisplayingTool = tool
let transition: ComponentTransition = .easeInOut(duration: 0.2)
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateOutToTool(inPlace: inPlace, transition: transition)
}
self.componentHostView?.animateOutToTool(inPlace: inPlace, transition: transition)
self.requestUpdate(transition: transition)
}
@ -4466,9 +4653,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self.isDisplayingTool = nil
let transition: ComponentTransition = .easeInOut(duration: 0.2)
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateInFromTool(inPlace: inPlace, transition: transition)
}
self.componentHostView?.animateInFromTool(inPlace: inPlace, transition: transition)
self.requestUpdate(transition: transition)
}
@ -4682,12 +4867,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
var location: CLLocationCoordinate2D?
if let subject = self.actualSubject {
if case let .asset(asset) = subject {
location = asset.location?.coordinate
} else if case let .draft(draft, _) = subject {
location = draft.location
}
if case let .draft(draft, _) = self.actualSubject {
location = draft.location
} else if case let .asset(asset) = self.subject {
location = asset.location?.coordinate
}
let locationController = storyLocationPickerController(
context: self.context,
@ -5156,7 +5339,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
func addWeather(_ weather: StickerPickerScreen.Weather.LoadedWeather?) {
guard let weather else {
return
}
let maxWeatherCount = 1
@ -5183,6 +5365,58 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
)
}
func getCaption() -> NSAttributedString {
return self.componentHostView?.getInputText() ?? NSAttributedString()
}
func switchToItem(_ identifier: String) {
guard let controller = self.controller, let mediaEditor = self.mediaEditor, let itemIndex = self.items.firstIndex(where: { $0.asset.localIdentifier == identifier }), case let .asset(asset) = self.subject, let currentItemIndex = self.items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) else {
return
}
let entities = self.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.entitiesView)
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
var updatedCurrentItem = self.items[currentItemIndex]
updatedCurrentItem.caption = self.getCaption()
if mediaEditor.values.hasChanges && updatedCurrentItem.values != mediaEditor.values {
updatedCurrentItem.values = mediaEditor.values
updatedCurrentItem.version += 1
if let resultImage = mediaEditor.resultImage {
mediaEditor.seek(0.0, andPlay: false)
makeEditorImageComposition(
context: self.ciContext,
postbox: self.context.account.postbox,
inputImage: resultImage,
dimensions: storyDimensions,
values: mediaEditor.values,
time: .zero,
textScale: 2.0,
completion: { [weak self] resultImage in
updatedCurrentItem.version += 1
updatedCurrentItem.thumbnail = resultImage
self?.items[currentItemIndex] = updatedCurrentItem
}
)
}
} else {
updatedCurrentItem.version += 1
self.items[currentItemIndex] = updatedCurrentItem
}
self.entitiesView.clearAll()
let targetItem = self.items[itemIndex]
controller.node.setup(
subject: .asset(targetItem.asset),
values: targetItem.values,
caption: targetItem.caption
)
}
func requestCompletion(playHaptic: Bool = true) {
guard let controller = self.controller else {
return
@ -5284,7 +5518,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result == self.componentHost.view {
if result == self.componentHostView {
let point = self.view.convert(point, to: self.previewContainerView)
if let previewResult = self.previewContainerView.hitTest(point, with: event) {
return previewResult
@ -6142,6 +6376,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
case message([MessageId])
case gift(StarGift.UniqueGift)
case sticker(TelegramMediaFile, [String])
case assets([PHAsset])
var dimensions: PixelDimensions {
switch self {
@ -6153,8 +6388,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
case let .draft(draft, _):
return draft.dimensions
case .message, .gift, .sticker, .videoCollage:
return PixelDimensions(width: 1080, height: 1920)
case .message, .gift, .sticker, .videoCollage, .assets:
return PixelDimensions(storyDimensions)
}
}
@ -6181,6 +6416,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
return .gift(gift)
case let .sticker(sticker, _):
return .sticker(sticker)
case let .assets(assets):
return .asset(assets.first!)
}
}
@ -6208,6 +6445,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
return false
case .sticker:
return false
case .assets:
return false
}
}
}
@ -6516,7 +6755,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
let privacy = privacy ?? self.state.privacy
let text = self.getCaption().string
let text = self.node.getCaption().string
let mentions = generateTextEntities(text, enabledTypes: [.mention], currentEntities: []).map { (text as NSString).substring(with: NSRange(location: $0.range.lowerBound + 1, length: $0.range.upperBound - $0.range.lowerBound - 1)) }
let coverImage: UIImage?
@ -6528,7 +6767,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
let stateContext = ShareWithPeersScreen.StateContext(
context: self.context,
subject: .stories(editing: false),
subject: .stories(editing: false, count: Int32(self.node.items.count(where: { $0.isEnabled }))),
editing: false,
initialPeerIds: Set(privacy.privacy.additionallyIncludePeers),
closeFriends: self.closeFriends.get(),
@ -7066,13 +7305,9 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self?.dismissed()
})
}
func getCaption() -> NSAttributedString {
return (self.node.componentHost.view as? MediaEditorScreenComponent.View)?.getInputText() ?? NSAttributedString()
}
fileprivate func checkCaptionLimit() -> Bool {
let caption = self.getCaption()
let caption = self.node.getCaption()
if caption.length > self.context.userLimits.maxStoryCaptionLength {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
@ -7121,7 +7356,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
var caption = self.getCaption()
var caption = self.node.getCaption()
caption = convertMarkdownToAttributes(caption)
var hasEntityChanges = false
@ -7170,7 +7405,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) {
self.saveDraft(id: randomId, edit: true)
self.saveDraft(id: randomId, isEdit: true)
self.completion(MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
@ -7459,7 +7694,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
duration = 5.0
case .sticker:
let image = generateImage(CGSize(width: 1080, height: 1920), contextGenerator: { size, context in
let image = generateImage(storyDimensions, contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
}, opaque: false, scale: 1.0)
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).png"
@ -7470,6 +7705,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
duration = 3.0
firstFrame = .single((image, nil))
case .assets:
fatalError()
}
let _ = combineLatest(queue: Queue.mainQueue(), firstFrame, videoResult)
@ -8360,6 +8597,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
case let .sticker(file, _):
exportSubject = .single(.sticker(file: file))
case .assets:
fatalError()
}
let _ = (exportSubject

View file

@ -11,7 +11,16 @@ import DrawingUI
extension MediaEditorScreenImpl {
func isEligibleForDraft() -> Bool {
if self.isEditingStory {
guard !self.isEditingStory else {
return false
}
if case .avatarEditor = self.mode {
return false
}
if case .coverEditor = self.mode {
return false
}
if case .assets = self.node.actualSubject {
return false
}
guard let mediaEditor = self.node.mediaEditor else {
@ -21,13 +30,6 @@ extension MediaEditorScreenImpl {
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
if case .avatarEditor = self.mode {
return false
}
if case .coverEditor = self.mode {
return false
}
let filteredEntities = self.node.entitiesView.entities.filter { entity in
if entity is DrawingMediaEntity {
return false
@ -44,34 +46,42 @@ extension MediaEditorScreenImpl {
let values = mediaEditor.values
let filteredValues = values.withUpdatedEntities([])
let caption = self.node.getCaption()
let caption = self.getCaption()
if let subject = self.node.subject {
if case .asset = subject, !values.hasChanges && caption.string.isEmpty {
return false
} else if case .message = subject, !filteredValues.hasChanges && filteredEntities.isEmpty && caption.string.isEmpty {
return false
} else if case .gift = subject, !filteredValues.hasChanges && filteredEntities.isEmpty && caption.string.isEmpty {
return false
} else if case .empty = subject, !self.node.hasAnyChanges && !self.node.drawingView.internalState.canUndo {
return false
} else if case .videoCollage = subject {
switch subject {
case .asset:
if !values.hasChanges && caption.string.isEmpty {
return false
}
case .message, .gift:
if !filteredValues.hasChanges && filteredEntities.isEmpty && caption.string.isEmpty {
return false
}
case .empty:
if !self.node.hasAnyChanges && !self.node.drawingView.internalState.canUndo {
return false
}
case .videoCollage:
return false
default:
break
}
}
return true
}
func saveDraft(id: Int64?, edit: Bool = false) {
func saveDraft(id: Int64?, isEdit: Bool = false, completion: ((MediaEditorDraft) -> Void)? = nil) {
guard case .storyEditor = self.mode, let subject = self.node.subject, let actualSubject = self.node.actualSubject, let mediaEditor = self.node.mediaEditor else {
return
}
try? FileManager.default.createDirectory(atPath: draftPath(engine: self.context.engine), withIntermediateDirectories: true)
let values = mediaEditor.values
let privacy = self.state.privacy
let forwardSource = self.forwardSource
let caption = self.getCaption()
let caption = self.node.getCaption()
let duration = mediaEditor.duration ?? 0.0
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
@ -99,10 +109,19 @@ extension MediaEditorScreenImpl {
}
if let resultImage = mediaEditor.resultImage {
if !edit {
if !isEdit {
mediaEditor.seek(0.0, andPlay: false)
}
makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: resultImage, dimensions: storyDimensions, values: values, time: .zero, textScale: 2.0, completion: { resultImage in
makeEditorImageComposition(
context: self.node.ciContext,
postbox: self.context.account.postbox,
inputImage: resultImage,
dimensions: storyDimensions,
values: values,
time: .zero,
textScale: 2.0,
completion: { resultImage in
guard let resultImage else {
return
}
@ -148,54 +167,64 @@ extension MediaEditorScreenImpl {
}
let context = self.context
func innerSaveDraft(media: MediaInput) {
func innerSaveDraft(media: MediaInput, save: Bool = true) -> MediaEditorDraft? {
let fittedSize = resultImage.size.aspectFitted(CGSize(width: 128.0, height: 128.0))
if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) {
let path = "\(Int64.random(in: .min ... .max)).\(media.fileExtension)"
let draft = MediaEditorDraft(
path: path,
isVideo: media.isVideo,
thumbnail: thumbnailImage,
dimensions: media.dimensions,
duration: media.duration,
values: values,
caption: caption,
privacy: privacy,
forwardInfo: forwardSource.flatMap { StoryId(peerId: $0.0.id, id: $0.1.id) },
timestamp: timestamp,
location: location,
expiresOn: expiresOn
)
switch media {
case let .image(image, _):
if let data = image.jpegData(compressionQuality: 0.87) {
try? data.write(to: URL(fileURLWithPath: draft.fullPath(engine: context.engine)))
}
case let .video(path, _, _):
try? FileManager.default.copyItem(atPath: path, toPath: draft.fullPath(engine: context.engine))
guard let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) else {
return nil
}
let path = "\(Int64.random(in: .min ... .max)).\(media.fileExtension)"
let draft = MediaEditorDraft(
path: path,
isVideo: media.isVideo,
thumbnail: thumbnailImage,
dimensions: media.dimensions,
duration: media.duration,
values: values,
caption: caption,
privacy: privacy,
forwardInfo: forwardSource.flatMap { StoryId(peerId: $0.0.id, id: $0.1.id) },
timestamp: timestamp,
location: location,
expiresOn: expiresOn
)
switch media {
case let .image(image, _):
if let data = image.jpegData(compressionQuality: 0.87) {
try? data.write(to: URL(fileURLWithPath: draft.fullPath(engine: context.engine)))
}
case let .video(path, _, _):
try? FileManager.default.copyItem(atPath: path, toPath: draft.fullPath(engine: context.engine))
}
if save {
if let id {
saveStorySource(engine: context.engine, item: draft, peerId: context.account.peerId, id: id)
} else {
addStoryDraft(engine: context.engine, item: draft)
}
}
return draft
}
switch subject {
case .empty:
break
case let .image(image, dimensions, _, _):
innerSaveDraft(media: .image(image: image, dimensions: dimensions))
if let draft = innerSaveDraft(media: .image(image: image, dimensions: dimensions)) {
completion?(draft)
}
case let .video(path, _, _, _, _, dimensions, _, _, _):
innerSaveDraft(media: .video(path: path, dimensions: dimensions, duration: duration))
if let draft = innerSaveDraft(media: .video(path: path, dimensions: dimensions, duration: duration)) {
completion?(draft)
}
case let .videoCollage(items):
let _ = items
case let .asset(asset):
if asset.mediaType == .video {
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
if let urlAsset = avAsset as? AVURLAsset {
innerSaveDraft(media: .video(path: urlAsset.url.relativePath, dimensions: PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration: duration))
if let draft = innerSaveDraft(media: .video(path: urlAsset.url.relativePath, dimensions: PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration: duration)) {
completion?(draft)
}
}
}
} else {
@ -203,22 +232,32 @@ extension MediaEditorScreenImpl {
options.deliveryMode = .highQualityFormat
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
if let image {
innerSaveDraft(media: .image(image: image, dimensions: PixelDimensions(image.size)))
if let draft = innerSaveDraft(media: .image(image: image, dimensions: PixelDimensions(image.size))) {
completion?(draft)
}
}
}
}
case let .draft(draft, _):
if draft.isVideo {
innerSaveDraft(media: .video(path: draft.fullPath(engine: context.engine), dimensions: draft.dimensions, duration: draft.duration ?? 0.0))
if let draft = innerSaveDraft(media: .video(path: draft.fullPath(engine: context.engine), dimensions: draft.dimensions, duration: draft.duration ?? 0.0)) {
completion?(draft)
}
} else if let image = UIImage(contentsOfFile: draft.fullPath(engine: context.engine)) {
innerSaveDraft(media: .image(image: image, dimensions: draft.dimensions))
if let draft = innerSaveDraft(media: .image(image: image, dimensions: draft.dimensions)) {
completion?(draft)
}
}
case .message, .gift:
if let pixel = generateSingleColorImage(size: CGSize(width: 1, height: 1), color: .black) {
innerSaveDraft(media: .image(image: pixel, dimensions: PixelDimensions(width: 1080, height: 1920)))
if let draft = innerSaveDraft(media: .image(image: pixel, dimensions: PixelDimensions(width: 1080, height: 1920))) {
completion?(draft)
}
}
case .sticker:
break
case .assets:
break
}
if case let .draft(draft, _) = actualSubject {

View file

@ -0,0 +1,140 @@
import Foundation
import UIKit
import Display
import ComponentFlow
final class SelectionPanelButtonContentComponent: Component {
let count: Int32
let isSelected: Bool
let tag: AnyObject?
init(
count: Int32,
isSelected: Bool,
tag: AnyObject?
) {
self.count = count
self.isSelected = isSelected
self.tag = tag
}
static func ==(lhs: SelectionPanelButtonContentComponent, rhs: SelectionPanelButtonContentComponent) -> Bool {
return lhs.count == rhs.count && lhs.isSelected == rhs.isSelected
}
final class View: UIView, ComponentTaggedView {
private var component: SelectionPanelButtonContentComponent?
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
let backgroundView: BlurredBackgroundView
private let outline = SimpleLayer()
private let icon = SimpleLayer()
private let label = ComponentView<Empty>()
init() {
self.backgroundView = BlurredBackgroundView(color: UIColor(white: 0.2, alpha: 0.45), enableBlur: true)
self.icon.opacity = 0.0
super.init(frame: CGRect())
self.addSubview(self.backgroundView)
self.layer.addSublayer(self.icon)
self.layer.addSublayer(self.outline)
self.outline.contents = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.clear(bounds)
let lineWidth: CGFloat = 2.0 - UIScreenPixel
context.setLineWidth(lineWidth)
context.setStrokeColor(UIColor.white.cgColor)
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0))
})?.cgImage
self.icon.contents = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.clear(bounds)
let lineWidth: CGFloat = 2.0 - UIScreenPixel
context.setLineWidth(lineWidth)
context.setStrokeColor(UIColor.white.cgColor)
context.move(to: CGPoint(x: 11.0, y: 11.0))
context.addLine(to: CGPoint(x: size.width - 11.0, y: size.height - 11.0))
context.strokePath()
context.move(to: CGPoint(x: size.width - 11.0, y: 11.0))
context.addLine(to: CGPoint(x: 11.0, y: size.height - 11.0))
context.strokePath()
})?.cgImage
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: SelectionPanelButtonContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
let size = CGSize(width: 33.0, height: 33.0)
let backgroundFrame = CGRect(origin: .zero, size: size)
self.backgroundView.frame = backgroundFrame
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.width / 2.0, transition: .immediate)
self.icon.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
self.icon.bounds = CGRect(origin: .zero, size: size)
self.outline.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
self.outline.bounds = CGRect(origin: .zero, size: size)
let labelSize = self.label.update(
transition: .immediate,
component: AnyComponent(
Text(
text: "\(component.count)",
font: Font.with(size: 18.0, design: .round, weight: .semibold),
color: .white
)
),
environment: {},
containerSize: size
)
let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
if let labelView = self.label.view {
if labelView.superview == nil {
self.addSubview(labelView)
}
labelView.center = labelFrame.center
labelView.bounds = CGRect(origin: .zero, size: labelFrame.size)
}
if (previousComponent?.isSelected ?? false) != component.isSelected {
let changeTransition: ComponentTransition = .easeInOut(duration: 0.2)
changeTransition.setAlpha(layer: self.icon, alpha: component.isSelected ? 1.0 : 0.0)
changeTransition.setTransform(layer: self.icon, transform: !component.isSelected ? CATransform3DMakeRotation(.pi / 4.0, 0.0, 0.0, 1.0) : CATransform3DIdentity)
if let labelView = self.label.view {
changeTransition.setAlpha(view: labelView, alpha: component.isSelected ? 0.0 : 1.0)
changeTransition.setTransform(view: labelView, transform: component.isSelected ? CATransform3DMakeRotation(-.pi / 4.0, 0.0, 0.0, 1.0) : CATransform3DIdentity)
}
}
return size
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View file

@ -0,0 +1,670 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import AccountContext
import MediaEditor
import MediaAssetsContext
import CheckNode
import TelegramPresentationData
final class SelectionPanelComponent: Component {
let previewContainerView: PortalSourceView
let frame: CGRect
let items: [MediaEditorScreenImpl.EditingItem]
let selectedItemId: String
let itemTapped: (String?) -> Void
let itemSelectionToggled: (String) -> Void
let itemReordered: (String, String) -> Void
init(
previewContainerView: PortalSourceView,
frame: CGRect,
items: [MediaEditorScreenImpl.EditingItem],
selectedItemId: String,
itemTapped: @escaping (String?) -> Void,
itemSelectionToggled: @escaping (String) -> Void,
itemReordered: @escaping (String, String) -> Void
) {
self.previewContainerView = previewContainerView
self.frame = frame
self.items = items
self.selectedItemId = selectedItemId
self.itemTapped = itemTapped
self.itemSelectionToggled = itemSelectionToggled
self.itemReordered = itemReordered
}
static func ==(lhs: SelectionPanelComponent, rhs: SelectionPanelComponent) -> Bool {
return lhs.frame == rhs.frame && lhs.items == rhs.items && lhs.selectedItemId == rhs.selectedItemId
}
final class View: UIView, UIGestureRecognizerDelegate {
final class ItemView: UIView {
private let backgroundNode: ASImageNode
private let imageNode: ImageNode
private let checkNode: InteractiveCheckNode
private var selectionLayer: SimpleShapeLayer?
var toggleSelection: () -> Void = {}
override init(frame: CGRect) {
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
self.imageNode = ImageNode()
self.imageNode.contentMode = .scaleAspectFill
self.checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: defaultDarkColorPresentationTheme, style: .overlay))
super.init(frame: frame)
self.clipsToBounds = true
self.layer.cornerRadius = 6.0
self.addSubview(self.backgroundNode.view)
self.addSubview(self.imageNode.view)
self.addSubview(self.checkNode.view)
self.checkNode.valueChanged = { [weak self] value in
guard let self else {
return
}
self.toggleSelection()
}
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
fileprivate var item: MediaEditorScreenImpl.EditingItem?
func update(item: MediaEditorScreenImpl.EditingItem, number: Int, isSelected: Bool, isEnabled: Bool, size: CGSize, portalView: PortalView?, transition: ComponentTransition) {
let previousItem = self.item
self.item = item
if previousItem?.asset.localIdentifier != item.asset.localIdentifier || previousItem?.version != item.version {
let imageSignal: Signal<UIImage?, NoError>
if let thumbnail = item.thumbnail {
imageSignal = .single(thumbnail)
self.imageNode.contentMode = .scaleAspectFill
} else {
imageSignal = assetImage(asset: item.asset, targetSize:CGSize(width: 128.0 * UIScreenScale, height: 128.0 * UIScreenScale), exact: false, synchronous: true)
self.imageNode.contentUpdated = { [weak self] image in
if let self {
if self.backgroundNode.image == nil {
if let image, image.size.width > image.size.height {
self.imageNode.contentMode = .scaleAspectFit
Queue.concurrentDefaultQueue().async {
let colors = mediaEditorGetGradientColors(from: image)
let gradientImage = mediaEditorGenerateGradientImage(size: CGSize(width: 3.0, height: 128.0), colors: colors.array)
Queue.mainQueue().async {
self.backgroundNode.image = gradientImage
}
}
} else {
self.imageNode.contentMode = .scaleAspectFill
}
}
}
}
}
self.imageNode.setSignal(imageSignal)
}
let backgroundSize = CGSize(width: size.width, height: floorToScreenPixels(size.width / 9.0 * 16.0))
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize)
self.imageNode.frame = CGRect(origin: .zero, size: size)
//self.checkNode.content = .counter(number)
self.checkNode.setSelected(isEnabled, animated: previousItem != nil)
let checkSize = CGSize(width: 29.0, height: 29.0)
self.checkNode.frame = CGRect(origin: CGPoint(x: size.width - checkSize.width - 4.0, y: 4.0), size: checkSize)
if isSelected, let portalView {
portalView.view.frame = CGRect(origin: .zero, size: size)
self.insertSubview(portalView.view, aboveSubview: self.imageNode.view)
}
let lineWidth: CGFloat = 2.0 - UIScreenPixel
let selectionFrame = CGRect(origin: .zero, size: size)
if isSelected {
let selectionLayer: SimpleShapeLayer
if let current = self.selectionLayer {
selectionLayer = current
} else {
selectionLayer = SimpleShapeLayer()
self.selectionLayer = selectionLayer
self.layer.addSublayer(selectionLayer)
selectionLayer.fillColor = UIColor.clear.cgColor
selectionLayer.strokeColor = UIColor.white.cgColor
selectionLayer.lineWidth = lineWidth
selectionLayer.frame = selectionFrame
selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
// if !transition.animation.isImmediate {
// let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
// selectionLayer.animate(from: initialPath, to: selectionLayer.path as AnyObject, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
// selectionLayer.animateShapeLineWidth(from: 0.0, to: lineWidth, duration: 0.2)
// }
}
} else if let selectionLayer = self.selectionLayer {
self.selectionLayer = nil
selectionLayer.removeFromSuperlayer()
// let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
// selectionLayer.animate(from: selectionLayer.path, to: targetPath, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false)
// selectionLayer.animateShapeLineWidth(from: selectionLayer.lineWidth, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
// selectionLayer.removeFromSuperlayer()
// })
}
}
}
private let backgroundView: BlurredBackgroundView
private let backgroundMaskView: UIView
private let backgroundMaskPanelView: UIView
private let scrollView: UIScrollView
private var itemViews: [AnyHashable: ItemView] = [:]
private var portalView: PortalView?
private var reorderRecognizer: ReorderGestureRecognizer?
private var reorderingItem: (id: AnyHashable, initialPosition: CGPoint, position: CGPoint, snapshotView: UIView)?
private var tapRecognizer: UITapGestureRecognizer?
private var component: SelectionPanelComponent?
private var state: EmptyComponentState?
override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: UIColor(white: 0.2, alpha: 0.45), enableBlur: true)
self.backgroundMaskView = UIView(frame: .zero)
self.backgroundMaskPanelView = UIView(frame: .zero)
self.backgroundMaskPanelView.backgroundColor = UIColor.white
self.backgroundMaskPanelView.clipsToBounds = true
self.backgroundMaskPanelView.layer.cornerRadius = 10.0
self.scrollView = UIScrollView(frame: .zero)
self.scrollView.contentInsetAdjustmentBehavior = .never
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.layer.cornerRadius = 10.0
super.init(frame: frame)
self.backgroundView.mask = self.backgroundMaskView
let reorderRecognizer = ReorderGestureRecognizer(
shouldBegin: { [weak self] point in
guard let self, let item = self.item(at: point) else {
return (allowed: false, requiresLongPress: false, item: nil)
}
return (allowed: true, requiresLongPress: true, item: item)
},
willBegin: { point in
},
began: { [weak self] item in
guard let self else {
return
}
self.setReorderingItem(item: item)
},
ended: { [weak self] in
guard let self else {
return
}
self.setReorderingItem(item: nil)
},
moved: { [weak self] distance in
guard let self else {
return
}
self.moveReorderingItem(distance: distance)
},
isActiveUpdated: { _ in
}
)
reorderRecognizer.delegate = self
self.reorderRecognizer = reorderRecognizer
self.addGestureRecognizer(reorderRecognizer)
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap))
self.tapRecognizer = tapRecognizer
self.addGestureRecognizer(tapRecognizer)
self.addSubview(self.backgroundView)
self.addSubview(self.scrollView)
self.backgroundMaskView.addSubview(self.backgroundMaskPanelView)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
deinit {
}
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
guard let component = self.component else {
return
}
self.reorderRecognizer?.isEnabled = false
self.reorderRecognizer?.isEnabled = true
let location = gestureRecognizer.location(in: self)
if let itemView = self.item(at: location), let item = itemView.item, item.asset.localIdentifier != component.selectedItemId {
component.itemTapped(item.asset.localIdentifier)
} else {
component.itemTapped(nil)
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer is UITapGestureRecognizer {
return true
}
if otherGestureRecognizer is UIPanGestureRecognizer {
if gestureRecognizer === self.reorderRecognizer, ![.began, .changed].contains(gestureRecognizer.state) {
gestureRecognizer.isEnabled = false
gestureRecognizer.isEnabled = true
return true
} else {
return false
}
}
return false
}
func item(at point: CGPoint) -> ItemView? {
let point = self.convert(point, to: self.scrollView)
for (_, itemView) in self.itemViews {
if itemView.frame.contains(point) {
return itemView
}
}
return nil
}
func setReorderingItem(item: ItemView?) {
self.tapRecognizer?.isEnabled = false
self.tapRecognizer?.isEnabled = true
var mappedItem: (AnyHashable, ItemView)?
if let item {
for (id, visibleItem) in self.itemViews {
if visibleItem === item {
mappedItem = (id, visibleItem)
break
}
}
}
if self.reorderingItem?.id != mappedItem?.0 {
let transition: ComponentTransition = .spring(duration: 0.4)
if let (id, itemView) = mappedItem, let snapshotView = itemView.snapshotView(afterScreenUpdates: false) {
itemView.isHidden = true
let position = self.scrollView.convert(itemView.center, to: self)
snapshotView.center = position
transition.setScale(view: snapshotView, scale: 0.9)
self.addSubview(snapshotView)
self.reorderingItem = (id, position, position, snapshotView)
} else {
if let (id, _, _, snapshotView) = self.reorderingItem {
if let itemView = self.itemViews[id] {
if let innerSnapshotView = snapshotView.snapshotView(afterScreenUpdates: false) {
innerSnapshotView.center = self.convert(snapshotView.center, to: self.scrollView)
innerSnapshotView.transform = CGAffineTransformMakeScale(0.9, 0.9)
self.scrollView.addSubview(innerSnapshotView)
transition.setPosition(view: innerSnapshotView, position: itemView.center, completion: { [weak innerSnapshotView] _ in
innerSnapshotView?.removeFromSuperview()
itemView.isHidden = false
})
transition.setScale(view: innerSnapshotView, scale: 1.0)
}
transition.setPosition(view: snapshotView, position: self.scrollView.convert(itemView.center, to: self), completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
transition.setScale(view: snapshotView, scale: 1.0)
transition.setAlpha(view: snapshotView, alpha: 0.0)
}
}
self.reorderingItem = nil
}
self.state?.updated(transition: transition)
}
}
func moveReorderingItem(distance: CGPoint) {
guard let component = self.component else {
return
}
if let (id, initialPosition, _, snapshotView) = self.reorderingItem {
let targetPosition = CGPoint(x: initialPosition.x + distance.x, y: initialPosition.y + distance.y)
self.reorderingItem = (id, initialPosition, targetPosition, snapshotView)
snapshotView.center = targetPosition
let mappedPosition = self.convert(targetPosition, to: self.scrollView)
if let visibleReorderingItem = self.itemViews[id], let fromId = self.itemViews[id]?.item?.asset.localIdentifier {
for (_, visibleItem) in self.itemViews {
if visibleItem === visibleReorderingItem {
continue
}
if visibleItem.frame.contains(mappedPosition), let toId = visibleItem.item?.asset.localIdentifier {
component.itemReordered(fromId, toId)
break
}
}
}
}
}
func animateIn(from buttonView: SelectionPanelButtonContentComponent.View) {
}
func animateOut(to buttonView: SelectionPanelButtonContentComponent.View, completion: @escaping () -> Void) {
completion()
}
func update(component: SelectionPanelComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
if self.portalView == nil {
if let portalView = PortalView(matchPosition: false) {
portalView.view.layer.rasterizationScale = UIScreenScale
let scale = 95.0 / component.previewContainerView.frame.width
portalView.view.transform = CGAffineTransformMakeScale(scale, scale)
component.previewContainerView.addPortal(view: portalView)
self.portalView = portalView
}
}
var validIds = Set<AnyHashable>()
let itemSize = CGSize(width: 95.0, height: 112.0)
let spacing: CGFloat = 4.0
var itemFrame: CGRect = CGRect(origin: CGPoint(x: spacing, y: spacing), size: itemSize)
var index = 1
for item in component.items {
let id = item.asset.localIdentifier
validIds.insert(id)
var itemTransition = transition
let itemView: ItemView
if let current = self.itemViews[id] {
itemView = current
} else {
itemView = ItemView(frame: itemFrame)
self.scrollView.addSubview(itemView)
self.itemViews[id] = itemView
itemTransition = .immediate
}
itemView.toggleSelection = { [weak self] in
guard let self, let component = self.component else {
return
}
component.itemSelectionToggled(id)
}
itemView.update(item: item, number: index, isSelected: item.asset.localIdentifier == component.selectedItemId, isEnabled: item.isEnabled, size: itemFrame.size, portalView: self.portalView, transition: itemTransition)
itemTransition.setBounds(view: itemView, bounds: CGRect(origin: .zero, size: itemFrame.size))
itemTransition.setPosition(view: itemView, position: itemFrame.center)
itemFrame.origin.x += itemSize.width + spacing
index += 1
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
transition.setAlpha(view: itemView, alpha: 0.0, completion: { [weak itemView] _ in
itemView?.removeFromSuperview()
})
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
let contentSize = CGSize(width: itemFrame.minX, height: itemSize.height + spacing * 2.0)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let backgroundSize = CGSize(width: min(availableSize.width - 24.0, contentSize.width), height: contentSize.height)
self.backgroundView.frame = CGRect(origin: .zero, size: availableSize)
self.backgroundView.update(size: availableSize, transition: .immediate)
let contentFrame = CGRect(origin: CGPoint(x: availableSize.width - 12.0 - backgroundSize.width, y: component.frame.minY), size: backgroundSize)
self.backgroundMaskPanelView.frame = contentFrame
self.scrollView.frame = contentFrame
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, transition: transition)
}
}
private final class ReorderGestureRecognizer: UIGestureRecognizer {
private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: SelectionPanelComponent.View.ItemView?)
private let willBegin: (CGPoint) -> Void
private let began: (SelectionPanelComponent.View.ItemView) -> Void
private let ended: () -> Void
private let moved: (CGPoint) -> Void
private let isActiveUpdated: (Bool) -> Void
private var initialLocation: CGPoint?
private var longTapTimer: SwiftSignalKit.Timer?
private var longPressTimer: SwiftSignalKit.Timer?
private var itemView: SelectionPanelComponent.View.ItemView?
public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: SelectionPanelComponent.View.ItemView?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (SelectionPanelComponent.View.ItemView) -> Void, ended: @escaping () -> Void, moved: @escaping (CGPoint) -> Void, isActiveUpdated: @escaping (Bool) -> Void) {
self.shouldBegin = shouldBegin
self.willBegin = willBegin
self.began = began
self.ended = ended
self.moved = moved
self.isActiveUpdated = isActiveUpdated
super.init(target: nil, action: nil)
}
deinit {
self.longTapTimer?.invalidate()
self.longPressTimer?.invalidate()
}
private func startLongTapTimer() {
self.longTapTimer?.invalidate()
let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in
self?.longTapTimerFired()
}, queue: Queue.mainQueue())
self.longTapTimer = longTapTimer
longTapTimer.start()
}
private func stopLongTapTimer() {
self.itemView = nil
self.longTapTimer?.invalidate()
self.longTapTimer = nil
}
private func startLongPressTimer() {
self.longPressTimer?.invalidate()
let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in
self?.longPressTimerFired()
}, queue: Queue.mainQueue())
self.longPressTimer = longPressTimer
longPressTimer.start()
}
private func stopLongPressTimer() {
self.itemView = nil
self.longPressTimer?.invalidate()
self.longPressTimer = nil
}
override public func reset() {
super.reset()
self.itemView = nil
self.stopLongTapTimer()
self.stopLongPressTimer()
self.initialLocation = nil
self.isActiveUpdated(false)
}
private func longTapTimerFired() {
guard let location = self.initialLocation else {
return
}
self.longTapTimer?.invalidate()
self.longTapTimer = nil
self.willBegin(location)
}
private func longPressTimerFired() {
guard let _ = self.initialLocation else {
return
}
self.isActiveUpdated(true)
self.state = .began
self.longPressTimer?.invalidate()
self.longPressTimer = nil
self.longTapTimer?.invalidate()
self.longTapTimer = nil
if let itemView = self.itemView {
self.began(itemView)
}
self.isActiveUpdated(true)
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if self.numberOfTouches > 1 {
self.isActiveUpdated(false)
self.state = .failed
self.ended()
return
}
if self.state == .possible {
if let location = touches.first?.location(in: self.view) {
let (allowed, requiresLongPress, itemView) = self.shouldBegin(location)
if allowed {
self.isActiveUpdated(true)
self.itemView = itemView
self.initialLocation = location
if requiresLongPress {
self.startLongTapTimer()
self.startLongPressTimer()
} else {
self.state = .began
if let itemView = self.itemView {
self.began(itemView)
}
}
} else {
self.isActiveUpdated(false)
self.state = .failed
}
} else {
self.isActiveUpdated(false)
self.state = .failed
}
}
}
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
self.initialLocation = nil
self.stopLongTapTimer()
if self.longPressTimer != nil {
self.stopLongPressTimer()
self.isActiveUpdated(false)
self.state = .failed
}
if self.state == .began || self.state == .changed {
self.isActiveUpdated(false)
self.ended()
self.state = .failed
}
}
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
self.initialLocation = nil
self.stopLongTapTimer()
if self.longPressTimer != nil {
self.isActiveUpdated(false)
self.stopLongPressTimer()
self.state = .failed
}
if self.state == .began || self.state == .changed {
self.isActiveUpdated(false)
self.ended()
self.state = .failed
}
}
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
self.state = .changed
let offset = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y)
self.moved(offset)
} else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil {
let touchLocation = touch.location(in: self.view)
let dX = touchLocation.x - initialTapLocation.x
let dY = touchLocation.y - initialTapLocation.y
if dX * dX + dY * dY > 3.0 * 3.0 {
self.stopLongTapTimer()
self.stopLongPressTimer()
self.initialLocation = nil
self.isActiveUpdated(false)
self.state = .failed
}
}
}
}

View file

@ -497,7 +497,7 @@ private class GiftIconLayer: SimpleLayer {
for attribute in gift.attributes {
if case let .model(_, fileValue, _) = attribute {
file = fileValue
} else if case let .backdrop(_, innerColor, _, _, _, _) = attribute {
} else if case let .backdrop(_, _, innerColor, _, _, _, _) = attribute {
color = UIColor(rgb: UInt32(bitPattern: innerColor))
}
}
@ -563,7 +563,7 @@ private class GiftIconLayer: SimpleLayer {
for attribute in gift.attributes {
if case let .model(_, fileValue, _) = attribute {
file = fileValue
} else if case let .backdrop(_, innerColor, _, _, _, _) = attribute {
} else if case let .backdrop(_, _, innerColor, _, _, _, _) = attribute {
color = UIColor(rgb: UInt32(bitPattern: innerColor))
}
}

View file

@ -4867,6 +4867,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
return profileGifts.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo)
},
buyGift: { [weak profileGifts] slug, peerId in
guard let profileGifts else {
return .never()
}
return profileGifts.buyStarGift(slug: slug, peerId: peerId)
},
shareStory: { [weak self] uniqueGift in
guard let self, let controller = self.controller else {
return
@ -10012,7 +10018,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
viewControllers = viewControllers.filter { !($0 is AttachmentController)}
rootController.setViewControllers(viewControllers, animated: false)
rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
}
},
cancelled: {}
@ -10047,7 +10053,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self.openBotPreviewEditor(target: .botPreview(id: self.peerId, language: pane.currentBotPreviewLanguage?.id), source: result, transitionIn: (transitionView, transitionRect, transitionImage))
},
multipleCompletion: { _ in },
multipleCompletion: { _, _ in },
dismissed: {},
groupsPresented: {}
)

View file

@ -477,42 +477,52 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
itemTransition = .immediate
}
let ribbonText: String?
var ribbonText: String?
var ribbonColor: GiftItemComponent.Ribbon.Color = .blue
var ribbonFont: GiftItemComponent.Ribbon.Font = .generic
var ribbonOutline: UIColor?
let peer: GiftItemComponent.Peer?
let subject: GiftItemComponent.Subject
var resellPrice: Int64?
switch product.gift {
case let .generic(gift):
subject = .starGift(gift: gift, price: "⭐️ \(gift.price)")
peer = product.fromPeer.flatMap { .peer($0) } ?? .anonymous
if let availability = gift.availability {
ribbonText = params.presentationData.strings.PeerInfo_Gifts_OneOf(compactNumericCountString(Int(availability.total), decimalSeparator: params.presentationData.dateTimeFormat.decimalSeparator)).string
} else {
ribbonText = nil
}
case let .unique(gift):
if product.pinnedToTop {
ribbonFont = .monospaced
ribbonText = "#\(gift.number)"
subject = .uniqueGift(gift: gift, price: nil)
peer = nil
resellPrice = gift.resellStars
if let _ = resellPrice {
//TODO:localize
ribbonText = "sale"
ribbonFont = .larger
ribbonColor = .green
ribbonOutline = params.presentationData.theme.list.blocksBackgroundColor
} else {
ribbonText = params.presentationData.strings.PeerInfo_Gifts_OneOf(compactNumericCountString(Int(gift.availability.issued), decimalSeparator: params.presentationData.dateTimeFormat.decimalSeparator)).string
}
for attribute in gift.attributes {
if case let .backdrop(_, innerColor, outerColor, _, _, _) = attribute {
ribbonColor = .custom(outerColor, innerColor)
break
if product.pinnedToTop {
ribbonFont = .monospaced
ribbonText = "#\(gift.number)"
} else {
ribbonText = params.presentationData.strings.PeerInfo_Gifts_OneOf(compactNumericCountString(Int(gift.availability.issued), decimalSeparator: params.presentationData.dateTimeFormat.decimalSeparator)).string
}
for attribute in gift.attributes {
if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
ribbonColor = .custom(outerColor, innerColor)
break
}
}
}
}
let peer: GiftItemComponent.Peer?
let subject: GiftItemComponent.Subject
switch product.gift {
case let .generic(gift):
subject = .starGift(gift: gift, price: "⭐️ \(gift.price)")
peer = product.fromPeer.flatMap { .peer($0) } ?? .anonymous
case let .unique(gift):
subject = .uniqueGift(gift: gift)
peer = nil
}
let _ = visibleItem.update(
transition: itemTransition,
component: AnyComponent(
@ -522,7 +532,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
strings: params.presentationData.strings,
peer: peer,
subject: subject,
ribbon: ribbonText.flatMap { GiftItemComponent.Ribbon(text: $0, font: ribbonFont, color: ribbonColor) },
ribbon: ribbonText.flatMap { GiftItemComponent.Ribbon(text: $0, font: ribbonFont, color: ribbonColor, outline: ribbonOutline) },
resellPrice: resellPrice,
isHidden: !product.savedToProfile,
isPinned: product.pinnedToTop,
isEditing: self.isReordering,
@ -589,6 +600,18 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
return self.profileGifts.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo)
},
buyGift: { [weak self] slug, peerId in
guard let self else {
return .never()
}
return self.profileGifts.buyStarGift(slug: slug, peerId: peerId)
},
updateResellStars: { [weak self] price in
guard let self, case let .unique(uniqueGift) = product.gift else {
return
}
self.profileGifts.updateStarGiftResellPrice(slug: uniqueGift.slug, price: price)
},
togglePinnedToTop: { [weak self] pinnedToTop in
guard let self else {
return false

View file

@ -125,7 +125,7 @@ final class GiftListItemComponent: Component {
theme: component.theme,
strings: component.context.sharedContext.currentPresentationData.with { $0 }.strings,
peer: nil,
subject: .uniqueGift(gift: gift),
subject: .uniqueGift(gift: gift, price: nil),
ribbon: nil,
isHidden: false,
isSelected: gift.id == component.selectedId,

View file

@ -456,10 +456,11 @@ final class UserAppearanceScreenComponent: Component {
attributes: [
.model(name: "", file: file, rarity: 0),
.pattern(name: "", file: patternFile, rarity: 0),
.backdrop(name: "", innerColor: innerColor, outerColor: outerColor, patternColor: patternColor, textColor: textColor, rarity: 0)
.backdrop(name: "", id: 0, innerColor: innerColor, outerColor: outerColor, patternColor: patternColor, textColor: textColor, rarity: 0)
],
availability: StarGift.UniqueGift.Availability(issued: 0, total: 0),
giftAddress: nil
giftAddress: nil,
resellStars: nil
)
signal = component.context.engine.accountData.setStarGiftStatus(starGift: gift, expirationDate: emojiStatus.expirationDate)
} else {
@ -1090,7 +1091,7 @@ final class UserAppearanceScreenComponent: Component {
case let .pattern(_, file, _):
patternFileId = file.fileId.id
self.cachedIconFiles[file.fileId.id] = file
case let .backdrop(_, innerColorValue, outerColorValue, patternColorValue, textColorValue, _):
case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, textColorValue, _):
innerColor = innerColorValue
outerColor = outerColorValue
patternColor = patternColorValue

View file

@ -991,10 +991,22 @@ final class ShareWithPeersScreenComponent: Component {
}
let sectionTitle: String
if section.id == 0, case .stories = component.stateContext.subject {
sectionTitle = component.coverItem == nil ? environment.strings.Story_Privacy_PostStoryAsHeader : ""
if section.id == 0, case let .stories(_, count) = component.stateContext.subject {
if component.coverItem == nil {
if count > 1 {
sectionTitle = environment.strings.Story_Privacy_PostStoriesAsHeader
} else {
sectionTitle = environment.strings.Story_Privacy_PostStoryAsHeader
}
} else {
sectionTitle = ""
}
} else if section.id == 2 {
sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader
if case let .stories(_, count) = component.stateContext.subject, count > 1 {
sectionTitle = environment.strings.Story_Privacy_WhoCanViewStoriesHeader
} else {
sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader
}
} else if section.id == 1 {
if case let .members(isGroup, _, _) = component.stateContext.subject {
sectionTitle = isGroup ? environment.strings.BoostGift_Members_SectionTitle : environment.strings.BoostGift_Subscribers_SectionTitle
@ -1637,12 +1649,21 @@ final class ShareWithPeersScreenComponent: Component {
}
let footerValue = environment.strings.Story_Privacy_KeepOnMyPageHours(Int32(component.timeout / 3600))
var footerText = environment.strings.Story_Privacy_KeepOnMyPageInfo(footerValue).string
if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel == true {
footerText = isSendAsGroup ? environment.strings.Story_Privacy_KeepOnGroupPageInfo(footerValue).string : environment.strings.Story_Privacy_KeepOnChannelPageInfo(footerValue).string
var footerText: String
if case let .stories(_, count) = component.stateContext.subject, count > 1 {
if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel == true {
footerText = isSendAsGroup ? environment.strings.Story_Privacy_KeepOnGroupPageManyInfo(footerValue).string : environment.strings.Story_Privacy_KeepOnChannelPageManyInfo(footerValue).string
} else {
footerText = environment.strings.Story_Privacy_KeepOnMyPageManyInfo(footerValue).string
}
} else {
if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel == true {
footerText = isSendAsGroup ? environment.strings.Story_Privacy_KeepOnGroupPageInfo(footerValue).string : environment.strings.Story_Privacy_KeepOnChannelPageInfo(footerValue).string
} else {
footerText = environment.strings.Story_Privacy_KeepOnMyPageInfo(footerValue).string
}
}
let footerSize = sectionFooter.update(
transition: sectionFooterTransition,
component: AnyComponent(MultilineTextComponent(
@ -2371,7 +2392,7 @@ final class ShareWithPeersScreenComponent: Component {
)
var footersTotalHeight: CGFloat = 0.0
if case let .stories(editing) = component.stateContext.subject {
if case let .stories(editing, _) = component.stateContext.subject {
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor)
let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor)
let link = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor)
@ -2451,7 +2472,7 @@ final class ShareWithPeersScreenComponent: Component {
itemHeight: peerItemSize.height,
itemCount: peers.count
))
} else if case let .stories(editing) = component.stateContext.subject {
} else if case let .stories(editing, _) = component.stateContext.subject {
if !editing && hasChannels {
sections.append(ItemLayout.Section(
id: 0,
@ -2533,12 +2554,17 @@ final class ShareWithPeersScreenComponent: Component {
switch component.stateContext.subject {
case .peers:
title = environment.strings.Story_Privacy_PostStoryAs
case let .stories(editing):
case let .stories(editing, count):
if editing {
title = environment.strings.Story_Privacy_EditStory
} else {
title = environment.strings.Story_Privacy_ShareStory
actionButtonTitle = environment.strings.Story_Privacy_PostStory
if count > 1 {
title = environment.strings.Story_Privacy_ShareStories
actionButtonTitle = environment.strings.Story_Privacy_PostStories(count)
} else {
title = environment.strings.Story_Privacy_ShareStory
actionButtonTitle = environment.strings.Story_Privacy_PostStory
}
}
case let .chats(grayList):
if grayList {
@ -2627,7 +2653,7 @@ final class ShareWithPeersScreenComponent: Component {
inset = 1000.0
} else if case .channels = component.stateContext.subject {
inset = 1000.0
} else if case let .stories(editing) = component.stateContext.subject {
} else if case let .stories(editing, _) = component.stateContext.subject {
if editing {
inset = 351.0
inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight
@ -3026,7 +3052,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = []
var optionItems: [ShareWithPeersScreenComponent.OptionItem] = []
var coverItem: ShareWithPeersScreenComponent.CoverItem?
if case let .stories(editing) = stateContext.subject {
if case let .stories(editing, _) = stateContext.subject {
var everyoneSubtitle = presentationData.strings.Story_Privacy_ExcludePeople
if (stateContext.stateValue?.savedSelectedPeers[.everyone]?.count ?? 0) > 0 {
var peerNamesArray: [String] = []

View file

@ -44,7 +44,7 @@ public extension ShareWithPeersScreen {
final class StateContext {
public enum Subject: Equatable {
case peers(peers: [EnginePeer], peerId: EnginePeer.Id?)
case stories(editing: Bool)
case stories(editing: Bool, count: Int32)
case chats(blocked: Bool)
case contacts(base: EngineStoryPrivacy.Base)
case contactsSearch(query: String, onlyContacts: Bool)

View file

@ -126,7 +126,7 @@ public final class StarsAvatarComponent: Component {
theme: component.theme,
strings: component.context.sharedContext.currentPresentationData.with { $0 }.strings,
peer: nil,
subject: .uniqueGift(gift: gift),
subject: .uniqueGift(gift: gift, price: nil),
mode: .thumbnail
)
),

View file

@ -105,7 +105,8 @@ private final class SheetContent: CombinedComponent {
let minAmount: StarsAmount?
let maxAmount: StarsAmount?
let configuration = StarsWithdrawConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
let withdrawConfiguration = StarsWithdrawConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
switch component.mode {
case let .withdraw(status):
@ -113,7 +114,7 @@ private final class SheetContent: CombinedComponent {
amountTitle = environment.strings.Stars_Withdraw_AmountTitle
amountPlaceholder = environment.strings.Stars_Withdraw_AmountPlaceholder
minAmount = configuration.minWithdrawAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
minAmount = withdrawConfiguration.minWithdrawAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
maxAmount = status.balances.availableBalance
amountLabel = nil
case .accountWithdraw:
@ -121,7 +122,7 @@ private final class SheetContent: CombinedComponent {
amountTitle = environment.strings.Stars_Withdraw_AmountTitle
amountPlaceholder = environment.strings.Stars_Withdraw_AmountPlaceholder
minAmount = configuration.minWithdrawAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
minAmount = withdrawConfiguration.minWithdrawAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
maxAmount = state.balance
amountLabel = nil
case .paidMedia:
@ -130,9 +131,9 @@ private final class SheetContent: CombinedComponent {
amountPlaceholder = environment.strings.Stars_PaidContent_AmountPlaceholder
minAmount = StarsAmount(value: 1, nanos: 0)
maxAmount = configuration.maxPaidMediaAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
maxAmount = withdrawConfiguration.maxPaidMediaAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
if let usdWithdrawRate = configuration.usdWithdrawRate, let amount = state.amount, amount > StarsAmount.zero {
if let usdWithdrawRate = withdrawConfiguration.usdWithdrawRate, let amount = state.amount, amount > StarsAmount.zero {
let usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0
amountLabel = "\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))"
} else {
@ -144,7 +145,16 @@ private final class SheetContent: CombinedComponent {
amountPlaceholder = environment.strings.Stars_SendStars_AmountPlaceholder
minAmount = StarsAmount(value: 1, nanos: 0)
maxAmount = configuration.maxPaidMediaAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
maxAmount = withdrawConfiguration.maxPaidMediaAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
amountLabel = nil
case let .starGiftResell(update):
//TODO:localize
titleString = update ? "Edit Price" : "Sell Gift"
amountTitle = "PRICE IN STARS"
amountPlaceholder = "Enter Price"
minAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMinAmount, nanos: 0)
maxAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMaxAmount, nanos: 0)
amountLabel = nil
}
@ -214,10 +224,16 @@ private final class SheetContent: CombinedComponent {
}
let amountFont = Font.regular(13.0)
let boldAmountFont = Font.semibold(13.0)
let amountTextColor = theme.list.freeTextColor
let amountMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), bold: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), link: MarkdownAttributeSet(font: amountFont, textColor: theme.list.itemAccentColor), linkAttribute: { contents in
let amountMarkdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor),
bold: MarkdownAttributeSet(font: boldAmountFont, textColor: amountTextColor),
link: MarkdownAttributeSet(font: amountFont, textColor: theme.list.itemAccentColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
}
)
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme)
}
@ -252,6 +268,18 @@ private final class SheetContent: CombinedComponent {
text: .plain(amountInfoString),
maximumNumberOfLines: 0
))
case .starGiftResell:
//TODO:localize
let amountInfoString: NSAttributedString
if let value = state.amount?.value, value > 0 {
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString("You will receive **\(Int32(floor(Float(value) * 0.8))) Stars**.", attributes: amountMarkdownAttributes, textAlignment: .natural))
} else {
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString("You will receive **80%**.", attributes: amountMarkdownAttributes, textAlignment: .natural))
}
amountFooter = AnyComponent(MultilineTextComponent(
text: .plain(amountInfoString),
maximumNumberOfLines: 0
))
default:
amountFooter = nil
}
@ -305,8 +333,15 @@ private final class SheetContent: CombinedComponent {
let buttonString: String
if case .paidMedia = component.mode {
buttonString = environment.strings.Stars_PaidContent_Create
} else if case .starGiftResell = component.mode {
//TODO:localize
if let amount = state.amount, amount.value > 0 {
buttonString = "Sell for # \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))"
} else {
buttonString = "Sell"
}
} else if let amount = state.amount {
buttonString = "\(environment.strings.Stars_Withdraw_Withdraw) # \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))"
buttonString = "\(environment.strings.Stars_Withdraw_Withdraw) # \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))"
} else {
buttonString = environment.strings.Stars_Withdraw_Withdraw
}
@ -318,8 +353,9 @@ private final class SheetContent: CombinedComponent {
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
if let range = buttonAttributedString.string.range(of: "#"), let starImage = state.cachedStarImage?.0 {
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.foregroundColor, value: theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string))
}
let button = button.update(
@ -394,6 +430,8 @@ private final class SheetContent: CombinedComponent {
amount = initialValue.flatMap { StarsAmount(value: $0, nanos: 0) }
case .reaction:
amount = nil
case .starGiftResell:
amount = nil
}
self.amount = amount
@ -514,9 +552,11 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
case accountWithdraw
case paidMedia(Int64?)
case reaction(Int64?)
case starGiftResell(Bool)
}
private let context: AccountContext
private let mode: StarsWithdrawScreen.Mode
fileprivate let completion: (Int64) -> Void
public init(
@ -525,6 +565,7 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
completion: @escaping (Int64) -> Void
) {
self.context = context
self.mode = mode
self.completion = completion
super.init(
@ -558,12 +599,17 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
func presentMinAmountTooltip(_ minAmount: Int64) {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
var text = presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(minAmount))).string
if case .starGiftResell = self.mode {
text = "You cannot sell gift for less than \(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(minAmount)))."
}
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .image(
image: UIImage(bundleImageName: "Premium/Stars/StarLarge")!,
title: nil,
text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(minAmount))).string,
text: text,
round: false,
undoText: nil
),

View file

@ -5068,7 +5068,7 @@ public final class StoryItemSetContainerComponent: Component {
let stateContext = ShareWithPeersScreen.StateContext(
context: context,
subject: .stories(editing: true),
subject: .stories(editing: true, count: 1),
editing: true,
initialSelectedPeers: selectedPeers,
closeFriends: component.closeFriends.get(),

View file

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

View file

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

View file

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

View file

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

View file

@ -1124,7 +1124,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
let sendGiftTitle: String
var isIncoming = message.effectivelyIncoming(context.account.peerId)
for media in message.media {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _) = action.action {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _, _) = action.action {
if isUpgrade && message.author?.id == context.account.peerId {
isIncoming = true
}

View file

@ -2,6 +2,7 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import Postbox
import TelegramCore
import SwiftSignalKit
@ -17,6 +18,7 @@ import TelegramBaseController
import ContextUI
import SliderContextItem
import UndoUI
import MarqueeComponent
private func normalizeValue(_ value: CGFloat) -> CGFloat {
return round(value * 10.0) / 10.0
@ -147,6 +149,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
private let albumArtNode: TransformImageNode
private var largeAlbumArtNode: TransformImageNode?
private let titleNode: TextNode
private let title: ComponentView<Empty>
private let descriptionNode: TextNode
private let shareNode: HighlightableButtonNode
private let artistButton: HighlightTrackingButtonNode
@ -236,6 +239,8 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.title = ComponentView<Empty>()
self.descriptionNode = TextNode()
self.descriptionNode.isUserInteractionEnabled = false
self.descriptionNode.displaysAsynchronously = false
@ -295,7 +300,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
self.addSubnode(self.collapseNode)
self.addSubnode(self.albumArtNode)
self.addSubnode(self.titleNode)
//self.addSubnode(self.titleNode)
self.addSubnode(self.descriptionNode)
self.addSubnode(self.artistButton)
self.addSubnode(self.shareNode)
@ -725,8 +730,25 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
}
self.artistButton.isUserInteractionEnabled = hasArtist
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - sideInset * 2.0 - leftInset - rightInset - infoLabelsLeftInset - infoLabelsRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(
MarqueeComponent(attributedText: titleString ?? NSAttributedString())
),
environment: {},
containerSize: CGSize(width: width - sideInset * 2.0 - leftInset - rightInset - infoLabelsLeftInset - infoLabelsRightInset + MarqueeComponent.innerPadding, height: CGFloat.greatestFiniteMagnitude)
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.view.addSubview(titleView)
}
transition.updateFrame(view: titleView, frame: CGRect(origin: CGPoint(x: self.isExpanded ? floor((width - titleSize.width) / 2.0) : (leftInset + sideInset + infoLabelsLeftInset) - MarqueeComponent.innerPadding, y: infoVerticalOrigin + 1.0), size: titleSize))
}
let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode)
let (descriptionLayout, descriptionApply) = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - sideInset * 2.0 - leftInset - rightInset - infoLabelsLeftInset - infoLabelsRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))

View file

@ -81,8 +81,10 @@ import AccountFreezeInfoScreen
import JoinSubjectScreen
import OldChannelsController
import InviteLinksUI
import GiftStoreScreen
import SendInviteLinkScreen
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
let tokens = Bag<Void>()
@ -2985,11 +2987,11 @@ public final class SharedAccountContextImpl: SharedAccountContext {
))
controller.navigationPresentation = .modal
let _ = combineLatest(
let _ = (combineLatest(
queue: Queue.mainQueue(),
controller.result,
options.get()
).startStandalone(next: { [weak controller] result, options in
options.get())
|> take(1)).startStandalone(next: { [weak controller] result, options in
if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer, let starsContext = context.starsContext {
if case .starGiftTransfer = source {
presentTransferAlertImpl?(EnginePeer(peer))
@ -3268,6 +3270,14 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return controller
}
public func makeGiftStoreController(context: AccountContext, peerId: EnginePeer.Id, gift: StarGift.Gift) -> ViewController {
guard let starsContext = context.starsContext else {
fatalError()
}
let controller = GiftStoreScreen(context: context, starsContext: starsContext, peerId: peerId, gift: gift)
return controller
}
public func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController {
let mappedSubject: PremiumPrivacyScreen.Subject
let introSource: PremiumIntroSource
@ -3562,7 +3572,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return mediaPickerController(context: context, hasSearch: hasSearch, completion: completion)
}
public func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any]) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController {
public func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController {
return storyMediaPickerController(context: context, isDark: isDark, forCollage: forCollage, selectionLimit: selectionLimit, getSourceRect: getSourceRect, completion: completion, multipleCompletion: multipleCompletion, dismissed: dismissed, groupsPresented: groupsPresented)
}
@ -3657,6 +3667,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StarsWithdrawScreen(context: context, mode: .accountWithdraw, completion: completion)
}
public func makeStarGiftResellScreen(context: AccountContext, update: Bool, completion: @escaping (Int64) -> Void) -> ViewController {
return StarsWithdrawScreen(context: context, mode: .starGiftResell(update), completion: completion)
}
public func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController {
return StarsTransactionScreen(context: context, subject: .gift(message))
}
@ -3674,7 +3688,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
public func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, dismissed: (() -> Void)?) -> ViewController {
let controller = GiftViewScreen(context: context, subject: .uniqueGift(gift), shareStory: shareStory)
let controller = GiftViewScreen(context: context, subject: .uniqueGift(gift, nil), shareStory: shareStory)
controller.disposed = {
dismissed?()
}
@ -3723,7 +3737,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
externalState.storyTarget = target
if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
}
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId))

View file

@ -391,6 +391,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
return .asset(asset)
case let .draft(draft):
return .draft(draft, nil)
case let .assets(assets):
return .assets(assets)
}
}
@ -451,7 +453,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
if let customTarget, case .botPreview = customTarget {
externalState.storyTarget = customTarget
self.proceedWithStoryUpload(target: customTarget, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
self.proceedWithStoryUpload(target: customTarget, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
dismissCameraImpl?()
return
@ -484,7 +486,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
externalState.isPeerArchived = channel.storiesHidden ?? false
}
self.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
self.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
dismissCameraImpl?()
})
@ -548,8 +550,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
})
}
public func proceedWithStoryUpload(target: Stories.PendingTarget, result: MediaEditorScreenResult, existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) {
guard let result = result as? MediaEditorScreenImpl.Result else {
public func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) {
guard let results = results as? [MediaEditorScreenImpl.Result] else {
return
}
let context = self.context
@ -657,83 +659,85 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
}
if let _ = self.chatListController as? ChatListControllerImpl {
var media: EngineStoryInputMedia?
if let mediaResult = result.media {
switch mediaResult {
case let .image(image, dimensions):
let tempFile = TempBox.shared.tempFile(fileName: "file")
defer {
TempBox.shared.dispose(tempFile)
}
if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) {
media = .image(dimensions: dimensions, data: imageData, stickers: result.stickers)
}
case let .video(content, firstFrameImage, values, duration, dimensions):
let adjustments: VideoMediaResourceAdjustments
if let valuesData = try? JSONEncoder().encode(values) {
let data = MemoryBuffer(data: valuesData)
let digest = MemoryBuffer(data: data.md5Digest())
adjustments = VideoMediaResourceAdjustments(data: data, digest: digest, isStory: true)
let resource: TelegramMediaResource
switch content {
case let .imageFile(path):
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
case let .videoFile(path):
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
case let .asset(localIdentifier):
resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments))
}
for result in results {
var media: EngineStoryInputMedia?
if let mediaResult = result.media {
switch mediaResult {
case let .image(image, dimensions):
let tempFile = TempBox.shared.tempFile(fileName: "file")
defer {
TempBox.shared.dispose(tempFile)
}
let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6, tempFilePath: tempFile.path) }
let firstFrameFile = imageData.flatMap { data -> TempBoxFile? in
let file = TempBox.shared.tempFile(fileName: "image.jpg")
if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) {
return file
} else {
return nil
}
if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) {
media = .image(dimensions: dimensions, data: imageData, stickers: result.stickers)
}
var coverTime: Double?
if let coverImageTimestamp = values.coverImageTimestamp {
if let trimRange = values.videoTrimRange {
coverTime = min(duration, coverImageTimestamp - trimRange.lowerBound)
} else {
coverTime = min(duration, coverImageTimestamp)
case let .video(content, firstFrameImage, values, duration, dimensions):
let adjustments: VideoMediaResourceAdjustments
if let valuesData = try? JSONEncoder().encode(values) {
let data = MemoryBuffer(data: valuesData)
let digest = MemoryBuffer(data: data.md5Digest())
adjustments = VideoMediaResourceAdjustments(data: data, digest: digest, isStory: true)
let resource: TelegramMediaResource
switch content {
case let .imageFile(path):
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
case let .videoFile(path):
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
case let .asset(localIdentifier):
resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments))
}
let tempFile = TempBox.shared.tempFile(fileName: "file")
defer {
TempBox.shared.dispose(tempFile)
}
let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6, tempFilePath: tempFile.path) }
let firstFrameFile = imageData.flatMap { data -> TempBoxFile? in
let file = TempBox.shared.tempFile(fileName: "image.jpg")
if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) {
return file
} else {
return nil
}
}
var coverTime: Double?
if let coverImageTimestamp = values.coverImageTimestamp {
if let trimRange = values.videoTrimRange {
coverTime = min(duration, coverImageTimestamp - trimRange.lowerBound)
} else {
coverTime = min(duration, coverImageTimestamp)
}
}
media = .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers, coverTime: coverTime)
}
media = .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers, coverTime: coverTime)
default:
break
}
default:
break
} else if let existingMedia {
media = .existing(media: existingMedia._asMedia())
}
if let media {
let _ = (context.engine.messages.uploadStory(
target: target,
media: media,
mediaAreas: result.mediaAreas,
text: result.caption.string,
entities: generateChatInputTextEntities(result.caption),
pin: result.options.pin,
privacy: result.options.privacy,
isForwardingDisabled: result.options.isForwardingDisabled,
period: result.options.timeout,
randomId: result.randomId,
forwardInfo: forwardInfo
)
|> deliverOnMainQueue).startStandalone(next: { stableId in
moveStorySource(engine: context.engine, peerId: context.account.peerId, from: result.randomId, to: Int64(stableId))
})
}
} else if let existingMedia {
media = .existing(media: existingMedia._asMedia())
}
if let media {
let _ = (context.engine.messages.uploadStory(
target: target,
media: media,
mediaAreas: result.mediaAreas,
text: result.caption.string,
entities: generateChatInputTextEntities(result.caption),
pin: result.options.pin,
privacy: result.options.privacy,
isForwardingDisabled: result.options.isForwardingDisabled,
period: result.options.timeout,
randomId: result.randomId,
forwardInfo: forwardInfo
)
|> deliverOnMainQueue).startStandalone(next: { stableId in
moveStorySource(engine: context.engine, peerId: context.account.peerId, from: result.randomId, to: Int64(stableId))
})
}
completionImpl()
}

View file

@ -1483,7 +1483,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
externalState.storyTarget = target
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
}
})
if let navigationController = self.controller?.getNavigationController() {

2
third-party/td/td vendored

@ -1 +1 @@
Subproject commit f1b7500310baa496c0b779e4273a3aff0f14f42f
Subproject commit 04adfc87deea4c804def118e88c89a08c388b32b