diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 996f7705aa..99b7b96bc3 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -736,7 +736,6 @@ public enum PeerInfoControllerMode { case generic case calls(messages: [EngineMessage]) - case nearbyPeer(distance: Int32) case group(sourceMessageId: MessageId) case reaction(MessageId) case forumTopic(thread: ChatReplyThreadMessage) @@ -1371,7 +1370,6 @@ public protocol SharedAccountContext: AnyObject { func makePeerInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool, requestsContext: PeerInvitationImportersContext?) -> ViewController? func makeChannelAdminController(context: AccountContext, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant) -> ViewController? func makeDeviceContactInfoController(context: ShareControllerAccountContext, environment: ShareControllerEnvironment, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController - func makePeersNearbyController(context: AccountContext) -> ViewController func makeComposeController(context: AccountContext) -> ViewController func makeChatListController(context: AccountContext, location: ChatListControllerLocation, controlsHistoryPreload: Bool, hideNetworkActivityStatus: Bool, previewing: Bool, enableDebugActions: Bool) -> ChatListController func makeChatController(context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, botStart: ChatControllerInitialBotStart?, mode: ChatControllerPresentationMode, params: ChatControllerParams?) -> ChatController @@ -1469,7 +1467,7 @@ public protocol SharedAccountContext: AnyObject { func makeGiftStoreController(context: AccountContext, peerId: EnginePeer.Id, gift: StarGift.Gift) -> ViewController func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController - func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], actionTitle: String?, isEditing: Bool, expandIfNeeded: Bool, parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView?, CGRect?) -> Bool)?, actionPerformed: ((Bool) -> Void)?) -> ViewController + func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], actionTitle: String?, isEditing: Bool, expandIfNeeded: Bool, parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView?, CGRect?) -> Bool)?, actionPerformed: (([StickerPackScreenActionResult]) -> Void)?) -> ViewController func makeCameraScreen(context: AccountContext, mode: CameraScreenMode, cameraHolder: Any?, transitionIn: CameraScreenTransitionIn?, transitionOut: @escaping (Bool) -> CameraScreenTransitionOut?, completion: @escaping (Any, @escaping () -> Void) -> Void, transitionedOut: (() -> Void)?) -> ViewController func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: (url: String, name: String?)?, remainingCount: Int32, completion: @escaping ([MediaEditorScreenResult], MediaEditorTransitionOutExternalState, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController @@ -1638,6 +1636,23 @@ public protocol ChatSendMessageActionSheetController: ViewController { typealias SendParameters = ChatSendMessageActionSheetControllerSendParameters } +public enum StickerPackScreenActionKind { + case add + case remove(positionInList: Int) +} + +public struct StickerPackScreenActionResult { + public let info: StickerPackCollectionInfo + public let items: [StickerPackItem] + public let action: StickerPackScreenActionKind + + public init(info: StickerPackCollectionInfo, items: [StickerPackItem], action: StickerPackScreenActionKind) { + self.info = info + self.items = items + self.action = action + } +} + public protocol AccountContext: AnyObject { var sharedContext: SharedAccountContext { get } var account: Account { get } diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 7c4e1b3f04..2272d982e7 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -1420,7 +1420,6 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog }) }) }, openScheduledMessages: { - }, openPeersNearby: { }, displaySearchResultsTooltip: { _, _ in }, unarchivePeer: { }, scrollToTop: { diff --git a/submodules/AuthorizationUI/BUILD b/submodules/AuthorizationUI/BUILD index cc280f1551..df41d76f17 100644 --- a/submodules/AuthorizationUI/BUILD +++ b/submodules/AuthorizationUI/BUILD @@ -48,6 +48,7 @@ swift_library( "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/Utils/DeviceModel", "//submodules/PresentationDataUtils", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift index ddb76da69a..eee43282af 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift @@ -386,7 +386,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) ) - let buttonHeight: CGFloat = 50.0 + let buttonHeight: CGFloat = 52.0 let bottomPanelPadding: CGFloat = 12.0 let titleSpacing: CGFloat = -24.0 let listSpacing: CGFloat = 12.0 @@ -416,7 +416,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { listView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - listSize.width) / 2.0), y: originY), size: listSize) } - let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding + let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 10.0 : bottomPanelPadding let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset let priceString: String @@ -463,7 +463,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: buttonHeight) + containerSize: CGSize(width: availableSize.width - 30.0 * 2.0, height: buttonHeight) ) if let buttonView = self.button.view { if buttonView.superview == nil { diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index c9c94d3288..1e98cd8670 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -5376,21 +5376,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if case .broadcast = channel.info { canClear = false deleteTitle = strongSelf.presentationData.strings.Channel_LeaveChannel - if channel.addressName == nil && channel.flags.contains(.isCreator) { - canRemoveGlobally = true - } } else { deleteTitle = strongSelf.presentationData.strings.Group_DeleteGroup - if channel.addressName == nil && channel.flags.contains(.isCreator) { - canRemoveGlobally = true - } + } + if strongSelf.canDeletePeerGloballyAsCreator(mainPeer) { + canRemoveGlobally = true } if let addressName = channel.addressName, !addressName.isEmpty { canClear = false } } - } else if case let .legacyGroup(group) = chatPeer { - if case .creator = group.role { + } else if case .legacyGroup = chatPeer { + if strongSelf.canDeletePeerGloballyAsCreator(mainPeer) { canRemoveGlobally = true } } else if case let .user(user) = chatPeer, user.botInfo != nil { @@ -5843,6 +5840,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController canRemoveGlobally = true } + if deleteGloballyIfPossible && self.canDeletePeerGloballyAsCreator(mainPeer) { + self.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: true, completion: { + removed() + }) + completion(true) + return + } + if canRemoveGlobally { let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] @@ -5946,6 +5951,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } + private func canDeletePeerGloballyAsCreator(_ peer: EnginePeer) -> Bool { + if case let .channel(channel) = peer { + return !channel.isMonoForum && channel.flags.contains(.isCreator) && channel.addressName == nil + } else if case let .legacyGroup(group) = peer, case .creator = group.role { + return true + } else { + return false + } + } + func archiveChats(peerIds: [PeerId]) { guard !peerIds.isEmpty else { return diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 0833307f24..d02bb39e63 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -153,7 +153,6 @@ public final class ChatPanelInterfaceInteraction { public let displaySendMessageOptions: (ASDisplayNode, ContextGesture) -> Void public let openScheduledMessages: () -> Void public let displaySearchResultsTooltip: (ASDisplayNode, CGRect) -> Void - public let openPeersNearby: () -> Void public let unarchivePeer: () -> Void public let scrollToTop: () -> Void public let viewReplies: (MessageId?, ChatReplyThreadMessage) -> Void @@ -285,7 +284,6 @@ public final class ChatPanelInterfaceInteraction { displaySlowmodeTooltip: @escaping (UIView, CGRect) -> Void, displaySendMessageOptions: @escaping (ASDisplayNode, ContextGesture) -> Void, openScheduledMessages: @escaping () -> Void, - openPeersNearby: @escaping () -> Void, displaySearchResultsTooltip: @escaping (ASDisplayNode, CGRect) -> Void, unarchivePeer: @escaping () -> Void, scrollToTop: @escaping () -> Void, @@ -417,7 +415,6 @@ public final class ChatPanelInterfaceInteraction { self.displaySlowmodeTooltip = displaySlowmodeTooltip self.displaySendMessageOptions = displaySendMessageOptions self.openScheduledMessages = openScheduledMessages - self.openPeersNearby = openPeersNearby self.displaySearchResultsTooltip = displaySearchResultsTooltip self.unarchivePeer = unarchivePeer self.scrollToTop = scrollToTop @@ -557,7 +554,6 @@ public final class ChatPanelInterfaceInteraction { }, displaySlowmodeTooltip: { _, _ in }, displaySendMessageOptions: { _, _ in }, openScheduledMessages: { - }, openPeersNearby: { }, displaySearchResultsTooltip: { _, _ in }, unarchivePeer: { }, scrollToTop: { diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index 33844f7452..35500162d5 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -184,7 +184,7 @@ public final class SheetComponent: C private let dimView: UIView private let scrollView: ScrollView private let backgroundView: SheetBackgroundView - private var effectView: UIVisualEffectView? + private var effectView: SheetBackgroundBlurView? private let clipView: SheetBackgroundView private let contentView: ComponentView private var headerView: ComponentView? @@ -411,13 +411,13 @@ public final class SheetComponent: C } var backgroundColor: UIColor = .clear + var blurEffectStyle: UIBlurEffect.Style? switch component.backgroundColor { case let .blur(style): + blurEffectStyle = style == .dark ? .dark : .light self.backgroundView.isHidden = true if self.effectView == nil { - let effectView = UIVisualEffectView(effect: UIBlurEffect(style: style == .dark ? .dark : .light)) - effectView.layer.cornerRadius = self.backgroundView.layer.cornerRadius - effectView.layer.masksToBounds = true + let effectView = SheetBackgroundBlurView() self.backgroundView.superview?.insertSubview(effectView, aboveSubview: self.backgroundView) self.effectView = effectView } @@ -471,8 +471,9 @@ public final class SheetComponent: C transition.setFrame(view: self.clipView, frame: clipFrame, completion: nil) transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: clipFrame.size), completion: nil) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) - if let effectView = self.effectView { + if let effectView = self.effectView, let blurEffectStyle = blurEffectStyle { transition.setFrame(view: effectView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) + effectView.update(style: blurEffectStyle, size: contentSize, topCornerRadius: topCornerRadius, bottomCornerRadius: topCornerRadius, transition: transition) } self.backgroundView.update(size: contentSize, color: backgroundColor, topCornerRadius: topCornerRadius, bottomCornerRadius: topCornerRadius, transition: transition) } else { @@ -483,14 +484,19 @@ public final class SheetComponent: C transition.setFrame(view: self.clipView, frame: clipFrame) transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height)), completion: nil) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: glassInset, y: -glassInset), size: CGSize(width: contentSize.width, height: contentSize.height)), completion: nil) + if let effectView = self.effectView, let blurEffectStyle = blurEffectStyle { + transition.setFrame(view: effectView, frame: CGRect(origin: CGPoint(x: glassInset, y: -glassInset), size: CGSize(width: contentSize.width, height: contentSize.height)), completion: nil) + effectView.update(style: blurEffectStyle, size: contentSize, topCornerRadius: topCornerRadius + 1.5, bottomCornerRadius: bottomCornerRadius, transition: transition) + } case .legacy: let clipFrame = CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 100.0)) self.clipView.update(size: clipFrame.size, color: .clear, topCornerRadius: topCornerRadius, bottomCornerRadius: bottomCornerRadius, transition: transition) transition.setFrame(view: self.clipView, frame: clipFrame) transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: clipFrame.size), completion: nil) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) - if let effectView = self.effectView { + if let effectView = self.effectView, let blurEffectStyle = blurEffectStyle { transition.setFrame(view: effectView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) + effectView.update(style: blurEffectStyle, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0), topCornerRadius: topCornerRadius + 1.5, bottomCornerRadius: bottomCornerRadius, transition: transition) } } self.backgroundView.update(size: contentSize, color: backgroundColor, topCornerRadius: topCornerRadius + 1.5, bottomCornerRadius: bottomCornerRadius, transition: transition) @@ -614,3 +620,31 @@ public final class SheetBackgroundView: UIView { transition.setBackgroundColor(view: self.bottomCornersView, color: color) } } + +private final class SheetBackgroundBlurView: UIView { + private let clipView = SheetBackgroundView() + private let effectView = UIVisualEffectView() + private var currentStyle: UIBlurEffect.Style? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addSubview(self.clipView) + self.clipView.bottomCornersView.addSubview(self.effectView) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + func update(style: UIBlurEffect.Style, size: CGSize, topCornerRadius: CGFloat, bottomCornerRadius: CGFloat, transition: ComponentTransition) { + if self.currentStyle != style { + self.currentStyle = style + self.effectView.effect = UIBlurEffect(style: style) + } + + self.clipView.update(size: size, color: .clear, topCornerRadius: topCornerRadius, bottomCornerRadius: bottomCornerRadius, transition: transition) + transition.setFrame(view: self.clipView, frame: CGRect(origin: .zero, size: size)) + transition.setFrame(view: self.effectView, frame: CGRect(origin: .zero, size: size)) + } +} diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index dfee64ae07..d74bf3fad2 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -375,49 +375,6 @@ public class ContactsController: ViewController { } } - self.contactsNode.openPeopleNearby = { [weak self] in - let _ = (DeviceAccess.authorizationStatus(subject: .location(.tracking)) - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] status in - guard let strongSelf = self else { - return - } - let presentPeersNearby = { - let controller = strongSelf.context.sharedContext.makePeersNearbyController(context: strongSelf.context) - controller.navigationPresentation = .master - if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController { - var controllers = navigationController.viewControllers.filter { !($0 is PermissionController) } - controllers.append(controller) - navigationController.setViewControllers(controllers, animated: true) - strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true) - } - } - - switch status { - case .allowed: - presentPeersNearby() - default: - let controller = PermissionController(context: strongSelf.context, splashScreen: false) - controller.setState(.permission(.nearbyLocation(status: PermissionRequestStatus(accessType: status))), animated: false) - controller.navigationPresentation = .master - controller.proceed = { result in - if result { - presentPeersNearby() - } else { - let _ = (strongSelf.navigationController as? NavigationController)?.popViewController(animated: true) - } - } - if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController { - navigationController.pushViewController(controller, completion: { [weak self] in - if let strongSelf = self { - strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true) - } - }) - } - } - }) - } - self.contactsNode.openInvite = { [weak self] in let _ = (DeviceAccess.authorizationStatus(subject: .contacts) |> take(1) diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index 363ddfa3ea..4496c9e8ff 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -64,7 +64,6 @@ final class ContactsControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { var requestOpenPeerFromSearch: ((ContactListPeer) -> Void)? var requestOpenDisabledPeerFromSearch: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? var requestAddContact: ((String) -> Void)? - var openPeopleNearby: (() -> Void)? var openInvite: (() -> Void)? var openQrScan: (() -> Void)? var openStories: ((EnginePeer, ASDisplayNode) -> Void)? diff --git a/submodules/DateSelectionUI/BUILD b/submodules/DateSelectionUI/BUILD deleted file mode 100644 index be8c15b508..0000000000 --- a/submodules/DateSelectionUI/BUILD +++ /dev/null @@ -1,24 +0,0 @@ -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -swift_library( - name = "DateSelectionUI", - module_name = "DateSelectionUI", - srcs = glob([ - "Sources/**/*.swift", - ]), - copts = [ - "-warnings-as-errors", - ], - deps = [ - "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/AsyncDisplayKit:AsyncDisplayKit", - "//submodules/Display:Display", - "//submodules/AccountContext:AccountContext", - "//submodules/TelegramStringFormatting:TelegramStringFormatting", - "//submodules/TelegramPresentationData:TelegramPresentationData", - "//submodules/UIKitRuntimeUtils:UIKitRuntimeUtils", - ], - visibility = [ - "//visibility:public", - ], -) diff --git a/submodules/DateSelectionUI/Sources/DateSelectionActionSheetController.swift b/submodules/DateSelectionUI/Sources/DateSelectionActionSheetController.swift deleted file mode 100644 index ef88e03178..0000000000 --- a/submodules/DateSelectionUI/Sources/DateSelectionActionSheetController.swift +++ /dev/null @@ -1,146 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import TelegramPresentationData -import TelegramStringFormatting -import SwiftSignalKit -import AccountContext -import UIKitRuntimeUtils - -public final class DateSelectionActionSheetController: ActionSheetController { - private var presentationDisposable: Disposable? - - private let _ready = Promise() - override public var ready: Promise { - return self._ready - } - - public init(context: AccountContext, title: String?, currentValue: Int32, minimumDate: Date? = nil, maximumDate: Date? = nil, emptyTitle: String? = nil, applyValue: @escaping (Int32?) -> Void) { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let strings = presentationData.strings - - super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) - - self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) - } - }).strict() - - self._ready.set(.single(true)) - - var updatedValue = currentValue - var items: [ActionSheetItem] = [] - if let title = title { - items.append(ActionSheetTextItem(title: title)) - } - items.append(DateSelectionActionSheetItem(strings: strings, currentValue: currentValue, minimumDate: minimumDate, maximumDate: maximumDate, valueChanged: { value in - updatedValue = value - })) - if let emptyTitle = emptyTitle { - items.append(ActionSheetButtonItem(title: emptyTitle, action: { [weak self] in - self?.dismissAnimated() - applyValue(nil) - })) - } - items.append(ActionSheetButtonItem(title: strings.Wallpaper_Set, action: { [weak self] in - self?.dismissAnimated() - applyValue(updatedValue) - })) - self.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in - self?.dismissAnimated() - }), - ]) - ]) - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDisposable?.dispose() - } -} - -private final class DateSelectionActionSheetItem: ActionSheetItem { - let strings: PresentationStrings - - let currentValue: Int32 - let minimumDate: Date? - let maximumDate: Date? - let valueChanged: (Int32) -> Void - - init(strings: PresentationStrings, currentValue: Int32, minimumDate: Date?, maximumDate: Date?, valueChanged: @escaping (Int32) -> Void) { - self.strings = strings - self.currentValue = roundDateToDays(currentValue) - self.minimumDate = minimumDate - self.maximumDate = maximumDate - self.valueChanged = valueChanged - } - - func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - return DateSelectionActionSheetItemNode(theme: theme, strings: self.strings, currentValue: self.currentValue, minimumDate: self.minimumDate, maximumDate: self.maximumDate, valueChanged: self.valueChanged) - } - - func updateNode(_ node: ActionSheetItemNode) { - } -} - -private final class DateSelectionActionSheetItemNode: ActionSheetItemNode { - private let theme: ActionSheetControllerTheme - private let strings: PresentationStrings - - private let valueChanged: (Int32) -> Void - private let pickerView: UIDatePicker - - init(theme: ActionSheetControllerTheme, strings: PresentationStrings, currentValue: Int32, minimumDate: Date?, maximumDate: Date?, valueChanged: @escaping (Int32) -> Void) { - self.theme = theme - self.strings = strings - self.valueChanged = valueChanged - - UILabel.setDateLabel(theme.primaryTextColor) - - self.pickerView = UIDatePicker() - self.pickerView.timeZone = TimeZone(secondsFromGMT: 0) - self.pickerView.datePickerMode = .countDownTimer - self.pickerView.datePickerMode = .date - self.pickerView.date = Date(timeIntervalSince1970: Double(roundDateToDays(currentValue))) - self.pickerView.locale = localeWithStrings(strings) - if #available(iOS 13.4, *) { - self.pickerView.preferredDatePickerStyle = .wheels - } - if let minimumDate = minimumDate { - self.pickerView.minimumDate = minimumDate - } - if let maximumDate = maximumDate { - self.pickerView.maximumDate = maximumDate - } else { - self.pickerView.maximumDate = Date(timeIntervalSince1970: Double(Int32.max - 1)) - } - self.pickerView.setValue(theme.primaryTextColor, forKey: "textColor") - - super.init(theme: theme) - - self.view.addSubview(self.pickerView) - self.pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged) - } - - public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let size = CGSize(width: constrainedSize.width, height: 216.0) - - self.pickerView.frame = CGRect(origin: CGPoint(), size: size) - - self.updateInternalLayout(size, constrainedSize: constrainedSize) - return size - } - - @objc private func datePickerUpdated() { - self.valueChanged(roundDateToDays(Int32(self.pickerView.date.timeIntervalSince1970))) - } -} - diff --git a/submodules/DrawingUI/BUILD b/submodules/DrawingUI/BUILD index e95073b967..d46a545f30 100644 --- a/submodules/DrawingUI/BUILD +++ b/submodules/DrawingUI/BUILD @@ -107,6 +107,7 @@ swift_library( "//submodules/TelegramUI/Components/DynamicCornerRadiusView", "//submodules/TelegramUI/Components/StickerPickerScreen", "//submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation", + "//submodules/TelegramUI/Components/SegmentControlComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/DrawingUI/Sources/ColorPickerScreen.swift b/submodules/DrawingUI/Sources/ColorPickerScreen.swift index dfcdf6bef8..ca955ce133 100644 --- a/submodules/DrawingUI/Sources/ColorPickerScreen.swift +++ b/submodules/DrawingUI/Sources/ColorPickerScreen.swift @@ -10,7 +10,7 @@ import TelegramPresentationData import SheetComponent import ViewControllerComponent import BlurredBackgroundComponent -import SegmentedControlNode +import SegmentControlComponent import MultilineTextComponent import HexColor import MediaEditor @@ -1494,73 +1494,6 @@ private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: }) } -private class SegmentedControlComponent: Component { - let values: [String] - let selectedIndex: Int - let selectionChanged: (Int) -> Void - - init(values: [String], selectedIndex: Int, selectionChanged: @escaping (Int) -> Void) { - self.values = values - self.selectedIndex = selectedIndex - self.selectionChanged = selectionChanged - } - - static func ==(lhs: SegmentedControlComponent, rhs: SegmentedControlComponent) -> Bool { - if lhs.values != rhs.values { - return false - } - if lhs.selectedIndex != rhs.selectedIndex { - return false - } - return true - } - - final class View: UIView { - private let backgroundNode: NavigationBackgroundNode - private let node: SegmentedControlNode - - init() { - self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.1)) - self.node = SegmentedControlNode(theme: SegmentedControlTheme(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0x6f7075, alpha: 0.6), shadowColor: .black, textColor: UIColor(rgb: 0xffffff), dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)), items: [], selectedIndex: 0) - - super.init(frame: CGRect()) - - self.addSubview(self.backgroundNode.view) - self.addSubview(self.node.view) - } - - required init?(coder aDecoder: NSCoder) { - preconditionFailure() - } - - func update(component: SegmentedControlComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { - self.node.items = component.values.map { SegmentedControlItem(title: $0) } - self.node.selectedIndex = component.selectedIndex - let selectionChanged = component.selectionChanged - self.node.selectedIndexChanged = { [weak self] index in - self?.window?.endEditing(true) - selectionChanged(index) - } - - let size = self.node.updateLayout(.stretchToFill(width: availableSize.width), transition: transition.containedViewLayoutTransition) - transition.setFrame(view: self.node.view, frame: CGRect(origin: CGPoint(), size: size)) - - transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size)) - self.backgroundNode.update(size: size, cornerRadius: 10.0, transition: .immediate) - - return size - } - } - - func makeView() -> View { - return View() - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, transition: transition) - } -} - final class ColorSwatchComponent: Component { enum SwatchType: Equatable { case main @@ -1904,7 +1837,7 @@ private final class ColorPickerContent: CombinedComponent { if let image = self.cachedCloseImage { closeImage = image } else { - closeImage = generateCloseButtonImage(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xa8aab1))! + closeImage = generateCloseButtonImage(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff))! self.cachedCloseImage = closeImage } return closeImage @@ -1954,7 +1887,7 @@ private final class ColorPickerContent: CombinedComponent { let eyedropperButton = Child(Button.self) let closeButton = Child(Button.self) let title = Child(MultilineTextComponent.self) - let modeControl = Child(SegmentedControlComponent.self) + let modeControl = Child(SegmentControlComponent.self) let colorGrid = Child(ColorGridComponent.self) let colorSpectrum = Child(ColorSpectrumComponent.self) @@ -2051,12 +1984,20 @@ private final class ColorPickerContent: CombinedComponent { var contentHeight: CGFloat = 58.0 + //backgroundColor: .clear, foregroundColor: UIColor(rgb: 0x6f7075, alpha: 0.6), shadowColor: .black, textColor: UIColor(rgb: 0xffffff), dividerColor: UIColor(rgb: 0x505155, alpha: 0.6) let modeControl = modeControl.update( - component: SegmentedControlComponent( - values: [strings.Paint_ColorGrid, strings.Paint_ColorSpectrum, strings.Paint_ColorSliders], - selectedIndex: 0, - selectionChanged: { [weak state] index in - state?.updateSelectedMode(index) + component: SegmentControlComponent( + theme: SegmentControlComponent.Theme(backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.07), legacyBackgroundColor: .clear, foregroundColor: UIColor(rgb: 0x6f7075, alpha: 0.6), textColor: .white, dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)), + items: [ + .init(id: 0, title: strings.Paint_ColorGrid), + .init(id: 1, title: strings.Paint_ColorSpectrum), + .init(id: 2, title: strings.Paint_ColorSliders), + ], + selectedId: state.selectedMode, + action: { [weak state] index in + if let value = index.base as? Int { + state?.updateSelectedMode(value) + } } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), @@ -2379,6 +2320,7 @@ private final class ColorPickerSheetComponent: CombinedComponent { }) } )), + style: .glass, backgroundColor: .blur(.dark), animateOut: animateOut ), diff --git a/submodules/DrawingUI/Sources/DrawingVideoRecorder.swift b/submodules/DrawingUI/Sources/DrawingVideoRecorder.swift index 5bd68600bd..6cd3b91c06 100644 --- a/submodules/DrawingUI/Sources/DrawingVideoRecorder.swift +++ b/submodules/DrawingUI/Sources/DrawingVideoRecorder.swift @@ -8,7 +8,7 @@ import AVFoundation public final class EntityVideoRecorder { private weak var mediaEditor: MediaEditor? private weak var entitiesView: DrawingEntitiesView? - + private let maxDuration: Double private let camera: Camera @@ -21,6 +21,9 @@ public final class EntityVideoRecorder { private let micLevelPromise = Promise() private var changingPositionDisposable: Disposable? + private var positionDisposable: Disposable? + private var currentCameraPosition: Camera.Position = .front + private var recordingInitialPosition: Camera.Position = .front public var duration: Signal { return self.durationPromise.get() @@ -35,7 +38,7 @@ public final class EntityVideoRecorder { public init(mediaEditor: MediaEditor, entitiesView: DrawingEntitiesView) { self.mediaEditor = mediaEditor self.entitiesView = entitiesView - + self.maxDuration = min(60.0, mediaEditor.duration ?? 60.0) self.previewView = CameraSimplePreviewView(frame: .zero, main: true) @@ -86,6 +89,10 @@ public final class EntityVideoRecorder { } self.micLevelPromise.set(camera.audioLevel) + self.positionDisposable = (camera.position + |> deliverOnMainQueue).start(next: { [weak self] position in + self?.currentCameraPosition = position + }) let start = mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0 mediaEditor.stop() @@ -107,6 +114,7 @@ public final class EntityVideoRecorder { deinit { self.recordingDisposable.dispose() self.changingPositionDisposable?.dispose() + self.positionDisposable?.dispose() } public func setup( @@ -145,6 +153,7 @@ public final class EntityVideoRecorder { mediaEditor.maybeMuteVideo() mediaEditor.play() + self.recordingInitialPosition = self.currentCameraPosition self.start = CACurrentMediaTime() self.recordingDisposable.set((self.camera.startRecording() |> deliverOnMainQueue).startStrict(next: { [weak self] recordingData in @@ -169,16 +178,20 @@ public final class EntityVideoRecorder { } self.recordingDisposable.set((self.camera.stopRecording() |> deliverOnMainQueue).startStrict(next: { [weak self] result in - guard let self, let mediaEditor = self.mediaEditor, let entitiesView = self.entitiesView, case let .finished(mainResult, _, _, _, _) = result else { + guard let self, let mediaEditor = self.mediaEditor, let entitiesView = self.entitiesView, case let .finished(mainResult, _, _, positionChangeTimestamps, _) = result else { return } if save { let duration = AVURLAsset(url: URL(fileURLWithPath: mainResult.path)).duration + let mirroringChanges = self.additionalVideoMirroringChanges( + initialPosition: self.recordingInitialPosition, + positionChangeTimestamps: positionChangeTimestamps + ) let start = mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0 mediaEditor.setAdditionalVideoOffset(-start, apply: false) mediaEditor.setAdditionalVideoTrimRange(0 ..< duration.seconds, apply: true) - mediaEditor.setAdditionalVideo(mainResult.path, positionChanges: []) + mediaEditor.setAdditionalVideo(mainResult.path, mirroringChanges: mirroringChanges, positionChanges: []) mediaEditor.stop() Queue.mainQueue().justDispatch { @@ -217,7 +230,7 @@ public final class EntityVideoRecorder { self.mediaEditor?.maybeUnmuteVideo() self.entitiesView?.remove(uuid: self.entity.uuid, animated: true) - self.mediaEditor?.setAdditionalVideo(nil, positionChanges: []) + self.mediaEditor?.setAdditionalVideo(nil, mirroringChanges: [], positionChanges: []) completion() } @@ -226,4 +239,22 @@ public final class EntityVideoRecorder { public func togglePosition() { self.camera.togglePosition() } + + private func additionalVideoMirroringChanges(initialPosition: Camera.Position, positionChangeTimestamps: [(Bool, Double)]) -> [VideoMirroringChange] { + var result: [VideoMirroringChange] = [ + VideoMirroringChange(isMirrored: initialPosition == .front, timestamp: 0.0) + ] + for (isMirrored, timestamp) in positionChangeTimestamps.sorted(by: { $0.1 < $1.1 }) { + result.append(VideoMirroringChange(isMirrored: isMirrored, timestamp: timestamp)) + } + + var deduplicated: [VideoMirroringChange] = [] + for change in result { + if deduplicated.last?.isMirrored == change.isMirrored { + continue + } + deduplicated.append(change) + } + return deduplicated + } } diff --git a/submodules/FeaturedStickersScreen/Sources/ChatMediaInputTrendingPane.swift b/submodules/FeaturedStickersScreen/Sources/ChatMediaInputTrendingPane.swift index 44beeb380e..a29ba2dd44 100644 --- a/submodules/FeaturedStickersScreen/Sources/ChatMediaInputTrendingPane.swift +++ b/submodules/FeaturedStickersScreen/Sources/ChatMediaInputTrendingPane.swift @@ -255,6 +255,70 @@ public final class ChatMediaInputTrendingPane: ChatMediaInputPane { self.disposable?.dispose() self.installDisposable.dispose() } + + private func presentStickerPackActionOverlay(_ actions: [StickerPackScreenActionResult]) { + guard let action = actions.first else { + return + } + + var animateInAsReplacement = false + if let navigationController = self.interaction.getNavigationController() { + for controller in navigationController.overlayControllers { + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitActionAndReplacementAnimation() + animateInAsReplacement = true + } + } + } + + var presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + if let forceTheme = self.forceTheme { + presentationData = presentationData.withUpdated(theme: forceTheme) + } + + let controller: UndoOverlayController + switch action.action { + case .add: + controller = UndoOverlayController( + presentationData: presentationData, + content: .stickersModified( + title: presentationData.strings.StickerPackActionInfo_AddedTitle, + text: presentationData.strings.StickerPackActionInfo_AddedText(action.info.title).string, + undo: false, + info: action.info, + topItem: action.items.first, + context: self.context + ), + elevatedLayout: false, + animateInAsReplacement: animateInAsReplacement, + action: { _ in + return true + } + ) + case let .remove(positionInList): + controller = UndoOverlayController( + presentationData: presentationData, + content: .stickersModified( + title: presentationData.strings.StickerPackActionInfo_RemovedTitle, + text: presentationData.strings.StickerPackActionInfo_RemovedText(action.info.title).string, + undo: true, + info: action.info, + topItem: action.items.first, + context: self.context + ), + elevatedLayout: false, + animateInAsReplacement: animateInAsReplacement, + action: { [weak self] overlayAction in + if case .undo = overlayAction { + let _ = self?.context.engine.stickers.addStickerPackInteractively(info: action.info, items: action.items, positionInList: positionInList).start() + } + return true + } + ) + } + + self.interaction.getNavigationController()?.presentOverlay(controller: controller) + } public func activate() { if self.isActivated { @@ -374,7 +438,9 @@ public final class ChatMediaInputTrendingPane: ChatMediaInputPane { return false } }, - actionPerformed: nil + actionPerformed: { [weak self] actions in + self?.presentStickerPackActionOverlay(actions) + } ) strongSelf.interaction.presentController(controller, nil) } diff --git a/submodules/PassportUI/BUILD b/submodules/PassportUI/BUILD index ab6d2f27cd..28b67dd27e 100644 --- a/submodules/PassportUI/BUILD +++ b/submodules/PassportUI/BUILD @@ -24,12 +24,12 @@ swift_library( "//submodules/OverlayStatusController:OverlayStatusController", "//submodules/LegacyUI:LegacyUI", "//submodules/ImageCompression:ImageCompression", - "//submodules/DateSelectionUI:DateSelectionUI", "//submodules/PasswordSetupUI:PasswordSetupUI", "//submodules/AppBundle:AppBundle", "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/Markdown:Markdown", "//submodules/PhoneNumberFormat:PhoneNumberFormat", + "//submodules/TelegramUI/Components/ChatTimerScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift b/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift index 93db136475..de0a6c4d05 100644 --- a/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift +++ b/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift @@ -9,8 +9,8 @@ import TelegramStringFormatting import AccountContext import GalleryUI import CountrySelectionUI -import DateSelectionUI import AppBundle +import ChatTimerScreen private enum SecureIdDocumentFormTextField { case identifier @@ -2368,49 +2368,76 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode() - override var ready: Promise { - return self._ready - } - - init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, currentValue: Int32, applyValue: @escaping (Int32?) -> Void) { - let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - let strings = presentationData.strings - - super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) - - self._ready.set(.single(true)) - - self.presentationDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) - } - }) - - var updatedValue = currentValue - var items: [ActionSheetItem] = [] - items.append(PeerBanTimeoutActionSheetItem(strings: strings, currentValue: currentValue, valueChanged: { value in - updatedValue = value - })) - items.append(ActionSheetButtonItem(title: strings.Wallpaper_Set, action: { [weak self] in - self?.dismissAnimated() - applyValue(updatedValue) - })) - self.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in - self?.dismissAnimated() - }), - ]) - ]) - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDisposable?.dispose() - } -} - -private final class PeerBanTimeoutActionSheetItem: ActionSheetItem { - let strings: PresentationStrings - - let currentValue: Int32 - let valueChanged: (Int32) -> Void - - init(strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) { - self.strings = strings - self.currentValue = roundDateToDays(currentValue) - self.valueChanged = valueChanged - } - - func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - return PeerBanTimeoutActionSheetItemNode(theme: theme, strings: self.strings, currentValue: self.currentValue, valueChanged: self.valueChanged) - } - - func updateNode(_ node: ActionSheetItemNode) { - } -} - -private final class PeerBanTimeoutActionSheetItemNode: ActionSheetItemNode { - private let theme: ActionSheetControllerTheme - private let strings: PresentationStrings - - private let valueChanged: (Int32) -> Void - private let pickerView: UIDatePicker - - init(theme: ActionSheetControllerTheme, strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) { - self.theme = theme - self.strings = strings - self.valueChanged = valueChanged - - UILabel.setDateLabel(theme.primaryTextColor) - - self.pickerView = UIDatePicker() - self.pickerView.datePickerMode = .countDownTimer - self.pickerView.datePickerMode = .date - self.pickerView.date = Date(timeIntervalSince1970: Double(roundDateToDays(currentValue))) - self.pickerView.locale = localeWithStrings(strings) - self.pickerView.minimumDate = Date() - self.pickerView.maximumDate = Date(timeIntervalSince1970: Double(Int32.max - 1)) - if #available(iOS 13.4, *) { - self.pickerView.preferredDatePickerStyle = .wheels - } - self.pickerView.setValue(theme.primaryTextColor, forKey: "textColor") - - super.init(theme: theme) - - self.view.addSubview(self.pickerView) - self.pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged) - } - - public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let size = CGSize(width: constrainedSize.width, height: 216.0) - - self.pickerView.frame = CGRect(origin: CGPoint(), size: size) - - self.updateInternalLayout(size, constrainedSize: constrainedSize) - return size - } - - @objc private func datePickerUpdated() { - self.valueChanged(roundDateToDays(Int32(self.pickerView.date.timeIntervalSince1970))) - } -} diff --git a/submodules/PeersNearbyIconNode/BUILD b/submodules/PeersNearbyIconNode/BUILD deleted file mode 100644 index bcb7f6d667..0000000000 --- a/submodules/PeersNearbyIconNode/BUILD +++ /dev/null @@ -1,21 +0,0 @@ -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -swift_library( - name = "PeersNearbyIconNode", - module_name = "PeersNearbyIconNode", - srcs = glob([ - "Sources/**/*.swift", - ]), - copts = [ - "-warnings-as-errors", - ], - deps = [ - "//submodules/AsyncDisplayKit:AsyncDisplayKit", - "//submodules/Display:Display", - "//submodules/TelegramPresentationData:TelegramPresentationData", - "//submodules/LegacyComponents:LegacyComponents", - ], - visibility = [ - "//visibility:public", - ], -) diff --git a/submodules/PeersNearbyIconNode/Sources/PeersNearbyIconNode.swift b/submodules/PeersNearbyIconNode/Sources/PeersNearbyIconNode.swift deleted file mode 100644 index 22b2a8f99b..0000000000 --- a/submodules/PeersNearbyIconNode/Sources/PeersNearbyIconNode.swift +++ /dev/null @@ -1,209 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import TelegramPresentationData -import LegacyComponents - -private final class PeersNearbyIconWavesNodeParams: NSObject { - let color: UIColor - let progress: CGFloat - - init(color: UIColor, progress: CGFloat) { - self.color = color - self.progress = progress - - super.init() - } -} - -private func degToRad(_ degrees: CGFloat) -> CGFloat { - return degrees * CGFloat.pi / 180.0 -} - -public final class PeersNearbyIconWavesNode: ASDisplayNode { - public var color: UIColor { - didSet { - self.setNeedsDisplay() - } - } - - private var effectiveProgress: CGFloat = 0.0 { - didSet { - self.setNeedsDisplay() - } - } - - public init(color: UIColor) { - self.color = color - - super.init() - - self.isLayerBacked = true - self.isOpaque = false - } - - override public func willEnterHierarchy() { - super.willEnterHierarchy() - - self.pop_removeAnimation(forKey: "indefiniteProgress") - - let animation = POPBasicAnimation() - animation.property = (POPAnimatableProperty.property(withName: "progress", initializer: { property in - property?.readBlock = { node, values in - values?.pointee = (node as! PeersNearbyIconWavesNode).effectiveProgress - } - property?.writeBlock = { node, values in - (node as! PeersNearbyIconWavesNode).effectiveProgress = values!.pointee - } - property?.threshold = 0.01 - }) as! POPAnimatableProperty) - animation.fromValue = CGFloat(0.0) as NSNumber - animation.toValue = CGFloat(1.0) as NSNumber - animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) - animation.duration = 3.5 - animation.repeatForever = true - self.pop_add(animation, forKey: "indefiniteProgress") - } - - override public func didExitHierarchy() { - super.didExitHierarchy() - - self.pop_removeAnimation(forKey: "indefiniteProgress") - } - - override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - let t = CACurrentMediaTime() - let value: CGFloat = CGFloat(t.truncatingRemainder(dividingBy: 2.0)) / 2.0 - return PeersNearbyIconWavesNodeParams(color: self.color, progress: value) - } - - @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { - let context = UIGraphicsGetCurrentContext()! - - if !isRasterizing { - context.setBlendMode(.copy) - context.setFillColor(UIColor.clear.cgColor) - context.fill(bounds) - } - - if let parameters = parameters as? PeersNearbyIconWavesNodeParams { - let center = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0) - let radius: CGFloat = bounds.width * 0.3333 - let range: CGFloat = (bounds.width - radius * 2.0) / 2.0 - - context.setFillColor(parameters.color.cgColor) - - let draw: (CGContext, CGFloat) -> Void = { context, pos in - let path = CGMutablePath() - - let pathRadius: CGFloat = bounds.width * 0.3333 + range * pos - path.addEllipse(in: CGRect(x: center.x - pathRadius, y: center.y - pathRadius, width: pathRadius * 2.0, height: pathRadius * 2.0)) - - let strokedPath = path.copy(strokingWithWidth: 1.0, lineCap: .round, lineJoin: .miter, miterLimit: 10.0) - context.addPath(strokedPath) - context.fillPath() - } - - let position = parameters.progress - var alpha = position / 0.5 - if alpha > 1.0 { - alpha = 2.0 - alpha - } - context.setAlpha(alpha * 0.7) - - draw(context, position) - - var progress = parameters.progress + 0.3333 - if progress > 1.0 { - progress = progress - 1.0 - } - - var largerPos = progress - var largerAlpha = largerPos / 0.5 - if largerAlpha > 1.0 { - largerAlpha = 2.0 - largerAlpha - } - context.setAlpha(largerAlpha * 0.7) - - draw(context, largerPos) - - progress = parameters.progress + 0.6666 - if progress > 1.0 { - progress = progress - 1.0 - } - - largerPos = progress - largerAlpha = largerPos / 0.5 - if largerAlpha > 1.0 { - largerAlpha = 2.0 - largerAlpha - } - context.setAlpha(largerAlpha * 0.7) - - draw(context, largerPos) - } - } -} - -private func generateIcon(size: CGSize, color: UIColor, contentColor: UIColor) -> UIImage { - return generateImage(size, rotatedContext: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - - context.setFillColor(color.cgColor) - context.fillEllipse(in: bounds) - - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: size.width / 120.0, y: size.height / 120.0) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - context.translateBy(x: 0.0, y: 6.0) - context.setFillColor(contentColor.cgColor) - - if size.width == 120.0 { - context.translateBy(x: 30.0, y: 30.0) - } - - let _ = try? drawSvgPath(context, path: "M27.8628211,52.2347452 L27.8628211,27.1373017 L2.76505663,27.1373017 C1.55217431,27.1373017 0.568938916,26.1540663 0.568938916,24.941184 C0.568938916,24.0832172 1.06857435,23.3038117 1.84819149,22.9456161 L51.2643819,0.241311309 C52.586928,-0.366333451 54.1516568,0.213208572 54.7593016,1.53575465 C55.0801868,2.23416513 55.080181,3.03785964 54.7592857,3.7362655 L32.0544935,53.1516391 C31.548107,54.2537536 30.2441593,54.7366865 29.1420449,54.2302999 C28.3624433,53.8720978 27.8628211,53.0927006 27.8628211,52.2347452 Z ") - })! -} - -public final class PeersNearbyIconNode: ASDisplayNode { - private var theme: PresentationTheme - - private var iconNode: ASImageNode - private var wavesNode: PeersNearbyIconWavesNode - - public init(theme: PresentationTheme) { - self.theme = theme - - self.iconNode = ASImageNode() - self.iconNode.isOpaque = false - self.wavesNode = PeersNearbyIconWavesNode(color: theme.list.itemAccentColor) - - super.init() - - self.addSubnode(self.iconNode) - self.addSubnode(self.wavesNode) - } - - public func updateTheme(_ theme: PresentationTheme) { - guard self.theme !== theme else { - return - } - self.theme = theme - - self.iconNode.image = generateIcon(size: self.bounds.size, color: self.theme.list.itemAccentColor, contentColor: self.theme.list.itemCheckColors.foregroundColor) - self.wavesNode.color = theme.list.itemAccentColor - } - - override public func layout() { - super.layout() - - if let image = self.iconNode.image, image.size.width == self.bounds.width { - } else { - self.iconNode.image = generateIcon(size: self.bounds.size, color: self.theme.list.itemAccentColor, contentColor: self.theme.list.itemCheckColors.foregroundColor) - } - self.iconNode.frame = self.bounds - self.wavesNode.frame = self.bounds.insetBy(dx: -self.bounds.width * 0.3, dy: -self.bounds.height * 0.3) - } -} diff --git a/submodules/PeersNearbyUI/BUILD b/submodules/PeersNearbyUI/BUILD deleted file mode 100644 index 4ebbdcdbb5..0000000000 --- a/submodules/PeersNearbyUI/BUILD +++ /dev/null @@ -1,39 +0,0 @@ -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -swift_library( - name = "PeersNearbyUI", - module_name = "PeersNearbyUI", - srcs = glob([ - "Sources/**/*.swift", - ]), - copts = [ - "-warnings-as-errors", - ], - deps = [ - "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/AsyncDisplayKit:AsyncDisplayKit", - "//submodules/Display:Display", - "//submodules/TelegramCore:TelegramCore", - "//submodules/TelegramPresentationData:TelegramPresentationData", - "//submodules/AccountContext:AccountContext", - "//submodules/TelegramUIPreferences:TelegramUIPreferences", - "//submodules/ItemListUI:ItemListUI", - "//submodules/OverlayStatusController:OverlayStatusController", - "//submodules/DeviceLocationManager:DeviceLocationManager", - "//submodules/AlertUI:AlertUI", - "//submodules/PresentationDataUtils:PresentationDataUtils", - "//submodules/ItemListPeerItem:ItemListPeerItem", - "//submodules/TelegramPermissionsUI:TelegramPermissionsUI", - "//submodules/ItemListPeerActionItem:ItemListPeerActionItem", - "//submodules/PeersNearbyIconNode:PeersNearbyIconNode", - "//submodules/Geocoding:Geocoding", - "//submodules/AppBundle:AppBundle", - "//submodules/AnimatedStickerNode:AnimatedStickerNode", - "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", - "//submodules/TelegramStringFormatting:TelegramStringFormatting", - "//submodules/TelegramNotices:TelegramNotices", - ], - visibility = [ - "//visibility:public", - ], -) diff --git a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift deleted file mode 100644 index 8dae8cc38d..0000000000 --- a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift +++ /dev/null @@ -1,659 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import SwiftSignalKit -import TelegramCore -import MapKit -import TelegramPresentationData -import TelegramUIPreferences -import ItemListUI -import PresentationDataUtils -import OverlayStatusController -import DeviceLocationManager -import AccountContext -import AlertUI -import PresentationDataUtils -import ItemListPeerItem -import TelegramPermissionsUI -import ItemListPeerActionItem -import Geocoding -import AppBundle -import ContextUI -import TelegramNotices -import TelegramStringFormatting - -private let maxUsersDisplayedLimit: Int32 = 5 - -private struct PeerNearbyEntry { - let peer: EnginePeer - let memberCount: Int32? - let expires: Int32 - let distance: Int32 -} - -private func arePeersNearbyEqual(_ lhs: PeerNearbyEntry?, _ rhs: PeerNearbyEntry?) -> Bool { - if let lhs = lhs, let rhs = rhs { - return lhs.peer == rhs.peer && lhs.expires == rhs.expires && lhs.distance == rhs.distance - } else { - return (lhs != nil) == (rhs != nil) - } -} - -private func arePeerNearbyArraysEqual(_ lhs: [PeerNearbyEntry], _ rhs: [PeerNearbyEntry]) -> Bool { - if lhs.count != rhs.count { - return false - } - for i in 0 ..< lhs.count { - if lhs[i].peer != rhs[i].peer || lhs[i].expires != rhs[i].expires || lhs[i].distance != rhs[i].distance { - return false - } - } - return true -} - -private final class PeersNearbyControllerArguments { - let context: AccountContext - let toggleVisibility: (Bool) -> Void - let openProfile: (EnginePeer, Int32) -> Void - let openChat: (EnginePeer) -> Void - let openCreateGroup: (Double, Double, String?) -> Void - let contextAction: (EnginePeer, ASDisplayNode, ContextGesture?) -> Void - let expandUsers: () -> Void - - init(context: AccountContext, toggleVisibility: @escaping (Bool) -> Void, openProfile: @escaping (EnginePeer, Int32) -> Void, openChat: @escaping (EnginePeer) -> Void, openCreateGroup: @escaping (Double, Double, String?) -> Void, contextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?) -> Void, expandUsers: @escaping () -> Void) { - self.context = context - self.toggleVisibility = toggleVisibility - self.openProfile = openProfile - self.openChat = openChat - self.openCreateGroup = openCreateGroup - self.contextAction = contextAction - self.expandUsers = expandUsers - } -} - -private enum PeersNearbySection: Int32 { - case header - case users - case groups - case channels -} - -private enum PeersNearbyEntry: ItemListNodeEntry { - case header(PresentationTheme, String) - - case usersHeader(PresentationTheme, String, Bool) - case empty(PresentationTheme, String) - case visibility(PresentationTheme, String, Bool) - case user(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PeerNearbyEntry) - case expand(PresentationTheme, String) - - case groupsHeader(PresentationTheme, String, Bool) - case createGroup(PresentationTheme, String, Double?, Double?, String?) - case group(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PeerNearbyEntry, Bool) - - case channelsHeader(PresentationTheme, String) - case channel(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PeerNearbyEntry, Bool) - - var section: ItemListSectionId { - switch self { - case .header: - return PeersNearbySection.header.rawValue - case .usersHeader, .empty, .visibility, .user, .expand: - return PeersNearbySection.users.rawValue - case .groupsHeader, .createGroup, .group: - return PeersNearbySection.groups.rawValue - case .channelsHeader, .channel: - return PeersNearbySection.channels.rawValue - } - } - - var stableId: Int32 { - switch self { - case .header: - return 0 - case .usersHeader: - return 1 - case .empty: - return 2 - case .visibility: - return 3 - case let .user(index, _, _, _, _, _): - return 4 + index - case .expand: - return 1000 - case .groupsHeader: - return 1001 - case .createGroup: - return 1002 - case let .group(index, _, _, _, _, _, _): - return 1003 + index - case .channelsHeader: - return 2000 - case let .channel(index, _, _, _, _, _, _): - return 2001 + index - } - } - - static func ==(lhs: PeersNearbyEntry, rhs: PeersNearbyEntry) -> Bool { - switch lhs { - case let .header(lhsTheme, lhsText): - if case let .header(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .usersHeader(lhsTheme, lhsText, lhsLoading): - if case let .usersHeader(rhsTheme, rhsText, rhsLoading) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsLoading == rhsLoading { - return true - } else { - return false - } - case let .empty(lhsTheme, lhsText): - if case let .empty(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .visibility(lhsTheme, lhsText, lhsStop): - if case let .visibility(rhsTheme, rhsText, rhsStop) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsStop == rhsStop { - return true - } else { - return false - } - - case let .user(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsDisplayOrder, lhsPeer): - if case let .user(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsDisplayOrder, rhsPeer) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsDisplayOrder == rhsDisplayOrder, arePeersNearbyEqual(lhsPeer, rhsPeer) { - return true - } else { - return false - } - case let .expand(lhsTheme, lhsText): - if case let .expand(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .groupsHeader(lhsTheme, lhsText, lhsLoading): - if case let .groupsHeader(rhsTheme, rhsText, rhsLoading) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsLoading == rhsLoading { - return true - } else { - return false - } - case let .createGroup(lhsTheme, lhsText, lhsLatitude, lhsLongitude, lhsAddress): - if case let .createGroup(rhsTheme, rhsText, rhsLatitude, rhsLongitude, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsLatitude == rhsLatitude && lhsLongitude == rhsLongitude && lhsAddress == rhsAddress { - return true - } else { - return false - } - case let .group(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsDisplayOrder, lhsPeer, lhsHighlighted): - if case let .group(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsDisplayOrder, rhsPeer, rhsHighlighted) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsDisplayOrder == rhsDisplayOrder, arePeersNearbyEqual(lhsPeer, rhsPeer), lhsHighlighted == rhsHighlighted { - return true - } else { - return false - } - case let .channelsHeader(lhsTheme, lhsText): - if case let .channelsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .channel(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsDisplayOrder, lhsPeer, lhsHighlighted): - if case let .channel(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsDisplayOrder, rhsPeer, rhsHighlighted) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsDisplayOrder == rhsDisplayOrder, arePeersNearbyEqual(lhsPeer, rhsPeer), lhsHighlighted == rhsHighlighted { - return true - } else { - return false - } - } - } - - static func <(lhs: PeersNearbyEntry, rhs: PeersNearbyEntry) -> Bool { - return lhs.stableId < rhs.stableId - } - - func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { - let arguments = arguments as! PeersNearbyControllerArguments - switch self { - case let .header(theme, text): - return PeersNearbyHeaderItem(context: arguments.context, theme: theme, text: text, sectionId: self.section) - case let .usersHeader(_, text, loading): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, activityIndicator: loading ? .left : .none, sectionId: self.section) - case let .empty(theme, text): - return ItemListPlaceholderItem(theme: theme, text: text, sectionId: self.section, style: .blocks) - case let .visibility(theme, title, stop): - return ItemListPeerActionItem(presentationData: presentationData, icon: stop ? PresentationResourcesItemList.makeInvisibleIcon(theme) : PresentationResourcesItemList.makeVisibleIcon(theme), title: title, alwaysPlain: false, sectionId: self.section, color: stop ? .destructive : .accent, editing: false, action: { - arguments.toggleVisibility(!stop) - }) - case let .user(_, _, strings, dateTimeFormat, nameDisplayOrder, peer): - var text = strings.Map_DistanceAway(shortStringForDistance(strings: strings, distance: peer.distance)).string - let isSelfPeer = peer.peer.id == arguments.context.account.peerId - if isSelfPeer { - text = strings.PeopleNearby_VisibleUntil(humanReadableStringForTimestamp(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: peer.expires).string).string - } - return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer.peer, aliasHandling: .standard, nameColor: .primary, nameStyle: .distinctBold, presence: nil, text: .text(text, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: !isSelfPeer, sectionId: self.section, action: { - if !isSelfPeer { - arguments.openProfile(peer.peer, peer.distance) - } - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, contextAction: nil, hasTopGroupInset: false, tag: nil) - case let .expand(theme, title): - return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: { - arguments.expandUsers() - }) - case let .groupsHeader(_, text, loading): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, activityIndicator: loading ? .left : .none, sectionId: self.section) - case let .createGroup(theme, title, latitude, longitude, address): - return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.createGroupIcon(theme), title: title, alwaysPlain: false, sectionId: self.section, editing: false, action: { - if let latitude = latitude, let longitude = longitude { - arguments.openCreateGroup(latitude, longitude, address) - } - }) - case let .group(_, _, strings, dateTimeFormat, nameDisplayOrder, peer, highlighted): - var text: ItemListPeerItemText - if let memberCount = peer.memberCount { - text = .text("\(strings.Map_DistanceAway(shortStringForDistance(strings: strings, distance: peer.distance)).string), \(memberCount > 0 ? strings.Conversation_StatusMembers(memberCount) : strings.PeopleNearby_NoMembers)", .secondary) - } else { - text = .text(strings.Map_DistanceAway(shortStringForDistance(strings: strings, distance: peer.distance)).string, .secondary) - } - return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer.peer, aliasHandling: .standard, nameColor: .primary, nameStyle: .distinctBold, presence: nil, text: text, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, highlighted: highlighted, selectable: true, sectionId: self.section, action: { - arguments.openChat(peer.peer) - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, contextAction: { node, gesture in - arguments.contextAction(peer.peer, node, gesture) - }, hasTopGroupInset: false, tag: nil) - case let .channelsHeader(_, text): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .channel(_, _, strings, dateTimeFormat, nameDisplayOrder, peer, highlighted): - var text: ItemListPeerItemText - if let memberCount = peer.memberCount { - text = .text("\(strings.Map_DistanceAway(shortStringForDistance(strings: strings, distance: peer.distance)).string), \(strings.Conversation_StatusSubscribers(memberCount))", .secondary) - } else { - text = .text(strings.Map_DistanceAway(shortStringForDistance(strings: strings, distance: peer.distance)).string, .secondary) - } - return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer.peer, aliasHandling: .standard, nameColor: .primary, nameStyle: .distinctBold, presence: nil, text: text, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, highlighted: highlighted, selectable: true, sectionId: self.section, action: { - arguments.openChat(peer.peer) - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, contextAction: { node, gesture in - arguments.contextAction(peer.peer, node, gesture) - }, hasTopGroupInset: false, tag: nil) - } - } -} - -private struct PeersNearbyData: Equatable { - let latitude: Double - let longitude: Double - let address: String? - let visible: Bool - let accountPeerId: EnginePeer.Id - let users: [PeerNearbyEntry] - let groups: [PeerNearbyEntry] - let channels: [PeerNearbyEntry] - - init(latitude: Double, longitude: Double, address: String?, visible: Bool, accountPeerId: EnginePeer.Id, users: [PeerNearbyEntry], groups: [PeerNearbyEntry], channels: [PeerNearbyEntry]) { - self.latitude = latitude - self.longitude = longitude - self.address = address - self.visible = visible - self.accountPeerId = accountPeerId - self.users = users - self.groups = groups - self.channels = channels - } - - static func ==(lhs: PeersNearbyData, rhs: PeersNearbyData) -> Bool { - return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude && lhs.address == rhs.address && lhs.visible == rhs.visible && lhs.accountPeerId == rhs.accountPeerId && arePeerNearbyArraysEqual(lhs.users, rhs.users) && arePeerNearbyArraysEqual(lhs.groups, rhs.groups) && arePeerNearbyArraysEqual(lhs.channels, rhs.channels) - } -} - -private func peersNearbyControllerEntries(data: PeersNearbyData?, state: PeersNearbyState, presentationData: PresentationData, displayLoading: Bool, expanded: Bool, chatLocation: ChatLocation?) -> [PeersNearbyEntry] { - var entries: [PeersNearbyEntry] = [] - - entries.append(.header(presentationData.theme, presentationData.strings.PeopleNearby_DiscoverDescription)) - entries.append(.usersHeader(presentationData.theme, presentationData.strings.PeopleNearby_Users.uppercased(), displayLoading && data == nil)) - - let visible = state.visibilityExpires != nil - entries.append(.visibility(presentationData.theme, visible ? presentationData.strings.PeopleNearby_MakeInvisible : presentationData.strings.PeopleNearby_MakeVisible, visible)) - - if let data = data, !data.users.isEmpty { - var index: Int32 = 0 - var users = data.users.filter { $0.peer.id != data.accountPeerId } - var effectiveExpanded = expanded - if users.count > maxUsersDisplayedLimit && !expanded { - users = Array(users.prefix(Int(maxUsersDisplayedLimit))) - } else { - effectiveExpanded = true - } - - for user in users { - entries.append(.user(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, user)) - index += 1 - } - - if !effectiveExpanded { - entries.append(.expand(presentationData.theme, presentationData.strings.PeopleNearby_ShowMorePeople(Int32(data.users.count) - maxUsersDisplayedLimit))) - } - } - - var highlightedPeerId: EnginePeer.Id? - if let chatLocation = chatLocation, case let .peer(peerId) = chatLocation { - highlightedPeerId = peerId - } - - entries.append(.groupsHeader(presentationData.theme, presentationData.strings.PeopleNearby_Groups.uppercased(), displayLoading && data == nil)) - entries.append(.createGroup(presentationData.theme, presentationData.strings.PeopleNearby_CreateGroup, data?.latitude, data?.longitude, data?.address)) - if let data = data, !data.groups.isEmpty { - var i: Int32 = 0 - for group in data.groups { - entries.append(.group(i, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, group, highlightedPeerId == group.peer.id)) - i += 1 - } - } - - if let data = data, !data.channels.isEmpty { - var i: Int32 = 0 - for channel in data.channels { - entries.append(.channel(i, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, channel, highlightedPeerId == channel.peer.id)) - i += 1 - } - } - - return entries -} - -private final class ContextControllerContentSourceImpl: ContextControllerContentSource { - let controller: ViewController - weak var sourceNode: ASDisplayNode? - - let navigationController: NavigationController? = nil - - let passthroughTouches: Bool = true - - init(controller: ViewController, sourceNode: ASDisplayNode?) { - self.controller = controller - self.sourceNode = sourceNode - } - - func transitionInfo() -> ContextControllerTakeControllerInfo? { - let sourceNode = self.sourceNode - return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in - if let sourceNode = sourceNode { - return (sourceNode.view, sourceNode.bounds) - } else { - return nil - } - }) - } - - func animatedIn() { - } -} - -private func peerNearbyContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, present: @escaping (ViewController) -> Void) -> Signal<[ContextMenuItem], NoError> { - return .single([]) -} - -private class PeersNearbyControllerImpl: ItemListController { - fileprivate let chatLocation = Promise(nil) - - public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) { - if self.isNodeLoaded { - self.chatLocation.set(.single(data as? ChatLocation)) - } - } -} - -public func peersNearbyController(context: AccountContext) -> ViewController { - var pushControllerImpl: ((ViewController) -> Void)? - var replaceTopControllerImpl: ((ViewController) -> Void)? - var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? - var presentInGlobalOverlayImpl: ((ViewController) -> Void)? - var navigateToProfileImpl: ((EnginePeer, Int32) -> Void)? - var navigateToChatImpl: ((EnginePeer) -> Void)? - - let actionsDisposable = DisposableSet() - let checkCreationAvailabilityDisposable = MetaDisposable() - actionsDisposable.add(checkCreationAvailabilityDisposable) - - let dataPromise = Promise(nil) - let addressPromise = Promise(nil) - let expandedPromise = ValuePromise(false) - - let chatLocationPromise = Promise(nil) - - let coordinatePromise = Promise(nil) - coordinatePromise.set(.single(nil) |> then(currentLocationManagerCoordinate(manager: context.sharedContext.locationManager!, timeout: 5.0))) - - let arguments = PeersNearbyControllerArguments(context: context, toggleVisibility: { visible in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - if visible { - presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.PeopleNearby_MakeVisibleTitle, text: presentationData.strings.PeopleNearby_MakeVisibleDescription, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - let _ = (coordinatePromise.get() - |> deliverOnMainQueue).start(next: { coordinate in - if let coordinate = coordinate { - let _ = context.engine.peersNearby.updatePeersNearbyVisibility(update: .visible(latitude: coordinate.latitude, longitude: coordinate.longitude), background: false).start() - } - }) - })]), nil) - - - } else { - let _ = context.engine.peersNearby.updatePeersNearbyVisibility(update: .invisible, background: false).start() - } - }, openProfile: { peer, distance in - navigateToProfileImpl?(peer, distance) - }, openChat: { peer in - navigateToChatImpl?(peer) - }, openCreateGroup: { latitude, longitude, address in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - var cancelImpl: (() -> Void)? - let progressSignal = Signal { subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - presentControllerImpl?(controller, nil) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.5, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - cancelImpl = { - checkCreationAvailabilityDisposable.set(nil) - } - checkCreationAvailabilityDisposable.set((context.engine.peers.checkPublicChannelCreationAvailability(location: true) - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - |> deliverOnMainQueue).start(next: { available in - if available { - let controller = PermissionController(context: context, splashScreen: true) - controller.navigationPresentation = .modalInLargeLayout - controller.setState(.custom(icon: .icon(PermissionControllerCustomIcon(light: UIImage(bundleImageName: "Location/LocalGroupLightIcon"), dark: UIImage(bundleImageName: "Location/LocalGroupDarkIcon"))), title: presentationData.strings.LocalGroup_Title, subtitle: address, text: presentationData.strings.LocalGroup_Text, buttonTitle: presentationData.strings.LocalGroup_ButtonTitle, secondaryButtonTitle: nil, footerText: presentationData.strings.LocalGroup_IrrelevantWarning), animated: false) - controller.proceed = { result in - let controller = context.sharedContext.makeCreateGroupController(context: context, peerIds: [], initialTitle: nil, mode: .locatedGroup(latitude: latitude, longitude: longitude, address: address), completion: nil) - controller.navigationPresentation = .modalInLargeLayout - replaceTopControllerImpl?(controller) - } - pushControllerImpl?(controller) - } else { - presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.CreateGroup_ErrorLocatedGroupsTooMuch, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) - } - })) - }, contextAction: { peer, node, gesture in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.previewing), params: nil) - chatController.canReadHistory.set(false) - let contextController = makeContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: peerNearbyContextMenuItems(context: context, peerId: peer.id, present: { c in - presentControllerImpl?(c, nil) - }) |> map { ContextController.Items(content: .list($0), animationCache: nil) }, gesture: gesture) - presentInGlobalOverlayImpl?(contextController) - }, expandUsers: { - expandedPromise.set(true) - }) - - let dataSignal: Signal = coordinatePromise.get() - |> distinctUntilChanged(isEqual: { lhs, rhs in - return lhs?.latitude == rhs?.latitude && lhs?.longitude == rhs?.longitude - }) - |> mapToSignal { coordinate -> Signal in - guard let coordinate = coordinate else { - return .single(nil) - /*let peersNearbyContext = PeersNearbyContext(network: context.account.network, stateManager: context.account.stateManager, coordinate: nil) - return peersNearbyContext.get() - |> map { peersNearby -> PeersNearbyData in - var isVisible = false - if let peersNearby { - for peer in peersNearby { - if case .selfPeer = peer { - isVisible = true - break - } - } - } - return PeersNearbyData(latitude: 0.0, longitude: 0.0, address: nil, visible: isVisible, accountPeerId: context.account.peerId, users: [], groups: [], channels: []) - }*/ - } - - return Signal { subscriber in - let peersNearbyContext = PeersNearbyContext(network: context.account.network, stateManager: context.account.stateManager, coordinate: (latitude: coordinate.latitude, longitude: coordinate.longitude)) - - let peersNearby: Signal = combineLatest(peersNearbyContext.get(), addressPromise.get()) - |> mapToSignal { peersNearby, address -> Signal<([PeerNearby]?, String?), NoError> in - if let address = address { - return .single((peersNearby, address)) - } else { - return reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) - |> map { placemark in - return (peersNearby, placemark?.fullAddress) - } - } - } - |> mapToSignal { peersNearby, address -> Signal in - guard let peersNearby = peersNearby else { - return .single(nil) - } - let peerIds = peersNearby.map { entry -> EnginePeer.Id in - switch entry { - case let .peer(id, _, _): - return id - case .selfPeer: - return context.account.peerId - } - } - return context.engine.data.get( - EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)), - EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) - ) - |> map { peerMap, participantCountMap -> PeersNearbyData? in - var users: [PeerNearbyEntry] = [] - var groups: [PeerNearbyEntry] = [] - var visible = false - for peerNearby in peersNearby { - switch peerNearby { - case let .peer(id, expires, distance): - if let maybePeer = peerMap[id], let peer = maybePeer { - if id.namespace == Namespaces.Peer.CloudUser { - users.append(PeerNearbyEntry(peer: peer, memberCount: nil, expires: expires, distance: distance)) - } else { - var participantCount: Int32? - if let maybeParticipantCount = participantCountMap[id] { - participantCount = maybeParticipantCount.flatMap(Int32.init) - } - groups.append(PeerNearbyEntry(peer: peer, memberCount: participantCount, expires: expires, distance: distance)) - } - } - case let .selfPeer(expires): - visible = true - if let maybePeer = peerMap[context.account.peerId], let peer = maybePeer { - users.append(PeerNearbyEntry(peer: peer, memberCount: nil, expires: expires, distance: 0)) - } - } - } - return PeersNearbyData(latitude: coordinate.latitude, longitude: coordinate.longitude, address: address, visible: visible, accountPeerId: context.account.peerId, users: users, groups: groups, channels: []) - } - } - - let disposable = peersNearby.start(next: { data in - subscriber.putNext(data) - }) - - return ActionDisposable { - disposable.dispose() - let _ = peersNearbyContext.get() - } - } - } - dataPromise.set(.single(nil) |> then(dataSignal)) - - let previousData = Atomic(value: nil) - let displayLoading: Signal = .single(false) - |> then( - .single(true) - |> delay(1.0, queue: Queue.mainQueue()) - ) - - let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), chatLocationPromise.get(), displayLoading, expandedPromise.get(), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.peersNearby))) - |> deliverOnMainQueue - |> map { presentationData, data, chatLocation, displayLoading, expanded, view -> (ItemListControllerState, (ItemListNodeState, Any)) in - let previous = previousData.swap(data) - let state = view?.get(PeersNearbyState.self) ?? .default - - var crossfade = false - if (data?.users.isEmpty ?? true) != (previous?.users.isEmpty ?? true) { - crossfade = true - } - if (data?.groups.isEmpty ?? true) != (previous?.groups.isEmpty ?? true) { - crossfade = true - } - - let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PeopleNearby_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: peersNearbyControllerEntries(data: data, state: state, presentationData: presentationData, displayLoading: displayLoading, expanded: expanded, chatLocation: chatLocation), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: !crossfade) - - return (controllerState, (listState, arguments)) - } - |> afterDisposed { - actionsDisposable.dispose() - } - - let controller = PeersNearbyControllerImpl(context: context, state: signal) - chatLocationPromise.set(controller.chatLocation.get()) - controller.didDisappear = { [weak controller] _ in - controller?.clearItemNodesHighlight(animated: true) - } - navigateToProfileImpl = { [weak controller] peer, distance in - if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .nearbyPeer(distance: distance), avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) { - navigationController.pushViewController(controller) - } - } - navigateToChatImpl = { [weak controller] peer in - if let navigationController = controller?.navigationController as? NavigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil)) - } - } - pushControllerImpl = { [weak controller] c in - if let controller = controller { - (controller.navigationController as? NavigationController)?.pushViewController(c, animated: true) - } - } - replaceTopControllerImpl = { [weak controller] c in - if let controller = controller { - (controller.navigationController as? NavigationController)?.replaceTopController(c, animated: true) - } - } - presentControllerImpl = { [weak controller] c, p in - if let controller = controller { - controller.present(c, in: .window(.root), with: p) - } - } - presentInGlobalOverlayImpl = { [weak controller] c in - if let controller = controller { - controller.presentInGlobalOverlay(c) - } - } - return controller -} diff --git a/submodules/PeersNearbyUI/Sources/PeersNearbyHeaderItem.swift b/submodules/PeersNearbyUI/Sources/PeersNearbyHeaderItem.swift deleted file mode 100644 index e1b70cb30c..0000000000 --- a/submodules/PeersNearbyUI/Sources/PeersNearbyHeaderItem.swift +++ /dev/null @@ -1,127 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import SwiftSignalKit -import TelegramPresentationData -import ItemListUI -import PresentationDataUtils -import AnimatedStickerNode -import TelegramAnimatedStickerNode -import AccountContext - -class PeersNearbyHeaderItem: ListViewItem, ItemListItem { - let context: AccountContext - let theme: PresentationTheme - let text: String - let sectionId: ItemListSectionId - - init(context: AccountContext, theme: PresentationTheme, text: String, sectionId: ItemListSectionId) { - self.context = context - self.theme = theme - self.text = text - self.sectionId = sectionId - } - - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { - async { - let node = PeersNearbyHeaderItemNode() - let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) - - node.contentSize = layout.contentSize - node.insets = layout.insets - - Queue.mainQueue().async { - completion(node, { - return (nil, { _ in apply() }) - }) - } - } - } - - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { - Queue.mainQueue().async { - guard let nodeValue = node() as? PeersNearbyHeaderItemNode else { - assertionFailure() - return - } - - let makeLayout = nodeValue.asyncLayout() - - async { - let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) - Queue.mainQueue().async { - completion(layout, { _ in - apply() - }) - } - } - } - } -} - -private let titleFont = Font.regular(13.0) - -class PeersNearbyHeaderItemNode: ListViewItemNode { - private let titleNode: TextNode - private var animationNode: AnimatedStickerNode - - private var item: PeersNearbyHeaderItem? - - init() { - self.titleNode = TextNode() - self.titleNode.isUserInteractionEnabled = false - self.titleNode.contentMode = .left - self.titleNode.contentsScale = UIScreen.main.scale - - self.animationNode = DefaultAnimatedStickerNodeImpl() - - super.init(layerBacked: false) - - self.addSubnode(self.titleNode) - self.addSubnode(self.animationNode) - } - - func asyncLayout() -> (_ item: PeersNearbyHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { - let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - - return { item, params, neighbors in - let leftInset: CGFloat = 32.0 + params.leftInset - let topInset: CGFloat = 92.0 - - let attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.freeTextColor) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - - let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height) - let insets = itemListNeighborsGroupedInsets(neighbors, params) - - let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - - return (layout, { [weak self] in - if let strongSelf = self { - if strongSelf.item == nil { - strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "Compass"), width: 192, height: 192, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) - strongSelf.animationNode.visibility = true - } - strongSelf.item = item - strongSelf.accessibilityLabel = attributedText.string - - let iconSize = CGSize(width: 96.0, height: 96.0) - strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize) - strongSelf.animationNode.updateLayout(size: iconSize) - - let _ = titleApply() - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset + 8.0), size: titleLayout.size) - } - }) - } - } - - override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - } - - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - } -} diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index d4bef601c4..5fd4a90825 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -3645,8 +3645,16 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let controller = context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: loadedPack.flatMap { [$0] } ?? [], actionTitle: nil, isEditing: false, expandIfNeeded: false, parentNavigationController: navigationController, sendSticker: { _, _, _ in return false - }, actionPerformed: { added in - updatedInstalled = added + }, actionPerformed: { actions in + guard let action = actions.first?.action else { + return + } + switch action { + case .add: + updatedInstalled = true + case .remove: + updatedInstalled = false + } }) presentController(controller) break diff --git a/submodules/QrCodeUI/BUILD b/submodules/QrCodeUI/BUILD index e7eb7e221b..8a41508602 100644 --- a/submodules/QrCodeUI/BUILD +++ b/submodules/QrCodeUI/BUILD @@ -11,6 +11,7 @@ swift_library( ], deps = [ "//submodules/TelegramCore:TelegramCore", + "//submodules/MtProtoKit:MtProtoKit", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", @@ -40,6 +41,8 @@ swift_library( "//submodules/Components/MultilineTextComponent", "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/TelegramUI/Components/SegmentControlComponent", + "//submodules/UrlEscaping:UrlEscaping", ], visibility = [ "//visibility:public", diff --git a/submodules/QrCodeUI/Sources/QrCodeScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScreen.swift index 0323caf18b..0cbbbe41ea 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScreen.swift @@ -18,47 +18,56 @@ import Markdown import TextFormat import QrCode import LottieComponent +import MtProtoKit +import SegmentControlComponent +import UrlEscaping -private func shareQrCode(context: AccountContext, link: String, ecl: String, view: UIView) { - let _ = (qrCode(string: link, color: .black, backgroundColor: .white, icon: .custom(UIImage(bundleImageName: "Chat/Links/QrLogo")), ecl: ecl) - |> map { _, generator -> UIImage? in - let imageSize = CGSize(width: 768.0, height: 768.0) - let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), scale: 1.0)) - return context?.generateImage() - } - |> deliverOnMainQueue).start(next: { image in - guard let image = image else { - return - } - - let activityController = UIActivityViewController(activityItems: [image], applicationActivities: nil) +private func shareQrCode(sharedContext: SharedAccountContext, subject: QrCodeScreen.Subject, asImage: Bool, view: UIView) { + let shareImpl: (Any) -> Void = { item in + let activityController = UIActivityViewController(activityItems: [item], applicationActivities: nil) if let window = view.window { activityController.popoverPresentationController?.sourceView = window activityController.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0)) } - context.sharedContext.applicationBindings.presentNativeController(activityController) - }) + sharedContext.applicationBindings.presentNativeController(activityController) + } + if asImage { + let _ = (qrCode(string: subject.link, color: .black, backgroundColor: .white, icon: subject.icon, ecl: subject.ecl) + |> map { _, generator -> UIImage? in + let imageSize = CGSize(width: 768.0, height: 768.0) + let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), scale: 1.0)) + return context?.generateImage() + } + |> deliverOnMainQueue).start(next: { image in + guard let image else { + return + } + shareImpl(image) + }) + } else { + shareImpl(subject.link) + } } private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment - let context: AccountContext + let sharedContext: SharedAccountContext let subject: QrCodeScreen.Subject let dismiss: () -> Void init( - context: AccountContext, + sharedContext: SharedAccountContext, subject: QrCodeScreen.Subject, dismiss: @escaping () -> Void ) { - self.context = context + self.sharedContext = sharedContext self.subject = subject self.dismiss = dismiss } static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { - if lhs.context !== rhs.context { + if lhs.sharedContext !== rhs.sharedContext { return false } return true @@ -70,11 +79,19 @@ private final class SheetContent: CombinedComponent { private var initialBrightness: CGFloat? private var brightnessArguments: (Double, Double, CGFloat, CGFloat)? private var animator: ConstantDisplayLinkAnimator? - - init(context: AccountContext) { + + var selectedProxyExternalLink: Bool + + init(sharedContext: SharedAccountContext, subject: QrCodeScreen.Subject) { + if case let .proxy(_, externalLink) = subject { + self.selectedProxyExternalLink = externalLink + } else { + self.selectedProxyExternalLink = false + } + super.init() - self.idleTimerExtensionDisposable.set(context.sharedContext.applicationBindings.pushIdleTimerExtension()) + self.idleTimerExtensionDisposable.set(sharedContext.applicationBindings.pushIdleTimerExtension()) self.animator = ConstantDisplayLinkAnimator(update: { [weak self] in self?.updateBrightness() @@ -116,16 +133,18 @@ private final class SheetContent: CombinedComponent { } func makeState() -> State { - return State(context: self.context) + return State(sharedContext: self.sharedContext, subject: self.subject) } static var body: Body { let qrCode = Child(PlainButtonComponent.self) let closeButton = Child(GlassBarButtonComponent.self) let title = Child(Text.self) + let segmentControl = Child(SegmentControlComponent.self) let text = Child(BalancedTextComponent.self) let button = Child(ButtonComponent.self) + let secondaryButton = Child(ButtonComponent.self) return { context in let environment = context.environment[EnvironmentType.self] @@ -134,10 +153,15 @@ private final class SheetContent: CombinedComponent { let theme = environment.theme let strings = environment.strings - - let link = component.subject.link - let ecl = component.subject.ecl - + let state = context.state + + let effectiveSubject: QrCodeScreen.Subject + if case let .proxy(server, _) = component.subject { + effectiveSubject = .proxy(server: server, externalLink: state.selectedProxyExternalLink) + } else { + effectiveSubject = component.subject + } + let titleString: String let textString: String switch component.subject { @@ -154,6 +178,9 @@ private final class SheetContent: CombinedComponent { case .chatFolder: titleString = strings.InviteLink_QRCodeFolder_Title textString = strings.InviteLink_QRCodeFolder_Text + case .proxy: + titleString = "" + textString = strings.SocksProxySetup_ShareQRCodeInfo default: titleString = "" textString = "" @@ -185,24 +212,67 @@ private final class SheetContent: CombinedComponent { ) let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 - - let title = title.update( - component: Text(text: titleString, font: Font.semibold(17.0), color: theme.list.itemPrimaryTextColor), - availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), - transition: .immediate - ) - context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height)) - ) - contentSize.height += title.size.height + + if case .proxy = component.subject { + let constrainedSegmentWidth = min(constrainedTitleWidth, max(200.0, context.availableSize.width - 144.0)) + + let theme = SegmentControlComponent.Theme( + backgroundColor: theme.rootController.navigationBar.segmentedBackgroundColor, + legacyBackgroundColor: theme.overallDarkAppearance ? theme.list.itemBlocksBackgroundColor : theme.rootController.navigationBar.segmentedBackgroundColor, + foregroundColor: theme.actionSheet.opaqueItemBackgroundColor, + textColor: theme.rootController.navigationBar.segmentedTextColor, + dividerColor: theme.rootController.navigationBar.segmentedDividerColor + ) + + //TODO:localize + let segmentControl = segmentControl.update( + component: SegmentControlComponent( + theme: theme, + items: [ + SegmentControlComponent.Item(id: AnyHashable(false), title: "tg:// link"), + SegmentControlComponent.Item(id: AnyHashable(true), title: "t.me link") + ], + selectedId: AnyHashable(state.selectedProxyExternalLink), + action: { id in + guard let externalLink = id.base as? Bool else { + return + } + if state.selectedProxyExternalLink != externalLink { + state.selectedProxyExternalLink = externalLink + state.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) + } + } + ), + availableSize: CGSize(width: constrainedSegmentWidth, height: 36.0), + transition: .immediate + ) + context.add(segmentControl + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height)) + ) + contentSize.height += segmentControl.size.height + } else { + let title = title.update( + component: Text(text: titleString, font: Font.semibold(17.0), color: theme.list.itemPrimaryTextColor), + availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height)) + ) + contentSize.height += title.size.height + } contentSize.height += 13.0 let qrCode = qrCode.update( component: PlainButtonComponent( - content: AnyComponent(QrCodeComponent(context: component.context, link: link, ecl: ecl)), + content: AnyComponent( + QrCodeComponent( + subject: effectiveSubject + ) + ), action: { [weak controller] in if let view = controller?.view { - shareQrCode(context: component.context, link: link, ecl: ecl, view: view) + shareQrCode(sharedContext: component.sharedContext, subject: effectiveSubject, asImage: true, view: view) } }, animateScale: false @@ -250,8 +320,7 @@ private final class SheetContent: CombinedComponent { style: .glass, color: theme.list.itemCheckColors.fillColor, foreground: theme.list.itemCheckColors.foregroundColor, - pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), - cornerRadius: 10.0, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) ), content: AnyComponentWithIdentity( id: AnyHashable(0), @@ -261,7 +330,7 @@ private final class SheetContent: CombinedComponent { displaysProgress: false, action: { [weak controller] in if let view = controller?.view { - shareQrCode(context: component.context, link: link, ecl: ecl, view: view) + shareQrCode(sharedContext: component.sharedContext, subject: effectiveSubject, asImage: true, view: view) } } ), @@ -272,8 +341,42 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) ) contentSize.height += button.size.height + + if case .proxy = component.subject { + contentSize.height += 8.0 + + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + let secondaryButton = secondaryButton.update( + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemAccentColor.withMultipliedAlpha(0.1), + foreground: theme.list.itemAccentColor, + pressedColor: theme.list.itemAccentColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: strings.SocksProxySetup_ShareLink, font: Font.semibold(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .center)))) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak controller] in + if let view = controller?.view { + shareQrCode(sharedContext: component.sharedContext, subject: effectiveSubject, asImage: false, view: view) + } + } + ), + availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), + transition: .immediate + ) + context.add(secondaryButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + secondaryButton.size.height / 2.0)) + ) + contentSize.height += secondaryButton.size.height + } + contentSize.height += buttonInsets.bottom - + return contentSize } } @@ -282,19 +385,19 @@ private final class SheetContent: CombinedComponent { private final class QrCodeSheetComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment - private let context: AccountContext + private let sharedContext: SharedAccountContext private let subject: QrCodeScreen.Subject init( - context: AccountContext, + sharedContext: SharedAccountContext, subject: QrCodeScreen.Subject ) { - self.context = context + self.sharedContext = sharedContext self.subject = subject } static func ==(lhs: QrCodeSheetComponent, rhs: QrCodeSheetComponent) -> Bool { - if lhs.context !== rhs.context { + if lhs.sharedContext !== rhs.sharedContext { return false } return true @@ -312,7 +415,7 @@ private final class QrCodeSheetComponent: CombinedComponent { let sheet = sheet.update( component: SheetComponent( content: AnyComponent(SheetContent( - context: context.component.context, + sharedContext: context.component.sharedContext, subject: context.component.subject, dismiss: { animateOut.invoke(Action { _ in @@ -372,10 +475,11 @@ public final class QrCodeScreen: ViewControllerComponentContainer { case groupCall } - public enum Subject { + public enum Subject: Equatable { case peer(peer: EnginePeer) case invite(invite: ExportedInvitation, type: SubjectType) case chatFolder(slug: String) + case proxy(server: ProxyServerSettings, externalLink: Bool) var link: String { switch self { @@ -389,39 +493,79 @@ public final class QrCodeScreen: ViewControllerComponentContainer { } else { return "https://t.me/addlist/\(slug)" } + case let .proxy(server, externalLink): + var link: String + let serverHost = server.host.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "" + switch server.connection { + case let .mtp(secret): + let secret = MTProxySecret.parseData(secret)?.serializeToString() ?? "" + link = "\(externalLink ? "https://t.me/proxy" : "tg://proxy")?server=\(serverHost)&port=\(server.port)" + link += "&secret=\(secret.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" + case let .socks5(username, password): + link = "\(externalLink ? "https://t.me/socks" : "tg://socks")?server=\(serverHost)&port=\(server.port)" + if let username, !username.isEmpty { + link += "&user=\(username.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" + } + if let password, !password.isEmpty { + link += "&pass=\(password.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" + } + } + return link } } var ecl: String { switch self { - case .peer: - return "Q" - case .invite: - return "Q" - case .chatFolder: + case .peer, .invite, .chatFolder, .proxy: return "Q" } } + + var icon: QrCodeIcon { + switch self { + case .peer, .invite, .chatFolder: + return .custom(UIImage(bundleImageName: "Chat/Links/QrLogo")) + case .proxy: + return .proxy + } + } } - - private let context: AccountContext - + public init( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, subject: QrCodeScreen.Subject ) { - self.context = context - super.init( context: context, component: QrCodeSheetComponent( - context: context, + sharedContext: context.sharedContext, subject: subject ), navigationBarAppearance: .none, statusBarStyle: .ignore, - theme: .default // + theme: .default, + updatedPresentationData: updatedPresentationData + ) + + self.navigationPresentation = .flatModal + } + + public init( + sharedContext: SharedAccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal), + subject: QrCodeScreen.Subject + ) { + super.init( + component: QrCodeSheetComponent( + sharedContext: sharedContext, + subject: subject + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + presentationMode: .default, + theme: .default, + updatedPresentationData: updatedPresentationData ) self.navigationPresentation = .flatModal @@ -439,28 +583,16 @@ public final class QrCodeScreen: ViewControllerComponentContainer { } private final class QrCodeComponent: Component { - let context: AccountContext - let link: String - let ecl: String + let subject: QrCodeScreen.Subject init( - context: AccountContext, - link: String, - ecl: String + subject: QrCodeScreen.Subject ) { - self.context = context - self.link = link - self.ecl = ecl + self.subject = subject } static func ==(lhs: QrCodeComponent, rhs: QrCodeComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.link != rhs.link { - return false - } - if lhs.ecl != rhs.ecl { + if lhs.subject != rhs.subject { return false } return true @@ -502,9 +634,14 @@ private final class QrCodeComponent: Component { let previousComponent = self.component self.component = component self.state = state - - if previousComponent?.link != component.link { - self.imageNode.setSignal(qrCode(string: component.link, color: .black, backgroundColor: .white, icon: .cutout, ecl: component.ecl) |> beforeNext { [weak self] size, _ in + + var isProxy = false + if case .proxy = component.subject { + isProxy = true + } + + if previousComponent?.subject != component.subject { + self.imageNode.setSignal(qrCode(string: component.subject.link, color: .black, backgroundColor: .white, icon: isProxy ? .proxy : .cutout, ecl: component.subject.ecl) |> beforeNext { [weak self] size, _ in guard let self else { return } @@ -524,7 +661,7 @@ private final class QrCodeComponent: Component { let imageFrame = CGRect(origin: CGPoint(x: (size.width - imageSize.width) / 2.0, y: (size.height - imageSize.height) / 2.0), size: imageSize) self.imageNode.frame = imageFrame - if let qrCodeSize = self.qrCodeSize { + if !isProxy, let qrCodeSize = self.qrCodeSize { let (_, cutoutFrame, _) = qrCodeCutout(size: qrCodeSize, dimensions: imageSize, scale: nil) let _ = self.icon.update( diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerActionSheetController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerActionSheetController.swift deleted file mode 100644 index bcc32f5847..0000000000 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerActionSheetController.swift +++ /dev/null @@ -1,446 +0,0 @@ -import Foundation -import UIKit -import Display -import TelegramCore -import Postbox -import AsyncDisplayKit -import UIKit -import SwiftSignalKit -import TelegramPresentationData -import ActivityIndicator -import OverlayStatusController -import AccountContext -import PresentationDataUtils -import UrlEscaping - -public final class ProxyServerActionSheetController: ActionSheetController { - private let sharedContext: SharedAccountContext - private var presentationDisposable: Disposable? - - private let _ready = Promise() - override public var ready: Promise { - return self._ready - } - - private var isDismissed: Bool = false - - convenience public init(context: AccountContext, server: ProxyServerSettings) { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.init(sharedContext: context.sharedContext, presentationData: presentationData, accountManager: context.sharedContext.accountManager, postbox: context.account.postbox, network: context.account.network, server: server, updatedPresentationData: context.sharedContext.presentationData) - } - - public init(sharedContext: SharedAccountContext, presentationData: PresentationData, accountManager: AccountManager, postbox: Postbox, network: Network, server: ProxyServerSettings, updatedPresentationData: Signal?) { - self.sharedContext = sharedContext - let sheetTheme = ActionSheetControllerTheme(presentationData: presentationData) - super.init(theme: sheetTheme) - - self._ready.set(.single(true)) - - var items: [ActionSheetItem] = [] - if case .mtp = server.connection { - items.append(ActionSheetTextItem(title: presentationData.strings.SocksProxySetup_AdNoticeHelp)) - } - items.append(ProxyServerInfoItem(strings: presentationData.strings, network: network, server: server)) - items.append(ProxyServerActionItem(sharedContext: sharedContext, accountManager:accountManager, postbox: postbox, network: network, presentationData: presentationData, server: server, dismiss: { [weak self] success in - guard let strongSelf = self, !strongSelf.isDismissed else { - return - } - strongSelf.isDismissed = true - if success { - strongSelf.present(OverlayStatusController(theme: presentationData.theme, type: .shieldSuccess(presentationData.strings.SocksProxySetup_ProxyEnabled, false)), in: .window(.root)) - } - strongSelf.dismissAnimated() - }, present: { [weak self] c, a in - self?.present(c, in: .window(.root), with: a) - })) - self.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { [weak self] in - self?.dismissAnimated() - }) - ]) - ]) - - if let updatedPresentationData = updatedPresentationData { - self.presentationDisposable = updatedPresentationData.start(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) - } - }) - } - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDisposable?.dispose() - } -} - -private final class ProxyServerInfoItem: ActionSheetItem { - private let strings: PresentationStrings - private let network: Network - private let server: ProxyServerSettings - - init(strings: PresentationStrings, network: Network, server: ProxyServerSettings) { - self.strings = strings - self.network = network - self.server = server - } - - func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - return ProxyServerInfoItemNode(theme: theme, strings: self.strings, network: self.network, server: self.server) - } - - func updateNode(_ node: ActionSheetItemNode) { - } -} - -private enum ProxyServerInfoStatusType { - case generic(String) - case failed(String) -} - -private final class ProxyServerInfoItemNode: ActionSheetItemNode { - private let theme: ActionSheetControllerTheme - private let strings: PresentationStrings - private let textFont: UIFont - - private let network: Network - private let server: ProxyServerSettings - - private let fieldNodes: [(ImmediateTextNode, ImmediateTextNode)] - private let statusTextNode: ImmediateTextNode - - private let statusDisposable = MetaDisposable() - - init(theme: ActionSheetControllerTheme, strings: PresentationStrings, network: Network, server: ProxyServerSettings) { - self.theme = theme - self.strings = strings - self.network = network - self.server = server - - self.textFont = Font.regular(floor(theme.baseFontSize * 16.0 / 17.0)) - - var fieldNodes: [(ImmediateTextNode, ImmediateTextNode)] = [] - let serverTitleNode = ImmediateTextNode() - serverTitleNode.isUserInteractionEnabled = false - serverTitleNode.displaysAsynchronously = false - serverTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Hostname, font: textFont, textColor: theme.secondaryTextColor) - let serverTextNode = ImmediateTextNode() - serverTextNode.isUserInteractionEnabled = false - serverTextNode.displaysAsynchronously = false - serverTextNode.attributedText = NSAttributedString(string: urlEncodedStringFromString(server.host), font: textFont, textColor: theme.primaryTextColor) - fieldNodes.append((serverTitleNode, serverTextNode)) - - let portTitleNode = ImmediateTextNode() - portTitleNode.isUserInteractionEnabled = false - portTitleNode.displaysAsynchronously = false - portTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Port, font: textFont, textColor: theme.secondaryTextColor) - let portTextNode = ImmediateTextNode() - portTextNode.isUserInteractionEnabled = false - portTextNode.displaysAsynchronously = false - portTextNode.attributedText = NSAttributedString(string: "\(server.port)", font: textFont, textColor: theme.primaryTextColor) - fieldNodes.append((portTitleNode, portTextNode)) - - switch server.connection { - case let .socks5(username, password): - if let username = username { - let usernameTitleNode = ImmediateTextNode() - usernameTitleNode.isUserInteractionEnabled = false - usernameTitleNode.displaysAsynchronously = false - usernameTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Username, font: textFont, textColor: theme.secondaryTextColor) - let usernameTextNode = ImmediateTextNode() - usernameTextNode.isUserInteractionEnabled = false - usernameTextNode.displaysAsynchronously = false - usernameTextNode.attributedText = NSAttributedString(string: username, font: textFont, textColor: theme.primaryTextColor) - fieldNodes.append((usernameTitleNode, usernameTextNode)) - } - - if let password = password { - let passwordTitleNode = ImmediateTextNode() - passwordTitleNode.isUserInteractionEnabled = false - passwordTitleNode.displaysAsynchronously = false - passwordTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Password, font: textFont, textColor: theme.secondaryTextColor) - let passwordTextNode = ImmediateTextNode() - passwordTextNode.isUserInteractionEnabled = false - passwordTextNode.displaysAsynchronously = false - passwordTextNode.attributedText = NSAttributedString(string: password, font: textFont, textColor: theme.primaryTextColor) - fieldNodes.append((passwordTitleNode, passwordTextNode)) - } - case .mtp: - let passwordTitleNode = ImmediateTextNode() - passwordTitleNode.isUserInteractionEnabled = false - passwordTitleNode.displaysAsynchronously = false - passwordTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Secret, font: textFont, textColor: theme.secondaryTextColor) - let passwordTextNode = ImmediateTextNode() - passwordTextNode.isUserInteractionEnabled = false - passwordTextNode.displaysAsynchronously = false - passwordTextNode.attributedText = NSAttributedString(string: "•••••", font: textFont, textColor: theme.primaryTextColor) - fieldNodes.append((passwordTitleNode, passwordTextNode)) - } - - let statusTitleNode = ImmediateTextNode() - statusTitleNode.isUserInteractionEnabled = false - statusTitleNode.displaysAsynchronously = false - statusTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Status, font: textFont, textColor: theme.secondaryTextColor) - let statusTextNode = ImmediateTextNode() - statusTextNode.isUserInteractionEnabled = false - statusTextNode.displaysAsynchronously = false - statusTextNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_ProxyStatusChecking, font: textFont, textColor: theme.primaryTextColor) - fieldNodes.append((statusTitleNode, statusTextNode)) - - self.fieldNodes = fieldNodes - self.statusTextNode = statusTextNode - - super.init(theme: theme) - - for (lhs, rhs) in fieldNodes { - self.addSubnode(lhs) - self.addSubnode(rhs) - } - } - - deinit { - self.statusDisposable.dispose() - } - - override func didLoad() { - super.didLoad() - - let statusesContext = ProxyServersStatuses(network: network, servers: .single([self.server])) - self.statusDisposable.set((statusesContext.statuses() - |> map { return $0.first?.value } - |> distinctUntilChanged - |> deliverOnMainQueue).start(next: { [weak self] status in - if let strongSelf = self, let status = status { - let statusType: ProxyServerInfoStatusType - switch status { - case .checking: - statusType = .generic(strongSelf.strings.SocksProxySetup_ProxyStatusChecking) - case let .available(rtt): - let pingTime = Int(rtt * 1000.0) - statusType = .generic(strongSelf.strings.SocksProxySetup_ProxyStatusPing("\(pingTime)").string) - case .notAvailable: - statusType = .failed(strongSelf.strings.SocksProxySetup_ProxyStatusUnavailable) - } - strongSelf.setStatus(statusType) - } - })) - } - - func setStatus(_ status: ProxyServerInfoStatusType) { - let attributedString: NSAttributedString - switch status { - case let .generic(text): - attributedString = NSAttributedString(string: text, font: textFont, textColor: theme.primaryTextColor) - case let .failed(text): - attributedString = NSAttributedString(string: text, font: textFont, textColor: theme.destructiveActionTextColor) - } - self.statusTextNode.attributedText = attributedString - self.requestLayoutUpdate() - } - - public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let size = CGSize(width: constrainedSize.width, height: 36.0 * CGFloat(self.fieldNodes.count) + 12.0) - - var offset: CGFloat = 15.0 - for (lhs, rhs) in self.fieldNodes { - let lhsSize = lhs.updateLayout(CGSize(width: size.width - 18.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)) - lhs.frame = CGRect(origin: CGPoint(x: 18, y: offset), size: lhsSize) - - let rhsSize = rhs.updateLayout(CGSize(width: max(1.0, size.width - 18 * 2.0 - lhsSize.width - 4.0), height: CGFloat.greatestFiniteMagnitude)) - rhs.frame = CGRect(origin: CGPoint(x: size.width - 18 - rhsSize.width, y: offset), size: rhsSize) - - offset += 36.0 - } - - self.updateInternalLayout(size, constrainedSize: constrainedSize) - return size - } -} - -private final class ProxyServerActionItem: ActionSheetItem { - private let sharedContext: SharedAccountContext - private let accountManager: AccountManager - private let postbox: Postbox - private let network: Network - private let presentationData: PresentationData - private let server: ProxyServerSettings - private let dismiss: (Bool) -> Void - private let present: (ViewController, Any?) -> Void - - init(sharedContext: SharedAccountContext, accountManager: AccountManager, postbox: Postbox, network: Network, presentationData: PresentationData, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { - self.sharedContext = sharedContext - self.accountManager = accountManager - self.postbox = postbox - self.network = network - self.presentationData = presentationData - self.server = server - self.dismiss = dismiss - self.present = present - } - - func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - return ProxyServerActionItemNode(sharedContext: self.sharedContext, accountManager: self.accountManager, postbox: self.postbox, network: self.network, presentationData: self.presentationData, theme: theme, server: self.server, dismiss: self.dismiss, present: self.present) - } - - func updateNode(_ node: ActionSheetItemNode) { - } -} - -private final class ProxyServerActionItemNode: ActionSheetItemNode { - private let sharedContext: SharedAccountContext - private let accountManager: AccountManager - private let postbox: Postbox - private let network: Network - private let presentationData: PresentationData - private let theme: ActionSheetControllerTheme - private let server: ProxyServerSettings - private let dismiss: (Bool) -> Void - private let present: (ViewController, Any?) -> Void - - private let buttonNode: HighlightableButtonNode - private let titleNode: ImmediateTextNode - private let activityIndicator: ActivityIndicator - - private let disposable = MetaDisposable() - private var revertSettings: ProxySettings? - - init(sharedContext: SharedAccountContext, accountManager: AccountManager, postbox: Postbox, network: Network, presentationData: PresentationData, theme: ActionSheetControllerTheme, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { - self.sharedContext = sharedContext - self.accountManager = accountManager - self.postbox = postbox - self.network = network - self.theme = theme - self.presentationData = presentationData - self.server = server - self.dismiss = dismiss - self.present = present - - let titleFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) - - self.titleNode = ImmediateTextNode() - self.titleNode.isUserInteractionEnabled = false - self.titleNode.displaysAsynchronously = false - self.titleNode.attributedText = NSAttributedString(string: presentationData.strings.SocksProxySetup_ConnectAndSave, font: titleFont, textColor: theme.controlAccentColor) - - self.activityIndicator = ActivityIndicator(type: .custom(theme.controlAccentColor, 22.0, 1.5, false)) - self.activityIndicator.isHidden = true - - self.buttonNode = HighlightableButtonNode() - - super.init(theme: theme) - - self.addSubnode(self.titleNode) - self.addSubnode(self.activityIndicator) - self.addSubnode(self.buttonNode) - - self.buttonNode.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor - } else { - UIView.animate(withDuration: 0.3, animations: { - strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor - }) - } - } - } - - self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) - } - - deinit { - self.disposable.dispose() - if let revertSettings = self.revertSettings { - let _ = updateProxySettingsInteractively(accountManager: self.accountManager, { _ in - return revertSettings - }) - } - } - - public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let size = CGSize(width: constrainedSize.width, height: 57.0) - - self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) - - let labelSize = self.titleNode.updateLayout(CGSize(width: max(1.0, size.width - 10.0), height: size.height)) - let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) - let activitySize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0)) - self.titleNode.frame = titleFrame - self.activityIndicator.frame = CGRect(origin: CGPoint(x: 14.0, y: titleFrame.minY - 0.0), size: activitySize) - - self.updateInternalLayout(size, constrainedSize: constrainedSize) - return size - } - - @objc private func buttonPressed() { - let proxyServerSettings = self.server - let _ = (self.accountManager.transaction { transaction -> ProxySettings in - var currentSettings: ProxySettings? - let _ = updateProxySettingsInteractively(transaction: transaction, { settings in - currentSettings = settings - var settings = settings - if let index = settings.servers.firstIndex(of: proxyServerSettings) { - settings.servers[index] = proxyServerSettings - settings.activeServer = proxyServerSettings - } else { - settings.servers.insert(proxyServerSettings, at: 0) - settings.activeServer = proxyServerSettings - } - settings.enabled = true - return settings - }) - return currentSettings ?? ProxySettings.defaultSettings - } |> deliverOnMainQueue).start(next: { [weak self] previousSettings in - if let strongSelf = self { - strongSelf.revertSettings = previousSettings - strongSelf.buttonNode.isUserInteractionEnabled = false - strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.SocksProxySetup_Connecting, font: Font.regular(20.0), textColor: strongSelf.theme.primaryTextColor) - strongSelf.activityIndicator.isHidden = false - strongSelf.requestLayoutUpdate() - - let signal = strongSelf.network.connectionStatus - |> filter { status in - switch status { - case let .online(proxyAddress): - if proxyAddress == proxyServerSettings.host { - return true - } else { - return false - } - default: - return false - } - } - |> map { _ -> Bool in - return true - } - |> timeout(15.0, queue: Queue.mainQueue(), alternate: .single(false)) - |> deliverOnMainQueue - strongSelf.disposable.set(signal.start(next: { value in - if let strongSelf = self { - strongSelf.activityIndicator.isHidden = true - strongSelf.revertSettings = nil - if value { - strongSelf.dismiss(true) - } else { - let _ = updateProxySettingsInteractively(accountManager: strongSelf.accountManager, { _ in - return previousSettings - }) - strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.SocksProxySetup_ConnectAndSave, font: Font.regular(20.0), textColor: strongSelf.theme.controlAccentColor) - strongSelf.buttonNode.isUserInteractionEnabled = true - strongSelf.requestLayoutUpdate() - - strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.SocksProxySetup_FailedToConnect, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) - } - } - })) - } - }) - } -} diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift index 560bd3fce2..88fb4aadee 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift @@ -10,20 +10,7 @@ import PresentationDataUtils import AccountContext import UrlEscaping import UrlHandling - -private func shareLink(for server: ProxyServerSettings) -> String { - var link: String - switch server.connection { - case let .mtp(secret): - let secret = MTProxySecret.parseData(secret)?.serializeToString() ?? "" - link = "tg://proxy?server=\(server.host)&port=\(server.port)" - link += "&secret=\(secret.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" - case let .socks5(username, password): - link = "https://t.me/socks?server=\(server.host)&port=\(server.port)" - link += "&user=\(username?.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")&pass=\(password?.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" - } - return link -} +import QrCodeUI private final class ProxyServerSettingsControllerArguments { let updateState: ((ProxyServerSettingsControllerState) -> ProxyServerSettingsControllerState) -> Void @@ -300,7 +287,7 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context: statePromise.set(stateValue.modify { f($0) }) } - var presentControllerImpl: ((ViewController, Any?) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? var shareImpl: (() -> Void)? @@ -370,8 +357,8 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context: let controller = ItemListController(presentationData: ItemListPresentationData(presentationData), updatedPresentationData: updatedPresentationData |> map(ItemListPresentationData.init(_:)), state: signal, tabBarItem: nil) controller.navigationPresentation = .modal - presentControllerImpl = { [weak controller] c, d in - controller?.present(c, in: .window(.root), with: d) + pushControllerImpl = { [weak controller] c in + controller?.push(c) } dismissImpl = { [weak controller] in let _ = controller?.dismiss() @@ -381,12 +368,14 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context: guard let server = proxyServerSettings(with: state) else { return } - - let link = shareLink(for: server) controller?.view.endEditing(true) - let controller = ShareProxyServerActionSheetController(presentationData: presentationData, updatedPresentationData: updatedPresentationData, link: link) - presentControllerImpl?(controller, nil) + let controller = QrCodeScreen( + sharedContext: sharedContext, + updatedPresentationData: (presentationData, updatedPresentationData), + subject: .proxy(server: server, externalLink: false) + ) + pushControllerImpl?(controller) } return controller diff --git a/submodules/SettingsUI/Sources/Data and Storage/ShareProxyServerActionSheetController.swift b/submodules/SettingsUI/Sources/Data and Storage/ShareProxyServerActionSheetController.swift deleted file mode 100644 index 1ac427d213..0000000000 --- a/submodules/SettingsUI/Sources/Data and Storage/ShareProxyServerActionSheetController.swift +++ /dev/null @@ -1,184 +0,0 @@ -import Foundation -import UIKit -import Display -import TelegramCore -import AsyncDisplayKit -import UIKit -import SwiftSignalKit -import TelegramPresentationData -import QrCode - -public final class ShareProxyServerActionSheetController: ActionSheetController { - private var presentationDisposable: Disposable? - - private let _ready = Promise() - override public var ready: Promise { - return self._ready - } - - private var isDismissed: Bool = false - - public init(presentationData: PresentationData, updatedPresentationData: Signal, link: String) { - let sheetTheme = ActionSheetControllerTheme(presentationData: presentationData) - super.init(theme: sheetTheme) - - let presentActivityController: (Any) -> Void = { [weak self] item in - let activityController = UIActivityViewController(activityItems: [item], applicationActivities: nil) - if let window = self?.view.window, let rootViewController = window.rootViewController { - activityController.popoverPresentationController?.sourceView = window - activityController.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0)) - rootViewController.present(activityController, animated: true, completion: nil) - } - } - - var items: [ActionSheetItem] = [] - items.append(ProxyServerQRCodeItem(strings: presentationData.strings, link: link, ready: { [weak self] in - self?._ready.set(.single(true)) - })) - items.append(ActionSheetButtonItem(title: presentationData.strings.SocksProxySetup_ShareQRCode, action: { [weak self] in - self?.dismissAnimated() - let _ = (qrCode(string: link, color: .black, backgroundColor: .white, icon: .proxy) - |> map { _, generator -> UIImage? in - let imageSize = CGSize(width: 768.0, height: 768.0) - let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), scale: 1.0)) - return context?.generateImage() - } - |> deliverOnMainQueue).start(next: { image in - if let image = image { - presentActivityController(image) - } - }) - })) - items.append(ActionSheetButtonItem(title: presentationData.strings.SocksProxySetup_ShareLink, action: { [weak self] in - self?.dismissAnimated() - presentActivityController(link) - })) - self.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { [weak self] in - self?.dismissAnimated() - }) - ]) - ]) - - self.presentationDisposable = updatedPresentationData.start(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) - } - }) - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDisposable?.dispose() - } -} - -private final class ProxyServerQRCodeItem: ActionSheetItem { - private let strings: PresentationStrings - private let link: String - private let ready: () -> Void - - init(strings: PresentationStrings, link: String, ready: @escaping () -> Void = {}) { - self.strings = strings - self.link = link - self.ready = ready - } - - func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - return ProxyServerQRCodeItemNode(theme: theme, strings: self.strings, link: self.link, ready: self.ready) - } - - func updateNode(_ node: ActionSheetItemNode) { - } -} - -private final class ProxyServerQRCodeItemNode: ActionSheetItemNode { - private let theme: ActionSheetControllerTheme - private let strings: PresentationStrings - private let link: String - - private let label: ASTextNode - private let imageNode: TransformImageNode - - private let ready: () -> Void - - private var cachedHasLabel = true - private var cachedHasImage = true - - init(theme: ActionSheetControllerTheme, strings: PresentationStrings, link: String, ready: @escaping () -> Void = {}) { - self.theme = theme - self.strings = strings - self.link = link - self.ready = ready - - let textFont = Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)) - - self.label = ASTextNode() - self.label.isUserInteractionEnabled = false - self.label.maximumNumberOfLines = 0 - self.label.displaysAsynchronously = false - self.label.truncationMode = .byTruncatingTail - self.label.isUserInteractionEnabled = false - self.label.attributedText = NSAttributedString(string: strings.SocksProxySetup_ShareQRCodeInfo, font: textFont, textColor: self.theme.secondaryTextColor, paragraphAlignment: .center) - - self.imageNode = TransformImageNode() - self.imageNode.clipsToBounds = true - self.imageNode.setSignal(qrCode(string: link, color: .black, backgroundColor: .white, icon: .proxy) |> map { $0.1 }, attemptSynchronously: true) - self.imageNode.cornerRadius = 14.0 - - super.init(theme: theme) - - self.addSubnode(self.label) - self.addSubnode(self.imageNode) - } - - override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let imageInset: CGFloat = 44.0 - let side = constrainedSize.width - imageInset * 2.0 - var imageSize = CGSize(width: side, height: side) - - let makeLayout = self.imageNode.asyncLayout() - let apply = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil)) - apply() - - var labelSize = self.label.measure(CGSize(width: max(1.0, constrainedSize.width - 64.0), height: constrainedSize.height)) - - self.cachedHasImage = constrainedSize.width < constrainedSize.height - if !self.cachedHasImage { - imageSize = CGSize() - } - - self.ready() - - self.cachedHasLabel = constrainedSize.height > 480 || !self.cachedHasImage - if !self.cachedHasLabel { - labelSize = CGSize() - } - let size = CGSize(width: constrainedSize.width, height: 14.0 + (labelSize.height > 0.0 ? labelSize.height + 14.0 : 0.0) + (imageSize.height > 0.0 ? imageSize.height + 14.0 : 8.0)) - - let inset: CGFloat = 32.0 - let spacing: CGFloat = 18.0 - if self.cachedHasLabel { - labelSize = self.label.measure(CGSize(width: max(1.0, size.width - inset * 2.0), height: size.height)) - self.label.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: spacing), size: labelSize) - } else { - labelSize = CGSize() - } - - if !self.cachedHasImage { - imageSize = CGSize() - } else { - imageSize = CGSize(width: size.width - imageInset * 2.0, height: size.width - imageInset * 2.0) - } - let imageOrigin = CGPoint(x: imageInset, y: self.label.frame.maxY + spacing - 4.0) - self.imageNode.frame = CGRect(origin: imageOrigin, size: imageSize) - - self.updateInternalLayout(size, constrainedSize: constrainedSize) - return size - } -} diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift index 9d92ab1692..9f35c20ff3 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift @@ -15,6 +15,7 @@ import Geocoding import WallpaperResources import Sunrise import ThemeSettingsThemeItem +import ChatTimerScreen private enum TriggerMode { case system @@ -487,32 +488,44 @@ public func themeAutoNightSettingsController(context: AccountContext) -> ViewCon break } - presentControllerImpl?(ThemeAutoNightTimeSelectionActionSheet(context: context, currentValue: currentValue, applyValue: { value in - guard let value = value else { - return - } - updateSettings { settings in - var settings = settings - switch settings.trigger { - case let .timeBased(setting): - switch setting { - case var .manual(fromSeconds, toSeconds): - switch field { - case .from: - fromSeconds = value - case .to: - toSeconds = value + let controller = ChatTimerScreen( + context: context, + configuration: ChatTimerScreen.Configuration( + style: .default, + picker: .timeOfDay, + currentValue: currentValue, + pickerValueMapping: .secondsFromMidnightGMT, + primaryActionTitle: { strings, _, _ in + strings.Wallpaper_Set + } + ), + completion: { value in + guard let value = value else { + return + } + updateSettings { settings in + var settings = settings + switch settings.trigger { + case let .timeBased(setting): + switch setting { + case var .manual(fromSeconds, toSeconds): + switch field { + case .from: + fromSeconds = value + case .to: + toSeconds = value + } + settings.trigger = .timeBased(setting: .manual(fromSeconds: fromSeconds, toSeconds: toSeconds)) + default: + break } - settings.trigger = .timeBased(setting: .manual(fromSeconds: fromSeconds, toSeconds: toSeconds)) default: break - } - default: - break + } + return settings } - return settings - } - })) + }) + presentControllerImpl?(controller) return settings } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightTimeSelectionActionSheet.swift b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightTimeSelectionActionSheet.swift deleted file mode 100644 index b357293898..0000000000 --- a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightTimeSelectionActionSheet.swift +++ /dev/null @@ -1,132 +0,0 @@ -import Foundation -import Display -import AsyncDisplayKit -import UIKit -import SwiftSignalKit -import TelegramCore -import TelegramPresentationData -import TelegramStringFormatting -import AccountContext -import UIKitRuntimeUtils - -final class ThemeAutoNightTimeSelectionActionSheet: ActionSheetController { - private var presentationDisposable: Disposable? - - private let _ready = Promise() - override var ready: Promise { - return self._ready - } - - init(context: AccountContext, currentValue: Int32, emptyTitle: String? = nil, applyValue: @escaping (Int32?) -> Void) { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let strings = presentationData.strings - - super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) - - self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) - } - }) - - self._ready.set(.single(true)) - - var updatedValue = currentValue - var items: [ActionSheetItem] = [] - items.append(ThemeAutoNightTimeSelectionActionSheetItem(strings: strings, currentValue: currentValue, valueChanged: { value in - updatedValue = value - })) - if let emptyTitle = emptyTitle { - items.append(ActionSheetButtonItem(title: emptyTitle, action: { [weak self] in - self?.dismissAnimated() - applyValue(nil) - })) - } - items.append(ActionSheetButtonItem(title: strings.Wallpaper_Set, action: { [weak self] in - self?.dismissAnimated() - applyValue(updatedValue) - })) - self.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in - self?.dismissAnimated() - }), - ]) - ]) - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDisposable?.dispose() - } -} - -private final class ThemeAutoNightTimeSelectionActionSheetItem: ActionSheetItem { - let strings: PresentationStrings - - let currentValue: Int32 - let valueChanged: (Int32) -> Void - - init(strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) { - self.strings = strings - self.currentValue = currentValue - self.valueChanged = valueChanged - } - - func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - return ThemeAutoNightTimeSelectionActionSheetItemNode(theme: theme, strings: self.strings, currentValue: self.currentValue, valueChanged: self.valueChanged) - } - - func updateNode(_ node: ActionSheetItemNode) { - } -} - -private final class ThemeAutoNightTimeSelectionActionSheetItemNode: ActionSheetItemNode { - private let theme: ActionSheetControllerTheme - private let strings: PresentationStrings - - private let valueChanged: (Int32) -> Void - private let pickerView: UIDatePicker - - init(theme: ActionSheetControllerTheme, strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) { - self.theme = theme - self.strings = strings - self.valueChanged = valueChanged - - UILabel.setDateLabel(theme.primaryTextColor) - - self.pickerView = UIDatePicker() - self.pickerView.datePickerMode = .countDownTimer - self.pickerView.datePickerMode = .time - self.pickerView.timeZone = TimeZone(secondsFromGMT: 0) - self.pickerView.date = Date(timeIntervalSince1970: Double(currentValue)) - self.pickerView.locale = Locale.current - if #available(iOS 13.4, *) { - self.pickerView.preferredDatePickerStyle = .wheels - } - self.pickerView.setValue(theme.primaryTextColor, forKey: "textColor") - - super.init(theme: theme) - - self.view.addSubview(self.pickerView) - self.pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged) - } - - public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let size = CGSize(width: constrainedSize.width, height: 216.0) - - self.pickerView.frame = CGRect(origin: CGPoint(), size: size) - - self.updateInternalLayout(size, constrainedSize: constrainedSize) - return size - } - - @objc private func datePickerUpdated() { - self.valueChanged(Int32(self.pickerView.date.timeIntervalSince1970)) - } -} - diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index 207d05b122..5cfbfa3d10 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -109,7 +109,6 @@ enum AccountStateMutationOperation { case UpdateLangPack(String, Api.LangPackDifference?) case UpdateMinAvailableMessage(MessageId) case UpdatePeerChatInclusion(peerId: PeerId, groupId: PeerGroupId, changedGroup: Bool) - case UpdatePeersNearby([PeerNearby]) case UpdateTheme(TelegramTheme) case SyncChatListFilters case UpdateChatListFilterOrder(order: [Int32]) @@ -554,11 +553,7 @@ struct AccountMutableState { mutating func updatePeerChatInclusion(peerId: PeerId, groupId: PeerGroupId, changedGroup: Bool) { self.addOperation(.UpdatePeerChatInclusion(peerId: peerId, groupId: groupId, changedGroup: changedGroup)) } - - mutating func updatePeersNearby(_ peersNearby: [PeerNearby]) { - self.addOperation(.UpdatePeersNearby(peersNearby)) - } - + mutating func updateTheme(_ theme: TelegramTheme) { self.addOperation(.UpdateTheme(theme)) } @@ -746,7 +741,7 @@ struct AccountMutableState { mutating func addOperation(_ operation: AccountStateMutationOperation) { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .AddPeerLiveTypingDraftUpdate, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .UpdateWallpaper, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateGroupCallChainBlocks, .UpdateGroupCallMessage, .UpdateGroupCallOpaqueMessage, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsDefaultPrivacy, .ReportMessageDelivery, .UpdateMonoForumNoPaidException, .UpdateStarGiftAuctionState, .UpdateStarGiftAuctionMyState, .UpdateEmojiGameInfo: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .AddPeerLiveTypingDraftUpdate, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdateTheme, .UpdateWallpaper, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateGroupCallChainBlocks, .UpdateGroupCallMessage, .UpdateGroupCallOpaqueMessage, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsDefaultPrivacy, .ReportMessageDelivery, .UpdateMonoForumNoPaidException, .UpdateStarGiftAuctionState, .UpdateStarGiftAuctionMyState, .UpdateEmojiGameInfo: break case let .AddMessages(messages, location): for message in messages { @@ -884,7 +879,6 @@ struct AccountReplayedFinalState { let updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] let groupCallMessageUpdates: [GroupCallMessageUpdate] let storyUpdates: [InternalStoryUpdate] - let updatedPeersNearby: [PeerNearby]? let isContactUpdates: [(PeerId, Bool)] let delayNotificatonsUntil: Int32? let updatedIncomingThreadReadStates: [PeerAndBoundThreadId: MessageId.Id] @@ -915,7 +909,6 @@ struct AccountFinalStateEvents { let updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] let groupCallMessageUpdates: [GroupCallMessageUpdate] let storyUpdates: [InternalStoryUpdate] - let updatedPeersNearby: [PeerNearby]? let isContactUpdates: [(PeerId, Bool)] let displayAlerts: [(text: String, isDropAuth: Bool)] let dismissBotWebViews: [Int64] @@ -938,10 +931,10 @@ struct AccountFinalStateEvents { let updatedEmojiGameInfo: EmojiGameInfo? var isEmpty: Bool { - return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.sentScheduledMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.groupCallMessageUpdates.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedStarsBalance.isEmpty && self.updatedTonBalance.isEmpty && self.updatedStarsRevenueStatus.isEmpty && self.reportMessageDelivery.isEmpty && self.addedConferenceInvitationMessagesIds.isEmpty && self.updatedStarGiftAuctionState.isEmpty && self.updatedStarGiftAuctionMyState.isEmpty && self.updatedEmojiGameInfo == nil + return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.sentScheduledMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.groupCallMessageUpdates.isEmpty && self.storyUpdates.isEmpty && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedStarsBalance.isEmpty && self.updatedTonBalance.isEmpty && self.updatedStarsRevenueStatus.isEmpty && self.reportMessageDelivery.isEmpty && self.addedConferenceInvitationMessagesIds.isEmpty && self.updatedStarGiftAuctionState.isEmpty && self.updatedStarGiftAuctionMyState.isEmpty && self.updatedEmojiGameInfo == nil } - init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], groupCallMessageUpdates: [GroupCallMessageUpdate] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [PeerAndBoundThreadId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [PeerAndBoundThreadId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedStarsBalance: [PeerId: StarsAmount] = [:], updatedTonBalance: [PeerId: StarsAmount] = [:], updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:], sentScheduledMessageIds: Set = Set(), reportMessageDelivery: Set = Set(), addedConferenceInvitationMessagesIds: [MessageId] = [], updatedStarGiftAuctionState: [Int64: GiftAuctionContext.State.AuctionState] = [:], updatedStarGiftAuctionMyState: [Int64: GiftAuctionContext.State.MyState] = [:], updatedEmojiGameInfo: EmojiGameInfo? = nil) { + init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], groupCallMessageUpdates: [GroupCallMessageUpdate] = [], storyUpdates: [InternalStoryUpdate] = [], isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [PeerAndBoundThreadId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [PeerAndBoundThreadId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedStarsBalance: [PeerId: StarsAmount] = [:], updatedTonBalance: [PeerId: StarsAmount] = [:], updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:], sentScheduledMessageIds: Set = Set(), reportMessageDelivery: Set = Set(), addedConferenceInvitationMessagesIds: [MessageId] = [], updatedStarGiftAuctionState: [Int64: GiftAuctionContext.State.AuctionState] = [:], updatedStarGiftAuctionMyState: [Int64: GiftAuctionContext.State.MyState] = [:], updatedEmojiGameInfo: EmojiGameInfo? = nil) { self.addedIncomingMessageIds = addedIncomingMessageIds self.addedReactionEvents = addedReactionEvents self.wasScheduledMessageIds = wasScheduledMessageIds @@ -953,7 +946,6 @@ struct AccountFinalStateEvents { self.updatedGroupCallParticipants = updatedGroupCallParticipants self.groupCallMessageUpdates = groupCallMessageUpdates self.storyUpdates = storyUpdates - self.updatedPeersNearby = updatedPeersNearby self.isContactUpdates = isContactUpdates self.displayAlerts = displayAlerts self.dismissBotWebViews = dismissBotWebViews @@ -989,7 +981,6 @@ struct AccountFinalStateEvents { self.updatedGroupCallParticipants = state.updatedGroupCallParticipants self.groupCallMessageUpdates = state.groupCallMessageUpdates self.storyUpdates = state.storyUpdates - self.updatedPeersNearby = state.updatedPeersNearby self.isContactUpdates = state.isContactUpdates self.displayAlerts = state.state.state.displayAlerts self.dismissBotWebViews = state.state.state.dismissBotWebViews diff --git a/submodules/TelegramCore/Sources/Settings/PeerContactSettings.swift b/submodules/TelegramCore/Sources/Settings/PeerContactSettings.swift index 8b4d19f0a2..69d84fa463 100644 --- a/submodules/TelegramCore/Sources/Settings/PeerContactSettings.swift +++ b/submodules/TelegramCore/Sources/Settings/PeerContactSettings.swift @@ -7,7 +7,7 @@ extension PeerStatusSettings { init(apiSettings: Api.PeerSettings) { switch apiSettings { case let .peerSettings(peerSettingsData): - let (flags, geoDistance, requestChatTitle, requestChatDate, businessBotId, businessBotManageUrl, chargePaidMessageStars, registrationMonth, phoneCountry, nameChangeDate, photoChangeDate) = (peerSettingsData.flags, peerSettingsData.geoDistance, peerSettingsData.requestChatTitle, peerSettingsData.requestChatDate, peerSettingsData.businessBotId, peerSettingsData.businessBotManageUrl, peerSettingsData.chargePaidMessageStars, peerSettingsData.registrationMonth, peerSettingsData.phoneCountry, peerSettingsData.nameChangeDate, peerSettingsData.photoChangeDate) + let (flags, requestChatTitle, requestChatDate, businessBotId, businessBotManageUrl, chargePaidMessageStars, registrationMonth, phoneCountry, nameChangeDate, photoChangeDate) = (peerSettingsData.flags, peerSettingsData.requestChatTitle, peerSettingsData.requestChatDate, peerSettingsData.businessBotId, peerSettingsData.businessBotManageUrl, peerSettingsData.chargePaidMessageStars, peerSettingsData.registrationMonth, peerSettingsData.phoneCountry, peerSettingsData.nameChangeDate, peerSettingsData.photoChangeDate) var result = PeerStatusSettings.Flags() if (flags & (1 << 1)) != 0 { result.insert(.canAddContact) @@ -44,7 +44,6 @@ extension PeerStatusSettings { } self = PeerStatusSettings( flags: result, - geoDistance: geoDistance, requestChatTitle: requestChatTitle, requestChatDate: requestChatDate, requestChatIsChannel: (flags & (1 << 10)) != 0, diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 7fde2c2909..6949921867 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1819,19 +1819,6 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: updatedState.updatePeerChatInclusion(peerId: peer.peerId, groupId: PeerGroupId(rawValue: folderId), changedGroup: true) } } - case let .updatePeerLocated(updatePeerLocatedData): - var peersNearby: [PeerNearby] = [] - for peer in updatePeerLocatedData.peers { - switch peer { - case let .peerLocated(peerLocatedData): - let (peer, expires, distance) = (peerLocatedData.peer, peerLocatedData.expires, peerLocatedData.distance) - peersNearby.append(.peer(id: peer.peerId, expires: expires, distance: distance)) - case let .peerSelfLocated(peerSelfLocatedData): - let expires = peerSelfLocatedData.expires - peersNearby.append(.selfPeer(expires: expires)) - } - } - updatedState.updatePeersNearby(peersNearby) case let .updateNewScheduledMessage(updateNewScheduledMessageData): var peerIsForum = false if let peerId = updateNewScheduledMessageData.message.peerId { @@ -3808,7 +3795,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) var currentAddQuickReplyMessages: OptimizeAddMessagesState? for operation in operations { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .AddPeerLiveTypingDraftUpdate, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateGroupCallChainBlocks, .UpdateGroupCallMessage, .UpdateGroupCallOpaqueMessage, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsDefaultPrivacy, .ReportMessageDelivery, .UpdateMonoForumNoPaidException, .UpdateStarGiftAuctionState, .UpdateStarGiftAuctionMyState, .UpdateEmojiGameInfo: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .AddPeerLiveTypingDraftUpdate, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateGroupCallChainBlocks, .UpdateGroupCallMessage, .UpdateGroupCallOpaqueMessage, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsDefaultPrivacy, .ReportMessageDelivery, .UpdateMonoForumNoPaidException, .UpdateStarGiftAuctionState, .UpdateStarGiftAuctionMyState, .UpdateEmojiGameInfo: if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) } @@ -3927,7 +3914,6 @@ func replayFinalState( var updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [] var groupCallMessageUpdates: [GroupCallMessageUpdate] = [] var storyUpdates: [InternalStoryUpdate] = [] - var updatedPeersNearby: [PeerNearby]? var isContactUpdates: [(PeerId, Bool)] = [] var stickerPackOperations: [AccountStateUpdateStickerPacksOperation] = [] var recentlyUsedStickers: [MediaId: (MessageIndex, TelegramMediaFile)] = [:] @@ -5220,8 +5206,6 @@ func replayFinalState( } case let .UpdateIsContact(peerId, value): isContactUpdates.append((peerId, value)) - case let .UpdatePeersNearby(peersNearby): - updatedPeersNearby = peersNearby case let .UpdateTheme(theme): updatedThemes[theme.id] = theme case let .UpdateWallpaper(peerId, wallpaper): @@ -6159,7 +6143,6 @@ func replayFinalState( updatedGroupCallParticipants: updatedGroupCallParticipants, groupCallMessageUpdates: groupCallMessageUpdates, storyUpdates: storyUpdates, - updatedPeersNearby: updatedPeersNearby, isContactUpdates: isContactUpdates, delayNotificatonsUntil: delayNotificatonsUntil, updatedIncomingThreadReadStates: updatedIncomingThreadReadStates, diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index b559213273..1c7108f812 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -40,10 +40,6 @@ private final class UpdatedWebpageSubscriberContext { let subscribers = Bag<(TelegramMediaWebpage) -> Void>() } -private final class UpdatedPeersNearbySubscriberContext { - let subscribers = Bag<([PeerNearby]) -> Void>() -} - private final class UpdatedStarsBalanceSubscriberContext { let subscribers = Bag<([PeerId: StarsAmount]) -> Void>() } @@ -366,7 +362,6 @@ public final class AccountStateManager { } private var updatedWebpageContexts: [MediaId: UpdatedWebpageSubscriberContext] = [:] - private var updatedPeersNearbyContext = UpdatedPeersNearbySubscriberContext() private var updatedStarsBalanceContext = UpdatedStarsBalanceSubscriberContext() private var updatedTonBalanceContext = UpdatedStarsBalanceSubscriberContext() private var updatedStarsRevenueStatusContext = UpdatedStarsRevenueStatusSubscriberContext() @@ -1127,9 +1122,6 @@ public final class AccountStateManager { if !events.updatedWebpages.isEmpty { strongSelf.notifyUpdatedWebpages(events.updatedWebpages) } - if let updatedPeersNearby = events.updatedPeersNearby { - strongSelf.notifyUpdatedPeersNearby(updatedPeersNearby) - } if !events.updatedStarsBalance.isEmpty { strongSelf.notifyUpdatedStarsBalance(events.updatedStarsBalance) } @@ -1709,34 +1701,7 @@ public final class AccountStateManager { } } } - - public func updatedPeersNearby() -> Signal<[PeerNearby], NoError> { - let queue = self.queue - return Signal { [weak self] subscriber in - let disposable = MetaDisposable() - queue.async { - if let strongSelf = self { - let index = strongSelf.updatedPeersNearbyContext.subscribers.add({ peersNearby in - subscriber.putNext(peersNearby) - }) - - disposable.set(ActionDisposable { - if let strongSelf = self { - strongSelf.updatedPeersNearbyContext.subscribers.remove(index) - } - }) - } - } - return disposable - } - } - - private func notifyUpdatedPeersNearby(_ updatedPeersNearby: [PeerNearby]) { - for subscriber in self.updatedPeersNearbyContext.subscribers.copyItems() { - subscriber(updatedPeersNearby) - } - } - + public func updatedStarsBalance() -> Signal<[PeerId: StarsAmount], NoError> { let queue = self.queue return Signal { [weak self] subscriber in @@ -2235,12 +2200,6 @@ public final class AccountStateManager { } } - public func updatedPeersNearby() -> Signal<[PeerNearby], NoError> { - return self.impl.signalWith { impl, subscriber in - return impl.updatedPeersNearby().start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) - } - } - public func updatedStarsBalance() -> Signal<[PeerId: StarsAmount], NoError> { return self.impl.signalWith { impl, subscriber in return impl.updatedStarsBalance().start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift index e04ff49eb1..df65fb9223 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift @@ -589,7 +589,7 @@ public final class CachedChannelData: CachedPeerData { var peerIds = Set() if let legacyValue = decoder.decodeOptionalInt32ForKey("pcs") { - self.peerStatusSettings = PeerStatusSettings(flags: PeerStatusSettings.Flags(rawValue: legacyValue), geoDistance: nil, managingBot: nil) + self.peerStatusSettings = PeerStatusSettings(flags: PeerStatusSettings.Flags(rawValue: legacyValue), managingBot: nil) } else if let peerStatusSettings = decoder.decodeObjectForKey("pss", decoder: { PeerStatusSettings(decoder: $0) }) as? PeerStatusSettings { self.peerStatusSettings = peerStatusSettings } else { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift index a2dfa1b17b..65104f04fc 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift @@ -232,7 +232,7 @@ public final class CachedGroupData: CachedPeerData { self.exportedInvitation = decoder.decode(ExportedInvitation.self, forKey: "i") self.botInfos = decoder.decodeObjectArrayWithDecoderForKey("b") as [CachedPeerBotInfo] if let legacyValue = decoder.decodeOptionalInt32ForKey("pcs") { - self.peerStatusSettings = PeerStatusSettings(flags: PeerStatusSettings.Flags(rawValue: legacyValue), geoDistance: nil, managingBot: nil) + self.peerStatusSettings = PeerStatusSettings(flags: PeerStatusSettings.Flags(rawValue: legacyValue), managingBot: nil) } else if let peerStatusSettings = decoder.decodeObjectForKey("pss", decoder: { PeerStatusSettings(decoder: $0) }) as? PeerStatusSettings { self.peerStatusSettings = peerStatusSettings } else { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index fa9819893f..8a6008028f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -1418,7 +1418,7 @@ public final class CachedUserData: CachedPeerData { self.botInfo = decoder.decodeObjectForKey("bi") as? BotInfo self.editableBotInfo = decoder.decodeObjectForKey("ebi") as? EditableBotInfo if let legacyValue = decoder.decodeOptionalInt32ForKey("pcs") { - self.peerStatusSettings = PeerStatusSettings(flags: PeerStatusSettings.Flags(rawValue: legacyValue), geoDistance: nil, managingBot: nil) + self.peerStatusSettings = PeerStatusSettings(flags: PeerStatusSettings.Flags(rawValue: legacyValue), managingBot: nil) } else if let peerStatusSettings = decoder.decodeObjectForKey("pss", decoder: { PeerStatusSettings(decoder: $0) }) as? PeerStatusSettings { self.peerStatusSettings = peerStatusSettings } else { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 753e3024ab..e4cbebfe78 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -433,12 +433,6 @@ public struct PreferencesKeys { return key }() - public static let peersNearby: ValueBoxKey = { - let key = ValueBoxKey(length: 4) - key.setInt32(0, value: PreferencesKeyValues.peersNearby.rawValue) - return key - }() - public static let chatListFiltersFeaturedState: ValueBoxKey = { let key = ValueBoxKey(length: 4) key.setInt32(0, value: PreferencesKeyValues.chatListFiltersFeaturedState.rawValue) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerStatusSettings.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerStatusSettings.swift index 1c2f485dff..2e6e74475c 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerStatusSettings.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerStatusSettings.swift @@ -33,7 +33,6 @@ public struct PeerStatusSettings: PostboxCoding, Equatable { } public var flags: PeerStatusSettings.Flags - public var geoDistance: Int32? public var requestChatTitle: String? public var requestChatDate: Int32? public var requestChatIsChannel: Bool? @@ -46,7 +45,6 @@ public struct PeerStatusSettings: PostboxCoding, Equatable { public init() { self.flags = PeerStatusSettings.Flags() - self.geoDistance = nil self.requestChatTitle = nil self.requestChatDate = nil self.managingBot = nil @@ -59,7 +57,6 @@ public struct PeerStatusSettings: PostboxCoding, Equatable { public init( flags: PeerStatusSettings.Flags, - geoDistance: Int32? = nil, requestChatTitle: String? = nil, requestChatDate: Int32? = nil, requestChatIsChannel: Bool? = nil, @@ -71,7 +68,6 @@ public struct PeerStatusSettings: PostboxCoding, Equatable { photoChangeDate: Int32? = nil ) { self.flags = flags - self.geoDistance = geoDistance self.requestChatTitle = requestChatTitle self.requestChatDate = requestChatDate self.requestChatIsChannel = requestChatIsChannel @@ -85,7 +81,6 @@ public struct PeerStatusSettings: PostboxCoding, Equatable { public init(decoder: PostboxDecoder) { self.flags = Flags(rawValue: decoder.decodeInt32ForKey("flags", orElse: 0)) - self.geoDistance = decoder.decodeOptionalInt32ForKey("geoDistance") self.requestChatTitle = decoder.decodeOptionalStringForKey("requestChatTitle") self.requestChatDate = decoder.decodeOptionalInt32ForKey("requestChatDate") self.requestChatIsChannel = decoder.decodeOptionalBoolForKey("requestChatIsChannel") @@ -99,11 +94,6 @@ public struct PeerStatusSettings: PostboxCoding, Equatable { public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.flags.rawValue, forKey: "flags") - if let geoDistance = self.geoDistance { - encoder.encodeInt32(geoDistance, forKey: "geoDistance") - } else { - encoder.encodeNil(forKey: "geoDistance") - } if let requestChatTitle = self.requestChatTitle { encoder.encodeString(requestChatTitle, forKey: "requestChatTitle") } else { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift index 054b229e73..da15cbfcad 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift @@ -98,7 +98,7 @@ public final class CachedSecretChatData: CachedPeerData { public init(decoder: PostboxDecoder) { if let legacyValue = decoder.decodeOptionalInt32ForKey("pcs") { - self.peerStatusSettings = PeerStatusSettings(flags: PeerStatusSettings.Flags(rawValue: legacyValue), geoDistance: nil, managingBot: nil) + self.peerStatusSettings = PeerStatusSettings(flags: PeerStatusSettings.Flags(rawValue: legacyValue), managingBot: nil) } else if let peerStatusSettings = decoder.decodeObjectForKey("pss", decoder: { PeerStatusSettings(decoder: $0) }) as? PeerStatusSettings { self.peerStatusSettings = peerStatusSettings } else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift index dd54a5fdfa..1e3761fae4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift @@ -118,20 +118,17 @@ public enum EnginePeer: Equatable { } public var flags: Flags - public var geoDistance: Int32? public var requestChatTitle: String? public var requestChatDate: Int32? public var requestChatIsChannel: Bool? public init( flags: Flags, - geoDistance: Int32?, requestChatTitle: String?, requestChatDate: Int32?, requestChatIsChannel: Bool? ) { self.flags = flags - self.geoDistance = geoDistance self.requestChatTitle = requestChatTitle self.requestChatDate = requestChatDate self.requestChatIsChannel = requestChatIsChannel @@ -363,7 +360,6 @@ public extension EnginePeer.StatusSettings { init(_ statusSettings: PeerStatusSettings) { self.init( flags: Flags(rawValue: statusSettings.flags.rawValue), - geoDistance: statusSettings.geoDistance, requestChatTitle: statusSettings.requestChatTitle, requestChatDate: statusSettings.requestChatDate, requestChatIsChannel: statusSettings.requestChatIsChannel diff --git a/submodules/TelegramCore/Sources/TelegramEngine/PeersNearby/PeersNearby.swift b/submodules/TelegramCore/Sources/TelegramEngine/PeersNearby/PeersNearby.swift deleted file mode 100644 index 39b45bccb0..0000000000 --- a/submodules/TelegramCore/Sources/TelegramEngine/PeersNearby/PeersNearby.swift +++ /dev/null @@ -1,309 +0,0 @@ -import Foundation -import SwiftSignalKit -import Postbox -import TelegramApi - - -private typealias SignalKitTimer = SwiftSignalKit.Timer - -public enum PeerNearby { - case selfPeer(expires: Int32) - case peer(id: PeerId, expires: Int32, distance: Int32) - - var expires: Int32 { - switch self { - case let .selfPeer(expires), let .peer(_, expires, _): - return expires - } - } -} - -public enum PeerNearbyVisibilityUpdate { - case visible(latitude: Double, longitude: Double) - case location(latitude: Double, longitude: Double) - case invisible -} - -func _internal_updatePeersNearbyVisibility(account: Account, update: PeerNearbyVisibilityUpdate, background: Bool) -> Signal { - var flags: Int32 = 0 - var geoPoint: Api.InputGeoPoint - var selfExpires: Int32? - - switch update { - case let .visible(latitude, longitude): - flags |= (1 << 0) - geoPoint = .inputGeoPoint(.init(flags: 0, lat: latitude, long: longitude, accuracyRadius: nil)) - selfExpires = 10800 - case let .location(latitude, longitude): - geoPoint = .inputGeoPoint(.init(flags: 0, lat: latitude, long: longitude, accuracyRadius: nil)) - case .invisible: - flags |= (1 << 0) - geoPoint = .inputGeoPointEmpty - selfExpires = 0 - } - - let _ = (account.postbox.transaction { transaction in - transaction.updatePreferencesEntry(key: PreferencesKeys.peersNearby, { entry in - var settings = entry?.get(PeersNearbyState.self) ?? PeersNearbyState.default - if case .invisible = update { - settings.visibilityExpires = nil - } else if let expires = selfExpires { - settings.visibilityExpires = expires - } - return PreferencesEntry(settings) - }) - }).start() - - if background { - flags |= (1 << 1) - } - - return account.network.request(Api.functions.contacts.getLocated(flags: flags, geoPoint: geoPoint, selfExpires: selfExpires)) - |> map(Optional.init) - |> `catch` { error -> Signal in - if error.errorCode == 406 { - if error.errorDescription == "USERPIC_PRIVACY_REQUIRED" { - let _ = (account.postbox.transaction { transaction in - transaction.updatePreferencesEntry(key: PreferencesKeys.peersNearby, { entry in - var settings = entry?.get(PeersNearbyState.self) ?? PeersNearbyState.default - settings.visibilityExpires = nil - return PreferencesEntry(settings) - }) - }).start() - } - return .single(nil) - } else { - return .single(nil) - } - } - |> mapToSignal { updates -> Signal in - if let updates = updates { - account.stateManager.addUpdates(updates) - } - return .complete() - } -} - -public final class PeersNearbyContext { - private let queue: Queue = Queue.mainQueue() - private var subscribers = Bag<([PeerNearby]?) -> Void>() - private let disposable = MetaDisposable() - private var timer: SignalKitTimer? - - private var entries: [PeerNearby]? - - public init(network: Network, stateManager: AccountStateManager, coordinate: (latitude: Double, longitude: Double)) { - let expiryExtension: Double = 10.0 - - let poll = network.request(Api.functions.contacts.getLocated(flags: 0, geoPoint: .inputGeoPoint(.init(flags: 0, lat: coordinate.latitude, long: coordinate.longitude, accuracyRadius: nil)), selfExpires: nil)) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> castError(Void.self) - |> mapToSignal { updates -> Signal<[PeerNearby], Void> in - var peersNearby: [PeerNearby] = [] - if let updates = updates { - switch updates { - case let .updates(updatesData): - let updates = updatesData.updates - for update in updates { - if case let .updatePeerLocated(updatePeerLocatedData) = update { - let peers = updatePeerLocatedData.peers - for peer in peers { - switch peer { - case let .peerLocated(peerLocatedData): - let (peer, expires, distance) = (peerLocatedData.peer, peerLocatedData.expires, peerLocatedData.distance) - peersNearby.append(.peer(id: peer.peerId, expires: expires, distance: distance)) - case let .peerSelfLocated(peerSelfLocatedData): - let expires = peerSelfLocatedData.expires - peersNearby.append(.selfPeer(expires: expires)) - } - } - } - } - default: - break - } - stateManager.addUpdates(updates) - } - return .single(peersNearby) - |> then( - stateManager.updatedPeersNearby() - |> castError(Void.self) - ) - } - - let error: Signal = .single(Void()) |> then(Signal.fail(Void()) |> suspendAwareDelay(25.0, queue: self.queue)) - let combined = combineLatest(poll, error) - |> map { data, _ -> [PeerNearby] in - return data - } - |> restartIfError - |> `catch` { _ -> Signal<[PeerNearby], NoError> in - } - - self.disposable.set((combined - |> deliverOn(self.queue)).start(next: { [weak self] updatedEntries in - guard let strongSelf = self else { - return - } - - let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - var entries = strongSelf.entries?.filter { Double($0.expires) + expiryExtension > timestamp } ?? [] - let updatedEntries = updatedEntries.filter { Double($0.expires) + expiryExtension > timestamp } - - var existingPeerIds: [PeerId: Int] = [:] - var existingSelfPeer: Int? - for i in 0 ..< entries.count { - if case let .peer(id, _, _) = entries[i] { - existingPeerIds[id] = i - } else if case .selfPeer = entries[i] { - existingSelfPeer = i - } - } - - var selfPeer: PeerNearby? - for entry in updatedEntries { - switch entry { - case .selfPeer: - if let index = existingSelfPeer { - entries[index] = entry - } else { - selfPeer = entry - } - case let .peer(id, _, _): - if let index = existingPeerIds[id] { - entries[index] = entry - } else { - entries.append(entry) - } - } - } - - if let peer = selfPeer { - entries.insert(peer, at: 0) - } - - strongSelf.entries = entries - for subscriber in strongSelf.subscribers.copyItems() { - subscriber(strongSelf.entries) - } - })) - - self.timer = SignalKitTimer(timeout: 2.0, repeat: true, completion: { [weak self] in - guard let strongSelf = self else { - return - } - - let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - strongSelf.entries = strongSelf.entries?.filter { Double($0.expires) + expiryExtension > timestamp } - for subscriber in strongSelf.subscribers.copyItems() { - subscriber(strongSelf.entries) - } - }, queue: self.queue) - self.timer?.start() - } - - deinit { - self.disposable.dispose() - self.timer?.invalidate() - } - - public func get() -> Signal<[PeerNearby]?, NoError> { - let queue = self.queue - return Signal { [weak self] subscriber in - if let strongSelf = self { - subscriber.putNext(strongSelf.entries) - - let index = strongSelf.subscribers.add({ entries in - subscriber.putNext(entries) - }) - - return ActionDisposable { - queue.async { - if let strongSelf = self { - strongSelf.subscribers.remove(index) - } - } - } - } else { - return EmptyDisposable - } - } |> runOn(queue) - } -} - -public func updateChannelGeoLocation(postbox: Postbox, network: Network, channelId: PeerId, coordinate: (latitude: Double, longitude: Double)?, address: String?) -> Signal { - return postbox.transaction { transaction -> Peer? in - return transaction.getPeer(channelId) - } - |> mapToSignal { channel -> Signal in - guard let channel = channel, let apiChannel = apiInputChannel(channel) else { - return .single(false) - } - - let geoPoint: Api.InputGeoPoint - if let (latitude, longitude) = coordinate, let _ = address { - geoPoint = .inputGeoPoint(.init(flags: 0, lat: latitude, long: longitude, accuracyRadius: nil)) - } else { - geoPoint = .inputGeoPointEmpty - } - - return network.request(Api.functions.channels.editLocation(channel: apiChannel, geoPoint: geoPoint, address: address ?? "")) - |> map { result -> Bool in - switch result { - case .boolTrue: - return true - case .boolFalse: - return false - } - } - |> `catch` { error -> Signal in - return .single(false) - } - |> mapToSignal { result in - if result { - return postbox.transaction { transaction in - transaction.updatePeerCachedData(peerIds: Set([channelId]), update: { (_, current) -> CachedPeerData? in - let current: CachedChannelData = current as? CachedChannelData ?? CachedChannelData() - let peerGeoLocation: PeerGeoLocation? - if let (latitude, longitude) = coordinate, let address = address { - peerGeoLocation = PeerGeoLocation(latitude: latitude, longitude: longitude, address: address) - } else { - peerGeoLocation = nil - } - return current.withUpdatedPeerGeoLocation(peerGeoLocation) - }) - } - |> map { _ in - return result - } - } else { - return .single(result) - } - } - } -} - -public struct PeersNearbyState: Codable, Equatable { - public var visibilityExpires: Int32? - - public static var `default` = PeersNearbyState(visibilityExpires: nil) - - public init(visibilityExpires: Int32?) { - self.visibilityExpires = visibilityExpires - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: StringCodingKey.self) - - self.visibilityExpires = try container.decodeIfPresent(Int32.self, forKey: "expires") - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: StringCodingKey.self) - - try container.encodeIfPresent(self.visibilityExpires, forKey: "expires") - } -} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/PeersNearby/TelegramEnginePeersNearby.swift b/submodules/TelegramCore/Sources/TelegramEngine/PeersNearby/TelegramEnginePeersNearby.swift deleted file mode 100644 index 3117401c2c..0000000000 --- a/submodules/TelegramCore/Sources/TelegramEngine/PeersNearby/TelegramEnginePeersNearby.swift +++ /dev/null @@ -1,15 +0,0 @@ -import SwiftSignalKit - -public extension TelegramEngine { - final class PeersNearby { - private let account: Account - - init(account: Account) { - self.account = account - } - - public func updatePeersNearbyVisibility(update: PeerNearbyVisibilityUpdate, background: Bool) -> Signal { - return _internal_updatePeersNearbyVisibility(account: self.account, update: update, background: background) - } - } -} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/TelegramEngine.swift b/submodules/TelegramCore/Sources/TelegramEngine/TelegramEngine.swift index 5b186894b5..077c83f9c4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/TelegramEngine.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/TelegramEngine.swift @@ -13,10 +13,6 @@ public final class TelegramEngine { return SecureId(account: self.account) }() - public lazy var peersNearby: PeersNearby = { - return PeersNearby(account: self.account) - }() - public lazy var payments: Payments = { return Payments(account: self.account) }() diff --git a/submodules/TelegramPermissions/Sources/Permission.swift b/submodules/TelegramPermissions/Sources/Permission.swift index c48f46d83b..dbb933b105 100644 --- a/submodules/TelegramPermissions/Sources/Permission.swift +++ b/submodules/TelegramPermissions/Sources/Permission.swift @@ -9,7 +9,6 @@ public enum PermissionKind: Int32 { case notifications case siri case cellularData - case nearbyLocation } public enum PermissionRequestStatus { @@ -39,7 +38,6 @@ public enum PermissionState: Equatable { case notifications(status: PermissionRequestStatus) case siri(status: PermissionRequestStatus) case cellularData(status: PermissionRequestStatus) - case nearbyLocation(status: PermissionRequestStatus) public var kind: PermissionKind { switch self { @@ -51,8 +49,6 @@ public enum PermissionState: Equatable { return .siri case .cellularData: return .cellularData - case .nearbyLocation: - return .nearbyLocation } } @@ -66,8 +62,6 @@ public enum PermissionState: Equatable { return status case let .cellularData(status): return status - case let .nearbyLocation(status): - return status } } } diff --git a/submodules/TelegramPermissionsUI/BUILD b/submodules/TelegramPermissionsUI/BUILD index 54bc103cd8..08170f9c3b 100644 --- a/submodules/TelegramPermissionsUI/BUILD +++ b/submodules/TelegramPermissionsUI/BUILD @@ -21,7 +21,6 @@ swift_library( "//submodules/Markdown:Markdown", "//submodules/TelegramPermissions:TelegramPermissions", "//submodules/DeviceAccess:DeviceAccess", - "//submodules/PeersNearbyIconNode:PeersNearbyIconNode", "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/AppBundle:AppBundle", "//submodules/PresentationDataUtils:PresentationDataUtils", diff --git a/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift b/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift index 9843334806..ad652d8081 100644 --- a/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift +++ b/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift @@ -6,7 +6,6 @@ import TelegramCore import TelegramPresentationData import TextFormat import TelegramPermissions -import PeersNearbyIconNode import SolidRoundedButtonNode import PresentationDataUtils import Markdown @@ -38,7 +37,6 @@ public final class PermissionContentNode: ASDisplayNode { private let filterHitTest: Bool private let iconNode: ASImageNode - private let nearbyIconNode: PeersNearbyIconNode? private let animationNode: AnimatedStickerNode? private let titleNode: ImmediateTextNode private let subtitleNode: ImmediateTextNode @@ -78,13 +76,7 @@ public final class PermissionContentNode: ASDisplayNode { self.animationNode?.setup(source: AnimatedStickerNodeLocalFileSource(name: animation), width: 320, height: 320, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) self.animationNode?.visibility = true - - self.nearbyIconNode = nil - } else if kind == PermissionKind.nearbyLocation.rawValue { - self.nearbyIconNode = PeersNearbyIconNode(theme: theme) - self.animationNode = nil } else { - self.nearbyIconNode = nil self.animationNode = nil } @@ -149,7 +141,6 @@ public final class PermissionContentNode: ASDisplayNode { } self.addSubnode(self.iconNode) - self.nearbyIconNode.flatMap { self.addSubnode($0) } self.animationNode.flatMap { self.addSubnode($0) } self.addSubnode(self.titleNode) self.addSubnode(self.subtitleNode) @@ -255,11 +246,6 @@ public final class PermissionContentNode: ASDisplayNode { imageSize = icon.size contentHeight += imageSize.height + imageSpacing } - if let _ = self.nearbyIconNode, size.width < size.height { - imageSpacing = floor(availableHeight * 0.12) - imageSize = CGSize(width: 120.0, height: 120.0) - contentHeight += imageSize.height + imageSpacing - } if let _ = self.animationNode, size.width < size.height { imageSpacing = floor(availableHeight * 0.12) imageSize = CGSize(width: 240.0, height: 240.0) @@ -275,7 +261,6 @@ public final class PermissionContentNode: ASDisplayNode { let contentOrigin = insets.top + floor((size.height - insets.top - insets.bottom - contentHeight) / 2.0) - verticalOffset let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: contentOrigin), size: imageSize) - let nearbyIconFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: contentOrigin), size: imageSize) let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: contentOrigin), size: imageSize) let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: iconFrame.maxY + imageSpacing), size: titleSize) @@ -300,9 +285,6 @@ public final class PermissionContentNode: ASDisplayNode { } transition.updateFrame(node: self.iconNode, frame: iconFrame) - if let nearbyIconNode = self.nearbyIconNode { - transition.updateFrame(node: nearbyIconNode, frame: nearbyIconFrame) - } if let animationNode = self.animationNode { transition.updateFrame(node: animationNode, frame: animationFrame) animationNode.updateLayout(size: animationFrame.size) diff --git a/submodules/TelegramPermissionsUI/Sources/PermissionController.swift b/submodules/TelegramPermissionsUI/Sources/PermissionController.swift index d0c2f05323..1bb200eb5e 100644 --- a/submodules/TelegramPermissionsUI/Sources/PermissionController.swift +++ b/submodules/TelegramPermissionsUI/Sources/PermissionController.swift @@ -114,10 +114,7 @@ public final class PermissionController: ViewController { self.state = state if case let .permission(permission) = state, let state = permission { - if case .nearbyLocation = state { - } else { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Permissions_Skip, style: .plain, target: self, action: #selector(PermissionController.nextPressed)) - } + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Permissions_Skip, style: .plain, target: self, action: #selector(PermissionController.nextPressed)) switch state { case let .contacts(status): @@ -186,28 +183,6 @@ public final class PermissionController: ViewController { strongSelf.proceed?(true) } } - case let .nearbyLocation(status): - self.title = self.presentationData.strings.Permissions_PeopleNearbyTitle_v0 - - if self.locationManager == nil { - self.locationManager = LocationManager() - } - - self.allow = { [weak self] in - if let strongSelf = self { - switch status { - case .requestable: - DeviceAccess.authorizeAccess(to: .location(.tracking), locationManager: strongSelf.locationManager, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, { [weak self] result in - self?.proceed?(result) - }) - case .denied, .unreachable: - strongSelf.openAppSettings() - strongSelf.proceed?(false) - default: - break - } - } - } } } else if case let .custom(icon, _, _, _, _, _, _) = state { if case .animation = icon, case .modal = self.navigationPresentation { diff --git a/submodules/TelegramPermissionsUI/Sources/PermissionControllerNode.swift b/submodules/TelegramPermissionsUI/Sources/PermissionControllerNode.swift index 538b5a3925..f20b4fe20d 100644 --- a/submodules/TelegramPermissionsUI/Sources/PermissionControllerNode.swift +++ b/submodules/TelegramPermissionsUI/Sources/PermissionControllerNode.swift @@ -195,16 +195,6 @@ final class PermissionControllerNode: ASDisplayNode { text = self.presentationData.strings.Permissions_CellularDataText_v0 buttonTitle = self.presentationData.strings.Permissions_CellularDataAllowInSettings_v0 hasPrivacyPolicy = false - case let .nearbyLocation(status): - icon = nil - title = self.presentationData.strings.Permissions_PeopleNearbyTitle_v0 - text = self.presentationData.strings.Permissions_PeopleNearbyText_v0 - if status == .denied { - buttonTitle = self.presentationData.strings.Permissions_PeopleNearbyAllowInSettings_v0 - } else { - buttonTitle = self.presentationData.strings.Permissions_PeopleNearbyAllow_v0 - } - hasPrivacyPolicy = false } let contentNode = PermissionContentNode(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, kind: dataState.kind.rawValue, icon: .image(icon), title: title, text: text, buttonTitle: buttonTitle, secondaryButtonTitle: nil, buttonAction: { [weak self] in diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index ff32a5575d..bdaac16397 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -64,8 +64,6 @@ public enum PresentationResourceKey: Int32 { case itemListCloudFetchIcon case itemListCloseIconImage case itemListRemoveIconImage - case itemListMakeVisibleIcon - case itemListMakeInvisibleIcon case itemListEditThemeIcon case itemListCornersTop case itemListCornersBottom diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift index da99384779..fac4f1a8a2 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift @@ -269,19 +269,7 @@ public struct PresentationResourcesItemList { }) }) } - - public static func makeVisibleIcon(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.itemListMakeVisibleIcon.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Contact List/MakeVisibleIcon"), color: theme.list.itemAccentColor) - }) - } - - public static func makeInvisibleIcon(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.itemListMakeInvisibleIcon.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Contact List/MakeInvisibleIcon"), color: theme.list.itemDestructiveColor) - }) - } - + public static func editThemeIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.itemListEditThemeIcon.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Settings/EditTheme"), color: theme.list.itemAccentColor) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index a33742767d..78a75b33a2 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -137,7 +137,6 @@ swift_library( "//submodules/OpenInExternalAppUI:OpenInExternalAppUI", "//submodules/LegacyUI:LegacyUI", "//submodules/ImageCompression:ImageCompression", - "//submodules/DateSelectionUI:DateSelectionUI", "//submodules/PasswordSetupUI:PasswordSetupUI", "//submodules/Pdf:Pdf", "//submodules/InstantPageUI:InstantPageUI", @@ -154,7 +153,6 @@ swift_library( "//submodules/ItemListPeerItem:ItemListPeerItem", "//submodules/ContactsPeerItem:ContactsPeerItem", "//submodules/TelegramPermissionsUI:TelegramPermissionsUI", - "//submodules/PeersNearbyIconNode:PeersNearbyIconNode", "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/PasscodeUI:PasscodeUI", "//submodules/CallListUI:CallListUI", @@ -175,7 +173,6 @@ swift_library( "//submodules/LegacyMediaPickerUI:LegacyMediaPickerUI", "//submodules/MimeTypes:MimeTypes", "//submodules/LocalMediaResources:LocalMediaResources", - "//submodules/PeersNearbyUI:PeersNearbyUI", "//submodules/Geocoding:Geocoding", "//submodules/PeerInfoUI:PeerInfoUI", "//submodules/PeerAvatarGalleryUI:PeerAvatarGalleryUI", @@ -244,6 +241,7 @@ swift_library( "//submodules/GradientBackground:GradientBackground", "//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode", "//submodules/ComponentFlow:ComponentFlow", + "//submodules/TelegramUI/Components/ChatTimerScreen", "//submodules/AdUI:AdUI", "//submodules/SparseItemGrid:SparseItemGrid", "//submodules/CalendarMessageScreen:CalendarMessageScreen", diff --git a/submodules/TelegramUI/Components/Ads/AdsInfoScreen/BUILD b/submodules/TelegramUI/Components/Ads/AdsInfoScreen/BUILD index 1dbdf28129..db32ff429f 100644 --- a/submodules/TelegramUI/Components/Ads/AdsInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/Ads/AdsInfoScreen/BUILD @@ -10,19 +10,15 @@ swift_library( "-warnings-as-errors", ], deps = [ - "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", "//submodules/Components/ViewControllerComponent", - "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/ResizableSheetComponent", "//submodules/Components/MultilineTextComponent", "//submodules/Components/BalancedTextComponent", - "//submodules/Components/SolidRoundedButtonComponent", "//submodules/Components/BundleIconComponent", - "//submodules/Components/BlurredBackgroundComponent", - "//submodules/TelegramUI/Components/ScrollComponent", "//submodules/TelegramPresentationData", "//submodules/AccountContext", "//submodules/AppBundle", @@ -30,6 +26,8 @@ swift_library( "//submodules/PresentationDataUtils", "//submodules/ContextUI", "//submodules/UndoUI", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/TelegramUI/Components/Ads/AdsReportScreen", ], visibility = [ diff --git a/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift b/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift index 8b0c09230d..065e9057e2 100644 --- a/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift +++ b/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import Display -import AsyncDisplayKit import ComponentFlow import SwiftSignalKit import TelegramCore @@ -9,142 +8,141 @@ import Markdown import TextFormat import TelegramPresentationData import ViewControllerComponent -import ScrollComponent +import ResizableSheetComponent import BundleIconComponent import BalancedTextComponent import MultilineTextComponent -import SolidRoundedButtonComponent import AccountContext -import ScrollComponent -import BlurredBackgroundComponent import PresentationDataUtils import ContextUI import UndoUI +import GlassBarButtonComponent +import ButtonComponent import AdsReportScreen private let moreTag = GenericComponentViewTag() -private final class ScrollContent: CombinedComponent { - typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment) - +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + let context: AccountContext let mode: AdsInfoScreen.Mode + let bottomInset: CGFloat let openPremium: () -> Void - let dismiss: () -> Void - + init( context: AccountContext, mode: AdsInfoScreen.Mode, - openPremium: @escaping () -> Void, - dismiss: @escaping () -> Void + bottomInset: CGFloat, + openPremium: @escaping () -> Void ) { self.context = context self.mode = mode + self.bottomInset = bottomInset self.openPremium = openPremium - self.dismiss = dismiss } - - static func ==(lhs: ScrollContent, rhs: ScrollContent) -> Bool { + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { if lhs.context !== rhs.context { return false } + if lhs.mode != rhs.mode { + return false + } + if lhs.bottomInset != rhs.bottomInset { + return false + } return true } - + final class State: ComponentState { var cachedIconImage: (UIImage, PresentationTheme)? var cachedChevronImage: (UIImage, PresentationTheme)? - - let playOnce = ActionSlot() - private var didPlayAnimation = false - - func playAnimationIfNeeded() { - guard !self.didPlayAnimation else { - return - } - self.didPlayAnimation = true - self.playOnce.invoke(Void()) - } } - + func makeState() -> State { return State() } - + static var body: Body { let iconBackground = Child(Image.self) let icon = Child(BundleIconComponent.self) - + let title = Child(BalancedTextComponent.self) let text = Child(BalancedTextComponent.self) let list = Child(List.self) - + let infoBackground = Child(RoundedRectangle.self) let infoTitle = Child(MultilineTextComponent.self) let infoText = Child(MultilineTextComponent.self) - + let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: []) - + return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let component = context.component let state = context.state - - let theme = environment.theme + + let theme = environment.theme.withModalBlocksBackground() let strings = environment.strings - let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 } - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let sideInset: CGFloat = 16.0 + environment.safeInsets.left let textSideInset: CGFloat = 30.0 + environment.safeInsets.left - + let titleFont = Font.semibold(20.0) let textFont = Font.regular(15.0) - + let textColor = theme.actionSheet.primaryTextColor let secondaryTextColor = theme.actionSheet.secondaryTextColor let linkColor = theme.actionSheet.controlAccentColor - - let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in - return (TelegramTextAttributes.URL, contents) - }) - + + let markdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: textFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: linkColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + let spacing: CGFloat = 16.0 var contentSize = CGSize(width: context.availableSize.width, height: 30.0) - + let iconSize = CGSize(width: 90.0, height: 90.0) let gradientImage: UIImage - + if let (current, currentTheme) = state.cachedIconImage, currentTheme === theme { gradientImage = current } else { - gradientImage = generateGradientFilledCircleImage(diameter: iconSize.width, colors: [ - UIColor(rgb: 0x6e91ff).cgColor, - UIColor(rgb: 0x9472ff).cgColor, - UIColor(rgb: 0xcc6cdd).cgColor - ], direction: .diagonal)! - context.state.cachedIconImage = (gradientImage, theme) + gradientImage = generateGradientFilledCircleImage( + diameter: iconSize.width, + colors: [ + UIColor(rgb: 0x6e91ff).cgColor, + UIColor(rgb: 0x9472ff).cgColor, + UIColor(rgb: 0xcc6cdd).cgColor + ], + direction: .diagonal + )! + state.cachedIconImage = (gradientImage, theme) } - + 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)) - ) - + 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: "Ads/AdsLogo", tintColor: theme.list.itemCheckColors.foregroundColor), - availableSize: CGSize(width: 90, height: 90), + availableSize: CGSize(width: 90.0, height: 90.0), transition: .immediate ) - context.add(icon - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + iconBackground.size.height / 2.0)) - ) + 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 += spacing + 1.0 - + let title = title.update( component: BalancedTextComponent( text: .plain(NSAttributedString(string: strings.AdsInfo_Title, font: titleFont, textColor: textColor)), @@ -155,12 +153,10 @@ private final class ScrollContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), transition: .immediate ) - context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) - ) + context.add(title.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))) contentSize.height += title.size.height contentSize.height += spacing - 8.0 - + let text = text.update( component: BalancedTextComponent( text: .plain(NSAttributedString(string: strings.AdsInfo_Info, font: textFont, textColor: secondaryTextColor)), @@ -171,14 +167,12 @@ private final class ScrollContent: CombinedComponent { 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)) - ) + context.add(text.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0))) contentSize.height += text.size.height contentSize.height += spacing - + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) - + let respectText: String let adsText: String let infoRawText: String @@ -189,76 +183,79 @@ private final class ScrollContent: CombinedComponent { infoRawText = strings.AdsInfo_Launch_Text case .bot: respectText = strings.AdsInfo_Bot_Respect_Text - adsText = strings.AdsInfo_Bot_Ads_Text + adsText = strings.AdsInfo_Bot_Ads_Text infoRawText = strings.AdsInfo_Bot_Launch_Text case .search: respectText = strings.AdsInfo_Search_Respect_Text adsText = strings.AdsInfo_Search_Ads_Text infoRawText = strings.AdsInfo_Search_Launch_Text } - + var items: [AnyComponentWithIdentity] = [] items.append( AnyComponentWithIdentity( id: "respect", - component: AnyComponent(ParagraphComponent( - title: strings.AdsInfo_Respect_Title, - titleColor: textColor, - text: respectText, - textColor: secondaryTextColor, - accentColor: linkColor, - iconName: "Ads/Privacy", - iconColor: linkColor - )) + component: AnyComponent( + ParagraphComponent( + title: strings.AdsInfo_Respect_Title, + titleColor: textColor, + text: respectText, + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Ads/Privacy", + iconColor: linkColor + ) + ) ) ) if case .search = component.mode { - } else { items.append( AnyComponentWithIdentity( id: "split", - component: AnyComponent(ParagraphComponent( - title: component.mode == .bot ? strings.AdsInfo_Bot_Split_Title : strings.AdsInfo_Split_Title, - titleColor: textColor, - text: component.mode == .bot ? strings.AdsInfo_Bot_Split_Text : strings.AdsInfo_Split_Text, - textColor: secondaryTextColor, - accentColor: linkColor, - iconName: "Ads/Split", - iconColor: linkColor - )) + component: AnyComponent( + ParagraphComponent( + title: component.mode == .bot ? strings.AdsInfo_Bot_Split_Title : strings.AdsInfo_Split_Title, + titleColor: textColor, + text: component.mode == .bot ? strings.AdsInfo_Bot_Split_Text : strings.AdsInfo_Split_Text, + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Ads/Split", + iconColor: linkColor + ) + ) ) ) } items.append( AnyComponentWithIdentity( id: "ads", - component: AnyComponent(ParagraphComponent( - title: strings.AdsInfo_Ads_Title, - titleColor: textColor, - text: adsText, - textColor: secondaryTextColor, - accentColor: linkColor, - iconName: "Premium/BoostPerk/NoAds", - iconColor: linkColor, - action: { - component.openPremium() - } - )) + component: AnyComponent( + ParagraphComponent( + title: strings.AdsInfo_Ads_Title, + titleColor: textColor, + text: adsText, + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Premium/BoostPerk/NoAds", + iconColor: linkColor, + action: { + component.openPremium() + } + ) + ) ) ) - + let list = list.update( component: List(items), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 10000.0), transition: context.transition ) - context.add(list - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0)) - ) + context.add(list.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0))) contentSize.height += list.size.height contentSize.height += spacing - 9.0 - + let infoTitleAttributedString = NSMutableAttributedString(string: strings.AdsInfo_Launch_Title, font: titleFont, textColor: textColor) let infoTitle = infoTitle.update( component: MultilineTextComponent( @@ -270,17 +267,17 @@ private final class ScrollContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - sideInset * 3.5, height: context.availableSize.height), transition: .immediate ) - - if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { + + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== theme { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) } - + var infoString = infoRawText if let spaceRegex { let nsRange = NSRange(infoString.startIndex..., in: infoString) let matches = spaceRegex.matches(in: infoString, options: [], range: nsRange) var modifiedString = infoString - + for match in matches.reversed() { let matchRange = Range(match.range, in: infoString)! let matchedSubstring = String(infoString[matchRange]) @@ -289,10 +286,12 @@ private final class ScrollContent: CombinedComponent { } infoString = modifiedString } + let infoAttributedString = parseMarkdownIntoAttributedString(infoString, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString if let range = infoAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { infoAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: infoAttributedString.string)) } + let infoText = infoText.update( component: MultilineTextComponent( text: .plain(infoAttributedString), @@ -309,190 +308,47 @@ private final class ScrollContent: CombinedComponent { } }, tapAction: { _, _ in - component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.AdsInfo_Launch_Text_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + component.context.sharedContext.openExternalUrl( + context: component.context, + urlContext: .generic, + url: strings.AdsInfo_Launch_Text_URL, + forceExternal: true, + presentationData: presentationData, + navigationController: nil, + dismissInput: {} + ) } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 3.5, height: context.availableSize.height), transition: .immediate ) - + let infoPadding: CGFloat = 13.0 let infoSpacing: CGFloat = 6.0 let totalInfoHeight = infoPadding + infoTitle.size.height + infoSpacing + infoText.size.height + infoPadding - + let infoBackground = infoBackground.update( component: RoundedRectangle( color: theme.overallDarkAppearance ? theme.list.itemModalBlocksBackgroundColor : theme.list.blocksBackgroundColor, - cornerRadius: 10.0 + cornerRadius: 26.0 ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: totalInfoHeight), transition: .immediate ) - context.add(infoBackground - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + infoBackground.size.height / 2.0)) - ) + context.add(infoBackground.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + infoBackground.size.height / 2.0))) contentSize.height += infoPadding - - context.add(infoTitle - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + infoTitle.size.height / 2.0)) - ) + + context.add(infoTitle.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + infoTitle.size.height / 2.0))) contentSize.height += infoTitle.size.height contentSize.height += infoSpacing - - context.add(infoText - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + infoText.size.height / 2.0)) - ) + + context.add(infoText.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + infoText.size.height / 2.0))) contentSize.height += infoText.size.height contentSize.height += infoPadding - contentSize.height += spacing - - contentSize.height += 12.0 + 50.0 - if environment.safeInsets.bottom > 0 { - contentSize.height += environment.safeInsets.bottom + 5.0 - } else { - contentSize.height += 12.0 - } - - state.playAnimationIfNeeded() - - return contentSize - } - } -} + contentSize.height += spacing - 5.0 + contentSize.height += component.bottomInset -private final class ContainerComponent: CombinedComponent { - typealias EnvironmentType = ViewControllerComponentContainer.Environment - - class ExternalState { - var contentHeight: CGFloat = 0.0 - } - - let context: AccountContext - let mode: AdsInfoScreen.Mode - let externalState: ExternalState - let openPremium: () -> Void - let openContextMenu: () -> Void - let dismiss: () -> Void - - init( - context: AccountContext, - mode: AdsInfoScreen.Mode, - externalState: ExternalState, - openPremium: @escaping () -> Void, - openContextMenu: @escaping () -> Void, - dismiss: @escaping () -> Void - ) { - self.context = context - self.mode = mode - self.externalState = externalState - self.openPremium = openPremium - self.openContextMenu = openContextMenu - self.dismiss = dismiss - } - - static func ==(lhs: ContainerComponent, rhs: ContainerComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.mode != rhs.mode { - return false - } - return true - } - - final class State: ComponentState { - var topContentOffset: CGFloat? - var bottomContentOffset: CGFloat? - - var cachedMoreImage: (UIImage, PresentationTheme)? - } - - func makeState() -> State { - return State() - } - - static var body: Body { - let background = Child(Rectangle.self) - let scroll = Child(ScrollComponent.self) - let scrollExternalState = ScrollComponent.ExternalState() - - let moreButton = Child(Button.self) - - return { context in - let environment = context.environment[EnvironmentType.self] - let state = context.state - - let theme = environment.theme - - let openContextMenu = context.component.openContextMenu - let dismiss = context.component.dismiss - - let background = background.update( - component: Rectangle(color: theme.overallDarkAppearance ? theme.list.modalBlocksBackgroundColor : theme.list.plainBackgroundColor), - environment: {}, - availableSize: context.availableSize, - transition: context.transition - ) - context.add(background - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) - ) - - let scroll = scroll.update( - component: ScrollComponent( - content: AnyComponent(ScrollContent( - context: context.component.context, - mode: context.component.mode, - openPremium: context.component.openPremium, - dismiss: { - dismiss() - } - )), - externalState: scrollExternalState, - contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 1.0, right: 0.0), - contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in - state?.topContentOffset = topContentOffset - state?.bottomContentOffset = bottomContentOffset - Queue.mainQueue().justDispatch { - state?.updated(transition: .immediate) - } - }, - contentOffsetWillCommit: { targetContentOffset in - } - ), - environment: { environment }, - availableSize: context.availableSize, - transition: context.transition - ) - context.component.externalState.contentHeight = scrollExternalState.contentHeight - - context.add(scroll - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) - ) - - if case .bot = context.component.mode { - let moreImage: UIImage - if let (image, theme) = state.cachedMoreImage, theme === environment.theme { - moreImage = image - } else { - moreImage = generateMoreButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: environment.theme.actionSheet.inputClearButtonColor)! - state.cachedMoreImage = (moreImage, environment.theme) - } - let moreButton = moreButton.update( - component: Button( - content: AnyComponent(Image(image: moreImage)), - action: { - openContextMenu() - } - ).tagged(moreTag), - availableSize: CGSize(width: 30.0, height: 30.0), - transition: .immediate - ) - context.add(moreButton - .position(CGPoint(x: context.availableSize.width - 16.0 - moreButton.size.width / 2.0, y: 13.0 + moreButton.size.height / 2.0)) - ) - } - - return context.availableSize + return contentSize } } } @@ -506,8 +362,8 @@ private final class ParagraphComponent: CombinedComponent { let iconName: String let iconColor: UIColor let action: () -> Void - - public init( + + init( title: String, titleColor: UIColor, text: String, @@ -526,7 +382,7 @@ private final class ParagraphComponent: CombinedComponent { self.iconColor = iconColor self.action = action } - + static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool { if lhs.title != rhs.title { return false @@ -551,37 +407,38 @@ private final class ParagraphComponent: CombinedComponent { } return true } - + static var body: Body { let title = Child(MultilineTextComponent.self) let text = Child(MultilineTextComponent.self) let icon = Child(BundleIconComponent.self) - + return { context in let component = context.component - + let leftInset: CGFloat = 32.0 let rightInset: CGFloat = 24.0 let textSideInset: CGFloat = leftInset + 8.0 let spacing: CGFloat = 5.0 - let textTopInset: CGFloat = 9.0 - + let title = title.update( component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: component.title, - font: Font.semibold(15.0), - textColor: component.titleColor, - paragraphAlignment: .natural - )), + text: .plain( + NSAttributedString( + string: component.title, + font: Font.semibold(15.0), + textColor: component.titleColor, + paragraphAlignment: .natural + ) + ), horizontalAlignment: .center, maximumNumberOfLines: 1 ), availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) - + let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) let textColor = component.textColor @@ -594,7 +451,7 @@ private final class ParagraphComponent: CombinedComponent { return (TelegramTextAttributes.URL, contents) } ) - + let text = text.update( component: MultilineTextComponent( text: .markdown(text: component.text, attributes: markdownAttributes), @@ -616,774 +473,238 @@ private final class ParagraphComponent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height), transition: .immediate ) - + let icon = icon.update( - component: BundleIconComponent( - name: component.iconName, - tintColor: component.iconColor - ), + component: BundleIconComponent(name: component.iconName, tintColor: component.iconColor), availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), transition: .immediate ) - - context.add(title - .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) - ) - - context.add(text - .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) - ) - - context.add(icon - .position(CGPoint(x: 15.0, y: textTopInset + 18.0)) - ) - + + context.add(title.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))) + context.add(text.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))) + context.add(icon.position(CGPoint(x: 15.0, y: textTopInset + 18.0))) + return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0) } } } +private final class AdsInfoSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment -public class AdsInfoScreen: ViewController { + let context: AccountContext + let mode: AdsInfoScreen.Mode + let message: EngineMessage? + + init( + context: AccountContext, + mode: AdsInfoScreen.Mode, + message: EngineMessage? + ) { + self.context = context + self.mode = mode + self.message = message + } + + static func ==(lhs: AdsInfoSheetComponent, rhs: AdsInfoSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.mode != rhs.mode { + return false + } + if lhs.message?.id != rhs.message?.id { + return false + } + return true + } + + static var body: Body { + let sheet = Child(ResizableSheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let dismiss: (Bool) -> Void = { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else if let controller = controller() { + controller.dismiss(completion: nil) + } + } + + let theme = environment.theme.withModalBlocksBackground() + + let bottomButtonInsets = ContainerViewLayout.concentricInsets( + bottomInset: environment.safeInsets.bottom, + innerDiameter: 52.0, + sideInset: 30.0 + ) + let contentBottomInset = bottomButtonInsets.bottom + 52.0 + 16.0 + + let defaultHeight: CGFloat? + if case .search = context.component.mode { + defaultHeight = nil + } else { + let footerBottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : 12.0 + let panelHeight: CGFloat = 12.0 + 50.0 + footerBottomInset + 28.0 + let containerTopInset = environment.statusBarHeight + 10.0 + defaultHeight = max(0.0, context.availableSize.width + 128.0 + panelHeight - containerTopInset) + } + + let rightItem: AnyComponent? + if case .bot = context.component.mode, context.component.message?.adAttribute != nil { + rightItem = AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity( + id: "more", + component: AnyComponent( + BundleIconComponent( + name: "Chat/Context Menu/More", + tintColor: theme.chat.inputPanel.panelControlColor + ) + ) + ), + action: { _ in + (controller() as? AdsInfoScreen)?.infoPressed() + }, + tag: moreTag + ) + ) + } else { + rightItem = nil + } + + let sheet = sheet.update( + component: ResizableSheetComponent( + content: AnyComponent( + SheetContent( + context: context.component.context, + mode: context.component.mode, + bottomInset: contentBottomInset, + openPremium: { + guard let controller = controller() as? AdsInfoScreen else { + return + } + let navigationController = controller.navigationController + let accountContext = controller.context + let forceDark = controller.forceDark + dismiss(true) + + Queue.mainQueue().after(0.3) { + let premiumController = accountContext.sharedContext.makePremiumIntroController( + context: accountContext, + source: .ads, + forceDark: forceDark, + dismissed: nil + ) + navigationController?.pushViewController(premiumController, animated: true) + } + } + ) + ), + titleItem: nil, + leftItem: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity( + id: "close", + component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + ) + ), + action: { _ in + dismiss(true) + } + ) + ), + rightItem: rightItem, + hasTopEdgeEffect: false, + bottomItem: AnyComponent( + ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent( + Text( + text: environment.strings.AdsInfo_Understood, + font: Font.semibold(17.0), + color: theme.list.itemCheckColors.foregroundColor + ) + ) + ), + action: { + dismiss(true) + } + ) + ), + backgroundColor: .color(theme.overallDarkAppearance ? theme.list.modalBlocksBackgroundColor : theme.list.plainBackgroundColor), + defaultHeight: defaultHeight, + animateOut: animateOut + ), + environment: { + environment + ResizableSheetComponentEnvironment( + theme: theme, + statusBarHeight: environment.statusBarHeight, + safeInsets: environment.safeInsets, + inputHeight: 0.0, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + screenSize: context.availableSize, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + dismiss(animated) + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + context.add(sheet.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))) + + return context.availableSize + } + } +} + +public final class AdsInfoScreen: ViewControllerComponentContainer { public enum Mode: Equatable { case channel case bot case search } - - final class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate { - private var presentationData: PresentationData - private weak var controller: AdsInfoScreen? - - let dim: ASDisplayNode - let wrappingView: UIView - let containerView: UIView - - let contentView: ComponentHostView - let footerContainerView: UIView - let footerView: ComponentHostView - - private var containerExternalState = ContainerComponent.ExternalState() - - private(set) var isExpanded = false - private var panGestureRecognizer: UIPanGestureRecognizer? - private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?)? - - private let hapticFeedback = HapticFeedback() - - private var currentIsVisible: Bool = false - private var currentLayout: ContainerViewLayout? - - init(context: AccountContext, controller: AdsInfoScreen) { - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - if controller.forceDark { - self.presentationData = self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) - } - self.presentationData = self.presentationData.withUpdated(theme: self.presentationData.theme.withModalBlocksBackground()) - - self.controller = controller - - self.dim = ASDisplayNode() - self.dim.alpha = 0.0 - self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) - - self.wrappingView = UIView() - self.containerView = UIView() - self.contentView = ComponentHostView() - - self.footerContainerView = UIView() - self.footerView = ComponentHostView() - - super.init() - - self.containerView.clipsToBounds = true - self.containerView.backgroundColor = self.presentationData.theme.overallDarkAppearance ? self.presentationData.theme.list.modalBlocksBackgroundColor : self.presentationData.theme.list.plainBackgroundColor - - self.addSubnode(self.dim) - - self.view.addSubview(self.wrappingView) - self.wrappingView.addSubview(self.containerView) - self.containerView.addSubview(self.contentView) - - self.containerView.addSubview(self.footerContainerView) - self.footerContainerView.addSubview(self.footerView) - } - - override func didLoad() { - super.didLoad() - - let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) - panRecognizer.delegate = self.wrappedGestureRecognizerDelegate - panRecognizer.delaysTouchesBegan = false - panRecognizer.cancelsTouchesInView = true - self.panGestureRecognizer = panRecognizer - self.wrappingView.addGestureRecognizer(panRecognizer) - - self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) - self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) - } - - @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.controller?.dismiss(animated: true) - } - } - - override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - if let layout = self.currentLayout { - if case .regular = layout.metrics.widthClass { - return false - } - } - return true - } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer { - if let scrollView = otherGestureRecognizer.view as? UIScrollView { - if scrollView.contentSize.width > scrollView.contentSize.height { - return false - } - } - return true - } - return false - } - - private var isDismissing = false - func animateIn() { - ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0) - - let targetPosition = self.containerView.center - let startPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height) - - self.containerView.center = startPosition - let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) - transition.animateView(allowUserInteraction: true, { - self.containerView.center = targetPosition - }, completion: { _ in - }) - } - - func animateOut(completion: @escaping () -> Void = {}) { - self.isDismissing = true - - let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) - positionTransition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.center.x, y: self.bounds.height + self.containerView.bounds.height / 2.0), completion: { [weak self] _ in - self?.controller?.dismiss(animated: false, completion: completion) - }) - let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) - alphaTransition.updateAlpha(node: self.dim, alpha: 0.0) - - self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) - } - - func requestLayout(transition: ComponentTransition) { - guard let layout = self.currentLayout else { - return - } - self.containerLayoutUpdated(layout: layout, forceUpdate: true, transition: transition) - } - - private var dismissOffset: CGFloat? - func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, transition: ComponentTransition) { - 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)) - - let isLandscape = layout.orientation == .landscape - - var containerTopInset: CGFloat = 0.0 - let clipFrame: CGRect - if layout.metrics.widthClass == .compact { - self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25) - if isLandscape { - self.containerView.layer.cornerRadius = 0.0 - } else { - self.containerView.layer.cornerRadius = 10.0 - } - - if #available(iOS 11.0, *) { - if layout.safeInsets.bottom.isZero { - self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - } else { - self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] - } - } - - if isLandscape { - clipFrame = CGRect(origin: CGPoint(), size: layout.size) - } else { - let coveredByModalTransition: CGFloat = 0.0 - containerTopInset = 10.0 - if let statusBarHeight = layout.statusBarHeight { - containerTopInset += statusBarHeight - } - - let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - coveredByModalTransition * 10.0), size: CGSize(width: layout.size.width, height: layout.size.height - containerTopInset)) - let maxScale: CGFloat = (layout.size.width - 16.0 * 2.0) / layout.size.width - let containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition - let maxScaledTopInset: CGFloat = containerTopInset - 10.0 - let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition - let containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0)) - - clipFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height) - } - } else { - self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4) - self.containerView.layer.cornerRadius = 10.0 - - let verticalInset: CGFloat = 44.0 - - let maxSide = max(layout.size.width, layout.size.height) - let minSide = min(layout.size.width, layout.size.height) - let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0) - clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) - } - - transition.setFrame(view: self.containerView, frame: clipFrame) - - var effectiveExpanded = self.isExpanded - if case .regular = layout.metrics.widthClass { - effectiveExpanded = true - } - - self.updated(transition: transition, forceUpdate: forceUpdate) - - let contentHeight = self.containerExternalState.contentHeight - if contentHeight > 0.0 && contentHeight < 400.0, let view = self.footerView.componentView as? FooterComponent.View { - view.backgroundView.alpha = 0.0 - view.separator.opacity = 0.0 - } - let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset - let topInset: CGFloat - if let (panInitialTopInset, panOffset, _) = self.panGestureArguments { - if effectiveExpanded { - topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset)) - } else { - topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) - } - } else if let dismissOffset = self.dismissOffset, !dismissOffset.isZero { - topInset = edgeTopInset * dismissOffset - } else { - topInset = effectiveExpanded ? 0.0 : edgeTopInset - } - transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil) - - let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset) - self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition) - - let footerHeight = self.footerHeight - let convertedFooterFrame = self.view.convert(CGRect(origin: CGPoint(x: clipFrame.minX, y: clipFrame.maxY - footerHeight), size: CGSize(width: clipFrame.width, height: footerHeight)), to: self.containerView) - transition.setFrame(view: self.footerContainerView, frame: convertedFooterFrame) - } - - func updated(transition: ComponentTransition, forceUpdate: Bool = false) { - guard let controller = self.controller, let layout = self.currentLayout else { - return - } - let environment = ViewControllerComponentContainer.Environment( - statusBarHeight: 0.0, - navigationHeight: 0.0, - safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right), - additionalInsets: layout.additionalInsets, - inputHeight: layout.inputHeight ?? 0.0, - metrics: layout.metrics, - deviceMetrics: layout.deviceMetrics, - orientation: layout.metrics.orientation, - isVisible: self.currentIsVisible, - theme: self.presentationData.theme, - strings: self.presentationData.strings, - dateTimeFormat: self.presentationData.dateTimeFormat, - controller: { [weak self] in - return self?.controller - } - ) - let contentSize = self.contentView.update( - transition: transition, - component: AnyComponent( - ContainerComponent( - context: controller.context, - mode: controller.mode, - externalState: self.containerExternalState, - openPremium: { [weak self] in - guard let self, let controller = self.controller else { - return - } - - let context = controller.context - let forceDark = controller.forceDark - let navigationController = controller.navigationController - controller.dismiss(animated: true) - - Queue.mainQueue().after(0.3) { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: forceDark, dismissed: nil) - navigationController?.pushViewController(controller, animated: true) - } - }, - openContextMenu: { [weak self] in - guard let self else { - return - } - self.infoPressed() - }, - dismiss: { [weak self] in - guard let self, let controller = self.controller else { - return - } - controller.dismiss(animated: true) - } - ) - ), - environment: { environment }, - forceUpdate: forceUpdate, - containerSize: self.containerView.bounds.size - ) - self.contentView.frame = CGRect(origin: .zero, size: contentSize) - - let footerHeight = self.footerHeight - let footerSize = self.footerView.update( - transition: .immediate, - component: AnyComponent( - FooterComponent( - context: controller.context, - theme: self.presentationData.theme, - title: self.presentationData.strings.AdsInfo_Understood, - showBackground: controller.mode != .search, - action: { [weak self] in - guard let self else { - return - } - self.buttonPressed() - } - ) - ), - environment: {}, - containerSize: CGSize(width: self.containerView.bounds.width, height: footerHeight) - ) - self.footerView.frame = CGRect(origin: .zero, size: footerSize) - } - - private var didPlayAppearAnimation = false - func updateIsVisible(isVisible: Bool) { - if self.currentIsVisible == isVisible { - return - } - self.currentIsVisible = isVisible - - guard let layout = self.currentLayout else { - return - } - self.containerLayoutUpdated(layout: layout, transition: .immediate) - - if !self.didPlayAppearAnimation { - self.didPlayAppearAnimation = true - self.animateIn() - } - } - - private var footerHeight: CGFloat { - guard let layout = self.currentLayout else { - return 58.0 - } - - var footerHeight: CGFloat = 8.0 + 50.0 - footerHeight += layout.intrinsicInsets.bottom > 0.0 ? layout.intrinsicInsets.bottom + 5.0 : 8.0 - return footerHeight - } - - private var defaultTopInset: CGFloat { - guard let layout = self.currentLayout, let controller = self.controller else { - return 210.0 - } - if case .compact = layout.metrics.widthClass { - let bottomPanelPadding: CGFloat = 12.0 - let bottomInset: CGFloat = layout.intrinsicInsets.bottom > 0.0 ? layout.intrinsicInsets.bottom + 5.0 : bottomPanelPadding - let panelHeight: CGFloat = bottomPanelPadding + 50.0 + bottomInset + 28.0 - - var defaultTopInset = layout.size.height - layout.size.width - 128.0 - panelHeight - - let containerTopInset = 10.0 + (layout.statusBarHeight ?? 0.0) - let contentHeight = self.containerExternalState.contentHeight - let footerHeight = self.footerHeight - if contentHeight > 0.0 { - if case .search = controller.mode { - return (layout.size.height - containerTopInset) - contentHeight - } else { - let delta = (layout.size.height - defaultTopInset - containerTopInset) - contentHeight - footerHeight - 16.0 - if delta > 0.0 { - defaultTopInset += delta - } - } - } - return defaultTopInset - } else { - return 210.0 - } - } - - private func findVerticalScrollView(view: UIView?) -> UIScrollView? { - if let view = view { - if let view = view as? UIScrollView, view.contentSize.height > view.contentSize.width { - return view - } - return findVerticalScrollView(view: view.superview) - } else { - return nil - } - } - - @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { - guard let layout = self.currentLayout, let controller = self.controller else { - return - } - - let isLandscape = layout.orientation == .landscape - let edgeTopInset = isLandscape ? 0.0 : defaultTopInset - - switch recognizer.state { - case .began: - let point = recognizer.location(in: self.view) - let currentHitView = self.hitTest(point, with: nil) - - var scrollView = self.findVerticalScrollView(view: currentHitView) - if scrollView?.frame.height == self.frame.width { - scrollView = nil - } - if scrollView?.isDescendant(of: self.view) == false { - scrollView = nil - } - - let topInset: CGFloat - if self.isExpanded { - topInset = 0.0 - } else { - topInset = edgeTopInset - } - - self.panGestureArguments = (topInset, 0.0, scrollView) - case .changed: - guard let (topInset, panOffset, scrollView) = self.panGestureArguments else { - return - } - let contentOffset = scrollView?.contentOffset.y ?? 0.0 - - var translation = recognizer.translation(in: self.view).y - if case .search = controller.mode { - translation = max(0.0, translation) - } + fileprivate let context: AccountContext + fileprivate let mode: Mode + fileprivate let message: EngineMessage? + fileprivate let forceDark: Bool - var currentOffset = topInset + translation - - let epsilon = 1.0 - if let scrollView = scrollView, contentOffset <= -scrollView.contentInset.top + epsilon { - scrollView.bounces = false - scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) - } else if let scrollView = scrollView { - translation = panOffset - currentOffset = topInset + translation - if self.isExpanded { - recognizer.setTranslation(CGPoint(), in: self.view) - } else if currentOffset > 0.0 { - scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) - } - } - - if scrollView == nil { - translation = max(0.0, translation) - } - - self.panGestureArguments = (topInset, translation, scrollView) - - if !self.isExpanded { - if currentOffset > 0.0, let scrollView = scrollView { - scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView) - } - } - - var bounds = self.bounds - if self.isExpanded { - bounds.origin.y = -max(0.0, translation - edgeTopInset) - } else { - bounds.origin.y = -translation - } - bounds.origin.y = min(0.0, bounds.origin.y) - self.bounds = bounds - - self.containerLayoutUpdated(layout: layout, transition: .immediate) - case .ended: - guard let (currentTopInset, panOffset, scrollView) = self.panGestureArguments else { - return - } - self.panGestureArguments = nil - - let contentOffset = scrollView?.contentOffset.y ?? 0.0 - - var translation = recognizer.translation(in: self.view).y - var velocity = recognizer.velocity(in: self.view) - if case .search = controller.mode { - translation = max(0.0, translation) - velocity.y = max(0.0, velocity.y) - } - - if self.isExpanded { - if contentOffset > 0.1 { - velocity = CGPoint() - } - } - - var bounds = self.bounds - if self.isExpanded { - bounds.origin.y = -max(0.0, translation - edgeTopInset) - } else { - bounds.origin.y = -translation - } - bounds.origin.y = min(0.0, bounds.origin.y) - - scrollView?.bounces = true - - let offset = currentTopInset + panOffset - let topInset: CGFloat = edgeTopInset - - var dismissing = false - if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) { - self.controller?.dismiss(animated: true, completion: nil) - dismissing = true - } else if self.isExpanded { - if velocity.y > 300.0 || offset > topInset / 2.0 { - self.isExpanded = false - if let scrollView = scrollView { - scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) - } - - let distance = topInset - offset - let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) - let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) - - self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) - } else { - self.isExpanded = true - - self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) - } - } else if scrollView != nil, (velocity.y < -300.0 || offset < topInset / 2.0) { - let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset) - let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) - self.isExpanded = true - - self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) - } else { - if let scrollView = scrollView { - scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) - } - - self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) - } - - if !dismissing { - var bounds = self.bounds - let previousBounds = bounds - bounds.origin.y = 0.0 - self.bounds = bounds - self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) - } - case .cancelled: - self.panGestureArguments = nil - - self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) - default: - break - } - } - - func updateDismissOffset(_ offset: CGFloat) { - guard self.isExpanded, let layout = self.currentLayout else { - return - } - - self.dismissOffset = offset - self.containerLayoutUpdated(layout: layout, transition: .immediate) - } - - func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) { - guard isExpanded != self.isExpanded else { - return - } - self.dismissOffset = nil - self.isExpanded = isExpanded - - guard let layout = self.currentLayout else { - return - } - self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) - } - - func displayUndo(_ content: UndoOverlayContent) { - guard let controller = self.controller else { - return - } - let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 } - controller.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in - return true - }), in: .current) - } - - func infoPressed() { - guard let referenceView = self.contentView.findTaggedView(tag: moreTag), let controller = self.controller, let message = controller.message, let adAttribute = message.adAttribute else { - return - } - - let context = controller.context - let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 } - - var actions: [ContextMenuItem] = [] - if adAttribute.sponsorInfo != nil || adAttribute.additionalInfo != nil { - actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfo, textColor: .primary, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Channels"), color: theme.actionSheet.primaryTextColor) - }, iconSource: nil, action: { [weak self] c, _ in - var subItems: [ContextMenuItem] = [] - - subItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, textColor: .primary, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, iconSource: nil, iconPosition: .left, action: { c, _ in - c?.popItems() - }))) - - subItems.append(.separator) - - if let sponsorInfo = adAttribute.sponsorInfo { - subItems.append(.action(ContextMenuActionItem(text: sponsorInfo, textColor: .primary, textLayout: .multiline, textFont: .custom(font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 0.8)), height: nil, verticalOffset: nil), badge: nil, icon: { theme in - return nil - }, iconSource: nil, action: { [weak self] c, _ in - c?.dismiss(completion: { - UIPasteboard.general.string = sponsorInfo - - self?.displayUndo(.copy(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfoCopied)) - }) - }))) - } - if let additionalInfo = adAttribute.additionalInfo { - subItems.append(.action(ContextMenuActionItem(text: additionalInfo, textColor: .primary, textLayout: .multiline, textFont: .custom(font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 0.8)), height: nil, verticalOffset: nil), badge: nil, icon: { theme in - return nil - }, iconSource: nil, action: { [weak self] c, _ in - c?.dismiss(completion: { - UIPasteboard.general.string = additionalInfo - - self?.displayUndo(.copy(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfoCopied)) - }) - }))) - } - - c?.pushItems(items: .single(ContextController.Items(content: .list(subItems)))) - }))) - } - - let removeAd = self.controller?.removeAd - if adAttribute.canReport { - actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_ReportAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) - }, iconSource: nil, action: { [weak self] _, f in - f(.default) - - guard let navigationController = self?.controller?.navigationController as? NavigationController else { - return - } - - let _ = (context.engine.messages.reportAdMessage(opaqueId: adAttribute.opaqueId, option: nil) - |> deliverOnMainQueue).start(next: { [weak navigationController] result in - if case let .options(title, options) = result { - Queue.mainQueue().after(0.2) { - navigationController?.pushViewController( - AdsReportScreen( - context: context, - opaqueId: adAttribute.opaqueId, - title: title, - options: options, - completed: { - // removeAd?(adAttribute.opaqueId) - } - ) - ) - } - } - }) - }))) - - actions.append(.separator) - - actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_RemoveAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor) - }, iconSource: nil, action: { [weak self] c, _ in - c?.dismiss(completion: { - if context.isPremium { - removeAd?(adAttribute.opaqueId) - } else { - self?.presentNoAdsDemo() - } - }) - }))) - } else { - if !actions.isEmpty { - actions.append(.separator) - } - actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Hide, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor) - }, iconSource: nil, action: { [weak self] c, _ in - c?.dismiss(completion: { - if context.isPremium { - removeAd?(adAttribute.opaqueId) - } else { - self?.presentNoAdsDemo() - } - }) - }))) - } - - let contextController = makeContextController(presentationData: presentationData, source: .reference(AdsInfoContextReferenceContentSource(controller: controller, sourceView: referenceView, insets: .zero, contentInsets: .zero)), items: .single(ContextController.Items(content: .list(actions))), gesture: nil) - controller.presentInGlobalOverlay(contextController) - } - - func presentNoAdsDemo() { - guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { - return - } - let context = controller.context - var replaceImpl: ((ViewController) -> Void)? - let demoController = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: false, action: { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: false, dismissed: nil) - replaceImpl?(controller) - }, dismissed: nil) - replaceImpl = { [weak demoController] c in - demoController?.replace(with: c) - } - controller.dismiss(animated: true) - Queue.mainQueue().after(0.4) { - navigationController.pushViewController(demoController) - } - } - - func buttonPressed() { - self.controller?.dismiss(animated: true) - } - } - - var node: Node { - return self.displayNode as! Node - } - - private let context: AccountContext - private let mode: Mode - private let message: EngineMessage? - private let forceDark: Bool - - private var currentLayout: ContainerViewLayout? - public var removeAd: (Data) -> Void = { _ in } - + public init( context: AccountContext, mode: Mode, @@ -1394,189 +715,300 @@ public class AdsInfoScreen: ViewController { self.mode = mode self.message = message self.forceDark = forceDark - - super.init(navigationBarPresentationData: nil) - - self.navigationPresentation = .flatModal + + super.init( + context: context, + component: AdsInfoSheetComponent( + context: context, + mode: mode, + message: message + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: forceDark ? .dark : .default + ) + self.statusBar.statusBarStyle = .Ignore - + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) } - + required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - override open func loadDisplayNode() { - self.displayNode = Node(context: self.context, controller: self) - self.displayNodeDidLoad() - + + public override func viewDidLoad() { + super.viewDidLoad() + self.view.disablesInteractiveModalDismiss = true } - - public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { - self.view.endEditing(true) - if flag { - self.node.animateOut(completion: { - super.dismiss(animated: false, completion: {}) - completion?() - }) - } else { - super.dismiss(animated: false, completion: {}) - completion?() + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() } } - - override open func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - self.node.updateIsVisible(isVisible: true) - } - - override open func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - - self.node.updateIsVisible(isVisible: false) - } - - override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - self.currentLayout = layout - super.containerLayoutUpdated(layout, transition: transition) - - self.node.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) - } -} - -private final class FooterComponent: Component { - let context: AccountContext - let theme: PresentationTheme - let title: String - let showBackground: Bool - let action: () -> Void - - init(context: AccountContext, theme: PresentationTheme, title: String, showBackground: Bool, action: @escaping () -> Void) { - self.context = context - self.theme = theme - self.title = title - self.showBackground = showBackground - self.action = action - } - static func ==(lhs: FooterComponent, rhs: FooterComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.title != rhs.title { - return false - } - if lhs.showBackground != rhs.showBackground { - return false - } - return true + fileprivate func displayUndo(_ content: UndoOverlayContent) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + self.present( + UndoOverlayController( + presentationData: presentationData, + content: content, + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in + return true + } + ), + in: .current + ) } - final class View: UIView { - let backgroundView: BlurredBackgroundView - let separator = SimpleLayer() - - private let button = ComponentView() - - private var component: FooterComponent? - private weak var state: EmptyComponentState? - - override init(frame: CGRect) { - self.backgroundView = BlurredBackgroundView(color: nil) - - super.init(frame: frame) - - self.backgroundView.clipsToBounds = true - - self.addSubview(self.backgroundView) - self.layer.addSublayer(self.separator) + fileprivate func infoPressed() { + guard + let referenceView = self.node.hostView.findTaggedView(tag: moreTag), + let message = self.message, + let adAttribute = message.adAttribute + else { + return } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func update(component: FooterComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.component = component - self.state = state - - let bounds = CGRect(origin: .zero, size: availableSize) - - self.backgroundView.updateColor(color: component.theme.rootController.tabBar.backgroundColor, transition: transition.containedViewLayoutTransition) - self.backgroundView.update(size: bounds.size, transition: transition.containedViewLayoutTransition) - transition.setFrame(view: self.backgroundView, frame: bounds) - - self.separator.backgroundColor = component.theme.rootController.tabBar.separatorColor.cgColor - transition.setFrame(layer: self.separator, frame: CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: UIScreenPixel))) - - self.backgroundView.isHidden = !component.showBackground - self.separator.isHidden = !component.showBackground - - let buttonSize = self.button.update( - transition: .immediate, - component: AnyComponent( - SolidRoundedButtonComponent( - title: component.title, - theme: SolidRoundedButtonComponent.Theme(theme: component.theme), - font: .bold, - fontSize: 17.0, - height: 50.0, - cornerRadius: 10.0, - gloss: false, - animationName: nil, - iconPosition: .left, - action: { - component.action() + + let context = self.context + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var actions: [ContextMenuItem] = [] + if adAttribute.sponsorInfo != nil || adAttribute.additionalInfo != nil { + actions.append( + .action( + ContextMenuActionItem( + text: presentationData.strings.Chat_ContextMenu_AdSponsorInfo, + textColor: .primary, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Channels"), color: theme.actionSheet.primaryTextColor) + }, + iconSource: nil, + action: { [weak self] c, _ in + var subItems: [ContextMenuItem] = [] + + subItems.append( + .action( + ContextMenuActionItem( + text: presentationData.strings.Common_Back, + textColor: .primary, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, + iconSource: nil, + iconPosition: .left, + action: { c, _ in + c?.popItems() + } + ) + ) + ) + + subItems.append(.separator) + + if let sponsorInfo = adAttribute.sponsorInfo { + subItems.append( + .action( + ContextMenuActionItem( + text: sponsorInfo, + textColor: .primary, + textLayout: .multiline, + textFont: .custom(font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 0.8)), height: nil, verticalOffset: nil), + badge: nil, + icon: { _ in + return nil + }, + iconSource: nil, + action: { [weak self] c, _ in + c?.dismiss(completion: { + UIPasteboard.general.string = sponsorInfo + self?.displayUndo(.copy(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfoCopied)) + }) + } + ) + ) + ) + } + + if let additionalInfo = adAttribute.additionalInfo { + subItems.append( + .action( + ContextMenuActionItem( + text: additionalInfo, + textColor: .primary, + textLayout: .multiline, + textFont: .custom(font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 0.8)), height: nil, verticalOffset: nil), + badge: nil, + icon: { _ in + return nil + }, + iconSource: nil, + action: { [weak self] c, _ in + c?.dismiss(completion: { + UIPasteboard.general.string = additionalInfo + self?.displayUndo(.copy(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfoCopied)) + }) + } + ) + ) + ) + } + + c?.pushItems(items: .single(ContextController.Items(content: .list(subItems)))) } ) - ), - environment: {}, - containerSize: CGSize(width: availableSize.width - 32.0, height: availableSize.height) + ) ) - - if let view = self.button.view { - if view.superview == nil { - self.addSubview(view) - } - let buttonFrame = CGRect(origin: CGPoint(x: 16.0, y: 8.0), size: buttonSize) - view.frame = buttonFrame + } + + let removeAd = self.removeAd + if adAttribute.canReport { + actions.append( + .action( + ContextMenuActionItem( + text: presentationData.strings.Chat_ContextMenu_ReportAd, + textColor: .primary, + textLayout: .twoLinesMax, + textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), + badge: nil, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) + }, + iconSource: nil, + action: { [weak self] _, f in + f(.default) + + guard let navigationController = self?.navigationController as? NavigationController else { + return + } + + let _ = ( + context.engine.messages.reportAdMessage(opaqueId: adAttribute.opaqueId, option: nil) + |> deliverOnMainQueue + ).start(next: { [weak navigationController] result in + if case let .options(title, options) = result { + Queue.mainQueue().after(0.2) { + navigationController?.pushViewController( + AdsReportScreen( + context: context, + opaqueId: adAttribute.opaqueId, + title: title, + options: options, + completed: { + } + ) + ) + } + } + }) + } + ) + ) + ) + + actions.append(.separator) + + actions.append( + .action( + ContextMenuActionItem( + text: presentationData.strings.Chat_ContextMenu_RemoveAd, + textColor: .primary, + textLayout: .twoLinesMax, + textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), + badge: nil, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor) + }, + iconSource: nil, + action: { [weak self] c, _ in + c?.dismiss(completion: { + if context.isPremium { + removeAd(adAttribute.opaqueId) + } else { + self?.presentNoAdsDemo() + } + }) + } + ) + ) + ) + } else { + if !actions.isEmpty { + actions.append(.separator) } - - return availableSize + actions.append( + .action( + ContextMenuActionItem( + text: presentationData.strings.SponsoredMessageMenu_Hide, + textColor: .primary, + textLayout: .twoLinesMax, + textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), + badge: nil, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor) + }, + iconSource: nil, + action: { [weak self] c, _ in + c?.dismiss(completion: { + if context.isPremium { + removeAd(adAttribute.opaqueId) + } else { + self?.presentNoAdsDemo() + } + }) + } + ) + ) + ) + } + + let contextController = makeContextController( + presentationData: presentationData, + source: .reference( + AdsInfoContextReferenceContentSource( + controller: self, + sourceView: referenceView, + insets: .zero, + contentInsets: .zero + ) + ), + items: .single(ContextController.Items(content: .list(actions))), + gesture: nil + ) + self.presentInGlobalOverlay(contextController) + } + + fileprivate func presentNoAdsDemo() { + guard let navigationController = self.navigationController as? NavigationController else { + return + } + + let context = self.context + var replaceImpl: ((ViewController) -> Void)? + let demoController = context.sharedContext.makePremiumDemoController( + context: context, + subject: .noAds, + forceDark: false, + action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: false, dismissed: nil) + replaceImpl?(controller) + }, + dismissed: nil + ) + replaceImpl = { [weak demoController] controller in + demoController?.replace(with: controller) + } + + self.dismissAnimated() + Queue.mainQueue().after(0.4) { + navigationController.pushViewController(demoController) } } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - -private func generateMoreButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { - return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setFillColor(backgroundColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - - context.setFillColor(foregroundColor.cgColor) - - let circleSize = CGSize(width: 4.0, height: 4.0) - context.fillEllipse(in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.height - circleSize.width) / 2.0), y: floorToScreenPixels((size.height - circleSize.height) / 2.0)), size: circleSize)) - - context.fillEllipse(in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.height - circleSize.width) / 2.0) - circleSize.width - 3.0, y: floorToScreenPixels((size.height - circleSize.height) / 2.0)), size: circleSize)) - - context.fillEllipse(in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.height - circleSize.width) / 2.0) + circleSize.width + 3.0, y: floorToScreenPixels((size.height - circleSize.height) / 2.0)), size: circleSize)) - }) } private final class AdsInfoContextReferenceContentSource: ContextReferenceContentSource { @@ -1584,15 +1016,19 @@ private final class AdsInfoContextReferenceContentSource: ContextReferenceConten let sourceView: UIView let insets: UIEdgeInsets let contentInsets: UIEdgeInsets - + init(controller: ViewController, sourceView: UIView, insets: UIEdgeInsets, contentInsets: UIEdgeInsets = UIEdgeInsets()) { self.controller = controller self.sourceView = sourceView self.insets = insets self.contentInsets = contentInsets } - + func transitionInfo() -> ContextControllerReferenceViewInfo? { - return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds.inset(by: self.insets), insets: self.contentInsets) + return ContextControllerReferenceViewInfo( + referenceView: self.sourceView, + contentAreaInScreenSpace: UIScreen.main.bounds.inset(by: self.insets), + insets: self.contentInsets + ) } } diff --git a/submodules/TelegramUI/Components/Ads/AdsReportScreen/BUILD b/submodules/TelegramUI/Components/Ads/AdsReportScreen/BUILD index 99414e0246..7d5de96873 100644 --- a/submodules/TelegramUI/Components/Ads/AdsReportScreen/BUILD +++ b/submodules/TelegramUI/Components/Ads/AdsReportScreen/BUILD @@ -19,6 +19,7 @@ swift_library( "//submodules/Components/ComponentDisplayAdapters", "//submodules/Components/MultilineTextComponent", "//submodules/Components/BalancedTextComponent", + "//submodules/Components/BundleIconComponent", "//submodules/TelegramPresentationData", "//submodules/AccountContext", "//submodules/AppBundle", @@ -30,6 +31,7 @@ swift_library( "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/TelegramUI/Components/NavigationStackComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift index 86441c6117..ab0770cf96 100644 --- a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift +++ b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift @@ -17,6 +17,8 @@ import NavigationStackComponent import ItemListUI import UndoUI import AccountContext +import BundleIconComponent +import GlassBarButtonComponent private enum ReportResult { case reported @@ -37,22 +39,19 @@ private final class SheetPageContent: CombinedComponent { let subtitle: String let items: [Item] let action: (Item) -> Void - let pop: () -> Void init( context: AccountContext, title: String?, subtitle: String, items: [Item], - action: @escaping (Item) -> Void, - pop: @escaping () -> Void + action: @escaping (Item) -> Void ) { self.context = context self.title = title self.subtitle = subtitle self.items = items self.action = action - self.pop = pop } static func ==(lhs: SheetPageContent, rhs: SheetPageContent) -> Bool { @@ -72,7 +71,6 @@ private final class SheetPageContent: CombinedComponent { } final class State: ComponentState { - var backArrowImage: (UIImage, PresentationTheme)? } func makeState() -> State { @@ -80,8 +78,7 @@ private final class SheetPageContent: CombinedComponent { } static var body: Body { - let background = Child(RoundedRectangle.self) - let back = Child(Button.self) + let background = Child(Rectangle.self) let title = Child(Text.self) let subtitle = Child(MultilineTextComponent.self) let section = Child(ListSectionComponent.self) @@ -89,7 +86,6 @@ private final class SheetPageContent: CombinedComponent { return { context in let environment = context.environment[EnvironmentType.self] let component = context.component - let state = context.state let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let theme = environment.theme @@ -97,10 +93,10 @@ private final class SheetPageContent: CombinedComponent { let sideInset: CGFloat = 16.0 + environment.safeInsets.left - var contentSize = CGSize(width: context.availableSize.width, height: 18.0) + var contentSize = CGSize(width: context.availableSize.width, height: 26.0) let background = background.update( - component: RoundedRectangle(color: theme.list.modalBlocksBackgroundColor, cornerRadius: 8.0), + component: Rectangle(color: theme.list.modalBlocksBackgroundColor), availableSize: CGSize(width: context.availableSize.width, height: 1000.0), transition: .immediate ) @@ -108,73 +104,38 @@ private final class SheetPageContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) ) - let backArrowImage: UIImage - if let (cached, cachedTheme) = state.backArrowImage, cachedTheme === theme { - backArrowImage = cached - } else { - backArrowImage = NavigationBarTheme.generateBackArrowImage(color: theme.list.itemAccentColor)! - state.backArrowImage = (backArrowImage, theme) - } - - let backContents: AnyComponent - if component.title == nil { - backContents = AnyComponent(Text(text: strings.Common_Cancel, font: Font.regular(17.0), color: theme.list.itemAccentColor)) - } else { - backContents = AnyComponent( - HStack([ - AnyComponentWithIdentity(id: "arrow", component: AnyComponent(Image(image: backArrowImage, contentMode: .center))), - AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: strings.Common_Back, font: Font.regular(17.0), color: theme.list.itemAccentColor))) - ], spacing: 6.0) - ) - } - let back = back.update( - component: Button( - content: backContents, - action: { - component.pop() - } - ), - availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), - transition: .immediate - ) - context.add(back - .position(CGPoint(x: sideInset + back.size.width / 2.0 - (component.title != nil ? 8.0 : 0.0), y: contentSize.height + back.size.height / 2.0)) - ) - - let constrainedTitleWidth = context.availableSize.width - (back.size.width + 16.0) * 2.0 + let constrainedTitleWidth = context.availableSize.width - 60.0 * 2.0 let title = title.update( component: Text(text: strings.ReportAd_Title, font: Font.semibold(17.0), color: theme.list.itemPrimaryTextColor), availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), transition: .immediate ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) + ) + contentSize.height += title.size.height + if let subtitleText = component.title { let subtitle = subtitle.update( component: MultilineTextComponent(text: .plain(NSAttributedString(string: subtitleText, font: Font.regular(13.0), textColor: theme.list.itemSecondaryTextColor)), truncationType: .end, maximumNumberOfLines: 1), availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), transition: .immediate ) - context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0 - 8.0)) - ) - contentSize.height += title.size.height context.add(subtitle - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + subtitle.size.height / 2.0 - 9.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + 4.0 + subtitle.size.height / 2.0)) ) - contentSize.height += subtitle.size.height - contentSize.height += 8.0 - } else { - context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) - ) - contentSize.height += title.size.height + contentSize.height += 4.0 + subtitle.size.height contentSize.height += 24.0 + } else { + contentSize.height += 40.0 } var items: [AnyComponentWithIdentity] = [] for item in component.items { items.append(AnyComponentWithIdentity(id: item.title, component: AnyComponent(ListActionItemComponent( theme: theme, + style: .glass, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( @@ -195,6 +156,7 @@ private final class SheetPageContent: CombinedComponent { let section = section.update( component: ListSectionComponent( theme: theme, + style: .glass, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: component.subtitle.uppercased(), @@ -315,6 +277,7 @@ private final class SheetContent: CombinedComponent { static var body: Body { let navigation = Child(NavigationStackComponent.self) + let backButton = Child(GlassBarButtonComponent.self) return { context in let environment = context.environment[EnvironmentType.self] @@ -360,9 +323,6 @@ private final class SheetContent: CombinedComponent { }, action: { item in action(item) - }, - pop: { - component.dismiss() } ) ))) @@ -377,10 +337,6 @@ private final class SheetContent: CombinedComponent { }, action: { item in action(item) - }, - pop: { [weak state] in - state?.pushedOptions.removeLast() - update(.spring(duration: 0.45)) } ) ))) @@ -402,10 +358,39 @@ private final class SheetContent: CombinedComponent { ) context.add(navigation .position(CGPoint(x: context.availableSize.width / 2.0, y: navigation.size.height / 2.0)) - .clipsToBounds(true) - .cornerRadius(8.0) ) contentSize.height += navigation.size.height + + let isBack = items.count > 1 + let barButtonSize = CGSize(width: 44.0, height: 44.0) + let backButton = backButton.update( + component: GlassBarButtonComponent( + size: barButtonSize, + backgroundColor: nil, + isDark: environment.theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: isBack ? "back" : "close", component: AnyComponent( + BundleIconComponent( + name: isBack ? "Navigation/Back" : "Navigation/Close", + tintColor: environment.theme.chat.inputPanel.panelControlColor + ) + )), + action: { [weak state] _ in + if isBack { + state?.pushedOptions.removeLast() + update(.spring(duration: 0.45)) + } else { + component.dismiss() + } + } + ), + environment: {}, + availableSize: barButtonSize, + transition: context.transition + ) + context.add(backButton + .position(CGPoint(x: 16.0 + backButton.size.width / 2.0, y: 16.0 + backButton.size.height / 2.0)) + ) return contentSize } @@ -495,8 +480,10 @@ private final class SheetContainerComponent: CombinedComponent { state?.updated(transition: transition) } )), + style: .glass, backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor), followContentSizeChanges: true, + clipsContent: true, externalState: sheetExternalState, animateOut: animateOut ), diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index 19df7c4f3d..400c7c8e72 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -1552,6 +1552,7 @@ final class AvatarEditorScreenComponent: Component { videoVolume: nil, additionalVideoPath: nil, additionalVideoIsDual: false, + additionalVideoMirroringChanges: [], additionalVideoPosition: nil, additionalVideoScale: nil, additionalVideoRotation: nil, diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraVideoSource.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraVideoSource.swift index c06460e8dc..e102a7057b 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraVideoSource.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraVideoSource.swift @@ -139,6 +139,7 @@ final class LiveStreamMediaSource { videoVolume: nil, additionalVideoPath: nil, additionalVideoIsDual: true, + additionalVideoMirroringChanges: [], additionalVideoPosition: nil, additionalVideoScale: 1.625, additionalVideoRotation: 0.0, diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index 1e8c045d20..467c26d310 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -139,7 +139,6 @@ public final class ChatRecentActionsController: TelegramBaseController { }, displaySlowmodeTooltip: { _, _ in }, displaySendMessageOptions: { _, _ in }, openScheduledMessages: { - }, openPeersNearby: { }, displaySearchResultsTooltip: { _, _ in }, unarchivePeer: { }, scrollToTop: { diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift index c73df15c44..c14cdb2137 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift @@ -664,8 +664,6 @@ public final class ChatTextInputPanelComponent: Component { }, openScheduledMessages: { }, - openPeersNearby: { - }, displaySearchResultsTooltip: { _, _ in }, unarchivePeer: { diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 5f4d8181d2..1a92f1c0e9 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -2792,6 +2792,70 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { self.present = present } + private func presentStickerPackActionOverlay(_ actions: [StickerPackScreenActionResult], interaction: Interaction) { + guard let action = actions.first else { + return + } + + var animateInAsReplacement = false + if let navigationController = interaction.navigationController() { + for controller in navigationController.overlayControllers { + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitActionAndReplacementAnimation() + animateInAsReplacement = true + } + } + } + + var presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + if let forceTheme = self.forceTheme { + presentationData = presentationData.withUpdated(theme: forceTheme) + } + + let controller: UndoOverlayController + switch action.action { + case .add: + controller = UndoOverlayController( + presentationData: presentationData, + content: .stickersModified( + title: presentationData.strings.StickerPackActionInfo_AddedTitle, + text: presentationData.strings.StickerPackActionInfo_AddedText(action.info.title).string, + undo: false, + info: action.info, + topItem: action.items.first, + context: self.context + ), + elevatedLayout: false, + animateInAsReplacement: animateInAsReplacement, + action: { _ in + return true + } + ) + case let .remove(positionInList): + controller = UndoOverlayController( + presentationData: presentationData, + content: .stickersModified( + title: presentationData.strings.StickerPackActionInfo_RemovedTitle, + text: presentationData.strings.StickerPackActionInfo_RemovedText(action.info.title).string, + undo: true, + info: action.info, + topItem: action.items.first, + context: self.context + ), + elevatedLayout: false, + animateInAsReplacement: animateInAsReplacement, + action: { [weak self] overlayAction in + if case .undo = overlayAction { + let _ = self?.context.engine.stickers.addStickerPackInteractively(info: action.info, items: action.items, positionInList: positionInList).start() + } + return true + } + ) + } + + interaction.presentGlobalOverlayController(controller, nil) + } + public func setGestureRecognizerEnabled(view: UIView, isEnabled: Bool, itemAtPoint: @escaping (CGPoint) -> (AnyHashable, CALayer, TelegramMediaFile)?) { self.viewRecords = self.viewRecords.filter({ $0.view != nil }) @@ -3062,7 +3126,12 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { let controller = strongSelf.context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], actionTitle: nil, isEditing: false, expandIfNeeded: false, parentNavigationController: interaction.navigationController(), sendSticker: { file, sourceView, sourceRect in sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil) return true - }, actionPerformed: nil) + }, actionPerformed: { [weak self] actions in + guard let self else { + return + } + self.presentStickerPackActionOverlay(actions, interaction: interaction) + }) interaction.navigationController()?.view.window?.endEditing(true) interaction.presentController(controller, nil) diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift index bb5eeb9b62..2a2f156234 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift @@ -428,7 +428,9 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { return false } }, - actionPerformed: nil + actionPerformed: { [weak self] actions in + self?.presentStickerPackActionOverlay(actions) + } ) strongSelf.interaction.presentController(controller, nil) } @@ -529,6 +531,73 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { self.installDisposable.dispose() } + private func presentStickerPackActionOverlay(_ actions: [StickerPackScreenActionResult]) { + guard let action = actions.first else { + return + } + + var animateInAsReplacement = false + if let navigationController = self.interaction.getNavigationController() { + for controller in navigationController.overlayControllers { + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitActionAndReplacementAnimation() + animateInAsReplacement = true + } + } + } + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: self.theme) + let controller: UndoOverlayController + switch action.action { + case .add: + self.setPackInstalledState(id: action.info.id, installed: true) + controller = UndoOverlayController( + presentationData: presentationData, + content: .stickersModified( + title: presentationData.strings.StickerPackActionInfo_AddedTitle, + text: presentationData.strings.StickerPackActionInfo_AddedText(action.info.title).string, + undo: false, + info: action.info, + topItem: action.items.first, + context: self.context + ), + elevatedLayout: false, + animateInAsReplacement: animateInAsReplacement, + action: { _ in + return true + } + ) + case let .remove(positionInList): + self.setPackInstalledState(id: action.info.id, installed: false) + controller = UndoOverlayController( + presentationData: presentationData, + content: .stickersModified( + title: presentationData.strings.StickerPackActionInfo_RemovedTitle, + text: presentationData.strings.StickerPackActionInfo_RemovedText(action.info.title).string, + undo: true, + info: action.info, + topItem: action.items.first, + context: self.context + ), + elevatedLayout: false, + animateInAsReplacement: animateInAsReplacement, + action: { [weak self] overlayAction in + if case .undo = overlayAction { + let _ = self?.context.engine.stickers.addStickerPackInteractively(info: action.info, items: action.items, positionInList: positionInList).start() + self?.setPackInstalledState(id: action.info.id, installed: true) + } + return true + } + ) + } + + if let navigationController = self.interaction.getNavigationController() { + navigationController.presentOverlay(controller: controller) + } else { + self.interaction.presentController(controller, nil) + } + } + func updateText(_ text: String, languageCode: String?) { if self.selectedPack != nil { self.clearSelectedPack(applySearchResults: false) diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD index 565d960620..44eacc4261 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD @@ -24,7 +24,10 @@ swift_library( "//submodules/TelegramStringFormatting", "//submodules/PresentationDataUtils", "//submodules/Components/SolidRoundedButtonComponent", + "//submodules/Components/ResizableSheetComponent", + "//submodules/Components/BundleIconComponent", "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/AnimatedCounterComponent", "//submodules/AvatarNode", diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index cd8b140f4d..d2e78c8b9a 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -11,7 +11,6 @@ import AccountContext import TelegramCore import MultilineTextComponent import MultilineTextWithEntitiesComponent -import SolidRoundedButtonComponent import PresentationDataUtils import Markdown import UndoUI @@ -22,28 +21,314 @@ import QrCodeUI import InviteLinksUI import PlainButtonComponent import AnimatedCounterComponent +import BundleIconComponent +import GlassBarButtonComponent +import ResizableSheetComponent -private final class ChatFolderLinkPreviewScreenComponent: Component { +private struct ChatFolderLinkPreviewResolvedData: Equatable { + let title: String + let topBadge: String? + let descriptionText: NSAttributedString + let listHeaderTitle: String + let listHeaderActionItems: [AnimatedCounterComponent.Item] + let showsListHeaderAction: Bool + let actionButtonTitle: String? + let actionButtonBadge: Int + let actionButtonEnabled: Bool + let allChatsAdded: Bool + let canAddChatCount: Int + let isLinkList: Bool + + static func ==(lhs: ChatFolderLinkPreviewResolvedData, rhs: ChatFolderLinkPreviewResolvedData) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.topBadge != rhs.topBadge { + return false + } + if !lhs.descriptionText.isEqual(to: rhs.descriptionText) { + return false + } + if lhs.listHeaderTitle != rhs.listHeaderTitle { + return false + } + if lhs.listHeaderActionItems != rhs.listHeaderActionItems { + return false + } + if lhs.showsListHeaderAction != rhs.showsListHeaderAction { + return false + } + if lhs.actionButtonTitle != rhs.actionButtonTitle { + return false + } + if lhs.actionButtonBadge != rhs.actionButtonBadge { + return false + } + if lhs.actionButtonEnabled != rhs.actionButtonEnabled { + return false + } + if lhs.allChatsAdded != rhs.allChatsAdded { + return false + } + if lhs.canAddChatCount != rhs.canAddChatCount { + return false + } + if lhs.isLinkList != rhs.isLinkList { + return false + } + return true + } +} + +private func chatFolderLinkPreviewResolvedData( + component: ChatFolderLinkPreviewScreenComponent, + theme: PresentationTheme, + strings: PresentationStrings, + selectedItems: Set +) -> ChatFolderLinkPreviewResolvedData { + let isLinkList: Bool + if case .linkList = component.subject { + isLinkList = true + } else { + isLinkList = false + } + + var allChatsAdded = false + var canAddChatCount = 0 + + let title: String + if isLinkList { + title = strings.FolderLinkPreview_TitleShare + } else if let linkContents = component.linkContents { + if case .remove = component.subject { + title = strings.FolderLinkPreview_TitleRemove + } else if linkContents.localFilterId != nil { + if linkContents.alreadyMemberPeerIds == Set(linkContents.peers.map(\.id)) { + allChatsAdded = true + } + canAddChatCount = linkContents.peers.count - linkContents.alreadyMemberPeerIds.count + + if allChatsAdded { + title = strings.FolderLinkPreview_TitleAddFolder + } else { + title = strings.FolderLinkPreview_TitleAddChats(Int32(canAddChatCount)) + } + } else { + title = strings.FolderLinkPreview_TitleAddFolder + } + } else { + title = " " + } + + let topBadge: String? + if isLinkList || allChatsAdded { + topBadge = nil + } else if case .remove = component.subject { + topBadge = nil + } else if let linkContents = component.linkContents, linkContents.localFilterId != nil, canAddChatCount != 0 { + topBadge = "+\(canAddChatCount)" + } else { + topBadge = nil + } + + let descriptionText: NSAttributedString + if isLinkList { + descriptionText = NSAttributedString(string: strings.FolderLinkPreview_TextLinkList) + } else if let linkContents = component.linkContents { + if case .remove = component.subject { + descriptionText = NSAttributedString(string: strings.FolderLinkPreview_TextRemoveFolder, font: Font.regular(15.0), textColor: theme.list.freeTextColor) + } else if allChatsAdded { + descriptionText = NSAttributedString(string: strings.FolderLinkPreview_TextAllAdded, font: Font.regular(15.0), textColor: theme.list.freeTextColor) + } else if linkContents.localFilterId == nil { + descriptionText = NSAttributedString(string: strings.FolderLinkPreview_TextAddFolder, font: Font.regular(15.0), textColor: theme.list.freeTextColor) + } else if let title = linkContents.title { + let chatCountString = strings.FolderLinkPreview_TextAddChatsCount(Int32(canAddChatCount)) + + let textValue = NSMutableAttributedString(string: strings.FolderLinkPreview_TextAddChatsV2) + textValue.addAttributes([ + .font: Font.regular(15.0), + .foregroundColor: theme.list.freeTextColor + ], range: NSRange(location: 0, length: textValue.length)) + + let folderRange = (textValue.string as NSString).range(of: "{folder}") + if folderRange.location != NSNotFound { + textValue.replaceCharacters(in: folderRange, with: "") + textValue.insert(title.attributedString(font: Font.semibold(15.0), textColor: theme.list.freeTextColor), at: folderRange.location) + } + + let chatsRange = (textValue.string as NSString).range(of: "{chats}") + if chatsRange.location != NSNotFound { + textValue.replaceCharacters(in: chatsRange, with: "") + textValue.insert(NSAttributedString(string: chatCountString, font: Font.semibold(15.0), textColor: theme.list.freeTextColor), at: chatsRange.location) + } + + descriptionText = textValue + } else { + descriptionText = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: theme.list.freeTextColor) + } + } else { + descriptionText = NSAttributedString(string: " ") + } + + let listHeaderTitle: String + if isLinkList { + listHeaderTitle = strings.FolderLinkPreview_LinkSectionHeader + } else if let linkContents = component.linkContents { + if case .remove = component.subject { + listHeaderTitle = strings.FolderLinkPreview_RemoveSectionSelectedHeader(Int32(linkContents.peers.count)) + } else if allChatsAdded { + listHeaderTitle = strings.FolderLinkPreview_ChatSectionHeader(Int32(linkContents.peers.count)) + } else { + listHeaderTitle = strings.FolderLinkPreview_ChatSectionJoinHeader(Int32(linkContents.peers.count)) + } + } else { + listHeaderTitle = " " + } + + var listHeaderActionItems: [AnimatedCounterComponent.Item] = [] + if !isLinkList, let linkContents = component.linkContents { + let dynamicIndex = strings.FolderLinkPreview_ListSelectionSelectAllFormat.range(of: "{dynamic}") + let staticIndex = strings.FolderLinkPreview_ListSelectionSelectAllFormat.range(of: "{static}") + var headerActionItemIndices: [Int: Int] = [:] + if let dynamicIndex, let staticIndex { + if dynamicIndex.lowerBound < staticIndex.lowerBound { + headerActionItemIndices[0] = 0 + headerActionItemIndices[1] = 1 + } else { + headerActionItemIndices[0] = 1 + headerActionItemIndices[1] = 0 + } + } else if dynamicIndex != nil { + headerActionItemIndices[0] = 0 + } else if staticIndex != nil { + headerActionItemIndices[1] = 0 + } + + let dynamicItem: AnimatedCounterComponent.Item + let staticItem: AnimatedCounterComponent.Item + + if selectedItems.count == linkContents.peers.count { + dynamicItem = AnimatedCounterComponent.Item(id: AnyHashable(0), text: strings.FolderLinkPreview_ListSelectionSelectAllDynamicPartDeselect, numericValue: 0) + staticItem = AnimatedCounterComponent.Item(id: AnyHashable(1), text: strings.FolderLinkPreview_ListSelectionSelectAllStaticPartDeselect, numericValue: 1) + } else { + dynamicItem = AnimatedCounterComponent.Item(id: AnyHashable(0), text: strings.FolderLinkPreview_ListSelectionSelectAllDynamicPartSelect, numericValue: 1) + staticItem = AnimatedCounterComponent.Item(id: AnyHashable(1), text: strings.FolderLinkPreview_ListSelectionSelectAllStaticPartSelect, numericValue: 1) + } + + if let dynamicIndex = headerActionItemIndices[0], let staticIndex = headerActionItemIndices[1] { + if dynamicIndex < staticIndex { + listHeaderActionItems = [dynamicItem, staticItem] + } else { + listHeaderActionItems = [staticItem, dynamicItem] + } + } else if headerActionItemIndices[0] != nil { + listHeaderActionItems = [dynamicItem] + } else if headerActionItemIndices[1] != nil { + listHeaderActionItems = [staticItem] + } + } + + let showsListHeaderAction: Bool + if isLinkList { + showsListHeaderAction = false + } else if let linkContents = component.linkContents { + showsListHeaderAction = !allChatsAdded && linkContents.peers.count > 1 + } else { + showsListHeaderAction = false + } + + let actionButtonTitle: String? + let actionButtonBadge: Int + if isLinkList { + actionButtonTitle = nil + actionButtonBadge = 0 + } else if case .remove = component.subject { + actionButtonBadge = selectedItems.count + if selectedItems.isEmpty { + actionButtonTitle = strings.FolderLinkPreview_ButtonRemoveFolder + } else { + actionButtonTitle = strings.FolderLinkPreview_ButtonRemoveFolderAndChats + } + } else if allChatsAdded { + actionButtonBadge = 0 + actionButtonTitle = strings.Common_OK + } else if let linkContents = component.linkContents { + actionButtonBadge = max(0, selectedItems.count - (linkContents.peers.count - canAddChatCount)) + if linkContents.localFilterId != nil { + if actionButtonBadge == 0 { + actionButtonTitle = strings.FolderLinkPreview_ButtonDoNotJoinChats + } else { + actionButtonTitle = strings.FolderLinkPreview_ButtonJoinChats + } + } else { + actionButtonTitle = strings.FolderLinkPreview_ButtonAddFolder + } + } else { + actionButtonTitle = " " + actionButtonBadge = 0 + } + + return ChatFolderLinkPreviewResolvedData( + title: title, + topBadge: topBadge, + descriptionText: descriptionText, + listHeaderTitle: listHeaderTitle, + listHeaderActionItems: listHeaderActionItems, + showsListHeaderAction: showsListHeaderAction, + actionButtonTitle: actionButtonTitle, + actionButtonBadge: actionButtonBadge, + actionButtonEnabled: !selectedItems.isEmpty || component.linkContents?.localFilterId != nil, + allChatsAdded: allChatsAdded, + canAddChatCount: canAddChatCount, + isLinkList: isLinkList + ) +} + +private final class ChatFolderLinkPreviewContentComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment - + let context: AccountContext let subject: ChatFolderLinkPreviewScreen.Subject let linkContents: ChatFolderLinkContents? - let completion: (() -> Void)? - + let theme: PresentationTheme + let resolvedData: ChatFolderLinkPreviewResolvedData + let selectedItems: Set + let linkListItems: [ExportedChatFolderLink] + let peerAction: (EnginePeer) -> Void + let toggleAllSelection: () -> Void + let openCreateLink: () -> Void + let openLink: (ExportedChatFolderLink) -> Void + let openLinkContextAction: (ExportedChatFolderLink, ContextExtractedContentContainingView, ContextGesture?) -> Void + init( context: AccountContext, subject: ChatFolderLinkPreviewScreen.Subject, linkContents: ChatFolderLinkContents?, - completion: (() -> Void)? + theme: PresentationTheme, + resolvedData: ChatFolderLinkPreviewResolvedData, + selectedItems: Set, + linkListItems: [ExportedChatFolderLink], + peerAction: @escaping (EnginePeer) -> Void, + toggleAllSelection: @escaping () -> Void, + openCreateLink: @escaping () -> Void, + openLink: @escaping (ExportedChatFolderLink) -> Void, + openLinkContextAction: @escaping (ExportedChatFolderLink, ContextExtractedContentContainingView, ContextGesture?) -> Void ) { self.context = context self.subject = subject self.linkContents = linkContents - self.completion = completion + self.theme = theme + self.resolvedData = resolvedData + self.selectedItems = selectedItems + self.linkListItems = linkListItems + self.peerAction = peerAction + self.toggleAllSelection = toggleAllSelection + self.openCreateLink = openCreateLink + self.openLink = openLink + self.openLinkContextAction = openLinkContextAction } - - static func ==(lhs: ChatFolderLinkPreviewScreenComponent, rhs: ChatFolderLinkPreviewScreenComponent) -> Bool { + + static func ==(lhs: ChatFolderLinkPreviewContentComponent, rhs: ChatFolderLinkPreviewContentComponent) -> Bool { if lhs.context !== rhs.context { return false } @@ -53,396 +338,67 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { if lhs.linkContents !== rhs.linkContents { return false } + if lhs.theme !== rhs.theme { + return false + } + if lhs.resolvedData != rhs.resolvedData { + return false + } + if lhs.selectedItems != rhs.selectedItems { + return false + } + if lhs.linkListItems != rhs.linkListItems { + return false + } return true } - - private struct ItemLayout: Equatable { - var containerSize: CGSize - var containerInset: CGFloat - var bottomInset: CGFloat - var topInset: CGFloat - var contentHeight: CGFloat - - init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat, contentHeight: CGFloat) { - self.containerSize = containerSize - self.containerInset = containerInset - self.bottomInset = bottomInset - self.topInset = topInset - self.contentHeight = contentHeight - } - } - - private final class ScrollView: UIScrollView { - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - return super.hitTest(point, with: event) - } - - override func touchesShouldCancel(in view: UIView) -> Bool { - return true - } - } - - final class AnimationHint { - init() { - } - } - - final class View: UIView, UIScrollViewDelegate { - private let dimView: UIView - private let backgroundLayer: SimpleLayer - private let navigationBarContainer: SparseContainerView - private let scrollView: ScrollView - private let scrollContentClippingView: SparseContainerView - private let scrollContentView: UIView - private let bottomBackgroundLayer: SimpleLayer - private let bottomSeparatorLayer: SimpleLayer - + + final class View: UIView { private let topIcon = ComponentView() - - private let title = ComponentView() - private let leftButton = ComponentView() private let descriptionText = ComponentView() - private let actionButton = ComponentView() - private let listHeaderText = ComponentView() private let listHeaderAction = ComponentView() + private let itemContainerView: UIView private var items: [AnyHashable: ComponentView] = [:] - - private var selectedItems = Set() - - private var linkListItems: [ExportedChatFolderLink] = [] - - private let bottomOverscrollLimit: CGFloat - - private var ignoreScrolling: Bool = false - - private var component: ChatFolderLinkPreviewScreenComponent? + + private var component: ChatFolderLinkPreviewContentComponent? private weak var state: EmptyComponentState? - private var environment: ViewControllerComponentContainer.Environment? - private var itemLayout: ItemLayout? - - private var topOffsetDistance: CGFloat? - - private var joinDisposable: Disposable? - - private var inProgress: Bool = false - + private var environment: EnvironmentType? + override init(frame: CGRect) { - self.bottomOverscrollLimit = 200.0 - - self.dimView = UIView() - - self.backgroundLayer = SimpleLayer() - self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - self.backgroundLayer.cornerRadius = 10.0 - - self.navigationBarContainer = SparseContainerView() - - self.scrollView = ScrollView() - - self.scrollContentClippingView = SparseContainerView() - self.scrollContentClippingView.clipsToBounds = true - - self.scrollContentView = UIView() - self.itemContainerView = UIView() self.itemContainerView.clipsToBounds = true - self.itemContainerView.layer.cornerRadius = 10.0 - - self.bottomBackgroundLayer = SimpleLayer() - self.bottomSeparatorLayer = SimpleLayer() - + self.itemContainerView.layer.cornerRadius = 26.0 + super.init(frame: frame) - - self.addSubview(self.dimView) - self.layer.addSublayer(self.backgroundLayer) - - self.addSubview(self.navigationBarContainer) - - self.scrollView.delaysContentTouches = false - self.scrollView.canCancelContentTouches = true - self.scrollView.clipsToBounds = false - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.scrollView.contentInsetAdjustmentBehavior = .never - } - if #available(iOS 13.0, *) { - self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false - } - self.scrollView.showsVerticalScrollIndicator = false - self.scrollView.showsHorizontalScrollIndicator = false - self.scrollView.alwaysBounceHorizontal = false - self.scrollView.alwaysBounceVertical = true - self.scrollView.scrollsToTop = false - self.scrollView.delegate = self - self.scrollView.clipsToBounds = true - - self.addSubview(self.scrollContentClippingView) - self.scrollContentClippingView.addSubview(self.scrollView) - - self.scrollView.addSubview(self.scrollContentView) - - self.scrollContentView.addSubview(self.itemContainerView) - - self.layer.addSublayer(self.bottomBackgroundLayer) - self.layer.addSublayer(self.bottomSeparatorLayer) - - self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + + self.addSubview(self.itemContainerView) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - deinit { - self.joinDisposable?.dispose() - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - if !self.ignoreScrolling { - self.updateScrolling(transition: .immediate) - } - } - - func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { - guard let itemLayout = self.itemLayout, let topOffsetDistance = self.topOffsetDistance else { - return - } - - if scrollView.contentOffset.y <= -100.0 && velocity.y <= -2.0 { - self.environment?.controller()?.dismiss() - } else { - var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset - if topOffset > 0.0 { - topOffset = max(0.0, topOffset) - - if topOffset < topOffsetDistance { - targetContentOffset.pointee.y = scrollView.contentOffset.y - scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true) - } - } - } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.bounds.contains(point) { - return nil - } - if !self.backgroundLayer.frame.contains(point) { - return self.dimView - } - - if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { - return result - } - - let result = super.hitTest(point, with: event) - return result - } - - @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - guard let environment = self.environment, let controller = environment.controller() else { - return - } - controller.dismiss() - } - } - - private func updateScrolling(transition: ComponentTransition) { - guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { - return - } - var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset - topOffset = max(0.0, topOffset) - transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) - - let bottomDistance = itemLayout.contentHeight - self.scrollView.bounds.maxY - let bottomAlphaDistance: CGFloat = 30.0 - var bottomAlpha: CGFloat = bottomDistance / bottomAlphaDistance - bottomAlpha = max(0.0, min(1.0, bottomAlpha)) - - let bottomOverlayAlpha: CGFloat = bottomAlpha - transition.setAlpha(layer: self.bottomBackgroundLayer, alpha: bottomOverlayAlpha) - transition.setAlpha(layer: self.bottomSeparatorLayer, alpha: bottomOverlayAlpha) - - transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) - - let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) - self.topOffsetDistance = topOffsetDistance - var topOffsetFraction = topOffset / topOffsetDistance - topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) - - let transitionFactor: CGFloat = 1.0 - topOffsetFraction - controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) - } - - func animateIn() { - self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY - self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.bottomBackgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.bottomSeparatorLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - if let actionButtonView = self.actionButton.view { - actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } - } - - func animateOut(completion: @escaping () -> Void) { - if let controller = self.environment?.controller() { - controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut)) - } - - var animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY - if self.scrollView.contentOffset.y < 0.0 { - animateOffset += -self.scrollView.contentOffset.y - } - - self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in - completion() - }) - self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - self.bottomBackgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - self.bottomSeparatorLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - if let actionButtonView = self.actionButton.view { - actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - } - } - - func update(component: ChatFolderLinkPreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - let animationHint = transition.userData(AnimationHint.self) - - var contentTransition = transition - if animationHint != nil { - contentTransition = .immediate - } - - let environment = environment[ViewControllerComponentContainer.Environment.self].value - let themeUpdated = self.environment?.theme !== environment.theme - - let resetScrolling = self.scrollView.bounds.width != availableSize.width - - let sideInset: CGFloat = 16.0 - - if self.component?.linkContents == nil, let linkContents = component.linkContents { - if case let .remove(_, defaultSelectedPeerIds) = component.subject { - for peer in linkContents.peers { - if defaultSelectedPeerIds.contains(peer.id) { - self.selectedItems.insert(peer.id) - } - } - } else { - for peer in linkContents.peers { - self.selectedItems.insert(peer.id) - } - } - } - - if self.component == nil, case let .linkList(_, initialLinks) = component.subject { - self.linkListItems = initialLinks - } - + + func update(component: ChatFolderLinkPreviewContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let environment = environment[EnvironmentType.self].value + self.component = component self.state = state self.environment = environment - - if themeUpdated { - self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor - self.itemContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor - self.bottomBackgroundLayer.backgroundColor = environment.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor - self.bottomSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor - } - - transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) - - var contentHeight: CGFloat = 0.0 - - let leftButtonSize = self.leftButton.update( - transition: contentTransition, - component: AnyComponent(Button( - content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.list.itemAccentColor)), - action: { [weak self] in - guard let self, let controller = self.environment?.controller() else { - return - } - controller.dismiss() - } - ).minSize(CGSize(width: 44.0, height: 56.0))), - environment: {}, - containerSize: CGSize(width: 120.0, height: 100.0) - ) - let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: leftButtonSize) - if let leftButtonView = self.leftButton.view { - if leftButtonView.superview == nil { - self.navigationBarContainer.addSubview(leftButtonView) - } - transition.setFrame(view: leftButtonView, frame: leftButtonFrame) - } - - let titleString: String - var allChatsAdded = false - var canAddChatCount = 0 - if case .linkList = component.subject { - titleString = environment.strings.FolderLinkPreview_TitleShare - } else if let linkContents = component.linkContents { - if case .remove = component.subject { - titleString = environment.strings.FolderLinkPreview_TitleRemove - } else if linkContents.localFilterId != nil { - if linkContents.alreadyMemberPeerIds == Set(linkContents.peers.map(\.id)) { - allChatsAdded = true - } - canAddChatCount = linkContents.peers.map(\.id).count - linkContents.alreadyMemberPeerIds.count - - if allChatsAdded { - titleString = environment.strings.FolderLinkPreview_TitleAddFolder - } else { - titleString = environment.strings.FolderLinkPreview_TitleAddChats(Int32(canAddChatCount)) - } - } else { - titleString = environment.strings.FolderLinkPreview_TitleAddFolder - } - } else { - titleString = " " - } - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) - ) - let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: 18.0), size: titleSize) - if let titleView = self.title.view { - if titleView.superview == nil { - self.navigationBarContainer.addSubview(titleView) - } - contentTransition.setFrame(view: titleView, frame: titleFrame) - } - - contentHeight += 44.0 - contentHeight += 14.0 - - var topBadge: String? - if case .linkList = component.subject { - } else if case .remove = component.subject { - } else if !allChatsAdded, let linkContents = component.linkContents, linkContents.localFilterId != nil, canAddChatCount != 0 { - topBadge = "+\(canAddChatCount)" - } - + self.itemContainerView.backgroundColor = component.theme.list.itemBlocksBackgroundColor + + let sideInset: CGFloat = 16.0 + var contentHeight: CGFloat = 58.0 + let topIconSize = self.topIcon.update( - transition: contentTransition, + transition: transition, component: AnyComponent(ChatFolderLinkHeaderComponent( context: component.context, - theme: environment.theme, + theme: component.theme, strings: environment.strings, title: component.linkContents?.title ?? ChatFolderTitle(text: "Folder", entities: [], enableAnimations: true), - badge: topBadge + badge: component.resolvedData.topBadge )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset, height: 1000.0) @@ -450,62 +406,23 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { let topIconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - topIconSize.width) * 0.5), y: contentHeight), size: topIconSize) if let topIconView = self.topIcon.view { if topIconView.superview == nil { - self.scrollContentView.addSubview(topIconView) + self.addSubview(topIconView) } - contentTransition.setFrame(view: topIconView, frame: topIconFrame) + transition.setFrame(view: topIconView, frame: topIconFrame) topIconView.isHidden = component.linkContents == nil } - + contentHeight += topIconSize.height contentHeight += 20.0 - - let text: NSAttributedString - if case .linkList = component.subject { - text = NSAttributedString(string: environment.strings.FolderLinkPreview_TextLinkList) - } else if let linkContents = component.linkContents { - if case .remove = component.subject { - text = NSAttributedString(string: environment.strings.FolderLinkPreview_TextRemoveFolder, font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor) - } else if allChatsAdded { - text = NSAttributedString(string: environment.strings.FolderLinkPreview_TextAllAdded, font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor) - } else if linkContents.localFilterId == nil { - text = NSAttributedString(string: environment.strings.FolderLinkPreview_TextAddFolder, font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor) - } else if let title = linkContents.title { - let chatCountString: String = environment.strings.FolderLinkPreview_TextAddChatsCount(Int32(canAddChatCount)) - - let textValue = NSMutableAttributedString(string: environment.strings.FolderLinkPreview_TextAddChatsV2) - textValue.addAttributes([ - .font: Font.regular(15.0), - .foregroundColor: environment.theme.list.freeTextColor - ], range: NSRange(location: 0, length: textValue.length)) - - let folderRange = (textValue.string as NSString).range(of: "{folder}") - if folderRange.location != NSNotFound { - textValue.replaceCharacters(in: folderRange, with: "") - textValue.insert(title.attributedString(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor), at: folderRange.location) - } - - let chatsRange = (textValue.string as NSString).range(of: "{chats}") - if chatsRange.location != NSNotFound { - textValue.replaceCharacters(in: chatsRange, with: "") - textValue.insert(NSAttributedString(string: chatCountString, font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor), at: chatsRange.location) - } - - text = textValue - } else { - text = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor) - } - } else { - text = NSAttributedString(string: " ") - } - + let descriptionTextSize = self.descriptionText.update( - transition: .immediate, + transition: transition, component: AnyComponent(MultilineTextWithEntitiesComponent( context: component.context, animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, - placeholderColor: environment.theme.list.freeTextColor.withMultipliedAlpha(0.1), - text: .plain(text), + placeholderColor: component.theme.list.freeTextColor.withMultipliedAlpha(0.1), + text: .plain(component.resolvedData.descriptionText), horizontalAlignment: .center, maximumNumberOfLines: 0 )), @@ -515,24 +432,23 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize) if let descriptionTextView = self.descriptionText.view { if descriptionTextView.superview == nil { - self.scrollContentView.addSubview(descriptionTextView) + self.addSubview(descriptionTextView) } - descriptionTextView.bounds = CGRect(origin: CGPoint(), size: descriptionTextFrame.size) - contentTransition.setPosition(view: descriptionTextView, position: descriptionTextFrame.center) + transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame) } - + contentHeight += descriptionTextFrame.height contentHeight += 39.0 - + var singleItemHeight: CGFloat = 0.0 - var itemsHeight: CGFloat = 0.0 var validIds: [AnyHashable] = [] - if case let .linkList(folderId, _) = component.subject { + + if case .linkList = component.subject { do { let id = AnyHashable("action") validIds.append(id) - + let item: ComponentView var itemTransition = transition if let current = self.items[id] { @@ -542,41 +458,41 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { item = ComponentView() self.items[id] = item } - + let itemSize = item.update( transition: itemTransition, component: AnyComponent(ActionListItemComponent( - theme: environment.theme, + theme: component.theme, sideInset: 0.0, iconName: "Contact List/LinkActionIcon", title: environment.strings.InviteLink_Create, - hasNext: !self.linkListItems.isEmpty, - action: { [weak self] in - self?.openCreateLink() + hasNext: !component.linkListItems.isEmpty, + action: { [weak component] in + component?.openCreateLink() } )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) ) let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) - + if let itemView = item.view { if itemView.superview == nil { self.itemContainerView.addSubview(itemView) } itemTransition.setFrame(view: itemView, frame: itemFrame) } - + itemsHeight += itemSize.height singleItemHeight = itemSize.height } - - for i in 0 ..< self.linkListItems.count { - let link = self.linkListItems[i] - + + for i in 0 ..< component.linkListItems.count { + let link = component.linkListItems[i] + let id = AnyHashable(link.link) validIds.append(id) - + let item: ComponentView var itemTransition = transition if let current = self.items[id] { @@ -586,94 +502,24 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { item = ComponentView() self.items[id] = item } - - let subtitle: String = environment.strings.ChatListFilter_LinkLabelChatCount(Int32(link.peerIds.count)) - + + let subtitle = environment.strings.ChatListFilter_LinkLabelChatCount(Int32(link.peerIds.count)) let itemComponent = LinkListItemComponent( - theme: environment.theme, + theme: component.theme, sideInset: 0.0, title: link.title.isEmpty ? link.link : link.title, link: link, label: subtitle, selectionState: .none, - hasNext: i != self.linkListItems.count - 1, - action: { [weak self] link in - guard let self else { - return - } - self.openLink(link: link) + hasNext: i != component.linkListItems.count - 1, + action: { [weak component] link in + component?.openLink(link) }, - contextAction: { [weak self] link, sourceView, gesture in - guard let self, let component = self.component, let environment = self.environment else { - return - } - - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - - var itemList: [ContextMenuItem] = [] - - itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, f in - f(.default) - - UIPasteboard.general.string = link.link - - if let self, let component = self.component, let controller = self.environment?.controller() { - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } - }))) - - itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - if let self, let component = self.component, let controller = self.environment?.controller() { - controller.present(QrCodeScreen(context: component.context, updatedPresentationData: nil, subject: .chatFolder(slug: link.slug)), in: .window(.root)) - } - }))) - - itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - if let self, let component = self.component { - self.linkListItems.removeAll(where: { $0.link == link.link }) - self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) - - let context = component.context - let _ = (context.engine.peers.editChatFolderLink(filterId: folderId, link: link, title: nil, peerIds: nil, revoke: true) - |> deliverOnMainQueue).start(completed: { - let _ = (context.engine.peers.deleteChatFolderLink(filterId: folderId, link: link) - |> deliverOnMainQueue).start(completed: { - }) - }) - } - }))) - - let items = ContextController.Items(content: .list(itemList)) - - let controller = makeContextController( - presentationData: presentationData, - source: .extracted(LinkListContextExtractedContentSource(contentView: sourceView)), - items: .single(items), - recognizer: nil, - gesture: gesture - ) - - environment.controller()?.forEachController({ controller in - if let controller = controller as? UndoOverlayController { - controller.dismiss() - } - return true - }) - environment.controller()?.presentInGlobalOverlay(controller) + contextAction: { [weak component] link, sourceView, gesture in + component?.openLinkContextAction(link, sourceView, gesture) } ) - + let itemSize = item.update( transition: itemTransition, component: AnyComponent(itemComponent), @@ -681,24 +527,24 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) ) let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) - + if let itemView = item.view { if itemView.superview == nil { self.itemContainerView.addSubview(itemView) } itemTransition.setFrame(view: itemView, frame: itemFrame) } - + itemsHeight += itemSize.height singleItemHeight = itemSize.height } } else if let linkContents = component.linkContents { for i in 0 ..< linkContents.peers.count { let peer = linkContents.peers[i] - + let id = AnyHashable(peer.id) validIds.append(id) - + let item: ComponentView var itemTransition = transition if let current = self.items[id] { @@ -708,7 +554,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { item = ComponentView() self.items[id] = item } - + var subtitle: String? if case let .channel(channel) = peer, case .broadcast = channel.info { if linkContents.alreadyMemberPeerIds.contains(peer.id) { @@ -723,67 +569,40 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { subtitle = environment.strings.FolderLinkPreview_LabelPeerMembers(Int32(memberCount)) } } - + let itemSize = item.update( transition: itemTransition, component: AnyComponent(PeerListItemComponent( context: component.context, - theme: environment.theme, + theme: component.theme, strings: environment.strings, sideInset: 0.0, title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), peer: peer, subtitle: subtitle, - selectionState: .editing(isSelected: self.selectedItems.contains(peer.id), isTinted: linkContents.alreadyMemberPeerIds.contains(peer.id)), + selectionState: .editing(isSelected: component.selectedItems.contains(peer.id), isTinted: linkContents.alreadyMemberPeerIds.contains(peer.id)), hasNext: i != linkContents.peers.count - 1, - action: { [weak self] peer in - guard let self, let component = self.component, let linkContents = component.linkContents, let controller = self.environment?.controller() else { - return - } - - if case .remove = component.subject { - if self.selectedItems.contains(peer.id) { - self.selectedItems.remove(peer.id) - } else { - self.selectedItems.insert(peer.id) - } - self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) - } else if linkContents.alreadyMemberPeerIds.contains(peer.id) { - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let text: String - if case let .channel(channel) = peer, case .broadcast = channel.info { - text = presentationData.strings.FolderLinkPreview_ToastAlreadyMemberChannel - } else { - text = presentationData.strings.FolderLinkPreview_ToastAlreadyMemberGroup - } - controller.present(UndoOverlayController(presentationData: presentationData, content: .peers(context: component.context, peers: [peer], title: nil, text: text, customUndoText: nil), elevatedLayout: false, action: { _ in true }), in: .current) - } else { - if self.selectedItems.contains(peer.id) { - self.selectedItems.remove(peer.id) - } else { - self.selectedItems.insert(peer.id) - } - self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) - } + action: { [weak component] peer in + component?.peerAction(peer) } )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) ) let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) - + if let itemView = item.view { if itemView.superview == nil { self.itemContainerView.addSubview(itemView) } itemTransition.setFrame(view: itemView, frame: itemFrame) } - + itemsHeight += itemSize.height singleItemHeight = itemSize.height } } - + var removeIds: [AnyHashable] = [] for (id, item) in self.items { if !validIds.contains(id) { @@ -794,71 +613,13 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { for id in removeIds { self.items.removeValue(forKey: id) } - - let listHeaderTitle: String - if case .linkList = component.subject { - listHeaderTitle = environment.strings.FolderLinkPreview_LinkSectionHeader - } else if let linkContents = component.linkContents { - if case .remove = component.subject { - listHeaderTitle = environment.strings.FolderLinkPreview_RemoveSectionSelectedHeader(Int32(linkContents.peers.count)) - } else if allChatsAdded { - listHeaderTitle = environment.strings.FolderLinkPreview_ChatSectionHeader(Int32(linkContents.peers.count)) - } else { - listHeaderTitle = environment.strings.FolderLinkPreview_ChatSectionJoinHeader(Int32(linkContents.peers.count)) - } - } else { - listHeaderTitle = " " - } - - var listHeaderActionItems: [AnimatedCounterComponent.Item] = [] - - let dynamicIndex = environment.strings.FolderLinkPreview_ListSelectionSelectAllFormat.range(of: "{dynamic}") - let staticIndex = environment.strings.FolderLinkPreview_ListSelectionSelectAllFormat.range(of: "{static}") - var headerActionItemIndices: [Int: Int] = [:] - if let dynamicIndex, let staticIndex { - if dynamicIndex.lowerBound < staticIndex.lowerBound { - headerActionItemIndices[0] = 0 - headerActionItemIndices[1] = 1 - } else { - headerActionItemIndices[0] = 1 - headerActionItemIndices[1] = 0 - } - } else if dynamicIndex != nil { - headerActionItemIndices[0] = 0 - } else if staticIndex != nil { - headerActionItemIndices[1] = 0 - } - - let dynamicItem: AnimatedCounterComponent.Item - let staticItem: AnimatedCounterComponent.Item - - if self.selectedItems.count == self.items.count { - dynamicItem = AnimatedCounterComponent.Item(id: AnyHashable(0), text: environment.strings.FolderLinkPreview_ListSelectionSelectAllDynamicPartDeselect, numericValue: 0) - staticItem = AnimatedCounterComponent.Item(id: AnyHashable(1), text: environment.strings.FolderLinkPreview_ListSelectionSelectAllStaticPartDeselect, numericValue: 1) - } else { - dynamicItem = AnimatedCounterComponent.Item(id: AnyHashable(0), text: environment.strings.FolderLinkPreview_ListSelectionSelectAllDynamicPartSelect, numericValue: 1) - staticItem = AnimatedCounterComponent.Item(id: AnyHashable(1), text: environment.strings.FolderLinkPreview_ListSelectionSelectAllStaticPartSelect, numericValue: 1) - } - - if let dynamicIndex = headerActionItemIndices[0], let staticIndex = headerActionItemIndices[1] { - if dynamicIndex < staticIndex { - listHeaderActionItems = [dynamicItem, staticItem] - } else { - listHeaderActionItems = [staticItem, dynamicItem] - } - } else if headerActionItemIndices[0] != nil { - listHeaderActionItems = [dynamicItem] - } else if headerActionItemIndices[1] != nil { - listHeaderActionItems = [staticItem] - } - - let listHeaderBody = MarkdownAttributeSet(font: Font.with(size: 13.0, design: .regular, traits: [.monospacedNumbers]), textColor: environment.theme.list.freeTextColor) - + + let listHeaderBody = MarkdownAttributeSet(font: Font.with(size: 13.0, design: .regular, traits: [.monospacedNumbers]), textColor: component.theme.list.freeTextColor) let listHeaderTextSize = self.listHeaderText.update( - transition: .immediate, + transition: transition, component: AnyComponent(MultilineTextComponent( text: .markdown( - text: listHeaderTitle, + text: component.resolvedData.listHeaderTitle, attributes: MarkdownAttributes( body: listHeaderBody, bold: listHeaderBody, @@ -872,40 +633,25 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { ) if let listHeaderTextView = self.listHeaderText.view { if listHeaderTextView.superview == nil { - listHeaderTextView.layer.anchorPoint = CGPoint() - self.scrollContentView.addSubview(listHeaderTextView) + self.addSubview(listHeaderTextView) } let listHeaderTextFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: listHeaderTextSize) - contentTransition.setPosition(view: listHeaderTextView, position: listHeaderTextFrame.origin) - listHeaderTextView.bounds = CGRect(origin: CGPoint(), size: listHeaderTextFrame.size) + transition.setFrame(view: listHeaderTextView, frame: listHeaderTextFrame) listHeaderTextView.isHidden = component.linkContents == nil } - + let listHeaderActionSize = self.listHeaderAction.update( transition: transition, component: AnyComponent(PlainButtonComponent( content: AnyComponent(AnimatedCounterComponent( font: Font.regular(13.0), - color: environment.theme.list.itemAccentColor, + color: component.theme.list.itemAccentColor, alignment: .right, - items: listHeaderActionItems + items: component.resolvedData.listHeaderActionItems )), effectAlignment: .right, - action: { [weak self] in - guard let self, let component = self.component, let linkContents = component.linkContents else { - return - } - if self.selectedItems.count != linkContents.peers.count { - for peer in linkContents.peers { - self.selectedItems.insert(peer.id) - } - } else { - self.selectedItems.removeAll() - for peerId in linkContents.alreadyMemberPeerIds { - self.selectedItems.insert(peerId) - } - } - self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) + action: { [weak component] in + component?.toggleAllSelection() } )), environment: {}, @@ -913,415 +659,544 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { ) if let listHeaderActionView = self.listHeaderAction.view { if listHeaderActionView.superview == nil { - listHeaderActionView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) - self.scrollContentView.addSubview(listHeaderActionView) + self.addSubview(listHeaderActionView) } let listHeaderActionFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - 15.0 - listHeaderActionSize.width, y: contentHeight), size: listHeaderActionSize) - contentTransition.setFrame(view: listHeaderActionView, frame: listHeaderActionFrame) - - if let linkContents = component.linkContents, !allChatsAdded, linkContents.peers.count > 1 { - listHeaderActionView.isHidden = false - } else { - listHeaderActionView.isHidden = true - } + transition.setFrame(view: listHeaderActionView, frame: listHeaderActionFrame) + listHeaderActionView.isHidden = !component.resolvedData.showsListHeaderAction } - + contentHeight += listHeaderTextSize.height contentHeight += 6.0 - - contentTransition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: itemsHeight))) - - var initialContentHeight = contentHeight - initialContentHeight += min(itemsHeight, floor(singleItemHeight * 3.5)) - + + transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: itemsHeight))) + contentHeight += itemsHeight - contentHeight += 24.0 - initialContentHeight += 24.0 - - let actionButtonTitle: String - var actionButtonBadge: Int = 0 - if case .remove = component.subject { - actionButtonBadge = self.selectedItems.count - if self.selectedItems.isEmpty { - actionButtonTitle = environment.strings.FolderLinkPreview_ButtonRemoveFolder - } else { - actionButtonTitle = environment.strings.FolderLinkPreview_ButtonRemoveFolderAndChats - } - } else if allChatsAdded { - actionButtonBadge = 0 - actionButtonTitle = environment.strings.Common_OK - } else if let linkContents = component.linkContents { - actionButtonBadge = max(0, self.selectedItems.count - (linkContents.peers.count - canAddChatCount)) - if linkContents.localFilterId != nil { - if actionButtonBadge == 0 { - actionButtonTitle = environment.strings.FolderLinkPreview_ButtonDoNotJoinChats - } else { - actionButtonTitle = environment.strings.FolderLinkPreview_ButtonJoinChats - } - } else { - actionButtonTitle = environment.strings.FolderLinkPreview_ButtonAddFolder - } - } else { - actionButtonTitle = " " + contentHeight += component.resolvedData.isLinkList ? 54.0 : 24.0 + + if itemsHeight == 0.0 && singleItemHeight == 0.0 { + transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: 0.0))) } - let actionButtonSize = self.actionButton.update( - transition: transition, - component: AnyComponent(ButtonComponent( - background: ButtonComponent.Background( - color: environment.theme.list.itemCheckColors.fillColor, - foreground: environment.theme.list.itemCheckColors.foregroundColor, - pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) - ), - content: AnyComponentWithIdentity( - id: actionButtonTitle, - component: AnyComponent(ButtonTextContentComponent( - text: actionButtonTitle, - badge: actionButtonBadge, - textColor: environment.theme.list.itemCheckColors.foregroundColor, - badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, - badgeForeground: environment.theme.list.itemCheckColors.fillColor - )) - ), - isEnabled: !self.selectedItems.isEmpty || component.linkContents?.localFilterId != nil, - displaysProgress: self.inProgress, - action: { [weak self] in - guard let self, let component = self.component, let linkContents = component.linkContents, let controller = self.environment?.controller() else { - return + contentHeight += 52.0 + 3.0 + environment.safeInsets.bottom + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class ChatFolderLinkPreviewScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: ChatFolderLinkPreviewScreen.Subject + let linkContents: ChatFolderLinkContents? + let completion: (() -> Void)? + + init( + context: AccountContext, + subject: ChatFolderLinkPreviewScreen.Subject, + linkContents: ChatFolderLinkContents?, + completion: (() -> Void)? + ) { + self.context = context + self.subject = subject + self.linkContents = linkContents + self.completion = completion + } + + static func ==(lhs: ChatFolderLinkPreviewScreenComponent, rhs: ChatFolderLinkPreviewScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + if lhs.linkContents !== rhs.linkContents { + return false + } + return true + } + + final class State: ComponentState { + var selectedItems = Set() + var didInitializeSelection = false + var linkListItems: [ExportedChatFolderLink] = [] + var didInitializeLinkList = false + var inProgress = false + var joinDisposable: Disposable? + + deinit { + self.joinDisposable?.dispose() + } + } + + func makeState() -> State { + return State() + } + + final class View: UIView { + private let sheet = ComponentView<(EnvironmentType, ResizableSheetComponentEnvironment)>() + private let animateOut = ActionSlot>() + + private var isDismissing = false + + private var component: ChatFolderLinkPreviewScreenComponent? + private weak var state: State? + private var environment: EnvironmentType? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func dismiss(controller: @escaping () -> ViewController?, animated: Bool) { + guard !self.isDismissing else { + return + } + self.isDismissing = true + + let performDismiss: () -> Void = { + if let controller = controller() as? ChatFolderLinkPreviewScreen { + controller.completePendingDismiss() + controller.dismiss(animated: false) + } else { + controller()?.dismiss(animated: false) + } + } + + if animated { + self.animateOut.invoke(Action { _ in + performDismiss() + }) + } else { + performDismiss() + } + } + + private func ensureInitializedState(component: ChatFolderLinkPreviewScreenComponent, state: State) { + if !state.didInitializeSelection, let linkContents = component.linkContents { + state.didInitializeSelection = true + if case let .remove(_, defaultSelectedPeerIds) = component.subject { + for peer in linkContents.peers { + if defaultSelectedPeerIds.contains(peer.id) { + state.selectedItems.insert(peer.id) } - - if case let .remove(folderId, _) = component.subject { - self.inProgress = true - self.state?.updated(transition: .immediate) - - component.completion?() - - let disposable = DisposableSet() - disposable.add(component.context.account.postbox.addHiddenChatIds(peerIds: Array(self.selectedItems))) - disposable.add(component.context.account.viewTracker.addHiddenChatListFilterIds([folderId])) - - let folderTitle: ChatFolderTitle - if let title = linkContents.title { - folderTitle = title - } else { - folderTitle = ChatFolderTitle(text: "", entities: [], enableAnimations: true) - } - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) - - var additionalText: String? - if !self.selectedItems.isEmpty { - additionalText = presentationData.strings.FolderLinkPreview_ToastLeftChatsText(Int32(self.selectedItems.count)) - } - - var chatListController: ChatListController? - if let navigationController = controller.navigationController as? NavigationController { - for viewController in navigationController.viewControllers.reversed() { - if viewController is ChatFolderLinkPreviewScreen { - continue - } - - if let rootController = viewController as? TabBarController { - for c in rootController.controllers { - if let c = c as? ChatListController { - chatListController = c - break - } - } - } else if let c = viewController as? ChatListController { - chatListController = c - break - } - + } + } else { + for peer in linkContents.peers { + state.selectedItems.insert(peer.id) + } + } + } + + if !state.didInitializeLinkList, case let .linkList(_, initialLinks) = component.subject { + state.didInitializeLinkList = true + state.linkListItems = initialLinks + } + } + + private func toggleAllSelection() { + guard let component = self.component, let state = self.state, let linkContents = component.linkContents else { + return + } + if state.selectedItems.count != linkContents.peers.count { + for peer in linkContents.peers { + state.selectedItems.insert(peer.id) + } + } else { + state.selectedItems.removeAll() + for peerId in linkContents.alreadyMemberPeerIds { + state.selectedItems.insert(peerId) + } + } + state.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) + } + + private func peerAction(peer: EnginePeer) { + guard let component = self.component, let state = self.state, let linkContents = component.linkContents, let controller = self.environment?.controller() else { + return + } + + if case .remove = component.subject { + if state.selectedItems.contains(peer.id) { + state.selectedItems.remove(peer.id) + } else { + state.selectedItems.insert(peer.id) + } + state.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) + } else if linkContents.alreadyMemberPeerIds.contains(peer.id) { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let text: String + if case let .channel(channel) = peer, case .broadcast = channel.info { + text = presentationData.strings.FolderLinkPreview_ToastAlreadyMemberChannel + } else { + text = presentationData.strings.FolderLinkPreview_ToastAlreadyMemberGroup + } + controller.present(UndoOverlayController(presentationData: presentationData, content: .peers(context: component.context, peers: [peer], title: nil, text: text, customUndoText: nil), elevatedLayout: false, action: { _ in true }), in: .current) + } else { + if state.selectedItems.contains(peer.id) { + state.selectedItems.remove(peer.id) + } else { + state.selectedItems.insert(peer.id) + } + state.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) + } + } + + private func presentLinkContextAction(link: ExportedChatFolderLink, sourceView: ContextExtractedContentContainingView, gesture: ContextGesture?) { + guard let component = self.component, let environment = self.environment else { + return + } + guard case let .linkList(folderId, _) = component.subject else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + var itemList: [ContextMenuItem] = [] + + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in + generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + UIPasteboard.general.string = link.link + + if let self, let component = self.component, let controller = self.environment?.controller() { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in false }), in: .window(.root)) + } + }))) + + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in + generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + if let self, let component = self.component, let controller = self.environment?.controller() { + controller.present(QrCodeScreen(context: component.context, updatedPresentationData: nil, subject: .chatFolder(slug: link.slug)), in: .window(.root)) + } + }))) + + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in + generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + guard let self, let component = self.component, let state = self.state else { + return + } + + state.linkListItems.removeAll(where: { $0.link == link.link }) + state.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) + + let context = component.context + let _ = (context.engine.peers.editChatFolderLink(filterId: folderId, link: link, title: nil, peerIds: nil, revoke: true) + |> deliverOnMainQueue).start(completed: { + let _ = (context.engine.peers.deleteChatFolderLink(filterId: folderId, link: link) + |> deliverOnMainQueue).start(completed: { + }) + }) + }))) + + let items = ContextController.Items(content: .list(itemList)) + let controller = makeContextController( + presentationData: presentationData, + source: .extracted(LinkListContextExtractedContentSource(contentView: sourceView)), + items: .single(items), + recognizer: nil, + gesture: gesture + ) + + environment.controller()?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismiss() + } + return true + }) + environment.controller()?.presentInGlobalOverlay(controller) + } + + private func performMainAction() { + guard let component = self.component, let state = self.state, let environment = self.environment, let controller = environment.controller() else { + return + } + guard let linkContents = component.linkContents else { + controller.dismiss() + return + } + + let resolvedData = chatFolderLinkPreviewResolvedData( + component: component, + theme: environment.theme.withModalBlocksBackground(), + strings: environment.strings, + selectedItems: state.selectedItems + ) + + if case let .remove(folderId, _) = component.subject { + state.inProgress = true + state.updated(transition: .immediate) + + component.completion?() + + let disposable = DisposableSet() + disposable.add(component.context.account.postbox.addHiddenChatIds(peerIds: Array(state.selectedItems))) + disposable.add(component.context.account.viewTracker.addHiddenChatListFilterIds([folderId])) + + let folderTitle: ChatFolderTitle + if let title = linkContents.title { + folderTitle = title + } else { + folderTitle = ChatFolderTitle(text: "", entities: [], enableAnimations: true) + } + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + let additionalText: String? = state.selectedItems.isEmpty ? nil : presentationData.strings.FolderLinkPreview_ToastLeftChatsText(Int32(state.selectedItems.count)) + + var chatListController: ChatListController? + if let navigationController = controller.navigationController as? NavigationController { + for viewController in navigationController.viewControllers.reversed() { + if viewController is ChatFolderLinkPreviewScreen { + continue + } + + if let rootController = viewController as? TabBarController { + for controller in rootController.controllers { + if let controller = controller as? ChatListController { + chatListController = controller break } } - - let undoText = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastLeftTitleV2) - let folderRange = (undoText.string as NSString).range(of: "{folder}") - if folderRange.location != NSNotFound { - undoText.replaceCharacters(in: folderRange, with: "") - undoText.insert(folderTitle.rawAttributedString, at: folderRange.location) - } - - let context = component.context - let selectedItems = self.selectedItems - let undoOverlayController = UndoOverlayController( - presentationData: presentationData, - content: .removedChat(context: component.context, title: undoText, text: additionalText), - elevatedLayout: false, - action: { value in - if case .commit = value { - let _ = (context.engine.peers.leaveChatFolder(folderId: folderId, removePeerIds: Array(selectedItems)) - |> deliverOnMainQueue).start(completed: { - Queue.mainQueue().after(1.0, { - disposable.dispose() - }) - }) - return true - } else if case .undo = value { - disposable.dispose() - return true - } - return false - } - ) - - if let chatListController, chatListController.view.window != nil { - chatListController.present(undoOverlayController, in: .current) - } else { - controller.present(undoOverlayController, in: .window(.root)) - } - - controller.dismiss() - } else if allChatsAdded { - controller.dismiss() - } else if let _ = component.linkContents { - if self.joinDisposable == nil, !self.selectedItems.isEmpty { - let joinSignal: Signal - switch component.subject { - case .linkList, .remove: - return - case let .slug(slug): - joinSignal = component.context.engine.peers.joinChatFolderLink(slug: slug, peerIds: Array(self.selectedItems)) - |> map(Optional.init) - case let .updates(updates): - var result: JoinChatFolderResult? - if let localFilterId = updates.chatFolderLinkContents.localFilterId, let title = updates.chatFolderLinkContents.title { - result = JoinChatFolderResult(folderId: localFilterId, title: title, newChatCount: self.selectedItems.count) - } - joinSignal = component.context.engine.peers.joinAvailableChatsInFolder(updates: updates, peerIds: Array(self.selectedItems)) - |> map { _ -> JoinChatFolderResult? in - } - |> then(Signal.single(result)) - } - - self.inProgress = true - self.state?.updated(transition: .immediate) - - self.joinDisposable = (joinSignal - |> deliverOnMainQueue).start(next: { [weak self] result in - guard let self, let component = self.component, let controller = self.environment?.controller() else { - return - } - - if let result, let navigationController = controller.navigationController as? NavigationController { - var chatListController: ChatListController? - for viewController in navigationController.viewControllers { - if let rootController = viewController as? TabBarController { - for c in rootController.controllers { - if let c = c as? ChatListController { - chatListController = c - break - } - } - } else if let c = viewController as? ChatListController { - chatListController = c - break - } - } - - if let chatListController { - navigationController.popToRoot(animated: true) - let context = component.context - chatListController.navigateToFolder(folderId: result.folderId, completion: { [weak context, weak chatListController] in - guard let context, let chatListController else { - return - } - - let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) - - var isUpdates = false - if case .updates = component.subject { - isUpdates = true - } else { - if component.linkContents?.localFilterId != nil { - isUpdates = true - } - } - - if isUpdates { - let titleString = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastChatsAddedTitleV2) - let folderRange = (titleString.string as NSString).range(of: "{folder}") - if folderRange.location != NSNotFound { - titleString.replaceCharacters(in: folderRange, with: "") - titleString.insert(result.title.rawAttributedString, at: folderRange.location) - } - - chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universalWithEntities(context: component.context, animation: "anim_add_to_folder", scale: 0.1, colors: ["__allcolors__": UIColor.white], title: titleString, text: NSAttributedString(string: presentationData.strings.FolderLinkPreview_ToastChatsAddedText(Int32(result.newChatCount))), animateEntities: true, customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) - } else if result.newChatCount != 0 { - let animationBackgroundColor: UIColor - if presentationData.theme.overallDarkAppearance { - animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor - } else { - animationBackgroundColor = UIColor(rgb: 0x474747) - } - - let titleString = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastChatsAddedTitleV2) - let folderRange = (titleString.string as NSString).range(of: "{folder}") - if folderRange.location != NSNotFound { - titleString.replaceCharacters(in: folderRange, with: "") - titleString.insert(result.title.rawAttributedString, at: folderRange.location) - } - - chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universalWithEntities(context: component.context, animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: titleString, text: NSAttributedString(string: presentationData.strings.FolderLinkPreview_ToastFolderAddedText(Int32(result.newChatCount))), animateEntities: true, customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) - } else { - let animationBackgroundColor: UIColor - if presentationData.theme.overallDarkAppearance { - animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor - } else { - animationBackgroundColor = UIColor(rgb: 0x474747) - } - - let titleString = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitleV2) - let folderRange = (titleString.string as NSString).range(of: "{folder}") - if folderRange.location != NSNotFound { - titleString.replaceCharacters(in: folderRange, with: "") - titleString.insert(result.title.rawAttributedString, at: folderRange.location) - } - - chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universalWithEntities(context: component.context, animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: titleString, text: NSAttributedString(string: ""), animateEntities: true, customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) - } - }) - } - } - - controller.dismiss() - }, error: { [weak self] error in - guard let self, let component = self.component, let controller = self.environment?.controller() else { - return - } - - let navigationController = controller.navigationController as? NavigationController - - switch error { - case .generic: - controller.dismiss() - case let .dialogFilterLimitExceeded(limit, _): - let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .folders, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in - guard let navigationController else { - return true - } - navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .folders)) - return true - }) - controller.push(limitController) - controller.dismiss() - case let .sharedFolderLimitExceeded(limit, _): - let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .membershipInSharedFolders, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in - guard let navigationController else { - return true - } - navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .membershipInSharedFolders)) - return true - }) - controller.push(limitController) - controller.dismiss() - case let .tooManyChannels(limit, _): - let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .chatsPerFolder, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in - guard let navigationController else { - return true - } - navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .chatsPerFolder)) - return true - }) - controller.push(limitController) - controller.dismiss() - case let .tooManyChannelsInAccount(limit, _): - let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .channels, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in - guard let navigationController else { - return true - } - navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .groupsAndChannels)) - return true - }) - controller.push(limitController) - controller.dismiss() - } + } else if let controller = viewController as? ChatListController { + chatListController = controller + } + + break + } + } + + let undoText = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastLeftTitleV2) + let folderRange = (undoText.string as NSString).range(of: "{folder}") + if folderRange.location != NSNotFound { + undoText.replaceCharacters(in: folderRange, with: "") + undoText.insert(folderTitle.rawAttributedString, at: folderRange.location) + } + + let context = component.context + let selectedItems = state.selectedItems + let undoOverlayController = UndoOverlayController( + presentationData: presentationData, + content: .removedChat(context: component.context, title: undoText, text: additionalText), + elevatedLayout: false, + action: { value in + if case .commit = value { + let _ = (context.engine.peers.leaveChatFolder(folderId: folderId, removePeerIds: Array(selectedItems)) + |> deliverOnMainQueue).start(completed: { + Queue.mainQueue().after(1.0, { + disposable.dispose() }) - } else { - controller.dismiss() + }) + return true + } else if case .undo = value { + disposable.dispose() + return true + } + return false + } + ) + + if let chatListController, chatListController.view.window != nil { + chatListController.present(undoOverlayController, in: .current) + } else { + controller.present(undoOverlayController, in: .window(.root)) + } + + controller.dismiss() + return + } + + if resolvedData.allChatsAdded { + controller.dismiss() + return + } + + guard state.joinDisposable == nil, !state.selectedItems.isEmpty else { + controller.dismiss() + return + } + + let joinSignal: Signal + switch component.subject { + case .linkList, .remove: + return + case let .slug(slug): + joinSignal = component.context.engine.peers.joinChatFolderLink(slug: slug, peerIds: Array(state.selectedItems)) + |> map(Optional.init) + case let .updates(updates): + var result: JoinChatFolderResult? + if let localFilterId = updates.chatFolderLinkContents.localFilterId, let title = updates.chatFolderLinkContents.title { + result = JoinChatFolderResult(folderId: localFilterId, title: title, newChatCount: state.selectedItems.count) + } + joinSignal = component.context.engine.peers.joinAvailableChatsInFolder(updates: updates, peerIds: Array(state.selectedItems)) + |> map { _ -> JoinChatFolderResult? in + } + |> then(Signal.single(result)) + } + + state.inProgress = true + state.updated(transition: .immediate) + + state.joinDisposable = (joinSignal + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + + if let result, let navigationController = controller.navigationController as? NavigationController { + var chatListController: ChatListController? + for viewController in navigationController.viewControllers { + if let rootController = viewController as? TabBarController { + for controller in rootController.controllers { + if let controller = controller as? ChatListController { + chatListController = controller + break + } } + } else if let controller = viewController as? ChatListController { + chatListController = controller + break } } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) - ) - - var bottomPanelHeight: CGFloat = 0.0 - - if case .linkList = component.subject { - bottomPanelHeight += 30.0 - } else { - bottomPanelHeight += 14.0 + environment.safeInsets.bottom + actionButtonSize.height - let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) - if let actionButtonView = self.actionButton.view { - if actionButtonView.superview == nil { - self.addSubview(actionButtonView) + + if let chatListController { + navigationController.popToRoot(animated: true) + let context = component.context + chatListController.navigateToFolder(folderId: result.folderId, completion: { [weak context, weak chatListController] in + guard let context, let chatListController else { + return + } + + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + + var isUpdates = false + if case .updates = component.subject { + isUpdates = true + } else if component.linkContents?.localFilterId != nil { + isUpdates = true + } + + if isUpdates { + let titleString = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastChatsAddedTitleV2) + let folderRange = (titleString.string as NSString).range(of: "{folder}") + if folderRange.location != NSNotFound { + titleString.replaceCharacters(in: folderRange, with: "") + titleString.insert(result.title.rawAttributedString, at: folderRange.location) + } + + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universalWithEntities(context: component.context, animation: "anim_add_to_folder", scale: 0.1, colors: ["__allcolors__": UIColor.white], title: titleString, text: NSAttributedString(string: presentationData.strings.FolderLinkPreview_ToastChatsAddedText(Int32(result.newChatCount))), animateEntities: true, customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) + } else if result.newChatCount != 0 { + let animationBackgroundColor: UIColor + if presentationData.theme.overallDarkAppearance { + animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor + } else { + animationBackgroundColor = UIColor(rgb: 0x474747) + } + + let titleString = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastChatsAddedTitleV2) + let folderRange = (titleString.string as NSString).range(of: "{folder}") + if folderRange.location != NSNotFound { + titleString.replaceCharacters(in: folderRange, with: "") + titleString.insert(result.title.rawAttributedString, at: folderRange.location) + } + + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universalWithEntities(context: component.context, animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: titleString, text: NSAttributedString(string: presentationData.strings.FolderLinkPreview_ToastFolderAddedText(Int32(result.newChatCount))), animateEntities: true, customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) + } else { + let animationBackgroundColor: UIColor + if presentationData.theme.overallDarkAppearance { + animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor + } else { + animationBackgroundColor = UIColor(rgb: 0x474747) + } + + let titleString = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitleV2) + let folderRange = (titleString.string as NSString).range(of: "{folder}") + if folderRange.location != NSNotFound { + titleString.replaceCharacters(in: folderRange, with: "") + titleString.insert(result.title.rawAttributedString, at: folderRange.location) + } + + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universalWithEntities(context: component.context, animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: titleString, text: NSAttributedString(string: ""), animateEntities: true, customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) + } + }) } - transition.setFrame(view: actionButtonView, frame: actionButtonFrame) } - - transition.setFrame(layer: self.bottomBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: bottomPanelHeight))) - transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel))) - } - - if let controller = environment.controller() { - let subLayout = ContainerViewLayout( - size: availableSize, - metrics: environment.metrics, - deviceMetrics: environment.deviceMetrics, - intrinsicInsets: UIEdgeInsets(top: 0.0, left: sideInset - 12.0, bottom: bottomPanelHeight, right: sideInset), - safeInsets: UIEdgeInsets(), - additionalInsets: UIEdgeInsets(), - statusBarHeight: nil, - inputHeight: nil, - inputHeightIsInteractivellyChanging: false, - inVoiceOver: false - ) - controller.presentationContext.containerLayoutUpdated(subLayout, transition: transition.containedViewLayoutTransition) - } - - contentHeight += bottomPanelHeight - initialContentHeight += bottomPanelHeight - - let containerInset: CGFloat = environment.statusBarHeight + 10.0 - let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) - - let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) - - self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset, contentHeight: scrollContentHeight) - - transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) - - transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) - transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) - - let scrollClippingFrame: CGRect - if case .linkList = component.subject { - scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height - (containerInset + 56.0) + 1000.0)) - } else { - scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height - bottomPanelHeight - 8.0 - (containerInset + 56.0))) - } - transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) - transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) - - self.ignoreScrolling = true - transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) - let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) - if contentSize != self.scrollView.contentSize { - self.scrollView.contentSize = contentSize - } - if resetScrolling { - self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) - } - self.ignoreScrolling = false - self.updateScrolling(transition: transition) - - return availableSize + + controller.dismiss() + }, error: { [weak self] error in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + + let navigationController = controller.navigationController as? NavigationController + + switch error { + case .generic: + controller.dismiss() + case let .dialogFilterLimitExceeded(limit, _): + let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .folders, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in + guard let navigationController else { + return true + } + navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .folders)) + return true + }) + controller.push(limitController) + controller.dismiss() + case let .sharedFolderLimitExceeded(limit, _): + let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .membershipInSharedFolders, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in + guard let navigationController else { + return true + } + navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .membershipInSharedFolders)) + return true + }) + controller.push(limitController) + controller.dismiss() + case let .tooManyChannels(limit, _): + let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .chatsPerFolder, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in + guard let navigationController else { + return true + } + navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .chatsPerFolder)) + return true + }) + controller.push(limitController) + controller.dismiss() + case let .tooManyChannelsInAccount(limit, _): + let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .channels, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in + guard let navigationController else { + return true + } + navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .groupsAndChannels)) + return true + }) + controller.push(limitController) + controller.dismiss() + } + }) } - + private func openLink(link: ExportedChatFolderLink) { guard let component = self.component else { return @@ -1329,7 +1204,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { guard case let .linkList(folderId, _) = component.subject else { return } - + let _ = (component.context.engine.peers.currentChatListFilters() |> deliverOnMainQueue).start(next: { [weak self] filters in guard let self, let component = self.component else { @@ -1341,7 +1216,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { guard case let .filter(_, title, _, data) = filter else { return } - + let peerIds = data.includePeers.peers let _ = (component.context.engine.data.get( EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) @@ -1350,7 +1225,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { guard let self, let component = self.component, let controller = self.environment?.controller() else { return } - + let peers = peers.compactMap({ peer -> EnginePeer? in guard let peer else { return nil @@ -1360,16 +1235,17 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } return peer }) - + let navigationController = controller.navigationController - controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: { _ in }, presentController: { [weak navigationController] c in - (navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root)) + controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: { _ in + }, presentController: { [weak navigationController] controller in + (navigationController?.topViewController as? ViewController)?.present(controller, in: .window(.root)) })) controller.dismiss() }) }) } - + private func openCreateLink() { guard let component = self.component else { return @@ -1377,7 +1253,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { guard case let .linkList(folderId, _) = component.subject else { return } - + let _ = (component.context.engine.peers.currentChatListFilters() |> deliverOnMainQueue).start(next: { [weak self] filters in guard let self, let component = self.component else { @@ -1389,7 +1265,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { guard case let .filter(_, title, _, data) = filter else { return } - + let peerIds = data.includePeers.peers let _ = (component.context.engine.data.get( EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) @@ -1398,7 +1274,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { guard let self, let component = self.component, let controller = self.environment?.controller() else { return } - + let peers = peers.compactMap({ peer -> EnginePeer? in guard let peer else { return nil @@ -1408,78 +1284,73 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } return peer }) + if peers.allSatisfy({ !canShareLinkToPeer(peer: $0) }) { let navigationController = controller.navigationController - controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: nil, linkUpdated: { _ in }, presentController: { [weak navigationController] c in - (navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root)) + controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: nil, linkUpdated: { _ in + }, presentController: { [weak navigationController] controller in + (navigationController?.topViewController as? ViewController)?.present(controller, in: .window(.root)) })) } else { var enabledPeerIds: [EnginePeer.Id] = [] - for peer in peers { - if canShareLinkToPeer(peer: peer) { - enabledPeerIds.append(peer.id) - } + for peer in peers where canShareLinkToPeer(peer: peer) { + enabledPeerIds.append(peer.id) } - + let _ = (component.context.engine.peers.exportChatFolder(filterId: folderId, title: "", peerIds: enabledPeerIds) |> deliverOnMainQueue).start(next: { [weak self] link in - guard let self, let component = self.component, let controller = self.environment?.controller() else { + guard let self, let component = self.component, let state = self.state, let controller = self.environment?.controller() else { return } - - self.linkListItems.insert(link, at: 0) - self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) - + + state.linkListItems.insert(link, at: 0) + state.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) + let navigationController = controller.navigationController controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: { [weak self] updatedLink in - guard let self else { + guard let self, let state = self.state else { return } - if let index = self.linkListItems.firstIndex(where: { $0.link == link.link }) { + if let index = state.linkListItems.firstIndex(where: { $0.link == link.link }) { if let updatedLink { - self.linkListItems[index] = updatedLink + state.linkListItems[index] = updatedLink } else { - self.linkListItems.remove(at: index) - } - } else { - if let updatedLink { - self.linkListItems.insert(updatedLink, at: 0) + state.linkListItems.remove(at: index) } + } else if let updatedLink { + state.linkListItems.insert(updatedLink, at: 0) } - self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) - }, presentController: { [weak navigationController] c in - (navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root)) + state.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) + }, presentController: { [weak navigationController] controller in + (navigationController?.topViewController as? ViewController)?.present(controller, in: .window(.root)) })) - + controller.dismiss() }, error: { [weak self] error in guard let self, let component = self.component, let controller = self.environment?.controller() else { return } - + let context = component.context let navigationController = controller.navigationController as? NavigationController - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - + let text: String switch error { case .generic: text = presentationData.strings.ChatListFilter_CreateLinkUnknownError case let .sharedFolderLimitExceeded(limit, _): - let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .membershipInSharedFolders, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in + let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .membershipInSharedFolders, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in guard let navigationController else { return true } navigationController.pushViewController(PremiumIntroScreen(context: context, source: .membershipInSharedFolders)) return true }) - controller.push(limitController) - return case let .limitExceeded(limit, _): - let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .linksPerSharedFolder, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in + let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .linksPerSharedFolder, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in guard let navigationController else { return true } @@ -1487,10 +1358,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { return true }) controller.push(limitController) - return case let .tooManyChannels(limit, _): - let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .chatsPerFolder, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in + let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .chatsPerFolder, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in guard let navigationController else { return true } @@ -1499,10 +1369,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { }) controller.push(limitController) controller.dismiss() - return case let .tooManyChannelsInAccount(limit, _): - let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .channels, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in + let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .channels, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in guard let navigationController else { return true } @@ -1511,24 +1380,159 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { }) controller.push(limitController) controller.dismiss() - return case .someUserTooManyChannels: text = presentationData.strings.ChatListFilter_CreateLinkErrorSomeoneHasChannelLimit } + controller.present(textAlertController(context: component.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }) } }) }) } + + func update(component: ChatFolderLinkPreviewScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + self.ensureInitializedState(component: component, state: state) + + self.component = component + self.state = state + + let environmentValue = environment[EnvironmentType.self].value + self.environment = environmentValue + let controller = environmentValue.controller + let theme = environmentValue.theme.withModalBlocksBackground() + + let dismiss: (Bool) -> Void = { [weak self] animated in + self?.dismiss(controller: controller, animated: animated) + } + + let resolvedData = chatFolderLinkPreviewResolvedData( + component: component, + theme: theme, + strings: environmentValue.strings, + selectedItems: state.selectedItems + ) + + let bottomItem: AnyComponent? + if let actionButtonTitle = resolvedData.actionButtonTitle { + bottomItem = AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: actionButtonTitle, + component: AnyComponent(ButtonTextContentComponent( + text: actionButtonTitle, + badge: resolvedData.actionButtonBadge, + textColor: theme.list.itemCheckColors.foregroundColor, + badgeBackground: theme.list.itemCheckColors.foregroundColor, + badgeForeground: theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: resolvedData.actionButtonEnabled, + displaysProgress: state.inProgress, + action: { [weak self] in + self?.performMainAction() + } + )) + } else { + bottomItem = nil + } + + let sheetSize = self.sheet.update( + transition: transition, + component: AnyComponent(ResizableSheetComponent( + content: AnyComponent(ChatFolderLinkPreviewContentComponent( + context: component.context, + subject: component.subject, + linkContents: component.linkContents, + theme: theme, + resolvedData: resolvedData, + selectedItems: state.selectedItems, + linkListItems: state.linkListItems, + peerAction: { [weak self] peer in + self?.peerAction(peer: peer) + }, + toggleAllSelection: { [weak self] in + self?.toggleAllSelection() + }, + openCreateLink: { [weak self] in + self?.openCreateLink() + }, + openLink: { [weak self] link in + self?.openLink(link: link) + }, + openLinkContextAction: { [weak self] link, sourceView, gesture in + self?.presentLinkContextAction(link: link, sourceView: sourceView, gesture: gesture) + } + )), + titleItem: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: resolvedData.title, font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor)) + )), + leftItem: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + dismiss(true) + } + ) + ), + rightItem: nil, + hasTopEdgeEffect: false, + bottomItem: bottomItem, + backgroundColor: .color(theme.list.modalBlocksBackgroundColor), + animateOut: self.animateOut + )), + environment: { + environmentValue + ResizableSheetComponentEnvironment( + theme: theme, + statusBarHeight: environmentValue.statusBarHeight, + safeInsets: environmentValue.safeInsets, + inputHeight: 0.0, + metrics: environmentValue.metrics, + deviceMetrics: environmentValue.deviceMetrics, + isDisplaying: environmentValue.isVisible, + isCentered: environmentValue.metrics.widthClass == .regular, + screenSize: availableSize, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + dismiss(animated) + } + ) + }, + containerSize: availableSize + ) + self.sheet.parentState = state + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) + } + transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize)) + } + + return availableSize + } } - + func makeView() -> View { return View(frame: CGRect()) } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + + func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -1540,56 +1544,53 @@ public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer { case remove(folderId: Int32, defaultSelectedPeerIds: [EnginePeer.Id]) case linkList(folderId: Int32, initialLinks: [ExportedChatFolderLink]) } - + private let context: AccountContext private var linkContentsDisposable: Disposable? - - private var isDismissed: Bool = false - + + private var isDismissed = false + private var dismissCompletion: (() -> Void)? + public init(context: AccountContext, subject: Subject, contents: ChatFolderLinkContents, completion: (() -> Void)? = nil) { self.context = context - + super.init(context: context, component: ChatFolderLinkPreviewScreenComponent(context: context, subject: subject, linkContents: contents, completion: completion), navigationBarAppearance: .none) - + self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal self.blocksBackgroundWhenInOverlay = true - self.automaticallyControlPresentationContextLayout = false self.lockOrientation = true } - + required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + deinit { self.linkContentsDisposable?.dispose() } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - } - + override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - + self.view.disablesInteractiveModalDismiss = true - - if let componentView = self.node.hostView.componentView as? ChatFolderLinkPreviewScreenComponent.View { - componentView.animateIn() - } } - + + func completePendingDismiss() { + let dismissCompletion = self.dismissCompletion + self.dismissCompletion = nil + dismissCompletion?() + } + override public func dismiss(completion: (() -> Void)? = nil) { if !self.isDismissed { self.isDismissed = true - - if let componentView = self.node.hostView.componentView as? ChatFolderLinkPreviewScreenComponent.View { - componentView.animateOut(completion: { [weak self] in - completion?() - self?.dismiss(animated: false) - }) + self.dismissCompletion = completion + + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() } else { + self.completePendingDismiss() self.dismiss(animated: false) } } @@ -1600,19 +1601,17 @@ private final class LinkListContextExtractedContentSource: ContextExtractedConte let keepInPlace: Bool = false let ignoreContentTouches: Bool = false let blurBackground: Bool = true - - //let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center - + private let contentView: ContextExtractedContentContainingView - + init(contentView: ContextExtractedContentContainingView) { self.contentView = contentView } - + func takeView() -> ContextControllerTakeViewInfo? { return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds) } - + func putBack() -> ContextControllerPutBackViewInfo? { return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) } diff --git a/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift b/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift index 91ded2f86e..6f7925d9dc 100644 --- a/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift +++ b/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift @@ -28,7 +28,7 @@ public enum ChatTimerScreenMode { private protocol TimerPickerView: UIView { } -private class TimerCustomPickerView: UIPickerView, TimerPickerView { +private final class TimerCustomPickerView: UIPickerView, TimerPickerView { var selectorColor: UIColor? = nil { didSet { for subview in self.subviews { @@ -42,10 +42,8 @@ private class TimerCustomPickerView: UIPickerView, TimerPickerView { override func didAddSubview(_ subview: UIView) { super.didAddSubview(subview) - if let selectorColor = self.selectorColor { - if subview.bounds.height <= 1.0 { - subview.backgroundColor = selectorColor - } + if let selectorColor = self.selectorColor, subview.bounds.height <= 1.0 { + subview.backgroundColor = selectorColor } } @@ -53,16 +51,14 @@ private class TimerCustomPickerView: UIPickerView, TimerPickerView { super.didMoveToWindow() if let selectorColor = self.selectorColor { - for subview in self.subviews { - if subview.bounds.height <= 1.0 { - subview.backgroundColor = selectorColor - } + for subview in self.subviews where subview.bounds.height <= 1.0 { + subview.backgroundColor = selectorColor } } } } -private class TimerDatePickerView: UIDatePicker, TimerPickerView { +private final class TimerDatePickerView: UIDatePicker, TimerPickerView { var selectorColor: UIColor? = nil { didSet { for subview in self.subviews { @@ -76,10 +72,8 @@ private class TimerDatePickerView: UIDatePicker, TimerPickerView { override func didAddSubview(_ subview: UIView) { super.didAddSubview(subview) - if let selectorColor = self.selectorColor { - if subview.bounds.height <= 1.0 { - subview.backgroundColor = selectorColor - } + if let selectorColor = self.selectorColor, subview.bounds.height <= 1.0 { + subview.backgroundColor = selectorColor } } @@ -87,10 +81,8 @@ private class TimerDatePickerView: UIDatePicker, TimerPickerView { super.didMoveToWindow() if let selectorColor = self.selectorColor { - for subview in self.subviews { - if subview.bounds.height <= 1.0 { - subview.backgroundColor = selectorColor - } + for subview in self.subviews where subview.bounds.height <= 1.0 { + subview.backgroundColor = selectorColor } } } @@ -99,7 +91,7 @@ private class TimerDatePickerView: UIDatePicker, TimerPickerView { private let digitsCharacterSet = CharacterSet(charactersIn: "0123456789") private let nondigitsCharacterSet = CharacterSet(charactersIn: "0123456789").inverted -private class TimerPickerItemView: UIView { +private final class TimerPickerItemView: UIView { let valueLabel = UILabel() let unitLabel = UILabel() @@ -114,7 +106,7 @@ private class TimerPickerItemView: UIView { didSet { if let (value, string) = self.value { let components = string.components(separatedBy: " ") - if value == viewOnceTimeout { + if value == viewOnceTimeout || string.rangeOfCharacter(from: digitsCharacterSet) == nil { self.valueLabel.text = string self.unitLabel.text = "" } else if components.count > 1 { @@ -155,16 +147,34 @@ private class TimerPickerItemView: UIView { self.valueLabel.sizeToFit() self.unitLabel.sizeToFit() - if let (value, _) = self.value, value == viewOnceTimeout { - self.valueLabel.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.frame.width - self.valueLabel.frame.size.width) / 2.0), y: floor((self.frame.height - self.valueLabel.frame.height) / 2.0)), size: self.valueLabel.frame.size) + if self.unitLabel.text?.isEmpty ?? false { + self.valueLabel.frame = CGRect( + origin: CGPoint( + x: floorToScreenPixels((self.frame.width - self.valueLabel.frame.size.width) / 2.0), + y: floor((self.frame.height - self.valueLabel.frame.height) / 2.0) + ), + size: self.valueLabel.frame.size + ) } else { - self.valueLabel.frame = CGRect(origin: CGPoint(x: self.frame.width / 2.0 - 28.0 - self.valueLabel.frame.size.width, y: floor((self.frame.height - self.valueLabel.frame.height) / 2.0)), size: self.valueLabel.frame.size) - self.unitLabel.frame = CGRect(origin: CGPoint(x: self.frame.width / 2.0 - 20.0, y: floor((self.frame.height - self.unitLabel.frame.height) / 2.0) + 2.0), size: self.unitLabel.frame.size) + self.valueLabel.frame = CGRect( + origin: CGPoint( + x: self.frame.width / 2.0 - 28.0 - self.valueLabel.frame.size.width, + y: floor((self.frame.height - self.valueLabel.frame.height) / 2.0) + ), + size: self.valueLabel.frame.size + ) + self.unitLabel.frame = CGRect( + origin: CGPoint( + x: self.frame.width / 2.0 - 20.0, + y: floor((self.frame.height - self.unitLabel.frame.height) / 2.0) + 2.0 + ), + size: self.unitLabel.frame.size + ) } } } -private var timerValues: [Int32] = { +private let timerValues: [Int32] = { var values: [Int32] = [] for i in 1 ..< 20 { values.append(Int32(i)) @@ -197,34 +207,19 @@ private let autoremoveTimerValues: [Int32] = [ private final class ChatTimerSheetContentComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment - let style: ChatTimerScreenStyle - let mode: ChatTimerScreenMode - let currentTime: Int32? + let configuration: ChatTimerScreen.Configuration let dismiss: () -> Void init( - style: ChatTimerScreenStyle, - mode: ChatTimerScreenMode, - currentTime: Int32?, + configuration: ChatTimerScreen.Configuration, dismiss: @escaping () -> Void ) { - self.style = style - self.mode = mode - self.currentTime = currentTime + self.configuration = configuration self.dismiss = dismiss } static func ==(lhs: ChatTimerSheetContentComponent, rhs: ChatTimerSheetContentComponent) -> Bool { - if lhs.style != rhs.style { - return false - } - if lhs.mode != rhs.mode { - return false - } - if lhs.currentTime != rhs.currentTime { - return false - } - return true + return lhs.configuration == rhs.configuration } final class View: UIView, UIPickerViewDataSource, UIPickerViewDelegate { @@ -248,113 +243,172 @@ private final class ChatTimerSheetContentComponent: Component { fatalError("init(coder:) has not been implemented") } + private func pickerTextColor(configuration: ChatTimerScreen.Configuration, environment: EnvironmentType) -> UIColor { + if let pickerTextColor = configuration.pickerTextColor { + return pickerTextColor(environment.theme) + } + + switch configuration.style { + case .default: + return environment.theme.actionSheet.primaryTextColor + case .media: + return .white + } + } + + private func mapPickerTimestamp(_ timestamp: Int32, mapping: ChatTimerScreen.Configuration.PickerValueMapping) -> Int32 { + switch mapping { + case .rawTimestamp: + return timestamp + case .roundDateToDaysUTC: + return roundDateToDays(timestamp) + case .secondsFromMidnightGMT: + return timestamp + } + } + private func selectedValue() -> Int32? { guard let component = self.component, let pickerView = self.pickerView else { return nil } - if let pickerView = pickerView as? TimerCustomPickerView { - switch component.mode { - case .sendTimer: - let row = pickerView.selectedRow(inComponent: 0) - if row == 0 { - return viewOnceTimeout - } else { - return timerValues[row - 1] - } - case .autoremove: - return autoremoveTimerValues[pickerView.selectedRow(inComponent: 0)] - case .mute: + switch component.configuration.picker { + case let .fixedValues(values, _, _): + guard let pickerView = pickerView as? TimerCustomPickerView else { return nil } - } else if let pickerView = pickerView as? TimerDatePickerView { - return Int32(pickerView.date.timeIntervalSince1970) - } else { - return nil + let row = pickerView.selectedRow(inComponent: 0) + guard row >= 0, row < values.count else { + return nil + } + return values[row] + case .timeOfDay, .date, .dateTime: + guard let pickerView = pickerView as? TimerDatePickerView else { + return nil + } + return self.mapPickerTimestamp(Int32(pickerView.date.timeIntervalSince1970), mapping: component.configuration.pickerValueMapping) } } - private func pickerTextColor(component: ChatTimerSheetContentComponent, environment: EnvironmentType) -> UIColor { - switch component.mode { - case .sendTimer: - return .white - case .autoremove: - if case .media = component.style { - return .white - } else { - return environment.theme.list.itemPrimaryTextColor + private func fixedValueSelectionIndex( + values: [Int32], + selectedValue: Int32, + strategy: ChatTimerScreen.Configuration.FixedSelectionStrategy + ) -> Int { + switch strategy { + case .exact: + return values.firstIndex(of: selectedValue) ?? 0 + case .closestLowerOrEqual: + var index = 0 + for i in 0 ..< values.count { + if values[i] <= selectedValue { + index = i + } } - case .mute: - if case .media = component.style { - return .white - } else { - return environment.theme.list.itemPrimaryTextColor + return index + case .firstGreaterOrEqual: + var index = max(0, values.count - 1) + for i in 0 ..< values.count { + if selectedValue <= values[i] { + index = i + break + } } + return index } } - private func selectAutoremoveValue(_ value: Int32, in pickerView: TimerCustomPickerView) { - var selectedRowIndex = 0 - for i in 0 ..< autoremoveTimerValues.count { - if autoremoveTimerValues[i] <= value { - selectedRowIndex = i - } - } - pickerView.selectRow(selectedRowIndex, inComponent: 0, animated: false) - } - - private func setupPickerView(component: ChatTimerSheetContentComponent, environment: EnvironmentType) { + private func setupPickerView(configuration: ChatTimerScreen.Configuration, environment: EnvironmentType) { let previousSelectedValue = self.selectedValue() let previousDate = (self.pickerView as? TimerDatePickerView)?.date - if let pickerView = self.pickerView { - pickerView.removeFromSuperview() - } + self.pickerView?.removeFromSuperview() - switch component.mode { - case .sendTimer: - let pickerView = TimerCustomPickerView() - pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18) - pickerView.dataSource = self - pickerView.delegate = self - self.addSubview(pickerView) - self.pickerView = pickerView - - if let previousSelectedValue { - if previousSelectedValue == viewOnceTimeout { - pickerView.selectRow(0, inComponent: 0, animated: false) - } else if let index = timerValues.firstIndex(of: previousSelectedValue) { - pickerView.selectRow(index + 1, inComponent: 0, animated: false) - } - } - case .autoremove: + switch configuration.picker { + case let .fixedValues(values, selectionStrategy, _): let pickerView = TimerCustomPickerView() pickerView.dataSource = self pickerView.delegate = self - pickerView.selectorColor = self.pickerTextColor(component: component, environment: environment).withMultipliedAlpha(0.18) + pickerView.selectorColor = self.pickerTextColor(configuration: configuration, environment: environment).withMultipliedAlpha(0.18) self.addSubview(pickerView) self.pickerView = pickerView - if let previousSelectedValue { - self.selectAutoremoveValue(previousSelectedValue, in: pickerView) - } else if let currentTime = component.currentTime { - self.selectAutoremoveValue(currentTime, in: pickerView) + if let selectedValue = previousSelectedValue ?? configuration.currentValue { + let index = self.fixedValueSelectionIndex(values: values, selectedValue: selectedValue, strategy: selectionStrategy) + pickerView.selectRow(index, inComponent: 0, animated: false) } - case .mute: + case .timeOfDay: let pickerView = TimerDatePickerView() - pickerView.locale = localeWithStrings(environment.strings) - pickerView.datePickerMode = .dateAndTime - pickerView.minimumDate = Date() + pickerView.datePickerMode = .time + pickerView.timeZone = TimeZone(secondsFromGMT: 0) + pickerView.locale = Locale.current if #available(iOS 13.4, *) { pickerView.preferredDatePickerStyle = .wheels } - pickerView.setValue(self.pickerTextColor(component: component, environment: environment), forKey: "textColor") - pickerView.setValue(false, forKey: "highlightsToday") - pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18) + pickerView.setValue(self.pickerTextColor(configuration: configuration, environment: environment), forKey: "textColor") + pickerView.selectorColor = self.pickerTextColor(configuration: configuration, environment: environment).withMultipliedAlpha(0.18) pickerView.addTarget(self, action: #selector(self.datePickerChanged), for: .valueChanged) - if let previousDate { - pickerView.date = max(previousDate, Date()) + let initialTimestamp: Int32 + if let currentValue = configuration.currentValue { + initialTimestamp = self.mapPickerTimestamp(currentValue, mapping: configuration.pickerValueMapping) + } else { + initialTimestamp = 0 } + let date = previousDate ?? Date(timeIntervalSince1970: Double(initialTimestamp)) + pickerView.date = date + self.addSubview(pickerView) + self.pickerView = pickerView + case .date: + let pickerView = TimerDatePickerView() + pickerView.datePickerMode = .date + pickerView.timeZone = TimeZone(secondsFromGMT: 0) + pickerView.locale = localeWithStrings(environment.strings) + if #available(iOS 13.4, *) { + pickerView.preferredDatePickerStyle = .wheels + } + pickerView.minimumDate = configuration.minimumDate + pickerView.maximumDate = configuration.maximumDate ?? Date(timeIntervalSince1970: Double(Int32.max - 1)) + pickerView.setValue(self.pickerTextColor(configuration: configuration, environment: environment), forKey: "textColor") + pickerView.selectorColor = self.pickerTextColor(configuration: configuration, environment: environment).withMultipliedAlpha(0.18) + pickerView.addTarget(self, action: #selector(self.datePickerChanged), for: .valueChanged) + let initialTimestamp: Int32 + if let currentValue = configuration.currentValue { + initialTimestamp = self.mapPickerTimestamp(currentValue, mapping: configuration.pickerValueMapping) + } else { + initialTimestamp = Int32(Date().timeIntervalSince1970) + } + var initialDate = previousDate ?? Date(timeIntervalSince1970: Double(initialTimestamp)) + if let minimumDate = pickerView.minimumDate, initialDate < minimumDate { + initialDate = minimumDate + } + if let maximumDate = pickerView.maximumDate, initialDate > maximumDate { + initialDate = maximumDate + } + pickerView.date = initialDate + self.addSubview(pickerView) + self.pickerView = pickerView + case .dateTime: + let pickerView = TimerDatePickerView() + pickerView.datePickerMode = .dateAndTime + pickerView.locale = localeWithStrings(environment.strings) + pickerView.minimumDate = configuration.minimumDate ?? Date() + pickerView.maximumDate = configuration.maximumDate + if #available(iOS 13.4, *) { + pickerView.preferredDatePickerStyle = .wheels + } + pickerView.setValue(self.pickerTextColor(configuration: configuration, environment: environment), forKey: "textColor") + pickerView.setValue(false, forKey: "highlightsToday") + pickerView.selectorColor = self.pickerTextColor(configuration: configuration, environment: environment).withMultipliedAlpha(0.18) + pickerView.addTarget(self, action: #selector(self.datePickerChanged), for: .valueChanged) + + var date = previousDate ?? configuration.currentValue.flatMap { Date(timeIntervalSince1970: Double($0)) } ?? Date() + if let minimumDate = pickerView.minimumDate, date < minimumDate { + date = minimumDate + } + if let maximumDate = pickerView.maximumDate, date > maximumDate { + date = maximumDate + } + pickerView.date = date self.addSubview(pickerView) self.pickerView = pickerView } @@ -364,89 +418,31 @@ private final class ChatTimerSheetContentComponent: Component { self.state?.updated(transition: .immediate) } - private func title(strings: PresentationStrings) -> String { - guard let component = self.component else { - return "" - } - - switch component.mode { - case .sendTimer: - return strings.Conversation_Timer_Title - case .autoremove: - return strings.Conversation_DeleteTimer_SetupTitle - case .mute: - return strings.Conversation_Mute_SetupTitle - } - } - - private func primaryButtonTitle(component: ChatTimerSheetContentComponent, environment: EnvironmentType) -> String { - switch component.mode { - case .sendTimer: - return environment.strings.Conversation_Timer_Send - case .autoremove: - return environment.strings.Conversation_DeleteTimer_Apply - case .mute: - if let pickerView = self.pickerView as? TimerDatePickerView { - let now = Int32(Date().timeIntervalSince1970) - let timeInterval = max(0, Int32(pickerView.date.timeIntervalSince1970) - now) - - if timeInterval > 0 { - let timeString = stringForPreciseRelativeTimestamp(strings: environment.strings, relativeTimestamp: Int32(pickerView.date.timeIntervalSince1970), relativeTo: now, dateTimeFormat: environment.dateTimeFormat) - return environment.strings.Conversation_Mute_ApplyMuteUntil(timeString).string - } else { - return environment.strings.Common_Close - } - } else { - return environment.strings.Common_Close - } - } - } - - private func complete(value: Int32) { + private func complete(selectedValue: Int32?) { guard !self.isCompleting else { return } self.isCompleting = true + let transformedValue: Int32? + if let component = self.component { + transformedValue = component.configuration.completionValueTransform(selectedValue) + } else { + transformedValue = nil + } if let controller = self.environment?.controller() as? ChatTimerScreen { - controller.completion(value) + controller.completion(transformedValue) } self.component?.dismiss() } - private func completeWithPickerValue() { - guard let component = self.component, let pickerView = self.pickerView else { - return - } - - if let pickerView = pickerView as? TimerCustomPickerView { - switch component.mode { - case .sendTimer: - let row = pickerView.selectedRow(inComponent: 0) - let value: Int32 - if row == 0 { - value = viewOnceTimeout - } else { - value = timerValues[row - 1] - } - self.complete(value: value) - case .autoremove: - self.complete(value: autoremoveTimerValues[pickerView.selectedRow(inComponent: 0)]) - case .mute: - break - } - } else if let pickerView = pickerView as? TimerDatePickerView { - switch component.mode { - case .mute: - let timeInterval = max(0, Int32(pickerView.date.timeIntervalSince1970) - Int32(Date().timeIntervalSince1970)) - self.complete(value: timeInterval) - default: - break - } - } - } - - func update(component: ChatTimerSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + func update( + component: ChatTimerSheetContentComponent, + availableSize: CGSize, + state: EmptyComponentState, + environment: Environment, + transition: ComponentTransition + ) -> CGSize { let environment = environment[EnvironmentType.self].value let previousComponent = self.component let previousEnvironment = self.environment @@ -464,12 +460,12 @@ private final class ChatTimerSheetContentComponent: Component { self.environment = environment self.state = state - if self.pickerView == nil || previousComponent?.mode != component.mode || previousComponent?.style != component.style || themeUpdated || stringsUpdated { - self.setupPickerView(component: component, environment: environment) + if self.pickerView == nil || previousComponent?.configuration != component.configuration || themeUpdated || stringsUpdated { + self.setupPickerView(configuration: component.configuration, environment: environment) } let titleColor: UIColor - switch component.style { + switch component.configuration.style { case .default: titleColor = environment.theme.actionSheet.primaryTextColor case .media: @@ -506,19 +502,33 @@ private final class ChatTimerSheetContentComponent: Component { transition.setFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: closeButtonSize)) } - let titleSize = self.title.update( - transition: transition, - component: AnyComponent( - Text(text: self.title(strings: environment.strings), font: Font.semibold(17.0), color: titleColor) - ), - environment: {}, - containerSize: CGSize(width: availableSize.width - 120.0, height: 44.0) - ) - if let titleView = self.title.view { - if titleView.superview == nil { - self.addSubview(titleView) + let titleText = component.configuration.title(environment.strings) + if let titleText, !titleText.isEmpty { + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + Text(text: titleText, font: Font.semibold(17.0), color: titleColor) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - 120.0, height: 44.0) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame( + view: titleView, + frame: CGRect( + origin: CGPoint( + x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), + y: floorToScreenPixels(16.0 + (barButtonSize.height - titleSize.height) / 2.0) + ), + size: titleSize + ) + ) } - transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: floorToScreenPixels(16.0 + (barButtonSize.height - titleSize.height) / 2.0)), size: titleSize)) + } else if let titleView = self.title.view, titleView.superview != nil { + titleView.removeFromSuperview() } var contentHeight: CGFloat = 68.0 @@ -531,7 +541,8 @@ private final class ChatTimerSheetContentComponent: Component { contentHeight += 17.0 let buttonSideInset: CGFloat = 30.0 - let primaryButtonTitle = self.primaryButtonTitle(component: component, environment: environment) + let selectedValue = self.selectedValue() + let primaryButtonTitle = component.configuration.primaryActionTitle(environment.strings, environment.dateTimeFormat, selectedValue) let primaryButtonSize = self.primaryButton.update( transition: transition, component: AnyComponent(ButtonComponent( @@ -547,7 +558,7 @@ private final class ChatTimerSheetContentComponent: Component { isEnabled: true, displaysProgress: false, action: { [weak self] in - self?.completeWithPickerValue() + self?.complete(selectedValue: self?.selectedValue()) } )), environment: {}, @@ -561,26 +572,33 @@ private final class ChatTimerSheetContentComponent: Component { } contentHeight += primaryButtonSize.height - if case .autoremove = component.mode, component.currentTime != nil { + if let secondaryAction = component.configuration.secondaryAction { contentHeight += 8.0 - let secondaryButtonTitle = environment.strings.Conversation_DeleteTimer_Disable + let foregroundColor: UIColor + switch secondaryAction.style { + case .accent: + foregroundColor = environment.theme.actionSheet.controlAccentColor + case .destructive: + foregroundColor = environment.theme.list.itemDestructiveColor + } + let secondaryButtonTitle = secondaryAction.title(environment.strings) let secondaryButtonSize = self.secondaryButton.update( transition: transition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( style: .glass, - color: environment.theme.list.itemDestructiveColor.withMultipliedAlpha(0.1), - foreground: environment.theme.list.itemDestructiveColor, - pressedColor: environment.theme.list.itemDestructiveColor.withMultipliedAlpha(0.8) + color: foregroundColor.withMultipliedAlpha(0.1), + foreground: foregroundColor, + pressedColor: foregroundColor.withMultipliedAlpha(0.2) ), content: AnyComponentWithIdentity(id: AnyHashable(secondaryButtonTitle), component: AnyComponent( - Text(text: secondaryButtonTitle, font: Font.semibold(17.0), color: environment.theme.list.itemDestructiveColor) + Text(text: secondaryButtonTitle, font: Font.semibold(17.0), color: foregroundColor) )), isEnabled: true, displaysProgress: false, action: { [weak self] in - self?.complete(value: 0) + self?.complete(selectedValue: secondaryAction.value()) } )), environment: {}, @@ -608,22 +626,20 @@ private final class ChatTimerSheetContentComponent: Component { } func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - guard let component = self.component else { + guard let configuration = self.component?.configuration else { return 0 } - switch component.mode { - case .sendTimer: - return timerValues.count + 1 - case .autoremove: - return autoremoveTimerValues.count - case .mute: + switch configuration.picker { + case let .fixedValues(values, _, _): + return values.count + case .timeOfDay, .date, .dateTime: return 0 } } func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent componentIndex: Int, reusing view: UIView?) -> UIView { - guard let component = self.component, let environment = self.environment else { + guard let configuration = self.component?.configuration, let environment = self.environment else { return UIView() } @@ -633,24 +649,14 @@ private final class ChatTimerSheetContentComponent: Component { } else { itemView = TimerPickerItemView() } - itemView.textColor = self.pickerTextColor(component: component, environment: environment) + itemView.textColor = self.pickerTextColor(configuration: configuration, environment: environment) - switch component.mode { - case .sendTimer: - if row == 0 { - let string = environment.strings.MediaPicker_Timer_ViewOnce - itemView.value = (viewOnceTimeout, string) - } else { - let value = timerValues[row - 1] - let string = timeIntervalString(strings: environment.strings, value: value) - itemView.value = (value, string) - } - case .autoremove: - let value = autoremoveTimerValues[row] - let string = timeIntervalString(strings: environment.strings, value: value) - itemView.value = (value, string) - case .mute: - preconditionFailure() + switch configuration.picker { + case let .fixedValues(values, _, formatter): + let value = values[row] + itemView.value = (value, formatter(environment.strings, value)) + case .timeOfDay, .date, .dateTime: + break } return itemView @@ -673,31 +679,14 @@ private final class ChatTimerSheetContentComponent: Component { private final class ChatTimerSheetComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment - let style: ChatTimerScreenStyle - let mode: ChatTimerScreenMode - let currentTime: Int32? + let configuration: ChatTimerScreen.Configuration - init( - style: ChatTimerScreenStyle, - mode: ChatTimerScreenMode, - currentTime: Int32? - ) { - self.style = style - self.mode = mode - self.currentTime = currentTime + init(configuration: ChatTimerScreen.Configuration) { + self.configuration = configuration } static func ==(lhs: ChatTimerSheetComponent, rhs: ChatTimerSheetComponent) -> Bool { - if lhs.style != rhs.style { - return false - } - if lhs.mode != rhs.mode { - return false - } - if lhs.currentTime != rhs.currentTime { - return false - } - return true + return lhs.configuration == rhs.configuration } final class View: UIView { @@ -743,7 +732,7 @@ private final class ChatTimerSheetComponent: Component { ) let backgroundColor: UIColor - switch component.style { + switch component.configuration.style { case .default: backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor case .media: @@ -754,9 +743,7 @@ private final class ChatTimerSheetComponent: Component { transition: transition, component: AnyComponent(SheetComponent( content: AnyComponent(ChatTimerSheetContentComponent( - style: component.style, - mode: component.mode, - currentTime: component.currentTime, + configuration: component.configuration, dismiss: { [weak self] in self?.dismiss() } @@ -793,9 +780,233 @@ private final class ChatTimerSheetComponent: Component { } public final class ChatTimerScreen: ViewControllerComponentContainer { - fileprivate let completion: (Int32) -> Void + public final class Configuration: Equatable { + public enum ActionStyle { + case accent + case destructive + } + + public enum PickerValueMapping { + case rawTimestamp + case roundDateToDaysUTC + case secondsFromMidnightGMT + } + + public enum FixedSelectionStrategy { + case exact + case closestLowerOrEqual + case firstGreaterOrEqual + } + + public final class SecondaryAction: Equatable { + public let title: (PresentationStrings) -> String + public let style: ActionStyle + public let value: () -> Int32? + + public init( + title: @escaping (PresentationStrings) -> String, + style: ActionStyle, + value: @escaping () -> Int32? + ) { + self.title = title + self.style = style + self.value = value + } + + public static func ==(lhs: SecondaryAction, rhs: SecondaryAction) -> Bool { + return lhs === rhs + } + } + + public enum PickerKind { + case timeOfDay + case date + case dateTime + case fixedValues( + values: [Int32], + selectionStrategy: FixedSelectionStrategy, + formatter: (PresentationStrings, Int32) -> String + ) + } + + public let style: ChatTimerScreenStyle + public let title: (PresentationStrings) -> String? + public let picker: PickerKind + public let currentValue: Int32? + public let minimumDate: Date? + public let maximumDate: Date? + public let pickerValueMapping: PickerValueMapping + public let primaryActionTitle: (PresentationStrings, PresentationDateTimeFormat, Int32?) -> String + public let secondaryAction: SecondaryAction? + public let completionValueTransform: (Int32?) -> Int32? + public let pickerTextColor: ((PresentationTheme) -> UIColor)? + + public init( + style: ChatTimerScreenStyle, + title: @escaping (PresentationStrings) -> String? = { _ in nil }, + picker: PickerKind, + currentValue: Int32?, + minimumDate: Date? = nil, + maximumDate: Date? = nil, + pickerValueMapping: PickerValueMapping, + primaryActionTitle: @escaping (PresentationStrings, PresentationDateTimeFormat, Int32?) -> String, + secondaryAction: SecondaryAction? = nil, + completionValueTransform: @escaping (Int32?) -> Int32? = { $0 }, + pickerTextColor: ((PresentationTheme) -> UIColor)? = nil + ) { + self.style = style + self.title = title + self.picker = picker + self.currentValue = currentValue + self.minimumDate = minimumDate + self.maximumDate = maximumDate + self.pickerValueMapping = pickerValueMapping + self.primaryActionTitle = primaryActionTitle + self.secondaryAction = secondaryAction + self.completionValueTransform = completionValueTransform + self.pickerTextColor = pickerTextColor + } + + public static func ==(lhs: Configuration, rhs: Configuration) -> Bool { + return lhs === rhs + } + } + + fileprivate let completion: (Int32?) -> Void + + private static func legacyConfiguration( + style: ChatTimerScreenStyle, + mode: ChatTimerScreenMode, + currentTime: Int32? + ) -> Configuration { + switch mode { + case .sendTimer: + return Configuration( + style: style, + title: { strings in + strings.Conversation_Timer_Title + }, + picker: .fixedValues( + values: [viewOnceTimeout] + timerValues, + selectionStrategy: .exact, + formatter: { strings, value in + if value == viewOnceTimeout { + return strings.MediaPicker_Timer_ViewOnce + } else { + return timeIntervalString(strings: strings, value: value) + } + } + ), + currentValue: currentTime ?? viewOnceTimeout, + pickerValueMapping: .rawTimestamp, + primaryActionTitle: { strings, _, _ in + strings.Conversation_Timer_Send + }, + pickerTextColor: { _ in + .white + } + ) + case .autoremove: + return Configuration( + style: style, + title: { strings in + strings.Conversation_DeleteTimer_SetupTitle + }, + picker: .fixedValues( + values: autoremoveTimerValues, + selectionStrategy: .closestLowerOrEqual, + formatter: { strings, value in + timeIntervalString(strings: strings, value: value) + } + ), + currentValue: currentTime, + pickerValueMapping: .rawTimestamp, + primaryActionTitle: { strings, _, _ in + strings.Conversation_DeleteTimer_Apply + }, + secondaryAction: currentTime != nil ? Configuration.SecondaryAction( + title: { strings in + strings.Conversation_DeleteTimer_Disable + }, + style: .destructive, + value: { + 0 + } + ) : nil, + pickerTextColor: { theme in + if case .media = style { + return .white + } else { + return theme.list.itemPrimaryTextColor + } + } + ) + case .mute: + return Configuration( + style: style, + title: { strings in + strings.Conversation_Mute_SetupTitle + }, + picker: .dateTime, + currentValue: currentTime, + minimumDate: Date(), + pickerValueMapping: .rawTimestamp, + primaryActionTitle: { strings, dateTimeFormat, selectedValue in + if let selectedValue { + let now = Int32(Date().timeIntervalSince1970) + let timeInterval = max(0, selectedValue - now) + if timeInterval > 0 { + let timeString = stringForPreciseRelativeTimestamp( + strings: strings, + relativeTimestamp: selectedValue, + relativeTo: now, + dateTimeFormat: dateTimeFormat + ) + return strings.Conversation_Mute_ApplyMuteUntil(timeString).string + } + } + return strings.Common_Close + }, + completionValueTransform: { selectedValue in + guard let selectedValue else { + return nil + } + return max(0, selectedValue - Int32(Date().timeIntervalSince1970)) + }, + pickerTextColor: { theme in + if case .media = style { + return .white + } else { + return theme.list.itemPrimaryTextColor + } + } + ) + } + } public init( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + configuration: Configuration, + completion: @escaping (Int32?) -> Void + ) { + self.completion = completion + + super.init( + context: context, + component: ChatTimerSheetComponent(configuration: configuration), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: configuration.style == .media ? .dark : .default, + updatedPresentationData: updatedPresentationData + ) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + public convenience init( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, style: ChatTimerScreenStyle, @@ -803,24 +1014,15 @@ public final class ChatTimerScreen: ViewControllerComponentContainer { currentTime: Int32? = nil, completion: @escaping (Int32) -> Void ) { - self.completion = completion - - super.init( + let configuration = Self.legacyConfiguration(style: style, mode: mode, currentTime: currentTime) + self.init( context: context, - component: ChatTimerSheetComponent( - style: style, - mode: mode, - currentTime: currentTime - ), - navigationBarAppearance: .none, - statusBarStyle: .ignore, - theme: style == .media ? .dark : .default, - updatedPresentationData: updatedPresentationData + updatedPresentationData: updatedPresentationData, + configuration: configuration, + completion: { value in + completion(value ?? 0) + } ) - - self.statusBar.statusBarStyle = .Ignore - self.navigationPresentation = .flatModal - self.blocksBackgroundWhenInOverlay = true } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index f73dfebfa5..3209e5c488 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -416,11 +416,11 @@ public final class MediaEditor { return (artist: artist, title: title) } - func playerAndThumbnails(_ signal: Signal, mirror: Bool = false) -> Signal<(AVPlayer, [UIImage], Double)?, NoError> { + func playerAndThumbnails(_ signal: Signal, mirroringChanges: [VideoMirroringChange] = []) -> Signal<(AVPlayer, [UIImage], Double)?, NoError> { return signal |> mapToSignal { player -> Signal<(AVPlayer, [UIImage], Double)?, NoError> in if let player, let asset = player.currentItem?.asset { - return videoFrames(asset: asset, count: framesCount, mirror: mirror) + return videoFrames(asset: asset, count: framesCount, mirroringChanges: mirroringChanges) |> map { framesAndUpdateTimestamp in return (player, framesAndUpdateTimestamp.0, framesAndUpdateTimestamp.1) } @@ -434,7 +434,8 @@ public final class MediaEditor { playerAndThumbnails(self.playerPromise.get()), self.additionalPlayersPromise.get() |> mapToSignal { players in - return combineLatest(players.compactMap { playerAndThumbnails(.single($0), mirror: true) }) + let mirroringChanges = self.values.additionalVideoMirroringChanges + return combineLatest(players.compactMap { playerAndThumbnails(.single($0), mirroringChanges: mirroringChanges) }) }, self.audioPlayerPromise.get(), self.valuesPromise.get(), @@ -566,6 +567,7 @@ public final class MediaEditor { videoVolume: 1.0, additionalVideoPath: nil, additionalVideoIsDual: false, + additionalVideoMirroringChanges: [], additionalVideoPosition: nil, additionalVideoScale: nil, additionalVideoRotation: nil, @@ -629,9 +631,7 @@ public final class MediaEditor { return } let additionalTexture = additionalImage.flatMap { loadTexture(image: $0, device: device) } - if mirror { - self.renderer.videoFinishPass.additionalTextureRotation = .rotate0DegreesMirrored - } + self.renderer.videoFinishPass.additionalTextureRotation = mirror ? .rotate0DegreesMirrored : .rotate0Degrees let hasTransparency = imageHasTransparency(image) self.renderer.consume(main: .texture(texture, time, hasTransparency, nil, 1.0, .zero), additionals: additionalTexture.flatMap { [.texture($0, time, false, nil, 1.0, .zero)] } ?? [], render: true, displayEnabled: false) } @@ -1821,9 +1821,9 @@ public final class MediaEditor { self.updateAdditionalVideoPlaybackRange() } - public func setAdditionalVideo(_ path: String?, isDual: Bool = false, positionChanges: [VideoPositionChange]) { + public func setAdditionalVideo(_ path: String?, isDual: Bool = false, mirroringChanges: [VideoMirroringChange] = [], positionChanges: [VideoPositionChange]) { self.updateValues(mode: .skipRendering) { values in - var values = values.withUpdatedAdditionalVideo(path: path, isDual: isDual, positionChanges: positionChanges) + var values = values.withUpdatedAdditionalVideo(path: path, isDual: isDual, mirroringChanges: mirroringChanges, positionChanges: positionChanges) if path == nil { values = values.withUpdatedAdditionalVideoOffset(nil).withUpdatedAdditionalVideoTrimRange(nil).withUpdatedAdditionalVideoVolume(nil) } @@ -2414,7 +2414,29 @@ public final class MediaEditor { } -public func videoFrames(asset: AVAsset?, count: Int, initialPlaceholder: UIImage? = nil, initialTimestamp: Double? = nil, mirror: Bool = false) -> Signal<([UIImage], Double), NoError> { +private func videoFrameMirroring(at timestamp: Double, changes: [VideoMirroringChange]) -> Bool { + guard let firstChange = changes.first else { + return false + } + var isMirrored = firstChange.isMirrored + for change in changes { + if timestamp >= change.timestamp { + isMirrored = change.isMirrored + } else { + break + } + } + return isMirrored +} + +private func mirroredVideoFrame(_ image: UIImage, mirrored: Bool) -> UIImage { + guard mirrored, let cgImage = image.cgImage else { + return image + } + return UIImage(cgImage: cgImage, scale: image.scale, orientation: .upMirrored) +} + +public func videoFrames(asset: AVAsset?, count: Int, initialPlaceholder: UIImage? = nil, initialTimestamp: Double? = nil, mirroringChanges: [VideoMirroringChange] = []) -> Signal<([UIImage], Double), NoError> { func blurredImage(_ image: UIImage) -> UIImage? { guard let image = image.cgImage else { return nil @@ -2481,35 +2503,46 @@ public func videoFrames(asset: AVAsset?, count: Int, initialPlaceholder: UIImage } else { firstFrame = generateSingleColorImage(size: CGSize(width: 24.0, height: 36.0), color: .black)! } + firstFrame = mirroredVideoFrame(firstFrame, mirrored: videoFrameMirroring(at: 0.0, changes: mirroringChanges)) if let asset { return Signal { subscriber in subscriber.putNext((Array(repeating: firstFrame, count: count), initialTimestamp ?? CACurrentMediaTime())) var timestamps: [NSValue] = [] + var requestedTimes: [CMTime] = [] let duration = asset.duration.seconds let interval = duration / Double(count) for i in 0 ..< count { - timestamps.append(NSValue(time: CMTime(seconds: Double(i) * interval, preferredTimescale: CMTimeScale(1000)))) + let requestedTime = CMTime(seconds: Double(i) * interval, preferredTimescale: CMTimeScale(1000)) + requestedTimes.append(requestedTime) + timestamps.append(NSValue(time: requestedTime)) } - var updatedFrames: [UIImage] = [] - imageGenerator?.generateCGImagesAsynchronously(forTimes: timestamps) { _, image, _, _, _ in + var updatedFrames = Array(repeating: firstFrame, count: count) + var remainingFrames = count + imageGenerator?.generateCGImagesAsynchronously(forTimes: timestamps) { requestedTime, image, actualTime, _, _ in + guard let frameIndex = requestedTimes.firstIndex(where: { CMTimeCompare($0, requestedTime) == 0 }) else { + return + } if let image { - updatedFrames.append(UIImage(cgImage: image, scale: 1.0, orientation: mirror ? .upMirrored : .up)) - if updatedFrames.count == count { + let frameTimestamp = actualTime.seconds.isFinite ? actualTime.seconds : requestedTime.seconds + updatedFrames[frameIndex] = mirroredVideoFrame( + UIImage(cgImage: image), + mirrored: videoFrameMirroring(at: frameTimestamp, changes: mirroringChanges) + ) + remainingFrames -= 1 + if remainingFrames == 0 { subscriber.putNext((updatedFrames, CACurrentMediaTime())) subscriber.putCompletion() } else { - var tempFrames = updatedFrames - for _ in 0 ..< count - updatedFrames.count { - tempFrames.append(firstFrame) - } - subscriber.putNext((tempFrames, CACurrentMediaTime())) + subscriber.putNext((updatedFrames, CACurrentMediaTime())) } - } else { - if let previous = updatedFrames.last { - updatedFrames.append(previous) + } else if remainingFrames > 0 { + remainingFrames -= 1 + if remainingFrames == 0 { + subscriber.putNext((updatedFrames, CACurrentMediaTime())) + subscriber.putCompletion() } } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index dc285429ae..f5a5aee9bc 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -22,7 +22,7 @@ public enum EditorToolKey: Int32, CaseIterable { case blur case curves case stickerOutline - + static let adjustmentToolsKeys: [EditorToolKey] = [ .enhance, .brightness, @@ -44,7 +44,7 @@ public struct VideoPositionChange: Codable, Equatable { case translationFrom case timestamp } - + public let additional: Bool public let translationFrom: CGPoint? public let timestamp: Double @@ -60,6 +60,24 @@ public struct VideoPositionChange: Codable, Equatable { } } +public struct VideoMirroringChange: Codable, Equatable { + private enum CodingKeys: String, CodingKey { + case isMirrored + case timestamp + } + + public let isMirrored: Bool + public let timestamp: Double + + public init( + isMirrored: Bool, + timestamp: Double + ) { + self.isMirrored = isMirrored + self.timestamp = timestamp + } +} + public struct MediaAudioTrack: Codable, Equatable { private enum CodingKeys: String, CodingKey { case path @@ -291,6 +309,9 @@ public final class MediaEditorValues: Codable, Equatable, CustomStringConvertibl if lhs.additionalVideoIsDual != rhs.additionalVideoIsDual { return false } + if lhs.additionalVideoMirroringChanges != rhs.additionalVideoMirroringChanges { + return false + } if lhs.additionalVideoPosition != rhs.additionalVideoPosition { return false } @@ -402,6 +423,7 @@ public final class MediaEditorValues: Codable, Equatable, CustomStringConvertibl case videoVolume case additionalVideoPath case additionalVideoIsDual + case additionalVideoMirroringChanges case additionalVideoPosition case additionalVideoScale case additionalVideoRotation @@ -591,6 +613,7 @@ public final class MediaEditorValues: Codable, Equatable, CustomStringConvertibl public let additionalVideoPath: String? public let additionalVideoIsDual: Bool + public let additionalVideoMirroringChanges: [VideoMirroringChange] public let additionalVideoPosition: CGPoint? public let additionalVideoScale: CGFloat? public let additionalVideoRotation: CGFloat? @@ -660,6 +683,7 @@ public final class MediaEditorValues: Codable, Equatable, CustomStringConvertibl videoVolume: CGFloat?, additionalVideoPath: String?, additionalVideoIsDual: Bool, + additionalVideoMirroringChanges: [VideoMirroringChange], additionalVideoPosition: CGPoint?, additionalVideoScale: CGFloat?, additionalVideoRotation: CGFloat?, @@ -700,6 +724,7 @@ public final class MediaEditorValues: Codable, Equatable, CustomStringConvertibl self.videoVolume = videoVolume self.additionalVideoPath = additionalVideoPath self.additionalVideoIsDual = additionalVideoIsDual + self.additionalVideoMirroringChanges = additionalVideoMirroringChanges self.additionalVideoPosition = additionalVideoPosition self.additionalVideoScale = additionalVideoScale self.additionalVideoRotation = additionalVideoRotation @@ -755,6 +780,7 @@ public final class MediaEditorValues: Codable, Equatable, CustomStringConvertibl self.additionalVideoPath = try container.decodeIfPresent(String.self, forKey: .additionalVideoPath) self.additionalVideoIsDual = try container.decodeIfPresent(Bool.self, forKey: .additionalVideoIsDual) ?? false + self.additionalVideoMirroringChanges = try container.decodeIfPresent([VideoMirroringChange].self, forKey: .additionalVideoMirroringChanges) ?? [] self.additionalVideoPosition = try container.decodeIfPresent(CGPoint.self, forKey: .additionalVideoPosition) self.additionalVideoScale = try container.decodeIfPresent(CGFloat.self, forKey: .additionalVideoScale) self.additionalVideoRotation = try container.decodeIfPresent(CGFloat.self, forKey: .additionalVideoRotation) @@ -829,6 +855,7 @@ public final class MediaEditorValues: Codable, Equatable, CustomStringConvertibl try container.encodeIfPresent(self.additionalVideoPath, forKey: .additionalVideoPath) try container.encodeIfPresent(self.additionalVideoIsDual, forKey: .additionalVideoIsDual) + try container.encodeIfPresent(self.additionalVideoMirroringChanges, forKey: .additionalVideoMirroringChanges) try container.encodeIfPresent(self.additionalVideoPosition, forKey: .additionalVideoPosition) try container.encodeIfPresent(self.additionalVideoScale, forKey: .additionalVideoScale) try container.encodeIfPresent(self.additionalVideoRotation, forKey: .additionalVideoRotation) @@ -869,125 +896,125 @@ public final class MediaEditorValues: Codable, Equatable, CustomStringConvertibl } public func makeCopy() -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedCrop(offset: CGPoint, scale: CGFloat, rotation: CGFloat, mirroring: Bool) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: offset, cropRect: self.cropRect, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: offset, cropRect: self.cropRect, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } public func withUpdatedCropRect(cropRect: CGRect, rotation: CGFloat, mirroring: Bool) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: .zero, cropRect: cropRect, cropScale: 1.0, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: .zero, cropRect: cropRect, cropScale: 1.0, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedGradientColors(gradientColors: [UIColor]) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedVideoIsMuted(_ videoIsMuted: Bool) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedVideoIsFullHd(_ videoIsFullHd: Bool) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedVideoIsMirrored(_ videoIsMirrored: Bool) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedVideoVolume(_ videoVolume: CGFloat?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } - func withUpdatedAdditionalVideo(path: String?, isDual: Bool, positionChanges: [VideoPositionChange]) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: path, additionalVideoIsDual: isDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: positionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + func withUpdatedAdditionalVideo(path: String?, isDual: Bool, mirroringChanges: [VideoMirroringChange], positionChanges: [VideoPositionChange]) -> MediaEditorValues { + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: path, additionalVideoIsDual: isDual, additionalVideoMirroringChanges: mirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: positionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } public func withUpdatedAdditionalVideo(position: CGPoint, scale: CGFloat, rotation: CGFloat) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: position, additionalVideoScale: scale, additionalVideoRotation: rotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: position, additionalVideoScale: scale, additionalVideoRotation: rotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } public func withUpdatedAdditionalVideoPositionChanges(additionalVideoPositionChanges: [VideoPositionChange]) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedAdditionalVideoTrimRange(_ additionalVideoTrimRange: Range?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedAdditionalVideoOffset(_ additionalVideoOffset: Double?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedAdditionalVideoVolume(_ additionalVideoVolume: CGFloat?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedCollage(_ collage: [VideoCollageItem]) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } public func withUpdatedVideoTrimRange(_ videoTrimRange: Range) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedDrawingAndEntities(drawing: UIImage?, entities: [CodableDrawingEntity]) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: drawing, maskDrawing: self.maskDrawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: drawing, maskDrawing: self.maskDrawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } public func withUpdatedMaskDrawing(maskDrawing: UIImage?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedToolValues(_ toolValues: [EditorToolKey: Any]) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedAudioTrack(_ audioTrack: MediaAudioTrack?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedAudioTrackTrimRange(_ audioTrackTrimRange: Range?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedAudioTrackOffset(_ audioTrackOffset: Double?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedAudioTrackVolume(_ audioTrackVolume: CGFloat?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedAudioTrackSamples(_ audioTrackSamples: MediaAudioTrackSamples?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedCollageTrackSamples(_ collageTrackSamples: MediaAudioTrackSamples?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } func withUpdatedNightTheme(_ nightTheme: Bool) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } public func withUpdatedEntities(_ entities: [CodableDrawingEntity]) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } public func withUpdatedCoverImageTimestamp(_ coverImageTimestamp: Double?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } public func withUpdatedCoverDimensions(_ coverDimensions: CGSize?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: coverDimensions, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: coverDimensions, qualityPreset: self.qualityPreset) } public func withUpdatedQualityPreset(_ qualityPreset: MediaQualityPreset?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoBounce: self.videoBounce, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoMirroringChanges: self.additionalVideoMirroringChanges, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: qualityPreset) } public var resultDimensions: PixelDimensions { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index 92b590f103..4c622874ad 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -485,7 +485,7 @@ public final class MediaEditorVideoExport { } else if let additionalPath = self.configuration.values.additionalVideoPath { let asset = AVURLAsset(url: URL(fileURLWithPath: additionalPath)) additionalAsset = asset - signals = [.single(.video(asset: asset, rect: nil, scale: 1.0, offset: .zero, rotation: textureRotatonForAVAsset(asset, mirror: true), duration: asset.duration.seconds, trimRange: nil, trimOffset: nil, volume: nil))] + signals = [.single(.video(asset: asset, rect: nil, scale: 1.0, offset: .zero, rotation: textureRotatonForAVAsset(asset, mirror: false), duration: asset.duration.seconds, trimRange: nil, trimOffset: nil, volume: nil))] } var audioAsset: AVAsset? diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift index bf17eb05e1..90daf25142 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift @@ -264,8 +264,7 @@ private class VideoInputContext: NSObject, InputContext, AVPlayerItemOutputPullD super.init() - //TODO: mirror if self.additionalPlayer == nil && self.mirror - self.textureRotation = textureRotatonForAVAsset(self.playerItem.asset, mirror: rect == nil ? additional : false) + self.textureRotation = textureRotatonForAVAsset(self.playerItem.asset, mirror: false) let colorProperties: [String: Any] = [ AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift index 65206a070c..240c81a650 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift @@ -265,7 +265,7 @@ private func lookupSpringValue(_ t: CGFloat) -> CGFloat { for i in 0 ..< table.count - 2 { let lhs = table[i] let rhs = table[i + 1] - + if t >= lhs.0 && t <= rhs.0 { let fraction = (t - lhs.0) / (rhs.0 - lhs.0) let value = lhs.1 + fraction * (rhs.1 - lhs.1) @@ -289,7 +289,7 @@ struct VideoEncodeParameters { final class VideoFinishPass: RenderPass { private var cachedTexture: MTLTexture? - + var gradientPipelineState: MTLRenderPipelineState? var mainPipelineState: MTLRenderPipelineState? @@ -444,9 +444,8 @@ final class VideoFinishPass: RenderPass { if let position = values.additionalVideoPosition, let scale = values.additionalVideoScale, let rotation = values.additionalVideoRotation { self.additionalPosition = VideoFinishPass.VideoPosition(position: position, size: CGSize(width: 1080.0 / 4.0, height: 1440.0 / 4.0), scale: scale, rotation: rotation, mirroring: false, baseScale: self.additionalPosition.baseScale) } - if !values.additionalVideoPositionChanges.isEmpty { - self.videoPositionChanges = values.additionalVideoPositionChanges - } + self.videoPositionChanges = values.additionalVideoPositionChanges + self.additionalVideoMirroringChanges = values.additionalVideoMirroringChanges self.videoDuration = videoDuration self.additionalVideoDuration = additionalVideoDuration self.videoRange = values.videoTrimRange @@ -486,12 +485,30 @@ final class VideoFinishPass: RenderPass { private var isSticker = true private var coverDimensions: CGSize? private var videoPositionChanges: [VideoPositionChange] = [] + private var additionalVideoMirroringChanges: [VideoMirroringChange] = [] private var videoDuration: Double? private var additionalVideoDuration: Double? private var videoRange: Range? private var additionalVideoRange: Range? private var additionalVideoOffset: Double? + private func additionalVideoMirroring(at timestamp: Double) -> Bool { + guard let firstChange = self.additionalVideoMirroringChanges.first else { + return false + } + let assetTimestamp = timestamp + (self.additionalVideoOffset ?? 0.0) + + var isMirrored = firstChange.isMirrored + for change in self.additionalVideoMirroringChanges { + if assetTimestamp >= change.timestamp { + isMirrored = change.isMirrored + } else { + break + } + } + return isMirrored + } + enum VideoType { case main case additional @@ -566,6 +583,7 @@ final class VideoFinishPass: RenderPass { var mainPosition = self.mainPosition var additionalPosition = self.additionalPosition + additionalPosition = VideoPosition(position: additionalPosition.position, size: additionalPosition.size, scale: additionalPosition.scale, rotation: additionalPosition.rotation, mirroring: self.additionalVideoMirroring(at: timestamp), baseScale: additionalPosition.baseScale) var disappearingPosition = self.mainPosition var transitionFraction = 1.0 @@ -588,7 +606,7 @@ final class VideoFinishPass: RenderPass { backgroundTexture = additionalInput backgroundTextureRotation = self.additionalTextureRotation - mainPosition = VideoPosition(position: mainPosition.position, size: CGSize(width: 1440.0, height: 1920.0), scale: mainPosition.scale, rotation: mainPosition.rotation, mirroring: mainPosition.mirroring, baseScale: mainPosition.baseScale) + mainPosition = VideoPosition(position: mainPosition.position, size: CGSize(width: 1440.0, height: 1920.0), scale: mainPosition.scale, rotation: mainPosition.rotation, mirroring: additionalPosition.mirroring, baseScale: mainPosition.baseScale) additionalPosition = VideoPosition(position: additionalPosition.position, size: CGSize(width: 1080.0 / 4.0, height: 1920.0 / 4.0), scale: additionalPosition.scale, rotation: additionalPosition.rotation, mirroring: additionalPosition.mirroring, baseScale: additionalPosition.baseScale) foregroundTexture = mainInput diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift index 0f51bd6e6a..b3344e2200 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift @@ -9,6 +9,21 @@ import Photos import MediaEditor import DrawingUI +private func additionalVideoMirroring(at timestamp: Double, changes: [VideoMirroringChange]) -> Bool { + guard let firstChange = changes.first else { + return false + } + var isMirrored = firstChange.isMirrored + for change in changes { + if timestamp >= change.timestamp { + isMirrored = change.isMirrored + } else { + break + } + } + return isMirrored +} + extension MediaEditorScreenImpl { func requestStoryCompletion(animated: Bool) { guard let mediaEditor = self.node.mediaEditor, !self.didComplete else { @@ -161,6 +176,10 @@ extension MediaEditorScreenImpl { } else { firstFrameTime = CMTime(seconds: mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60)) } + let additionalFirstFrameTime = CMTime( + seconds: max(0.0, firstFrameTime.seconds + (mediaEditor.values.additionalVideoOffset ?? 0.0)), + preferredTimescale: firstFrameTime.timescale + ) let videoResult: Signal var videoIsMirrored = false let duration: Double @@ -210,7 +229,7 @@ extension MediaEditorScreenImpl { let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath)) let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) avAssetGenerator.appliesPreferredTrackTransform = true - avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in + avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: additionalFirstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in if let additionalCGImage { subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage))) subscriber.putCompletion() @@ -302,7 +321,7 @@ extension MediaEditorScreenImpl { let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath)) let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) avAssetGenerator.appliesPreferredTrackTransform = true - avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in + avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: additionalFirstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in if let additionalCGImage { subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage))) subscriber.putCompletion() @@ -328,7 +347,7 @@ extension MediaEditorScreenImpl { let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath)) let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) avAssetGenerator.appliesPreferredTrackTransform = true - avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in + avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: additionalFirstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in if let additionalCGImage { subscriber.putNext((image, UIImage(cgImage: additionalCGImage))) subscriber.putCompletion() @@ -437,7 +456,15 @@ extension MediaEditorScreenImpl { let (image, additionalImage) = images var currentImage = mediaEditor.resultImage if let image { - mediaEditor.replaceSource(image, additionalImage: additionalImage, time: firstFrameTime, mirror: true) + mediaEditor.replaceSource( + image, + additionalImage: additionalImage, + time: firstFrameTime, + mirror: additionalVideoMirroring( + at: additionalFirstFrameTime.seconds, + changes: mediaEditor.values.additionalVideoMirroringChanges + ) + ) if let updatedImage = mediaEditor.getResultImage(mirror: videoIsMirrored) { currentImage = updatedImage } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index c35771e65c..6d9263e04f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/ChatListUI", "//submodules/TelegramUI/Components/Chat/ChatMessageItemView", "//submodules/ChatPresentationInterfaceState", + "//submodules/TelegramUI/Components/ChatScheduleTimeController", "//submodules/TelegramUI/Components/ChatTimerScreen", "//submodules/TelegramUI/Components/ChatTitleView", "//submodules/Components/ComponentDisplayAdapters", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoInteraction.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoInteraction.swift index 6f167a5832..fc9b68ca01 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoInteraction.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoInteraction.swift @@ -41,7 +41,6 @@ final class PeerInfoInteraction { let editingOpenAutoremoveMesages: () -> Void let openPermissions: () -> Void let openLocation: () -> Void - let editingOpenSetupLocation: () -> Void let openPeerInfo: (EnginePeer, Bool) -> Void let performMemberAction: (PeerInfoMember, PeerInfoMemberAction) -> Void let openPeerInfoContextMenu: (PeerInfoContextSubject, ASDisplayNode, CGRect?) -> Void @@ -120,7 +119,6 @@ final class PeerInfoInteraction { editingOpenAutoremoveMesages: @escaping () -> Void, openPermissions: @escaping () -> Void, openLocation: @escaping () -> Void, - editingOpenSetupLocation: @escaping () -> Void, openPeerInfo: @escaping (EnginePeer, Bool) -> Void, performMemberAction: @escaping (PeerInfoMember, PeerInfoMemberAction) -> Void, openPeerInfoContextMenu: @escaping (PeerInfoContextSubject, ASDisplayNode, CGRect?) -> Void, @@ -198,7 +196,6 @@ final class PeerInfoInteraction { self.editingOpenAutoremoveMesages = editingOpenAutoremoveMesages self.openPermissions = openPermissions self.openLocation = openLocation - self.editingOpenSetupLocation = editingOpenSetupLocation self.openPeerInfo = openPeerInfo self.performMemberAction = performMemberAction self.openPeerInfoContextMenu = openPeerInfoContextMenu diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift index e98f24d93c..e328408aa0 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift @@ -1332,7 +1332,6 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s let ItemRecentActions = 111 let ItemLocationHeader = 112 let ItemLocation = 113 - let ItemLocationSetup = 114 let ItemDeleteGroup = 115 let ItemReactions = 116 let ItemTopics = 117 @@ -1356,11 +1355,6 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s interaction.openLocation() } )) - if cachedData.flags.contains(.canChangePeerGeoLocation) { - items[.groupLocation]!.append(PeerInfoScreenActionItem(id: ItemLocationSetup, text: presentationData.strings.Group_Location_ChangeLocation, action: { - interaction.editingOpenSetupLocation() - })) - } } if isCreator || (channel.adminRights != nil && channel.hasPermission(.pinMessages)) { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 356ab64269..ca2e94f0b8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -518,9 +518,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro openLocation: { [weak self] in self?.openLocation() }, - editingOpenSetupLocation: { [weak self] in - self?.editingOpenSetupLocation() - }, openPeerInfo: { [weak self] peer, isMember in self?.openPeerInfo(peer: peer, isMember: isMember) }, @@ -4299,40 +4296,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let controller = LocationViewController(context: context, updatedPresentationData: self.controller?.updatedPresentationData, subject: EngineMessage(message), params: controllerParams) self.controller?.push(controller) } - - private func editingOpenSetupLocation() { - guard let data = self.data, let peer = data.peer else { - return - } - let controller = LocationPickerController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, mode: .pick, completion: { [weak self] location, _, _, address, _ in - guard let strongSelf = self else { - return - } - let addressSignal: Signal - if let address = address { - addressSignal = .single(address) - } else { - addressSignal = reverseGeocodeLocation(latitude: location.latitude, longitude: location.longitude) - |> map { placemark in - if let placemark = placemark { - return placemark.fullAddress - } else { - return "\(location.latitude), \(location.longitude)" - } - } - } - - let context = strongSelf.context - let _ = (addressSignal - |> mapToSignal { address -> Signal in - return updateChannelGeoLocation(postbox: context.account.postbox, network: context.account.network, channelId: peer.id, coordinate: (location.latitude, location.longitude), address: address) - } - |> deliverOnMainQueue).startStandalone() - }) - self.controller?.push(controller) - } - private func openPeerInfo(peer: EnginePeer, isMember: Bool) { let mode: PeerInfoControllerMode = .generic if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: mode, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift index 5e1350aaed..47d6a0ca5c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift @@ -6,10 +6,119 @@ import SwiftSignalKit import TelegramCore import LegacyMediaPickerUI import ChatHistorySearchContainerNode +import ChatScheduleTimeController import MediaResources import TelegramUIPreferences extension PeerInfoScreenNode { + private func presentMediaScheduleTimePicker(completion: @escaping (Int32, Bool) -> Void) { + guard let peerId = self.chatLocation.peerId else { + return + } + let _ = (self.context.account.viewTracker.peerView(peerId) + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerView in + guard let self, let controller = self.controller, let peer = peerViewMainPeer(peerView) else { + return + } + + var sendWhenOnlineAvailable = false + if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case .present = presence.status { + sendWhenOnlineAvailable = true + } + if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 { + sendWhenOnlineAvailable = false + } + + let mode: ChatScheduleTimeScreen.Mode + if peerId == self.context.account.peerId { + mode = .reminders + } else { + mode = .scheduledMessages(peerId: peer.id, sendWhenOnlineAvailable: sendWhenOnlineAvailable) + } + + let scheduleController = ChatScheduleTimeScreen( + context: self.context, + mode: mode, + currentTime: nil, + currentRepeatPeriod: nil, + minimalTime: nil, + silentPosting: false, + isDark: true, + completion: { result in + completion(result.time, result.silentPosting) + } + ) + self.view.endEditing(true) + controller.present(scheduleController, in: .window(.root)) + }) + } + + private func openScheduledMessages() { + guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { + return + } + + var mappedChatLocation = self.chatLocation + if case let .replyThread(message) = self.chatLocation, message.peerId == self.context.account.peerId { + mappedChatLocation = .peer(id: self.context.account.peerId) + } + + let scheduledController = self.context.sharedContext.makeChatController( + context: self.context, + chatLocation: mappedChatLocation, + subject: .scheduledMessages, + botStart: nil, + mode: .standard(.default), + params: nil + ) + scheduledController.navigationPresentation = .modal + navigationController.pushViewController(scheduledController) + } + + private func transformEditedMediaMessages(_ messages: [EnqueueMessage], replyToMessageId: EngineMessage.Id, silentPosting: Bool, scheduleTime: Int32?) -> [EnqueueMessage] { + let replySubject = EngineMessageReplySubject(messageId: replyToMessageId, quote: nil, innerSubject: nil) + let defaultThreadId: Int64? + if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.peerId == self.context.account.peerId { + defaultThreadId = replyThreadMessage.threadId + } else { + defaultThreadId = nil + } + + return messages.map { message in + var message = message.withUpdatedReplyToMessageId(replySubject) + + if let defaultThreadId { + var updateThreadId = false + switch message { + case let .message(_, _, _, _, threadId, _, _, _, _, _): + updateThreadId = threadId == nil + case let .forward(_, threadId, _, _, _): + updateThreadId = threadId == nil + } + if updateThreadId { + message = message.withUpdatedThreadId(defaultThreadId) + } + } + + return message.withUpdatedAttributes { attributes in + var attributes = attributes + for i in (0 ..< attributes.count).reversed() { + if attributes[i] is NotificationInfoMessageAttribute || attributes[i] is OutgoingScheduleInfoMessageAttribute { + attributes.remove(at: i) + } + } + if silentPosting { + attributes.append(NotificationInfoMessageAttribute(flags: .muted)) + } + if let scheduleTime { + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime, repeatPeriod: nil)) + } + return attributes + } + } + } + func openMessage(id: EngineMessage.Id) -> Bool { guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { return false @@ -113,17 +222,31 @@ extension PeerInfoScreenNode { } if let mediaReference = mediaReference, let peer = message.peers[message.id.peerId] { + let hasSilentPosting = peer.id != strongSelf.context.account.peerId + let hasSchedule = peer.id.namespace != Namespaces.Peer.SecretChat legacyMediaEditor(context: strongSelf.context, peer: EnginePeer(peer), threadTitle: message.associatedThreadInfo?.title, media: mediaReference, mode: .draw, initialCaption: NSAttributedString(), snapshots: snapshots, transitionCompletion: { transitionCompletion() }, getCaptionPanelView: { return nil - }, sendMessagesWithSignals: { [weak self] signals, _, _, _ in + }, hasSilentPosting: hasSilentPosting, hasSchedule: hasSchedule, reminder: peer.id == strongSelf.context.account.peerId, presentSchedulePicker: { [weak self] _, done in + self?.presentMediaScheduleTimePicker(completion: { time, silentPosting in + done(time, silentPosting) + }) + }, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime, _ in if let strongSelf = self { strongSelf.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(context: strongSelf.context, account: strongSelf.context.account, signals: signals!) |> deliverOnMainQueue).startStrict(next: { [weak self] messages in if let strongSelf = self { - let _ = enqueueMessages(account: strongSelf.context.account, peerId: strongSelf.peerId, messages: messages.map { $0.message.withUpdatedReplyToMessageId(.init(messageId: message.id, quote: nil, innerSubject: nil)) }).startStandalone() + let effectiveScheduleTime = scheduleTime == 0 ? nil : scheduleTime + let mappedMessages = strongSelf.transformEditedMediaMessages(messages.map(\.message), replyToMessageId: message.id, silentPosting: silentPosting, scheduleTime: effectiveScheduleTime) + let _ = (enqueueMessages(account: strongSelf.context.account, peerId: strongSelf.peerId, messages: mappedMessages) + |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in + guard let self, let effectiveScheduleTime, effectiveScheduleTime != scheduleWhenOnlineTimestamp else { + return + } + self.openScheduledMessages() + }) } })) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift index 97065b90f1..77d67c0e4d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift @@ -132,7 +132,6 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, displaySlowmodeTooltip: { _, _ in }, displaySendMessageOptions: { _, _ in }, openScheduledMessages: { - }, openPeersNearby: { }, displaySearchResultsTooltip: { _, _ in }, unarchivePeer: { }, scrollToTop: { diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 1d21f6dc9a..8332586724 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -792,7 +792,6 @@ final class PeerSelectionControllerNode: ASDisplayNode { strongSelf.presentInGlobalOverlay(controller, nil) }) }, openScheduledMessages: { - }, openPeersNearby: { }, displaySearchResultsTooltip: { _, _ in }, unarchivePeer: { }, scrollToTop: { diff --git a/submodules/TelegramUI/Components/ProxyServerPreviewScreen/Sources/ProxyServerPreviewScreen.swift b/submodules/TelegramUI/Components/ProxyServerPreviewScreen/Sources/ProxyServerPreviewScreen.swift index 4d43050f2d..7751f63cfc 100644 --- a/submodules/TelegramUI/Components/ProxyServerPreviewScreen/Sources/ProxyServerPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ProxyServerPreviewScreen/Sources/ProxyServerPreviewScreen.swift @@ -20,22 +20,25 @@ import OverlayStatusController private final class ProxyServerPreviewSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment - let context: AccountContext + let sharedContext: SharedAccountContext + let network: Network let server: ProxyServerSettings let cancel: (Bool) -> Void init( - context: AccountContext, + sharedContext: SharedAccountContext, + network: Network, server: ProxyServerSettings, cancel: @escaping (Bool) -> Void ) { - self.context = context + self.sharedContext = sharedContext + self.network = network self.server = server self.cancel = cancel } static func ==(lhs: ProxyServerPreviewSheetContent, rhs: ProxyServerPreviewSheetContent) -> Bool { - if lhs.context !== rhs.context { + if lhs.sharedContext !== rhs.sharedContext { return false } if lhs.server != rhs.server { @@ -45,7 +48,8 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { } final class State: ComponentState { - private let context: AccountContext + private let sharedContext: SharedAccountContext + private let network: Network private let server: ProxyServerSettings private var disposable = MetaDisposable() @@ -59,8 +63,9 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { private var revertSettings: ProxySettings? - init(context: AccountContext, server: ProxyServerSettings) { - self.context = context + init(sharedContext: SharedAccountContext, network: Network, server: ProxyServerSettings) { + self.sharedContext = sharedContext + self.network = network self.server = server super.init() @@ -71,7 +76,7 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { self.statusDisposable.dispose() if let revertSettings = self.revertSettings { - let _ = updateProxySettingsInteractively(accountManager: self.context.sharedContext.accountManager, { _ in + let _ = updateProxySettingsInteractively(accountManager: self.sharedContext.accountManager, { _ in return revertSettings }) } @@ -91,7 +96,7 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { return } - let statusesContext = ProxyServersStatuses(network: self.context.account.network, servers: .single([self.server])) + let statusesContext = ProxyServersStatuses(network: self.network, servers: .single([self.server])) self.statusesContext = statusesContext self.status = .checking @@ -114,13 +119,13 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { return } - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let presentationData = self.sharedContext.currentPresentationData.with { $0 } self.displayWarningIfNeeded { [weak self] in guard let self else { return } - let accountManager = self.context.sharedContext.accountManager + let accountManager = self.sharedContext.accountManager let proxyServerSettings = self.server let _ = (accountManager.transaction { transaction -> ProxySettings in var currentSettings: ProxySettings? @@ -145,7 +150,7 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { self.inProgress = true self.updated() - let signal = self.context.account.network.connectionStatus + let signal = self.network.connectionStatus |> filter { status in switch status { case let .online(proxyAddress): @@ -181,7 +186,7 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { let _ = updateProxySettingsInteractively(accountManager: accountManager, { _ in return previousSettings }).start() - self.controller?.present(textAlertController(sharedContext: self.context.sharedContext, title: nil, text: presentationData.strings.SocksProxySetup_FailedToConnect, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + self.controller?.present(textAlertController(sharedContext: self.sharedContext, title: nil, text: presentationData.strings.SocksProxySetup_FailedToConnect, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } })) @@ -195,9 +200,9 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { commit() return } - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let presentationData = self.sharedContext.currentPresentationData.with { $0 } let alertController = textAlertController( - context: context, + sharedContext: self.sharedContext, title: presentationData.strings.SocksProxySetup_Warning_Title, text: presentationData.strings.SocksProxySetup_Warning_Text, actions: [ @@ -212,7 +217,7 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, server: self.server) + return State(sharedContext: self.sharedContext, network: self.network, server: self.server) } static var body: Body { @@ -414,19 +419,22 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { private final class ProxyServerPreviewSheetComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment - let context: AccountContext + let sharedContext: SharedAccountContext + let network: Network let server: ProxyServerSettings init( - context: AccountContext, + sharedContext: SharedAccountContext, + network: Network, server: ProxyServerSettings ) { - self.context = context + self.sharedContext = sharedContext + self.network = network self.server = server } static func ==(lhs: ProxyServerPreviewSheetComponent, rhs: ProxyServerPreviewSheetComponent) -> Bool { - if lhs.context !== rhs.context { + if lhs.sharedContext !== rhs.sharedContext { return false } if lhs.server != rhs.server { @@ -446,7 +454,8 @@ private final class ProxyServerPreviewSheetComponent: CombinedComponent { let sheet = sheet.update( component: SheetComponent( content: AnyComponent(ProxyServerPreviewSheetContent( - context: context.component.context, + sharedContext: context.component.sharedContext, + network: context.component.network, server: context.component.server, cancel: { animate in if animate { @@ -504,18 +513,15 @@ private final class ProxyServerPreviewSheetComponent: CombinedComponent { } public class ProxyServerPreviewScreen: ViewControllerComponentContainer { - private let context: AccountContext - public init( context: AccountContext, server: ProxyServerSettings ) { - self.context = context - super.init( context: context, component: ProxyServerPreviewSheetComponent( - context: context, + sharedContext: context.sharedContext, + network: context.account.network, server: server ), navigationBarAppearance: .none, @@ -526,6 +532,28 @@ public class ProxyServerPreviewScreen: ViewControllerComponentContainer { self.navigationPresentation = .flatModal } + public init( + sharedContext: SharedAccountContext, + network: Network, + updatedPresentationData: (initial: PresentationData, signal: Signal), + server: ProxyServerSettings + ) { + super.init( + component: ProxyServerPreviewSheetComponent( + sharedContext: sharedContext, + network: network, + server: server + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + presentationMode: .default, + theme: .default, + updatedPresentationData: updatedPresentationData + ) + + self.navigationPresentation = .flatModal + } + required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift b/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift index 86a9eb018f..10b8e29b91 100644 --- a/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift +++ b/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift @@ -892,6 +892,7 @@ private extension MediaEditorValues { videoVolume: 1.0, additionalVideoPath: nil, additionalVideoIsDual: false, + additionalVideoMirroringChanges: [], additionalVideoPosition: nil, additionalVideoScale: nil, additionalVideoRotation: nil, @@ -1041,6 +1042,7 @@ private extension MediaEditorValues { videoVolume: 1.0, additionalVideoPath: nil, additionalVideoIsDual: false, + additionalVideoMirroringChanges: [], additionalVideoPosition: nil, additionalVideoScale: nil, additionalVideoRotation: nil, diff --git a/submodules/TelegramUI/Components/SegmentControlComponent/Sources/SegmentControlComponent.swift b/submodules/TelegramUI/Components/SegmentControlComponent/Sources/SegmentControlComponent.swift index 012de5aafb..f6bd7fe4e9 100644 --- a/submodules/TelegramUI/Components/SegmentControlComponent/Sources/SegmentControlComponent.swift +++ b/submodules/TelegramUI/Components/SegmentControlComponent/Sources/SegmentControlComponent.swift @@ -8,6 +8,57 @@ import TelegramPresentationData import SegmentedControlNode public final class SegmentControlComponent: Component { + public final class Theme: Equatable { + public let backgroundColor: UIColor + public let legacyBackgroundColor: UIColor + public let foregroundColor: UIColor + public let textColor: UIColor + public let dividerColor: UIColor + + public init( + backgroundColor: UIColor, + legacyBackgroundColor: UIColor, + foregroundColor: UIColor, + textColor: UIColor, + dividerColor: UIColor + ) { + self.backgroundColor = backgroundColor + self.legacyBackgroundColor = legacyBackgroundColor + self.foregroundColor = foregroundColor + self.textColor = textColor + self.dividerColor = dividerColor + } + + public convenience init(theme: PresentationTheme) { + self.init( + backgroundColor: theme.rootController.navigationBar.segmentedBackgroundColor, + legacyBackgroundColor: theme.overallDarkAppearance ? theme.list.itemBlocksBackgroundColor : theme.rootController.navigationBar.segmentedBackgroundColor, + foregroundColor: theme.rootController.navigationBar.segmentedForegroundColor, + textColor: theme.rootController.navigationBar.segmentedTextColor, + dividerColor: theme.rootController.navigationBar.segmentedDividerColor + ) + } + + public static func ==(lhs: Theme, rhs: Theme) -> Bool { + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.legacyBackgroundColor != rhs.legacyBackgroundColor { + return false + } + if lhs.foregroundColor != rhs.foregroundColor { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + if lhs.dividerColor != rhs.dividerColor { + return false + } + return true + } + } + public struct Item: Equatable { var id: AnyHashable var title: String @@ -18,13 +69,13 @@ public final class SegmentControlComponent: Component { } } - let theme: PresentationTheme + let theme: Theme let items: [Item] let selectedId: AnyHashable? let action: (AnyHashable) -> Void - + public init( - theme: PresentationTheme, + theme: Theme, items: [Item], selectedId: AnyHashable?, action: @escaping (AnyHashable) -> Void @@ -34,9 +85,23 @@ public final class SegmentControlComponent: Component { self.selectedId = selectedId self.action = action } + + public convenience init( + theme: PresentationTheme, + items: [Item], + selectedId: AnyHashable?, + action: @escaping (AnyHashable) -> Void + ) { + self.init( + theme: Theme(theme: theme), + items: items, + selectedId: selectedId, + action: action + ) + } public static func ==(lhs: SegmentControlComponent, rhs: SegmentControlComponent) -> Bool { - if lhs.theme !== rhs.theme { + if lhs.theme != rhs.theme { return false } if lhs.items != rhs.items { @@ -102,7 +167,7 @@ public final class SegmentControlComponent: Component { } func update(component: SegmentControlComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - let themeUpdated = self.component?.theme !== component.theme + let themeUpdated = self.component?.theme != component.theme self.component = component @@ -127,13 +192,12 @@ public final class SegmentControlComponent: Component { } if themeUpdated { - let backgroundColor = component.theme.rootController.navigationBar.segmentedBackgroundColor segmentedView.setTitleTextAttributes([ .font: Font.semibold(14.0), - .foregroundColor: component.theme.rootController.navigationBar.segmentedTextColor + .foregroundColor: component.theme.textColor ], for: .normal) - segmentedView.foregroundColor = component.theme.rootController.navigationBar.segmentedForegroundColor - segmentedView.backgroundColor = backgroundColor + segmentedView.foregroundColor = component.theme.foregroundColor + segmentedView.backgroundColor = component.theme.backgroundColor } controlSize = segmentedView.sizeThatFits(availableSize) @@ -146,16 +210,14 @@ public final class SegmentControlComponent: Component { segmentedNode = current if themeUpdated { - let backgroundColor = component.theme.overallDarkAppearance ? component.theme.list.itemBlocksBackgroundColor : component.theme.rootController.navigationBar.segmentedBackgroundColor - let controlTheme = SegmentedControlTheme(backgroundColor: backgroundColor, foregroundColor: component.theme.rootController.navigationBar.segmentedForegroundColor, shadowColor: .clear, textColor: component.theme.rootController.navigationBar.segmentedTextColor, dividerColor: component.theme.rootController.navigationBar.segmentedDividerColor) + let controlTheme = SegmentedControlTheme(backgroundColor: component.theme.legacyBackgroundColor, foregroundColor: component.theme.foregroundColor, shadowColor: .clear, textColor: component.theme.textColor, dividerColor: component.theme.dividerColor) segmentedNode.updateTheme(controlTheme) } } else { let mappedItems: [SegmentedControlItem] = component.items.map { item -> SegmentedControlItem in return SegmentedControlItem(title: item.title) } - let backgroundColor = component.theme.overallDarkAppearance ? component.theme.list.itemBlocksBackgroundColor : component.theme.rootController.navigationBar.segmentedBackgroundColor - let controlTheme = SegmentedControlTheme(backgroundColor: backgroundColor, foregroundColor: component.theme.rootController.navigationBar.segmentedForegroundColor, shadowColor: .clear, textColor: component.theme.rootController.navigationBar.segmentedTextColor, dividerColor: component.theme.rootController.navigationBar.segmentedDividerColor) + let controlTheme = SegmentedControlTheme(backgroundColor: component.theme.legacyBackgroundColor, foregroundColor: component.theme.foregroundColor, shadowColor: .clear, textColor: component.theme.textColor, dividerColor: component.theme.dividerColor) segmentedNode = SegmentedControlNode(theme: controlTheme, items: mappedItems, selectedIndex: component.items.firstIndex(where: { $0.id == component.selectedId }) ?? 0, cornerRadius: 18.0) self.legacySegmentedNode = segmentedNode self.addSubnode(segmentedNode) diff --git a/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD b/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD index 5dd601dc55..71952a3eca 100644 --- a/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD +++ b/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD @@ -24,6 +24,10 @@ swift_library( "//submodules/TelegramStringFormatting", "//submodules/PresentationDataUtils", "//submodules/Components/SolidRoundedButtonComponent", + "//submodules/Components/ResizableSheetComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/AvatarNode", "//submodules/CheckNode", "//submodules/Markdown", diff --git a/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift index 04691de605..10c8b85c0d 100644 --- a/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift +++ b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift @@ -1,16 +1,13 @@ import Foundation import UIKit import Display -import AsyncDisplayKit import ComponentFlow import SwiftSignalKit import ViewControllerComponent -import ComponentDisplayAdapters import TelegramPresentationData import AccountContext import TelegramCore -import MultilineTextComponent -import SolidRoundedButtonComponent +import ResizableSheetComponent import PresentationDataUtils import Markdown import UndoUI @@ -18,31 +15,65 @@ import AnimatedAvatarSetNode import AvatarNode import TelegramStringFormatting import ChatMessagePaymentAlertController +import ResizableSheetComponent +import ButtonComponent +import BundleIconComponent +import GlassBarButtonComponent +import MultilineTextComponent +import SolidRoundedButtonComponent -private final class SendInviteLinkScreenComponent: Component { +private func sendInviteLinkHasInviteSection(subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer]) -> Bool { + let premiumRestrictedUsers = peers.filter { peer in + return peer.canInviteWithPremium + } + + switch subject { + case let .chat(_, link): + if premiumRestrictedUsers.count == peers.count && link == nil { + return false + } else if link != nil && !premiumRestrictedUsers.isEmpty && peers.allSatisfy({ $0.premiumRequiredToContact }) { + return false + } else { + return true + } + case .groupCall: + return true + } +} + +private final class SendInviteLinkContentComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let subject: SendInviteLinkScreenSubject let peers: [TelegramForbiddenInvitePeer] let peerPresences: [EnginePeer.Id: EnginePeer.Presence] - let sendPaidMessageStars: [EnginePeer.Id: StarsAmount] + let selectedItems: Set + let theme: PresentationTheme + let toggleSelection: (EnginePeer.Id) -> Void + let openPremium: () -> Void init( context: AccountContext, subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer], peerPresences: [EnginePeer.Id: EnginePeer.Presence], - sendPaidMessageStars: [EnginePeer.Id: StarsAmount] + selectedItems: Set, + theme: PresentationTheme, + toggleSelection: @escaping (EnginePeer.Id) -> Void, + openPremium: @escaping () -> Void ) { self.context = context self.subject = subject self.peers = peers self.peerPresences = peerPresences - self.sendPaidMessageStars = sendPaidMessageStars + self.selectedItems = selectedItems + self.theme = theme + self.toggleSelection = toggleSelection + self.openPremium = openPremium } - static func ==(lhs: SendInviteLinkScreenComponent, rhs: SendInviteLinkScreenComponent) -> Bool { + static func ==(lhs: SendInviteLinkContentComponent, rhs: SendInviteLinkContentComponent) -> Bool { if lhs.context !== rhs.context { return false } @@ -52,40 +83,16 @@ private final class SendInviteLinkScreenComponent: Component { if lhs.peerPresences != rhs.peerPresences { return false } - if lhs.sendPaidMessageStars != rhs.sendPaidMessageStars { + if lhs.selectedItems != rhs.selectedItems { + return false + } + if lhs.theme !== rhs.theme { return false } return true } - private struct ItemLayout: Equatable { - var containerSize: CGSize - var containerInset: CGFloat - var bottomInset: CGFloat - var topInset: CGFloat - - init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { - self.containerSize = containerSize - self.containerInset = containerInset - self.bottomInset = bottomInset - self.topInset = topInset - } - } - - private final class ScrollView: UIScrollView { - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - return super.hitTest(point, with: event) - } - } - - final class View: UIView, UIScrollViewDelegate { - private let dimView: UIView - private let backgroundLayer: SimpleLayer - private let navigationBarContainer: SparseContainerView - private let scrollView: ScrollView - private let scrollContentClippingView: SparseContainerView - private let scrollContentView: UIView - + final class View: UIView { private var avatarsNode: AnimatedAvatarSetNode? private let avatarsContext = AnimatedAvatarSetContext() @@ -96,275 +103,46 @@ private final class SendInviteLinkScreenComponent: Component { private var premiumSeparatorRight: SimpleLayer? private var premiumSeparatorText: ComponentView? - private let leftButton = ComponentView() - private var title: ComponentView? private var descriptionText: ComponentView? - private var actionButton: ComponentView? private let itemContainerView: UIView private var items: [AnyHashable: ComponentView] = [:] - private var selectedItems = Set() - - private let bottomOverscrollLimit: CGFloat - - private var ignoreScrolling: Bool = false - - private var component: SendInviteLinkScreenComponent? + private var component: SendInviteLinkContentComponent? private weak var state: EmptyComponentState? - private var environment: ViewControllerComponentContainer.Environment? - private var itemLayout: ItemLayout? - - private var topOffsetDistance: CGFloat? - - private var createCallDisposable: Disposable? - private var isInProgress: Bool = false override init(frame: CGRect) { - self.bottomOverscrollLimit = 200.0 - - self.dimView = UIView() - - self.backgroundLayer = SimpleLayer() - self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - self.backgroundLayer.cornerRadius = 10.0 - - self.navigationBarContainer = SparseContainerView() - - self.scrollView = ScrollView() - - self.scrollContentClippingView = SparseContainerView() - self.scrollContentClippingView.clipsToBounds = true - - self.scrollContentView = UIView() - self.itemContainerView = UIView() self.itemContainerView.clipsToBounds = true - self.itemContainerView.layer.cornerRadius = 10.0 + self.itemContainerView.layer.cornerRadius = 26.0 super.init(frame: frame) - self.addSubview(self.dimView) - self.layer.addSublayer(self.backgroundLayer) - - self.addSubview(self.navigationBarContainer) - - self.scrollView.delaysContentTouches = true - self.scrollView.canCancelContentTouches = true - self.scrollView.clipsToBounds = false - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.scrollView.contentInsetAdjustmentBehavior = .never - } - if #available(iOS 13.0, *) { - self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false - } - self.scrollView.showsVerticalScrollIndicator = false - self.scrollView.showsHorizontalScrollIndicator = false - self.scrollView.alwaysBounceHorizontal = false - self.scrollView.alwaysBounceVertical = true - self.scrollView.scrollsToTop = false - self.scrollView.delegate = self - self.scrollView.clipsToBounds = true - - self.addSubview(self.scrollContentClippingView) - self.scrollContentClippingView.addSubview(self.scrollView) - - self.scrollView.addSubview(self.scrollContentView) - - self.scrollContentView.addSubview(self.itemContainerView) - - self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + self.addSubview(self.itemContainerView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - deinit { - self.createCallDisposable?.dispose() - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - if !self.ignoreScrolling { - self.updateScrolling(transition: .immediate) - } - } - - func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { - guard let itemLayout = self.itemLayout, let topOffsetDistance = self.topOffsetDistance else { - return - } - - var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset - topOffset = max(0.0, topOffset) - - if topOffset < topOffsetDistance { - targetContentOffset.pointee.y = scrollView.contentOffset.y - scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true) - } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.bounds.contains(point) { - return nil - } - if !self.backgroundLayer.frame.contains(point) { - return self.dimView - } - - if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { - return result - } - - let result = super.hitTest(point, with: event) - return result - } - - @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - guard let environment = self.environment, let controller = environment.controller() else { - return - } - controller.dismiss() - } - } - - private func updateScrolling(transition: ComponentTransition) { - guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { - return - } - var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset - topOffset = max(0.0, topOffset) - transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) - - transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) - - let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) - self.topOffsetDistance = topOffsetDistance - var topOffsetFraction = topOffset / topOffsetDistance - topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) - - let transitionFactor: CGFloat = 1.0 - topOffsetFraction - controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) - } - - func animateIn() { - self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY - self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - if let actionButtonView = self.actionButton?.view { - actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } - } - - func animateOut(completion: @escaping () -> Void) { - let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY - - self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in - completion() - }) - self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - if let actionButtonView = self.actionButton?.view { - actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - } - } - - private func presentPaidMessageAlertIfNeeded(peers: [EngineRenderedPeer], requiresStars: [EnginePeer.Id: StarsAmount], completion: @escaping () -> Void) { - guard let component = self.component else { - completion() - return - } - var totalAmount: StarsAmount = .zero - for peer in peers { - if let amount = requiresStars[peer.peerId] { - totalAmount = totalAmount + amount - } - } - if totalAmount.value > 0 { - let controller = chatMessagePaymentAlertController( - context: component.context, - presentationData: component.context.sharedContext.currentPresentationData.with { $0 }, - updatedPresentationData: nil, - peers: peers, - count: 1, - amount: totalAmount, - totalAmount: totalAmount, - hasCheck: false, - navigationController: self.environment?.controller()?.navigationController as? NavigationController, - completion: { _ in - completion() - } - ) - self.environment?.controller()?.present(controller, in: .window(.root)) - } else { - completion() - } - } - - func update(component: SendInviteLinkScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - let environment = environment[ViewControllerComponentContainer.Environment.self].value - let themeUpdated = self.environment?.theme !== environment.theme - - let resetScrolling = self.scrollView.bounds.width != availableSize.width - - let sideInset: CGFloat = 16.0 - - if self.component == nil { - for peer in component.peers { - switch component.subject { - case let .chat(_, link): - if link != nil && !peer.premiumRequiredToContact { - self.selectedItems.insert(peer.peer.id) - } - case .groupCall: - self.selectedItems.insert(peer.peer.id) - } - } - } + func update(component: SendInviteLinkContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let environment = environment[EnvironmentType.self].value + let theme = component.theme self.component = component self.state = state - self.environment = environment + + let sideInset: CGFloat = 16.0 + self.itemContainerView.backgroundColor = theme.list.itemBlocksBackgroundColor let premiumRestrictedUsers = component.peers.filter { peer in return peer.canInviteWithPremium } - var hasInviteLink = true - switch component.subject { - case let .chat(_, link): - if premiumRestrictedUsers.count == component.peers.count && link == nil { - hasInviteLink = false - } else if link != nil && !premiumRestrictedUsers.isEmpty && component.peers.allSatisfy({ $0.premiumRequiredToContact }) { - hasInviteLink = false - } - case .groupCall: - hasInviteLink = true - } - - if themeUpdated { - self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor - self.itemContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor - - var locations: [NSNumber] = [] - var colors: [CGColor] = [] - let numStops = 6 - for i in 0 ..< numStops { - let step = CGFloat(i) / CGFloat(numStops - 1) - locations.append(step as NSNumber) - colors.append(environment.theme.list.blocksBackgroundColor.withAlphaComponent(1.0 - step * step).cgColor) - } - } - - transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + let hasInviteSection = sendInviteLinkHasInviteSection(subject: component.subject, peers: component.peers) var contentHeight: CGFloat = 0.0 - contentHeight += 102.0 + contentHeight += 120.0 let avatarsNode: AnimatedAvatarSetNode if let current = self.avatarsNode { @@ -372,7 +150,7 @@ private final class SendInviteLinkScreenComponent: Component { } else { avatarsNode = AnimatedAvatarSetNode() self.avatarsNode = avatarsNode - self.scrollContentView.addSubview(avatarsNode.view) + self.addSubview(avatarsNode.view) } let avatarPeers: [EnginePeer] @@ -385,37 +163,15 @@ private final class SendInviteLinkScreenComponent: Component { let avatarsSize = avatarsNode.update( context: component.context, content: avatarsContent, - itemSize: CGSize(width: 60.0, height: 60.0), + itemSize: CGSize(width: 64.0, height: 64.0), customSpacing: 30.0, font: avatarPlaceholderFont(size: 28.0), animated: false, synchronousLoad: true ) - let avatarsFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - avatarsSize.width) * 0.5), y: 26.0), size: avatarsSize) + let avatarsFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - avatarsSize.width) * 0.5), y: 48.0), size: avatarsSize) transition.setFrame(view: avatarsNode.view, frame: avatarsFrame) - let leftButtonSize = self.leftButton.update( - transition: transition, - component: AnyComponent(Button( - content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.list.itemAccentColor)), - action: { [weak self] in - guard let self, let controller = self.environment?.controller() else { - return - } - controller.dismiss() - } - ).minSize(CGSize(width: 44.0, height: 56.0))), - environment: {}, - containerSize: CGSize(width: 120.0, height: 100.0) - ) - let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: leftButtonSize) - if let leftButtonView = self.leftButton.view { - if leftButtonView.superview == nil { - self.navigationBarContainer.addSubview(leftButtonView) - } - transition.setFrame(view: leftButtonView, frame: leftButtonFrame) - } - if !premiumRestrictedUsers.isEmpty { var premiumItemsTransition = transition @@ -447,15 +203,15 @@ private final class SendInviteLinkScreenComponent: Component { let premiumTitleSize = premiumTitle.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.SendInviteLink_TitleUpgradeToPremium, font: Font.semibold(24.0), textColor: environment.theme.list.itemPrimaryTextColor)) + text: .plain(NSAttributedString(string: environment.strings.SendInviteLink_TitleUpgradeToPremium, font: Font.semibold(24.0), textColor: theme.list.itemPrimaryTextColor)) )), environment: {}, - containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) let premiumTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - premiumTitleSize.width) * 0.5), y: contentHeight), size: premiumTitleSize) if let premiumTitleView = premiumTitle.view { if premiumTitleView.superview == nil { - self.scrollContentView.addSubview(premiumTitleView) + self.addSubview(premiumTitleView) } transition.setFrame(view: premiumTitleView, frame: premiumTitleFrame) } @@ -474,7 +230,6 @@ private final class SendInviteLinkScreenComponent: Component { } } else { let extraCount = premiumRestrictedUsers.count - 3 - var peersTextArray: [String] = [] for i in 0 ..< min(3, premiumRestrictedUsers.count) { peersTextArray.append("**\(premiumRestrictedUsers[i].peer.compactDisplayTitle)**") @@ -516,7 +271,6 @@ private final class SendInviteLinkScreenComponent: Component { text = environment.strings.SendInviteLink_TextCallsRestrictedOneUser(premiumRestrictedUsers[0].peer.compactDisplayTitle).string } else { let extraCount = premiumRestrictedUsers.count - 3 - var peersTextArray: [String] = [] for i in 0 ..< min(3, premiumRestrictedUsers.count) { peersTextArray.append("**\(premiumRestrictedUsers[i].peer.compactDisplayTitle)**") @@ -547,8 +301,8 @@ private final class SendInviteLinkScreenComponent: Component { } } - let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor) - let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor) + let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: theme.list.itemPrimaryTextColor) + let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: theme.list.itemPrimaryTextColor) let premiumTextSize = premiumText.update( transition: .immediate, @@ -568,7 +322,7 @@ private final class SendInviteLinkScreenComponent: Component { let premiumTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - premiumTextSize.width) * 0.5), y: contentHeight), size: premiumTextSize) if let premiumTextView = premiumText.view { if premiumTextView.superview == nil { - self.scrollContentView.addSubview(premiumTextView) + self.addSubview(premiumTextView) } transition.setFrame(view: premiumTextView, frame: premiumTextFrame) } @@ -576,11 +330,10 @@ private final class SendInviteLinkScreenComponent: Component { contentHeight += premiumTextSize.height contentHeight += 22.0 - let premiumButtonTitle = environment.strings.SendInviteLink_SubscribeToPremiumButton let premiumButtonSize = premiumButton.update( - transition: transition, + transition: premiumItemsTransition, component: AnyComponent(SolidRoundedButtonComponent( - title: premiumButtonTitle, + title: environment.strings.SendInviteLink_SubscribeToPremiumButton, badge: nil, theme: SolidRoundedButtonComponent.Theme( backgroundColor: .black, @@ -600,33 +353,23 @@ private final class SendInviteLinkScreenComponent: Component { animationName: nil, iconPosition: .right, iconSpacing: 4.0, - action: { [weak self] in - guard let self, let component = self.component, let controller = self.environment?.controller() else { - return - } - - let navigationController = controller.navigationController as? NavigationController - - controller.dismiss() - - let premiumController = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .settings, forceDark: false, dismissed: nil) - navigationController?.pushViewController(premiumController) + action: { + component.openPremium() } )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) ) - let premiumButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: premiumButtonSize) if let premiumButtonView = premiumButton.view { if premiumButtonView.superview == nil { - self.scrollContentView.addSubview(premiumButtonView) + self.addSubview(premiumButtonView) } - transition.setFrame(view: premiumButtonView, frame: premiumButtonFrame) + premiumItemsTransition.setFrame(view: premiumButtonView, frame: premiumButtonFrame) } contentHeight += premiumButtonSize.height - if hasInviteLink { + if hasInviteSection { let premiumSeparatorText: ComponentView if let current = self.premiumSeparatorText { premiumSeparatorText = current @@ -641,7 +384,7 @@ private final class SendInviteLinkScreenComponent: Component { } else { premiumSeparatorLeft = SimpleLayer() self.premiumSeparatorLeft = premiumSeparatorLeft - self.scrollContentView.layer.addSublayer(premiumSeparatorLeft) + self.layer.addSublayer(premiumSeparatorLeft) } let premiumSeparatorRight: SimpleLayer @@ -650,33 +393,32 @@ private final class SendInviteLinkScreenComponent: Component { } else { premiumSeparatorRight = SimpleLayer() self.premiumSeparatorRight = premiumSeparatorRight - self.scrollContentView.layer.addSublayer(premiumSeparatorRight) + self.layer.addSublayer(premiumSeparatorRight) } - premiumSeparatorLeft.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor - premiumSeparatorRight.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor + premiumSeparatorLeft.backgroundColor = theme.list.itemPlainSeparatorColor.cgColor + premiumSeparatorRight.backgroundColor = theme.list.itemPlainSeparatorColor.cgColor contentHeight += 19.0 let premiumSeparatorTextSize = premiumSeparatorText.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.SendInviteLink_PremiumOrSendSectionSeparator, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor)) + text: .plain(NSAttributedString(string: environment.strings.SendInviteLink_PremiumOrSendSectionSeparator, font: Font.regular(15.0), textColor: theme.list.itemSecondaryTextColor)) )), environment: {}, - containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) let premiumSeparatorTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - premiumSeparatorTextSize.width) * 0.5), y: contentHeight), size: premiumSeparatorTextSize) if let premiumSeparatorTextView = premiumSeparatorText.view { if premiumSeparatorTextView.superview == nil { - self.scrollContentView.addSubview(premiumSeparatorTextView) + self.addSubview(premiumSeparatorTextView) } transition.setFrame(view: premiumSeparatorTextView, frame: premiumSeparatorTextFrame) } let separatorWidth: CGFloat = 72.0 let separatorSpacing: CGFloat = 10.0 - transition.setFrame(layer: premiumSeparatorLeft, frame: CGRect(origin: CGPoint(x: premiumSeparatorTextFrame.minX - separatorSpacing - separatorWidth, y: premiumSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel))) transition.setFrame(layer: premiumSeparatorRight, frame: CGRect(origin: CGPoint(x: premiumSeparatorTextFrame.maxX + separatorSpacing, y: premiumSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel))) @@ -712,12 +454,7 @@ private final class SendInviteLinkScreenComponent: Component { } } - let containerInset: CGFloat = environment.statusBarHeight + 10.0 - - var initialContentHeight = contentHeight - let clippingY: CGFloat - - if hasInviteLink { + if hasInviteSection { let title: ComponentView if let current = self.title { title = current @@ -734,14 +471,6 @@ private final class SendInviteLinkScreenComponent: Component { self.descriptionText = descriptionText } - let actionButton: ComponentView - if let current = self.actionButton { - actionButton = current - } else { - actionButton = ComponentView() - self.actionButton = actionButton - } - let titleText: String switch component.subject { case let .chat(_, link): @@ -753,15 +482,15 @@ private final class SendInviteLinkScreenComponent: Component { let titleSize = title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: titleText, font: Font.semibold(24.0), textColor: environment.theme.list.itemPrimaryTextColor)) + text: .plain(NSAttributedString(string: titleText, font: Font.semibold(24.0), textColor: theme.list.itemPrimaryTextColor)) )), environment: {}, - containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize) if let titleView = title.view { if titleView.superview == nil { - self.scrollContentView.addSubview(titleView) + self.addSubview(titleView) } transition.setFrame(view: titleView, frame: titleFrame) } @@ -775,27 +504,21 @@ private final class SendInviteLinkScreenComponent: Component { if !premiumRestrictedUsers.isEmpty { if link != nil { text = environment.strings.SendInviteLink_TextSendInviteLink + } else if component.peers.count == 1 { + text = environment.strings.SendInviteLink_TextUnavailableSingleUser(component.peers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)).string } else { - if component.peers.count == 1 { - text = environment.strings.SendInviteLink_TextUnavailableSingleUser(component.peers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)).string - } else { - text = environment.strings.SendInviteLink_TextUnavailableMultipleUsers(Int32(component.peers.count)) - } + text = environment.strings.SendInviteLink_TextUnavailableMultipleUsers(Int32(component.peers.count)) } + } else if link != nil { + if component.peers.count == 1 { + text = environment.strings.SendInviteLink_TextAvailableSingleUser(component.peers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)).string + } else { + text = environment.strings.SendInviteLink_TextAvailableMultipleUsers(Int32(component.peers.count)) + } + } else if component.peers.count == 1 { + text = environment.strings.SendInviteLink_TextUnavailableSingleUser(component.peers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)).string } else { - if link != nil { - if component.peers.count == 1 { - text = environment.strings.SendInviteLink_TextAvailableSingleUser(component.peers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)).string - } else { - text = environment.strings.SendInviteLink_TextAvailableMultipleUsers(Int32(component.peers.count)) - } - } else { - if component.peers.count == 1 { - text = environment.strings.SendInviteLink_TextUnavailableSingleUser(component.peers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)).string - } else { - text = environment.strings.SendInviteLink_TextUnavailableMultipleUsers(Int32(component.peers.count)) - } - } + text = environment.strings.SendInviteLink_TextUnavailableMultipleUsers(Int32(component.peers.count)) } case let .groupCall(groupCall): switch groupCall { @@ -811,8 +534,8 @@ private final class SendInviteLinkScreenComponent: Component { } } - let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor) - let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor) + let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: theme.list.itemPrimaryTextColor) + let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: theme.list.itemPrimaryTextColor) let descriptionTextSize = descriptionText.update( transition: .immediate, @@ -824,7 +547,8 @@ private final class SendInviteLinkScreenComponent: Component { linkAttribute: { _ in nil } )), horizontalAlignment: .center, - maximumNumberOfLines: 0 + maximumNumberOfLines: 0, + lineSpacing: 0.2 )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0) @@ -832,96 +556,80 @@ private final class SendInviteLinkScreenComponent: Component { let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize) if let descriptionTextView = descriptionText.view { if descriptionTextView.superview == nil { - self.scrollContentView.addSubview(descriptionTextView) + self.addSubview(descriptionTextView) } transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame) } contentHeight += descriptionTextFrame.height contentHeight += 22.0 - initialContentHeight = contentHeight - - var singleItemHeight: CGFloat = 0.0 var itemsHeight: CGFloat = 0.0 var validIds: [AnyHashable] = [] if case .chat = component.subject { for i in 0 ..< component.peers.count { let peer = component.peers[i] + let id = AnyHashable(peer.peer.id) + validIds.append(id) - for _ in 0 ..< 1 { - //let id: AnyHashable = AnyHashable("\(peer.id)_\(j)") - let id = AnyHashable(peer.peer.id) - validIds.append(id) - - let item: ComponentView - var itemTransition = transition - if let current = self.items[id] { - item = current - } else { - itemTransition = .immediate - item = ComponentView() - self.items[id] = item - } - - let itemSubtitle: PeerListItemComponent.Subtitle - let canBeSelected : Bool - switch component.subject { - case let .chat(_, link): - canBeSelected = link != nil && !peer.premiumRequiredToContact - case .groupCall: - canBeSelected = true - } - if peer.premiumRequiredToContact { - itemSubtitle = .text(text: environment.strings.SendInviteLink_StatusAvailableToPremiumOnly, icon: .lock) - } else { - itemSubtitle = .presence(component.peerPresences[peer.peer.id]) - } - - let itemSize = item.update( - transition: itemTransition, - component: AnyComponent(PeerListItemComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - sideInset: 0.0, - title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), - subtitle: itemSubtitle, - peer: peer.peer, - selectionState: !canBeSelected ? .none : .editing(isSelected: self.selectedItems.contains(peer.peer.id)), - hasNext: i != component.peers.count - 1, - action: { [weak self] peer in - guard let self else { - return - } - if !canBeSelected { - return - } - if self.selectedItems.contains(peer.id) { - self.selectedItems.remove(peer.id) - } else { - self.selectedItems.insert(peer.id) - } - self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) - ) - let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) - - if let itemView = item.view { - if itemView.superview == nil { - self.itemContainerView.addSubview(itemView) - } - itemTransition.setFrame(view: itemView, frame: itemFrame) - } - - itemsHeight += itemSize.height - singleItemHeight = itemSize.height + let item: ComponentView + var itemTransition = transition + if let current = self.items[id] { + item = current + } else { + itemTransition = .immediate + item = ComponentView() + self.items[id] = item } + + let canBeSelected: Bool + switch component.subject { + case let .chat(_, link): + canBeSelected = link != nil && !peer.premiumRequiredToContact + case .groupCall: + canBeSelected = true + } + + let itemSubtitle: PeerListItemComponent.Subtitle + if peer.premiumRequiredToContact { + itemSubtitle = .text(text: environment.strings.SendInviteLink_StatusAvailableToPremiumOnly, icon: .lock) + } else { + itemSubtitle = .presence(component.peerPresences[peer.peer.id]) + } + + let itemSize = item.update( + transition: itemTransition, + component: AnyComponent(PeerListItemComponent( + context: component.context, + theme: theme, + strings: environment.strings, + sideInset: 0.0, + title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), + subtitle: itemSubtitle, + peer: peer.peer, + selectionState: !canBeSelected ? .none : .editing(isSelected: component.selectedItems.contains(peer.peer.id)), + hasNext: i != component.peers.count - 1, + action: { peer in + if canBeSelected { + component.toggleSelection(peer.id) + } + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) + if let itemView = item.view { + if itemView.superview == nil { + self.itemContainerView.addSubview(itemView) + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + + itemsHeight += itemSize.height } } + var removeIds: [AnyHashable] = [] for (id, item) in self.items { if !validIds.contains(id) { @@ -932,182 +640,17 @@ private final class SendInviteLinkScreenComponent: Component { for id in removeIds { self.items.removeValue(forKey: id) } - transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: itemsHeight))) - initialContentHeight += min(itemsHeight, floor(singleItemHeight * 2.5)) + transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: itemsHeight))) if itemsHeight != 0.0 { contentHeight += itemsHeight contentHeight += 24.0 - initialContentHeight += 24.0 } else { contentHeight += 4.0 } - let actionButtonTitle: String - let actionButtonBadge: String? - switch component.subject { - case let.chat(_, link): - if link != nil { - actionButtonTitle = self.selectedItems.isEmpty ? environment.strings.SendInviteLink_ActionSkip : environment.strings.SendInviteLink_ActionInvite - } else { - actionButtonTitle = environment.strings.SendInviteLink_ActionClose - } - actionButtonBadge = (self.selectedItems.isEmpty || link == nil) ? nil : "\(self.selectedItems.count)" - case .groupCall: - actionButtonTitle = environment.strings.SendInviteLink_ActionInvite - actionButtonBadge = nil - } - let actionButtonSize = actionButton.update( - transition: transition, - component: AnyComponent(SolidRoundedButtonComponent( - title: actionButtonTitle, - badge: actionButtonBadge, - theme: SolidRoundedButtonComponent.Theme(theme: environment.theme), - font: .bold, - fontSize: 17.0, - height: 50.0, - cornerRadius: 11.0, - gloss: false, - animationName: nil, - iconPosition: .right, - iconSpacing: 4.0, - isLoading: self.isInProgress, - action: { [weak self] in - guard let self, let component = self.component, let controller = self.environment?.controller() else { - return - } - - let link: String? - switch component.subject { - case let .chat(_, linkValue): - link = linkValue - case let .groupCall(groupCall): - switch groupCall { - case .create: - self.isInProgress = true - self.state?.updated(transition: .immediate) - - self.createCallDisposable = (component.context.engine.calls.createConferenceCall() - |> deliverOnMainQueue).startStrict(next: { [weak self] call in - guard let self, let component = self.component, let controller = self.environment?.controller() else { - return - } - - if self.selectedItems.isEmpty { - controller.dismiss() - } else { - let link = call.link - let selectedPeers = component.peers.filter { self.selectedItems.contains($0.peer.id) } - - self.presentPaidMessageAlertIfNeeded( - peers: selectedPeers.map { EngineRenderedPeer(peer: $0.peer) }, - requiresStars: component.sendPaidMessageStars, - completion: { [weak self] in - guard let self, let component = self.component, let controller = self.environment?.controller() else { - return - } - - for peerId in Array(self.selectedItems) { - var messageAttributes: [EngineMessage.Attribute] = [] - if let sendPaidMessageStars = component.sendPaidMessageStars[peerId] { - messageAttributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false)) - } - let _ = enqueueMessages(account: component.context.account, peerId: peerId, messages: [.message(text: link, attributes: messageAttributes, inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).startStandalone() - } - - let text: String - if selectedPeers.count == 1 { - text = environment.strings.Conversation_ShareLinkTooltip_Chat_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string - } else if selectedPeers.count == 2 { - text = environment.strings.Conversation_ShareLinkTooltip_TwoChats_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), selectedPeers[1].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string - } else { - text = environment.strings.Conversation_ShareLinkTooltip_ManyChats_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), "\(selectedPeers.count - 1)").string - } - - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - controller.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: false, text: text), elevatedLayout: false, action: { _ in return false }), in: .window(.root)) - - let navigationController = controller.navigationController as? NavigationController - - let context = component.context - controller.dismiss(completion: { [weak navigationController] in - if let navigationController, let peer = selectedPeers.first?.peer { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams( - navigationController: navigationController, - context: context, - chatLocation: .peer(peer) - )) - } - }) - } - ) - } - }) - - return - case let .existing(linkValue): - link = linkValue - } - } - - if self.selectedItems.isEmpty { - controller.dismiss() - } else if let link { - let selectedPeers = component.peers.filter { self.selectedItems.contains($0.peer.id) } - - self.presentPaidMessageAlertIfNeeded( - peers: selectedPeers.map { EngineRenderedPeer(peer: $0.peer) }, - requiresStars: component.sendPaidMessageStars, - completion: { [weak self] in - guard let self, let component = self.component, let controller = self.environment?.controller() else { - return - } - - for peerId in Array(self.selectedItems) { - var messageAttributes: [EngineMessage.Attribute] = [] - if let sendPaidMessageStars = component.sendPaidMessageStars[peerId] { - messageAttributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false)) - } - let _ = enqueueMessages(account: component.context.account, peerId: peerId, messages: [.message(text: link, attributes: messageAttributes, inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).startStandalone() - } - - let text: String - if selectedPeers.count == 1 { - text = environment.strings.Conversation_ShareLinkTooltip_Chat_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string - } else if selectedPeers.count == 2 { - text = environment.strings.Conversation_ShareLinkTooltip_TwoChats_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), selectedPeers[1].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string - } else { - text = environment.strings.Conversation_ShareLinkTooltip_ManyChats_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), "\(selectedPeers.count - 1)").string - } - - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - controller.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: false, text: text), elevatedLayout: false, action: { _ in return false }), in: .window(.root)) - - controller.dismiss() - } - ) - } else { - controller.dismiss() - } - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) - ) - let bottomPanelHeight = 15.0 + environment.safeInsets.bottom + actionButtonSize.height - let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) - if let actionButtonView = actionButton.view { - if actionButtonView.superview == nil { - self.addSubview(actionButtonView) - } - transition.setFrame(view: actionButtonView, frame: actionButtonFrame) - } - - contentHeight += bottomPanelHeight - initialContentHeight += bottomPanelHeight - - clippingY = actionButtonFrame.minY - 24.0 + contentHeight += 84.0 } else { if let title = self.title { self.title = nil @@ -1117,44 +660,502 @@ private final class SendInviteLinkScreenComponent: Component { self.descriptionText = nil descriptionText.view?.removeFromSuperview() } - if let actionButton = self.actionButton { - self.actionButton = nil - actionButton.view?.removeFromSuperview() + + for (_, item) in self.items { + item.view?.removeFromSuperview() } + self.items.removeAll() + transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: 0.0))) - initialContentHeight += environment.safeInsets.bottom - - clippingY = availableSize.height + contentHeight += 24.0 + environment.safeInsets.bottom } - let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class SendInviteLinkActionButtonComponent: Component { + let theme: PresentationTheme + let title: String + let badge: Int? + let displaysProgress: Bool + let action: () -> Void + + init( + theme: PresentationTheme, + title: String, + badge: Int?, + displaysProgress: Bool, + action: @escaping () -> Void + ) { + self.theme = theme + self.title = title + self.badge = badge + self.displaysProgress = displaysProgress + self.action = action + } + + static func ==(lhs: SendInviteLinkActionButtonComponent, rhs: SendInviteLinkActionButtonComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.badge != rhs.badge { + return false + } + if lhs.displaysProgress != rhs.displaysProgress { + return false + } + return true + } + + final class View: UIView { + private let button = ComponentView() + private var component: SendInviteLinkActionButtonComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: SendInviteLinkActionButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component - let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) - - self.scrollContentClippingView.layer.cornerRadius = 10.0 - - self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset) - - transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) - - transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) - transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) - - let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: clippingY - containerInset)) - transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) - transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) - - self.ignoreScrolling = true - transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) - let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) - if contentSize != self.scrollView.contentSize { - self.scrollView.contentSize = contentSize + let content: AnyComponentWithIdentity + if let badge = component.badge { + content = AnyComponentWithIdentity(id: "badge-\(component.title)-\(badge)", component: AnyComponent(ButtonTextContentComponent( + text: component.title, + badge: badge, + textColor: component.theme.list.itemCheckColors.foregroundColor, + badgeBackground: component.theme.list.itemCheckColors.foregroundColor, + badgeForeground: component.theme.list.itemCheckColors.fillColor + ))) + } else { + content = AnyComponentWithIdentity(id: "title-\(component.title)", component: AnyComponent(Text( + text: component.title, + font: Font.semibold(17.0), + color: component.theme.list.itemCheckColors.foregroundColor + ))) } - if resetScrolling { - self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) + + let buttonSize = self.button.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: component.theme.list.itemCheckColors.fillColor, + foreground: component.theme.list.itemCheckColors.foregroundColor, + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: content, + isEnabled: true, + displaysProgress: component.displaysProgress, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.action() + } + )), + environment: {}, + containerSize: availableSize + ) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: CGRect(origin: .zero, size: buttonSize)) + } + + return buttonSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class SendInviteLinkScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: SendInviteLinkScreenSubject + let peers: [TelegramForbiddenInvitePeer] + let peerPresences: [EnginePeer.Id: EnginePeer.Presence] + let sendPaidMessageStars: [EnginePeer.Id: StarsAmount] + + init( + context: AccountContext, + subject: SendInviteLinkScreenSubject, + peers: [TelegramForbiddenInvitePeer], + peerPresences: [EnginePeer.Id: EnginePeer.Presence], + sendPaidMessageStars: [EnginePeer.Id: StarsAmount] + ) { + self.context = context + self.subject = subject + self.peers = peers + self.peerPresences = peerPresences + self.sendPaidMessageStars = sendPaidMessageStars + } + + static func ==(lhs: SendInviteLinkScreenComponent, rhs: SendInviteLinkScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peers != rhs.peers { + return false + } + if lhs.peerPresences != rhs.peerPresences { + return false + } + if lhs.sendPaidMessageStars != rhs.sendPaidMessageStars { + return false + } + return true + } + + final class View: UIView { + private let sheet = ComponentView<(EnvironmentType, ResizableSheetComponentEnvironment)>() + private let animateOut = ActionSlot>() + + private var selectedItems = Set() + private var didInitializeSelection = false + private var isInProgress = false + private var isDismissing = false + + private var component: SendInviteLinkScreenComponent? + private weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private var createCallDisposable: Disposable? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.createCallDisposable?.dispose() + } + + private func dismiss(controller: @escaping () -> ViewController?, animated: Bool) { + guard !self.isDismissing else { + return + } + self.isDismissing = true + + let performDismiss: () -> Void = { + if let controller = controller() as? SendInviteLinkScreen { + controller.completePendingDismiss() + controller.dismiss(animated: false) + } else { + controller()?.dismiss(animated: false) + } + } + + if animated { + self.animateOut.invoke(Action { _ in + performDismiss() + }) + } else { + performDismiss() + } + } + + private func presentPaidMessageAlertIfNeeded(peers: [EngineRenderedPeer], requiresStars: [EnginePeer.Id: StarsAmount], completion: @escaping () -> Void) { + guard let component = self.component else { + completion() + return + } + var totalAmount: StarsAmount = .zero + for peer in peers { + if let amount = requiresStars[peer.peerId] { + totalAmount = totalAmount + amount + } + } + if totalAmount.value > 0 { + let controller = chatMessagePaymentAlertController( + context: component.context, + presentationData: component.context.sharedContext.currentPresentationData.with { $0 }, + updatedPresentationData: nil, + peers: peers, + count: 1, + amount: totalAmount, + totalAmount: totalAmount, + hasCheck: false, + navigationController: self.environment?.controller()?.navigationController as? NavigationController, + completion: { _ in + completion() + } + ) + self.environment?.controller()?.present(controller, in: .window(.root)) + } else { + completion() + } + } + + private func sendInviteLink(link: String, selectedPeers: [TelegramForbiddenInvitePeer], completion: (() -> Void)? = nil) { + guard let component = self.component, let environment = self.environment else { + return + } + self.presentPaidMessageAlertIfNeeded( + peers: selectedPeers.map { EngineRenderedPeer(peer: $0.peer) }, + requiresStars: component.sendPaidMessageStars, + completion: { [weak self] in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + + for peerId in Array(self.selectedItems) { + var messageAttributes: [EngineMessage.Attribute] = [] + if let sendPaidMessageStars = component.sendPaidMessageStars[peerId] { + messageAttributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false)) + } + let _ = enqueueMessages(account: component.context.account, peerId: peerId, messages: [.message(text: link, attributes: messageAttributes, inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).startStandalone() + } + + let text: String + if selectedPeers.count == 1 { + text = environment.strings.Conversation_ShareLinkTooltip_Chat_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string + } else if selectedPeers.count == 2 { + text = environment.strings.Conversation_ShareLinkTooltip_TwoChats_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), selectedPeers[1].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string + } else { + text = environment.strings.Conversation_ShareLinkTooltip_ManyChats_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), "\(selectedPeers.count - 1)").string + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: false, text: text), elevatedLayout: false, action: { _ in return false }), in: .window(.root)) + + completion?() + } + ) + } + + private func performMainAction() { + guard let component = self.component, let controller = self.environment?.controller() else { + return + } + + let link: String? + switch component.subject { + case let .chat(_, linkValue): + link = linkValue + case let .groupCall(groupCall): + switch groupCall { + case .create: + self.isInProgress = true + self.state?.updated(transition: .immediate) + + self.createCallDisposable = (component.context.engine.calls.createConferenceCall() + |> deliverOnMainQueue).startStrict(next: { [weak self] call in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + + if self.selectedItems.isEmpty { + controller.dismiss() + } else { + let link = call.link + let selectedPeers = component.peers.filter { self.selectedItems.contains($0.peer.id) } + self.sendInviteLink(link: link, selectedPeers: selectedPeers, completion: { [weak self] in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + let navigationController = controller.navigationController as? NavigationController + let context = component.context + controller.dismiss(completion: { [weak navigationController] in + if let navigationController, let peer = selectedPeers.first?.peer { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + context: context, + chatLocation: .peer(peer) + )) + } + }) + }) + } + }) + return + case let .existing(linkValue): + link = linkValue + } + } + + if self.selectedItems.isEmpty { + controller.dismiss() + } else if let link { + let selectedPeers = component.peers.filter { self.selectedItems.contains($0.peer.id) } + self.sendInviteLink(link: link, selectedPeers: selectedPeers, completion: { [weak self] in + self?.environment?.controller()?.dismiss() + }) + } else { + controller.dismiss() + } + } + + func update(component: SendInviteLinkScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + if !self.didInitializeSelection { + self.didInitializeSelection = true + for peer in component.peers { + switch component.subject { + case let .chat(_, link): + if link != nil && !peer.premiumRequiredToContact { + self.selectedItems.insert(peer.peer.id) + } + case .groupCall: + self.selectedItems.insert(peer.peer.id) + } + } + } + + self.component = component + self.state = state + + let environmentValue = environment[EnvironmentType.self].value + self.environment = environmentValue + let controller = environmentValue.controller + let theme = environmentValue.theme.withModalBlocksBackground() + let hasInviteSection = sendInviteLinkHasInviteSection(subject: component.subject, peers: component.peers) + + let dismiss: (Bool) -> Void = { [weak self] animated in + self?.dismiss(controller: controller, animated: animated) + } + + let actionTitle: String + let actionBadge: Int? + switch component.subject { + case let .chat(_, link): + if link != nil { + actionTitle = self.selectedItems.isEmpty ? environmentValue.strings.SendInviteLink_ActionSkip : environmentValue.strings.SendInviteLink_ActionInvite + } else { + actionTitle = environmentValue.strings.SendInviteLink_ActionClose + } + actionBadge = (self.selectedItems.isEmpty || link == nil) ? nil : self.selectedItems.count + case .groupCall: + actionTitle = environmentValue.strings.SendInviteLink_ActionInvite + actionBadge = nil + } + + let bottomItem: AnyComponent? + if hasInviteSection { + bottomItem = AnyComponent(SendInviteLinkActionButtonComponent( + theme: theme, + title: actionTitle, + badge: actionBadge, + displaysProgress: self.isInProgress, + action: { [weak self] in + self?.performMainAction() + } + )) + } else { + bottomItem = nil + } + + let sheetSize = self.sheet.update( + transition: transition, + component: AnyComponent(ResizableSheetComponent( + content: AnyComponent(SendInviteLinkContentComponent( + context: component.context, + subject: component.subject, + peers: component.peers, + peerPresences: component.peerPresences, + selectedItems: self.selectedItems, + theme: theme, + toggleSelection: { [weak self] peerId in + guard let self else { + return + } + if self.selectedItems.contains(peerId) { + self.selectedItems.remove(peerId) + } else { + self.selectedItems.insert(peerId) + } + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) + }, + openPremium: { [weak self] in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + let navigationController = controller.navigationController as? NavigationController + controller.dismiss() + let premiumController = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .settings, forceDark: false, dismissed: nil) + navigationController?.pushViewController(premiumController) + } + )), + titleItem: nil, + leftItem: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + dismiss(true) + } + ) + ), + rightItem: nil, + hasTopEdgeEffect: false, + bottomItem: bottomItem, + backgroundColor: .color(theme.list.modalBlocksBackgroundColor), + defaultHeight: 540.0, + animateOut: self.animateOut + )), + environment: { + environmentValue + ResizableSheetComponentEnvironment( + theme: theme, + statusBarHeight: environmentValue.statusBarHeight, + safeInsets: environmentValue.safeInsets, + inputHeight: 0.0, + metrics: environmentValue.metrics, + deviceMetrics: environmentValue.deviceMetrics, + isDisplaying: environmentValue.isVisible, + isCentered: environmentValue.metrics.widthClass == .regular, + screenSize: availableSize, + regularMetricsSize: nil, + dismiss: { animated in + dismiss(animated) + } + ) + }, + containerSize: availableSize + ) + self.sheet.parentState = state + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) + } + transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize)) } - self.ignoreScrolling = false - self.updateScrolling(transition: transition) return availableSize } @@ -1164,141 +1165,31 @@ private final class SendInviteLinkScreenComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + public class SendInviteLinkScreen: ViewControllerComponentContainer { - private let context: AccountContext - private let peers: [TelegramForbiddenInvitePeer] - private var isDismissed: Bool = false + private var dismissCompletion: (() -> Void)? private var presenceDisposable: Disposable? public init(context: AccountContext, subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer], theme: PresentationTheme? = nil) { - self.context = context - - #if DEBUG && false - var peers = peers - - if !"".isEmpty { - enum TestConfiguration: CaseIterable { - case singlePeerNoPremiumLink - case singlePeerPremiumLink - case singlePeerNoPremiumNoLink - case singlePeerPremiumNoLink - case somePeersNoPremiumLink - case somePeersOnePremiumLink - case somePeersAllPremiumLink - case somePeersNoPremiumNoLink - case somePeersOnePremiumNoLink - case somePeersAllPremiumNoLink - case morePeersNoPremiumLink - case morePeersOnePremiumLink - case morePeersAllPremiumLink - case morePeersNoPremiumNoLink - case morePeersOnePremiumNoLink - case morePeersAllPremiumNoLink - } - - var nextPeerId: Int64 = 1 - let makePeer: (Bool, Bool) -> TelegramForbiddenInvitePeer = { canInviteWithPremium, premiumRequiredToContact in - guard case let .user(user) = peers[0].peer else { - preconditionFailure() - } - let id = nextPeerId - nextPeerId += 1 - return TelegramForbiddenInvitePeer( - peer: .user(TelegramUser( - id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(id)), - accessHash: user.accessHash, - firstName: user.firstName, - lastName: user.lastName, - username: user.username, - phone: user.phone, - photo: user.photo, - botInfo: user.botInfo, - restrictionInfo: user.restrictionInfo, - flags: user.flags, - emojiStatus: user.emojiStatus, - usernames: user.usernames, - storiesHidden: user.storiesHidden, - nameColor: user.nameColor, - backgroundEmojiId: user.backgroundEmojiId, - profileColor: user.profileColor, - profileBackgroundEmojiId: user.profileBackgroundEmojiId, - subscriberCount: user.subscriberCount, - verificationIconFileId: user.verificationIconFileId - )), - canInviteWithPremium: canInviteWithPremium, - premiumRequiredToContact: premiumRequiredToContact - ) - } - - let caseIndex = 9 - let configuration = TestConfiguration.allCases[caseIndex] - do { - switch configuration { - case .singlePeerNoPremiumLink: - peers = [makePeer(false, false)] - link = "abcd" - case .singlePeerPremiumLink: - peers = [makePeer(true, false)] - link = "abcd" - case .singlePeerNoPremiumNoLink: - peers = [makePeer(false, false)] - link = nil - case .singlePeerPremiumNoLink: - peers = [makePeer(true, false)] - link = nil - case .somePeersNoPremiumLink: - peers = (0 ..< 3).map { _ in makePeer(false, false) } - link = "abcd" - case .somePeersOnePremiumLink: - peers = [ - makePeer(false, false), - makePeer(true, true), - makePeer(false, false) - ] - link = "abcd" - case .somePeersAllPremiumLink: - peers = (0 ..< 3).map { _ in makePeer(true, false) } - link = "abcd" - case .somePeersNoPremiumNoLink: - peers = (0 ..< 3).map { _ in makePeer(false, false) } - link = nil - case .somePeersOnePremiumNoLink: - peers = [ - makePeer(false, false), - makePeer(true, false), - makePeer(false, false) - ] - link = nil - case .somePeersAllPremiumNoLink: - peers = (0 ..< 3).map { _ in makePeer(true, false) } - link = nil - case .morePeersNoPremiumLink: - preconditionFailure() - case .morePeersOnePremiumLink: - preconditionFailure() - case .morePeersAllPremiumLink: - preconditionFailure() - case .morePeersNoPremiumNoLink: - preconditionFailure() - case .morePeersOnePremiumNoLink: - preconditionFailure() - case .morePeersAllPremiumNoLink: - preconditionFailure() - } - } - } - #endif - - self.peers = peers - - super.init(context: context, component: SendInviteLinkScreenComponent(context: context, subject: subject, peers: peers, peerPresences: [:], sendPaidMessageStars: [:]), navigationBarAppearance: .none, theme: theme.flatMap { .custom($0) } ?? .default) + super.init( + context: context, + component: SendInviteLinkScreenComponent( + context: context, + subject: subject, + peers: peers, + peerPresences: [:], + sendPaidMessageStars: [:] + ), + navigationBarAppearance: .none, + theme: theme.flatMap { .custom($0) } ?? .default + ) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal @@ -1328,7 +1219,16 @@ public class SendInviteLinkScreen: ViewControllerComponentContainer { parsedSendPaidMessageStars[id] = sendPaidMessageStars } } - self.updateComponent(component: AnyComponent(SendInviteLinkScreenComponent(context: context, subject: subject, peers: peers, peerPresences: parsedPresences, sendPaidMessageStars: parsedSendPaidMessageStars)), transition: .immediate) + self.updateComponent( + component: AnyComponent(SendInviteLinkScreenComponent( + context: context, + subject: subject, + peers: peers, + peerPresences: parsedPresences, + sendPaidMessageStars: parsedSendPaidMessageStars + )), + transition: .immediate + ) }) } @@ -1344,22 +1244,23 @@ public class SendInviteLinkScreen: ViewControllerComponentContainer { super.viewDidAppear(animated) self.view.disablesInteractiveModalDismiss = true - - if let componentView = self.node.hostView.componentView as? SendInviteLinkScreenComponent.View { - componentView.animateIn() - } } + func completePendingDismiss() { + let dismissCompletion = self.dismissCompletion + self.dismissCompletion = nil + dismissCompletion?() + } + override public func dismiss(completion: (() -> Void)? = nil) { if !self.isDismissed { self.isDismissed = true + self.dismissCompletion = completion - if let componentView = self.node.hostView.componentView as? SendInviteLinkScreenComponent.View { - componentView.animateOut(completion: { [weak self] in - completion?() - self?.dismiss(animated: false) - }) + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() } else { + self.completePendingDismiss() self.dismiss(animated: false) } } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD index 671515c2ed..311910ad39 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD @@ -42,9 +42,8 @@ swift_library( "//submodules/ItemListPeerActionItem", "//submodules/ItemListUI", "//submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController", - "//submodules/DateSelectionUI", "//submodules/TelegramStringFormatting", - "//submodules/TelegramUI/Components/TimeSelectionActionSheet", + "//submodules/TelegramUI/Components/ChatTimerScreen", "//submodules/TelegramUI/Components/ChatListHeaderComponent", "//submodules/AttachmentUI", "//submodules/SearchBarNode", diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift index c7d3799a5d..7c6418a7dd 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift @@ -23,11 +23,10 @@ import Markdown import PeerListItemComponent import AvatarNode import ListItemSliderSelectorComponent -import DateSelectionUI import PlainButtonComponent import TelegramStringFormatting import TextFormat -import TimeSelectionActionSheet +import ChatTimerScreen private let checkIcon: UIImage = { return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in @@ -505,14 +504,19 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { return } - let controller = DateSelectionActionSheetController( + let controller = ChatTimerScreen( context: component.context, - title: nil, - currentValue: Int32(clippedDate.timeIntervalSince1970), - minimumDate: nil, - maximumDate: nil, - emptyTitle: nil, - applyValue: { [weak self] value in + configuration: ChatTimerScreen.Configuration( + style: .default, + title: { strings in return isStartTime ? strings.BusinessMessageSetup_ScheduleStartTime : strings.BusinessMessageSetup_ScheduleEndTime }, + picker: .date, + currentValue: Int32(clippedDate.timeIntervalSince1970), + pickerValueMapping: .roundDateToDaysUTC, + primaryActionTitle: { strings, _, _ in + strings.Wallpaper_Set + } + ), + completion: { [weak self] value in guard let self else { return } @@ -544,33 +548,44 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { let hour = components.hour ?? 0 let minute = components.minute ?? 0 - let controller = TimeSelectionActionSheet(context: component.context, currentValue: Int32(hour * 60 * 60 + minute * 60), applyValue: { [weak self] value in - guard let self else { - return - } - guard let value else { - return - } - - let updatedHour = value / (60 * 60) - let updatedMinute = (value % (60 * 60)) / 60 - - let calendar = Calendar.current - var updatedComponents = calendar.dateComponents([.year, .month, .day], from: currentValue) - updatedComponents.hour = Int(updatedHour) - updatedComponents.minute = Int(updatedMinute) - - guard let updatedClippedDate = calendar.date(from: updatedComponents) else { - return - } - - if isStartTime { - self.customScheduleStart = updatedClippedDate - } else { - self.customScheduleEnd = updatedClippedDate - } - self.state?.updated(transition: .immediate) - }) + let controller = ChatTimerScreen( + context: component.context, + configuration: ChatTimerScreen.Configuration( + style: .default, + picker: .timeOfDay, + currentValue: Int32(hour * 60 * 60 + minute * 60), + pickerValueMapping: .secondsFromMidnightGMT, + primaryActionTitle: { strings, _, _ in + strings.Wallpaper_Set + } + ), + completion: { [weak self] value in + guard let self else { + return + } + guard let value else { + return + } + + let updatedHour = value / (60 * 60) + let updatedMinute = (value % (60 * 60)) / 60 + + let calendar = Calendar.current + var updatedComponents = calendar.dateComponents([.year, .month, .day], from: currentValue) + updatedComponents.hour = Int(updatedHour) + updatedComponents.minute = Int(updatedMinute) + + guard let updatedClippedDate = calendar.date(from: updatedComponents) else { + return + } + + if isStartTime { + self.customScheduleStart = updatedClippedDate + } else { + self.customScheduleEnd = updatedClippedDate + } + self.state?.updated(transition: .immediate) + }) self.environment?.controller()?.present(controller, in: .window(.root)) } } diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/BUILD index 737e18eef2..80e1d2d757 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/BUILD @@ -35,7 +35,7 @@ swift_library( "//submodules/TextFormat", "//submodules/UIKitRuntimeUtils", "//submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen", - "//submodules/TelegramUI/Components/TimeSelectionActionSheet", + "//submodules/TelegramUI/Components/ChatTimerScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift index 1bba33ac1f..553c126ca8 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift @@ -21,7 +21,7 @@ import LocationUI import TelegramStringFormatting import TextFormat import PlainButtonComponent -import TimeSelectionActionSheet +import ChatTimerScreen func clipMinutes(_ value: Int) -> Int { return value % (24 * 60) @@ -180,31 +180,42 @@ final class BusinessDaySetupScreenComponent: Component { return } - let controller = TimeSelectionActionSheet(context: component.context, currentValue: Int32(isStartTime ? clipMinutes(range.startMinute) : clipMinutes(range.endMinute)) * 60, applyValue: { [weak self] value in - guard let self else { - return - } - guard let value else { - return - } - if let index = self.ranges.firstIndex(where: { $0.id == rangeId }) { - var startMinute = range.startMinute - var endMinute = range.endMinute - if isStartTime { - startMinute = Int(value) / 60 - } else { - endMinute = Int(value) / 60 + let controller = ChatTimerScreen( + context: component.context, + configuration: ChatTimerScreen.Configuration( + style: .default, + picker: .timeOfDay, + currentValue: Int32(isStartTime ? clipMinutes(range.startMinute) : clipMinutes(range.endMinute)) * 60, + pickerValueMapping: .secondsFromMidnightGMT, + primaryActionTitle: { strings, _, _ in + strings.Wallpaper_Set } - if endMinute < startMinute { - endMinute = endMinute + 24 * 60 + ), + completion: { [weak self] value in + guard let self else { + return } - self.ranges[index].startMinute = startMinute - self.ranges[index].endMinute = endMinute - self.validateRanges() - - self.state?.updated(transition: .immediate) - } - }) + guard let value else { + return + } + if let index = self.ranges.firstIndex(where: { $0.id == rangeId }) { + var startMinute = range.startMinute + var endMinute = range.endMinute + if isStartTime { + startMinute = Int(value) / 60 + } else { + endMinute = Int(value) / 60 + } + if endMinute < startMinute { + endMinute = endMinute + 24 * 60 + } + self.ranges[index].startMinute = startMinute + self.ranges[index].endMinute = endMinute + self.validateRanges() + + self.state?.updated(transition: .immediate) + } + }) self.environment?.controller()?.present(controller, in: .window(.root)) } diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index b31513a97f..bae7fd368b 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -833,7 +833,7 @@ final class ChatbotSetupScreenComponent: Component { } controller.present(UndoOverlayController( presentationData: presentationData, - content: .invitedToVoiceChat(context: component.context, peer: peer, title: nil, text: environment.strings.ChatbotSetup_BotInstalled(peer.compactDisplayTitle).string, action: nil, duration: 2.0), + content: .actionSucceeded(title: nil, text: environment.strings.ChatbotSetup_BotInstalled(peer.compactDisplayTitle).string, cancel: nil, destructive: false), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index c0e8162235..87c58b5f20 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -633,7 +633,7 @@ final class StarsTransactionsScreenComponent: Component { starTransition.setFrame(view: topBalanceIconView, frame: topBalanceIconFrame) } - contentHeight += 181.0 + contentHeight += 197.0 let descriptionSize = self.descriptionView.update( transition: .immediate, diff --git a/submodules/TelegramUI/Components/TimeSelectionActionSheet/BUILD b/submodules/TelegramUI/Components/TimeSelectionActionSheet/BUILD deleted file mode 100644 index b4680d882d..0000000000 --- a/submodules/TelegramUI/Components/TimeSelectionActionSheet/BUILD +++ /dev/null @@ -1,25 +0,0 @@ -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -swift_library( - name = "TimeSelectionActionSheet", - module_name = "TimeSelectionActionSheet", - srcs = glob([ - "Sources/**/*.swift", - ]), - copts = [ - "-warnings-as-errors", - ], - deps = [ - "//submodules/Display", - "//submodules/AsyncDisplayKit", - "//submodules/SSignalKit/SwiftSignalKit", - "//submodules/TelegramCore", - "//submodules/TelegramPresentationData", - "//submodules/TelegramStringFormatting", - "//submodules/AccountContext", - "//submodules/UIKitRuntimeUtils", - ], - visibility = [ - "//visibility:public", - ], -) diff --git a/submodules/TelegramUI/Components/TimeSelectionActionSheet/Sources/TimeSelectionActionSheet.swift b/submodules/TelegramUI/Components/TimeSelectionActionSheet/Sources/TimeSelectionActionSheet.swift deleted file mode 100644 index 8ec3ff48af..0000000000 --- a/submodules/TelegramUI/Components/TimeSelectionActionSheet/Sources/TimeSelectionActionSheet.swift +++ /dev/null @@ -1,132 +0,0 @@ -import Foundation -import Display -import AsyncDisplayKit -import UIKit -import SwiftSignalKit -import TelegramCore -import TelegramPresentationData -import TelegramStringFormatting -import AccountContext -import UIKitRuntimeUtils - -public final class TimeSelectionActionSheet: ActionSheetController { - private var presentationDisposable: Disposable? - - private let _ready = Promise() - override public var ready: Promise { - return self._ready - } - - public init(context: AccountContext, currentValue: Int32, emptyTitle: String? = nil, applyValue: @escaping (Int32?) -> Void) { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let strings = presentationData.strings - - super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) - - self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) - } - }) - - self._ready.set(.single(true)) - - var updatedValue = currentValue - var items: [ActionSheetItem] = [] - items.append(TimeSelectionActionSheetItem(strings: strings, currentValue: currentValue, valueChanged: { value in - updatedValue = value - })) - if let emptyTitle = emptyTitle { - items.append(ActionSheetButtonItem(title: emptyTitle, action: { [weak self] in - self?.dismissAnimated() - applyValue(nil) - })) - } - items.append(ActionSheetButtonItem(title: strings.Wallpaper_Set, action: { [weak self] in - self?.dismissAnimated() - applyValue(updatedValue) - })) - self.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in - self?.dismissAnimated() - }), - ]) - ]) - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDisposable?.dispose() - } -} - -private final class TimeSelectionActionSheetItem: ActionSheetItem { - let strings: PresentationStrings - - let currentValue: Int32 - let valueChanged: (Int32) -> Void - - init(strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) { - self.strings = strings - self.currentValue = currentValue - self.valueChanged = valueChanged - } - - func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - return TimeSelectionActionSheetItemNode(theme: theme, strings: self.strings, currentValue: self.currentValue, valueChanged: self.valueChanged) - } - - func updateNode(_ node: ActionSheetItemNode) { - } -} - -private final class TimeSelectionActionSheetItemNode: ActionSheetItemNode { - private let theme: ActionSheetControllerTheme - private let strings: PresentationStrings - - private let valueChanged: (Int32) -> Void - private let pickerView: UIDatePicker - - init(theme: ActionSheetControllerTheme, strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) { - self.theme = theme - self.strings = strings - self.valueChanged = valueChanged - - UILabel.setDateLabel(theme.primaryTextColor) - - self.pickerView = UIDatePicker() - self.pickerView.datePickerMode = .countDownTimer - self.pickerView.datePickerMode = .time - self.pickerView.timeZone = TimeZone(secondsFromGMT: 0) - self.pickerView.date = Date(timeIntervalSince1970: Double(currentValue)) - self.pickerView.locale = Locale.current - if #available(iOS 13.4, *) { - self.pickerView.preferredDatePickerStyle = .wheels - } - self.pickerView.setValue(theme.primaryTextColor, forKey: "textColor") - - super.init(theme: theme) - - self.view.addSubview(self.pickerView) - self.pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged) - } - - public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let size = CGSize(width: constrainedSize.width, height: 216.0) - - self.pickerView.frame = CGRect(origin: CGPoint(), size: size) - - self.updateInternalLayout(size, constrainedSize: constrainedSize) - return size - } - - @objc private func datePickerUpdated() { - self.valueChanged(Int32(self.pickerView.date.timeIntervalSince1970)) - } -} - diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift index 000b909286..cf28d4c811 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift @@ -1940,7 +1940,48 @@ public class VideoMessageCameraScreen: ViewController { guard let self else { return } - let values = MediaEditorValues(peerId: self.context.account.peerId, originalDimensions: dimensions, cropOffset: .zero, cropRect: CGRect(origin: .zero, size: dimensions.cgSize), cropScale: 1.0, cropRotation: 0.0, cropMirroring: false, cropOrientation: nil, gradientColors: nil, videoTrimRange: self.node.previewState?.trimRange, videoBounce: false, videoIsMuted: false, videoIsFullHd: false, videoIsMirrored: false, videoVolume: nil, additionalVideoPath: nil, additionalVideoIsDual: false, additionalVideoPosition: nil, additionalVideoScale: nil, additionalVideoRotation: nil, additionalVideoPositionChanges: [], additionalVideoTrimRange: nil, additionalVideoOffset: nil, additionalVideoVolume: nil, collage: [], nightTheme: false, drawing: nil, maskDrawing: nil, entities: [], toolValues: [:], audioTrack: nil, audioTrackTrimRange: nil, audioTrackOffset: nil, audioTrackVolume: nil, audioTrackSamples: nil, collageTrackSamples: nil, coverImageTimestamp: nil, coverDimensions: nil, qualityPreset: .videoMessage) + let values = MediaEditorValues( + peerId: self.context.account.peerId, + originalDimensions: dimensions, + cropOffset: .zero, + cropRect: CGRect(origin: .zero, size: dimensions.cgSize), + cropScale: 1.0, + cropRotation: 0.0, + cropMirroring: false, + cropOrientation: nil, + gradientColors: nil, + videoTrimRange: self.node.previewState?.trimRange, + videoBounce: false, + videoIsMuted: false, + videoIsFullHd: false, + videoIsMirrored: false, + videoVolume: nil, + additionalVideoPath: nil, + additionalVideoIsDual: false, + additionalVideoMirroringChanges: [], + additionalVideoPosition: nil, + additionalVideoScale: nil, + additionalVideoRotation: nil, + additionalVideoPositionChanges: [], + additionalVideoTrimRange: nil, + additionalVideoOffset: nil, + additionalVideoVolume: nil, + collage: [], + nightTheme: false, + drawing: nil, + maskDrawing: nil, + entities: [], + toolValues: [:], + audioTrack: nil, + audioTrackTrimRange: nil, + audioTrackOffset: nil, + audioTrackVolume: nil, + audioTrackSamples: nil, + collageTrackSamples: nil, + coverImageTimestamp: nil, + coverDimensions: nil, + qualityPreset: .videoMessage + ) var resourceAdjustments: VideoMediaResourceAdjustments? = nil if let valuesData = try? JSONEncoder().encode(values) { diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/Context Menu/Contents.json b/submodules/TelegramUI/Images.xcassets/Contact List/Context Menu/Contents.json deleted file mode 100644 index 6e965652df..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Contact List/Context Menu/Contents.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "provides-namespace" : true - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/Context Menu/PeopleNearby.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Contact List/Context Menu/PeopleNearby.imageset/Contents.json deleted file mode 100644 index 6b7b65b4e5..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Contact List/Context Menu/PeopleNearby.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "peoplenearbyon_24.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/Context Menu/PeopleNearby.imageset/peoplenearbyon_24.pdf b/submodules/TelegramUI/Images.xcassets/Contact List/Context Menu/PeopleNearby.imageset/peoplenearbyon_24.pdf deleted file mode 100644 index 1eada00e63..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Contact List/Context Menu/PeopleNearby.imageset/peoplenearbyon_24.pdf +++ /dev/null @@ -1,145 +0,0 @@ -%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 2.934998 1.735001 cm -0.000000 0.000000 0.000000 scn -7.330000 17.464998 m -7.330000 18.423212 8.106786 19.199999 9.065001 19.199999 c -10.023214 19.199999 10.800000 18.423212 10.800000 17.464998 c -10.800000 16.506784 10.023214 15.729999 9.065001 15.729999 c -8.106786 15.729999 7.330000 16.506784 7.330000 17.464998 c -h -9.065001 20.529999 m -7.372247 20.529999 6.000000 19.157751 6.000000 17.464998 c -6.000000 15.772245 7.372247 14.399999 9.065001 14.399999 c -10.757753 14.399999 12.130000 15.772245 12.130000 17.464998 c -12.130000 19.157751 10.757753 20.529999 9.065001 20.529999 c -h -1.330000 3.865000 m -1.330000 4.026144 1.400150 4.232668 1.634496 4.483692 c -1.872207 4.738321 2.249572 5.004240 2.774642 5.255713 c -3.677844 5.688284 4.928888 6.035789 6.400000 6.230032 c -6.400001 4.564999 l -6.400001 3.755901 7.055903 3.099998 7.865001 3.099998 c -10.265000 3.099998 l -11.074098 3.099998 11.730000 3.755901 11.730000 4.564999 c -11.730000 6.230032 l -13.201113 6.035789 14.452158 5.688284 15.355359 5.255713 c -15.880429 5.004240 16.257793 4.738321 16.495506 4.483692 c -16.729851 4.232668 16.800003 4.026144 16.800003 3.865000 c -16.800003 3.677490 16.702744 3.422382 16.359842 3.113541 c -16.017097 2.804838 15.483717 2.496361 14.767962 2.223692 c -13.341063 1.680111 11.324945 1.330000 9.065001 1.330000 c -6.805056 1.330000 4.788938 1.680111 3.362040 2.223692 c -2.646284 2.496361 2.112905 2.804838 1.770159 3.113541 c -1.427257 3.422382 1.330000 3.677490 1.330000 3.865000 c -h -2.200152 6.455237 m -3.300211 6.982090 4.758119 7.368791 6.400000 7.570784 c -6.400000 8.099998 l -5.859936 8.099998 l -5.076447 8.099998 4.338713 8.757976 4.479389 9.657219 c -4.588190 10.352705 4.856467 11.405879 5.540887 12.298394 c -6.249626 13.222622 7.373408 13.929998 9.065001 13.929998 c -10.756596 13.929998 11.880377 13.222621 12.589115 12.298393 c -13.273535 11.405878 13.541812 10.352703 13.650612 9.657217 c -13.791287 8.757974 13.053553 8.099998 12.270063 8.099998 c -11.730000 8.099998 l -11.730000 7.570784 l -13.371881 7.368791 14.829790 6.982090 15.929850 6.455237 c -16.545578 6.160346 17.079411 5.807216 17.467701 5.391293 c -17.859352 4.971766 18.130001 4.456234 18.130001 3.865000 c -18.130001 3.168854 17.757156 2.582132 17.249931 2.125290 c -16.742554 1.668306 16.045780 1.287239 15.241435 0.980824 c -13.628130 0.366230 11.444248 0.000000 9.065001 0.000000 c -6.685753 0.000000 4.501871 0.366230 2.888566 0.980824 c -2.084221 1.287239 1.387449 1.668306 0.880069 2.125290 c -0.372844 2.582132 0.000000 3.168854 0.000000 3.865000 c -0.000000 4.456234 0.270649 4.971766 0.662301 5.391293 c -1.050590 5.807216 1.584423 6.160346 2.200152 6.455237 c -h -6.596293 11.489061 m -6.105442 10.848969 5.886520 10.046863 5.793407 9.451655 c -5.793330 9.451145 l -5.800257 9.444448 5.821226 9.429998 5.859936 9.429998 c -7.065000 9.429998 l -7.432269 9.429998 7.730000 9.132269 7.730000 8.764999 c -7.730000 4.564999 l -7.730000 4.490440 7.790442 4.429998 7.865001 4.429998 c -8.533002 4.429998 l -8.533002 6.764999 l -8.533002 7.058815 8.771186 7.296999 9.065001 7.296999 c -9.358817 7.296999 9.597001 7.058815 9.597001 6.764999 c -9.597001 4.429998 l -10.265000 4.429998 l -10.339560 4.429998 10.400000 4.490440 10.400000 4.564999 c -10.400000 8.764999 l -10.400000 8.941368 10.470061 9.110514 10.594772 9.235225 c -10.719484 9.359937 10.888630 9.429998 11.064999 9.429998 c -12.270063 9.429998 l -12.308775 9.429998 12.329742 9.444448 12.336671 9.451145 c -12.336594 9.451655 l -12.243481 10.046862 12.024561 10.848969 11.533709 11.489061 c -11.067177 12.097442 10.327614 12.599998 9.065001 12.599998 c -7.802389 12.599998 7.062826 12.097442 6.596293 11.489061 c -h -f* -n -Q - -endstream -endobj - -3 0 obj - 3674 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 24.000000 24.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 -0000003764 00000 n -0000003787 00000 n -0000003960 00000 n -0000004034 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -4093 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/MakeInvisibleIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Contact List/MakeInvisibleIcon.imageset/Contents.json deleted file mode 100644 index 7abcd976aa..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Contact List/MakeInvisibleIcon.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "ic_stopshowme.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/MakeInvisibleIcon.imageset/ic_stopshowme.pdf b/submodules/TelegramUI/Images.xcassets/Contact List/MakeInvisibleIcon.imageset/ic_stopshowme.pdf deleted file mode 100644 index 9cc4c3d65a..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Contact List/MakeInvisibleIcon.imageset/ic_stopshowme.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/MakeVisibleIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Contact List/MakeVisibleIcon.imageset/Contents.json deleted file mode 100644 index 97cc9be019..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Contact List/MakeVisibleIcon.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "ic_showme.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/MakeVisibleIcon.imageset/ic_showme.pdf b/submodules/TelegramUI/Images.xcassets/Contact List/MakeVisibleIcon.imageset/ic_showme.pdf deleted file mode 100644 index 0c5a6e2133..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Contact List/MakeVisibleIcon.imageset/ic_showme.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 1c1631c776..b1ea96b8da 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -45,6 +45,7 @@ import RecaptchaEnterprise import NavigationBarImpl import ContextUI import ContextControllerImpl +import ProxyServerPreviewScreen #if canImport(AppCenter) import AppCenter @@ -2518,7 +2519,7 @@ private func extractAccountManagerState(records: AccountRecordsView 0 ? currentValue : 7, + pickerValueMapping: .rawTimestamp, + primaryActionTitle: { strings, _, _ in + strings.Common_Done + } + ), + completion: { value in + guard let strongSelf = self, let value else { + return + } let _ = strongSelf.context.engine.peers.setChatMessageAutoremoveTimeoutInteractively(peerId: peer.id, timeout: value == 0 ? nil : value).startStandalone() } - }) + ) strongSelf.present(controller, in: .window(.root)) } } else { @@ -3881,12 +3911,6 @@ extension ChatControllerImpl { if let strongSelf = self { strongSelf.openScheduledMessages() } - }, openPeersNearby: { [weak self] in - if let strongSelf = self { - let controller = strongSelf.context.sharedContext.makePeersNearbyController(context: strongSelf.context) - controller.navigationPresentation = .master - strongSelf.effectiveNavigationController?.pushViewController(controller, animated: true, completion: { }) - } }, displaySearchResultsTooltip: { [weak self] node, nodeRect in if let strongSelf = self { strongSelf.searchResultsTooltipController?.dismiss() diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift index c73e8ed83e..2e4d308a10 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift @@ -119,7 +119,6 @@ import AudioWaveform import PeerNameColorScreen import ChatEmptyNode import ChatMediaInputStickerGridItem -import AdsInfoScreen extension ChatControllerImpl { func requestAudioRecorder(beginWithTone: Bool, existingDraft: ChatInterfaceMediaDraftState.Audio? = nil) { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift index 2ad7598c76..90e5a23d7e 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift @@ -118,7 +118,6 @@ import AudioWaveform import PeerNameColorScreen import ChatEmptyNode import ChatMediaInputStickerGridItem -import AdsInfoScreen extension ChatControllerImpl { func navigationButtonAction(_ action: ChatNavigationButtonAction) { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift index 8e52852d57..a48582f554 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift @@ -118,7 +118,6 @@ import AudioWaveform import PeerNameColorScreen import ChatEmptyNode import ChatMediaInputStickerGridItem -import AdsInfoScreen import FaceScanScreen import ForumCreateTopicScreen diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift index 2edb644a58..a40fe91b84 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift @@ -118,7 +118,6 @@ import AudioWaveform import PeerNameColorScreen import ChatEmptyNode import ChatMediaInputStickerGridItem -import AdsInfoScreen extension ChatControllerImpl { func openViewOnceMediaMessage(_ message: EngineMessage) { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift index 6e8d0ca603..c1a4b09d63 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift @@ -205,6 +205,7 @@ extension ChatControllerImpl { videoVolume: nil, additionalVideoPath: nil, additionalVideoIsDual: false, + additionalVideoMirroringChanges: [], additionalVideoPosition: nil, additionalVideoScale: nil, additionalVideoRotation: nil, diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift index 63900b1530..149167ad39 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift @@ -118,7 +118,6 @@ import AudioWaveform import PeerNameColorScreen import ChatEmptyNode import ChatMediaInputStickerGridItem -import AdsInfoScreen extension ChatControllerImpl { func unblockPeer() { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift index 3fd6ca7e9f..fb6e199ca2 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift @@ -118,7 +118,6 @@ import AudioWaveform import PeerNameColorScreen import ChatEmptyNode import ChatMediaInputStickerGridItem -import AdsInfoScreen import Photos import ChatThemeScreen diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 953692fad1..bd51acdff6 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1588,17 +1588,32 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let mediaReference = mediaReference, let peer = message.peers[message.id.peerId] { + let hasSilentPosting = peer.id != self.context.account.peerId + let hasSchedule = self.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat && self.presentationInterfaceState.sendPaidMessageStars == nil legacyMediaEditor(context: self.context, peer: EnginePeer(peer), threadTitle: self.contentData?.state.threadInfo?.title, media: mediaReference, mode: .draw, initialCaption: NSAttributedString(), snapshots: snapshots, transitionCompletion: { transitionCompletion() }, getCaptionPanelView: { [weak self] in return self?.getCaptionPanelView(isFile: false) - }, sendMessagesWithSignals: { [weak self] signals, _, _, isCaptionAbove in + }, hasSilentPosting: hasSilentPosting, hasSchedule: hasSchedule, reminder: peer.id == self.context.account.peerId, presentSchedulePicker: { [weak self] _, done in + guard let self else { + return + } + self.presentScheduleTimePicker(style: .media, presentInOverlay: true, completion: { [weak self] result in + guard let self else { + return + } + done(result.time, result.silentPosting) + if self.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp { + self.openScheduledMessages() + } + }) + }, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime, isCaptionAbove in if let self { var parameters: ChatSendMessageActionSheetController.SendParameters? if isCaptionAbove { parameters = ChatSendMessageActionSheetController.SendParameters(effect: nil, textIsAboveMedia: true) } - self.enqueueMediaMessages(signals: signals, silentPosting: false, replyToSubject: .init(messageId: message.id, quote: nil, innerSubject: nil), parameters: parameters) + self.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime == 0 ? nil : scheduleTime, replyToSubject: .init(messageId: message.id, quote: nil, innerSubject: nil), parameters: parameters) } }, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) @@ -10426,13 +10441,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G ) } - func presentScheduleTimePicker(style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, selectedRepeatPeriod: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32, Int32?) -> Void) { - self.presentScheduleTimePicker(style: style, selectedTime: selectedTime, selectedRepeatPeriod: selectedRepeatPeriod, dismissByTapOutside: dismissByTapOutside, completion: { result in + func presentScheduleTimePicker(style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, selectedRepeatPeriod: Int32? = nil, dismissByTapOutside: Bool = true, presentInOverlay: Bool = false, completion: @escaping (Int32, Int32?) -> Void) { + self.presentScheduleTimePicker(style: style, selectedTime: selectedTime, selectedRepeatPeriod: selectedRepeatPeriod, dismissByTapOutside: dismissByTapOutside, presentInOverlay: presentInOverlay, completion: { result in completion(result.time, result.repeatPeriod) }) } - func presentScheduleTimePicker(style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, selectedRepeatPeriod: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (ChatScheduleTimeScreen.Result) -> Void) { + func presentScheduleTimePicker(style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, selectedRepeatPeriod: Int32? = nil, dismissByTapOutside: Bool = true, presentInOverlay: Bool = false, completion: @escaping (ChatScheduleTimeScreen.Result) -> Void) { guard let peerId = self.chatLocation.peerId else { return } @@ -10470,7 +10485,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } ) strongSelf.chatDisplayNode.dismissInput() - if strongSelf.videoRecorderValue != nil { + if presentInOverlay || strongSelf.videoRecorderValue != nil { strongSelf.present(controller, in: .window(.root)) } else { strongSelf.push(controller) diff --git a/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift index bd86622a5a..fe69bffc7d 100644 --- a/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift @@ -12,101 +12,6 @@ import AccountContext import ChatPresentationInterfaceState import LegacyChatHeaderPanelComponent -private final class ChatInfoTitlePanelPeerNearbyInfoNode: ASDisplayNode { - private var theme: PresentationTheme? - - private let labelNode: ImmediateTextNode - private let filledBackgroundNode: LinkHighlightingNode - - private let openPeersNearby: () -> Void - - init(openPeersNearby: @escaping () -> Void) { - self.openPeersNearby = openPeersNearby - - self.labelNode = ImmediateTextNode() - self.labelNode.maximumNumberOfLines = 1 - self.labelNode.textAlignment = .center - - self.filledBackgroundNode = LinkHighlightingNode(color: .clear) - - super.init() - - self.addSubnode(self.filledBackgroundNode) - self.addSubnode(self.labelNode) - } - - override func didLoad() { - super.didLoad() - - let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) - self.view.addGestureRecognizer(tapRecognizer) - } - - @objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) { - self.openPeersNearby() - } - - func update(width: CGFloat, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, chatPeer: EnginePeer, distance: Int32, transition: ContainedViewLayoutTransition) -> CGFloat { - let primaryTextColor = serviceMessageColorComponents(theme: theme, wallpaper: wallpaper).primaryText - - if self.theme !== theme { - self.theme = theme - - self.labelNode.linkHighlightColor = primaryTextColor.withAlphaComponent(0.3) - } - - let topInset: CGFloat = 6.0 - let bottomInset: CGFloat = 6.0 - let sideInset: CGFloat = 16.0 - - let stringAndRanges = strings.Conversation_PeerNearbyDistance(chatPeer.compactDisplayTitle, shortStringForDistance(strings: strings, distance: distance)) - - let attributedString = NSMutableAttributedString(string: stringAndRanges.string, font: Font.regular(13.0), textColor: primaryTextColor) - - let boldAttributes = [NSAttributedString.Key.font: Font.semibold(13.0), NSAttributedString.Key(rawValue: "_Link"): true as NSNumber] - for range in stringAndRanges.ranges.prefix(1) { - attributedString.addAttributes(boldAttributes, range: range.range) - } - - self.labelNode.attributedText = attributedString - let labelLayout = self.labelNode.updateLayoutFullInfo(CGSize(width: width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - - var labelRects = labelLayout.linesRects() - if labelRects.count > 1 { - let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width }) - for i in 0 ..< sortedIndices.count { - let index = sortedIndices[i] - for j in -1 ... 1 { - if j != 0 && index + j >= 0 && index + j < sortedIndices.count { - if abs(labelRects[index + j].width - labelRects[index].width) < 40.0 { - labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width) - labelRects[index].size.width = labelRects[index + j].size.width - } - } - } - } - } - for i in 0 ..< labelRects.count { - labelRects[i] = labelRects[i].insetBy(dx: -6.0, dy: floor((labelRects[i].height - 20.0) / 2.0)) - labelRects[i].size.height = 20.0 - labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0) - } - - let backgroundLayout = self.filledBackgroundNode.asyncLayout() - let serviceColor = serviceMessageColorComponents(theme: theme, wallpaper: wallpaper) - let backgroundApply = backgroundLayout(serviceColor.fill, labelRects, 10.0, 10.0, 0.0) - backgroundApply() - - let backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0) - - let labelFrame = CGRect(origin: CGPoint(x: floor((width - labelLayout.size.width) / 2.0), y: topInset + floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size) - self.labelNode.frame = labelFrame - self.filledBackgroundNode.frame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) - - return topInset + backgroundSize.height + bottomInset - } -} - final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { private final class Params { let width: CGFloat diff --git a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift index 28c8125fab..93d51a53b5 100644 --- a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift @@ -248,101 +248,6 @@ private final class ChatInfoTitlePanelInviteInfoNode: ASDisplayNode { } } -private final class ChatInfoTitlePanelPeerNearbyInfoNode: ASDisplayNode { - private var theme: PresentationTheme? - - private let labelNode: ImmediateTextNode - private let filledBackgroundNode: LinkHighlightingNode - - private let openPeersNearby: () -> Void - - init(openPeersNearby: @escaping () -> Void) { - self.openPeersNearby = openPeersNearby - - self.labelNode = ImmediateTextNode() - self.labelNode.maximumNumberOfLines = 1 - self.labelNode.textAlignment = .center - - self.filledBackgroundNode = LinkHighlightingNode(color: .clear) - - super.init() - - self.addSubnode(self.filledBackgroundNode) - self.addSubnode(self.labelNode) - } - - override func didLoad() { - super.didLoad() - - let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) - self.view.addGestureRecognizer(tapRecognizer) - } - - @objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) { - self.openPeersNearby() - } - - func update(width: CGFloat, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, chatPeer: EnginePeer, distance: Int32, transition: ContainedViewLayoutTransition) -> CGFloat { - let primaryTextColor = serviceMessageColorComponents(theme: theme, wallpaper: wallpaper).primaryText - - if self.theme !== theme { - self.theme = theme - - self.labelNode.linkHighlightColor = primaryTextColor.withAlphaComponent(0.3) - } - - let topInset: CGFloat = 6.0 - let bottomInset: CGFloat = 6.0 - let sideInset: CGFloat = 16.0 - - let stringAndRanges = strings.Conversation_PeerNearbyDistance(chatPeer.compactDisplayTitle, shortStringForDistance(strings: strings, distance: distance)) - - let attributedString = NSMutableAttributedString(string: stringAndRanges.string, font: Font.regular(13.0), textColor: primaryTextColor) - - let boldAttributes = [NSAttributedString.Key.font: Font.semibold(13.0), NSAttributedString.Key(rawValue: "_Link"): true as NSNumber] - for range in stringAndRanges.ranges.prefix(1) { - attributedString.addAttributes(boldAttributes, range: range.range) - } - - self.labelNode.attributedText = attributedString - let labelLayout = self.labelNode.updateLayoutFullInfo(CGSize(width: width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - - var labelRects = labelLayout.linesRects() - if labelRects.count > 1 { - let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width }) - for i in 0 ..< sortedIndices.count { - let index = sortedIndices[i] - for j in -1 ... 1 { - if j != 0 && index + j >= 0 && index + j < sortedIndices.count { - if abs(labelRects[index + j].width - labelRects[index].width) < 40.0 { - labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width) - labelRects[index].size.width = labelRects[index + j].size.width - } - } - } - } - } - for i in 0 ..< labelRects.count { - labelRects[i] = labelRects[i].insetBy(dx: -6.0, dy: floor((labelRects[i].height - 20.0) / 2.0)) - labelRects[i].size.height = 20.0 - labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0) - } - - let backgroundLayout = self.filledBackgroundNode.asyncLayout() - let serviceColor = serviceMessageColorComponents(theme: theme, wallpaper: wallpaper) - let backgroundApply = backgroundLayout(serviceColor.fill, labelRects, 10.0, 10.0, 0.0) - backgroundApply() - - let backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0) - - let labelFrame = CGRect(origin: CGPoint(x: floor((width - labelLayout.size.width) / 2.0), y: topInset + floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size) - self.labelNode.frame = labelFrame - self.filledBackgroundNode.frame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) - - return topInset + backgroundSize.height + bottomInset - } -} - final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { private let context: AccountContext private let animationCache: AnimationCache @@ -360,7 +265,6 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { private var presentationInterfaceState: ChatPresentationInterfaceState? private var inviteInfoNode: ChatInfoTitlePanelInviteInfoNode? - private var peerNearbyInfoNode: ChatInfoTitlePanelPeerNearbyInfoNode? private var cachedChevronImage: (UIImage, PresentationTheme)? @@ -771,34 +675,6 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { inviteInfoNode?.removeFromSupernode() }) } - - if let chatPeer = chatPeer, let distance = interfaceState.contactStatus?.peerStatusSettings?.geoDistance { - var peerNearbyInfoTransition = transition - let peerNearbyInfoNode: ChatInfoTitlePanelPeerNearbyInfoNode - if let current = self.peerNearbyInfoNode { - peerNearbyInfoNode = current - } else { - peerNearbyInfoTransition = .immediate - peerNearbyInfoNode = ChatInfoTitlePanelPeerNearbyInfoNode(openPeersNearby: { [weak self] in - self?.interfaceInteraction?.openPeersNearby() - }) - self.addSubnode(peerNearbyInfoNode) - self.peerNearbyInfoNode = peerNearbyInfoNode - peerNearbyInfoNode.alpha = 0.0 - transition.updateAlpha(node: peerNearbyInfoNode, alpha: 1.0) - } - - if let peerNearbyInfoNode = self.peerNearbyInfoNode { - let peerNearbyHeight = peerNearbyInfoNode.update(width: width, theme: interfaceState.theme, strings: interfaceState.strings, wallpaper: interfaceState.chatWallpaper, chatPeer: chatPeer, distance: distance, transition: peerNearbyInfoTransition) - peerNearbyInfoTransition.updateFrame(node: peerNearbyInfoNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight + panelInset), size: CGSize(width: width, height: peerNearbyHeight))) - panelHeight += peerNearbyHeight - } - } else if let peerNearbyInfoNode = self.peerNearbyInfoNode { - self.peerNearbyInfoNode = nil - transition.updateAlpha(node: peerNearbyInfoNode, alpha: 0.0, completion: { [weak peerNearbyInfoNode] _ in - peerNearbyInfoNode?.removeFromSupernode() - }) - } return LayoutResult(backgroundHeight: initialPanelHeight, insetHeight: panelHeight + panelInset, hitTestSlop: hitTestSlop) } diff --git a/submodules/TelegramUI/Sources/ChatSecretAutoremoveTimerActionSheet.swift b/submodules/TelegramUI/Sources/ChatSecretAutoremoveTimerActionSheet.swift deleted file mode 100644 index aa1d97af6b..0000000000 --- a/submodules/TelegramUI/Sources/ChatSecretAutoremoveTimerActionSheet.swift +++ /dev/null @@ -1,183 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import UIKit -import TelegramCore -import SwiftSignalKit -import Photos -import TelegramPresentationData -import AccountContext - -final class ChatSecretAutoremoveTimerActionSheetController: ActionSheetController { - private var presentationDisposable: Disposable? - - private let _ready = Promise() - override var ready: Promise { - return self._ready - } - - init(context: AccountContext, currentValue: Int32, availableValues: [Int32]? = nil, applyValue: @escaping (Int32) -> Void) { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let strings = presentationData.strings - - super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) - - self.presentationDisposable = context.sharedContext.presentationData.startStrict(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) - } - }) - - self._ready.set(.single(true)) - - var updatedValue: Int32 - if currentValue > 0 { - updatedValue = currentValue - } else { - if let availableValues = availableValues { - updatedValue = availableValues[0] - } else { - updatedValue = 7 - } - } - self.setItemGroups([ - ActionSheetItemGroup(items: [ - AutoremoveTimeoutSelectorItem(strings: strings, currentValue: updatedValue, availableValues: availableValues, valueChanged: { value in - updatedValue = value - }), - ActionSheetButtonItem(title: strings.Common_Done, font: .bold, action: { [weak self] in - self?.dismissAnimated() - applyValue(updatedValue) - }) - ]), - ]) - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDisposable?.dispose() - } -} - -private final class AutoremoveTimeoutSelectorItem: ActionSheetItem { - let strings: PresentationStrings - - let currentValue: Int32 - let availableValues: [Int32]? - let valueChanged: (Int32) -> Void - - init(strings: PresentationStrings, currentValue: Int32, availableValues: [Int32]?, valueChanged: @escaping (Int32) -> Void) { - self.strings = strings - self.currentValue = currentValue - self.availableValues = availableValues - self.valueChanged = valueChanged - } - - func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - return AutoremoveTimeoutSelectorItemNode(theme: theme, strings: self.strings, currentValue: self.currentValue, availableValues: self.availableValues, valueChanged: self.valueChanged) - } - - func updateNode(_ node: ActionSheetItemNode) { - } -} - -private let defaultTimeoutValues: [Int32] = [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 30, - 1 * 60, - 1 * 60 * 60, - 24 * 60 * 60, - 7 * 24 * 60 * 60 -] - -private final class AutoremoveTimeoutSelectorItemNode: ActionSheetItemNode, UIPickerViewDelegate, UIPickerViewDataSource { - private let theme: ActionSheetControllerTheme - private let strings: PresentationStrings - - private let timeoutValues: [Int32] - - private let valueChanged: (Int32) -> Void - private let pickerView: UIPickerView - - init(theme: ActionSheetControllerTheme, strings: PresentationStrings, currentValue: Int32, availableValues: [Int32]?, valueChanged: @escaping (Int32) -> Void) { - self.theme = theme - self.strings = strings - self.valueChanged = valueChanged - - self.pickerView = UIPickerView() - - if let availableValues = availableValues { - self.timeoutValues = [0] + availableValues.filter({ $0 > 0 }) - } else { - self.timeoutValues = defaultTimeoutValues - } - - super.init(theme: theme) - - self.pickerView.delegate = self - self.pickerView.dataSource = self - self.view.addSubview(self.pickerView) - - self.pickerView.reloadAllComponents() - var index: Int = 0 - for i in 0 ..< self.timeoutValues.count { - if currentValue <= self.timeoutValues[i] { - index = i - break - } - } - self.pickerView.selectRow(index, inComponent: 0, animated: false) - } - - func numberOfComponents(in pickerView: UIPickerView) -> Int { - return 1 - } - - func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - return self.timeoutValues.count - } - - func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { - return 40.0 - } - - func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { - if self.timeoutValues[row] == 0 { - return NSAttributedString(string: self.strings.Profile_MessageLifetimeForever, font: Font.medium(15.0), textColor: self.theme.primaryTextColor) - } else { - return NSAttributedString(string: timeIntervalString(strings: self.strings, value: self.timeoutValues[row]), font: Font.medium(15.0), textColor: self.theme.primaryTextColor) - } - } - - func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - self.valueChanged(self.timeoutValues[row]) - } - - public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let size = CGSize(width: constrainedSize.width, height: 180.0) - - self.pickerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 180.0)) - - self.updateInternalLayout(size, constrainedSize: constrainedSize) - return size - } -} diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index aab652e2ae..6115cb322f 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -13,7 +13,6 @@ import DeviceLocationManager import ItemListUI import LegacyUI import ChatListUI -import PeersNearbyUI import PeerInfoUI import SettingsUI import UrlHandling @@ -2244,15 +2243,11 @@ public final class SharedAccountContextImpl: SharedAccountContext { public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, forceUpdate: Bool, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView?, CGRect?) -> Bool)?, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?, progress: Promise?, completion: (() -> Void)?) { openResolvedUrlImpl(resolvedUrl, context: context, urlContext: urlContext, navigationController: navigationController, forceExternal: forceExternal, forceUpdate: forceUpdate, openPeer: openPeer, sendFile: sendFile, sendSticker: sendSticker, sendEmoji: sendEmoji, requestMessageActionUrlAuth: requestMessageActionUrlAuth, joinVoiceChat: joinVoiceChat, present: present, dismissInput: dismissInput, contentContext: contentContext, progress: progress, completion: completion) } - + public func makeDeviceContactInfoController(context: ShareControllerAccountContext, environment: ShareControllerEnvironment, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController { return deviceContactInfoController(context: context, environment: environment, subject: subject, completed: completed, cancelled: cancelled) } - - public func makePeersNearbyController(context: AccountContext) -> ViewController { - return peersNearbyController(context: context) - } - + public func makeChatController(context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, botStart: ChatControllerInitialBotStart?, mode: ChatControllerPresentationMode, params: ChatControllerParams?) -> ChatController { return ChatControllerImpl(context: context, chatLocation: chatLocation, subject: subject, botStart: botStart, mode: mode, params: params) } @@ -3719,16 +3714,21 @@ public final class SharedAccountContextImpl: SharedAccountContext { return controller } - public func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], actionTitle: String?, isEditing: Bool, expandIfNeeded: Bool, parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView?, CGRect?) -> Bool)?, actionPerformed: ((Bool) -> Void)?) -> ViewController { + public func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], actionTitle: String?, isEditing: Bool, expandIfNeeded: Bool, parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView?, CGRect?) -> Bool)?, actionPerformed: (([StickerPackScreenActionResult]) -> Void)?) -> ViewController { return StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: mainStickerPack, stickerPacks: stickerPacks, loadedStickerPacks: loadedStickerPacks, actionTitle: actionTitle, isEditing: isEditing, expandIfNeeded: expandIfNeeded, parentNavigationController: parentNavigationController, sendSticker: sendSticker, actionPerformed: { actions in - if let (_, _, action) = actions.first { + guard let actionPerformed = actionPerformed else { + return + } + actionPerformed(actions.map { info, items, action in + let mappedAction: StickerPackScreenActionKind switch action { case .add: - actionPerformed?(true) - case .remove: - actionPerformed?(false) + mappedAction = .add + case let .remove(positionInList): + mappedAction = .remove(positionInList: positionInList) } - } + return StickerPackScreenActionResult(info: info, items: items, action: mappedAction) + }) }) } diff --git a/submodules/WebUI/Sources/WebAppSecureStorageTransferScreen.swift b/submodules/WebUI/Sources/WebAppSecureStorageTransferScreen.swift index 4b71d9be38..2d90cb02a1 100644 --- a/submodules/WebUI/Sources/WebAppSecureStorageTransferScreen.swift +++ b/submodules/WebUI/Sources/WebAppSecureStorageTransferScreen.swift @@ -17,6 +17,7 @@ import ListSectionComponent import ListActionItemComponent import AccountContext import AvatarNode +import GlassBarButtonComponent private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -63,8 +64,7 @@ private final class SheetContent: CombinedComponent { } static var body: Body { - let closeButton = Child(Button.self) - + let closeButton = Child(GlassBarButtonComponent.self) let title = Child(MultilineTextComponent.self) let avatar = Child(AvatarComponent.self) let text = Child(MultilineTextComponent.self) @@ -87,22 +87,31 @@ private final class SheetContent: CombinedComponent { let boldTextFont = Font.semibold(13.0) let textColor = theme.actionSheet.primaryTextColor - var contentSize = CGSize(width: context.availableSize.width, height: 18.0) + var contentSize = CGSize(width: context.availableSize.width, height: 38.0) let closeButton = closeButton.update( - component: Button( - content: AnyComponent(Text(text: strings.Common_Cancel, font: Font.regular(17.0), color: theme.actionSheet.controlAccentColor)), - action: { [weak component] in - component?.dismiss() + component: GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + component.dismiss() } ), - availableSize: CGSize(width: 100.0, height: 30.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton - .position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 28.0)) + .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) ) - + let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString(string: strings.WebApp_ImportData_Title, font: titleFont, textColor: textColor)), @@ -114,10 +123,10 @@ private final class SheetContent: CombinedComponent { transition: .immediate ) context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height)) ) contentSize.height += title.size.height - contentSize.height += 24.0 + contentSize.height += 16.0 let avatar = avatar.update( component: AvatarComponent( @@ -185,6 +194,7 @@ private final class SheetContent: CombinedComponent { ) items.append(AnyComponentWithIdentity(id: key.uuid, component: AnyComponent(ListActionItemComponent( theme: theme, + style: .glass, title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 3.0)), contentInsets: UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0), leftIcon: .check(ListActionItemComponent.LeftIcon.Check(isSelected: key.uuid == state.selectedUuid, isEnabled: true, toggle: nil)), @@ -201,6 +211,7 @@ private final class SheetContent: CombinedComponent { let keys = keys.update( component: ListSectionComponent( theme: environment.theme, + style: .glass, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: strings.WebApp_ImportData_AccountHeader.uppercased(), @@ -221,9 +232,11 @@ private final class SheetContent: CombinedComponent { contentSize.height += keys.size.height contentSize.height += 24.0 + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let button = button.update( component: ButtonComponent( background: ButtonComponent.Background( + style: .glass, color: theme.list.itemCheckColors.fillColor, foreground: theme.list.itemCheckColors.foregroundColor, pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) @@ -247,7 +260,7 @@ private final class SheetContent: CombinedComponent { } } ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), transition: context.transition ) context.add(button @@ -255,11 +268,8 @@ private final class SheetContent: CombinedComponent { .cornerRadius(10.0) ) contentSize.height += button.size.height - contentSize.height += 7.0 - - let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom - contentSize.height += 5.0 + effectiveBottomInset - + contentSize.height += buttonInsets.bottom + return contentSize } } @@ -325,6 +335,7 @@ private final class SheetContainerComponent: CombinedComponent { }) } )), + style: .glass, backgroundColor: .color(theme.list.blocksBackgroundColor), followContentSizeChanges: true, externalState: sheetExternalState,