diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index c281d13c72..4cfb44a885 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10987,6 +10987,7 @@ Sorry for the inconvenience."; "ContactList.Context.Delete" = "Delete Contact"; "ContactList.Context.Select" = "Select"; +"ContactList.DeleteConfirmationSingle" = "Delete Contact"; "ContactList.DeleteConfirmation_1" = "Delete %@ Contact"; "ContactList.DeleteConfirmation_any" = "Delete %@ Contacts"; @@ -11082,3 +11083,12 @@ Sorry for the inconvenience."; "GroupInfo.Permissions.DontRestrictBoosters" = "Do Not Restrict Boosters"; "GroupInfo.Permissions.DontRestrictBoostersInfo" = "Choose how many boosts a user must give to the group to bypass restrictions on sending messages."; "GroupInfo.Permissions.DontRestrictBoostersEnableInfo" = "Turn this on to always allow users who boosted your group to send messages and media."; + +"Notification.Boost.Times_1" = "%@ time"; +"Notification.Boost.Times_any" = "%@ times"; + +"Notification.Boost.Single" = "%1$@ boosted the group"; +"Notification.Boost.Multiple" = "%1$@ boosted the group %2$@"; + +"Notification.Boost.SingleYou" = "You boosted the group"; +"Notification.Boost.MultipleYou" = "You boosted the group %1$@"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 4b07814010..f4e511baba 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -739,7 +739,6 @@ public enum InstalledStickerPacksControllerMode { case modal case masks case emoji - case groupEmoji(selectedPack: StickerPackCollectionInfo?, completion: (StickerPackCollectionInfo?) -> Void) } public let defaultContactLabel: String = "_$!!$_" diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 9c69a37534..9097db0ec2 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -981,6 +981,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { }, hideTranslationPanel: { }, openPremiumGift: { }, openPremiumRequiredForMessaging: { + }, openBoostToUnrestrict: { }, updateHistoryFilter: { _ in }, requestLayout: { _ in }, chatController: { diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index 9d0c54d93c..1014fca2d9 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -144,7 +144,7 @@ private final class CameraContext { transform = CGAffineTransformTranslate(transform, 0.0, -size.height) ciImage = ciImage.transformed(by: transform) } - ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: Camera.isDualCameraSupported ? 100.0 : 40.0).cropped(to: CGRect(origin: .zero, size: size)) + ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: Camera.isDualCameraSupported(forRoundVideo: true) ? 100.0 : 40.0).cropped(to: CGRect(origin: .zero, size: size)) if let cgImage = self.ciContext.createCGImage(ciImage, from: ciImage.extent) { let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right) if front { @@ -1056,15 +1056,18 @@ public final class Camera { } } - public static var isDualCameraSupported: Bool { + public static func isDualCameraSupported(forRoundVideo: Bool = false) -> Bool { if #available(iOS 13.0, *), AVCaptureMultiCamSession.isMultiCamSupported && !DeviceModel.current.isIpad { + if forRoundVideo && DeviceModel.current == .iPhoneXR { + return false + } return true } else { return false } } } - +e public final class CameraHolder { public let camera: Camera public let previewView: CameraPreviewView diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 30314949a0..6dffac93ee 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -173,6 +173,7 @@ public final class ChatPanelInterfaceInteraction { public let openPremiumGift: () -> Void public let openPremiumRequiredForMessaging: () -> Void public let updateHistoryFilter: ((ChatPresentationInterfaceState.HistoryFilter?) -> ChatPresentationInterfaceState.HistoryFilter?) -> Void + public let openBoostToUnrestrict: () -> Void public let requestLayout: (ContainedViewLayoutTransition) -> Void public let chatController: () -> ViewController? public let statuses: ChatPanelInterfaceInteractionStatuses? @@ -281,6 +282,7 @@ public final class ChatPanelInterfaceInteraction { hideTranslationPanel: @escaping () -> Void, openPremiumGift: @escaping () -> Void, openPremiumRequiredForMessaging: @escaping () -> Void, + openBoostToUnrestrict: @escaping () -> Void, updateHistoryFilter: @escaping ((ChatPresentationInterfaceState.HistoryFilter?) -> ChatPresentationInterfaceState.HistoryFilter?) -> Void, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, chatController: @escaping () -> ViewController?, @@ -389,6 +391,7 @@ public final class ChatPanelInterfaceInteraction { self.hideTranslationPanel = hideTranslationPanel self.openPremiumGift = openPremiumGift self.openPremiumRequiredForMessaging = openPremiumRequiredForMessaging + self.openBoostToUnrestrict = openBoostToUnrestrict self.updateHistoryFilter = updateHistoryFilter self.requestLayout = requestLayout @@ -505,6 +508,7 @@ public final class ChatPanelInterfaceInteraction { }, hideTranslationPanel: { }, openPremiumGift: { }, openPremiumRequiredForMessaging: { + }, openBoostToUnrestrict: { }, updateHistoryFilter: { _ in }, requestLayout: { _ in }, chatController: { diff --git a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift index 99543a244b..082666d28c 100644 --- a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift +++ b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift @@ -183,9 +183,6 @@ public final class MultilineTextWithEntitiesComponent: Component { } } - let size = self.textNode.updateLayout(availableSize) - self.textNode.frame = CGRect(origin: .zero, size: size) - self.textNode.visibility = true if let context = component.context, let animationCache = component.animationCache, let animationRenderer = component.animationRenderer, let placeholderColor = component.placeholderColor { self.textNode.arguments = TextNodeWithEntities.Arguments( @@ -197,6 +194,9 @@ public final class MultilineTextWithEntitiesComponent: Component { ) } + let size = self.textNode.updateLayout(availableSize) + self.textNode.frame = CGRect(origin: .zero, size: size) + return size } } diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 4128fa31d7..37c902668d 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -589,7 +589,15 @@ public class ContactsController: ViewController { public func requestDeleteContacts(peerIds: [EnginePeer.Id]) { let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] - items.append(ActionSheetButtonItem(title: self.presentationData.strings.ContactList_DeleteConfirmation(Int32(peerIds.count)), color: .destructive, action: { [weak self, weak actionSheet] in + + let actionTitle: String + if peerIds.count > 1 { + actionTitle = self.presentationData.strings.ContactList_DeleteConfirmation(Int32(peerIds.count)) + } else { + actionTitle = self.presentationData.strings.ContactList_DeleteConfirmationSingle + } + + items.append(ActionSheetButtonItem(title: actionTitle, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() guard let self else { diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index b3f2ebcbbb..f56914ff3f 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -1115,6 +1115,7 @@ public class StickerPickerScreen: ViewController { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -1169,6 +1170,7 @@ public class StickerPickerScreen: ViewController { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -1199,6 +1201,7 @@ public class StickerPickerScreen: ViewController { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -1440,6 +1443,7 @@ public class StickerPickerScreen: ViewController { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -1470,6 +1474,7 @@ public class StickerPickerScreen: ViewController { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index 6b7beda409..127cd008a7 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -48,6 +48,7 @@ public final class ItemListStickerPackItem: ListViewItem, ItemListItem { let editing: ItemListStickerPackItemEditing let enabled: Bool let playAnimatedStickers: Bool + let style: ItemListStyle public let sectionId: ItemListSectionId let action: (() -> Void)? let setPackIdWithRevealedOptions: (ItemCollectionId?, ItemCollectionId?) -> Void @@ -55,7 +56,7 @@ public final class ItemListStickerPackItem: ListViewItem, ItemListItem { let removePack: () -> Void let toggleSelected: () -> Void - public init(presentationData: ItemListPresentationData, context: AccountContext, packInfo: StickerPackCollectionInfo, itemCount: String, topItem: StickerPackItem?, unread: Bool, control: ItemListStickerPackItemControl, editing: ItemListStickerPackItemEditing, enabled: Bool, playAnimatedStickers: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, addPack: @escaping () -> Void, removePack: @escaping () -> Void, toggleSelected: @escaping () -> Void) { + public init(presentationData: ItemListPresentationData, context: AccountContext, packInfo: StickerPackCollectionInfo, itemCount: String, topItem: StickerPackItem?, unread: Bool, control: ItemListStickerPackItemControl, editing: ItemListStickerPackItemEditing, enabled: Bool, playAnimatedStickers: Bool, style: ItemListStyle = .blocks, sectionId: ItemListSectionId, action: (() -> Void)?, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, addPack: @escaping () -> Void, removePack: @escaping () -> Void, toggleSelected: @escaping () -> Void) { self.presentationData = presentationData self.context = context self.packInfo = packInfo @@ -66,6 +67,7 @@ public final class ItemListStickerPackItem: ListViewItem, ItemListItem { self.editing = editing self.enabled = enabled self.playAnimatedStickers = playAnimatedStickers + self.style = style self.sectionId = sectionId self.action = action self.setPackIdWithRevealedOptions = setPackIdWithRevealedOptions @@ -410,8 +412,20 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { let verticalInset: CGFloat = 11.0 let titleSpacing: CGFloat = 2.0 - let insets = itemListNeighborsGroupedInsets(neighbors, params) let separatorHeight = UIScreenPixel + let insets: UIEdgeInsets + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + insets = itemListNeighborsGroupedInsets(neighbors, params) + } var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)? var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)? @@ -543,9 +557,9 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index 5ca187fa5f..bdaeb73de9 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -652,7 +652,7 @@ private func channelPermissionsControllerEntries(context: AccountContext, presen let slowModeTimeout = state.modifiedSlowmodeTimeout ?? (cachedData.slowModeTimeout ?? 0) if !canSendText || !canSendMedia || slowModeTimeout > 0 { - let unrestrictBoosters = state.modifiedUnrestrictBoosters ?? (cachedData.boostsUnrestrict ?? 0) + let unrestrictBoosters = state.modifiedUnrestrictBoosters ?? (cachedData.boostsToUnrestrict ?? 0) let unrestrictEnabled = unrestrictBoosters > 0 entries.append(.unrestrictBoostersSwitch(presentationData.theme, presentationData.strings.GroupInfo_Permissions_DontRestrictBoosters, unrestrictEnabled)) @@ -780,6 +780,9 @@ public func channelPermissionsController(context: AccountContext, updatedPresent let updateDefaultRightsDisposable = MetaDisposable() actionsDisposable.add(updateDefaultRightsDisposable) + let updateUnrestrictBoostersDisposable = MetaDisposable() + actionsDisposable.add(updateUnrestrictBoostersDisposable) + let peerView = Promise() peerView.set(sourcePeerId.get() |> mapToSignal(context.account.viewTracker.peerView)) @@ -1144,6 +1147,12 @@ public func channelPermissionsController(context: AccountContext, updatedPresent return state } + let _ = (peerView.get() + |> take(1) + |> deliverOnMainQueue).start(next: { view in + updateUnrestrictBoostersDisposable.set((context.engine.peers.updateChannelBoostsToUnlockRestrictions(peerId: view.peerId, boosts: value) + |> deliverOnMainQueue).start()) + }) }, toggleIsOptionExpanded: { flags in updateState { state in var state = state diff --git a/submodules/PremiumUI/Sources/PageIndicatorComponent.swift b/submodules/PremiumUI/Sources/PageIndicatorComponent.swift index 66f0142f76..1e5497a529 100644 --- a/submodules/PremiumUI/Sources/PageIndicatorComponent.swift +++ b/submodules/PremiumUI/Sources/PageIndicatorComponent.swift @@ -231,21 +231,7 @@ private final class PageIndicatorView: UIView { private func updateDotPosition(currentPage: Int, animated: Bool) { let duration = animated ? self.animationDuration : 0 - if !animated { - if currentPage == 0 { - let x = -self.scrollView.contentInset.left - self.moveScrollView(x: x) - } else { - for _ in 0 ..< currentPage { - let x = self.scrollView.contentOffset.x + self.itemSize - self.moveScrollView(x: x, duration: duration) - } - if currentPage == self.pageCount - 1 { - let x = self.scrollView.contentSize.width - self.scrollView.bounds.width + self.scrollView.contentInset.right - self.moveScrollView(x: x, duration: duration) - } - } - } else { + let action: (Int, Double) -> Void = { currentPage, duration in if currentPage == 0 { let x = -self.scrollView.contentInset.left self.moveScrollView(x: x, duration: duration) @@ -260,6 +246,18 @@ private final class PageIndicatorView: UIView { self.moveScrollView(x: x, duration: duration) } } + + if !animated { + if currentPage == 0 { + action(currentPage, 0) + } else { + for i in 0 ..< currentPage { + action(i, 0) + } + } + } else { + action(currentPage, duration) + } } private func updateDotSize(currentPage: Int, animated: Bool) { diff --git a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift index 68e9e37211..d740f64aa0 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift @@ -466,6 +466,8 @@ private final class SheetContent: CombinedComponent { final class State: ComponentState { var cachedChevronImage: (UIImage, PresentationTheme)? + var cachedIconImage: UIImage? + private(set) var peer: EnginePeer? private(set) var memberPeer: EnginePeer? @@ -511,6 +513,10 @@ private final class SheetContent: CombinedComponent { } static var body: Body { + let iconBackground = Child(Image.self) + let icon = Child(BundleIconComponent.self) + //let icon = Child(LottieComponent.self) + let peerShortcut = Child(Button.self) let text = Child(BalancedTextComponent.self) let alternateText = Child(List.self) @@ -658,7 +664,7 @@ private final class SheetContent: CombinedComponent { } isCurrent = true case let .unrestrict(unrestrictCount): - textString = "Boost the group \(unrestrictCount) times to remove messaging restrictions. Your boosts will help \(peerName) to unlock new features." + textString = "Boost the group **\(unrestrictCount)** times to remove messaging restrictions. Your boosts will help **\(peerName)** to unlock new features." isCurrent = true default: if let remaining { @@ -674,7 +680,7 @@ private final class SheetContent: CombinedComponent { isCurrent = mode == .current } case .features: - textString = "" + textString = "By gaining **boosts**, your group reaches higher levels and unlocks more features." } let defaultTitle = strings.ChannelBoost_Level("\(level)").string @@ -733,7 +739,48 @@ private final class SheetContent: CombinedComponent { } if case .features = component.mode { - contentSize.height += 17.0 + contentSize.height -= 14.0 + + let iconSize = CGSize(width: 90.0, height: 90.0) + let gradientImage: UIImage + if let current = state.cachedIconImage { + gradientImage = current + } else { + gradientImage = generateFilledCircleImage(diameter: iconSize.width, color: theme.actionSheet.controlAccentColor)! + context.state.cachedIconImage = gradientImage + } + + let iconBackground = iconBackground.update( + component: Image(image: gradientImage), + availableSize: iconSize, + transition: .immediate + ) + context.add(iconBackground + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + iconBackground.size.height / 2.0)) + ) + + let icon = icon.update( + component: BundleIconComponent( + name: "Premium/BoostLarge", + tintColor: .white + ), + availableSize: CGSize(width: 90.0, height: 90.0), + transition: .immediate + ) +// let icon = icon.update( +// component: LottieComponent( +// content: LottieComponent.AppBundleContent(name: iconName), +// playOnce: state.playOnce +// ), +// availableSize: CGSize(width: 70, height: 70), +// transition: .immediate +// ) + + context.add(icon + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + iconBackground.size.height / 2.0)) + ) + contentSize.height += iconSize.height + contentSize.height += 52.0 } else { let limit = limit.update( component: PremiumLimitDisplayComponent( @@ -760,83 +807,83 @@ private final class SheetContent: CombinedComponent { ) contentSize.height += limit.size.height + 23.0 + } + + if myBoostCount > 0 { + let alternateTitle = isCurrent ? strings.ChannelBoost_YouBoostedChannelText(peerName).string : strings.ChannelBoost_YouBoostedOtherChannelText - if myBoostCount > 0 { - let alternateTitle = isCurrent ? strings.ChannelBoost_YouBoostedChannelText(peerName).string : strings.ChannelBoost_YouBoostedOtherChannelText - - var alternateBadge: String? - if myBoostCount > 1 { - alternateBadge = "X\(myBoostCount)" - } - - let alternateText = alternateText.update( - component: List( - [ - AnyComponentWithIdentity( - id: "title", - component: AnyComponent( - BoostedTitleContent(text: NSAttributedString(string: alternateTitle, font: Font.semibold(15.0), textColor: textColor), badge: alternateBadge) - ) - ), - AnyComponentWithIdentity( - id: "text", - component: AnyComponent( - BalancedTextComponent( - text: .markdown(text: textString, attributes: markdownAttributes), - horizontalAlignment: .center, - maximumNumberOfLines: 0, - lineSpacing: 0.1 - ) + var alternateBadge: String? + if myBoostCount > 1 { + alternateBadge = "X\(myBoostCount)" + } + + let alternateText = alternateText.update( + component: List( + [ + AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + BoostedTitleContent(text: NSAttributedString(string: alternateTitle, font: Font.semibold(15.0), textColor: textColor), badge: alternateBadge) + ) + ), + AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + BalancedTextComponent( + text: .markdown(text: textString, attributes: markdownAttributes), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1 ) ) - ], - centerAlignment: true - ), - availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), - transition: .immediate - ) - context.add(alternateText - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + alternateText.size.height / 2.0)) - .appear(Transition.Appear({ _, view, transition in - transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: 64.0), to: .zero, additive: true) - transition.animateAlpha(view: view, from: 0.0, to: 1.0) + ) + ], + centerAlignment: true + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(alternateText + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + alternateText.size.height / 2.0)) + .appear(Transition.Appear({ _, view, transition in + transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: 64.0), to: .zero, additive: true) + transition.animateAlpha(view: view, from: 0.0, to: 1.0) + })) + .disappear(Transition.Disappear({ view, transition, completion in + view.superview?.sendSubviewToBack(view) + transition.animatePosition(view: view, from: .zero, to: CGPoint(x: 0.0, y: -64.0), additive: true) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) })) - .disappear(Transition.Disappear({ view, transition, completion in - view.superview?.sendSubviewToBack(view) - transition.animatePosition(view: view, from: .zero, to: CGPoint(x: 0.0, y: -64.0), additive: true) - transition.setAlpha(view: view, alpha: 0.0, completion: { _ in - completion() - }) - })) - ) - contentSize.height += alternateText.size.height + 13.0 - } else { - let text = text.update( - component: BalancedTextComponent( - text: .markdown(text: textString, attributes: markdownAttributes), - horizontalAlignment: .center, - maximumNumberOfLines: 0, - lineSpacing: 0.2 - ), - availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), - transition: .immediate - ) - context.add(text - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0)) - .appear(Transition.Appear({ _, view, transition in - transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: 64.0), to: .zero, additive: true) - transition.animateAlpha(view: view, from: 0.0, to: 1.0) + ) + contentSize.height += alternateText.size.height + 20.0 + } else { + let text = text.update( + component: BalancedTextComponent( + text: .markdown(text: textString, attributes: markdownAttributes), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(text + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0)) + .appear(Transition.Appear({ _, view, transition in + transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: 64.0), to: .zero, additive: true) + transition.animateAlpha(view: view, from: 0.0, to: 1.0) + })) + .disappear(Transition.Disappear({ view, transition, completion in + view.superview?.sendSubviewToBack(view) + transition.animatePosition(view: view, from: .zero, to: CGPoint(x: 0.0, y: -64.0), additive: true) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) })) - .disappear(Transition.Disappear({ view, transition, completion in - view.superview?.sendSubviewToBack(view) - transition.animatePosition(view: view, from: .zero, to: CGPoint(x: 0.0, y: -64.0), additive: true) - transition.setAlpha(view: view, alpha: 0.0, completion: { _ in - completion() - }) - })) - ) - contentSize.height += text.size.height + 13.0 - } + ) + contentSize.height += text.size.height + 20.0 } if case .owner = component.mode, let status = component.status { @@ -886,7 +933,6 @@ private final class SheetContent: CombinedComponent { iconPosition: .left, action: { component.boost() - component.dismiss() } ), availableSize: CGSize(width: (context.availableSize.width - 8.0 - sideInset * 2.0) / 2.0, height: 50.0), @@ -1296,6 +1342,8 @@ private final class BoostLevelsContainerComponent: CombinedComponent { ) let titleString: String + var titleFont = Font.semibold(17.0) + switch component.mode { case let .owner(subject): if let status = component.status, let _ = status.nextLevelBoosts { @@ -1352,12 +1400,13 @@ private final class BoostLevelsContainerComponent: CombinedComponent { } case .features: //TODO:localize - titleString = "Features" + titleString = "Additional Features" + titleFont = Font.semibold(20.0) } - + let title = title.update( component: MultilineTextComponent( - text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)), + text: .plain(NSAttributedString(string: titleString, font: titleFont, textColor: theme.rootController.navigationBar.primaryTextColor)), horizontalAlignment: .center, truncationType: .end, maximumNumberOfLines: 1 @@ -1366,7 +1415,29 @@ private final class BoostLevelsContainerComponent: CombinedComponent { transition: context.transition ) - let topPanelAlpha: CGFloat = min(30.0, state.topContentOffset) / 30.0 + let topPanelAlpha: CGFloat + let titleOriginY: CGFloat + let titleScale: CGFloat + if case .features = component.mode { + if state.topContentOffset > 78.0 { + topPanelAlpha = min(30.0, state.topContentOffset - 78.0) / 30.0 + } else { + topPanelAlpha = 0.0 + } + + let titleTopOriginY = topPanel.size.height / 2.0 + let titleBottomOriginY: CGFloat = 146.0 + let titleOriginDelta = titleTopOriginY - titleBottomOriginY + + let fraction = min(1.0, state.topContentOffset / abs(titleOriginDelta)) + titleOriginY = titleBottomOriginY + fraction * titleOriginDelta + titleScale = 1.0 - max(0.0, fraction * 0.2) + } else { + topPanelAlpha = min(30.0, state.topContentOffset) / 30.0 + titleOriginY = topPanel.size.height / 2.0 + titleScale = 1.0 + } + context.add(topPanel .position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0)) .opacity(topPanelAlpha) @@ -1376,7 +1447,8 @@ private final class BoostLevelsContainerComponent: CombinedComponent { .opacity(topPanelAlpha) ) context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY)) + .scale(titleScale) ) if let openStats = component.openStats { @@ -1615,6 +1687,9 @@ public class PremiumBoostLevelsScreen: ViewController { private var dismissOffset: CGFloat? func containerLayoutUpdated(layout: ContainerViewLayout, transition: Transition) { + guard !self.isDismissing else { + return + } self.currentLayout = layout self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0)) @@ -1725,15 +1800,17 @@ public class PremiumBoostLevelsScreen: ViewController { controller.dismiss(animated: true) - let boostController = PremiumBoostLevelsScreen( - context: controller.context, - peerId: controller.peerId, - mode: .user(mode: .current), - status: controller.status, - myBoostStatus: nil, - forceDark: controller.forceDark - ) - navigationController.pushViewController(boostController, animated: true) + Queue.mainQueue().justDispatch { + let boostController = PremiumBoostLevelsScreen( + context: controller.context, + peerId: controller.peerId, + mode: .user(mode: .current), + status: controller.status, + myBoostStatus: nil, + forceDark: controller.forceDark + ) + navigationController.pushViewController(boostController, animated: true) + } }, copyLink: { [weak self, weak controller] link in guard let self else { diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index e646ae1ee0..ee7bc25662 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -317,12 +317,6 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { if !component.isCompleted { if let products = component.products { - let gradientColors: [UIColor] = [ - UIColor(rgb: 0x8e77ff), - UIColor(rgb: 0x9a6fff), - UIColor(rgb: 0xb36eee) - ] - let shortestOptionPrice: (Int64, NSDecimalNumber) if let product = products.last { shortestOptionPrice = (Int64(Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months)), product.storeProduct.priceValue.dividing(by: NSDecimalNumber(value: product.months))) @@ -352,14 +346,20 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { var accessibilitySubtitle = "" var pricePerMonth = environment.strings.Premium_PricePerMonth(product.storeProduct.pricePerMonth(Int(product.months))).string + var labelPrice = pricePerMonth if component.peers.count > 1 { - subtitle = "\(product.storeProduct.price) x \(component.peers.count)" pricePerMonth = product.storeProduct.multipliedPrice(count: component.peers.count) + + subtitle = "" + labelPrice = "\(product.storeProduct.price) x \(component.peers.count)" } else { if discountValue > 0 { subtitle = "**\(defaultPrice)** \(product.price)" accessibilitySubtitle = product.price } + + subtitle = "" + labelPrice = product.price } items.append(SectionGroupComponent.Item( @@ -369,13 +369,13 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { PremiumOptionComponent( title: giftTitle, subtitle: subtitle, - labelPrice: pricePerMonth, + labelPrice: labelPrice, discount: discount, multiple: component.peers.count > 1, selected: product.id == component.selectedProductId, primaryTextColor: textColor, secondaryTextColor: subtitleColor, - accentColor: gradientColors[i], + accentColor: environment.theme.list.itemAccentColor, checkForegroundColor: environment.theme.list.itemCheckColors.foregroundColor, checkBorderColor: environment.theme.list.itemCheckColors.strokeColor ) diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 1a4b54db11..e73b559d13 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -643,9 +643,9 @@ public enum PremiumPerk: CaseIterable { case .messageTags: return "Premium/Perk/MessageTags" case .lastSeen: - return "Premium/Perk/Wallpapers" + return "Premium/Perk/LastSeen" case .messagePrivacy: - return "Premium/Perk/MessageTags" + return "Premium/Perk/MessagePrivacy" } } } @@ -864,7 +864,6 @@ final class PremiumOptionComponent: CombinedComponent { transition: context.transition ) - var discountOffset: CGFloat = 0.0 let discountSize: CGSize if !component.discount.isEmpty { let discount = discount.update( @@ -872,7 +871,7 @@ final class PremiumOptionComponent: CombinedComponent { text: .plain( NSAttributedString( string: component.discount, - font: Font.with(size: component.multiple ? 13.0 : 14.0, design: .round, weight: .semibold, traits: []), + font: Font.with(size: 14.0, design: .round, weight: .semibold, traits: []), textColor: .white ) ), @@ -893,13 +892,7 @@ final class PremiumOptionComponent: CombinedComponent { transition: context.transition ) - let discountPosition: CGPoint - if component.multiple { - discountOffset = discountSize.width + 6.0 - discountPosition = CGPoint(x: insets.left + discountSize.width / 2.0, y: insets.top + title.size.height + discountSize.height / 2.0) - } else { - discountPosition = CGPoint(x: insets.left + title.size.width + 6.0 + discountSize.width / 2.0, y: insets.top + title.size.height / 2.0) - } + let discountPosition = CGPoint(x: insets.left + title.size.width + 6.0 + discountSize.width / 2.0, y: insets.top + title.size.height / 2.0) context.add(discountBackground .position(discountPosition) @@ -938,7 +931,7 @@ final class PremiumOptionComponent: CombinedComponent { transition: context.transition ) context.add(subtitle - .position(CGPoint(x: insets.left + subtitle.size.width / 2.0 + discountOffset, y: insets.top + title.size.height + spacing + subtitle.size.height / 2.0)) + .position(CGPoint(x: insets.left + subtitle.size.width / 2.0, y: insets.top + title.size.height + spacing + subtitle.size.height / 2.0)) ) subtitleSize = subtitle.size @@ -1894,11 +1887,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let layoutOptions = { if let products = state.products, products.count > 1, state.isPremium == false || (!context.component.justBought && state.canUpgrade) { var optionsItems: [SectionGroupComponent.Item] = [] - let gradientColors: [UIColor] = [ - UIColor(rgb: 0x8e77ff), - UIColor(rgb: 0x9a6fff), - UIColor(rgb: 0xb36eee) - ] let shortestOptionPrice: (Int64, NSDecimalNumber) if let product = products.first(where: { $0.id.hasSuffix(".monthly") }) { @@ -1967,7 +1955,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { selected: !product.isCurrent && product.id == state.selectedProductId, primaryTextColor: textColor, secondaryTextColor: subtitleColor, - accentColor: gradientColors[i], + accentColor: environment.theme.list.itemAccentColor, checkForegroundColor: environment.theme.list.itemCheckColors.foregroundColor, checkBorderColor: environment.theme.list.itemCheckColors.strokeColor ) diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index a26c18ac20..4b3e34a3bc 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -771,7 +771,7 @@ public class PremiumLimitsListScreen: ViewController { position: .top, model: .island, videoFile: configuration.videos["last_seen"], - decoration: .tag + decoration: .badgeStars )), title: strings.Premium_LastSeen, text: strings.Premium_LastSeenInfo, @@ -790,7 +790,7 @@ public class PremiumLimitsListScreen: ViewController { position: .top, model: .island, videoFile: configuration.videos["message_privacy"], - decoration: .tag + decoration: .swirlStars )), title: strings.Premium_MessagePrivacy, text: strings.Premium_MessagePrivacyInfo, diff --git a/submodules/PremiumUI/Sources/StoriesPageComponent.swift b/submodules/PremiumUI/Sources/StoriesPageComponent.swift index 3821086a4c..d03b7a9752 100644 --- a/submodules/PremiumUI/Sources/StoriesPageComponent.swift +++ b/submodules/PremiumUI/Sources/StoriesPageComponent.swift @@ -582,7 +582,6 @@ final class StoriesPageComponent: CombinedComponent { let topPanel = Child(BlurredBackgroundComponent.self) let topSeparator = Child(Rectangle.self) let title = Child(MultilineTextComponent.self) - let secondaryTitle = Child(MultilineTextComponent.self) let resetScroll = ActionSlot() @@ -664,17 +663,6 @@ final class StoriesPageComponent: CombinedComponent { availableSize: context.availableSize, transition: context.transition ) - - let secondaryTitle = secondaryTitle.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString(string: strings.Premium_Stories_AdditionalTitle, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)), - horizontalAlignment: .center, - truncationType: .end, - maximumNumberOfLines: 1 - ), - availableSize: context.availableSize, - transition: context.transition - ) let topPanelAlpha: CGFloat if state.topContentOffset > 78.0 { @@ -699,23 +687,9 @@ final class StoriesPageComponent: CombinedComponent { let titleOriginY: CGFloat = titleBottomOriginY + fraction * titleOriginDelta let titleScale = 1.0 - max(0.0, fraction * 0.2) - let titleAlpha: CGFloat - if fraction > 0.78 { - titleAlpha = max(0.0, 1.0 - (fraction - 0.78) / 0.16) - } else { - titleAlpha = 1.0 - } - let secondaryTitleAlpha: CGFloat = 1.0 - titleAlpha - context.add(title .position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY)) .scale(titleScale) - .opacity(titleAlpha) - ) - - context.add(secondaryTitle - .position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY)) - .opacity(secondaryTitleAlpha) ) return scroll.size diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 3e7b5cf329..37fbd0742d 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -1656,6 +1656,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -1704,6 +1705,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { groupId: AnyHashable(info.id), title: info.title, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -1762,6 +1764,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -1793,6 +1796,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, diff --git a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift index 7f913623ec..b0264ed24f 100644 --- a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift @@ -510,7 +510,7 @@ private func namespaceForMode(_ mode: InstalledStickerPacksControllerMode) -> It return Namespaces.ItemCollection.CloudStickerPacks case .masks: return Namespaces.ItemCollection.CloudMaskPacks - case .emoji, .groupEmoji: + case .emoji: return Namespaces.ItemCollection.CloudEmojiPacks } } @@ -574,11 +574,6 @@ private func installedStickerPacksControllerEntries(context: AccountContext, pre entries.append(.suggestAnimatedEmoji(presentationData.strings.StickerPacksSettings_SuggestAnimatedEmoji, stickerSettings.suggestAnimatedEmoji)) entries.append(.suggestAnimatedEmojiInfo(presentationData.theme, presentationData.strings.StickerPacksSettings_SuggestAnimatedEmojiInfo)) - case .groupEmoji: - entries.append(.info(presentationData.theme, "All members will be able to use these emoji in the group, even if they don't have Telegram Premium.\n")) - - //TODO:localize - entries.append(.packsTitle(presentationData.theme, "CHOOSE EMOJI PACK".uppercased())) } if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [namespaceForMode(mode)])] as? ItemCollectionInfosView { @@ -624,27 +619,21 @@ private func installedStickerPacksControllerEntries(context: AccountContext, pre } } - if case .groupEmoji = mode { - - } else { - var markdownString: String - switch mode { - case .general, .modal: - markdownString = presentationData.strings.StickerPacksSettings_ManagingHelp - case .masks: - markdownString = presentationData.strings.MaskStickerSettings_Info - case .emoji: - markdownString = presentationData.strings.EmojiStickerSettings_Info - case .groupEmoji: - markdownString = "" - } - let entities = generateTextEntities(markdownString, enabledTypes: [.mention]) - if let entity = entities.first { - markdownString.insert(contentsOf: "]()", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.upperBound)) - markdownString.insert(contentsOf: "[", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.lowerBound)) - } - entries.append(.packsInfo(presentationData.theme, markdownString)) + var markdownString: String + switch mode { + case .general, .modal: + markdownString = presentationData.strings.StickerPacksSettings_ManagingHelp + case .masks: + markdownString = presentationData.strings.MaskStickerSettings_Info + case .emoji: + markdownString = presentationData.strings.EmojiStickerSettings_Info } + let entities = generateTextEntities(markdownString, enabledTypes: [.mention]) + if let entity = entities.first { + markdownString.insert(contentsOf: "]()", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.upperBound)) + markdownString.insert(contentsOf: "[", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.lowerBound)) + } + entries.append(.packsInfo(presentationData.theme, markdownString)) return entries } @@ -917,7 +906,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta archivedPromise.set(.single(nil) |> then(context.engine.stickers.archivedStickerPacks(namespace: .masks) |> map(Optional.init))) quickReaction = .single(nil) emojiCount.set(.single(0)) - case .emoji, .groupEmoji: + case .emoji: featured.set(.single([])) archivedPromise.set(.single(nil) |> then(context.engine.stickers.archivedStickerPacks(namespace: .emoji) |> map(Optional.init))) quickReaction = .single(nil) @@ -1070,8 +1059,6 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta } else { if case .modal = mode { rightNavigationButton = nil - } else if case .groupEmoji = mode { - rightNavigationButton = nil } else { rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { updateState { @@ -1093,9 +1080,6 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta title = presentationData.strings.MaskStickerSettings_Title case .emoji: title = presentationData.strings.EmojiPacksSettings_Title - case .groupEmoji: - //TODO:localize - title = "Group Emoji Pack" } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) @@ -1250,48 +1234,43 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta packs.insert(packReference, at: 0) } if let mainStickerPack = mainStickerPack { - //TODO:localize - presentControllerImpl?(StickerPackScreen(context: context, mode: .settings, mainStickerPack: mainStickerPack, stickerPacks: [mainStickerPack], mainActionTitle: "Set as Group Emoji Pack", parentNavigationController: controller?.navigationController as? NavigationController, actionPerformed: { actions in - if case .groupEmoji = mode { - - } else { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - var animateInAsReplacement = false - if let navigationController = navigationControllerImpl?() { - for controller in navigationController.overlayControllers { - if let controller = controller as? UndoOverlayController { - controller.dismissWithCommitActionAndReplacementAnimation() - animateInAsReplacement = true - } + presentControllerImpl?(StickerPackScreen(context: context, mode: .settings, mainStickerPack: mainStickerPack, stickerPacks: [mainStickerPack], parentNavigationController: controller?.navigationController as? NavigationController, actionPerformed: { actions in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var animateInAsReplacement = false + if let navigationController = navigationControllerImpl?() { + for controller in navigationController.overlayControllers { + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitActionAndReplacementAnimation() + animateInAsReplacement = true } } - if let (info, items, action) = actions.first { - switch action { - case .add: - navigationControllerImpl?()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in - return true - })) - case let .remove(positionInList): - let removedTitle: String - let removedText: String - if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { - removedTitle = presentationData.strings.EmojiPackActionInfo_RemovedTitle - removedText = presentationData.strings.EmojiPackActionInfo_RemovedText(info.title).string - } else if info.id.namespace == Namespaces.ItemCollection.CloudMaskPacks { - removedTitle = presentationData.strings.MaskPackActionInfo_RemovedTitle - removedText = presentationData.strings.MaskPackActionInfo_RemovedText(info.title).string - } else { - removedTitle = presentationData.strings.StickerPackActionInfo_RemovedTitle - removedText = presentationData.strings.StickerPackActionInfo_RemovedText(info.title).string - } - - navigationControllerImpl?()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: removedTitle, text: removedText, undo: true, info: info, topItem: items.first, context: context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { action in - if case .undo = action { - let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: positionInList).start() - } - return true - })) + } + if let (info, items, action) = actions.first { + switch action { + case .add: + navigationControllerImpl?()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in + return true + })) + case let .remove(positionInList): + let removedTitle: String + let removedText: String + if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { + removedTitle = presentationData.strings.EmojiPackActionInfo_RemovedTitle + removedText = presentationData.strings.EmojiPackActionInfo_RemovedText(info.title).string + } else if info.id.namespace == Namespaces.ItemCollection.CloudMaskPacks { + removedTitle = presentationData.strings.MaskPackActionInfo_RemovedTitle + removedText = presentationData.strings.MaskPackActionInfo_RemovedText(info.title).string + } else { + removedTitle = presentationData.strings.StickerPackActionInfo_RemovedTitle + removedText = presentationData.strings.StickerPackActionInfo_RemovedText(info.title).string } + + navigationControllerImpl?()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: removedTitle, text: removedText, undo: true, info: info, topItem: items.first, context: context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { action in + if case .undo = action { + let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: positionInList).start() + } + return true + })) } } }), nil) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index f4a89cceb7..be8ecd8eb8 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -476,6 +476,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[508332649] = { return Api.Message.parse_message($0) } dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) } dict[721967202] = { return Api.Message.parse_messageService($0) } + dict[-872240531] = { return Api.MessageAction.parse_messageActionBoostApply($0) } dict[-988359047] = { return Api.MessageAction.parse_messageActionBotAllowed($0) } dict[-1781355374] = { return Api.MessageAction.parse_messageActionChannelCreate($0) } dict[-365344535] = { return Api.MessageAction.parse_messageActionChannelMigrateFrom($0) } diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index 71d2947203..f63228f098 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -664,6 +664,7 @@ public extension Api { } public extension Api { enum MessageAction: TypeConstructorDescription { + case messageActionBoostApply(boosts: Int32) case messageActionBotAllowed(flags: Int32, domain: String?, app: Api.BotApp?) case messageActionChannelCreate(title: String) case messageActionChannelMigrateFrom(title: String, chatId: Int64) @@ -708,6 +709,12 @@ public extension Api { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { + case .messageActionBoostApply(let boosts): + if boxed { + buffer.appendInt32(-872240531) + } + serializeInt32(boosts, buffer: buffer, boxed: false) + break case .messageActionBotAllowed(let flags, let domain, let app): if boxed { buffer.appendInt32(-988359047) @@ -1032,6 +1039,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { + case .messageActionBoostApply(let boosts): + return ("messageActionBoostApply", [("boosts", boosts as Any)]) case .messageActionBotAllowed(let flags, let domain, let app): return ("messageActionBotAllowed", [("flags", flags as Any), ("domain", domain as Any), ("app", app as Any)]) case .messageActionChannelCreate(let title): @@ -1117,6 +1126,17 @@ public extension Api { } } + public static func parse_messageActionBoostApply(_ reader: BufferReader) -> MessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.MessageAction.messageActionBoostApply(boosts: _1!) + } + else { + return nil + } + } public static func parse_messageActionBotAllowed(_ reader: BufferReader) -> MessageAction? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index c85af6eb72..ec3557d83e 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -224,7 +224,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } switch action { - case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults: + case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply: break case let .messageActionChannelMigrateFrom(_, chatId): result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index dda3010fa8..76454c3353 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -135,6 +135,8 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .giveawayLaunched) case let .messageActionGiveawayResults(winners, unclaimed): return TelegramMediaAction(action: .giveawayResults(winners: winners, unclaimed: unclaimed)) + case let .messageActionBoostApply(boosts): + return TelegramMediaAction(action: .boostsApplied(boosts: boosts)) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift index 3f5daf9186..f62e9d2921 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift @@ -260,7 +260,8 @@ public final class CachedChannelData: CachedPeerData { public let membersHidden: EnginePeerCachedInfoItem public let viewForumAsMessages: EnginePeerCachedInfoItem public let wallpaper: TelegramWallpaper? - public let boostsUnrestrict: Int32? + public let boostsToUnrestrict: Int32? + public let appliedBoosts: Int32? public let emojiPack: StickerPackCollectionInfo? public let peerIds: Set @@ -303,7 +304,8 @@ public final class CachedChannelData: CachedPeerData { self.membersHidden = .unknown self.viewForumAsMessages = .unknown self.wallpaper = nil - self.boostsUnrestrict = nil + self.boostsToUnrestrict = nil + self.appliedBoosts = nil self.emojiPack = nil } @@ -339,7 +341,8 @@ public final class CachedChannelData: CachedPeerData { membersHidden: EnginePeerCachedInfoItem, viewForumAsMessages: EnginePeerCachedInfoItem, wallpaper: TelegramWallpaper?, - boostsUnrestrict: Int32?, + boostsToUnrestrict: Int32?, + appliedBoosts: Int32?, emojiPack: StickerPackCollectionInfo? ) { self.isNotAccessible = isNotAccessible @@ -373,7 +376,8 @@ public final class CachedChannelData: CachedPeerData { self.membersHidden = membersHidden self.viewForumAsMessages = viewForumAsMessages self.wallpaper = wallpaper - self.boostsUnrestrict = boostsUnrestrict + self.boostsToUnrestrict = boostsToUnrestrict + self.appliedBoosts = appliedBoosts self.emojiPack = emojiPack var peerIds = Set() @@ -402,137 +406,141 @@ public final class CachedChannelData: CachedPeerData { } public func withUpdatedIsNotAccessible(_ isNotAccessible: Bool) -> CachedChannelData { - return CachedChannelData(isNotAccessible: isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedFlags(_ flags: CachedChannelFlags) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedAbout(_ about: String?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedParticipantsSummary(_ participantsSummary: CachedChannelParticipantsSummary) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedExportedInvitation(_ exportedInvitation: ExportedInvitation?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedBotInfos(_ botInfos: [CachedPeerBotInfo]) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedPeerStatusSettings(_ peerStatusSettings: PeerStatusSettings?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedStickerPack(_ stickerPack: StickerPackCollectionInfo?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedMinAvailableMessageId(_ minAvailableMessageId: MessageId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedMigrationReference(_ migrationReference: ChannelMigrationReference?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedLinkedDiscussionPeerId(_ linkedDiscussionPeerId: LinkedDiscussionPeerId) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedPeerGeoLocation(_ peerGeoLocation: PeerGeoLocation?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedSlowModeTimeout(_ slowModeTimeout: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedSlowModeValidUntilTimestamp(_ slowModeValidUntilTimestamp: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedStatsDatacenterId(_ statsDatacenterId: Int32) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedInvitedBy(_ invitedBy: PeerId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedInvitedOn(_ invitedOn: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedPhoto(_ photo: TelegramMediaImage?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedActiveCall(_ activeCall: ActiveCall?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedCallJoinPeerId(_ callJoinPeerId: PeerId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedAutoremoveTimeout(_ autoremoveTimeout: CachedPeerAutoremoveTimeout) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedPendingSuggestions(_ pendingSuggestions: [String]) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedThemeEmoticon(_ themeEmoticon: String?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedInviteRequestsPending(_ inviteRequestsPending: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedSendAsPeerId(_ sendAsPeerId: PeerId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedAllowedReactions(_ allowedReactions: EnginePeerCachedInfoItem) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedMembersHidden(_ membersHidden: EnginePeerCachedInfoItem) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedViewForumAsMessages(_ viewForumAsMessages: EnginePeerCachedInfoItem) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedWallpaper(_ wallpaper: TelegramWallpaper?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: self.emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) } - public func withUpdatedBoostsUnrestrict(_ boostsUnrestrict: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: boostsUnrestrict, emojiPack: self.emojiPack) + public func withUpdatedBoostsToUnrestrict(_ boostsToUnrestrict: Int32?) -> CachedChannelData { + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack) + } + + public func withUpdateAppliedBoosts(_ appliedBoosts: Int32?) -> CachedChannelData { + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: appliedBoosts, emojiPack: self.emojiPack) } public func withUpdatedEmojiPack(_ emojiPack: StickerPackCollectionInfo?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsUnrestrict: self.boostsUnrestrict, emojiPack: emojiPack) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: emojiPack) } - + public init(decoder: PostboxDecoder) { self.isNotAccessible = decoder.decodeInt32ForKey("isNotAccessible", orElse: 0) != 0 self.flags = CachedChannelFlags(rawValue: decoder.decodeInt32ForKey("f", orElse: 0)) @@ -648,7 +656,9 @@ public final class CachedChannelData: CachedPeerData { } } - self.boostsUnrestrict = decoder.decodeOptionalInt32ForKey("bu") + self.boostsToUnrestrict = decoder.decodeOptionalInt32ForKey("bu") + + self.appliedBoosts = decoder.decodeOptionalInt32ForKey("ab") if let emojiPack = decoder.decodeObjectForKey("ep", decoder: { StickerPackCollectionInfo(decoder: $0) }) as? StickerPackCollectionInfo { self.emojiPack = emojiPack @@ -821,12 +831,18 @@ public final class CachedChannelData: CachedPeerData { encoder.encodeNil(forKey: "wp") } - if let boostsUnrestrict = self.boostsUnrestrict { - encoder.encodeInt32(boostsUnrestrict, forKey: "bu") + if let boostsToUnrestrict = self.boostsToUnrestrict { + encoder.encodeInt32(boostsToUnrestrict, forKey: "bu") } else { encoder.encodeNil(forKey: "bu") } + if let appliedBoosts = self.appliedBoosts { + encoder.encodeInt32(appliedBoosts, forKey: "ab") + } else { + encoder.encodeNil(forKey: "ab") + } + if let emojiPack = self.emojiPack { encoder.encodeObject(emojiPack, forKey: "ep") } else { @@ -958,12 +974,19 @@ public final class CachedChannelData: CachedPeerData { if other.viewForumAsMessages != self.viewForumAsMessages { return false } + if other.wallpaper != self.wallpaper { return false } - if other.boostsUnrestrict != self.boostsUnrestrict { + + if other.boostsToUnrestrict != self.boostsToUnrestrict { return false } + + if other.appliedBoosts != self.appliedBoosts { + return false + } + if other.emojiPack != self.emojiPack { return false } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index f0668d34e9..6dbb501727 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -126,6 +126,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case giveawayLaunched case joinedChannel case giveawayResults(winners: Int32, unclaimed: Int32) + case boostsApplied(boosts: Int32) public init(decoder: PostboxDecoder) { let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) @@ -232,6 +233,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { self = .joinedChannel case 39: self = .giveawayResults(winners: decoder.decodeInt32ForKey("winners", orElse: 0), unclaimed: decoder.decodeInt32ForKey("unclaimed", orElse: 0)) + case 40: + self = .boostsApplied(boosts: decoder.decodeInt32ForKey("boosts", orElse: 0)) default: self = .unknown } @@ -451,6 +454,9 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { encoder.encodeInt32(39, forKey: "_rawValue") encoder.encodeInt32(winners, forKey: "winners") encoder.encodeInt32(unclaimed, forKey: "unclaimed") + case let .boostsApplied(boosts): + encoder.encodeInt32(40, forKey: "_rawValue") + encoder.encodeInt32(boosts, forKey: "boosts") } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift index 0813017891..ddeea363e8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift @@ -183,3 +183,33 @@ func _internal_updateDefaultChannelMemberBannedRights(account: Account, peerId: |> switchToLatest } +func _internal_updateChannelBoostsToUnlockRestrictions(account: Account, peerId: PeerId, boosts: Int32) -> Signal { + return account.postbox.transaction { transaction -> Signal in + guard let peer = transaction.getPeer(peerId), let inputChannel = apiInputChannel(peer) else { + return .complete() + } + return account.network.request(Api.functions.channels.setBoostsToUnblockRestrictions(channel: inputChannel, boosts: boosts)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result = result else { + return .complete() + } + account.stateManager.addUpdates(result) + return account.postbox.transaction { transaction -> Void in + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData in + if let cachedData = cachedData as? CachedChannelData { + return cachedData.withUpdatedBoostsToUnrestrict(boosts) + } + return cachedData + }) + } + |> ignoreValues + } + } + |> switchToLatest +} + + diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 69e52ed13d..2beec9a700 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -352,6 +352,10 @@ public extension TelegramEngine { public func updateDefaultChannelMemberBannedRights(peerId: PeerId, rights: TelegramChatBannedRights) -> Signal { return _internal_updateDefaultChannelMemberBannedRights(account: self.account, peerId: peerId, rights: rights) } + + public func updateChannelBoostsToUnlockRestrictions(peerId: PeerId, boosts: Int32) -> Signal { + return _internal_updateChannelBoostsToUnlockRestrictions(account: self.account, peerId: peerId, boosts: boosts) + } public func createChannel(title: String, description: String?, username: String? = nil) -> Signal { return _internal_createChannel(account: self.account, title: title, description: description, username: username) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index cd392adb0a..aa0b8b220f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -459,7 +459,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } switch fullChat { - case let .channelFull(flags, flags2, _, about, participantsCount, adminsCount, kickedCount, bannedCount, _, _, _, _, chatPhoto, _, apiExportedInvite, apiBotInfos, migratedFromChatId, migratedFromMaxId, pinnedMsgId, stickerSet, minAvailableMsgId, _, linkedChatId, location, slowmodeSeconds, slowmodeNextSendDate, statsDc, _, inputCall, ttl, pendingSuggestions, groupcallDefaultJoinAs, themeEmoticon, requestsPending, _, defaultSendAs, allowedReactions, _, wallpaper, _, boostsUnrestrict, emojiSet): + case let .channelFull(flags, flags2, _, about, participantsCount, adminsCount, kickedCount, bannedCount, _, _, _, _, chatPhoto, _, apiExportedInvite, apiBotInfos, migratedFromChatId, migratedFromMaxId, pinnedMsgId, stickerSet, minAvailableMsgId, _, linkedChatId, location, slowmodeSeconds, slowmodeNextSendDate, statsDc, _, inputCall, ttl, pendingSuggestions, groupcallDefaultJoinAs, themeEmoticon, requestsPending, _, defaultSendAs, allowedReactions, _, wallpaper, appliedBoosts, boostsUnrestrict, emojiSet): var channelFlags = CachedChannelFlags() if (flags & (1 << 3)) != 0 { channelFlags.insert(.canDisplayParticipants) @@ -674,7 +674,8 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedMembersHidden(.known(PeerMembersHidden(value: membersHidden))) .withUpdatedViewForumAsMessages(.known(forumViewAsMessages)) .withUpdatedWallpaper(wallpaper) - .withUpdatedBoostsUnrestrict(boostsUnrestrict) + .withUpdatedBoostsToUnrestrict(boostsUnrestrict) + .withUpdateAppliedBoosts(appliedBoosts) .withUpdatedEmojiPack(emojiPack) }) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index d88a54c12e..55ba7aef82 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -122,6 +122,7 @@ public enum PresentationResourceKey: Int32 { case chatTitleLockIcon case chatTitleMuteIcon case chatPanelLockIcon + case chatPanelBoostIcon case chatBubbleVerticalLineIncomingImage case chatBubbleVerticalLineOutgoingImage diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index 4e334ff8c8..e919168fe9 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -84,6 +84,12 @@ public struct PresentationResourcesChat { }) } + public static func chatPanelBoostIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatPanelBoostIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Premium/BoostChannel"), color: theme.chat.inputPanel.panelControlAccentColor) + }) + } + public static func chatTitleMuteIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatTitleMuteIcon.rawValue, { theme in return generateImage(CGSize(width: 9.0, height: 9.0), rotatedContext: { size, context in diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index c32d06c154..861a0885a5 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -959,6 +959,25 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } else { attributedString = parseMarkdownIntoAttributedString(strings.Notification_GiveawayResults(winners), attributes: MarkdownAttributes(body: bodyAttributes, bold: boldAttributes, link: bodyAttributes, linkAttribute: { _ in return nil })) } + case let .boostsApplied(boosts): + if message.author?.id == accountPeerId { + if boosts == 1 { + attributedString = NSAttributedString(string: strings.Notification_Boost_SingleYou, font: titleFont, textColor: primaryTextColor) + } else { + let boostsString = strings.Notification_Boost_Times(boosts) + attributedString = NSAttributedString(string: strings.Notification_Boost_MultipleYou(boostsString).string, font: titleFont, textColor: primaryTextColor) + } + } else { + let peerName = message.author?.compactDisplayTitle ?? "" + if boosts == 1 { + let resultTitleString = strings.Notification_Boost_Single(peerName) + attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + } else { + let boostsString = strings.Notification_Boost_Times(boosts) + let resultTitleString = strings.Notification_Boost_Multiple(peerName, boostsString) + attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + } + } case .unknown: attributedString = nil } diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index fbeb745242..28692a32ca 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -428,6 +428,7 @@ final class AvatarEditorScreenComponent: Component { groupId: "emoji", title: "Emoji", subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -448,6 +449,7 @@ final class AvatarEditorScreenComponent: Component { groupId: "stickers", title: "Stickers", subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -505,6 +507,7 @@ final class AvatarEditorScreenComponent: Component { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -538,6 +541,7 @@ final class AvatarEditorScreenComponent: Component { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 881fdd33fe..84de31e778 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1061,7 +1061,7 @@ private final class CameraScreenComponent: CombinedComponent { .disappear(.default(scale: true)) ) - if !isTablet && Camera.isDualCameraSupported { + if !isTablet && Camera.isDualCameraSupported(forRoundVideo: false) { let dualButton = dualButton.update( component: CameraButton( content: AnyComponentWithIdentity( @@ -1513,7 +1513,7 @@ public class CameraScreen: ViewController { self.previewBlurView = BlurView() self.previewBlurView.isUserInteractionEnabled = false - var isDualCameraEnabled = Camera.isDualCameraSupported + var isDualCameraEnabled = Camera.isDualCameraSupported(forRoundVideo: false) if isDualCameraEnabled { if let isDualCameraEnabledValue = UserDefaults.standard.object(forKey: "TelegramStoryCameraIsDualEnabled") as? NSNumber { isDualCameraEnabled = isDualCameraEnabledValue.boolValue diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 7a9723668c..b4894460ee 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -564,7 +564,12 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { false ) actionButtonMinWidthAndFinalizeLayout = (buttonWidth, continueLayout) - actualWidth = max(actualWidth, buttonWidth) + + var buttonInset: CGFloat = 0.0 + if let buttonIconImage { + buttonInset += buttonIconImage.size.width + 2.0 + } + actualWidth = max(actualWidth, buttonWidth + buttonInset * 2.0) } else { actionButtonMinWidthAndFinalizeLayout = nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift index e61efc2c4d..2b0a3c49a8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift @@ -321,6 +321,11 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } let (addButtonWidth, addContinueLayout) = makeAddButtonLayout(constrainedSize.width, 10.0, nil, false, addTitle.uppercased(), mainColor, false, false) + + let showAddButton = !(!canAdd && canMessage) + let showMessageButton = canMessage + let buttonCount = (showAddButton ? 1 : 0) + (showMessageButton ? 1 : 0) + let maxButtonWidth = max(messageButtonWidth, addButtonWidth) var maxContentWidth: CGFloat = avatarSize.width + 7.0 if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { @@ -328,7 +333,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } maxContentWidth = max(maxContentWidth, 7.0 + avatarSize.width + 7.0 + titleLayout.size.width + 7.0) maxContentWidth = max(maxContentWidth, 7.0 + avatarSize.width + 7.0 + textLayout.size.width + 7.0) - maxContentWidth = max(maxContentWidth, maxButtonWidth * 2.0) + maxContentWidth = max(maxContentWidth, maxButtonWidth * CGFloat(buttonCount)) maxContentWidth = max(maxContentWidth, 220.0) let contentWidth = maxContentWidth + layoutConstants.text.bubbleInsets.right * 2.0 diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index 322df5a485..824d24be99 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -167,6 +167,7 @@ public final class ChatRecentActionsController: TelegramBaseController { }, hideTranslationPanel: { }, openPremiumGift: { }, openPremiumRequiredForMessaging: { + }, openBoostToUnrestrict: { }, updateHistoryFilter: { _ in }, requestLayout: { _ in }, chatController: { diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 2687dc2e3d..addb6a16cc 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -231,7 +231,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let displayGiveawayParticipationStatus: (EngineMessage.Id) -> Void public let openPremiumStatusInfo: (EnginePeer.Id, UIView, Int64?, PeerNameColor) -> Void public let openRecommendedChannelContextMenu: (EnginePeer, UIView, ContextGesture?) -> Void - public let openGroupBoostInfo: (EnginePeer.Id, Int) -> Void + public let openGroupBoostInfo: (EnginePeer.Id?, Int) -> Void public let requestMessageUpdate: (MessageId, Bool) -> Void public let cancelInteractiveKeyboardGestures: () -> Void @@ -354,7 +354,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol displayGiveawayParticipationStatus: @escaping (EngineMessage.Id) -> Void, openPremiumStatusInfo: @escaping (EnginePeer.Id, UIView, Int64?, PeerNameColor) -> Void, openRecommendedChannelContextMenu: @escaping (EnginePeer, UIView, ContextGesture?) -> Void, - openGroupBoostInfo: @escaping (EnginePeer.Id, Int) -> Void, + openGroupBoostInfo: @escaping (EnginePeer.Id?, Int) -> Void, requestMessageUpdate: @escaping (MessageId, Bool) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, dismissTextInput: @escaping () -> Void, diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 8cc36dd5aa..e0a4acfe4a 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -639,7 +639,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } } - if file.isPremiumEmoji && !hasPremium { + if file.isPremiumEmoji && !hasPremium && groupId != AnyHashable("peerSpecific") { var animateInAsReplacement = false if let currentUndoOverlayController = strongSelf.currentUndoOverlayController { currentUndoOverlayController.dismissWithCommitActionAndReplacementAnimation() @@ -959,6 +959,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -1007,6 +1008,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { groupId: AnyHashable(info.id), title: info.title, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -1065,6 +1067,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -1095,6 +1098,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -1365,6 +1369,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -1395,6 +1400,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index f2057cc3e8..bce0b9310a 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -578,6 +578,7 @@ public final class EmojiStatusSelectionController: ViewController { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -632,6 +633,7 @@ public final class EmojiStatusSelectionController: ViewController { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -663,6 +665,7 @@ public final class EmojiStatusSelectionController: ViewController { groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 257035d561..13dc762e1f 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -771,6 +771,8 @@ private final class GroupHeaderLayer: UIView { private var tintSubtitleLayer: SimpleLayer? private var lockIconLayer: SimpleLayer? private var tintLockIconLayer: SimpleLayer? + private var badgeLayer: SimpleLayer? + private var tintBadgeLayer: SimpleLayer? private(set) var clearIconLayer: SimpleLayer? private var tintClearIconLayer: SimpleLayer? private var separatorLayer: SimpleLayer? @@ -816,6 +818,7 @@ private final class GroupHeaderLayer: UIView { actionButtonTitle: String?, title: String, subtitle: String?, + badge: String?, isPremiumLocked: Bool, hasClear: Bool, embeddedItems: [EmojiPagerContentComponent.Item]?, @@ -831,7 +834,7 @@ private final class GroupHeaderLayer: UIView { self.theme = theme themeUpdated = true } - + let needsVibrancy = !theme.overallDarkAppearance || forceNeedsVibrancy let textOffsetY: CGFloat @@ -978,10 +981,72 @@ private final class GroupHeaderLayer: UIView { self.tintTextLayer.isHidden = !needsVibrancy self.currentTextLayout = (title, color, textConstrainedWidth, textSize) } + + var badgeSize: CGSize = .zero + if let badge { + func generateBadgeImage(color: UIColor) -> UIImage? { + let string = NSAttributedString(string: badge, font: Font.semibold(11.0), textColor: .white) + let stringBounds = string.boundingRect(with: CGSize(width: 120, height: 18.0), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil) + + let badgeSize = CGSize(width: stringBounds.width + 8.0, height: 16.0) + return generateImage(badgeSize, opaque: false, scale: 0.0, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(color.cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: badgeSize), cornerRadius: badgeSize.height / 2.0).cgPath) + context.fillPath() + + context.setBlendMode(.clear) + + UIGraphicsPushContext(context) + + string.draw(with: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeSize.width - stringBounds.size.width) / 2.0), y: floorToScreenPixels((badgeSize.height - stringBounds.size.height) / 2.0)), size: stringBounds.size), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil) + + UIGraphicsPopContext() + }) + } + + let badgeLayer: SimpleLayer + if let current = self.badgeLayer { + badgeLayer = current + } else { + badgeLayer = SimpleLayer() + self.badgeLayer = badgeLayer + self.layer.addSublayer(badgeLayer) + + if let image = generateBadgeImage(color: color.withMultipliedAlpha(0.66)) { + badgeLayer.contents = image.cgImage + badgeLayer.bounds = CGRect(origin: .zero, size: image.size) + } + } + badgeSize = badgeLayer.bounds.size + + let tintBadgeLayer: SimpleLayer + if let current = self.tintBadgeLayer { + tintBadgeLayer = current + } else { + tintBadgeLayer = SimpleLayer() + self.tintBadgeLayer = tintBadgeLayer + self.tintContentLayer.addSublayer(tintBadgeLayer) + + if let image = generateBadgeImage(color: .white) { + tintBadgeLayer.contents = image.cgImage + } + } + } else { + if let badgeLayer = self.badgeLayer { + self.badgeLayer = nil + badgeLayer.removeFromSuperlayer() + } + if let tintBadgeLayer = self.tintBadgeLayer { + self.tintBadgeLayer = nil + tintBadgeLayer.removeFromSuperlayer() + } + } let textFrame: CGRect if subtitle == nil { - textFrame = CGRect(origin: CGPoint(x: titleHorizontalOffset + floor((constrainedSize.width - titleHorizontalOffset - textSize.width) / 2.0), y: textOffsetY), size: textSize) + textFrame = CGRect(origin: CGPoint(x: titleHorizontalOffset + floor((constrainedSize.width - titleHorizontalOffset - (textSize.width + badgeSize.width)) / 2.0), y: textOffsetY), size: textSize) } else { textFrame = CGRect(origin: CGPoint(x: titleHorizontalOffset, y: textOffsetY), size: textSize) } @@ -989,6 +1054,11 @@ private final class GroupHeaderLayer: UIView { self.tintTextLayer.frame = textFrame self.tintTextLayer.isHidden = !needsTintText + if let badgeLayer = self.badgeLayer, let tintBadgeLayer = self.tintBadgeLayer { + badgeLayer.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 4.0, y: 0.0), size: badgeLayer.frame.size) + tintBadgeLayer.frame = badgeLayer.frame + } + if isPremiumLocked { let lockIconLayer: SimpleLayer if let current = self.lockIconLayer { @@ -2536,6 +2606,7 @@ public final class EmojiPagerContentComponent: Component { public let groupId: AnyHashable public let title: String? public let subtitle: String? + public let badge: String? public let actionButtonTitle: String? public let isFeatured: Bool public let isPremiumLocked: Bool @@ -2553,6 +2624,7 @@ public final class EmojiPagerContentComponent: Component { groupId: AnyHashable, title: String?, subtitle: String?, + badge: String?, actionButtonTitle: String?, isFeatured: Bool, isPremiumLocked: Bool, @@ -2569,6 +2641,7 @@ public final class EmojiPagerContentComponent: Component { self.groupId = groupId self.title = title self.subtitle = subtitle + self.badge = badge self.actionButtonTitle = actionButtonTitle self.isFeatured = isFeatured self.isPremiumLocked = isPremiumLocked @@ -2598,6 +2671,9 @@ public final class EmojiPagerContentComponent: Component { if lhs.subtitle != rhs.subtitle { return false } + if lhs.badge != rhs.badge { + return false + } if lhs.actionButtonTitle != rhs.actionButtonTitle { return false } @@ -5540,6 +5616,7 @@ public final class EmojiPagerContentComponent: Component { actionButtonTitle: actionButtonTitle, title: title, subtitle: itemGroup.subtitle, + badge: itemGroup.badge, isPremiumLocked: itemGroup.isPremiumLocked, hasClear: itemGroup.hasClear, embeddedItems: itemGroup.isEmbedded ? itemGroup.items : nil, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift index cd246afb39..b6668cda80 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift @@ -208,6 +208,7 @@ public extension EmojiPagerContentComponent { var id: AnyHashable var title: String? var subtitle: String? + var badge: String? var isPremiumLocked: Bool var isFeatured: Bool var collapsedLineCount: Int? @@ -241,7 +242,7 @@ public extension EmojiPagerContentComponent { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleEmoji, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleEmoji, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) } } } @@ -317,7 +318,7 @@ public extension EmojiPagerContentComponent { itemGroupIndexById[groupId] = itemGroups.count let title = context.sharedContext.currentPresentationData.with({ $0 }).strings.EmojiInput_TrendingEmoji - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 0, isClearable: false, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 0, isClearable: false, headerItem: nil, items: [resultItem])) } } } @@ -373,7 +374,7 @@ public extension EmojiPagerContentComponent { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) } var existingIds = Set() @@ -432,7 +433,7 @@ public extension EmojiPagerContentComponent { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: topStatusTitle?.uppercased(), subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: topStatusTitle?.uppercased(), subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) } var existingIds = Set() @@ -592,7 +593,7 @@ public extension EmojiPagerContentComponent { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: topStatusTitle?.uppercased(), subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: topStatusTitle?.uppercased(), subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) } var existingIds = Set() @@ -738,7 +739,7 @@ public extension EmojiPagerContentComponent { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) } } } @@ -816,7 +817,7 @@ public extension EmojiPagerContentComponent { } } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) } } @@ -874,7 +875,7 @@ public extension EmojiPagerContentComponent { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: hasRecent && subject != .quickReaction, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: hasRecent && subject != .quickReaction, headerItem: nil, items: [resultItem])) } } else { let groupId = "recent" @@ -886,7 +887,7 @@ public extension EmojiPagerContentComponent { } } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) } } } @@ -958,7 +959,7 @@ public extension EmojiPagerContentComponent { popularInsertIndex += 1 } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: hasRecent && subject != .quickReaction, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: hasRecent && subject != .quickReaction, headerItem: nil, items: [resultItem])) } } } @@ -1023,7 +1024,7 @@ public extension EmojiPagerContentComponent { } } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) } } } else if [.profilePhoto, .groupPhoto].contains(subject) { @@ -1031,7 +1032,7 @@ public extension EmojiPagerContentComponent { let groupId = "recent" itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: topStatusTitle?.uppercased(), subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: topStatusTitle?.uppercased(), subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [])) if let featuredAvatarEmoji = featuredAvatarEmoji { for item in featuredAvatarEmoji.items { @@ -1100,7 +1101,7 @@ public extension EmojiPagerContentComponent { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) } if let featuredBackgroundIconEmoji { @@ -1208,15 +1209,11 @@ public extension EmojiPagerContentComponent { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Emoji_FrequentlyUsed, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: true, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Emoji_FrequentlyUsed, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: true, headerItem: nil, items: [resultItem])) } } } - - if !hasPremium { - maybeAppendUnicodeEmoji() - } - + var itemCollectionMapping: [ItemCollectionId: StickerPackCollectionInfo] = [:] for (id, info, _) in view.collectionInfos { if let info = info as? StickerPackCollectionInfo { @@ -1260,13 +1257,18 @@ public extension EmojiPagerContentComponent { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: peerSpecificPack.peer.compactDisplayTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) + //TODO:localize + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: peerSpecificPack.info.title, subtitle: nil, badge: "GROUP EMOJI", isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) } } let supergroupId: AnyHashable = peerSpecificPack.info.id skippedCollectionIds.insert(supergroupId) } + + if !hasPremium { + maybeAppendUnicodeEmoji() + } if areCustomEmojiEnabled { for entry in view.entries { @@ -1364,7 +1366,7 @@ public extension EmojiPagerContentComponent { break inner } } - itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: headerItem, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: title, subtitle: nil, badge: nil, isPremiumLocked: isPremiumLocked, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: headerItem, items: [resultItem])) } } @@ -1453,7 +1455,7 @@ public extension EmojiPagerContentComponent { isFeatured = false } - itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: isFeatured, collapsedLineCount: 3, isClearable: false, headerItem: headerItem, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, subtitle: nil, badge: nil, isPremiumLocked: isPremiumLocked, isFeatured: isFeatured, collapsedLineCount: 3, isClearable: false, headerItem: headerItem, items: [resultItem])) } } } @@ -1508,6 +1510,7 @@ public extension EmojiPagerContentComponent { groupId: group.id, title: group.title, subtitle: group.subtitle, + badge: group.badge, actionButtonTitle: nil, isFeatured: group.isFeatured, isPremiumLocked: group.isPremiumLocked, @@ -1994,6 +1997,7 @@ public extension EmojiPagerContentComponent { groupId: group.id, title: group.title, subtitle: group.subtitle, + badge: nil, actionButtonTitle: group.actionButtonTitle, isFeatured: group.isFeatured, isPremiumLocked: group.isPremiumLocked, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift index cc21a8caa1..6307c7d3d0 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift @@ -122,6 +122,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode groupId: AnyHashable(groupItem.info.id), title: groupItem.info.title, subtitle: nil, + badge: nil, actionButtonTitle: self.presentationData.strings.EmojiInput_AddPack(groupItem.info.title).string, isFeatured: true, isPremiumLocked: !self.hasPremiumForInstallation, @@ -286,6 +287,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -340,6 +342,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, @@ -373,6 +376,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode groupId: "search", title: nil, subtitle: nil, + badge: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, diff --git a/submodules/TelegramUI/Components/GroupStickerPackSetupController/BUILD b/submodules/TelegramUI/Components/GroupStickerPackSetupController/BUILD index 00b92ec4b5..8122488eeb 100644 --- a/submodules/TelegramUI/Components/GroupStickerPackSetupController/BUILD +++ b/submodules/TelegramUI/Components/GroupStickerPackSetupController/BUILD @@ -20,6 +20,9 @@ swift_library( "//submodules/AccountContext:AccountContext", "//submodules/StickerPackPreviewUI:StickerPackPreviewUI", "//submodules/ItemListStickerPackItem:ItemListStickerPackItem", + "//submodules/SearchBarNode:SearchBarNode", + "//submodules/SearchUI:SearchUI", + "//submodules/MergeLists:MergeLists", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackSetupController.swift b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackSetupController.swift index 0c13db0b1c..e730dba087 100644 --- a/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackSetupController.swift +++ b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackSetupController.swift @@ -259,6 +259,7 @@ private enum GroupStickerPackSearchState: Equatable { private struct GroupStickerPackSetupControllerState: Equatable { var isSaving: Bool + var searchingPacks: Bool } private func groupStickerPackSetupControllerEntries(context: AccountContext, presentationData: PresentationData, searchText: String, view: CombinedView, initialData: InitialStickerPackData?, searchState: GroupStickerPackSearchState, stickerSettings: StickerSettings, emoji: Bool) -> [GroupStickerPackEntry] { @@ -276,10 +277,10 @@ private func groupStickerPackSetupControllerEntries(context: AccountContext, pre case .searching: entries.append(.currentPack(0, presentationData.theme, presentationData.strings, .searching)) case let .found(data): - entries.append(.currentPack(0, presentationData.theme, presentationData.strings, .found(packInfo: data.info, topItem: data.item, subtitle: presentationData.strings.StickerPack_StickerCount(data.info.count)))) + entries.append(.currentPack(0, presentationData.theme, presentationData.strings, .found(packInfo: data.info, topItem: data.item, subtitle: emoji ? presentationData.strings.StickerPack_EmojiCount(data.info.count) : presentationData.strings.StickerPack_StickerCount(data.info.count)))) } entries.append(.searchInfo(presentationData.theme, emoji ? "All members will be able to use these emoji in the group, even if they don't have Telegram Premium." : presentationData.strings.Channel_Stickers_CreateYourOwn)) - entries.append(.packsTitle(presentationData.theme, emoji ? "CHOOSE FROM YOUR EMOJI" : presentationData.strings.Channel_Stickers_YourStickers)) + entries.append(.packsTitle(presentationData.theme, emoji ? "CHOOSE EMOJI PACK" : presentationData.strings.Channel_Stickers_YourStickers)) let namespace = emoji ? Namespaces.ItemCollection.CloudEmojiPacks : Namespaces.ItemCollection.CloudStickerPacks if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [namespace])] as? ItemCollectionInfosView { @@ -291,7 +292,15 @@ private func groupStickerPackSetupControllerEntries(context: AccountContext, pre if case let .found(found) = searchState { selected = found.info.id == info.id } - entries.append(.pack(index, presentationData.theme, presentationData.strings, info, entry.firstItem as? StickerPackItem, presentationData.strings.StickerPack_StickerCount(info.count == 0 ? entry.count : info.count), context.sharedContext.energyUsageSettings.loopStickers, selected)) + let count = info.count == 0 ? entry.count : info.count + + let thumbnail: StickerPackItem? + if let thumbnailRep = info.thumbnail { + thumbnail = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: thumbnailRep.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: info.immediateThumbnailData, mimeType: "", size: nil, attributes: []), indexKeys: []) + } else { + thumbnail = entry.firstItem as? StickerPackItem + } + entries.append(.pack(index, presentationData.theme, presentationData.strings, info, thumbnail, emoji ? presentationData.strings.StickerPack_EmojiCount(count) : presentationData.strings.StickerPack_StickerCount(count), context.sharedContext.energyUsageSettings.loopStickers, selected)) index += 1 } } @@ -302,7 +311,7 @@ private func groupStickerPackSetupControllerEntries(context: AccountContext, pre } public func groupStickerPackSetupController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, emoji: Bool = false, currentPackInfo: StickerPackCollectionInfo?, completion: ((StickerPackCollectionInfo?) -> Void)? = nil) -> ViewController { - let initialState = GroupStickerPackSetupControllerState(isSaving: false) + let initialState = GroupStickerPackSetupControllerState(isSaving: false, searchingPacks: false) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) @@ -433,10 +442,15 @@ public func groupStickerPackSetupController(context: AccountContext, updatedPres }) } - var rightNavigationButton: ItemListNavigationButton? if let _ = completion { - rightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: { + updateState { state in + var updatedState = state + updatedState.searchingPacks = true + return updatedState + } + }) } else { if initialData != nil { if state.isSaving { @@ -467,7 +481,7 @@ public func groupStickerPackSetupController(context: AccountContext, updatedPres return state } saveDisposable.set((context.engine.peers.updateGroupSpecificStickerset(peerId: peerId, info: info) - |> deliverOnMainQueue).start(error: { _ in + |> deliverOnMainQueue).start(error: { _ in updateState { state in var state = state state.isSaving = false @@ -483,6 +497,21 @@ public func groupStickerPackSetupController(context: AccountContext, updatedPres } } + var searchItem: ItemListControllerSearch? + if state.searchingPacks { + searchItem = GroupStickerSearchItem(context: context, cancel: { + updateState { state in + var updatedState = state + updatedState.searchingPacks = false + return updatedState + } + }, select: { pack in + arguments.selectStickerPack(pack) + }, dismissInput: { + dismissInputImpl?() + }) + } + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(emoji ? "Group Emoji Pack" : presentationData.strings.Channel_Info_Stickers), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let hasData = initialData != nil @@ -493,7 +522,7 @@ public func groupStickerPackSetupController(context: AccountContext, updatedPres emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: groupStickerPackSetupControllerEntries(context: context, presentationData: presentationData, searchText: searchState.0, view: view, initialData: initialData, searchState: searchState.1, stickerSettings: stickerSettings, emoji: emoji), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: hasData && hadData) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: groupStickerPackSetupControllerEntries(context: context, presentationData: presentationData, searchText: searchState.0, view: view, initialData: initialData, searchState: searchState.1, stickerSettings: stickerSettings, emoji: emoji), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: hasData && hadData) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() diff --git a/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerSearchContainerNode.swift b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerSearchContainerNode.swift new file mode 100644 index 0000000000..89ea9480b3 --- /dev/null +++ b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerSearchContainerNode.swift @@ -0,0 +1,328 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import MergeLists +import AccountContext +import SearchUI +import ItemListUI +import ItemListStickerPackItem + +private final class GroupStickerSearchContainerInteraction { + let packSelected: (StickerPackCollectionInfo) -> Void + + init(packSelected: @escaping (StickerPackCollectionInfo) -> Void) { + self.packSelected = packSelected + } +} + +private final class GroupStickerSearchEntry: Comparable, Identifiable { + let index: Int + let pack: StickerPackCollectionInfo + let topItem: ItemCollectionItem? + + init(index: Int, pack: StickerPackCollectionInfo, topItem: ItemCollectionItem?) { + self.index = index + self.pack = pack + self.topItem = topItem + } + + var stableId: ItemCollectionId { + return self.pack.id + } + + static func ==(lhs: GroupStickerSearchEntry, rhs: GroupStickerSearchEntry) -> Bool { + return lhs.index == rhs.index && lhs.pack == rhs.pack + } + + static func <(lhs: GroupStickerSearchEntry, rhs: GroupStickerSearchEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(context: AccountContext, presentationData: PresentationData, interaction: GroupStickerSearchContainerInteraction) -> ListViewItem { + let pack = self.pack + let count = presentationData.strings.StickerPack_EmojiCount(pack.count) + + return ItemListStickerPackItem(presentationData: ItemListPresentationData(presentationData), context: context, packInfo: pack, itemCount: count, topItem: self.topItem as? StickerPackItem, unread: false, control: .none, editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false, reorderable: false, selectable: false), enabled: true, playAnimatedStickers: true, style: .plain, sectionId: 0, action: { + interaction.packSelected(pack) + }, setPackIdWithRevealedOptions: { _, _ in + }, addPack: { + }, removePack: { + }, toggleSelected: { + }) + } +} + +struct GroupStickerSearchContainerTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let isSearching: Bool + let isEmpty: Bool + let query: String +} + +private func groupStickerSearchContainerPreparedRecentTransition(from fromEntries: [GroupStickerSearchEntry], to toEntries: [GroupStickerSearchEntry], isSearching: Bool, isEmpty: Bool, query: String, context: AccountContext, presentationData: PresentationData, interaction: GroupStickerSearchContainerInteraction) -> GroupStickerSearchContainerTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } + + return GroupStickerSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching, isEmpty: isEmpty, query: query) +} + +public final class GroupStickerSearchContainerNode: SearchDisplayControllerContentNode { + private let context: AccountContext + private let packSelected: (StickerPackCollectionInfo) -> Void + + private let listNode: ListView + + private let emptyResultsTitleNode: ImmediateTextNode + private let emptyResultsTextNode: ImmediateTextNode + + private var enqueuedTransitions: [(GroupStickerSearchContainerTransition, Bool)] = [] + private var validLayout: (ContainerViewLayout, CGFloat)? + + private let searchQuery = Promise() + private let searchDisposable = MetaDisposable() + + private let forceTheme: PresentationTheme? + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private let removeMemberDisposable = MetaDisposable() + + private let presentationDataPromise: Promise + + private var _hasDim: Bool = false + override public var hasDim: Bool { + return _hasDim + } + + public init(context: AccountContext, forceTheme: PresentationTheme?, packSelected: @escaping (StickerPackCollectionInfo) -> Void, updateActivity: @escaping (Bool) -> Void, pushController: @escaping (ViewController) -> Void) { + self.context = context + self.packSelected = packSelected + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationData = presentationData + + self.forceTheme = forceTheme + if let forceTheme = self.forceTheme { + self.presentationData = self.presentationData.withUpdated(theme: forceTheme) + } + self.presentationDataPromise = Promise(self.presentationData) + + self.listNode = ListView() + self.listNode.accessibilityPageScrolledString = { row, count in + return presentationData.strings.VoiceOver_ScrollStatus(row, count).string + } + + self.emptyResultsTitleNode = ImmediateTextNode() + self.emptyResultsTitleNode.displaysAsynchronously = false + self.emptyResultsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.ChatList_Search_NoResults, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor) + self.emptyResultsTitleNode.textAlignment = .center + self.emptyResultsTitleNode.isHidden = true + + self.emptyResultsTextNode = ImmediateTextNode() + self.emptyResultsTextNode.displaysAsynchronously = false + self.emptyResultsTextNode.maximumNumberOfLines = 0 + self.emptyResultsTextNode.textAlignment = .center + self.emptyResultsTextNode.isHidden = true + + super.init() + + self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor + self.listNode.isHidden = true + + self._hasDim = true + + self.addSubnode(self.listNode) + + self.addSubnode(self.emptyResultsTitleNode) + self.addSubnode(self.emptyResultsTextNode) + + let interaction = GroupStickerSearchContainerInteraction(packSelected: { [weak self] pack in + packSelected(pack) + self?.listNode.clearHighlightAnimated(true) + }) + + let foundItems = self.searchQuery.get() + |> mapToSignal { query -> Signal<[GroupStickerSearchEntry]?, NoError> in + guard let query, !query.isEmpty else { + return .single(nil) + } + return context.engine.stickers.searchEmojiSets(query: query) + |> mapToSignal { localResult in + return context.engine.stickers.searchEmojiSetsRemotely(query: query) + |> map { remoteResult -> [GroupStickerSearchEntry]? in + let mergedResult = localResult.merge(with: remoteResult) + var entries: [GroupStickerSearchEntry] = [] + var index = 0 + for info in mergedResult.infos { + if let pack = info.1 as? StickerPackCollectionInfo { + entries.append(GroupStickerSearchEntry(index: index, pack: pack, topItem: info.2)) + index += 1 + } + } + return entries + } + } + } + + let previousSearchItems = Atomic<[GroupStickerSearchEntry]?>(value: nil) + self.searchDisposable.set((combineLatest(self.searchQuery.get(), foundItems, self.presentationDataPromise.get()) + |> deliverOnMainQueue).start(next: { [weak self] query, entries, presentationData in + if let strongSelf = self { + let previousEntries = previousSearchItems.swap(entries) + updateActivity(false) + let firstTime = previousEntries == nil + let transition = groupStickerSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, isEmpty: entries?.isEmpty ?? false, query: query ?? "", context: context, presentationData: presentationData, interaction: interaction) + strongSelf.enqueueTransition(transition, firstTime: firstTime) + } + })) + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + var presentationData = presentationData + + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + if let forceTheme = strongSelf.forceTheme { + presentationData = presentationData.withUpdated(theme: forceTheme) + } + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) + } + } + }) + + self.listNode.beganInteractiveDragging = { [weak self] _ in + self?.dismissInput?() + } + } + + deinit { + self.searchDisposable.dispose() + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.listNode.backgroundColor = theme.chatList.backgroundColor + } + + override public func searchTextUpdated(text: String) { + if text.isEmpty { + self.searchQuery.set(.single(nil)) + } else { + self.searchQuery.set(.single(text)) + } + } + + private func enqueueTransition(_ transition: GroupStickerSearchContainerTransition, firstTime: Bool) { + enqueuedTransitions.append((transition, firstTime)) + + if let _ = self.validLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + if let (transition, firstTime) = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + options.insert(.PreferSynchronousDrawing) + options.insert(.PreferSynchronousResourceLoading) + if firstTime { + } else { + //options.insert(.AnimateAlpha) + } + + let isSearching = transition.isSearching + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + + strongSelf.listNode.isHidden = !isSearching + + strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(transition.query).string, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + + let emptyResults = transition.isSearching && transition.isEmpty + strongSelf.emptyResultsTitleNode.isHidden = !emptyResults + strongSelf.emptyResultsTextNode.isHidden = !emptyResults + + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + }) + } + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + let hadValidLayout = self.validLayout == nil + self.validLayout = (layout, navigationBarHeight) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + + var insets = layout.insets(options: [.input]) + insets.top += navigationBarHeight + insets.left += layout.safeInsets.left + insets.right += layout.safeInsets.right + + self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + let padding: CGFloat = 16.0 + let emptyTitleSize = self.emptyResultsTitleNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) + let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) + + let emptyTextSpacing: CGFloat = 8.0 + let emptyTotalHeight = emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing + let emptyTitleY = navigationBarHeight + floorToScreenPixels((layout.size.height - navigationBarHeight - max(insets.bottom, layout.intrinsicInsets.bottom) - emptyTotalHeight) / 2.0) + + transition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyTitleY), size: emptyTitleSize)) + transition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyTitleY + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) + + if !hadValidLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + override public func scrollToTop() { + 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 }) + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancel?() + } + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = self.view.hitTest(point, with: event) else { + return nil + } + if result === self.view { + return nil + } + return result + } +} diff --git a/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerSearchItem.swift b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerSearchItem.swift new file mode 100644 index 0000000000..e22fd30031 --- /dev/null +++ b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerSearchItem.swift @@ -0,0 +1,119 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import ItemListUI +import PresentationDataUtils +import AccountContext + +final class GroupStickerSearchItem: ItemListControllerSearch { + let context: AccountContext + let cancel: () -> Void + let select: (StickerPackCollectionInfo) -> Void + let dismissInput: () -> Void + + private var updateActivity: ((Bool) -> Void)? + private var activity: ValuePromise = ValuePromise(ignoreRepeated: false) + private let activityDisposable = MetaDisposable() + + init( + context: AccountContext, + cancel: @escaping () -> Void, + select: @escaping (StickerPackCollectionInfo) -> Void, + dismissInput: @escaping () -> Void + ) { + self.context = context + self.cancel = cancel + self.select = select + self.dismissInput = dismissInput + self.activityDisposable.set((self.activity.get() |> mapToSignal { value -> Signal in + if value { + return .single(value) |> delay(0.2, queue: Queue.mainQueue()) + } else { + return .single(value) + } + }).start(next: { [weak self] value in + self?.updateActivity?(value) + })) + } + + deinit { + self.activityDisposable.dispose() + } + + func isEqual(to: ItemListControllerSearch) -> Bool { + if let to = to as? GroupStickerSearchItem { + if self.context !== to.context { + return false + } + return true + } else { + return false + } + } + + func titleContentNode(current: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> NavigationBarContentNode & ItemListControllerSearchNavigationContentNode { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + if let current = current as? GroupStickerSearchNavigationContentNode { + current.updateTheme(presentationData.theme) + return current + } else { + return GroupStickerSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, cancel: self.cancel, updateActivity: { [weak self] value in + self?.updateActivity = value + }) + } + } + + func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode { + return GroupStickerSearchItemNode(context: self.context, packSelected: self.select, cancel: self.cancel, updateActivity: { [weak self] value in + self?.activity.set(value) + }, pushController: { c in + + }, dismissInput: self.dismissInput) + } +} + +private final class GroupStickerSearchItemNode: ItemListControllerSearchNode { + private let containerNode: GroupStickerSearchContainerNode + + init(context: AccountContext, packSelected: @escaping (StickerPackCollectionInfo) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, pushController: @escaping (ViewController) -> Void, dismissInput: @escaping () -> Void) { + self.containerNode = GroupStickerSearchContainerNode(context: context, forceTheme: nil, packSelected: { pack in + packSelected(pack) + cancel() + }, updateActivity: updateActivity, pushController: pushController) + self.containerNode.cancel = { + cancel() + } + + super.init() + + self.addSubnode(self.containerNode) + + self.containerNode.dismissInput = { + dismissInput() + } + } + + override func queryUpdated(_ query: String) { + self.containerNode.searchTextUpdated(text: query) + } + + override func scrollToTop() { + self.containerNode.scrollToTop() + } + + override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight))) + self.containerNode.containerLayoutUpdated(layout.withUpdatedSize(CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight)), navigationBarHeight: 0.0, transition: transition) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let result = self.containerNode.hitTest(self.view.convert(point, to: self.containerNode.view), with: event) { + return result + } + + return super.hitTest(point, with: event) + } +} diff --git a/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerSearchNavigationContentNode.swift b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerSearchNavigationContentNode.swift new file mode 100644 index 0000000000..408019f90b --- /dev/null +++ b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerSearchNavigationContentNode.swift @@ -0,0 +1,88 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import SearchBarNode + +private let searchBarFont = Font.regular(17.0) + +final class GroupStickerSearchNavigationContentNode: NavigationBarContentNode, ItemListControllerSearchNavigationContentNode { + private var theme: PresentationTheme + private let strings: PresentationStrings + + private let cancel: () -> Void + + private let searchBar: SearchBarNode + + private var queryUpdated: ((String) -> Void)? + var activity: Bool = false { + didSet { + self.searchBar.activity = activity + } + } + init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void, updateActivity: @escaping(@escaping(Bool)->Void) -> Void) { + self.theme = theme + self.strings = strings + + self.cancel = cancel + + self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern, displayBackground: false) + + super.init() + + self.addSubnode(self.searchBar) + + self.searchBar.cancel = { [weak self] in + self?.searchBar.deactivate(clear: false) + self?.cancel() + } + + self.searchBar.textUpdated = { [weak self] query, _ in + self?.queryUpdated?(query) + } + + updateActivity({ [weak self] value in + self?.activity = value + }) + + self.updatePlaceholder() + } + + func setQueryUpdated(_ f: @escaping (String) -> Void) { + self.queryUpdated = f + } + + func updateTheme(_ theme: PresentationTheme) { + self.theme = theme + self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: self.theme), strings: self.strings) + self.updatePlaceholder() + } + + func updatePlaceholder() { + self.searchBar.placeholderString = NSAttributedString(string: self.strings.Common_Search, font: searchBarFont, textColor: self.theme.rootController.navigationSearchBar.inputPlaceholderTextColor) + } + + override var nominalHeight: CGFloat { + return 54.0 + } + + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { + let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 54.0)) + self.searchBar.frame = searchBarFrame + self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition) + } + + func activate() { + self.searchBar.activate() + } + + func deactivate() { + self.searchBar.deactivate(clear: false) + } +} + diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift index 3081218bc0..92015ddc47 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift @@ -14,6 +14,10 @@ private func fullEntityMediaPath(_ path: String) -> String { } public final class DrawingStickerEntity: DrawingEntity, Codable { + public enum DecodingError: Error { + case generic + } + public enum Content: Equatable { public enum ImageType: Equatable { case sticker @@ -262,7 +266,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { } else if let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .videoFile) { self.content = .video(file) } else { - fatalError() + throw DrawingStickerEntity.DecodingError.generic } self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) self.position = try container.decode(CGPoint.self, forKey: .position) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 40fcce5064..33e0a5fd38 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -414,6 +414,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, hideTranslationPanel: { }, openPremiumGift: { }, openPremiumRequiredForMessaging: { + }, openBoostToUnrestrict: { }, updateHistoryFilter: { _ in }, requestLayout: { _ in }, chatController: { diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index d082f509c0..f9fd1e1fa5 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -728,6 +728,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, hideTranslationPanel: { }, openPremiumGift: { }, openPremiumRequiredForMessaging: { + }, openBoostToUnrestrict: { }, updateHistoryFilter: { _ in }, requestLayout: { _ in }, chatController: { diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index f0bfb15166..6906d2abfd 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -1461,11 +1461,24 @@ final class ShareWithPeersScreenComponent: Component { } if case .channels = component.stateContext.subject { if case let .channel(channel) = peer, channel.addressName == nil, index == nil { + let title: String + let text: String + + switch channel.info { + case .broadcast: + title = environment.strings.BoostGift_Channels_PrivateChannel_Title + text = environment.strings.BoostGift_Channels_PrivateChannel_Text + case .group: + //TODO:localize + title = "Group is Private" + text = "Are you sure you want to add a private group? Users won't be able to join it without an invite link." + } + let alertController = textAlertController( context: component.context, forceTheme: environment.theme, - title: environment.strings.BoostGift_Channels_PrivateChannel_Title, - text: environment.strings.BoostGift_Channels_PrivateChannel_Text, + title: title, + text: text, actions: [ TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.BoostGift_Channels_PrivateChannel_Add, action: { @@ -2382,9 +2395,10 @@ final class ShareWithPeersScreenComponent: Component { subtitle = environment.strings.BoostGift_Subscribers_Subtitle("\(10)").string actionButtonTitle = environment.strings.BoostGift_Subscribers_Save case .channels: - title = environment.strings.BoostGift_Channels_Title - subtitle = environment.strings.BoostGift_Channels_Subtitle("\(component.context.userLimits.maxGiveawayChannelsCount)").string - actionButtonTitle = environment.strings.BoostGift_Channels_Save + //TODO:localize + title = "Add Groups or Channels"// environment.strings.BoostGift_Channels_Title + subtitle = "select up to \(component.context.userLimits.maxGiveawayChannelsCount) groups or channels" //environment.strings.BoostGift_Channels_Subtitle("\(component.context.userLimits.maxGiveawayChannelsCount)").string + actionButtonTitle = "Save Groups and Channels" // environment.strings.BoostGift_Channels_Save } let titleComponent: AnyComponent diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift index 89d1bdf7a2..715bad782b 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift @@ -640,7 +640,7 @@ public extension ShareWithPeersScreen { } for item in searchResults { - if let peer = item.peer, case let .channel(channel) = peer, case .broadcast = channel.info { + if let peer = item.peer, case .channel = peer { selectedPeers.append(peer) existingIds.insert(peer.id) } @@ -672,7 +672,7 @@ public extension ShareWithPeersScreen { if self.initialPeerIds.contains(peer.id) { return false } - if case let .channel(channel) = peer, case .broadcast = channel.info { + if case .channel = peer { return true } return false diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index f83e5aaeba..1419496161 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -292,6 +292,7 @@ public class ImmediateTextNodeWithEntities: TextNode { public var displaySpoilers = false public var displaySpoilerEffect = true public var spoilerColor: UIColor = .black + public var balancedTextLayout: Bool = false private var enableLooping: Bool = true diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift index 5f58798e50..dc23023e42 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift @@ -621,7 +621,7 @@ public class VideoMessageCameraScreen: ViewController { self.previewContainerView = UIView() self.previewContainerView.clipsToBounds = true - let isDualCameraEnabled = Camera.isDualCameraSupported + let isDualCameraEnabled = Camera.isDualCameraSupported(forRoundVideo: true) let isFrontPosition = "".isEmpty self.mainPreviewView = CameraSimplePreviewView(frame: .zero, main: true, roundVideo: true) diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostLarge.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostLarge.imageset/Contents.json new file mode 100644 index 0000000000..23d4f8e500 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostLarge.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "boosts.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostLarge.imageset/boosts.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BoostLarge.imageset/boosts.pdf new file mode 100644 index 0000000000..f9c4e98c98 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostLarge.imageset/boosts.pdf @@ -0,0 +1,100 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 28.934570 23.770996 cm +0.000000 0.000000 0.000000 scn +19.893095 16.420483 m +19.893103 16.420439 19.893110 16.420393 19.893118 16.420349 c +24.322737 22.894407 l +25.011452 23.900990 24.290668 25.267513 23.071022 25.267513 c +16.783052 25.267513 l +16.020958 25.267513 15.406286 25.824291 15.287010 26.530622 c +15.286899 26.530460 l +15.260291 26.687939 15.258309 26.852856 15.284810 27.020691 c +17.311213 39.854584 l +17.563419 41.451893 15.474540 42.282173 14.561391 40.947567 c +0.267549 20.056564 l +-0.421166 19.049982 0.299617 17.683458 1.519263 17.683458 c +7.807235 17.683458 l +8.739199 17.683458 9.450692 16.850807 9.305341 15.930248 c +7.278937 3.096359 l +7.026731 1.499050 9.115610 0.668770 10.028760 2.003372 c +19.893095 16.420483 l +h +19.104115 28.767513 m +19.790094 33.112053 l +25.151236 40.947567 l +26.064384 42.282173 28.153263 41.451893 27.901056 39.854580 c +26.002724 27.831802 l +25.198483 28.413969 24.199286 28.767513 23.071022 28.767513 c +19.104115 28.767513 l +h +27.211313 20.918013 m +28.156090 22.298841 28.298967 23.884453 27.854807 25.267483 c +33.660732 25.267483 l +34.880375 25.267483 35.601158 23.900959 34.912445 22.894377 c +20.618603 2.003372 l +19.705454 0.668770 17.616575 1.499050 17.868782 3.096359 c +18.724150 8.513695 l +27.211313 20.918013 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1339 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 90.000000 90.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001429 00000 n +0000001452 00000 n +0000001625 00000 n +0000001699 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1758 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Perk/LastSeen.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Perk/LastSeen.imageset/Contents.json new file mode 100644 index 0000000000..443d8f2b1f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Perk/LastSeen.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "lastseen_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Perk/LastSeen.imageset/lastseen_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Perk/LastSeen.imageset/lastseen_30.pdf new file mode 100644 index 0000000000..2087b89da7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Perk/LastSeen.imageset/lastseen_30.pdf @@ -0,0 +1,169 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 30.000000 30.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.320312 4.036133 cm +0.000000 0.000000 0.000000 scn +15.718013 7.249816 m +15.405374 7.990487 12.549557 9.993635 11.723054 10.108183 c +11.210857 10.179170 10.094700 8.757028 9.820968 7.139155 c +9.652993 6.146357 9.900538 5.153749 10.575783 4.550840 c +12.351411 2.965429 16.030653 6.509145 15.718013 7.249816 c +h +20.036879 19.735321 m +18.253841 20.338758 14.037626 18.223207 12.869061 12.033148 c +12.869061 11.732358 15.300022 9.989953 16.974861 9.388252 c +17.829952 9.081054 20.971378 12.265821 21.642756 15.454392 c +22.042732 17.354000 20.948334 19.426855 20.036879 19.735321 c +h +5.331334 5.154034 m +4.841030 5.573311 1.180003 6.640569 0.478038 6.303137 c +-0.223926 5.965705 -0.489510 1.657463 2.111791 1.081186 c +4.713092 0.504911 5.821639 4.734756 5.331334 5.154034 c +h +1.053483 8.698494 m +1.543788 8.279216 5.287134 6.983948 5.989099 7.321380 c +6.691063 7.658812 11.170075 16.921753 6.587785 17.884901 c +5.138152 18.189598 2.305758 16.301899 1.450079 13.825531 c +0.615815 11.411140 0.811438 8.905476 1.053483 8.698494 c +h +f* +n +Q + +endstream +endobj + +2 0 obj + 1098 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 30.000000 30.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 18.799999 m +0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c +1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c +5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c +18.799999 30.000000 l +22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c +27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c +30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c +30.000000 11.200001 l +30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c +28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c +24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c +11.200000 0.000000 l +7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c +2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c +0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c +0.000000 18.799999 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 944 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 46 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000001356 00000 n +0000001379 00000 n +0000002571 00000 n +0000002593 00000 n +0000002891 00000 n +0000002993 00000 n +0000003014 00000 n +0000003187 00000 n +0000003261 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +3321 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Perk/MessagePrivacy.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Perk/MessagePrivacy.imageset/Contents.json new file mode 100644 index 0000000000..fe71658b83 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Perk/MessagePrivacy.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "messageprivacy_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Perk/MessagePrivacy.imageset/messageprivacy_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Perk/MessagePrivacy.imageset/messageprivacy_30.pdf new file mode 100644 index 0000000000..7a037b9846 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Perk/MessagePrivacy.imageset/messageprivacy_30.pdf @@ -0,0 +1,185 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 30.000000 30.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.000000 4.484375 cm +0.000000 0.000000 0.000000 scn +10.000000 20.135742 m +15.522848 20.135742 20.000000 16.065603 20.000000 11.044833 c +20.000000 6.024063 15.522848 1.953924 10.000000 1.953924 c +9.153261 1.953924 8.331102 2.049595 7.545889 2.229696 c +7.397357 2.263765 7.228708 2.107983 6.947171 1.847927 c +6.606689 1.533424 6.101101 1.066412 5.266314 0.598253 c +4.199766 0.000120 2.722059 0.052011 2.471971 0.156912 c +2.232201 0.257484 2.416753 0.457399 2.741760 0.809465 c +2.966608 1.053034 3.258681 1.369423 3.523984 1.776117 c +4.172771 2.770672 3.904685 3.954702 3.613619 4.167799 c +1.337573 5.834144 0.000000 8.181331 0.000000 11.044833 c +0.000000 16.065603 4.477152 20.135742 10.000000 20.135742 c +h +6.335000 14.010747 m +6.335000 16.034870 7.975876 17.675747 10.000000 17.675747 c +12.024124 17.675747 13.665000 16.034870 13.665000 14.010747 c +13.665000 12.722709 l +14.121655 12.482201 14.492952 12.106080 14.727516 11.645723 c +15.000000 11.110944 15.000000 10.410878 15.000000 9.010747 c +15.000000 7.610616 15.000000 6.910550 14.727516 6.375771 c +14.487833 5.905365 14.105381 5.522914 13.634976 5.283231 c +13.100197 5.010747 12.400131 5.010747 11.000000 5.010747 c +9.000000 5.010747 l +7.599869 5.010747 6.899803 5.010747 6.365024 5.283231 c +5.894619 5.522914 5.512167 5.905365 5.272484 6.375771 c +5.000000 6.910550 5.000000 7.610616 5.000000 9.010747 c +5.000000 10.410878 5.000000 11.110944 5.272484 11.645723 c +5.507047 12.106080 5.878345 12.482201 6.335000 12.722709 c +6.335000 14.010747 l +h +12.335000 14.010747 m +12.335000 12.995256 l +11.973238 13.010747 11.538372 13.010747 11.000000 13.010747 c +9.000000 13.010747 l +8.461628 13.010747 8.026762 13.010747 7.665000 12.995256 c +7.665000 14.010747 l +7.665000 15.300332 8.710415 16.345747 10.000000 16.345747 c +11.289585 16.345747 12.335000 15.300332 12.335000 14.010747 c +h +f* +n +Q + +endstream +endobj + +2 0 obj + 1905 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 30.000000 30.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 18.799999 m +0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c +1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c +5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c +18.799999 30.000000 l +22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c +27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c +30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c +30.000000 11.200001 l +30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c +28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c +24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c +11.200000 0.000000 l +7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c +2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c +0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c +0.000000 18.799999 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 944 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 46 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000002163 00000 n +0000002186 00000 n +0000003378 00000 n +0000003400 00000 n +0000003698 00000 n +0000003800 00000 n +0000003821 00000 n +0000003994 00000 n +0000004068 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +4128 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index b4391721f7..d2d836d20d 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -38,7 +38,6 @@ import BotPaymentsUI import DeleteChatPeerActionSheetItem import HashtagSearchUI import LegacyMediaPickerUI -import WebSearchUI import Emoji import PeerAvatarGalleryUI import PeerInfoUI @@ -76,7 +75,6 @@ import ChatSendMessageActionUI import ChatTextLinkEditUI import WebUI import PremiumUI -import PremiumGiftAttachmentScreen import ImageTransparency import StickerPackPreviewUI import TextNodeWithEntities @@ -93,8 +91,6 @@ import StorageUsageScreen import AvatarEditorScreen import ChatScheduleTimeController import ICloudResources -import LegacyCamera -import LegacyInstantVideoController import StoryContainerScreen import MoreHeaderButton import VolumeButtons @@ -439,7 +435,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G weak var messageTooltipController: TooltipController? weak var videoUnmuteTooltipController: TooltipController? var didDisplayVideoUnmuteTooltip = false + var didDisplayGroupEmojiTip = false var didDisplaySendWhenOnlineTip = false + let displaySendWhenOnlineTipDisposable = MetaDisposable() + weak var silentPostTooltipController: TooltipController? weak var mediaRecordingModeTooltipController: TooltipController? weak var mediaRestrictedTooltipController: TooltipController? @@ -1055,6 +1054,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G openMessageByAction = true } } + case .boostsApplied: + strongSelf.controllerInteraction?.openGroupBoostInfo(nil, 0) + return true default: break } @@ -4415,7 +4417,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let boostController = PremiumBoostLevelsScreen( context: self.context, peerId: peerId, - mode: .user(mode: .groupPeer(userId, count)), + mode: userId.flatMap { .user(mode: .groupPeer($0, count)) } ?? .user(mode: .current), status: boostStatus, myBoostStatus: myBoostStatus ) @@ -6565,6 +6567,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.searchQuerySuggestionState?.1.dispose() self.preloadSavedMessagesChatsDisposable?.dispose() self.recorderDataDisposable.dispose() + self.displaySendWhenOnlineTipDisposable.dispose() } deallocate() } @@ -8154,12 +8157,30 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> delay(4.0, queue: Queue.mainQueue()) )) - if !strongSelf.didDisplaySendWhenOnlineTip { - strongSelf.didDisplaySendWhenOnlineTip = true + if !strongSelf.didDisplayGroupEmojiTip, value { + strongSelf.didDisplayGroupEmojiTip = true + Queue.mainQueue().after(2.0) { - strongSelf.displaySendWhenOnlineTooltip() + strongSelf.displayGroupEmojiTooltip() } } + + if !strongSelf.didDisplaySendWhenOnlineTip, value { + strongSelf.didDisplaySendWhenOnlineTip = true + + strongSelf.displaySendWhenOnlineTipDisposable.set( + (strongSelf.typingActivityPromise.get() + |> filter { !$0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + if let strongSelf = self { + Queue.mainQueue().after(2.0) { + strongSelf.displaySendWhenOnlineTooltip() + } + } + }) + ) + } } else { strongSelf.typingActivityPromise.set(.single(false)) } @@ -10357,6 +10378,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self, let slowmodeState = strongSelf.presentationInterfaceState.slowmodeState else { return } + + if let boostsToUnrestrict = (strongSelf.peerView?.cachedData as? CachedChannelData)?.boostsToUnrestrict, boostsToUnrestrict > 0 { + strongSelf.interfaceInteraction?.openBoostToUnrestrict() + return + } + let rect = sourceView.convert(nodeRect, to: strongSelf.view) if let slowmodeTooltipController = strongSelf.slowmodeTooltipController { if let arguments = slowmodeTooltipController.presentationArguments as? TooltipControllerPresentationArguments, case let .node(f) = arguments.sourceAndRect, let (previousNode, previousRect) = f() { @@ -10830,6 +10857,26 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let controller = PremiumIntroScreen(context: self.context, source: .settings) self.push(controller) + }, openBoostToUnrestrict: { [weak self] in + guard let self, let peerId = self.chatLocation.peerId, let cachedData = self.peerView?.cachedData as? CachedChannelData, let boostToUnrestrict = cachedData.boostsToUnrestrict else { + return + } + let _ = combineLatest(queue: Queue.mainQueue(), + context.engine.peers.getChannelBoostStatus(peerId: peerId), + context.engine.peers.getMyBoostStatus() + ).startStandalone(next: { [weak self] boostStatus, myBoostStatus in + guard let self, let boostStatus, let myBoostStatus else { + return + } + let boostController = PremiumBoostLevelsScreen( + context: self.context, + peerId: peerId, + mode: .user(mode: .unrestrict(Int(boostToUnrestrict))), + status: boostStatus, + myBoostStatus: myBoostStatus + ) + self.push(boostController) + }) }, updateHistoryFilter: { [weak self] update in guard let self else { return @@ -12623,112 +12670,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - func getCaptionPanelView() -> TGCaptionPanelView? { - var isScheduledMessages = false - if case .scheduledMessages = self.presentationInterfaceState.subject { - isScheduledMessages = true - } - return self.context.sharedContext.makeGalleryCaptionPanelView(context: self.context, chatLocation: self.presentationInterfaceState.chatLocation, isScheduledMessages: isScheduledMessages, customEmojiAvailable: self.presentationInterfaceState.customEmojiAvailable, present: { [weak self] c in - self?.present(c, in: .window(.root)) - }, presentInGlobalOverlay: { [weak self] c in - guard let self else { - return - } - self.presentInGlobalOverlay(c) - }) as? TGCaptionPanelView - } - - func openCamera(cameraView: TGAttachmentCameraView? = nil) { - guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { - return - } - let _ = peer - - let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in - let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) - return entry ?? GeneratedMediaStoreSettings.defaultSettings - } - |> deliverOnMainQueue).startStandalone(next: { [weak self] settings in - guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { - return - } - - var enablePhoto = true - var enableVideo = true - - if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall { - enableVideo = false - } - - var bannedSendPhotos: (Int32, Bool)? - var bannedSendVideos: (Int32, Bool)? - - if let channel = peer as? TelegramChannel { - if let value = channel.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = value - } - if let value = channel.hasBannedPermission(.banSendVideos) { - bannedSendVideos = value - } - } else if let group = peer as? TelegramGroup { - if group.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = (Int32.max, false) - } - if group.hasBannedPermission(.banSendVideos) { - bannedSendVideos = (Int32.max, false) - } - } - - if bannedSendPhotos != nil { - enablePhoto = false - } - if bannedSendVideos != nil { - enableVideo = false - } - - let storeCapturedMedia = peer.id.namespace != Namespaces.Peer.SecretChat - let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText - - presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: nil, parentController: strongSelf, attachmentController: self?.attachmentController, editingMedia: false, saveCapturedPhotos: storeCapturedMedia, mediaGrouping: true, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in - if let strongSelf = self { - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) - if !inputText.string.isEmpty { - strongSelf.clearInputText() - } - } - }, recognizedQRCode: { [weak self] code in - if let strongSelf = self { - if let (host, port, username, password, secret) = parseProxyUrl(code) { - strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil) - } - } - }, presentSchedulePicker: { [weak self] _, done in - if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in - if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { - strongSelf.openScheduledMessages() - } - } - }) - } - }, presentTimerPicker: { [weak self] done in - if let strongSelf = self { - strongSelf.presentTimerPicker(style: .media, completion: { time in - done(time) - }) - } - }, getCaptionPanelView: { [weak self] in - return self?.getCaptionPanelView() - }, dismissedWithResult: { [weak self] in - self?.attachmentController?.dismiss(animated: false, completion: nil) - }, finishedTransitionIn: { [weak self] in - self?.attachmentController?.scrollToTop?() - }) - }) - } - public func presentAttachmentBot(botId: PeerId, payload: String?, justInstalled: Bool) { self.attachmentController?.dismiss(animated: true, completion: nil) self.presentAttachmentMenu(subject: .bot(id: botId, payload: payload, justInstalled: justInstalled)) @@ -12893,1501 +12834,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - enum AttachMenuSubject { - case `default` - case edit(mediaOptions: MessageMediaEditingOptions, mediaReference: AnyMediaReference) - case bot(id: PeerId, payload: String?, justInstalled: Bool) - case gift - } - - func presentAttachmentMenu(subject: AttachMenuSubject) { - guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { - return - } - - let context = self.context - - let inputIsActive = self.presentationInterfaceState.inputMode == .text - - self.chatDisplayNode.dismissInput() - - var banSendText: (Int32, Bool)? - var bannedSendPhotos: (Int32, Bool)? - var bannedSendVideos: (Int32, Bool)? - var bannedSendFiles: (Int32, Bool)? - - var canSendPolls = true - if let peer = peer as? TelegramUser, peer.botInfo == nil { - canSendPolls = false - } else if peer is TelegramSecretChat { - canSendPolls = false - } else if let channel = peer as? TelegramChannel { - if let value = channel.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = value - } - if let value = channel.hasBannedPermission(.banSendVideos) { - bannedSendVideos = value - } - if let value = channel.hasBannedPermission(.banSendFiles) { - bannedSendFiles = value - } - if let value = channel.hasBannedPermission(.banSendText) { - banSendText = value - } - if channel.hasBannedPermission(.banSendPolls) != nil { - canSendPolls = false - } - } else if let group = peer as? TelegramGroup { - if group.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = (Int32.max, false) - } - if group.hasBannedPermission(.banSendVideos) { - bannedSendVideos = (Int32.max, false) - } - if group.hasBannedPermission(.banSendFiles) { - bannedSendFiles = (Int32.max, false) - } - if group.hasBannedPermission(.banSendText) { - banSendText = (Int32.max, false) - } - if group.hasBannedPermission(.banSendPolls) { - canSendPolls = false - } - } - - var availableButtons: [AttachmentButtonType] = [.gallery, .file] - if banSendText == nil { - availableButtons.append(.location) - availableButtons.append(.contact) - } - if canSendPolls { - availableButtons.insert(.poll, at: max(0, availableButtons.count - 1)) - } - - let presentationData = self.presentationData - - var isScheduledMessages = false - if case .scheduledMessages = self.presentationInterfaceState.subject { - isScheduledMessages = true - } - - var peerType: AttachMenuBots.Bot.PeerFlags = [] - if let user = peer as? TelegramUser { - if let _ = user.botInfo { - peerType.insert(.bot) - } else { - peerType.insert(.user) - } - } else if let _ = peer as? TelegramGroup { - peerType = .group - } else if let channel = peer as? TelegramChannel { - if case .broadcast = channel.info { - peerType = .channel - } else { - peerType = .group - } - } - - let buttons: Signal<([AttachmentButtonType], [AttachmentButtonType], AttachmentButtonType?), NoError> - if !isScheduledMessages && !peer.isDeleted { - buttons = self.context.engine.messages.attachMenuBots() - |> map { attachMenuBots in - var buttons = availableButtons - var allButtons = availableButtons - var initialButton: AttachmentButtonType? - switch subject { - case .default: - initialButton = .gallery - case .edit: - break - case .gift: - initialButton = .gift - default: - break - } - - for bot in attachMenuBots.reversed() { - var peerType = peerType - if bot.peer.id == peer.id { - peerType.insert(.sameBot) - peerType.remove(.bot) - } - let button: AttachmentButtonType = .app(bot) - if !bot.peerTypes.intersection(peerType).isEmpty { - buttons.insert(button, at: 1) - - if case let .bot(botId, _, _) = subject { - if initialButton == nil && bot.peer.id == botId { - initialButton = button - } - } - } - allButtons.insert(button, at: 1) - } - - return (buttons, allButtons, initialButton) - } - } else { - buttons = .single((availableButtons, availableButtons, .gallery)) - } - - let dataSettings = self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in - let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) - return entry ?? GeneratedMediaStoreSettings.defaultSettings - } - - let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) - let premiumGiftOptions: [CachedPremiumGiftOption] - if !premiumConfiguration.isPremiumDisabled && premiumConfiguration.showPremiumGiftInAttachMenu, let user = peer as? TelegramUser, !user.isPremium && !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) { - premiumGiftOptions = self.presentationInterfaceState.premiumGiftOptions - } else { - premiumGiftOptions = [] - } - - let _ = combineLatest(queue: Queue.mainQueue(), buttons, dataSettings).startStandalone(next: { [weak self] buttonsAndInitialButton, dataSettings in - guard let strongSelf = self else { - return - } - - var (buttons, allButtons, initialButton) = buttonsAndInitialButton - if !premiumGiftOptions.isEmpty { - buttons.insert(.gift, at: 1) - } - - guard let initialButton = initialButton else { - if case let .bot(botId, botPayload, botJustInstalled) = subject { - if let button = allButtons.first(where: { button in - if case let .app(bot) = button, bot.peer.id == botId { - return true - } else { - return false - } - }), case let .app(bot) = button { - let content: UndoOverlayContent - if botJustInstalled { - if bot.flags.contains(.showInSettings) { - content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(bot.shortName).string, timeout: 5.0, customUndoText: nil) - } else { - content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsAdded(bot.shortName).string, timeout: 5.0, customUndoText: nil) - } - } else { - content = .info(title: nil, text: strongSelf.presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError, timeout: nil, customUndoText: nil) - } - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current) - } else { - let _ = (context.engine.messages.getAttachMenuBot(botId: botId) - |> deliverOnMainQueue).startStandalone(next: { bot in - let controller = webAppTermsAlertController(context: context, updatedPresentationData: strongSelf.updatedPresentationData, bot: bot, completion: { allowWrite in - let _ = (context.engine.messages.addBotToAttachMenu(botId: botId, allowWrite: allowWrite) - |> deliverOnMainQueue).startStandalone(error: { _ in - - }, completed: { - strongSelf.presentAttachmentBot(botId: botId, payload: botPayload, justInstalled: true) - }) - }) - strongSelf.present(controller, in: .window(.root)) - }, error: { _ in - strongSelf.present(textAlertController(context: context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }) - } - } - return - } - - let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText - - let currentMediaController = Atomic(value: nil) - let currentFilesController = Atomic(value: nil) - let currentLocationController = Atomic(value: nil) - - strongSelf.canReadHistory.set(false) - - let attachmentController = AttachmentController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: strongSelf.chatLocation, isScheduledMessages: isScheduledMessages, buttons: buttons, initialButton: initialButton, makeEntityInputView: { [weak self] in - guard let strongSelf = self else { - return nil - } - return EntityInputView(context: strongSelf.context, isDark: false, areCustomEmojiEnabled: strongSelf.presentationInterfaceState.customEmojiAvailable) - }) - attachmentController.didDismiss = { [weak self] in - self?.attachmentController = nil - self?.canReadHistory.set(true) - } - attachmentController.getSourceRect = { [weak self] in - if let strongSelf = self { - return strongSelf.chatDisplayNode.frameForAttachmentButton()?.offsetBy(dx: strongSelf.chatDisplayNode.supernode?.frame.minX ?? 0.0, dy: 0.0) - } else { - return nil - } - } - attachmentController.requestController = { [weak self, weak attachmentController] type, completion in - guard let strongSelf = self else { - return - } - switch type { - case .gallery: - strongSelf.controllerNavigationDisposable.set(nil) - let existingController = currentMediaController.with { $0 } - if let controller = existingController { - completion(controller, controller.mediaPickerContext) - controller.prepareForReuse() - return - } - strongSelf.presentMediaPicker(saveEditedPhotos: dataSettings.storeEditedPhotos, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, present: { controller, mediaPickerContext in - let _ = currentMediaController.swap(controller) - if !inputText.string.isEmpty { - mediaPickerContext?.setCaption(inputText) - } - completion(controller, mediaPickerContext) - }, updateMediaPickerContext: { [weak attachmentController] mediaPickerContext in - attachmentController?.mediaPickerContext = mediaPickerContext - }, completion: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in - if !inputText.string.isEmpty { - self?.clearInputText() - } - self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) - }) - case .file: - strongSelf.controllerNavigationDisposable.set(nil) - let existingController = currentFilesController.with { $0 } - if let controller = existingController { - completion(controller, controller.mediaPickerContext) - controller.prepareForReuse() - return - } - let controller = strongSelf.context.sharedContext.makeAttachmentFileController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bannedSendMedia: bannedSendFiles, presentGallery: { [weak self, weak attachmentController] in - attachmentController?.dismiss(animated: true) - self?.presentFileGallery() - }, presentFiles: { [weak self, weak attachmentController] in - attachmentController?.dismiss(animated: true) - self?.presentICloudFileGallery() - }, send: { [weak self] mediaReference in - guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else { - return - } - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: strongSelf.transformEnqueueMessages([message])) - |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in - if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages { - strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() - } - }) - }) - if let controller = controller as? AttachmentFileControllerImpl { - let _ = currentFilesController.swap(controller) - completion(controller, controller.mediaPickerContext) - } - case .location: - strongSelf.controllerNavigationDisposable.set(nil) - let existingController = currentLocationController.with { $0 } - if let controller = existingController { - completion(controller, controller.mediaPickerContext) - controller.prepareForReuse() - return - } - let selfPeerId: PeerId - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - selfPeerId = peer.id - } else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.canBeAnonymous) { - selfPeerId = peer.id - } else { - selfPeerId = strongSelf.context.account.peerId - } - let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: selfPeerId)) - |> deliverOnMainQueue).startStandalone(next: { [weak self] selfPeer in - guard let strongSelf = self, let selfPeer = selfPeer else { - return - } - let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages - let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: EnginePeer(peer), selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _, _, _, _ in - guard let strongSelf = self else { - return - } - let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } - }) - } - }, nil) - strongSelf.sendMessages([message]) - }) - completion(controller, controller.mediaPickerContext) - - let _ = currentLocationController.swap(controller) - }) - case .contact: - let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true, requirePhoneNumbers: true)) - contactsController.presentScheduleTimePicker = { [weak self] completion in - if let strongSelf = self { - strongSelf.presentScheduleTimePicker(completion: completion) - } - } - contactsController.navigationPresentation = .modal - completion(contactsController, contactsController.mediaPickerContext) - strongSelf.controllerNavigationDisposable.set((contactsController.result - |> deliverOnMainQueue).startStrict(next: { [weak self] peers in - if let strongSelf = self, let (peers, _, silent, scheduleTime, text) = peers { - var textEnqueueMessage: EnqueueMessage? - if let text = text, text.length > 0 { - var attributes: [MessageAttribute] = [] - let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) - if !entities.isEmpty { - attributes.append(TextEntitiesMessageAttribute(entities: entities)) - } - textEnqueueMessage = .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - } - if peers.count > 1 { - var enqueueMessages: [EnqueueMessage] = [] - if let textEnqueueMessage = textEnqueueMessage { - enqueueMessages.append(textEnqueueMessage) - } - for peer in peers { - var media: TelegramMediaContact? - switch peer { - case let .peer(contact, _, _): - guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { - continue - } - let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - - let phone = contactData.basicData.phoneNumbers[0].value - media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil) - case let .deviceContact(_, basicData): - guard !basicData.phoneNumbers.isEmpty else { - continue - } - let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - - let phone = contactData.basicData.phoneNumbers[0].value - media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil) - } - - if let media = media { - let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } - }) - } - }, nil) - let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - enqueueMessages.append(message) - } - } - strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) - } else if let peer = peers.first { - let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError> - switch peer { - case let .peer(contact, _, _): - guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { - return - } - let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - let context = strongSelf.context - dataSignal = (strongSelf.context.sharedContext.contactDataManager?.basicData() ?? .single([:])) - |> take(1) - |> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> in - var stableId: String? - let queryPhoneNumber = formatPhoneNumber(context: context, number: phoneNumber) - outer: for (id, data) in basicData { - for phoneNumber in data.phoneNumbers { - if formatPhoneNumber(context: context, number: phoneNumber.value) == queryPhoneNumber { - stableId = id - break outer - } - } - } - - if let stableId = stableId { - return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil)) - |> take(1) - |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in - return (contact, extendedData) - } - } else { - return .single((contact, contactData)) - } - } - case let .deviceContact(id, _): - dataSignal = (strongSelf.context.sharedContext.contactDataManager?.extendedData(stableId: id) ?? .single(nil)) - |> take(1) - |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in - return (nil, extendedData) - } - } - strongSelf.controllerNavigationDisposable.set((dataSignal - |> deliverOnMainQueue).startStrict(next: { peerAndContactData in - if let strongSelf = self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 { - if contactData.isPrimitive { - let phone = contactData.basicData.phoneNumbers[0].value - let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil) - let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } - }) - } - }, nil) - - var enqueueMessages: [EnqueueMessage] = [] - if let textEnqueueMessage = textEnqueueMessage { - enqueueMessages.append(textEnqueueMessage) - } - enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) - strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) - } else { - let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in - guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else { - return - } - let phone = contactData.basicData.phoneNumbers[0].value - if let vCardData = contactData.serializedVCard() { - let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData) - let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } - }) - } - }, nil) - - var enqueueMessages: [EnqueueMessage] = [] - if let textEnqueueMessage = textEnqueueMessage { - enqueueMessages.append(textEnqueueMessage) - } - enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) - strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) - } - }), completed: nil, cancelled: nil) - strongSelf.effectiveNavigationController?.pushViewController(contactController) - } - } - })) - } - } - })) - case .poll: - let controller = strongSelf.configurePollCreation() - completion(controller, controller?.mediaPickerContext) - strongSelf.controllerNavigationDisposable.set(nil) - case .gift: - let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions - if !premiumGiftOptions.isEmpty { - let controller = PremiumGiftAttachmentScreen(context: context, peerIds: [peer.id], options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in - if let strongSelf = self { - strongSelf.push(c) - } - }, completion: { [weak self] in - if let strongSelf = self { - strongSelf.hintPlayNextOutgoingGift() - strongSelf.attachmentController?.dismiss(animated: true) - } - }) - completion(controller, controller.mediaPickerContext) - strongSelf.controllerNavigationDisposable.set(nil) - - let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: context.sharedContext.accountManager, peerId: peer.id).startStandalone() - } - case let .app(bot): - var payload: String? - var fromAttachMenu = true - if case let .bot(_, botPayload, _) = subject { - payload = botPayload - fromAttachMenu = false - } - let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false) - let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject - let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId) - controller.openUrl = { [weak self] url, concealed, commit in - self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) - } - controller.getNavigationController = { [weak self] in - return self?.effectiveNavigationController - } - controller.completion = { [weak self] in - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } - }) - strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() - } - } - completion(controller, controller.mediaPickerContext) - strongSelf.controllerNavigationDisposable.set(nil) - - if bot.flags.contains(.notActivated) { - let alertController = webAppTermsAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bot: bot, completion: { [weak self] allowWrite in - guard let self else { - return - } - if bot.flags.contains(.showInSettingsDisclaimer) { - let _ = self.context.engine.messages.acceptAttachMenuBotDisclaimer(botId: bot.peer.id).startStandalone() - } - let _ = (self.context.engine.messages.addBotToAttachMenu(botId: bot.peer.id, allowWrite: allowWrite) - |> deliverOnMainQueue).startStandalone(error: { _ in - }, completed: { [weak controller] in - controller?.refresh() - }) - }, - dismissed: { - strongSelf.attachmentController?.dismiss(animated: true) - }) - strongSelf.present(alertController, in: .window(.root)) - } - default: - break - } - } - let present = { - attachmentController.navigationPresentation = .flatModal - strongSelf.push(attachmentController) - strongSelf.attachmentController = attachmentController - - if case let .bot(botId, _, botJustInstalled) = subject, botJustInstalled { - if let button = allButtons.first(where: { button in - if case let .app(bot) = button, bot.peer.id == botId { - return true - } else { - return false - } - }), case let .app(bot) = button { - Queue.mainQueue().after(0.3) { - let content: UndoOverlayContent - if bot.flags.contains(.showInSettings) { - content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(bot.shortName).string, timeout: 5.0, customUndoText: nil) - } else { - content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsAdded(bot.shortName).string, timeout: 5.0, customUndoText: nil) - } - attachmentController.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current) - } - } - } - } - - if inputIsActive { - Queue.mainQueue().after(0.15, { - present() - }) - } else { - present() - } - }) - } - - func oldPresentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) { - let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in - let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) - return entry ?? GeneratedMediaStoreSettings.defaultSettings - } - |> deliverOnMainQueue).startStandalone(next: { [weak self] settings in - guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { - return - } - strongSelf.chatDisplayNode.dismissInput() - - var bannedSendMedia: (Int32, Bool)? - var canSendPolls = true - if let channel = peer as? TelegramChannel { - if let value = channel.hasBannedPermission(.banSendMedia) { - bannedSendMedia = value - } - if channel.hasBannedPermission(.banSendPolls) != nil { - canSendPolls = false - } - } else if let group = peer as? TelegramGroup { - if group.hasBannedPermission(.banSendMedia) { - bannedSendMedia = (Int32.max, false) - } - if group.hasBannedPermission(.banSendPolls) { - canSendPolls = false - } - } - - if editMediaOptions == nil, let (untilDate, personal) = bannedSendMedia { - let banDescription: String - if untilDate != 0 && untilDate != Int32.max { - banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMediaTimed(stringForFullDate(timestamp: untilDate, strings: strongSelf.presentationInterfaceState.strings, dateTimeFormat: strongSelf.presentationInterfaceState.dateTimeFormat)).string - } else if personal { - banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMedia - } else { - banDescription = strongSelf.presentationInterfaceState.strings.Conversation_DefaultRestrictedMedia - } - - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - var items: [ActionSheetItem] = [] - items.append(ActionSheetTextItem(title: banDescription)) - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_Location, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - self?.presentLocationPicker() - })) - if canSendPolls { - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.AttachmentMenu_Poll, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let controller = self?.configurePollCreation() { - self?.effectiveNavigationController?.pushViewController(controller) - } - })) - } - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_Contact, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - self?.presentContactPicker() - })) - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.present(actionSheet, in: .window(.root)) - - return - } - - let legacyController = LegacyController(presentation: .custom, theme: strongSelf.presentationData.theme, initialLayout: strongSelf.validLayout) - legacyController.blocksBackgroundWhenInOverlay = true - legacyController.acceptsFocusWhenInOverlay = true - legacyController.statusBar.statusBarStyle = .Ignore - legacyController.controllerLoaded = { [weak legacyController] in - legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true - } - - let emptyController = LegacyEmptyController(context: legacyController.context)! - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - legacyController.bind(controller: navigationController) - - legacyController.enableSizeClassSignal = true - - let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText - let menuEditMediaOptions = editMediaOptions.flatMap { options -> LegacyAttachmentMenuMediaEditing in - var result: LegacyAttachmentMenuMediaEditing = .none - if options.contains(.imageOrVideo) { - result = .imageOrVideo(editMediaReference) - } - return result - } - - var slowModeEnabled = false - if let channel = peer as? TelegramChannel, channel.isRestrictedBySlowmode { - slowModeEnabled = true - } - - let controller = legacyAttachmentMenu(context: strongSelf.context, peer: peer, threadTitle: strongSelf.threadInfo?.title, chatLocation: strongSelf.chatLocation, editMediaOptions: menuEditMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, canSendPolls: canSendPolls, updatedPresentationData: strongSelf.updatedPresentationData, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, initialCaption: inputText, openGallery: { - self?.presentOldMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, completion: { signals, silentPosting, scheduleTime in - if !inputText.string.isEmpty { - strongSelf.clearInputText() - } - if editMediaOptions != nil { - self?.editMessageMediaWithLegacySignals(signals) - } else { - self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) - } - }) - }, openCamera: { [weak self] cameraView, menuController in - if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - var enablePhoto = true - var enableVideo = true - - if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall { - enableVideo = false - } - - var bannedSendPhotos: (Int32, Bool)? - var bannedSendVideos: (Int32, Bool)? - - if let channel = peer as? TelegramChannel { - if let value = channel.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = value - } - if let value = channel.hasBannedPermission(.banSendVideos) { - bannedSendVideos = value - } - } else if let group = peer as? TelegramGroup { - if group.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = (Int32.max, false) - } - if group.hasBannedPermission(.banSendVideos) { - bannedSendVideos = (Int32.max, false) - } - } - - if bannedSendPhotos != nil { - enablePhoto = false - } - if bannedSendVideos != nil { - enableVideo = false - } - - presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: peer.id.namespace != Namespaces.Peer.SecretChat, mediaGrouping: true, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in - if let strongSelf = self { - if editMediaOptions != nil { - strongSelf.editMessageMediaWithLegacySignals(signals!) - } else { - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) - } - if !inputText.string.isEmpty { - strongSelf.clearInputText() - } - } - }, recognizedQRCode: { [weak self] code in - if let strongSelf = self { - if let (host, port, username, password, secret) = parseProxyUrl(code) { - strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil) - } - } - }, presentSchedulePicker: { [weak self] _, done in - if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in - if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { - strongSelf.openScheduledMessages() - } - } - }) - } - }, presentTimerPicker: { [weak self] done in - if let strongSelf = self { - strongSelf.presentTimerPicker(style: .media, completion: { time in - done(time) - }) - } - }, getCaptionPanelView: { [weak self] in - return self?.getCaptionPanelView() - }) - } - }, openFileGallery: { - self?.presentFileMediaPickerOptions(editingMessage: editMediaOptions != nil) - }, openWebSearch: { [weak self] in - self?.presentWebSearch(editingMessage: editMediaOptions != nil, attachment: false, present: { [weak self] c, a in - self?.present(c, in: .window(.root), with: a) - }) - }, openMap: { - self?.presentLocationPicker() - }, openContacts: { - self?.presentContactPicker() - }, openPoll: { - if let controller = self?.configurePollCreation() { - self?.effectiveNavigationController?.pushViewController(controller) - } - }, presentSelectionLimitExceeded: { - guard let strongSelf = self else { - return - } - let text: String - if slowModeEnabled { - text = strongSelf.presentationData.strings.Chat_SlowmodeAttachmentLimitReached - } else { - text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached - } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, presentCantSendMultipleFiles: { - guard let strongSelf = self else { - return - } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentMultipleFilesDisabled, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, presentJpegConversionAlert: { completion in - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.MediaPicker_KeepHeic, action: { - completion(false) - }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.MediaPicker_ConvertToJpeg, action: { - completion(true) - })], actionLayout: .vertical), in: .window(.root)) - }, presentSchedulePicker: { [weak self] _, done in - if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in - if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { - strongSelf.openScheduledMessages() - } - } - }) - } - }, presentTimerPicker: { [weak self] done in - if let strongSelf = self { - strongSelf.presentTimerPicker(style: .media, completion: { time in - done(time) - }) - } - }, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in - guard let strongSelf = self else { - completion() - return - } - if !inputText.string.isEmpty { - strongSelf.clearInputText() - } - if editMediaOptions != nil { - strongSelf.editMessageMediaWithLegacySignals(signals!) - completion() - } else { - let immediateCompletion = getAnimatedTransitionSource == nil - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: { - if !immediateCompletion { - completion() - } - }) - if immediateCompletion { - completion() - } - } - }, selectRecentlyUsedInlineBot: { [weak self] peer in - if let strongSelf = self, let addressName = peer.addressName { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState({ $0.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " "))) }).updatedInputMode({ _ in - return .text - }) - }) - } - }, getCaptionPanelView: { [weak self] in - return self?.getCaptionPanelView() - }, present: { [weak self] c, a in - self?.present(c, in: .window(.root), with: a) - }) - controller.didDismiss = { [weak legacyController] _ in - legacyController?.dismiss() - } - controller.customRemoveFromParentViewController = { [weak legacyController] in - legacyController?.dismiss() - } - - legacyController.blocksBackgroundWhenInOverlay = true - strongSelf.present(legacyController, in: .window(.root)) - controller.present(in: emptyController, sourceView: nil, animated: true) - - let presentationDisposable = strongSelf.updatedPresentationData.1.startStrict(next: { [weak controller] presentationData in - if let controller = controller { - controller.pallete = legacyMenuPaletteFromTheme(presentationData.theme, forceDark: false) - } - }) - legacyController.disposables.add(presentationDisposable) - }) - } - - func presentFileGallery(editingMessage: Bool = false) { - self.presentOldMediaPicker(fileMode: true, editingMedia: editingMessage, completion: { [weak self] signals, silentPosting, scheduleTime in - if editingMessage { - self?.editMessageMediaWithLegacySignals(signals) - } else { - self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) - } - }) - } - - func presentICloudFileGallery(editingMessage: Bool = false) { - let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), - TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), - TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] result in - guard let strongSelf = self else { - return - } - let (accountPeer, limits, premiumLimits) = result - let isPremium = accountPeer?.isPremium ?? false - - strongSelf.present(legacyICloudFilePicker(theme: strongSelf.presentationData.theme, completion: { [weak self] urls in - if let strongSelf = self, !urls.isEmpty { - var signals: [Signal] = [] - for url in urls { - signals.append(iCloudFileDescription(url)) - } - strongSelf.enqueueMediaMessageDisposable.set((combineLatest(signals) - |> deliverOnMainQueue).startStrict(next: { results in - if let strongSelf = self { - let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject - - for item in results { - if let item = item { - if item.fileSize > Int64(premiumLimits.maxUploadFileParts) * 512 * 1024 { - let controller = PremiumLimitScreen(context: strongSelf.context, subject: .files, count: 4, action: { - return true - }) - strongSelf.push(controller) - return - } else if item.fileSize > Int64(limits.maxUploadFileParts) * 512 * 1024 && !isPremium { - let context = strongSelf.context - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .files, count: 2, action: { - replaceImpl?(PremiumIntroScreen(context: context, source: .upload)) - return true - }) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - strongSelf.push(controller) - return - } - } - } - - var groupingKey: Int64? - var fileTypes: (music: Bool, other: Bool) = (false, false) - if results.count > 1 { - for item in results { - if let item = item { - let pathExtension = (item.fileName as NSString).pathExtension.lowercased() - if ["mp3", "m4a"].contains(pathExtension) { - fileTypes.music = true - } else { - fileTypes.other = true - } - } - } - } - if fileTypes.music != fileTypes.other { - groupingKey = Int64.random(in: Int64.min ... Int64.max) - } - - var messages: [EnqueueMessage] = [] - for item in results { - if let item = item { - let fileId = Int64.random(in: Int64.min ... Int64.max) - let mimeType = guessMimeTypeByFileExtension((item.fileName as NSString).pathExtension) - var previewRepresentations: [TelegramMediaImageRepresentation] = [] - if mimeType.hasPrefix("image/") || mimeType == "application/pdf" { - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: ICloudFileResource(urlData: item.urlData, thumbnail: true), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) - } - var attributes: [TelegramMediaFileAttribute] = [] - attributes.append(.FileName(fileName: item.fileName)) - if let audioMetadata = item.audioMetadata { - attributes.append(.Audio(isVoice: false, duration: audioMetadata.duration, title: audioMetadata.title, performer: audioMetadata.performer, waveform: nil)) - } - - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes) - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) - messages.append(message) - } - if let _ = groupingKey, messages.count % 10 == 0 { - groupingKey = Int64.random(in: Int64.min ... Int64.max) - } - } - - if !messages.isEmpty { - if editingMessage { - strongSelf.editMessageMediaWithMessages(messages) - } else { - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } - }) - } - }, nil) - strongSelf.sendMessages(messages) - } - } - } - })) - } - }), in: .window(.root)) - }) - } - - func presentFileMediaPickerOptions(editingMessage: Bool) { - let actionSheet = ActionSheetController(presentationData: self.presentationData) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Conversation_FilePhotoOrVideo, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.presentFileGallery(editingMessage: editingMessage) - } - }), - ActionSheetButtonItem(title: self.presentationData.strings.Conversation_FileICloudDrive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.presentICloudFileGallery(editingMessage: editingMessage) - } - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - self.chatDisplayNode.dismissInput() - self.present(actionSheet, in: .window(.root)) - } - - func presentMediaPicker(subject: MediaPickerScreen.Subject = .assets(nil, .default), saveEditedPhotos: Bool, bannedSendPhotos: (Int32, Bool)?, bannedSendVideos: (Int32, Bool)?, present: @escaping (MediaPickerScreen, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping ([Any], Bool, Int32?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void) { - guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { - return - } - var isScheduledMessages = false - if case .scheduledMessages = self.presentationInterfaceState.subject { - isScheduledMessages = true - } - let controller = MediaPickerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadTitle: self.threadInfo?.title, chatLocation: self.chatLocation, isScheduledMessages: isScheduledMessages, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, subject: subject, saveEditedPhotos: saveEditedPhotos) - let mediaPickerContext = controller.mediaPickerContext - controller.openCamera = { [weak self] cameraView in - self?.openCamera(cameraView: cameraView) - } - controller.presentWebSearch = { [weak self, weak controller] mediaGroups, activateOnDisplay in - self?.presentWebSearch(editingMessage: false, attachment: true, activateOnDisplay: activateOnDisplay, present: { [weak controller] c, a in - controller?.present(c, in: .current) - if let webSearchController = c as? WebSearchController { - webSearchController.searchingUpdated = { [weak mediaGroups] searching in - if let mediaGroups = mediaGroups, mediaGroups.isNodeLoaded { - let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) - transition.updateAlpha(node: mediaGroups.displayNode, alpha: searching ? 0.0 : 1.0) - mediaGroups.displayNode.isUserInteractionEnabled = !searching - } - } - webSearchController.present(mediaGroups, in: .current) - webSearchController.dismissed = { - updateMediaPickerContext(mediaPickerContext) - } - controller?.webSearchController = webSearchController - updateMediaPickerContext(webSearchController.mediaPickerContext) - } - }) - } - controller.presentSchedulePicker = { [weak self] media, done in - if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] time in - if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { - strongSelf.openScheduledMessages() - } - } - }) - } - } - controller.presentTimerPicker = { [weak self] done in - if let strongSelf = self { - strongSelf.presentTimerPicker(style: .media, completion: { time in - done(time) - }) - } - } - controller.getCaptionPanelView = { [weak self] in - return self?.getCaptionPanelView() - } - controller.legacyCompletion = { signals, silently, scheduleTime, getAnimatedTransitionSource, sendCompletion in - completion(signals, silently, scheduleTime, getAnimatedTransitionSource, sendCompletion) - } - present(controller, mediaPickerContext) - } - - func presentOldMediaPicker(fileMode: Bool, editingMedia: Bool, completion: @escaping ([Any], Bool, Int32) -> Void) { - let engine = self.context.engine - let _ = (self.context.sharedContext.accountManager.transaction { transaction -> Signal<(GeneratedMediaStoreSettings, EngineConfiguration.SearchBots), NoError> in - let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) - - return engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) - |> map { configuration -> (GeneratedMediaStoreSettings, EngineConfiguration.SearchBots) in - return (entry ?? GeneratedMediaStoreSettings.defaultSettings, configuration) - } - } - |> switchToLatest - |> deliverOnMainQueue).startStandalone(next: { [weak self] settings, searchBotsConfiguration in - guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { - return - } - let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText - var selectionLimit: Int = 100 - var slowModeEnabled = false - if let channel = peer as? TelegramChannel, channel.isRestrictedBySlowmode { - selectionLimit = 10 - slowModeEnabled = true - } - - let _ = legacyAssetPicker(context: strongSelf.context, presentationData: strongSelf.presentationData, editingMedia: editingMedia, fileMode: fileMode, peer: peer, threadTitle: strongSelf.threadInfo?.title, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, selectionLimit: selectionLimit).startStandalone(next: { generator in - if let strongSelf = self { - let legacyController = LegacyController(presentation: fileMode ? .navigation : .custom, theme: strongSelf.presentationData.theme, initialLayout: strongSelf.validLayout) - legacyController.navigationPresentation = .modal - legacyController.statusBar.statusBarStyle = strongSelf.presentationData.theme.rootController.statusBarStyle.style - legacyController.controllerLoaded = { [weak legacyController] in - legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true - legacyController?.view.disablesInteractiveModalDismiss = true - } - let controller = generator(legacyController.context) - - legacyController.bind(controller: controller) - legacyController.deferScreenEdgeGestures = [.top] - - configureLegacyAssetPicker(controller, context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak self, weak legacyController] in - if let strongSelf = self { - let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { results, selectionState, editingState, silentPosting in - if let legacyController = legacyController { - legacyController.dismiss() - } - legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { result in - if let strongSelf = self { - strongSelf.enqueueChatContextResult(results, result, hideVia: true) - } - }, enqueueMediaMessages: { signals in - if let strongSelf = self { - if editingMedia { - strongSelf.editMessageMediaWithLegacySignals(signals) - } else { - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting) - } - } - }) - })) - controller.getCaptionPanelView = { [weak self] in - return self?.getCaptionPanelView() - } - strongSelf.effectiveNavigationController?.pushViewController(controller) - } - }, presentSelectionLimitExceeded: { - guard let strongSelf = self else { - return - } - - let text: String - if slowModeEnabled { - text = strongSelf.presentationData.strings.Chat_SlowmodeAttachmentLimitReached - } else { - text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached - } - - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, presentSchedulePicker: { [weak self] media, done in - if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] time in - if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { - strongSelf.openScheduledMessages() - } - } - }) - } - }, presentTimerPicker: { [weak self] done in - if let strongSelf = self { - strongSelf.presentTimerPicker(style: .media, completion: { time in - done(time) - }) - } - }, getCaptionPanelView: { [weak self] in - return self?.getCaptionPanelView() - }) - controller.descriptionGenerator = legacyAssetPickerItemGenerator() - controller.completionBlock = { [weak legacyController] signals, silentPosting, scheduleTime in - if let legacyController = legacyController { - legacyController.dismiss(animated: true) - completion(signals!, silentPosting, scheduleTime) - } - } - controller.dismissalBlock = { [weak legacyController] in - if let legacyController = legacyController { - legacyController.dismiss(animated: true) - } - } - strongSelf.chatDisplayNode.dismissInput() - strongSelf.effectiveNavigationController?.pushViewController(legacyController) - } - }) - }) - } - - func presentWebSearch(editingMessage: Bool, attachment: Bool, activateOnDisplay: Bool = true, present: @escaping (ViewController, Any?) -> Void) { - guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { - return - } - - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) - |> deliverOnMainQueue).startStandalone(next: { [weak self] configuration in - if let strongSelf = self { - let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: configuration, mode: .media(attachment: attachment, completion: { [weak self] results, selectionState, editingState, silentPosting in - self?.attachmentController?.dismiss(animated: true, completion: nil) - legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self] result in - if let strongSelf = self { - strongSelf.enqueueChatContextResult(results, result, hideVia: true) - } - }, enqueueMediaMessages: { [weak self] signals in - if let strongSelf = self, !signals.isEmpty { - if editingMessage { - strongSelf.editMessageMediaWithLegacySignals(signals) - } else { - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting) - } - } - }) - }), activateOnDisplay: activateOnDisplay) - controller.attemptItemSelection = { [weak strongSelf] item in - guard let strongSelf, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { - return false - } - - enum ItemType { - case gif - case image - case video - } - - var itemType: ItemType? - switch item { - case let .internalReference(reference): - if reference.type == "gif" { - itemType = .gif - } else if reference.type == "photo" { - itemType = .image - } else if reference.type == "video" { - itemType = .video - } - case let .externalReference(reference): - if reference.type == "gif" { - itemType = .gif - } else if reference.type == "photo" { - itemType = .image - } else if reference.type == "video" { - itemType = .video - } - } - - var bannedSendPhotos: (Int32, Bool)? - var bannedSendVideos: (Int32, Bool)? - var bannedSendGifs: (Int32, Bool)? - - if let channel = peer as? TelegramChannel { - if let value = channel.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = value - } - if let value = channel.hasBannedPermission(.banSendVideos) { - bannedSendVideos = value - } - if let value = channel.hasBannedPermission(.banSendGifs) { - bannedSendGifs = value - } - } else if let group = peer as? TelegramGroup { - if group.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = (Int32.max, false) - } - if group.hasBannedPermission(.banSendVideos) { - bannedSendVideos = (Int32.max, false) - } - if group.hasBannedPermission(.banSendGifs) { - bannedSendGifs = (Int32.max, false) - } - } - - if let itemType { - switch itemType { - case .image: - if bannedSendPhotos != nil { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - - return false - } - case .video: - if bannedSendVideos != nil { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - - return false - } - case .gif: - if bannedSendGifs != nil { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - - return false - } - } - } - - return true - } - controller.getCaptionPanelView = { [weak strongSelf] in - return strongSelf?.getCaptionPanelView() - } - present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } - }) - } - - func presentLocationPicker() { - guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { - return - } - let selfPeerId: PeerId - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - selfPeerId = peer.id - } else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.canBeAnonymous) { - selfPeerId = peer.id - } else { - selfPeerId = self.context.account.peerId - } - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: selfPeerId)) - |> deliverOnMainQueue).startStandalone(next: { [weak self] selfPeer in - guard let strongSelf = self, let selfPeer = selfPeer else { - return - } - let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages - let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: EnginePeer(peer), selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _, _, _, _ in - guard let strongSelf = self else { - return - } - let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } - }) - } - }, nil) - strongSelf.sendMessages([message]) - }) - strongSelf.effectiveNavigationController?.pushViewController(controller) - strongSelf.chatDisplayNode.dismissInput() - }) - } - - func presentContactPicker() { - let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: self.context, updatedPresentationData: self.updatedPresentationData, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true)) - contactsController.navigationPresentation = .modal - self.chatDisplayNode.dismissInput() - self.effectiveNavigationController?.pushViewController(contactsController) - self.controllerNavigationDisposable.set((contactsController.result - |> deliverOnMainQueue).startStrict(next: { [weak self] peers in - if let strongSelf = self, let (peers, _, _, _, _) = peers { - if peers.count > 1 { - var enqueueMessages: [EnqueueMessage] = [] - for peer in peers { - var media: TelegramMediaContact? - switch peer { - case let .peer(contact, _, _): - guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { - continue - } - let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - - let phone = contactData.basicData.phoneNumbers[0].value - media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil) - case let .deviceContact(_, basicData): - guard !basicData.phoneNumbers.isEmpty else { - continue - } - let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - - let phone = contactData.basicData.phoneNumbers[0].value - media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil) - } - - if let media = media { - let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } - }) - } - }, nil) - let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - enqueueMessages.append(message) - } - } - strongSelf.sendMessages(enqueueMessages) - } else if let peer = peers.first { - let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError> - switch peer { - case let .peer(contact, _, _): - guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { - return - } - let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - let context = strongSelf.context - dataSignal = (strongSelf.context.sharedContext.contactDataManager?.basicData() ?? .single([:])) - |> take(1) - |> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> in - var stableId: String? - let queryPhoneNumber = formatPhoneNumber(context: context, number: phoneNumber) - outer: for (id, data) in basicData { - for phoneNumber in data.phoneNumbers { - if formatPhoneNumber(context: context, number: phoneNumber.value) == queryPhoneNumber { - stableId = id - break outer - } - } - } - - if let stableId = stableId { - return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil)) - |> take(1) - |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in - return (contact, extendedData) - } - } else { - return .single((contact, contactData)) - } - } - case let .deviceContact(id, _): - dataSignal = (strongSelf.context.sharedContext.contactDataManager?.extendedData(stableId: id) ?? .single(nil)) - |> take(1) - |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in - return (nil, extendedData) - } - } - strongSelf.controllerNavigationDisposable.set((dataSignal - |> deliverOnMainQueue).startStrict(next: { peerAndContactData in - if let strongSelf = self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 { - if contactData.isPrimitive { - let phone = contactData.basicData.phoneNumbers[0].value - let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil) - let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } - }) - } - }, nil) - let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - strongSelf.sendMessages([message]) - } else { - let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in - guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else { - return - } - let phone = contactData.basicData.phoneNumbers[0].value - if let vCardData = contactData.serializedVCard() { - let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData) - let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } - }) - } - }, nil) - let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - strongSelf.sendMessages([message]) - } - }), completed: nil, cancelled: nil) - strongSelf.effectiveNavigationController?.pushViewController(contactController) - } - } - })) - } - } - })) - } - func displayPollSolution(solution: TelegramMediaPollResults.Solution, sourceNode: ASDisplayNode, isAutomatic: Bool) { var maybeFoundItemNode: ChatMessageItemView? self.chatDisplayNode.historyNode.forEachItemNode { itemNode in @@ -16952,6 +15398,67 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } + func displayGroupEmojiTooltip() { + guard let rect = self.chatDisplayNode.frameForEmojiButton(), self.effectiveNavigationController?.topViewController === self else { + return + } + guard let emojiPack = (self.peerView?.cachedData as? CachedChannelData)?.emojiPack, let thumbnailFileId = emojiPack.thumbnailFileId else { + return + } + //TODO:localize + let _ = (self.context.engine.stickers.resolveInlineStickers(fileIds: [thumbnailFileId]) + |> deliverOnMainQueue).start(next: { files in + guard let emojiFile = files.values.first else { + return + } + + let textFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0) + let boldTextFont = Font.bold(self.presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0) + let textColor = UIColor.white + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in + return nil + }) + + let text = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("All members of this group can\nuse the # **\(emojiPack.title)** pack", attributes: markdownAttributes)) + + let range = (text.string as NSString).range(of: "#") + if range.location != NSNotFound { + text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: emojiFile.fileId.id, file: emojiFile), range: range) + } + + let tooltipScreen = TooltipScreen( + context: self.context, + account: self.context.account, + sharedContext: self.context.sharedContext, + text: .attributedString(text: text), +// style: .customBlur(UIColor(rgb: 0x000000, alpha: 0.8), 2.0), + location: .point(rect.offsetBy(dx: 0.0, dy: -3.0), .bottom), + displayDuration: .default, + cornerRadius: 10.0, + shouldDismissOnTouch: { point, _ in + return .ignore + } + ) + self.present(tooltipScreen, in: .current) + + +// self.emojiTooltipController?.dismiss() +// let tooltipController = TooltipController(content: .attributedText(text), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, timeout: 3.0, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true, padding: 8.0) +// self.emojiTooltipController = tooltipController +// tooltipController.dismissed = { [weak self, weak tooltipController] _ in +// if let strongSelf = self, let tooltipController = tooltipController, strongSelf.emojiTooltipController === tooltipController { +// strongSelf.emojiTooltipController = nil +// } +// } +// self.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in +// if let strongSelf = self { +// return (strongSelf.chatDisplayNode, rect.offsetBy(dx: 0.0, dy: -3.0)) +// } +// return nil +// })) + }) + } + func displayChecksTooltip() { self.checksTooltipController?.dismiss() diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 103f8f5eee..25a93b4bd4 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -3381,6 +3381,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let effectiveInputText = effectivePresentationInterfaceState.interfaceState.composeInputState.inputText + let peerSpecificEmojiPack = (self.controller?.peerView?.cachedData as? CachedChannelData)?.emojiPack + var inlineStickers: [MediaId: Media] = [:] var firstLockedPremiumEmoji: TelegramMediaFile? var bubbleUpEmojiOrStickersetsById: [Int64: ItemCollectionId] = [:] @@ -3391,7 +3393,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let packId = value.interactivelySelectedFromPackId { bubbleUpEmojiOrStickersetsById[file.fileId.id] = packId } - if file.isPremiumEmoji && !self.chatPresentationInterfaceState.isPremium && self.chatPresentationInterfaceState.chatLocation.peerId != self.context.account.peerId { + + var isPeerSpecific = false + for attribute in file.attributes { + if case let .CustomEmoji(_, _, _, packReference) = attribute, case let .id(id, _) = packReference { + isPeerSpecific = id == peerSpecificEmojiPack?.id.id + } + } + + if file.isPremiumEmoji && !self.chatPresentationInterfaceState.isPremium && self.chatPresentationInterfaceState.chatLocation.peerId != self.context.account.peerId && !isPeerSpecific { if firstLockedPremiumEmoji == nil { firstLockedPremiumEmoji = file } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift new file mode 100644 index 0000000000..3c9ab0de89 --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -0,0 +1,1634 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import TelegramStringFormatting +import TelegramNotices +import PresentationDataUtils +import TextFormat +import UrlHandling +import AccountContext +import ChatPresentationInterfaceState +import LegacyComponents +import LegacyUI +import AttachmentUI +import MediaPickerUI +import LegacyCamera +import LegacyMediaPickerUI +import LocationUI +import WebSearchUI +import WebUI +import UndoUI +import ICloudResources +import PhoneNumberFormat +import ChatEntityKeyboardInputNode +import PremiumUI +import PremiumGiftAttachmentScreen +import TelegramCallsUI + +extension ChatControllerImpl { + enum AttachMenuSubject { + case `default` + case edit(mediaOptions: MessageMediaEditingOptions, mediaReference: AnyMediaReference) + case bot(id: PeerId, payload: String?, justInstalled: Bool) + case gift + } + + func presentAttachmentMenu(subject: AttachMenuSubject) { + guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return + } + + let context = self.context + + let inputIsActive = self.presentationInterfaceState.inputMode == .text + + self.chatDisplayNode.dismissInput() + + var banSendText: (Int32, Bool)? + var bannedSendPhotos: (Int32, Bool)? + var bannedSendVideos: (Int32, Bool)? + var bannedSendFiles: (Int32, Bool)? + + var canSendPolls = true + if let peer = peer as? TelegramUser, peer.botInfo == nil { + canSendPolls = false + } else if peer is TelegramSecretChat { + canSendPolls = false + } else if let channel = peer as? TelegramChannel { + if let value = channel.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = value + } + if let value = channel.hasBannedPermission(.banSendVideos) { + bannedSendVideos = value + } + if let value = channel.hasBannedPermission(.banSendFiles) { + bannedSendFiles = value + } + if let value = channel.hasBannedPermission(.banSendText) { + banSendText = value + } + if channel.hasBannedPermission(.banSendPolls) != nil { + canSendPolls = false + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendVideos) { + bannedSendVideos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendFiles) { + bannedSendFiles = (Int32.max, false) + } + if group.hasBannedPermission(.banSendText) { + banSendText = (Int32.max, false) + } + if group.hasBannedPermission(.banSendPolls) { + canSendPolls = false + } + } + + var availableButtons: [AttachmentButtonType] = [.gallery, .file] + if banSendText == nil { + availableButtons.append(.location) + availableButtons.append(.contact) + } + if canSendPolls { + availableButtons.insert(.poll, at: max(0, availableButtons.count - 1)) + } + + let presentationData = self.presentationData + + var isScheduledMessages = false + if case .scheduledMessages = self.presentationInterfaceState.subject { + isScheduledMessages = true + } + + var peerType: AttachMenuBots.Bot.PeerFlags = [] + if let user = peer as? TelegramUser { + if let _ = user.botInfo { + peerType.insert(.bot) + } else { + peerType.insert(.user) + } + } else if let _ = peer as? TelegramGroup { + peerType = .group + } else if let channel = peer as? TelegramChannel { + if case .broadcast = channel.info { + peerType = .channel + } else { + peerType = .group + } + } + + let buttons: Signal<([AttachmentButtonType], [AttachmentButtonType], AttachmentButtonType?), NoError> + if !isScheduledMessages && !peer.isDeleted { + buttons = self.context.engine.messages.attachMenuBots() + |> map { attachMenuBots in + var buttons = availableButtons + var allButtons = availableButtons + var initialButton: AttachmentButtonType? + switch subject { + case .default: + initialButton = .gallery + case .edit: + break + case .gift: + initialButton = .gift + default: + break + } + + for bot in attachMenuBots.reversed() { + var peerType = peerType + if bot.peer.id == peer.id { + peerType.insert(.sameBot) + peerType.remove(.bot) + } + let button: AttachmentButtonType = .app(bot) + if !bot.peerTypes.intersection(peerType).isEmpty { + buttons.insert(button, at: 1) + + if case let .bot(botId, _, _) = subject { + if initialButton == nil && bot.peer.id == botId { + initialButton = button + } + } + } + allButtons.insert(button, at: 1) + } + + return (buttons, allButtons, initialButton) + } + } else { + buttons = .single((availableButtons, availableButtons, .gallery)) + } + + let dataSettings = self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in + let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) + return entry ?? GeneratedMediaStoreSettings.defaultSettings + } + + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) + let premiumGiftOptions: [CachedPremiumGiftOption] + if !premiumConfiguration.isPremiumDisabled && premiumConfiguration.showPremiumGiftInAttachMenu, let user = peer as? TelegramUser, !user.isPremium && !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) { + premiumGiftOptions = self.presentationInterfaceState.premiumGiftOptions + } else { + premiumGiftOptions = [] + } + + let _ = combineLatest(queue: Queue.mainQueue(), buttons, dataSettings).startStandalone(next: { [weak self] buttonsAndInitialButton, dataSettings in + guard let strongSelf = self else { + return + } + + var (buttons, allButtons, initialButton) = buttonsAndInitialButton + if !premiumGiftOptions.isEmpty { + buttons.insert(.gift, at: 1) + } + + guard let initialButton = initialButton else { + if case let .bot(botId, botPayload, botJustInstalled) = subject { + if let button = allButtons.first(where: { button in + if case let .app(bot) = button, bot.peer.id == botId { + return true + } else { + return false + } + }), case let .app(bot) = button { + let content: UndoOverlayContent + if botJustInstalled { + if bot.flags.contains(.showInSettings) { + content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(bot.shortName).string, timeout: 5.0, customUndoText: nil) + } else { + content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsAdded(bot.shortName).string, timeout: 5.0, customUndoText: nil) + } + } else { + content = .info(title: nil, text: strongSelf.presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError, timeout: nil, customUndoText: nil) + } + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current) + } else { + let _ = (context.engine.messages.getAttachMenuBot(botId: botId) + |> deliverOnMainQueue).startStandalone(next: { bot in + let controller = webAppTermsAlertController(context: context, updatedPresentationData: strongSelf.updatedPresentationData, bot: bot, completion: { allowWrite in + let _ = (context.engine.messages.addBotToAttachMenu(botId: botId, allowWrite: allowWrite) + |> deliverOnMainQueue).startStandalone(error: { _ in + + }, completed: { + strongSelf.presentAttachmentBot(botId: botId, payload: botPayload, justInstalled: true) + }) + }) + strongSelf.present(controller, in: .window(.root)) + }, error: { _ in + strongSelf.present(textAlertController(context: context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }) + } + } + return + } + + let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText + + let currentMediaController = Atomic(value: nil) + let currentFilesController = Atomic(value: nil) + let currentLocationController = Atomic(value: nil) + + strongSelf.canReadHistory.set(false) + + let attachmentController = AttachmentController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: strongSelf.chatLocation, isScheduledMessages: isScheduledMessages, buttons: buttons, initialButton: initialButton, makeEntityInputView: { [weak self] in + guard let strongSelf = self else { + return nil + } + return EntityInputView(context: strongSelf.context, isDark: false, areCustomEmojiEnabled: strongSelf.presentationInterfaceState.customEmojiAvailable) + }) + attachmentController.didDismiss = { [weak self] in + self?.attachmentController = nil + self?.canReadHistory.set(true) + } + attachmentController.getSourceRect = { [weak self] in + if let strongSelf = self { + return strongSelf.chatDisplayNode.frameForAttachmentButton()?.offsetBy(dx: strongSelf.chatDisplayNode.supernode?.frame.minX ?? 0.0, dy: 0.0) + } else { + return nil + } + } + attachmentController.requestController = { [weak self, weak attachmentController] type, completion in + guard let strongSelf = self else { + return + } + switch type { + case .gallery: + strongSelf.controllerNavigationDisposable.set(nil) + let existingController = currentMediaController.with { $0 } + if let controller = existingController { + completion(controller, controller.mediaPickerContext) + controller.prepareForReuse() + return + } + strongSelf.presentMediaPicker(saveEditedPhotos: dataSettings.storeEditedPhotos, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, present: { controller, mediaPickerContext in + let _ = currentMediaController.swap(controller) + if !inputText.string.isEmpty { + mediaPickerContext?.setCaption(inputText) + } + completion(controller, mediaPickerContext) + }, updateMediaPickerContext: { [weak attachmentController] mediaPickerContext in + attachmentController?.mediaPickerContext = mediaPickerContext + }, completion: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in + if !inputText.string.isEmpty { + self?.clearInputText() + } + self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) + }) + case .file: + strongSelf.controllerNavigationDisposable.set(nil) + let existingController = currentFilesController.with { $0 } + if let controller = existingController { + completion(controller, controller.mediaPickerContext) + controller.prepareForReuse() + return + } + let controller = strongSelf.context.sharedContext.makeAttachmentFileController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bannedSendMedia: bannedSendFiles, presentGallery: { [weak self, weak attachmentController] in + attachmentController?.dismiss(animated: true) + self?.presentFileGallery() + }, presentFiles: { [weak self, weak attachmentController] in + attachmentController?.dismiss(animated: true) + self?.presentICloudFileGallery() + }, send: { [weak self] mediaReference in + guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else { + return + } + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: strongSelf.transformEnqueueMessages([message])) + |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in + if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages { + strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() + } + }) + }) + if let controller = controller as? AttachmentFileControllerImpl { + let _ = currentFilesController.swap(controller) + completion(controller, controller.mediaPickerContext) + } + case .location: + strongSelf.controllerNavigationDisposable.set(nil) + let existingController = currentLocationController.with { $0 } + if let controller = existingController { + completion(controller, controller.mediaPickerContext) + controller.prepareForReuse() + return + } + let selfPeerId: PeerId + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + selfPeerId = peer.id + } else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.canBeAnonymous) { + selfPeerId = peer.id + } else { + selfPeerId = strongSelf.context.account.peerId + } + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: selfPeerId)) + |> deliverOnMainQueue).startStandalone(next: { [weak self] selfPeer in + guard let strongSelf = self, let selfPeer = selfPeer else { + return + } + let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages + let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: EnginePeer(peer), selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _, _, _, _ in + guard let strongSelf = self else { + return + } + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + }) + } + }, nil) + strongSelf.sendMessages([message]) + }) + completion(controller, controller.mediaPickerContext) + + let _ = currentLocationController.swap(controller) + }) + case .contact: + let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true, requirePhoneNumbers: true)) + contactsController.presentScheduleTimePicker = { [weak self] completion in + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(completion: completion) + } + } + contactsController.navigationPresentation = .modal + completion(contactsController, contactsController.mediaPickerContext) + strongSelf.controllerNavigationDisposable.set((contactsController.result + |> deliverOnMainQueue).startStrict(next: { [weak self] peers in + if let strongSelf = self, let (peers, _, silent, scheduleTime, text) = peers { + var textEnqueueMessage: EnqueueMessage? + if let text = text, text.length > 0 { + var attributes: [MessageAttribute] = [] + let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) + if !entities.isEmpty { + attributes.append(TextEntitiesMessageAttribute(entities: entities)) + } + textEnqueueMessage = .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + } + if peers.count > 1 { + var enqueueMessages: [EnqueueMessage] = [] + if let textEnqueueMessage = textEnqueueMessage { + enqueueMessages.append(textEnqueueMessage) + } + for peer in peers { + var media: TelegramMediaContact? + switch peer { + case let .peer(contact, _, _): + guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + continue + } + let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") + + let phone = contactData.basicData.phoneNumbers[0].value + media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil) + case let .deviceContact(_, basicData): + guard !basicData.phoneNumbers.isEmpty else { + continue + } + let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") + + let phone = contactData.basicData.phoneNumbers[0].value + media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil) + } + + if let media = media { + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + }) + } + }, nil) + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + enqueueMessages.append(message) + } + } + strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) + } else if let peer = peers.first { + let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError> + switch peer { + case let .peer(contact, _, _): + guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + return + } + let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") + let context = strongSelf.context + dataSignal = (strongSelf.context.sharedContext.contactDataManager?.basicData() ?? .single([:])) + |> take(1) + |> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> in + var stableId: String? + let queryPhoneNumber = formatPhoneNumber(context: context, number: phoneNumber) + outer: for (id, data) in basicData { + for phoneNumber in data.phoneNumbers { + if formatPhoneNumber(context: context, number: phoneNumber.value) == queryPhoneNumber { + stableId = id + break outer + } + } + } + + if let stableId = stableId { + return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil)) + |> take(1) + |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in + return (contact, extendedData) + } + } else { + return .single((contact, contactData)) + } + } + case let .deviceContact(id, _): + dataSignal = (strongSelf.context.sharedContext.contactDataManager?.extendedData(stableId: id) ?? .single(nil)) + |> take(1) + |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in + return (nil, extendedData) + } + } + strongSelf.controllerNavigationDisposable.set((dataSignal + |> deliverOnMainQueue).startStrict(next: { peerAndContactData in + if let strongSelf = self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 { + if contactData.isPrimitive { + let phone = contactData.basicData.phoneNumbers[0].value + let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil) + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + }) + } + }, nil) + + var enqueueMessages: [EnqueueMessage] = [] + if let textEnqueueMessage = textEnqueueMessage { + enqueueMessages.append(textEnqueueMessage) + } + enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) + } else { + let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in + guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else { + return + } + let phone = contactData.basicData.phoneNumbers[0].value + if let vCardData = contactData.serializedVCard() { + let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData) + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + }) + } + }, nil) + + var enqueueMessages: [EnqueueMessage] = [] + if let textEnqueueMessage = textEnqueueMessage { + enqueueMessages.append(textEnqueueMessage) + } + enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) + } + }), completed: nil, cancelled: nil) + strongSelf.effectiveNavigationController?.pushViewController(contactController) + } + } + })) + } + } + })) + case .poll: + let controller = strongSelf.configurePollCreation() + completion(controller, controller?.mediaPickerContext) + strongSelf.controllerNavigationDisposable.set(nil) + case .gift: + let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions + if !premiumGiftOptions.isEmpty { + let controller = PremiumGiftAttachmentScreen(context: context, peerIds: [peer.id], options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in + if let strongSelf = self { + strongSelf.push(c) + } + }, completion: { [weak self] in + if let strongSelf = self { + strongSelf.hintPlayNextOutgoingGift() + strongSelf.attachmentController?.dismiss(animated: true) + } + }) + completion(controller, controller.mediaPickerContext) + strongSelf.controllerNavigationDisposable.set(nil) + + let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: context.sharedContext.accountManager, peerId: peer.id).startStandalone() + } + case let .app(bot): + var payload: String? + var fromAttachMenu = true + if case let .bot(_, botPayload, _) = subject { + payload = botPayload + fromAttachMenu = false + } + let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false) + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId) + controller.openUrl = { [weak self] url, concealed, commit in + self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) + } + controller.getNavigationController = { [weak self] in + return self?.effectiveNavigationController + } + controller.completion = { [weak self] in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + }) + strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() + } + } + completion(controller, controller.mediaPickerContext) + strongSelf.controllerNavigationDisposable.set(nil) + + if bot.flags.contains(.notActivated) { + let alertController = webAppTermsAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bot: bot, completion: { [weak self] allowWrite in + guard let self else { + return + } + if bot.flags.contains(.showInSettingsDisclaimer) { + let _ = self.context.engine.messages.acceptAttachMenuBotDisclaimer(botId: bot.peer.id).startStandalone() + } + let _ = (self.context.engine.messages.addBotToAttachMenu(botId: bot.peer.id, allowWrite: allowWrite) + |> deliverOnMainQueue).startStandalone(error: { _ in + }, completed: { [weak controller] in + controller?.refresh() + }) + }, + dismissed: { + strongSelf.attachmentController?.dismiss(animated: true) + }) + strongSelf.present(alertController, in: .window(.root)) + } + default: + break + } + } + let present = { + attachmentController.navigationPresentation = .flatModal + strongSelf.push(attachmentController) + strongSelf.attachmentController = attachmentController + + if case let .bot(botId, _, botJustInstalled) = subject, botJustInstalled { + if let button = allButtons.first(where: { button in + if case let .app(bot) = button, bot.peer.id == botId { + return true + } else { + return false + } + }), case let .app(bot) = button { + Queue.mainQueue().after(0.3) { + let content: UndoOverlayContent + if bot.flags.contains(.showInSettings) { + content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(bot.shortName).string, timeout: 5.0, customUndoText: nil) + } else { + content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsAdded(bot.shortName).string, timeout: 5.0, customUndoText: nil) + } + attachmentController.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current) + } + } + } + } + + if inputIsActive { + Queue.mainQueue().after(0.15, { + present() + }) + } else { + present() + } + }) + } + + func oldPresentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) { + let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in + let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) + return entry ?? GeneratedMediaStoreSettings.defaultSettings + } + |> deliverOnMainQueue).startStandalone(next: { [weak self] settings in + guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { + return + } + strongSelf.chatDisplayNode.dismissInput() + + var bannedSendMedia: (Int32, Bool)? + var canSendPolls = true + if let channel = peer as? TelegramChannel { + if let value = channel.hasBannedPermission(.banSendMedia) { + bannedSendMedia = value + } + if channel.hasBannedPermission(.banSendPolls) != nil { + canSendPolls = false + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendMedia) { + bannedSendMedia = (Int32.max, false) + } + if group.hasBannedPermission(.banSendPolls) { + canSendPolls = false + } + } + + if editMediaOptions == nil, let (untilDate, personal) = bannedSendMedia { + let banDescription: String + if untilDate != 0 && untilDate != Int32.max { + banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMediaTimed(stringForFullDate(timestamp: untilDate, strings: strongSelf.presentationInterfaceState.strings, dateTimeFormat: strongSelf.presentationInterfaceState.dateTimeFormat)).string + } else if personal { + banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMedia + } else { + banDescription = strongSelf.presentationInterfaceState.strings.Conversation_DefaultRestrictedMedia + } + + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) + var items: [ActionSheetItem] = [] + items.append(ActionSheetTextItem(title: banDescription)) + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_Location, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + self?.presentLocationPicker() + })) + if canSendPolls { + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.AttachmentMenu_Poll, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let controller = self?.configurePollCreation() { + self?.effectiveNavigationController?.pushViewController(controller) + } + })) + } + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_Contact, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + self?.presentContactPicker() + })) + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.present(actionSheet, in: .window(.root)) + + return + } + + let legacyController = LegacyController(presentation: .custom, theme: strongSelf.presentationData.theme, initialLayout: strongSelf.validLayout) + legacyController.blocksBackgroundWhenInOverlay = true + legacyController.acceptsFocusWhenInOverlay = true + legacyController.statusBar.statusBarStyle = .Ignore + legacyController.controllerLoaded = { [weak legacyController] in + legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true + } + + let emptyController = LegacyEmptyController(context: legacyController.context)! + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + legacyController.bind(controller: navigationController) + + legacyController.enableSizeClassSignal = true + + let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText + let menuEditMediaOptions = editMediaOptions.flatMap { options -> LegacyAttachmentMenuMediaEditing in + var result: LegacyAttachmentMenuMediaEditing = .none + if options.contains(.imageOrVideo) { + result = .imageOrVideo(editMediaReference) + } + return result + } + + var slowModeEnabled = false + if let channel = peer as? TelegramChannel, channel.isRestrictedBySlowmode { + slowModeEnabled = true + } + + let controller = legacyAttachmentMenu(context: strongSelf.context, peer: peer, threadTitle: strongSelf.threadInfo?.title, chatLocation: strongSelf.chatLocation, editMediaOptions: menuEditMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, canSendPolls: canSendPolls, updatedPresentationData: strongSelf.updatedPresentationData, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, initialCaption: inputText, openGallery: { + self?.presentOldMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, completion: { signals, silentPosting, scheduleTime in + if !inputText.string.isEmpty { + strongSelf.clearInputText() + } + if editMediaOptions != nil { + self?.editMessageMediaWithLegacySignals(signals) + } else { + self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) + } + }) + }, openCamera: { [weak self] cameraView, menuController in + if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + var enablePhoto = true + var enableVideo = true + + if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall { + enableVideo = false + } + + var bannedSendPhotos: (Int32, Bool)? + var bannedSendVideos: (Int32, Bool)? + + if let channel = peer as? TelegramChannel { + if let value = channel.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = value + } + if let value = channel.hasBannedPermission(.banSendVideos) { + bannedSendVideos = value + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendVideos) { + bannedSendVideos = (Int32.max, false) + } + } + + if bannedSendPhotos != nil { + enablePhoto = false + } + if bannedSendVideos != nil { + enableVideo = false + } + + presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: peer.id.namespace != Namespaces.Peer.SecretChat, mediaGrouping: true, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in + if let strongSelf = self { + if editMediaOptions != nil { + strongSelf.editMessageMediaWithLegacySignals(signals!) + } else { + strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) + } + if !inputText.string.isEmpty { + strongSelf.clearInputText() + } + } + }, recognizedQRCode: { [weak self] code in + if let strongSelf = self { + if let (host, port, username, password, secret) = parseProxyUrl(code) { + strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil) + } + } + }, presentSchedulePicker: { [weak self] _, done in + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in + if let strongSelf = self { + done(time) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() + } + } + }) + } + }, presentTimerPicker: { [weak self] done in + if let strongSelf = self { + strongSelf.presentTimerPicker(style: .media, completion: { time in + done(time) + }) + } + }, getCaptionPanelView: { [weak self] in + return self?.getCaptionPanelView() + }) + } + }, openFileGallery: { + self?.presentFileMediaPickerOptions(editingMessage: editMediaOptions != nil) + }, openWebSearch: { [weak self] in + self?.presentWebSearch(editingMessage: editMediaOptions != nil, attachment: false, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }) + }, openMap: { + self?.presentLocationPicker() + }, openContacts: { + self?.presentContactPicker() + }, openPoll: { + if let controller = self?.configurePollCreation() { + self?.effectiveNavigationController?.pushViewController(controller) + } + }, presentSelectionLimitExceeded: { + guard let strongSelf = self else { + return + } + let text: String + if slowModeEnabled { + text = strongSelf.presentationData.strings.Chat_SlowmodeAttachmentLimitReached + } else { + text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached + } + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, presentCantSendMultipleFiles: { + guard let strongSelf = self else { + return + } + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentMultipleFilesDisabled, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, presentJpegConversionAlert: { completion in + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.MediaPicker_KeepHeic, action: { + completion(false) + }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.MediaPicker_ConvertToJpeg, action: { + completion(true) + })], actionLayout: .vertical), in: .window(.root)) + }, presentSchedulePicker: { [weak self] _, done in + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in + if let strongSelf = self { + done(time) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() + } + } + }) + } + }, presentTimerPicker: { [weak self] done in + if let strongSelf = self { + strongSelf.presentTimerPicker(style: .media, completion: { time in + done(time) + }) + } + }, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in + guard let strongSelf = self else { + completion() + return + } + if !inputText.string.isEmpty { + strongSelf.clearInputText() + } + if editMediaOptions != nil { + strongSelf.editMessageMediaWithLegacySignals(signals!) + completion() + } else { + let immediateCompletion = getAnimatedTransitionSource == nil + strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: { + if !immediateCompletion { + completion() + } + }) + if immediateCompletion { + completion() + } + } + }, selectRecentlyUsedInlineBot: { [weak self] peer in + if let strongSelf = self, let addressName = peer.addressName { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState({ $0.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " "))) }).updatedInputMode({ _ in + return .text + }) + }) + } + }, getCaptionPanelView: { [weak self] in + return self?.getCaptionPanelView() + }, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }) + controller.didDismiss = { [weak legacyController] _ in + legacyController?.dismiss() + } + controller.customRemoveFromParentViewController = { [weak legacyController] in + legacyController?.dismiss() + } + + legacyController.blocksBackgroundWhenInOverlay = true + strongSelf.present(legacyController, in: .window(.root)) + controller.present(in: emptyController, sourceView: nil, animated: true) + + let presentationDisposable = strongSelf.updatedPresentationData.1.startStrict(next: { [weak controller] presentationData in + if let controller = controller { + controller.pallete = legacyMenuPaletteFromTheme(presentationData.theme, forceDark: false) + } + }) + legacyController.disposables.add(presentationDisposable) + }) + } + + func presentFileGallery(editingMessage: Bool = false) { + self.presentOldMediaPicker(fileMode: true, editingMedia: editingMessage, completion: { [weak self] signals, silentPosting, scheduleTime in + if editingMessage { + self?.editMessageMediaWithLegacySignals(signals) + } else { + self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) + } + }) + } + + func presentICloudFileGallery(editingMessage: Bool = false) { + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), + TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), + TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] result in + guard let strongSelf = self else { + return + } + let (accountPeer, limits, premiumLimits) = result + let isPremium = accountPeer?.isPremium ?? false + + strongSelf.present(legacyICloudFilePicker(theme: strongSelf.presentationData.theme, completion: { [weak self] urls in + if let strongSelf = self, !urls.isEmpty { + var signals: [Signal] = [] + for url in urls { + signals.append(iCloudFileDescription(url)) + } + strongSelf.enqueueMediaMessageDisposable.set((combineLatest(signals) + |> deliverOnMainQueue).startStrict(next: { results in + if let strongSelf = self { + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + + for item in results { + if let item = item { + if item.fileSize > Int64(premiumLimits.maxUploadFileParts) * 512 * 1024 { + let controller = PremiumLimitScreen(context: strongSelf.context, subject: .files, count: 4, action: { + return true + }) + strongSelf.push(controller) + return + } else if item.fileSize > Int64(limits.maxUploadFileParts) * 512 * 1024 && !isPremium { + let context = strongSelf.context + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .files, count: 2, action: { + replaceImpl?(PremiumIntroScreen(context: context, source: .upload)) + return true + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + strongSelf.push(controller) + return + } + } + } + + var groupingKey: Int64? + var fileTypes: (music: Bool, other: Bool) = (false, false) + if results.count > 1 { + for item in results { + if let item = item { + let pathExtension = (item.fileName as NSString).pathExtension.lowercased() + if ["mp3", "m4a"].contains(pathExtension) { + fileTypes.music = true + } else { + fileTypes.other = true + } + } + } + } + if fileTypes.music != fileTypes.other { + groupingKey = Int64.random(in: Int64.min ... Int64.max) + } + + var messages: [EnqueueMessage] = [] + for item in results { + if let item = item { + let fileId = Int64.random(in: Int64.min ... Int64.max) + let mimeType = guessMimeTypeByFileExtension((item.fileName as NSString).pathExtension) + var previewRepresentations: [TelegramMediaImageRepresentation] = [] + if mimeType.hasPrefix("image/") || mimeType == "application/pdf" { + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: ICloudFileResource(urlData: item.urlData, thumbnail: true), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) + } + var attributes: [TelegramMediaFileAttribute] = [] + attributes.append(.FileName(fileName: item.fileName)) + if let audioMetadata = item.audioMetadata { + attributes.append(.Audio(isVoice: false, duration: audioMetadata.duration, title: audioMetadata.title, performer: audioMetadata.performer, waveform: nil)) + } + + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) + messages.append(message) + } + if let _ = groupingKey, messages.count % 10 == 0 { + groupingKey = Int64.random(in: Int64.min ... Int64.max) + } + } + + if !messages.isEmpty { + if editingMessage { + strongSelf.editMessageMediaWithMessages(messages) + } else { + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + }) + } + }, nil) + strongSelf.sendMessages(messages) + } + } + } + })) + } + }), in: .window(.root)) + }) + } + + func presentFileMediaPickerOptions(editingMessage: Bool) { + let actionSheet = ActionSheetController(presentationData: self.presentationData) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Conversation_FilePhotoOrVideo, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.presentFileGallery(editingMessage: editingMessage) + } + }), + ActionSheetButtonItem(title: self.presentationData.strings.Conversation_FileICloudDrive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.presentICloudFileGallery(editingMessage: editingMessage) + } + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + self.chatDisplayNode.dismissInput() + self.present(actionSheet, in: .window(.root)) + } + + func presentMediaPicker(subject: MediaPickerScreen.Subject = .assets(nil, .default), saveEditedPhotos: Bool, bannedSendPhotos: (Int32, Bool)?, bannedSendVideos: (Int32, Bool)?, present: @escaping (MediaPickerScreen, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping ([Any], Bool, Int32?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void) { + guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return + } + var isScheduledMessages = false + if case .scheduledMessages = self.presentationInterfaceState.subject { + isScheduledMessages = true + } + let controller = MediaPickerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadTitle: self.threadInfo?.title, chatLocation: self.chatLocation, isScheduledMessages: isScheduledMessages, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, subject: subject, saveEditedPhotos: saveEditedPhotos) + let mediaPickerContext = controller.mediaPickerContext + controller.openCamera = { [weak self] cameraView in + self?.openCamera(cameraView: cameraView) + } + controller.presentWebSearch = { [weak self, weak controller] mediaGroups, activateOnDisplay in + self?.presentWebSearch(editingMessage: false, attachment: true, activateOnDisplay: activateOnDisplay, present: { [weak controller] c, a in + controller?.present(c, in: .current) + if let webSearchController = c as? WebSearchController { + webSearchController.searchingUpdated = { [weak mediaGroups] searching in + if let mediaGroups = mediaGroups, mediaGroups.isNodeLoaded { + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + transition.updateAlpha(node: mediaGroups.displayNode, alpha: searching ? 0.0 : 1.0) + mediaGroups.displayNode.isUserInteractionEnabled = !searching + } + } + webSearchController.present(mediaGroups, in: .current) + webSearchController.dismissed = { + updateMediaPickerContext(mediaPickerContext) + } + controller?.webSearchController = webSearchController + updateMediaPickerContext(webSearchController.mediaPickerContext) + } + }) + } + controller.presentSchedulePicker = { [weak self] media, done in + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] time in + if let strongSelf = self { + done(time) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() + } + } + }) + } + } + controller.presentTimerPicker = { [weak self] done in + if let strongSelf = self { + strongSelf.presentTimerPicker(style: .media, completion: { time in + done(time) + }) + } + } + controller.getCaptionPanelView = { [weak self] in + return self?.getCaptionPanelView() + } + controller.legacyCompletion = { signals, silently, scheduleTime, getAnimatedTransitionSource, sendCompletion in + completion(signals, silently, scheduleTime, getAnimatedTransitionSource, sendCompletion) + } + present(controller, mediaPickerContext) + } + + func presentOldMediaPicker(fileMode: Bool, editingMedia: Bool, completion: @escaping ([Any], Bool, Int32) -> Void) { + let engine = self.context.engine + let _ = (self.context.sharedContext.accountManager.transaction { transaction -> Signal<(GeneratedMediaStoreSettings, EngineConfiguration.SearchBots), NoError> in + let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) + + return engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) + |> map { configuration -> (GeneratedMediaStoreSettings, EngineConfiguration.SearchBots) in + return (entry ?? GeneratedMediaStoreSettings.defaultSettings, configuration) + } + } + |> switchToLatest + |> deliverOnMainQueue).startStandalone(next: { [weak self] settings, searchBotsConfiguration in + guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { + return + } + let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText + var selectionLimit: Int = 100 + var slowModeEnabled = false + if let channel = peer as? TelegramChannel, channel.isRestrictedBySlowmode { + selectionLimit = 10 + slowModeEnabled = true + } + + let _ = legacyAssetPicker(context: strongSelf.context, presentationData: strongSelf.presentationData, editingMedia: editingMedia, fileMode: fileMode, peer: peer, threadTitle: strongSelf.threadInfo?.title, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, selectionLimit: selectionLimit).startStandalone(next: { generator in + if let strongSelf = self { + let legacyController = LegacyController(presentation: fileMode ? .navigation : .custom, theme: strongSelf.presentationData.theme, initialLayout: strongSelf.validLayout) + legacyController.navigationPresentation = .modal + legacyController.statusBar.statusBarStyle = strongSelf.presentationData.theme.rootController.statusBarStyle.style + legacyController.controllerLoaded = { [weak legacyController] in + legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true + legacyController?.view.disablesInteractiveModalDismiss = true + } + let controller = generator(legacyController.context) + + legacyController.bind(controller: controller) + legacyController.deferScreenEdgeGestures = [.top] + + configureLegacyAssetPicker(controller, context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak self, weak legacyController] in + if let strongSelf = self { + let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { results, selectionState, editingState, silentPosting in + if let legacyController = legacyController { + legacyController.dismiss() + } + legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { result in + if let strongSelf = self { + strongSelf.enqueueChatContextResult(results, result, hideVia: true) + } + }, enqueueMediaMessages: { signals in + if let strongSelf = self { + if editingMedia { + strongSelf.editMessageMediaWithLegacySignals(signals) + } else { + strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting) + } + } + }) + })) + controller.getCaptionPanelView = { [weak self] in + return self?.getCaptionPanelView() + } + strongSelf.effectiveNavigationController?.pushViewController(controller) + } + }, presentSelectionLimitExceeded: { + guard let strongSelf = self else { + return + } + + let text: String + if slowModeEnabled { + text = strongSelf.presentationData.strings.Chat_SlowmodeAttachmentLimitReached + } else { + text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached + } + + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, presentSchedulePicker: { [weak self] media, done in + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] time in + if let strongSelf = self { + done(time) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() + } + } + }) + } + }, presentTimerPicker: { [weak self] done in + if let strongSelf = self { + strongSelf.presentTimerPicker(style: .media, completion: { time in + done(time) + }) + } + }, getCaptionPanelView: { [weak self] in + return self?.getCaptionPanelView() + }) + controller.descriptionGenerator = legacyAssetPickerItemGenerator() + controller.completionBlock = { [weak legacyController] signals, silentPosting, scheduleTime in + if let legacyController = legacyController { + legacyController.dismiss(animated: true) + completion(signals!, silentPosting, scheduleTime) + } + } + controller.dismissalBlock = { [weak legacyController] in + if let legacyController = legacyController { + legacyController.dismiss(animated: true) + } + } + strongSelf.chatDisplayNode.dismissInput() + strongSelf.effectiveNavigationController?.pushViewController(legacyController) + } + }) + }) + } + + func presentWebSearch(editingMessage: Bool, attachment: Bool, activateOnDisplay: Bool = true, present: @escaping (ViewController, Any?) -> Void) { + guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return + } + + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) + |> deliverOnMainQueue).startStandalone(next: { [weak self] configuration in + if let strongSelf = self { + let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: configuration, mode: .media(attachment: attachment, completion: { [weak self] results, selectionState, editingState, silentPosting in + self?.attachmentController?.dismiss(animated: true, completion: nil) + legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self] result in + if let strongSelf = self { + strongSelf.enqueueChatContextResult(results, result, hideVia: true) + } + }, enqueueMediaMessages: { [weak self] signals in + if let strongSelf = self, !signals.isEmpty { + if editingMessage { + strongSelf.editMessageMediaWithLegacySignals(signals) + } else { + strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting) + } + } + }) + }), activateOnDisplay: activateOnDisplay) + controller.attemptItemSelection = { [weak strongSelf] item in + guard let strongSelf, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { + return false + } + + enum ItemType { + case gif + case image + case video + } + + var itemType: ItemType? + switch item { + case let .internalReference(reference): + if reference.type == "gif" { + itemType = .gif + } else if reference.type == "photo" { + itemType = .image + } else if reference.type == "video" { + itemType = .video + } + case let .externalReference(reference): + if reference.type == "gif" { + itemType = .gif + } else if reference.type == "photo" { + itemType = .image + } else if reference.type == "video" { + itemType = .video + } + } + + var bannedSendPhotos: (Int32, Bool)? + var bannedSendVideos: (Int32, Bool)? + var bannedSendGifs: (Int32, Bool)? + + if let channel = peer as? TelegramChannel { + if let value = channel.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = value + } + if let value = channel.hasBannedPermission(.banSendVideos) { + bannedSendVideos = value + } + if let value = channel.hasBannedPermission(.banSendGifs) { + bannedSendGifs = value + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendVideos) { + bannedSendVideos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendGifs) { + bannedSendGifs = (Int32.max, false) + } + } + + if let itemType { + switch itemType { + case .image: + if bannedSendPhotos != nil { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + return false + } + case .video: + if bannedSendVideos != nil { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + return false + } + case .gif: + if bannedSendGifs != nil { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + return false + } + } + } + + return true + } + controller.getCaptionPanelView = { [weak strongSelf] in + return strongSelf?.getCaptionPanelView() + } + present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + }) + } + + func presentLocationPicker() { + guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return + } + let selfPeerId: PeerId + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + selfPeerId = peer.id + } else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.canBeAnonymous) { + selfPeerId = peer.id + } else { + selfPeerId = self.context.account.peerId + } + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: selfPeerId)) + |> deliverOnMainQueue).startStandalone(next: { [weak self] selfPeer in + guard let strongSelf = self, let selfPeer = selfPeer else { + return + } + let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages + let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: EnginePeer(peer), selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _, _, _, _ in + guard let strongSelf = self else { + return + } + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + }) + } + }, nil) + strongSelf.sendMessages([message]) + }) + strongSelf.effectiveNavigationController?.pushViewController(controller) + strongSelf.chatDisplayNode.dismissInput() + }) + } + + func presentContactPicker() { + let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: self.context, updatedPresentationData: self.updatedPresentationData, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true)) + contactsController.navigationPresentation = .modal + self.chatDisplayNode.dismissInput() + self.effectiveNavigationController?.pushViewController(contactsController) + self.controllerNavigationDisposable.set((contactsController.result + |> deliverOnMainQueue).startStrict(next: { [weak self] peers in + if let strongSelf = self, let (peers, _, _, _, _) = peers { + if peers.count > 1 { + var enqueueMessages: [EnqueueMessage] = [] + for peer in peers { + var media: TelegramMediaContact? + switch peer { + case let .peer(contact, _, _): + guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + continue + } + let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") + + let phone = contactData.basicData.phoneNumbers[0].value + media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil) + case let .deviceContact(_, basicData): + guard !basicData.phoneNumbers.isEmpty else { + continue + } + let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") + + let phone = contactData.basicData.phoneNumbers[0].value + media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil) + } + + if let media = media { + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + }) + } + }, nil) + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + enqueueMessages.append(message) + } + } + strongSelf.sendMessages(enqueueMessages) + } else if let peer = peers.first { + let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError> + switch peer { + case let .peer(contact, _, _): + guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + return + } + let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") + let context = strongSelf.context + dataSignal = (strongSelf.context.sharedContext.contactDataManager?.basicData() ?? .single([:])) + |> take(1) + |> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> in + var stableId: String? + let queryPhoneNumber = formatPhoneNumber(context: context, number: phoneNumber) + outer: for (id, data) in basicData { + for phoneNumber in data.phoneNumbers { + if formatPhoneNumber(context: context, number: phoneNumber.value) == queryPhoneNumber { + stableId = id + break outer + } + } + } + + if let stableId = stableId { + return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil)) + |> take(1) + |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in + return (contact, extendedData) + } + } else { + return .single((contact, contactData)) + } + } + case let .deviceContact(id, _): + dataSignal = (strongSelf.context.sharedContext.contactDataManager?.extendedData(stableId: id) ?? .single(nil)) + |> take(1) + |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in + return (nil, extendedData) + } + } + strongSelf.controllerNavigationDisposable.set((dataSignal + |> deliverOnMainQueue).startStrict(next: { peerAndContactData in + if let strongSelf = self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 { + if contactData.isPrimitive { + let phone = contactData.basicData.phoneNumbers[0].value + let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil) + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + }) + } + }, nil) + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + strongSelf.sendMessages([message]) + } else { + let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in + guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else { + return + } + let phone = contactData.basicData.phoneNumbers[0].value + if let vCardData = contactData.serializedVCard() { + let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData) + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + }) + } + }, nil) + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + strongSelf.sendMessages([message]) + } + }), completed: nil, cancelled: nil) + strongSelf.effectiveNavigationController?.pushViewController(contactController) + } + } + })) + } + } + })) + } + + func getCaptionPanelView() -> TGCaptionPanelView? { + var isScheduledMessages = false + if case .scheduledMessages = self.presentationInterfaceState.subject { + isScheduledMessages = true + } + return self.context.sharedContext.makeGalleryCaptionPanelView(context: self.context, chatLocation: self.presentationInterfaceState.chatLocation, isScheduledMessages: isScheduledMessages, customEmojiAvailable: self.presentationInterfaceState.customEmojiAvailable, present: { [weak self] c in + self?.present(c, in: .window(.root)) + }, presentInGlobalOverlay: { [weak self] c in + guard let self else { + return + } + self.presentInGlobalOverlay(c) + }) as? TGCaptionPanelView + } + + func openCamera(cameraView: TGAttachmentCameraView? = nil) { + guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return + } + let _ = peer + + let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in + let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) + return entry ?? GeneratedMediaStoreSettings.defaultSettings + } + |> deliverOnMainQueue).startStandalone(next: { [weak self] settings in + guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { + return + } + + var enablePhoto = true + var enableVideo = true + + if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall { + enableVideo = false + } + + var bannedSendPhotos: (Int32, Bool)? + var bannedSendVideos: (Int32, Bool)? + + if let channel = peer as? TelegramChannel { + if let value = channel.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = value + } + if let value = channel.hasBannedPermission(.banSendVideos) { + bannedSendVideos = value + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendVideos) { + bannedSendVideos = (Int32.max, false) + } + } + + if bannedSendPhotos != nil { + enablePhoto = false + } + if bannedSendVideos != nil { + enableVideo = false + } + + let storeCapturedMedia = peer.id.namespace != Namespaces.Peer.SecretChat + let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText + + presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: nil, parentController: strongSelf, attachmentController: self?.attachmentController, editingMedia: false, saveCapturedPhotos: storeCapturedMedia, mediaGrouping: true, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in + if let strongSelf = self { + strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) + if !inputText.string.isEmpty { + strongSelf.clearInputText() + } + } + }, recognizedQRCode: { [weak self] code in + if let strongSelf = self { + if let (host, port, username, password, secret) = parseProxyUrl(code) { + strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil) + } + } + }, presentSchedulePicker: { [weak self] _, done in + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in + if let strongSelf = self { + done(time) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() + } + } + }) + } + }, presentTimerPicker: { [weak self] done in + if let strongSelf = self { + strongSelf.presentTimerPicker(style: .media, completion: { time in + done(time) + }) + } + }, getCaptionPanelView: { [weak self] in + return self?.getCaptionPanelView() + }, dismissedWithResult: { [weak self] in + self?.attachmentController?.dismiss(animated: false, completion: nil) + }, finishedTransitionIn: { [weak self] in + self?.attachmentController?.scrollToTop?() + }) + }) + } +} diff --git a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift index 75efeff8ef..2325486c84 100644 --- a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift @@ -11,6 +11,7 @@ import TelegramPresentationData import ChatInputPanelNode final class ChatRestrictedInputPanelNode: ChatInputPanelNode { + private let buttonNode: HighlightTrackingButtonNode private let textNode: ImmediateTextNode private var iconView: UIImageView? @@ -21,9 +22,34 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { self.textNode.maximumNumberOfLines = 2 self.textNode.textAlignment = .center + self.buttonNode = HighlightTrackingButtonNode() + self.buttonNode.isUserInteractionEnabled = false + super.init() self.addSubnode(self.textNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let self { + if highlighted { + self.iconView?.layer.removeAnimation(forKey: "opacity") + self.iconView?.alpha = 0.4 + self.textNode.layer.removeAnimation(forKey: "opacity") + self.textNode.alpha = 0.4 + } else { + self.iconView?.alpha = 1.0 + self.iconView?.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + self.textNode.alpha = 1.0 + self.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + @objc private func buttonPressed() { + self.interfaceInteraction?.openBoostToUnrestrict() } override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { @@ -45,6 +71,8 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { } var iconImage: UIImage? + var iconSpacing: CGFloat = 4.0 + var isUserInteractionEnabled = false if case let .replyThread(message) = interfaceState.chatLocation, message.peerId == self.context?.account.peerId { self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_PanelStatusAuthorHidden, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) @@ -63,17 +91,25 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { } else if personal { self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_RestrictedText, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) } else { - self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_DefaultRestrictedText, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) + if "".isEmpty { + //TODO:localize + iconSpacing = 0.0 + iconImage = PresentationResourcesChat.chatPanelBoostIcon(interfaceState.theme) + self.textNode.attributedText = NSAttributedString(string: "Boost this group to send messages", font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.panelControlAccentColor) + isUserInteractionEnabled = true + } else { + self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_DefaultRestrictedText, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor) + } } } + self.buttonNode.isUserInteractionEnabled = isUserInteractionEnabled let panelHeight = defaultHeight(metrics: metrics) let textSize = self.textNode.updateLayout(CGSize(width: width - leftInset - rightInset - 8.0 * 2.0, height: panelHeight)) - let textFrame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - textSize.width) / 2.0), y: floor((panelHeight - textSize.height) / 2.0)), size: textSize) - self.textNode.frame = textFrame + var originX: CGFloat = leftInset + floor((width - leftInset - rightInset - textSize.width) / 2.0) - if let iconImage = iconImage { + if let iconImage { let iconView: UIImageView if let current = self.iconView { iconView = current @@ -83,12 +119,21 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { self.view.addSubview(iconView) } iconView.image = iconImage - iconView.frame = CGRect(origin: CGPoint(x: textFrame.minX - 4.0 - iconImage.size.width, y: textFrame.minY + UIScreenPixel + floorToScreenPixels((textFrame.height - iconImage.size.height) / 2.0)), size: iconImage.size) + + let totalWidth = textSize.width + iconImage.size.width + iconSpacing + iconView.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - totalWidth) / 2.0), y: floor((panelHeight - textSize.height) / 2.0) + UIScreenPixel + floorToScreenPixels((textSize.height - iconImage.size.height) / 2.0)), size: iconImage.size) + + originX += iconImage.size.width + iconSpacing } else if let iconView = self.iconView { self.iconView = nil iconView.removeFromSuperview() } + let textFrame = CGRect(origin: CGPoint(x: originX, y: floor((panelHeight - textSize.height) / 2.0)), size: textSize) + self.textNode.frame = textFrame + + self.buttonNode.frame = textFrame.insetBy(dx: -8.0, dy: -12.0) + return panelHeight } diff --git a/submodules/TooltipUI/BUILD b/submodules/TooltipUI/BUILD index 6ef6c4ed14..551a211a51 100644 --- a/submodules/TooltipUI/BUILD +++ b/submodules/TooltipUI/BUILD @@ -25,6 +25,7 @@ swift_library( "//submodules/Markdown", "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", "//submodules/Components/BalancedTextComponent", + "//submodules/Components/MultilineTextWithEntitiesComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index f8d4f5234d..7d0975080a 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -17,6 +17,7 @@ import AvatarStoryIndicatorComponent import AccountContext import Markdown import BalancedTextComponent +import MultilineTextWithEntitiesComponent public enum TooltipActiveTextItem { case url(String, Bool) @@ -108,6 +109,8 @@ private class DownArrowsIconNode: ASDisplayNode { } private final class TooltipScreenNode: ViewControllerTracingNode { + private let context: AccountContext? + private let text: TooltipScreen.Text private let textAlignment: TooltipScreen.Alignment private let balancedTextLayout: Bool @@ -171,6 +174,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { cornerRadius: CGFloat? = nil, shouldDismissOnTouch: @escaping (CGPoint, CGRect) -> TooltipScreen.DismissOnTouch, requestDismiss: @escaping () -> Void, openActiveTextItem: ((TooltipActiveTextItem, TooltipActiveTextAction) -> Void)?) { + self.context = context self.tooltipStyle = style self.arrowStyle = arrowStyle self.icon = icon @@ -534,6 +538,8 @@ private final class TooltipScreenNode: ViewControllerTracingNode { } ) attributedText = parseMarkdownIntoAttributedString(text, attributes: markdownAttributes) + case let .attributedString(text): + attributedText = text } let highlightColor: UIColor? = UIColor.white.withAlphaComponent(0.5) @@ -594,21 +600,44 @@ private final class TooltipScreenNode: ViewControllerTracingNode { } } - let textSize = self.textView.update( - transition: .immediate, - component: AnyComponent(BalancedTextComponent( - text: .plain(attributedText), - balanced: self.balancedTextLayout, - horizontalAlignment: self.textAlignment == .center ? .center : .left, - maximumNumberOfLines: 0, - highlightColor: highlightColor, - highlightAction: highlightAction, - tapAction: tapAction, - longTapAction: longTapAction - )), - environment: {}, - containerSize: CGSize(width: containerWidth - contentInset * 2.0 - animationSize.width - animationSpacing - buttonInset, height: 1000000.0) - ) + let textSize: CGSize + if case .attributedString = self.text, let context = self.context { + textSize = self.textView.update( + transition: .immediate, + component: AnyComponent(MultilineTextWithEntitiesComponent( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.4), + text: .plain(attributedText), + horizontalAlignment: self.textAlignment == .center ? .center : .left, + truncationType: .end, + maximumNumberOfLines: 3, + lineSpacing: 0.2, + highlightAction: nil, + tapAction: { _, _ in + } + )), + environment: {}, + containerSize: CGSize(width: containerWidth - contentInset * 2.0 - animationSize.width - animationSpacing - buttonInset, height: 1000000.0) + ) + } else { + textSize = self.textView.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(attributedText), + balanced: self.balancedTextLayout, + horizontalAlignment: self.textAlignment == .center ? .center : .left, + maximumNumberOfLines: 0, + highlightColor: highlightColor, + highlightAction: highlightAction, + tapAction: tapAction, + longTapAction: longTapAction + )), + environment: {}, + containerSize: CGSize(width: containerWidth - contentInset * 2.0 - animationSize.width - animationSpacing - buttonInset, height: 1000000.0) + ) + } var backgroundFrame: CGRect @@ -941,6 +970,7 @@ public final class TooltipScreen: ViewController { case plain(text: String) case entities(text: String, entities: [MessageTextEntity]) case markdown(text: String) + case attributedString(text: NSAttributedString) } public class Action {