diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 23ac7ddcb1..48ae9e3b6d 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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 %@."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index b542956c50..52c271d609 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -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)?) -> 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, 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 diff --git a/submodules/AccountContext/Sources/AttachmentMainButtonState.swift b/submodules/AccountContext/Sources/AttachmentMainButtonState.swift index 49650fca00..76b2dd5299 100644 --- a/submodules/AccountContext/Sources/AttachmentMainButtonState.swift +++ b/submodules/AccountContext/Sources/AttachmentMainButtonState.swift @@ -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 } diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 7c89ba3ba3..4cab8932e6 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -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: diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index fceecdd61b..b74cbe5631 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -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) diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index f4e1600400..e7c445e693 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -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: diff --git a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift index fb1a9bd468..05495508a2 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift @@ -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 diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index fc7495d6d9..7904765738 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -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( diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index ed8c8ae367..ef5021db86 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -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 } } } diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index f88dbae702..6842c65076 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -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 { 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 { 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 diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index eae0aa5176..7436c56971 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -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( diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 5cd59d6571..c79048b077 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -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 } diff --git a/submodules/Display/Source/CAAnimationUtils.swift b/submodules/Display/Source/CAAnimationUtils.swift index ad9317de39..952098bdee 100644 --- a/submodules/Display/Source/CAAnimationUtils.swift +++ b/submodules/Display/Source/CAAnimationUtils.swift @@ -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) { diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 7f65ce1f9d..01b660ad8d 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -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 { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 8a64406ecd..9ea769f836 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -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(nil) + fileprivate let secondaryButtonStatePromise = Promise(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 { - 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 { + 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) } } ) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index ae593c5869..fd0edc75e8 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -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: diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index 9634d92981..fb6c088eef 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -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() { diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index 4827256474..33eec1e2b1 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -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 diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index fcaefc6eca..deb0fe67e3 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -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 diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index 5a4cffd9d3..d92f3ab328 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -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) diff --git a/submodules/TelegramApi/Sources/Api35.swift b/submodules/TelegramApi/Sources/Api35.swift index a67f9921d3..cfc3b45e13 100644 --- a/submodules/TelegramApi/Sources/Api35.swift +++ b/submodules/TelegramApi/Sources/Api35.swift @@ -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 - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index 6018b0652b..804687918d 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -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 - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api37.swift b/submodules/TelegramApi/Sources/Api37.swift index df11f101cb..e76c07649a 100644 --- a/submodules/TelegramApi/Sources/Api37.swift +++ b/submodules/TelegramApi/Sources/Api37.swift @@ -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) diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index c3de890ad5..8f39ceae97 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -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) { + 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) { 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) { + 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) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 3edb3bf37b..2cf9cd110d 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -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): diff --git a/submodules/TelegramCore/Sources/State/Holes.swift b/submodules/TelegramCore/Sources/State/Holes.swift index 30b75e8580..16de04770f 100644 --- a/submodules/TelegramCore/Sources/State/Holes.swift +++ b/submodules/TelegramCore/Sources/State/Holes.swift @@ -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 } diff --git a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift index 45e2c0b72e..7ee61d5784 100644 --- a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift +++ b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift @@ -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)) diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 9a21f99bfc..a57226687c 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -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! { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 29d2a04b11..dcee84083e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index d57e8b8173..e1cf771e0c 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -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) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift index 61fd06f150..d69ec3624d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index b7f9fc5829..9abcd3c23f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -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 } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 05c3d070b3..3425c1f487 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -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 { + 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 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 { 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 { 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 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 { + 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 { 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 { + 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 { 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 { + return account.network.request(Api.functions.payments.updateStarGiftPrice(slug: slug, resellStars: price ?? 0)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { updates -> Signal 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() + var state: Signal { + 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 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 + + public var state: Signal { + 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 + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 7cb3c562a7..59e0d2849f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -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) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 7c1a697456..4677f08a9e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -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 { + return _internal_buyStarGift(account: self.account, slug: slug, peerId: peerId) + } + public func upgradeStarGift(formId: Int64?, reference: StarGiftReference, keepOriginalInfo: Bool) -> Signal { 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 { return _internal_toggleStarGiftsNotifications(account: self.account, peerId: peerId, enabled: enabled) } + + public func updateStarGiftResalePrice(slug: String, price: Int64?) -> Signal { + return _internal_updateStarGiftResalePrice(account: self.account, slug: slug, price: price) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift index ef3ca6433d..2d01695c57 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift @@ -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 diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index e38a8d1eb0..49ecafc847 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -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) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index e0c228ffef..202428afd9 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -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, diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index f3cec6741b..f2cf4d1cd2 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -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] = [] - 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 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] = [] + 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 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 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index bf9eb2c51a..b0cc08742c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -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)) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 26241e9a6b..3938669792 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -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 ) ), diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 3dfb52532c..f6f46e22b6 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift index 8a167de994..2f3eb20f16 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift @@ -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)) diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index c10f7ae3de..379755dd6b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -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() private let button = ComponentView() private let label = ComponentView() + + private let ribbonOutline = UIImageView() private let ribbon = UIImageView() private let ribbonText = ComponentView() @@ -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() + 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() + } +}() diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index a2edd0c5ed..7117e7fbd7 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -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] = [:] private let tabSelector = ComponentView() 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() 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)) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD new file mode 100644 index 0000000000..60e90c4299 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/FilterSelectorComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/FilterSelectorComponent.swift new file mode 100644 index 0000000000..5b31427f89 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/FilterSelectorComponent.swift @@ -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() + + 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, 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, 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 + } + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift new file mode 100644 index 0000000000..a55122afa6 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift @@ -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 + 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, + 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, 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.. 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() + 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 +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift new file mode 100644 index 0000000000..46fee26be0 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift @@ -0,0 +1,1201 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import AccountContext +import ComponentFlow +import ViewControllerComponent +import MultilineTextComponent +import BalancedTextComponent +import BundleIconComponent +import Markdown +import TelegramStringFormatting +import PlainButtonComponent +import BlurredBackgroundComponent +import PremiumStarComponent +import TextFormat +import GiftItemComponent +import InAppPurchaseManager +import GiftViewScreen +import UndoUI +import ContextUI +import LottieComponent + +final class GiftStoreScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let starsContext: StarsContext + let peerId: EnginePeer.Id + let gift: StarGift.Gift + + init( + context: AccountContext, + starsContext: StarsContext, + peerId: EnginePeer.Id, + gift: StarGift.Gift + ) { + self.context = context + self.starsContext = starsContext + self.peerId = peerId + self.gift = gift + } + + static func ==(lhs: GiftStoreScreenComponent, rhs: GiftStoreScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peerId != rhs.peerId { + return false + } + if lhs.gift != rhs.gift { + return false + } + return true + } + + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + final class View: UIView, UIScrollViewDelegate { + private let topOverscrollLayer = SimpleLayer() + private let scrollView: ScrollView + private let loadingNode: LoadingShimmerNode + private let emptyResultsAnimation = ComponentView() + private let emptyResultsTitle = ComponentView() + private let emptyResultsAction = ComponentView() + + private let topPanel = ComponentView() + private let topSeparator = ComponentView() + private let cancelButton = ComponentView() + private let sortButton = ComponentView() + + private let balanceTitle = ComponentView() + private let balanceValue = ComponentView() + private let balanceIcon = ComponentView() + + private let title = ComponentView() + private let subtitle = ComponentView() + + private var starsItems: [AnyHashable: ComponentView] = [:] + private let filterSelector = ComponentView() + + private var isUpdating: Bool = false + + private var starsStateDisposable: Disposable? + private var starsState: StarsContext.State? + + private var component: GiftStoreScreenComponent? + private(set) weak var state: State? + private var environment: EnvironmentType? + + override init(frame: CGRect) { + self.scrollView = ScrollView() + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.alwaysBounceVertical = true + + self.loadingNode = LoadingShimmerNode() + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + self.addSubview(self.loadingNode.view) + + self.scrollView.layer.addSublayer(self.topOverscrollLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.starsStateDisposable?.dispose() + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + var nextScrollTransition: ComponentTransition? + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrolling(interactive: true, transition: self.nextScrollTransition ?? .immediate) + } + + private var currentGifts: ([StarGift], Set, Set, Set)? + private var effectiveGifts: [StarGift]? { + if let gifts = self.state?.starGiftsState?.gifts { + return gifts + } else { + return nil + } + } + + private func updateScrolling(interactive: Bool = false, transition: ComponentTransition) { + guard let environment = self.environment, let component = self.component, self.state?.starGiftsState?.dataState != .loading else { + return + } + + let availableWidth = self.scrollView.bounds.width + let contentOffset = self.scrollView.contentOffset.y + + let topPanelAlpha = min(20.0, max(0.0, contentOffset)) / 20.0 + if let topPanelView = self.topPanel.view, let topSeparator = self.topSeparator.view { + transition.setAlpha(view: topPanelView, alpha: topPanelAlpha) + transition.setAlpha(view: topSeparator, alpha: topPanelAlpha) + } + + let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0) + if let starGifts = self.effectiveGifts { + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + let optionSpacing: CGFloat = 10.0 + let optionWidth = (availableWidth - sideInset * 2.0 - optionSpacing * 2.0) / 3.0 + let starsOptionSize = CGSize(width: optionWidth, height: 154.0) + + var validIds: [AnyHashable] = [] + var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: environment.navigationHeight + 39.0 + 9.0), size: starsOptionSize) + + let controller = environment.controller + + for gift in starGifts { + guard case let .unique(uniqueGift) = gift else { + continue + } + var isVisible = false + if visibleBounds.intersects(itemFrame) { + isVisible = true + } + + if isVisible { + let itemId = AnyHashable(gift.id) + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.starsItems[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + if !transition.animation.isImmediate { + itemTransition = .immediate + } + self.starsItems[itemId] = visibleItem + } + + var ribbon: GiftItemComponent.Ribbon? + var ribbonColor: GiftItemComponent.Ribbon.Color = .blue + for attribute in uniqueGift.attributes { + if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute { + ribbonColor = .custom(outerColor, innerColor) + break + } + } + ribbon = GiftItemComponent.Ribbon( + text: "#\(uniqueGift.number)", + font: .monospaced, + color: ribbonColor + ) + + let subject: GiftItemComponent.Subject = .uniqueGift(gift: uniqueGift, price: "⭐️\(uniqueGift.resellStars ?? 0)") + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + GiftItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: nil, + subject: subject, + ribbon: ribbon + ) + ), + effectAlignment: .center, + action: { [weak self] in + if let self, let component = self.component, let state = self.state { + if let controller = controller() as? GiftStoreScreen { + let mainController: ViewController + if let parentController = controller.parentController() { + mainController = parentController + } else { + mainController = controller + } + let giftController = GiftViewScreen( + context: component.context, + subject: .uniqueGift(uniqueGift, state.peerId) + ) + mainController.push(giftController) + } + } + }, + animateAlpha: false + ) + ), + environment: {}, + containerSize: starsOptionSize + ) + if let itemView = visibleItem.view { + if itemView.superview == nil { + self.scrollView.addSubview(itemView) + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + } + itemFrame.origin.x += itemFrame.width + optionSpacing + if itemFrame.maxX > availableWidth { + itemFrame.origin.x = sideInset + itemFrame.origin.y += starsOptionSize.height + optionSpacing + } + } + + var removeIds: [AnyHashable] = [] + for (id, item) in self.starsItems { + if !validIds.contains(id) { + removeIds.append(id) + if let itemView = item.view { + if !transition.animation.isImmediate { + itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } + } + } + } + for id in removeIds { + self.starsItems.removeValue(forKey: id) + } + } + + let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height) + if interactive, bottomContentOffset < 320.0 { + self.state?.starGiftsContext.loadMore() + } + } + + func openSortContextMenu(sourceView: UIView) { + guard let component = self.component, let controller = self.environment?.controller() else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: "Sort by Price", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortValue"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + self?.state?.starGiftsContext.updateSorting(.value) + }))) + items.append(.action(ContextMenuActionItem(text: "Sort by Date", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortDate"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + self?.state?.starGiftsContext.updateSorting(.date) + }))) + items.append(.action(ContextMenuActionItem(text: "Sort by Number", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortNumber"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + self?.state?.starGiftsContext.updateSorting(.number) + }))) + + let contextController = ContextController(presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + controller.presentInGlobalOverlay(contextController) + } + + func openModelContextMenu(sourceView: UIView) { + guard let component = self.component, let controller = self.environment?.controller() else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let searchQueryPromise = ValuePromise("") + + let attributes = self.state?.starGiftsState?.attributes ?? [] + let modelAttributes = attributes.filter { attribute in + if case .model = attribute { + return true + } else { + return false + } + } + + let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? [] + let selectedModelAttributes = currentFilterAttributes.filter { attribute in + if case .model = attribute { + return true + } else { + return false + } + } + + //TODO:localize + var items: [ContextMenuItem] = [] + items.append(.custom(SearchContextItem( + context: component.context, + placeholder: "Search", + value: "", + valueChanged: { value in + searchQueryPromise.set(value) + } + ), false)) + items.append(.separator) + items.append(.custom(GiftAttributeListContextItem( + context: component.context, + attributes: modelAttributes, + selectedAttributes: selectedModelAttributes, + attributeCount: self.state?.starGiftsState?.attributeCount ?? [:], + searchQuery: searchQueryPromise.get(), + attributeSelected: { [weak self] attribute, exclusive in + guard let self else { + return + } + var updatedFilterAttributes: [ResaleGiftsContext.Attribute] + if exclusive { + updatedFilterAttributes = currentFilterAttributes.filter { attribute in + if case .model = attribute { + return false + } + return true + } + updatedFilterAttributes.append(attribute) + } else { + updatedFilterAttributes = currentFilterAttributes + if selectedModelAttributes.contains(attribute) { + updatedFilterAttributes.removeAll(where: { $0 == attribute }) + } else { + updatedFilterAttributes.append(attribute) + } + } + self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) + }, + selectAll: { [weak self] in + guard let self else { + return + } + let updatedFilterAttributes = currentFilterAttributes.filter { attribute in + if case .model = attribute { + return false + } + return true + } + self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) + } + ), false)) + + let contextController = ContextController( + context: component.context, + presentationData: presentationData, + source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) + controller.presentInGlobalOverlay(contextController) + } + + func openBackdropContextMenu(sourceView: UIView) { + guard let component = self.component, let controller = self.environment?.controller() else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let searchQueryPromise = ValuePromise("") + + let attributes = self.state?.starGiftsState?.attributes ?? [] + let backdropAttributes = attributes.filter { attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + } + + let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? [] + let selectedBackdropAttributes = currentFilterAttributes.filter { attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + } + + //TODO:localize + var items: [ContextMenuItem] = [] + items.append(.custom(SearchContextItem( + context: component.context, + placeholder: "Search", + value: "", + valueChanged: { value in + searchQueryPromise.set(value) + } + ), false)) + items.append(.separator) + items.append(.custom(GiftAttributeListContextItem( + context: component.context, + attributes: backdropAttributes, + selectedAttributes: selectedBackdropAttributes, + attributeCount: self.state?.starGiftsState?.attributeCount ?? [:], + searchQuery: searchQueryPromise.get(), + attributeSelected: { [weak self] attribute, exclusive in + guard let self else { + return + } + var updatedFilterAttributes: [ResaleGiftsContext.Attribute] + if exclusive { + updatedFilterAttributes = currentFilterAttributes.filter { attribute in + if case .backdrop = attribute { + return false + } + return true + } + updatedFilterAttributes.append(attribute) + } else { + updatedFilterAttributes = currentFilterAttributes + if selectedBackdropAttributes.contains(attribute) { + updatedFilterAttributes.removeAll(where: { $0 == attribute }) + } else { + updatedFilterAttributes.append(attribute) + } + } + self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) + }, + selectAll: { [weak self] in + guard let self else { + return + } + let updatedFilterAttributes = currentFilterAttributes.filter { attribute in + if case .backdrop = attribute { + return false + } + return true + } + self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) + } + ), false)) + + let contextController = ContextController( + context: component.context, + presentationData: presentationData, + source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) + controller.presentInGlobalOverlay(contextController) + } + + func openSymbolContextMenu(sourceView: UIView) { + guard let component = self.component, let controller = self.environment?.controller() else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let searchQueryPromise = ValuePromise("") + + let attributes = self.state?.starGiftsState?.attributes ?? [] + let patternAttributes = attributes.filter { attribute in + if case .pattern = attribute { + return true + } else { + return false + } + } + + let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? [] + let selectedPatternAttributes = currentFilterAttributes.filter { attribute in + if case .pattern = attribute { + return true + } else { + return false + } + } + + //TODO:localize + var items: [ContextMenuItem] = [] + items.append(.custom(SearchContextItem( + context: component.context, + placeholder: "Search", + value: "", + valueChanged: { value in + searchQueryPromise.set(value) + } + ), false)) + items.append(.separator) + items.append(.custom(GiftAttributeListContextItem( + context: component.context, + attributes: patternAttributes, + selectedAttributes: selectedPatternAttributes, + attributeCount: self.state?.starGiftsState?.attributeCount ?? [:], + searchQuery: searchQueryPromise.get(), + attributeSelected: { [weak self] attribute, exclusive in + guard let self else { + return + } + var updatedFilterAttributes: [ResaleGiftsContext.Attribute] + if exclusive { + updatedFilterAttributes = currentFilterAttributes.filter { attribute in + if case .pattern = attribute { + return false + } + return true + } + updatedFilterAttributes.append(attribute) + } else { + updatedFilterAttributes = currentFilterAttributes + if selectedPatternAttributes.contains(attribute) { + updatedFilterAttributes.removeAll(where: { $0 == attribute }) + } else { + updatedFilterAttributes.append(attribute) + } + } + self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) + }, + selectAll: { [weak self] in + guard let self else { + return + } + let updatedFilterAttributes = currentFilterAttributes.filter { attribute in + if case .pattern = attribute { + return false + } + return true + } + self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) + } + ), false)) + + let contextController = ContextController( + context: component.context, + presentationData: presentationData, + source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) + controller.presentInGlobalOverlay(contextController) + } + + func update(component: GiftStoreScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + self.state = state + + if self.component == nil { + self.starsStateDisposable = (component.starsContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + self.starsState = state + if !self.isUpdating { + self.state?.updated() + } + }) + } + self.component = component + + let theme = environment.theme + let strings = environment.strings + + if themeUpdated { + self.backgroundColor = environment.theme.list.blocksBackgroundColor + } + + let bottomContentInset: CGFloat = 24.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let headerSideInset: CGFloat = 24.0 + environment.safeInsets.left + + var contentHeight: CGFloat = 0.0 + contentHeight += environment.navigationHeight + + let topPanelHeight = environment.navigationHeight + 39.0 + + let topPanelSize = self.topPanel.update( + transition: transition, + component: AnyComponent(BlurredBackgroundComponent( + color: theme.rootController.navigationBar.blurredBackgroundColor + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: topPanelHeight) + ) + + 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 { + topPanelView.alpha = 0.0 + topSeparatorView.alpha = 0.0 + + 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, +// component: AnyComponent( +// PlainButtonComponent( +// content: AnyComponent( +// MultilineTextComponent( +// text: .plain(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)), +// horizontalAlignment: .center +// ) +// ), +// effectAlignment: .center, +// action: { +// controller()?.dismiss() +// }, +// animateScale: false +// ) +// ), +// environment: {}, +// containerSize: CGSize(width: availableSize.width, height: 100.0) +// ) +// let cancelButtonFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 16.0, y: environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0 - cancelButtonSize.height / 2.0), size: cancelButtonSize) +// if let cancelButtonView = self.cancelButton.view { +// if cancelButtonView.superview == nil { +// self.addSubview(cancelButtonView) +// } +// transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame) +// } + + let balanceTitleSize = self.balanceTitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Stars_Purchase_Balance, + font: Font.regular(14.0), + textColor: environment.theme.actionSheet.primaryTextColor + )), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: availableSize + ) + + let formattedBalance = formatStarsAmountText(self.starsState?.balance ?? StarsAmount.zero, dateTimeFormat: environment.dateTimeFormat) + let smallLabelFont = Font.regular(11.0) + let labelFont = Font.semibold(14.0) + let balanceText = tonAmountAttributedString(formattedBalance, integralFont: labelFont, fractionalFont: smallLabelFont, color: environment.theme.actionSheet.primaryTextColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator) + + let balanceValueSize = self.balanceValue.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(balanceText), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: availableSize + ) + let balanceIconSize = self.balanceIcon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent(name: "Premium/Stars/StarSmall", tintColor: nil)), + environment: {}, + containerSize: availableSize + ) + + if let balanceTitleView = self.balanceTitle.view, let balanceValueView = self.balanceValue.view, let balanceIconView = self.balanceIcon.view { + if balanceTitleView.superview == nil { + self.addSubview(balanceTitleView) + self.addSubview(balanceValueView) + self.addSubview(balanceIconView) + } + let navigationHeight = environment.navigationHeight - environment.statusBarHeight + let topBalanceOriginY = environment.statusBarHeight + (navigationHeight - balanceTitleSize.height - balanceValueSize.height) / 2.0 + balanceTitleView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceTitleSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height / 2.0) + balanceTitleView.bounds = CGRect(origin: .zero, size: balanceTitleSize) + balanceValueView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceValueSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0) + balanceValueView.bounds = CGRect(origin: .zero, size: balanceValueSize) + balanceIconView.center = CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - balanceValueSize.width - balanceIconSize.width / 2.0 - 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0 - UIScreenPixel) + balanceIconView.bounds = CGRect(origin: .zero, size: balanceIconSize) + } + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.gift.title ?? "Gift", font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - headerSideInset * 2.0, height: 100.0) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: 10.0), size: titleSize)) + } + + let effectiveCount: Int32 + if let count = self.effectiveGifts?.count { + effectiveCount = Int32(count) + } else if let resale = component.gift.availability?.resale { + effectiveCount = Int32(resale) + } else { + effectiveCount = 0 + } + + let subtitleSize = self.subtitle.update( + transition: transition, + component: AnyComponent(BalancedTextComponent( + text: .plain(NSAttributedString(string: "\(effectiveCount) for resale", font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - headerSideInset * 2.0, height: 100.0) + ) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) / 2.0), y: 31.0), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.addSubview(subtitleView) + } + transition.setFrame(view: subtitleView, frame: subtitleFrame) + } + + let optionSpacing: CGFloat = 10.0 + let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0 + + var sortingTitle = "Date" + var sortingIcon: String = "Peer Info/SortDate" + if let sorting = self.state?.starGiftsState?.sorting { + switch sorting { + case .date: + sortingTitle = "Date" + sortingIcon = "Peer Info/SortDate" + case .value: + sortingTitle = "Price" + sortingIcon = "Peer Info/SortValue" + case .number: + sortingTitle = "Number" + sortingIcon = "Peer Info/SortNumber" + } + } + + var filterItems: [FilterSelectorComponent.Item] = [] + filterItems.append(FilterSelectorComponent.Item( + id: AnyHashable(0), + iconName: sortingIcon, + title: sortingTitle, + action: { [weak self] view in + if let self { + self.openSortContextMenu(sourceView: view) + } + } + )) + + var modelTitle = "Model" + var backdropTitle = "Backdrop" + var symbolTitle = "Symbol" + if let filterAttributes = self.state?.starGiftsState?.filterAttributes { + var modelCount = 0 + var backdropCount = 0 + var symbolCount = 0 + + for attribute in filterAttributes { + switch attribute { + case .model: + modelCount += 1 + case .pattern: + symbolCount += 1 + case .backdrop: + backdropCount += 1 + } + } + + if modelCount > 0 { + if modelCount > 1 { + modelTitle = "\(modelCount) Models" + } else { + modelTitle = "1 Model" + } + } + if backdropCount > 0 { + if backdropCount > 1 { + backdropTitle = "\(backdropCount) Backdrops" + } else { + backdropTitle = "1 Backdrop" + } + } + if symbolCount > 0 { + if symbolCount > 1 { + symbolTitle = "\(symbolCount) Symbols" + } else { + symbolTitle = "1 Symbol" + } + } + } + + filterItems.append(FilterSelectorComponent.Item( + id: AnyHashable(1), + title: modelTitle, + action: { [weak self] view in + if let self { + self.openModelContextMenu(sourceView: view) + } + } + )) + filterItems.append(FilterSelectorComponent.Item( + id: AnyHashable(2), + title: backdropTitle, + action: { [weak self] view in + if let self { + self.openBackdropContextMenu(sourceView: view) + } + } + )) + filterItems.append(FilterSelectorComponent.Item( + id: AnyHashable(3), + title: symbolTitle, + action: { [weak self] view in + if let self { + self.openSymbolContextMenu(sourceView: view) + } + } + )) + + let filterSize = self.filterSelector.update( + transition: transition, + component: AnyComponent(FilterSelectorComponent( + context: component.context, + colors: FilterSelectorComponent.Colors( + foreground: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.65), + background: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15) + ), + items: filterItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0) + ) + if let filterSelectorView = self.filterSelector.view { + if filterSelectorView.superview == nil { + self.addSubview(filterSelectorView) + } + transition.setFrame(view: filterSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - filterSize.width) / 2.0), y: 56.0), size: filterSize)) + } + + if let starGifts = self.state?.starGiftsState?.gifts { + let starsOptionSize = CGSize(width: optionWidth, height: 154.0) + let optionSpacing: CGFloat = 10.0 + contentHeight += ceil(CGFloat(starGifts.count) / 3.0) * (starsOptionSize.height + optionSpacing) + contentHeight += -optionSpacing + 66.0 + } + + contentHeight += bottomContentInset + contentHeight += environment.safeInsets.bottom + + let previousBounds = self.scrollView.bounds + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { + self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) + } + if self.scrollView.contentSize != contentSize { + if contentSize.height < self.scrollView.contentSize.height, !transition.animation.isImmediate { + self.nextScrollTransition = transition + } + self.scrollView.contentSize = contentSize + self.nextScrollTransition = nil + } + let scrollInsets = UIEdgeInsets(top: topPanelHeight, left: 0.0, bottom: 0.0, right: 0.0) + if self.scrollView.scrollIndicatorInsets != scrollInsets { + self.scrollView.scrollIndicatorInsets = scrollInsets + } + + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + + self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0)) + + self.updateScrolling(transition: transition) + + var isLoading = false + if self.state?.starGiftsState?.gifts == nil || self.state?.starGiftsState?.dataState == .loading { + isLoading = true + } + + let loadingTransition: ComponentTransition = .easeInOut(duration: 0.25) + if isLoading { + self.loadingNode.update(size: availableSize, theme: environment.theme, transition: .immediate) + loadingTransition.setAlpha(view: self.loadingNode.view, alpha: 1.0) + } else { + loadingTransition.setAlpha(view: self.loadingNode.view, alpha: 0.0) + } + transition.setFrame(view: self.loadingNode.view, frame: CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight + 39.0 + 7.0), size: availableSize)) + + let fadeTransition = ComponentTransition.easeInOut(duration: 0.25) + if let effectiveGifts = self.effectiveGifts, effectiveGifts.isEmpty && self.state?.starGiftsState?.dataState != .loading { + let sideInset: CGFloat = 44.0 + let emptyAnimationHeight = 148.0 + let topInset: CGFloat = environment.navigationHeight + 39.0 + let bottomInset: CGFloat = environment.safeInsets.bottom + let visibleHeight = availableSize.height + let emptyAnimationSpacing: CGFloat = 20.0 + let emptyTextSpacing: CGFloat = 18.0 + + let emptyResultsTitleSize = self.emptyResultsTitle.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: "No Matching Gifts", font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center + ) + ), + environment: {}, + containerSize: availableSize + ) + let emptyResultsActionSize = self.emptyResultsAction.update( + transition: .immediate, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: "Clear Filters", font: Font.regular(17.0), textColor: theme.list.itemAccentColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + ) + ), + effectAlignment: .center, + action: { [weak self] in + guard let self else { + return + } + self.state?.starGiftsContext.updateFilterAttributes([]) + }, + animateScale: false + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: visibleHeight) + ) + let emptyResultsAnimationSize = self.emptyResultsAnimation.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "ChatListNoResults") + )), + environment: {}, + containerSize: CGSize(width: emptyAnimationHeight, height: emptyAnimationHeight) + ) + + let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyResultsTitleSize.height + emptyResultsActionSize.height + emptyTextSpacing + let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0) + + let emptyResultsAnimationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsAnimationSize.width) / 2.0), y: emptyAnimationY), size: emptyResultsAnimationSize) + + let emptyResultsTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsTitleSize.width) / 2.0), y: emptyResultsAnimationFrame.maxY + emptyAnimationSpacing), size: emptyResultsTitleSize) + + let emptyResultsActionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsActionSize.width) / 2.0), y: emptyResultsTitleFrame.maxY + emptyTextSpacing), size: emptyResultsActionSize) + + if let view = self.emptyResultsAnimation.view as? LottieComponent.View { + if view.superview == nil { + view.alpha = 0.0 + fadeTransition.setAlpha(view: view, alpha: 1.0) + self.insertSubview(view, belowSubview: self.loadingNode.view) + view.playOnce() + } + view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size) + ComponentTransition.immediate.setPosition(view: view, position: emptyResultsAnimationFrame.center) + } + if let view = self.emptyResultsTitle.view { + if view.superview == nil { + view.alpha = 0.0 + fadeTransition.setAlpha(view: view, alpha: 1.0) + self.insertSubview(view, belowSubview: self.loadingNode.view) + } + view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size) + ComponentTransition.immediate.setPosition(view: view, position: emptyResultsTitleFrame.center) + } + if let view = self.emptyResultsAction.view { + if view.superview == nil { + view.alpha = 0.0 + fadeTransition.setAlpha(view: view, alpha: 1.0) + self.insertSubview(view, belowSubview: self.loadingNode.view) + } + view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size) + ComponentTransition.immediate.setPosition(view: view, position: emptyResultsActionFrame.center) + + view.alpha = self.state?.starGiftsState?.attributes.isEmpty == true ? 0.0 : 1.0 + } + } else { + if let view = self.emptyResultsAnimation.view { + fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in + view.removeFromSuperview() + }) + } + if let view = self.emptyResultsTitle.view { + fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in + view.removeFromSuperview() + }) + } + if let view = self.emptyResultsAction.view { + fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in + view.removeFromSuperview() + }) + } + } + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + final class State: ComponentState { + private let context: AccountContext + var peerId: EnginePeer.Id + private var disposable: Disposable? + + fileprivate let starGiftsContext: ResaleGiftsContext + fileprivate var starGiftsState: ResaleGiftsContext.State? + + init( + context: AccountContext, + peerId: EnginePeer.Id, + giftId: Int64 + ) { + self.context = context + self.peerId = peerId + self.starGiftsContext = ResaleGiftsContext(account: context.account, giftId: giftId) + + super.init() + + self.disposable = (self.starGiftsContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + self.starGiftsState = state + self.updated() + }) + } + + deinit { + self.disposable?.dispose() + } + } + + func makeState() -> State { + return State(context: self.context, peerId: self.peerId, giftId: self.gift.id) + } + + func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class GiftStoreScreen: ViewControllerComponentContainer { + private let context: AccountContext + + public var parentController: () -> ViewController? = { + return nil + } + + public init( + context: AccountContext, + starsContext: StarsContext, + peerId: EnginePeer.Id, + gift: StarGift.Gift + ) { + self.context = context + + super.init(context: context, component: GiftStoreScreenComponent( + context: context, + starsContext: starsContext, + peerId: peerId, + gift: gift + ), navigationBarAppearance: .transparent, theme: .default, updatedPresentationData: nil) + + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Common_Back, style: .plain, target: nil, action: nil) + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? GiftStoreScreenComponent.View else { + return + } + componentView.scrollToTop() + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } +} + +private extension StarGift { + var id: String { + switch self { + case let .generic(gift): + return "\(gift.id)" + case let .unique(gift): + return gift.slug + } + } +} + +private final class GiftStoreReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceView: UIView + + let forceDisplayBelowKeyboard = true + + init(controller: ViewController, sourceView: UIView) { + self.controller = controller + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/LoadingShimmerComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/LoadingShimmerComponent.swift new file mode 100644 index 0000000000..b9f6c04b2f --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/LoadingShimmerComponent.swift @@ -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)) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/SearchContextItem.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/SearchContextItem.swift new file mode 100644 index 0000000000..585a863f69 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/SearchContextItem.swift @@ -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() + private let inputField = ComponentView() + private let inputFieldExternalState = TextFieldComponent.ExternalState() + private let inputPlaceholderView = ComponentView() + private let inputClear = ComponentView() + 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 + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift index de4bce26f3..12336824b9 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift @@ -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() + + 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, 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, 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 diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift index a5a063827f..ccfb5f2d93 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift @@ -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 ) ), diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift index 0070e7f7b3..e02517851e 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift @@ -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 ) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 8119ee549e..3125cacc51 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -53,10 +53,13 @@ private final class GiftViewSheetContent: CombinedComponent { let convertToStars: () -> Void let openStarsIntro: () -> Void let sendGift: (EnginePeer.Id) -> Void + let changeRecipient: () -> Void let openMyGifts: () -> Void let transferGift: () -> Void let upgradeGift: ((Int64?, Bool) -> Signal) + let buyGift: ((String, EnginePeer.Id) -> Signal) let shareGift: () -> Void + let resellGift: (Bool) -> Void let showAttributeInfo: (Any, String) -> Void let viewUpgraded: (EngineMessage.Id) -> Void let openMore: (ASDisplayNode, ContextGesture?) -> Void @@ -73,10 +76,13 @@ private final class GiftViewSheetContent: CombinedComponent { convertToStars: @escaping () -> Void, openStarsIntro: @escaping () -> Void, sendGift: @escaping (EnginePeer.Id) -> Void, + changeRecipient: @escaping () -> Void, openMyGifts: @escaping () -> Void, transferGift: @escaping () -> Void, upgradeGift: @escaping ((Int64?, Bool) -> Signal), + buyGift: @escaping ((String, EnginePeer.Id) -> Signal), shareGift: @escaping () -> Void, + resellGift: @escaping (Bool) -> Void, showAttributeInfo: @escaping (Any, String) -> Void, viewUpgraded: @escaping (EngineMessage.Id) -> Void, openMore: @escaping (ASDisplayNode, ContextGesture?) -> Void, @@ -92,10 +98,13 @@ private final class GiftViewSheetContent: CombinedComponent { self.convertToStars = convertToStars self.openStarsIntro = openStarsIntro self.sendGift = sendGift + self.changeRecipient = changeRecipient self.openMyGifts = openMyGifts self.transferGift = transferGift self.upgradeGift = upgradeGift + self.buyGift = buyGift self.shareGift = shareGift + self.resellGift = resellGift self.showAttributeInfo = showAttributeInfo self.viewUpgraded = viewUpgraded self.openMore = openMore @@ -115,17 +124,28 @@ private final class GiftViewSheetContent: CombinedComponent { final class State: ComponentState { private let context: AccountContext private(set) var subject: GiftViewScreen.Subject + private let upgradeGift: ((Int64?, Bool) -> Signal) + private let buyGift: ((String, EnginePeer.Id) -> Signal) + private let getController: () -> ViewController? private var disposable: Disposable? var initialized = false + var recipientPeerIdPromise = ValuePromise(nil) + var recipientPeerId: EnginePeer.Id? { + didSet { + self.recipientPeerIdPromise.set(self.recipientPeerId) + } + } + var peerMap: [EnginePeer.Id: EnginePeer] = [:] var starGiftsMap: [Int64: StarGift.Gift] = [:] var cachedCircleImage: UIImage? var cachedStarImage: (UIImage, PresentationTheme)? + var cachedSmallStarImage: (UIImage, PresentationTheme)? var cachedChevronImage: (UIImage, PresentationTheme)? var cachedSmallChevronImage: (UIImage, PresentationTheme)? @@ -138,6 +158,10 @@ private final class GiftViewSheetContent: CombinedComponent { var upgradeDisposable: Disposable? let levelsDisposable = MetaDisposable() + var buyForm: BotPaymentForm? + var buyFormDisposable: Disposable? + var buyDisposable: Disposable? + var inWearPreview = false var pendingWear = false var pendingTakeOff = false @@ -153,11 +177,13 @@ private final class GiftViewSheetContent: CombinedComponent { context: AccountContext, subject: GiftViewScreen.Subject, upgradeGift: @escaping ((Int64?, Bool) -> Signal), + buyGift: @escaping ((String, EnginePeer.Id) -> Signal), getController: @escaping () -> ViewController? ) { self.context = context self.subject = subject self.upgradeGift = upgradeGift + self.buyGift = buyGift self.getController = getController super.init() @@ -179,6 +205,7 @@ private final class GiftViewSheetContent: CombinedComponent { peerIds.append(contentsOf: media.peerIds) } } + if case let .unique(gift) = arguments.gift { if case let .peerId(peerId) = gift.owner { peerIds.append(peerId) @@ -192,6 +219,17 @@ private final class GiftViewSheetContent: CombinedComponent { break } } + + if let _ = arguments.resellStars { + self.buyFormDisposable = (context.engine.payments.fetchBotPaymentForm(source: .starGiftResale(slug: gift.slug, toPeerId: context.account.peerId), themeParams: nil) + |> deliverOnMainQueue).start(next: { [weak self] paymentForm in + guard let self else { + return + } + self.buyForm = paymentForm + self.updated() + }) + } } else if case let .generic(gift) = arguments.gift { if arguments.canUpgrade || arguments.upgradeStars != nil { self.sampleDisposable.add((context.engine.payments.starGiftUpgradePreview(giftId: gift.id) @@ -228,18 +266,38 @@ private final class GiftViewSheetContent: CombinedComponent { } } - self.disposable = combineLatest(queue: Queue.mainQueue(), - context.engine.data.get(EngineDataMap( - peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in - return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + let peerIdsSignal: Signal<[EnginePeer.Id], NoError> + if case let .uniqueGift(_, recipientPeerIdValue) = subject, let recipientPeerIdValue { + self.recipientPeerId = recipientPeerIdValue + self.recipientPeerIdPromise.set(recipientPeerIdValue) + peerIdsSignal = self.recipientPeerIdPromise.get() + |> map { recipientPeerId in + var peerIds = peerIds + if let recipientPeerId { + peerIds.append(recipientPeerId) } - )), + return peerIds + } + } else { + peerIdsSignal = .single(peerIds) + } + + self.disposable = combineLatest(queue: Queue.mainQueue(), + peerIdsSignal + |> distinctUntilChanged + |> mapToSignal { peerIds in + return context.engine.data.get(EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in + return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + } + )) + }, .single(nil) |> then(context.engine.payments.cachedStarGifts()) ).startStrict(next: { [weak self] peers, starGifts in if let strongSelf = self { var peersMap: [EnginePeer.Id: EnginePeer] = [:] - for peerId in peerIds { - if let maybePeer = peers[peerId], let peer = maybePeer { + for (peerId, maybePeer) in peers { + if let peer = maybePeer { peersMap[peerId] = peer } } @@ -262,10 +320,22 @@ private final class GiftViewSheetContent: CombinedComponent { }) } - if let starsContext = context.starsContext, let state = starsContext.currentState, state.balance < StarsAmount(value: 100, nanos: 0) { + var minRequiredAmount = StarsAmount(value: 100, nanos: 0) + if let resellStars = self.subject.arguments?.resellStars { + minRequiredAmount = StarsAmount(value: resellStars, nanos: 0) + } + + if let starsContext = context.starsContext, let state = starsContext.currentState, state.balance < minRequiredAmount { self.optionsPromise.set(context.engine.payments.starsTopUpOptions() |> map(Optional.init)) } + + if let controller = getController() as? GiftViewScreen { + controller.updateSubject.connect { [weak self] subject in + self?.subject = subject + self?.updated(transition: .easeInOut(duration: 0.25)) + } + } } deinit { @@ -273,6 +343,8 @@ private final class GiftViewSheetContent: CombinedComponent { self.sampleDisposable.dispose() self.upgradeFormDisposable?.dispose() self.upgradeDisposable?.dispose() + self.buyFormDisposable?.dispose() + self.buyDisposable?.dispose() self.levelsDisposable.dispose() } @@ -331,6 +403,201 @@ private final class GiftViewSheetContent: CombinedComponent { } } + func changeRecipient() { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let mode = ContactSelectionControllerMode.starsGifting(birthdays: nil, hasActions: false, showSelf: true, selfSubtitle: presentationData.strings.Premium_Gift_ContactSelection_BuySelf) + + //TODO:localize + let controller = self.context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams( + context: context, + mode: mode, + autoDismiss: true, + title: { _ in return "Change Recipient" }, + options: .single([]), + allowChannelsInSearch: false + )) + controller.navigationPresentation = .modal + let _ = (controller.result + |> deliverOnMainQueue).start(next: { [weak self] result in + if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer { + self?.recipientPeerId = peer.id + } + }) + + self.getController()?.push(controller) + } + + func commitBuy() { + guard let resellStars = self.subject.arguments?.resellStars, let starsContext = self.context.starsContext, let starsState = starsContext.currentState, case let .unique(uniqueGift) = self.subject.arguments?.gift else { + return + } + + let giftTitle = "\(uniqueGift.title) #\(uniqueGift.number)" + + let context = self.context + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let recipientPeerId = self.recipientPeerId ?? self.context.account.peerId + + let action = { + let proceed: (Int64) -> Void = { formId in + self.inProgress = true + self.updated() + + self.buyDisposable = (self.buyGift(uniqueGift.slug, recipientPeerId) + |> deliverOnMainQueue).start(completed: { [weak self, weak starsContext] in + guard let self, let controller = self.getController() as? GiftViewScreen else { + return + } + self.inProgress = false + + var animationFile: TelegramMediaFile? + for attribute in uniqueGift.attributes { + if case let .model(_, file, _) = attribute { + animationFile = file + break + } + } + + if let navigationController = controller.navigationController as? NavigationController { + if recipientPeerId == self.context.account.peerId { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak navigationController] peer in + guard let peer, let navigationController else { + return + } + + var controllers = Array(navigationController.viewControllers.prefix(1)) + if let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .myProfileGifts, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + controllers.append(controller) + } + navigationController.setViewControllers(controllers, animated: true) + navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds)) + + Queue.mainQueue().after(0.5, { + if let lastController = navigationController.viewControllers.last as? ViewController, let animationFile { + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .sticker(context: context, file: animationFile, loop: false, title: "Gift Acquired", text: "\(giftTitle) is now yours.", undoText: nil, customAction: nil), + elevatedLayout: lastController is ChatController, + action: { _ in + return true + } + ) + lastController.present(resultController, in: .window(.root)) + } + }) + }) + } else { + var controllers = Array(navigationController.viewControllers.prefix(1)) + let chatController = self.context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: recipientPeerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + chatController.hintPlayNextOutgoingGift() + controllers.append(chatController) + navigationController.setViewControllers(controllers, animated: true) + + Queue.mainQueue().after(0.5, { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: recipientPeerId)) + |> deliverOnMainQueue).start(next: { [weak navigationController] peer in + if let peer, let lastController = navigationController?.viewControllers.last as? ViewController, let animationFile { + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .sticker(context: context, file: animationFile, loop: false, title: "Gift Sent", text: "\(peer.compactDisplayTitle) has been notified about your gift.", undoText: nil, customAction: nil), + elevatedLayout: lastController is ChatController, + action: { _ in + return true + } + ) + lastController.present(resultController, in: .window(.root)) + } + }) + }) + } + } + + controller.animateSuccess() + self.updated(transition: .spring(duration: 0.4)) + + Queue.mainQueue().after(0.5) { + starsContext?.load(force: true) + } + }) + } + + if let buyForm = self.buyForm, let price = buyForm.invoice.prices.first?.amount { + if starsState.balance < StarsAmount(value: price, nanos: 0) { + let _ = (self.optionsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] options in + guard let self, let controller = self.getController() else { + return + } + let purchaseController = self.context.sharedContext.makeStarsPurchaseScreen( + context: self.context, + starsContext: starsContext, + options: options ?? [], + purpose: .upgradeStarGift(requiredStars: price), + completion: { [weak self, weak starsContext] stars in + guard let self, let starsContext else { + return + } + self.inProgress = true + self.updated() + + starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) + let _ = (starsContext.onUpdate + |> deliverOnMainQueue).start(next: { + proceed(buyForm.id) + }) + } + ) + controller.push(purchaseController) + }) + } else { + proceed(buyForm.id) + } + } + } + + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: recipientPeerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + let text: String + if recipientPeerId == self.context.account.peerId { + text = "Do you really want to buy **\(giftTitle)** for **\(resellStars)** Stars?" + } else { + text = "Do you really want to buy **\(giftTitle)** for **\(resellStars)** Stars and gift it to **\(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))**?" + } + + let alertController = textAlertController( + context: self.context, + title: "Confirm Payment", + text: text, + actions: [ + TextAlertAction(type: .defaultAction, title: "Buy for \(resellStars) Stars", action: { + action() + }), + TextAlertAction(type: .genericAction, title: "Cancel", action: { + }) + ], + actionLayout: .vertical, + parseMarkdown: true + ) + if let controller = self.getController() as? GiftViewScreen { + controller.present(alertController, in: .window(.root)) + } + }) + } + func commitUpgrade() { guard let arguments = self.subject.arguments, let peerId = arguments.peerId, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else { return @@ -352,8 +619,7 @@ private final class GiftViewSheetContent: CombinedComponent { self.inProgress = false self.inUpgradePreview = false - self.subject = .profileGift(peerId, result) - controller.subject = self.subject + controller.subject = .profileGift(peerId, result) controller.animateSuccess() self.updated(transition: .spring(duration: 0.4)) @@ -407,10 +673,12 @@ private final class GiftViewSheetContent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, subject: self.subject, upgradeGift: self.upgradeGift, getController: self.getController) + return State(context: self.context, subject: self.subject, upgradeGift: self.upgradeGift, buyGift: self.buyGift, getController: self.getController) } static var body: Body { + let priceButton = Child(PlainButtonComponent.self) + let buttons = Child(ButtonsComponent.self) let animation = Child(GiftCompositionComponent.self) let title = Child(MultilineTextComponent.self) @@ -418,7 +686,7 @@ private final class GiftViewSheetContent: CombinedComponent { let transferButton = Child(PlainButtonComponent.self) let wearButton = Child(PlainButtonComponent.self) - let shareButton = Child(PlainButtonComponent.self) + let resellButton = Child(PlainButtonComponent.self) let wearAvatar = Child(AvatarComponent.self) let wearPeerName = Child(MultilineTextComponent.self) @@ -478,6 +746,7 @@ private final class GiftViewSheetContent: CombinedComponent { var uniqueGift: StarGift.UniqueGift? var isSelfGift = false var isChannelGift = false + var isMyUniqueGift = false if case let .soldOutGift(gift) = subject { animationFile = gift.file @@ -525,6 +794,10 @@ private final class GiftViewSheetContent: CombinedComponent { isSelfGift = arguments.messageId?.peerId == component.context.account.peerId + if case let .peerId(peerId) = uniqueGift?.owner, peerId == component.context.account.peerId || isChannelGift { + isMyUniqueGift = true + } + if isSelfGift { titleString = strings.Gift_View_Self_Title } else { @@ -1095,6 +1368,9 @@ private final class GiftViewSheetContent: CombinedComponent { if !descriptionText.isEmpty { let linkColor = theme.actionSheet.controlAccentColor + if state.cachedSmallStarImage == nil || state.cachedSmallStarImage?.1 !== environment.theme { + state.cachedSmallStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Premium/Stars/ButtonStar"), color: .white)!, theme) + } if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) } @@ -1114,6 +1390,11 @@ private final class GiftViewSheetContent: CombinedComponent { descriptionText = descriptionText.replacingOccurrences(of: " >]", with: "\u{00A0}>]") let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + if let range = attributedString.string.range(of: "*"), let starImage = state.cachedSmallStarImage?.0 { + attributedString.addAttribute(.font, value: Font.regular(13.0), range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedString.string)) + } if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) } @@ -1207,142 +1488,180 @@ private final class GiftViewSheetContent: CombinedComponent { if !soldOut { if let uniqueGift { - switch uniqueGift.owner { - case let .peerId(peerId): - if let peer = state.peerMap[peerId] { - let ownerComponent: AnyComponent - if peer.id == component.context.account.peerId, peer.isPremium { - let animationContent: EmojiStatusComponent.Content - var color: UIColor? - var statusId: Int64 = 1 - if state.pendingWear { - var fileId: Int64? - for attribute in uniqueGift.attributes { - if case let .model(_, file, _) = attribute { - fileId = file.fileId.id - } - if case let .backdrop(_, innerColor, _, _, _, _) = attribute { - color = UIColor(rgb: UInt32(bitPattern: innerColor)) - } - } - if let fileId { - statusId = fileId - animationContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 18.0, height: 18.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: tableLinkColor, loopMode: .count(2)) - } else { - animationContent = .premium(color: tableLinkColor) - } - } else if let emojiStatus = peer.emojiStatus, !state.pendingTakeOff { - animationContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 18.0, height: 18.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: tableLinkColor, loopMode: .count(2)) - if case let .starGift(id, _, _, _, _, innerColor, _, _, _) = emojiStatus.content { - color = UIColor(rgb: UInt32(bitPattern: innerColor)) - if id == uniqueGift.id { - isWearing = true - state.pendingWear = false - } - } - } else { - animationContent = .premium(color: tableLinkColor) - state.pendingTakeOff = false - } - - ownerComponent = AnyComponent( - HStack([ - AnyComponentWithIdentity( - id: AnyHashable(0), - component: AnyComponent(Button( - content: AnyComponent( - PeerCellComponent( + if case let .uniqueGift(_, recipientPeerIdValue) = component.subject, let _ = recipientPeerIdValue, let recipientPeerId = state.recipientPeerId { + //TODO:localize + if let peer = state.peerMap[recipientPeerId] { + tableItems.append(.init( + id: "recipient", + title: "Recipient", + component: AnyComponent( + Button( + content: AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable(peer.id), + component: AnyComponent(PeerCellComponent( context: component.context, theme: theme, strings: strings, peer: peer - ) + )) ), - action: { - component.openPeer(peer) - Queue.mainQueue().after(0.6, { - component.cancel(false) - }) - } - )) + AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(ButtonContentComponent( + context: component.context, + text: "change", + color: theme.list.itemAccentColor + )) + ) + ], spacing: 4.0) ), - AnyComponentWithIdentity( - id: AnyHashable(statusId), - component: AnyComponent(EmojiStatusComponent( - context: component.context, - animationCache: component.context.animationCache, - animationRenderer: component.context.animationRenderer, - content: animationContent, - particleColor: color, - size: CGSize(width: 18.0, height: 18.0), - isVisibleForAnimations: true, - action: { - - }, - tag: statusTag - )) - ) - ], spacing: 2.0) + action: { [weak state] in + state?.changeRecipient() + } + ) ) - } else { - ownerComponent = AnyComponent(Button( - content: AnyComponent( - PeerCellComponent( - context: component.context, - theme: theme, - strings: strings, - peer: peer - ) - ), - action: { - component.openPeer(peer) - Queue.mainQueue().after(0.6, { - component.cancel(false) - }) - } - )) - } - tableItems.append(.init( - id: "owner", - title: strings.Gift_Unique_Owner, - component: ownerComponent )) } - case let .name(name): - tableItems.append(.init( - id: "name_owner", - title: strings.Gift_Unique_Owner, - component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: name, font: tableFont, textColor: tableTextColor))) - ) - )) - case let .address(address): - exported = true - - func formatAddress(_ str: String) -> String { - guard str.count == 48 && !str.hasSuffix(".ton") else { - return str - } - var result = str - let middleIndex = result.index(result.startIndex, offsetBy: str.count / 2) - result.insert("\n", at: middleIndex) - return result - } - - tableItems.append(.init( - id: "address_owner", - title: strings.Gift_Unique_Owner, - component: AnyComponent( - Button( - content: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: formatAddress(address), font: tableLargeMonospaceFont, textColor: tableLinkColor)), maximumNumberOfLines: 2, lineSpacing: 0.2) - ), - action: { - component.copyAddress(address) + } else { + switch uniqueGift.owner { + case let .peerId(peerId): + if let peer = state.peerMap[peerId] { + let ownerComponent: AnyComponent + if peer.id == component.context.account.peerId, peer.isPremium { + let animationContent: EmojiStatusComponent.Content + var color: UIColor? + var statusId: Int64 = 1 + if state.pendingWear { + var fileId: Int64? + for attribute in uniqueGift.attributes { + if case let .model(_, file, _) = attribute { + fileId = file.fileId.id + } + if case let .backdrop(_, _, innerColor, _, _, _, _) = attribute { + color = UIColor(rgb: UInt32(bitPattern: innerColor)) + } + } + if let fileId { + statusId = fileId + animationContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 18.0, height: 18.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: tableLinkColor, loopMode: .count(2)) + } else { + animationContent = .premium(color: tableLinkColor) + } + } else if let emojiStatus = peer.emojiStatus, !state.pendingTakeOff { + animationContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 18.0, height: 18.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: tableLinkColor, loopMode: .count(2)) + if case let .starGift(id, _, _, _, _, innerColor, _, _, _) = emojiStatus.content { + color = UIColor(rgb: UInt32(bitPattern: innerColor)) + if id == uniqueGift.id { + isWearing = true + state.pendingWear = false + } + } + } else { + animationContent = .premium(color: tableLinkColor) + state.pendingTakeOff = false } + + ownerComponent = AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + strings: strings, + peer: peer + ) + ), + action: { + component.openPeer(peer) + Queue.mainQueue().after(0.6, { + component.cancel(false) + }) + } + )) + ), + AnyComponentWithIdentity( + id: AnyHashable(statusId), + component: AnyComponent(EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: animationContent, + particleColor: color, + size: CGSize(width: 18.0, height: 18.0), + isVisibleForAnimations: true, + action: { + + }, + tag: statusTag + )) + ) + ], spacing: 2.0) + ) + } else { + ownerComponent = AnyComponent(Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + strings: strings, + peer: peer + ) + ), + action: { + component.openPeer(peer) + Queue.mainQueue().after(0.6, { + component.cancel(false) + }) + } + )) + } + tableItems.append(.init( + id: "owner", + title: strings.Gift_Unique_Owner, + component: ownerComponent + )) + } + case let .name(name): + tableItems.append(.init( + id: "name_owner", + title: strings.Gift_Unique_Owner, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: name, font: tableFont, textColor: tableTextColor))) ) - ) - )) + )) + case let .address(address): + exported = true + + func formatAddress(_ str: String) -> String { + guard str.count == 48 && !str.hasSuffix(".ton") else { + return str + } + var result = str + let middleIndex = result.index(result.startIndex, offsetBy: str.count / 2) + result.insert("\n", at: middleIndex) + return result + } + + tableItems.append(.init( + id: "address_owner", + title: strings.Gift_Unique_Owner, + component: AnyComponent( + Button( + content: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: formatAddress(address), font: tableLargeMonospaceFont, textColor: tableLinkColor)), maximumNumberOfLines: 2, lineSpacing: 0.2) + ), + action: { + component.copyAddress(address) + } + ) + ) + )) + } } } else if let peerId = subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] { var isBot = false @@ -1434,7 +1753,7 @@ private final class GiftViewSheetContent: CombinedComponent { } if let uniqueGift { - if case let .peerId(peerId) = uniqueGift.owner, peerId == component.context.account.peerId || isChannelGift { + if isMyUniqueGift, case let .peerId(peerId) = uniqueGift.owner { var canTransfer = true if let peer = state.peerMap[peerId], case let .channel(channel) = peer, !channel.flags.contains(.isCreator) { canTransfer = false @@ -1534,24 +1853,25 @@ private final class GiftViewSheetContent: CombinedComponent { ) buttonOriginX += buttonWidth + buttonSpacing - let shareButton = shareButton.update( + //TODO:localize + let resellButton = resellButton.update( component: PlainButtonComponent( content: AnyComponent( HeaderButtonComponent( - title: strings.Gift_View_Header_Share, - iconName: "Premium/Collectible/Share" + title: uniqueGift.resellStars == nil ? "sell" : "unlist", + iconName: uniqueGift.resellStars == nil ? "Premium/Collectible/Sell" : "Premium/Collectible/Unlist" ) ), effectAlignment: .center, action: { - component.shareGift() + component.resellGift(false) } ), environment: {}, availableSize: CGSize(width: buttonWidth, height: buttonHeight), transition: context.transition ) - context.add(shareButton + context.add(resellButton .position(CGPoint(x: buttonOriginX + buttonWidth / 2.0, y: headerHeight - buttonHeight / 2.0 - 16.0)) .appear(.default(scale: true, alpha: true)) .disappear(.default(scale: true, alpha: true)) @@ -1586,7 +1906,7 @@ private final class GiftViewSheetContent: CombinedComponent { value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor) percentage = Float(rarity) * 0.1 tag = modelButtonTag - case let .backdrop(name, _, _, _, _, rarity): + case let .backdrop(name, _, _, _, _, _, rarity): id = "backdrop" title = strings.Gift_Unique_Backdrop value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor) @@ -1906,15 +2226,59 @@ private final class GiftViewSheetContent: CombinedComponent { ) originY += table.size.height + 23.0 } + + var resellStars: Int64? + var selling = false + if let uniqueGift { + resellStars = uniqueGift.resellStars + + if incoming, let resellStars { + let priceButton = priceButton.update( + component: PlainButtonComponent( + content: AnyComponent( + PriceButtonComponent(price: resellStars) + ), + effectAlignment: .center, + action: { + component.resellGift(true) + }, + animateScale: false + ), + availableSize: CGSize(width: 120.0, height: 30.0), + transition: context.transition + ) + context.add(priceButton + .position(CGPoint(x: environment.safeInsets.left + 16.0 + priceButton.size.width / 2.0, y: 28.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + } + + if !incoming, let _ = resellStars { + if case let .uniqueGift(_, recipientPeerId) = component.subject, recipientPeerId != nil { + } else { + selling = true + } + } + } - if ((incoming && !converted && !upgraded) || exported) && (!showUpgradePreview && !showWearPreview) { + if ((incoming && !converted && !upgraded) || exported || selling) && (!showUpgradePreview && !showWearPreview) { let linkColor = theme.actionSheet.controlAccentColor if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme { state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme) } var addressToOpen: String? var descriptionText: String - if let uniqueGift, let address = uniqueGift.giftAddress, case .address = uniqueGift.owner { + if let uniqueGift, selling { + //TODO:localize + let ownerName: String + if case let .peerId(peerId) = uniqueGift.owner { + ownerName = state.peerMap[peerId]?.compactDisplayTitle ?? "" + } else { + ownerName = "" + } + descriptionText = "\(ownerName) is selling this gift and you can buy it." + } else if let uniqueGift, let address = uniqueGift.giftAddress, case .address = uniqueGift.owner { addressToOpen = address descriptionText = strings.Gift_View_TonGiftAddressInfo } else if savedToProfile { @@ -2114,14 +2478,15 @@ private final class GiftViewSheetContent: CombinedComponent { } var upgradeString = strings.Gift_Upgrade_Upgrade if let upgradeForm = state.upgradeForm, let price = upgradeForm.invoice.prices.first?.amount { - upgradeString += " # \(presentationStringsFormattedNumber(Int32(price), environment.dateTimeFormat.groupingSeparator))" + upgradeString += " # \(presentationStringsFormattedNumber(Int32(price), environment.dateTimeFormat.groupingSeparator))" } let buttonTitle = subject.arguments?.upgradeStars != nil ? strings.Gift_Upgrade_Confirm : upgradeString let buttonAttributedString = NSMutableAttributedString(string: buttonTitle, 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: environment.theme.list.itemCheckColors.foregroundColor, 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)) } buttonChild = button.update( component: ButtonComponent( @@ -2205,6 +2570,37 @@ private final class GiftViewSheetContent: CombinedComponent { availableSize: buttonSize, transition: context.transition ) + } else if !incoming, let resellStars, !isMyUniqueGift { + if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme { + state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme) + } + //TODO:localize + var upgradeString = "Buy for" + upgradeString += " # \(presentationStringsFormattedNumber(Int32(resellStars), environment.dateTimeFormat.groupingSeparator))" + + let buttonTitle = subject.arguments?.upgradeStars != nil ? strings.Gift_Upgrade_Confirm : upgradeString + let buttonAttributedString = NSMutableAttributedString(string: buttonTitle, 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: 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)) + } + buttonChild = button.update( + component: ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity( + id: AnyHashable("buy"), + component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) + ), + isEnabled: true, + displaysProgress: state.inProgress, + action: { [weak state] in + state?.commitBuy() + }), + availableSize: buttonSize, + transition: context.transition + ) } else { buttonChild = button.update( component: ButtonComponent( @@ -2252,10 +2648,13 @@ private final class GiftViewSheetComponent: CombinedComponent { let convertToStars: () -> Void let openStarsIntro: () -> Void let sendGift: (EnginePeer.Id) -> Void + let changeRecipient: () -> Void let openMyGifts: () -> Void let transferGift: () -> Void let upgradeGift: ((Int64?, Bool) -> Signal) + let buyGift: ((String, EnginePeer.Id) -> Signal) let shareGift: () -> Void + let resellGift: (Bool) -> Void let viewUpgraded: (EngineMessage.Id) -> Void let openMore: (ASDisplayNode, ContextGesture?) -> Void let showAttributeInfo: (Any, String) -> Void @@ -2270,10 +2669,13 @@ private final class GiftViewSheetComponent: CombinedComponent { convertToStars: @escaping () -> Void, openStarsIntro: @escaping () -> Void, sendGift: @escaping (EnginePeer.Id) -> Void, + changeRecipient: @escaping () -> Void, openMyGifts: @escaping () -> Void, transferGift: @escaping () -> Void, upgradeGift: @escaping ((Int64?, Bool) -> Signal), + buyGift: @escaping ((String, EnginePeer.Id) -> Signal), shareGift: @escaping () -> Void, + resellGift: @escaping (Bool) -> Void, viewUpgraded: @escaping (EngineMessage.Id) -> Void, openMore: @escaping (ASDisplayNode, ContextGesture?) -> Void, showAttributeInfo: @escaping (Any, String) -> Void @@ -2287,10 +2689,13 @@ private final class GiftViewSheetComponent: CombinedComponent { self.convertToStars = convertToStars self.openStarsIntro = openStarsIntro self.sendGift = sendGift + self.changeRecipient = changeRecipient self.openMyGifts = openMyGifts self.transferGift = transferGift self.upgradeGift = upgradeGift + self.buyGift = buyGift self.shareGift = shareGift + self.resellGift = resellGift self.viewUpgraded = viewUpgraded self.openMore = openMore self.showAttributeInfo = showAttributeInfo @@ -2324,10 +2729,7 @@ private final class GiftViewSheetComponent: CombinedComponent { cancel: { animate in if animate { if let controller = controller() as? GiftViewScreen { - controller.dismissAllTooltips() - animateOut.invoke(Action { [weak controller] _ in - controller?.dismiss(completion: nil) - }) + controller.dismissAnimated() } } else if let controller = controller() { controller.dismiss(animated: false, completion: nil) @@ -2340,10 +2742,13 @@ private final class GiftViewSheetComponent: CombinedComponent { convertToStars: context.component.convertToStars, openStarsIntro: context.component.openStarsIntro, sendGift: context.component.sendGift, + changeRecipient: context.component.changeRecipient, openMyGifts: context.component.openMyGifts, transferGift: context.component.transferGift, upgradeGift: context.component.upgradeGift, + buyGift: context.component.buyGift, shareGift: context.component.shareGift, + resellGift: context.component.resellGift, showAttributeInfo: context.component.showAttributeInfo, viewUpgraded: context.component.viewUpgraded, openMore: context.component.openMore, @@ -2371,6 +2776,7 @@ private final class GiftViewSheetComponent: CombinedComponent { if animated { if let controller = controller() as? GiftViewScreen { controller.dismissAllTooltips() + controller.dismissBalanceOverlay() animateOut.invoke(Action { _ in controller.dismiss(completion: nil) }) @@ -2378,6 +2784,7 @@ private final class GiftViewSheetComponent: CombinedComponent { } else { if let controller = controller() as? GiftViewScreen { controller.dismissAllTooltips() + controller.dismissBalanceOverlay() controller.dismiss(completion: nil) } } @@ -2416,26 +2823,26 @@ private final class GiftViewSheetComponent: CombinedComponent { public class GiftViewScreen: ViewControllerComponentContainer { public enum Subject: Equatable { case message(EngineMessage) - case uniqueGift(StarGift.UniqueGift) + case uniqueGift(StarGift.UniqueGift, EnginePeer.Id?) case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) case soldOutGift(StarGift.Gift) case upgradePreview([StarGift.UniqueGift.Attribute], String) case wearPreview(StarGift.UniqueGift) - var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, canExportDate: Int32?, upgradeMessageId: Int32?)? { + var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellStars: Int64?, canExportDate: Int32?, upgradeMessageId: Int32?)? { switch self { case let .message(message): if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction { switch action.action { - case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, _, upgradeMessageId, peerId, senderId, savedId): + case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, upgradeMessageId, peerId, senderId, savedId): var reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) } else { reference = .message(messageId: message.id) } - return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, canUpgrade, upgradeStars, nil, nil, upgradeMessageId) - case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, peerId, senderId, savedId): + return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId) + case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, peerId, senderId, savedId, _): var reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) @@ -2454,19 +2861,28 @@ public class GiftViewScreen: ViewControllerComponentContainer { } else { incoming = message.flags.contains(.Incoming) } - return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, nil, transferStars, canExportDate, nil) + + var resellStars: Int64? + if case let .unique(uniqueGift) = gift { + resellStars = uniqueGift.resellStars + } + return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellStars, canExportDate, nil) default: return nil } } - case let .uniqueGift(gift), let .wearPreview(gift): - return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, nil, nil, nil, nil) + case let .uniqueGift(gift, _), let .wearPreview(gift): + return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellStars, nil, nil) case let .profileGift(peerId, gift): var messageId: EngineMessage.Id? if case let .message(messageIdValue) = gift.reference { messageId = messageIdValue } - return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, gift.canExportDate, nil) + var resellStars: Int64? + if case let .unique(uniqueGift) = gift.gift { + resellStars = uniqueGift.resellStars + } + return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellStars, gift.canExportDate, nil) case .soldOutGift: return nil case .upgradePreview: @@ -2477,7 +2893,13 @@ public class GiftViewScreen: ViewControllerComponentContainer { } private let context: AccountContext - fileprivate var subject: GiftViewScreen.Subject + fileprivate var subject: GiftViewScreen.Subject { + didSet { + self.updateSubject.invoke(self.subject) + } + } + let updateSubject = ActionSlot() + public var disposed: () -> Void = {} fileprivate var showBalance = false { @@ -2497,6 +2919,8 @@ public class GiftViewScreen: ViewControllerComponentContainer { convertToStars: (() -> Void)? = nil, transferGift: ((Bool, EnginePeer.Id) -> Signal)? = nil, upgradeGift: ((Int64?, Bool) -> Signal)? = nil, + buyGift: ((String, EnginePeer.Id) -> Signal)? = nil, + updateResellStars: ((Int64?) -> Void)? = nil, togglePinnedToTop: ((Bool) -> Bool)? = nil, shareStory: ((StarGift.UniqueGift) -> Void)? = nil ) { @@ -2513,11 +2937,13 @@ public class GiftViewScreen: ViewControllerComponentContainer { var openMyGiftsImpl: (() -> Void)? var transferGiftImpl: (() -> Void)? var upgradeGiftImpl: ((Int64?, Bool) -> Signal)? + var buyGiftImpl: ((String, EnginePeer.Id) -> Signal)? var shareGiftImpl: (() -> Void)? + var resellGiftImpl: ((Bool) -> Void)? var openMoreImpl: ((ASDisplayNode, ContextGesture?) -> Void)? var showAttributeInfoImpl: ((Any, String) -> Void)? var viewUpgradedImpl: ((EngineMessage.Id) -> Void)? - + super.init( context: context, component: GiftViewSheetComponent( @@ -2543,6 +2969,9 @@ public class GiftViewScreen: ViewControllerComponentContainer { }, sendGift: { peerId in sendGiftImpl?(peerId) + }, + changeRecipient: { + }, openMyGifts: { openMyGiftsImpl?() @@ -2553,9 +2982,15 @@ public class GiftViewScreen: ViewControllerComponentContainer { upgradeGift: { formId, keepOriginalInfo in return upgradeGiftImpl?(formId, keepOriginalInfo) ?? .complete() }, + buyGift: { slug, peerId in + return buyGiftImpl?(slug, peerId) ?? .complete() + }, shareGift: { shareGiftImpl?() }, + resellGift: { update in + resellGiftImpl?(update) + }, viewUpgraded: { messageId in viewUpgradedImpl?(messageId) }, @@ -2854,6 +3289,11 @@ public class GiftViewScreen: ViewControllerComponentContainer { } if let upgradeGift { return upgradeGift(formId, keepOriginalInfo) + |> afterCompleted { + if formId != nil { + context.starsContext?.load(force: true) + } + } } else { return self.context.engine.payments.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo) |> afterCompleted { @@ -2864,6 +3304,23 @@ public class GiftViewScreen: ViewControllerComponentContainer { } } + buyGiftImpl = { [weak self] slug, peerId in + guard let self else { + return .complete() + } + if let buyGift { + return buyGift(slug, peerId) + |> afterCompleted { + context.starsContext?.load(force: true) + } + } else { + return self.context.engine.payments.buyStarGift(slug: slug, peerId: peerId) + |> afterCompleted { + context.starsContext?.load(force: true) + } + } + } + shareGiftImpl = { [weak self] in guard let self, let arguments = self.subject.arguments, case let .unique(gift) = arguments.gift else { return @@ -2939,6 +3396,108 @@ public class GiftViewScreen: ViewControllerComponentContainer { self.present(shareController, in: .window(.root)) } + resellGiftImpl = { [weak self] update in + guard let self, let arguments = self.subject.arguments, case let .profileGift(peerId, currentSubject) = self.subject, case let .unique(gift) = arguments.gift else { + return + } + + self.dismissAllTooltips() + + //TODO:localize + if let resellStars = gift.resellStars, resellStars > 0, !update { + let alertController = textAlertController( + context: context, + title: "Unlist This Item?", + text: "It will no longer be for sale.", + actions: [ + TextAlertAction(type: .defaultAction, title: "Unlist", action: { [weak self] in + guard let self else { + return + } + + self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(nil)))) + + let giftTitle = "\(gift.title) #\(gift.number)" + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let text = "\(giftTitle) is removed from sale." + let tooltipController = UndoOverlayController( + presentationData: presentationData, + content: .universalImage( + image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Unlist"), color: .white)!, + size: nil, + title: nil, + text: text, + customUndoText: nil, + timeout: 3.0 + ), + position: .bottom, + animateInAsReplacement: false, + appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0), + action: { action in + return false + } + ) + self.present(tooltipController, in: .window(.root)) + + if let updateResellStars { + updateResellStars(nil) + } else { + let _ = (context.engine.payments.updateStarGiftResalePrice(slug: gift.slug, price: nil) + |> deliverOnMainQueue).startStandalone() + } + }), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + }) + ], + actionLayout: .vertical + ) + self.present(alertController, in: .window(.root)) + } else { + let resellController = context.sharedContext.makeStarGiftResellScreen(context: context, update: update, completion: { [weak self] price in + guard let self else { + return + } + + self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(price)))) + + let giftTitle = "\(gift.title) #\(gift.number)" + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var text = "\(giftTitle) is now for sale!" + if update { + text = "\(giftTitle) is relisted for \(price) Stars." + } + + + let tooltipController = UndoOverlayController( + presentationData: presentationData, + content: .universalImage( + image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Sell"), color: .white)!, + size: nil, + title: nil, + text: text, + customUndoText: nil, + timeout: 3.0 + ), + position: .bottom, + animateInAsReplacement: false, + appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0), + action: { action in + return false + } + ) + self.present(tooltipController, in: .window(.root)) + + if let updateResellStars { + updateResellStars(price) + } else { + let _ = (context.engine.payments.updateStarGiftResalePrice(slug: gift.slug, price: price) + |> deliverOnMainQueue).startStandalone() + } + }) + self.push(resellController) + } + } + viewUpgradedImpl = { [weak self] messageId in guard let self, let navigationController = self.navigationController as? NavigationController else { return @@ -3066,6 +3625,13 @@ public class GiftViewScreen: ViewControllerComponentContainer { super.viewDidLoad() self.view.disablesInteractiveModalDismiss = true + + if let arguments = self.subject.arguments, let _ = self.subject.arguments?.resellStars { + if case let .unique(uniqueGift) = arguments.gift, case .peerId(self.context.account.peerId) = uniqueGift.owner { + } else { + self.showBalance = true + } + } } public override func viewWillDisappear(_ animated: Bool) { @@ -3095,6 +3661,10 @@ public class GiftViewScreen: ViewControllerComponentContainer { view.dismissAnimated() } + self.dismissBalanceOverlay() + } + + fileprivate func dismissBalanceOverlay() { if let view = self.balanceOverlay.view, view.superview != nil { view.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false) view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift index 6fb6a9d67f..517e0a777d 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift @@ -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 ) ), diff --git a/submodules/TelegramUI/Components/MarqueeComponent/BUILD b/submodules/TelegramUI/Components/MarqueeComponent/BUILD new file mode 100644 index 0000000000..543348ff1a --- /dev/null +++ b/submodules/TelegramUI/Components/MarqueeComponent/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/MarqueeComponent/Sources/MarqueeComponent.swift b/submodules/TelegramUI/Components/MarqueeComponent/Sources/MarqueeComponent.swift new file mode 100644 index 0000000000..6b1dac5eeb --- /dev/null +++ b/submodules/TelegramUI/Components/MarqueeComponent/Sources/MarqueeComponent.swift @@ -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, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index e74d17b390..b07b9b4bfb 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -65,6 +65,8 @@ swift_library( "//submodules/UrlEscaping", "//submodules/DeviceLocationManager", "//submodules/TelegramUI/Components/SaveProgressScreen", + "//submodules/TelegramUI/Components/MediaAssetsContext", + "//submodules/CheckNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift index 02770cd84f..1b053546ee 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 3699cfe229..ef0262ea56 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -326,6 +326,9 @@ final class MediaEditorScreenComponent: Component { private let switchCameraButton = ComponentView() + private let selectionButton = ComponentView() + private let selectionPanel = ComponentView() + private let textCancelButton = ComponentView() private let textDoneButton = ComponentView() private let textSize = ComponentView() @@ -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() @@ -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() + 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 diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreenDrafts.swift similarity index 61% rename from submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift rename to submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreenDrafts.swift index 03419f173a..26d9dad3e6 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreenDrafts.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorRecording.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreenRecording.swift similarity index 100% rename from submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorRecording.swift rename to submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreenRecording.swift diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelButtonContentComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelButtonContentComponent.swift new file mode 100644 index 0000000000..ca1ff99611 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelButtonContentComponent.swift @@ -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() + + 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, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelComponent.swift new file mode 100644 index 0000000000..b405b061f7 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelComponent.swift @@ -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 + 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() + + 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, 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, 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, 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, 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, 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 + } + } + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift index b45b22dcfe..81ba25209d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoGiftsCoverComponent.swift @@ -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)) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 9d556d26db..b3a328a3b1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -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: {} ) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 5344a720b0..3ca1bfee0b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift index 8db0756d2d..615e3ad7cf 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift @@ -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, diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift index 0bbf2fa9fe..303da9e6ba 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift @@ -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 diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 30e6265ef6..1c656cd9be 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -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] = [] diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift index 9047c66cc4..731bbf596e 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift @@ -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) diff --git a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift index b7abee888a..b372222740 100644 --- a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift @@ -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 ) ), diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index f55b8c698c..0ea0dd5b11 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -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 ), diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 2f0f3c2f74..856df6d79b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -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(), diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Collage.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Collage.imageset/Contents.json new file mode 100644 index 0000000000..7980b2cfd4 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Collage.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "combine.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Collage.imageset/combine.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Collage.imageset/combine.pdf new file mode 100644 index 0000000000..d7fee30cd9 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Collage.imageset/combine.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/SortNumber.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/SortNumber.imageset/Contents.json new file mode 100644 index 0000000000..fb212fad6b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/SortNumber.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "hash.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/SortNumber.imageset/hash.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/SortNumber.imageset/hash.pdf new file mode 100644 index 0000000000..4961e20e55 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/SortNumber.imageset/hash.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Sell.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Sell.imageset/Contents.json new file mode 100644 index 0000000000..56a854225f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Sell.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "sale.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Sell.imageset/sale.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Sell.imageset/sale.pdf new file mode 100644 index 0000000000..f714dfefe0 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Sell.imageset/sale.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Unlist.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Unlist.imageset/Contents.json new file mode 100644 index 0000000000..e0d2f367d2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Unlist.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "unsale.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Unlist.imageset/unsale.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Unlist.imageset/unsale.pdf new file mode 100644 index 0000000000..23f5daa966 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Unlist.imageset/unsale.pdf differ diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 197e0e91a6..a6cee571bb 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -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 } diff --git a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift index 412a55eab2..8a999cf103 100644 --- a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift @@ -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 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() + 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())) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 97d9a3c054..d234859ace 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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() @@ -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)) diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 78099bd0fa..3a6662cddd 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -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() } diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index fdc900c25c..922679abce 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -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() { diff --git a/third-party/td/td b/third-party/td/td index f1b7500310..04adfc87de 160000 --- a/third-party/td/td +++ b/third-party/td/td @@ -1 +1 @@ -Subproject commit f1b7500310baa496c0b779e4273a3aff0f14f42f +Subproject commit 04adfc87deea4c804def118e88c89a08c388b32b